[OTLP] Export LogRecord.CategoryName as InstrumentationScope name (#4941)

This commit is contained in:
Vishwesh Bankwar 2023-10-16 13:17:38 -07:00 committed by GitHub
parent 18c85fa403
commit 037cda86d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 4 deletions

View File

@ -19,6 +19,12 @@ attributes will be exported when
variable will be set to `true`.
([#4892](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4892))
* `LogRecord.CategoryName` will now be exported as
[InstrumentationScope](https://github.com/open-telemetry/opentelemetry-dotnet/blob/3c2bb7c93dd2e697636479a1882f49bb0c4a362e/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto#L71-L81)
`name` field under
[ScopeLogs](https://github.com/open-telemetry/opentelemetry-dotnet/blob/3c2bb7c93dd2e697636479a1882f49bb0c4a362e/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto#L64-L75).
([#4941](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4941))
## 1.6.0
Released 2023-Sep-05

View File

@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Google.Protobuf;
using OpenTelemetry.Internal;
@ -28,6 +29,8 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
internal sealed class OtlpLogRecordTransformer
{
internal static readonly ConcurrentBag<OtlpLogs.ScopeLogs> LogListPool = new();
private readonly SdkLimitOptions sdkLimitOptions;
private readonly ExperimentalOptions experimentalOptions;
@ -41,6 +44,9 @@ internal sealed class OtlpLogRecordTransformer
OtlpResource.Resource processResource,
in Batch<LogRecord> logRecordBatch)
{
// TODO: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4943
Dictionary<string, OtlpLogs.ScopeLogs> logsByCategory = new Dictionary<string, OtlpLogs.ScopeLogs>();
var request = new OtlpCollector.ExportLogsServiceRequest();
var resourceLogs = new OtlpLogs.ResourceLogs
@ -49,14 +55,18 @@ internal sealed class OtlpLogRecordTransformer
};
request.ResourceLogs.Add(resourceLogs);
var scopeLogs = new OtlpLogs.ScopeLogs();
resourceLogs.ScopeLogs.Add(scopeLogs);
foreach (var logRecord in logRecordBatch)
{
var otlpLogRecord = this.ToOtlpLog(logRecord);
if (otlpLogRecord != null)
{
if (!logsByCategory.TryGetValue(logRecord.CategoryName, out var scopeLogs))
{
scopeLogs = this.GetLogListFromPool(logRecord.CategoryName);
logsByCategory.Add(logRecord.CategoryName, scopeLogs);
resourceLogs.ScopeLogs.Add(scopeLogs);
}
scopeLogs.LogRecords.Add(otlpLogRecord);
}
}
@ -64,6 +74,45 @@ internal sealed class OtlpLogRecordTransformer
return request;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Return(OtlpCollector.ExportLogsServiceRequest request)
{
var resourceLogs = request.ResourceLogs.FirstOrDefault();
if (resourceLogs == null)
{
return;
}
foreach (var scope in resourceLogs.ScopeLogs)
{
scope.LogRecords.Clear();
LogListPool.Add(scope);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal OtlpLogs.ScopeLogs GetLogListFromPool(string name)
{
if (!LogListPool.TryTake(out var logs))
{
logs = new OtlpLogs.ScopeLogs
{
Scope = new OtlpCommon.InstrumentationScope
{
Name = name, // Name is enforced to not be null, but it can be empty.
Version = string.Empty, // proto requires this to be non-null.
},
};
}
else
{
logs.Scope.Name = name;
logs.Scope.Version = string.Empty;
}
return logs;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal OtlpLogs.LogRecord ToOtlpLog(LogRecord logRecord)
{

View File

@ -90,9 +90,11 @@ internal sealed class OtlpLogExporter : BaseExporter<LogRecord>
// Prevents the exporter's gRPC and HTTP operations from being instrumented.
using var scope = SuppressInstrumentationScope.Begin();
OtlpCollector.ExportLogsServiceRequest request = null;
try
{
var request = this.otlpLogRecordTransformer.BuildExportRequest(this.ProcessResource, logRecordBatch);
request = this.otlpLogRecordTransformer.BuildExportRequest(this.ProcessResource, logRecordBatch);
if (!this.exportClient.SendExportRequest(request))
{
@ -104,6 +106,13 @@ internal sealed class OtlpLogExporter : BaseExporter<LogRecord>
OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
return ExportResult.Failure;
}
finally
{
if (request != null)
{
this.otlpLogRecordTransformer.Return(request);
}
}
return ExportResult.Success;
}

View File

@ -26,6 +26,7 @@ using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
@ -1242,6 +1243,64 @@ public class OtlpLogExporterTests : Http2UnencryptedSupportTests
}
}
[Fact]
public void ValidateInstrumentationScope()
{
var logRecords = new List<LogRecord>();
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddOpenTelemetry(options => options
.AddInMemoryExporter(logRecords));
});
var logger1 = loggerFactory.CreateLogger("OtlpLogExporterTests-A");
logger1.LogInformation("Hello from red-tomato");
var logger2 = loggerFactory.CreateLogger("OtlpLogExporterTests-B");
logger2.LogInformation("Hello from green-tomato");
Assert.Equal(2, logRecords.Count);
var batch = new Batch<LogRecord>(logRecords.ToArray(), logRecords.Count);
var logRecordTransformer = new OtlpLogRecordTransformer(new(), new());
var resourceBuilder = ResourceBuilder.CreateEmpty();
var processResource = resourceBuilder.Build().ToOtlpResource();
var request = logRecordTransformer.BuildExportRequest(processResource, batch);
Assert.Single(request.ResourceLogs);
var scope1 = request.ResourceLogs[0].ScopeLogs.First();
var scope2 = request.ResourceLogs[0].ScopeLogs.Last();
Assert.Equal("OtlpLogExporterTests-A", scope1.Scope.Name);
Assert.Equal("OtlpLogExporterTests-B", scope2.Scope.Name);
Assert.Single(scope1.LogRecords);
Assert.Single(scope2.LogRecords);
var logrecord1 = scope1.LogRecords[0];
var logrecord2 = scope2.LogRecords[0];
Assert.Equal("Hello from red-tomato", logrecord1.Body.StringValue);
Assert.Equal("Hello from green-tomato", logrecord2.Body.StringValue);
// Validate LogListPool
Assert.Empty(OtlpLogRecordTransformer.LogListPool);
logRecordTransformer.Return(request);
Assert.Equal(2, OtlpLogRecordTransformer.LogListPool.Count);
request = logRecordTransformer.BuildExportRequest(processResource, batch);
Assert.Single(request.ResourceLogs);
// ScopeLogs will be reused.
Assert.Empty(OtlpLogRecordTransformer.LogListPool);
}
private static OtlpCommon.KeyValue TryGetAttribute(OtlpLogs.LogRecord record, string key)
{
return record.Attributes.FirstOrDefault(att => att.Key == key);