diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 90f64dd9c0a..09690b7182d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -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 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs index 3a7be7f04c8..73101b2f4e5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpLogRecordTransformer.cs @@ -14,6 +14,7 @@ // limitations under the License. // +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 LogListPool = new(); + private readonly SdkLimitOptions sdkLimitOptions; private readonly ExperimentalOptions experimentalOptions; @@ -41,6 +44,9 @@ internal OtlpCollector.ExportLogsServiceRequest BuildExportRequest( OtlpResource.Resource processResource, in Batch logRecordBatch) { + // TODO: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4943 + Dictionary logsByCategory = new Dictionary(); + var request = new OtlpCollector.ExportLogsServiceRequest(); var resourceLogs = new OtlpLogs.ResourceLogs @@ -49,14 +55,18 @@ internal OtlpCollector.ExportLogsServiceRequest BuildExportRequest( }; 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 OtlpCollector.ExportLogsServiceRequest BuildExportRequest( 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) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs index 715ec0de263..757691d1702 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -90,9 +90,11 @@ public override ExportResult Export(in Batch logRecordBatch) // 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 @@ public override ExportResult Export(in Batch logRecordBatch) OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); return ExportResult.Failure; } + finally + { + if (request != null) + { + this.otlpLogRecordTransformer.Return(request); + } + } return ExportResult.Success; } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs index 0c2e6c06f9b..00ba07d96a7 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs @@ -26,6 +26,7 @@ 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 void AddOtlpLogExporterLogRecordProcessorOptionsTest(ExportProcessorType } } + [Fact] + public void ValidateInstrumentationScope() + { + var logRecords = new List(); + 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(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);