From 1ddc2da7ebbab660252d178452be4268046f92d7 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 18 Oct 2023 18:55:23 +1000 Subject: [PATCH 1/4] Use SourceContext to populate InstrumentationScope.Name --- example/Example/Program.cs | 10 ++-- .../Sinks/OpenTelemetry/IncludedData.cs | 8 ++- .../Sinks/OpenTelemetry/LogRecordBuilder.cs | 23 ++++++--- .../Sinks/OpenTelemetry/OpenTelemetrySink.cs | 51 ++++++++++++++----- .../ProtocolHelpers/PackageIdentity.cs | 11 +--- .../ProtocolHelpers/RequestTemplateFactory.cs | 36 +++---------- .../LogRecordBuilderTests.cs | 18 +++---- .../OpenTelemetryUtilsTests.cs | 25 ++++----- .../PublicApiVisibilityTests.approved.txt | 1 + 9 files changed, 95 insertions(+), 88 deletions(-) diff --git a/example/Example/Program.cs b/example/Example/Program.cs index ecad78a..6cf317d 100644 --- a/example/Example/Program.cs +++ b/example/Example/Program.cs @@ -14,8 +14,6 @@ // ReSharper disable ExplicitCallerInfoArgument -#nullable enable - using Serilog; using System.Diagnostics; using Serilog.Sinks.OpenTelemetry; @@ -30,11 +28,9 @@ static void Main() { // create an ActivitySource (that is listened to) for creating an Activity // to test the trace and span ID enricher - using var listener = new ActivityListener - { - ShouldListenTo = _ => true, - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - }; + using var listener = new ActivityListener(); + listener.ShouldListenTo = _ => true; + listener.Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData; ActivitySource.AddActivityListener(listener); diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs index 113243f..ad9230b 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs @@ -76,5 +76,11 @@ public enum IncludedData /// /// Include pre-rendered values for any message template placeholders that use custom format specifiers, in message_template.renderings. /// - MessageTemplateRenderingsAttribute = 64 + MessageTemplateRenderingsAttribute = 64, + + /// + /// Preserve the value of the SourceContext property, in addition to using it as the OTLP InstrumentationScope name. If + /// not specified, the SourceContext property will be omitted from the individual log record attributes. + /// + SourceContextAttribute = 128 } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/LogRecordBuilder.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/LogRecordBuilder.cs index 52b0e4f..7e7eac8 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/LogRecordBuilder.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/LogRecordBuilder.cs @@ -17,6 +17,7 @@ using System.Globalization; using OpenTelemetry.Proto.Common.V1; using OpenTelemetry.Proto.Logs.V1; +using Serilog.Core; using Serilog.Events; using Serilog.Parsing; using Serilog.Sinks.OpenTelemetry.Formatting; @@ -26,18 +27,18 @@ namespace Serilog.Sinks.OpenTelemetry; static class LogRecordBuilder { - public static LogRecord ToLogRecord(LogEvent logEvent, IFormatProvider? formatProvider, IncludedData includedFields) + public static (LogRecord logRecord, string? scopeName) ToLogRecord(LogEvent logEvent, IFormatProvider? formatProvider, IncludedData includedData) { var logRecord = new LogRecord(); - ProcessProperties(logRecord, logEvent); + ProcessProperties(logRecord, logEvent, includedData, out var scopeName); ProcessTimestamp(logRecord, logEvent); - ProcessMessage(logRecord, logEvent, includedFields, formatProvider); + ProcessMessage(logRecord, logEvent, includedData, formatProvider); ProcessLevel(logRecord, logEvent); ProcessException(logRecord, logEvent); - ProcessIncludedFields(logRecord, logEvent, includedFields); + ProcessIncludedFields(logRecord, logEvent, includedData); - return logRecord; + return (logRecord, scopeName); } public static void ProcessMessage(LogRecord logRecord, LogEvent logEvent, IncludedData includedFields, IFormatProvider? formatProvider) @@ -70,10 +71,20 @@ public static void ProcessLevel(LogRecord logRecord, LogEvent logEvent) logRecord.SeverityNumber = PrimitiveConversions.ToSeverityNumber(level); } - public static void ProcessProperties(LogRecord logRecord, LogEvent logEvent) + public static void ProcessProperties(LogRecord logRecord, LogEvent logEvent, IncludedData includedData, out string? scopeName) { + scopeName = null; foreach (var property in logEvent.Properties) { + if (property is { Key: Constants.SourceContextPropertyName, Value: ScalarValue { Value: string sourceContext } }) + { + scopeName = sourceContext; + if ((includedData & IncludedData.SourceContextAttribute) != IncludedData.SourceContextAttribute) + { + continue; + } + } + var v = PrimitiveConversions.ToOpenTelemetryAnyValue(property.Value); logRecord.Attributes.Add(PrimitiveConversions.NewAttribute(property.Key, v)); } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs index cb8ea9a..2328bbd 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs @@ -13,6 +13,7 @@ // limitations under the License. using OpenTelemetry.Proto.Collector.Logs.V1; +using OpenTelemetry.Proto.Logs.V1; using Serilog.Core; using Serilog.Events; using Serilog.Sinks.OpenTelemetry.ProtocolHelpers; @@ -23,7 +24,7 @@ namespace Serilog.Sinks.OpenTelemetry; class OpenTelemetrySink : IBatchedLogEventSink, ILogEventSink, IDisposable { readonly IFormatProvider? _formatProvider; - readonly ExportLogsServiceRequest _requestTemplate; + readonly ResourceLogs _resourceLogsTemplate; readonly IExporter _exporter; readonly IncludedData _includedData; @@ -51,7 +52,7 @@ public OpenTelemetrySink( resourceAttributes = RequiredResourceAttributes.AddDefaults(resourceAttributes); } - _requestTemplate = RequestTemplateFactory.CreateRequestTemplate(resourceAttributes); + _resourceLogsTemplate = RequestTemplateFactory.CreateResourceLogs(resourceAttributes); } /// @@ -62,25 +63,46 @@ public void Dispose() (_exporter as IDisposable)?.Dispose(); } - void AddLogEventToRequest(LogEvent logEvent, ExportLogsServiceRequest request) - { - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, _formatProvider, _includedData); - request.ResourceLogs[0].ScopeLogs[0].LogRecords.Add(logRecord); - } - /// /// Transforms and sends the given batch of LogEvent objects /// to an OTLP endpoint. /// public Task EmitBatchAsync(IEnumerable batch) { - var request = _requestTemplate.Clone(); + var resourceLogs = _resourceLogsTemplate.Clone(); + + var anonymousScope = (ScopeLogs?)null; + var namedScopes = (Dictionary?)null; foreach (var logEvent in batch) { - AddLogEventToRequest(logEvent, request); + var (logRecord, scopeName) = LogRecordBuilder.ToLogRecord(logEvent, _formatProvider, _includedData); + if (scopeName == null) + { + if (anonymousScope == null) + { + anonymousScope = RequestTemplateFactory.CreateScopeLogs(null); + resourceLogs.ScopeLogs.Add(anonymousScope); + } + + anonymousScope.LogRecords.Add(logRecord); + } + else + { + namedScopes ??= new Dictionary(); + if (!namedScopes.TryGetValue(scopeName, out var namedScope)) + { + namedScope = RequestTemplateFactory.CreateScopeLogs(scopeName); + resourceLogs.ScopeLogs.Add(namedScope); + } + + namedScope.LogRecords.Add(logRecord); + } } + var request = new ExportLogsServiceRequest(); + request.ResourceLogs.Add(resourceLogs); + return _exporter.ExportAsync(request); } @@ -90,8 +112,13 @@ public Task EmitBatchAsync(IEnumerable batch) /// public void Emit(LogEvent logEvent) { - var request = _requestTemplate.Clone(); - AddLogEventToRequest(logEvent, request); + var (logRecord, scopeName) = LogRecordBuilder.ToLogRecord(logEvent, _formatProvider, _includedData); + var scopeLogs = RequestTemplateFactory.CreateScopeLogs(scopeName); + scopeLogs.LogRecords.Add(logRecord); + var resourceLogs = _resourceLogsTemplate.Clone(); + resourceLogs.ScopeLogs.Add(scopeLogs); + var request = new ExportLogsServiceRequest(); + request.ResourceLogs.Add(resourceLogs); _exporter.Export(request); } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PackageIdentity.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PackageIdentity.cs index 8a87ee1..55ff732 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PackageIdentity.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PackageIdentity.cs @@ -18,21 +18,12 @@ namespace Serilog.Sinks.OpenTelemetry.ProtocolHelpers; static class PackageIdentity { - public static string GetInstrumentationScopeName() - { - return typeof(RequestTemplateFactory).Assembly.GetName().Name - // Best we know about this, if it occurs. - ?? throw new InvalidOperationException("Sink assembly name could not be retrieved."); - } - - public static string GetInstrumentationScopeVersion() + public static string GetTelemetrySdkVersion() { return typeof(RequestTemplateFactory).Assembly.GetCustomAttribute()!.InformationalVersion; } public const string TelemetrySdkName = "serilog"; - public static string GetTelemetrySdkVersion() => GetInstrumentationScopeVersion(); - public const string TelemetrySdkLanguage = "dotnet"; } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs index 22afca4..5e67e55 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs @@ -25,29 +25,20 @@ static class RequestTemplateFactory { const string OpenTelemetrySchemaUrl = "https://opentelemetry.io/schemas/v1.13.0"; - public static ExportLogsServiceRequest CreateRequestTemplate(IReadOnlyDictionary resourceAttributes) + public static ScopeLogs CreateScopeLogs(string? scopeName) { - var resourceLogs = CreateResourceLogs(resourceAttributes); - resourceLogs.ScopeLogs.Add(CreateEmptyScopeLogs()); - - var request = new ExportLogsServiceRequest(); - request.ResourceLogs.Add(resourceLogs); + var scope = scopeName != null ? new InstrumentationScope + { + Name = scopeName, + } : null; - return request; - } - - static InstrumentationScope CreateInstrumentationScope() - { - var scope = new InstrumentationScope + return new ScopeLogs { - Name = PackageIdentity.GetInstrumentationScopeName(), - Version = PackageIdentity.GetInstrumentationScopeVersion() + Scope = scope }; - - return scope; } - static ResourceLogs CreateResourceLogs(IReadOnlyDictionary resourceAttributes) + public static ResourceLogs CreateResourceLogs(IReadOnlyDictionary resourceAttributes) { var resourceLogs = new ResourceLogs(); @@ -61,17 +52,6 @@ static ResourceLogs CreateResourceLogs(IReadOnlyDictionary resou return resourceLogs; } - static ScopeLogs CreateEmptyScopeLogs() - { - var scopeLogs = new ScopeLogs - { - Scope = CreateInstrumentationScope(), - SchemaUrl = OpenTelemetrySchemaUrl - }; - - return scopeLogs; - } - static RepeatedField ToResourceAttributes(IReadOnlyDictionary resourceAttributes) { var attributes = new RepeatedField(); diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs index 0ff2130..d01edce 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs @@ -65,7 +65,7 @@ public void TestProcessProperties() logEvent.AddOrUpdateProperty(prop); - LogRecordBuilder.ProcessProperties(logRecord, logEvent); + LogRecordBuilder.ProcessProperties(logRecord, logEvent, IncludedData.None, out _); Assert.Contains(propertyKeyValue, logRecord.Attributes); } @@ -122,7 +122,7 @@ public void IncludeMessageTemplateMD5Hash() { var logEvent = Some.SerilogEvent(messageTemplate: Some.TestMessageTemplate); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateMD5HashAttribute); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateMD5HashAttribute); var expectedHash = PrimitiveConversions.Md5Hash(Some.TestMessageTemplate); var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateMD5Hash, Value = new() { StringValue = expectedHash }}; @@ -137,7 +137,7 @@ public void IncludeMessageTemplateText() var logEvent = Some.SerilogEvent(messageTemplate, properties); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateTextAttribute); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateTextAttribute); var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateText, Value = new() { StringValue = messageTemplate } }; Assert.Contains(expectedAttribute, logRecord.Attributes); @@ -149,7 +149,7 @@ public void IncludeTraceIdWhenActivityIsNull() Assert.Null(Activity.Current); var logEvent = Some.DefaultSerilogEvent(); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField); Assert.True(logRecord.TraceId.IsEmpty); Assert.True(logRecord.SpanId.IsEmpty); @@ -170,7 +170,7 @@ public void IncludeTraceIdAndSpanId() var logEvent = CollectingSink.CollectSingle(log => log.Information("Hello, trace and span!")); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField); Assert.Equal(logRecord.TraceId, PrimitiveConversions.ToOpenTelemetryTraceId(Activity.Current.TraceId.ToHexString())); Assert.Equal(logRecord.SpanId, PrimitiveConversions.ToOpenTelemetrySpanId(Activity.Current.SpanId.ToHexString())); @@ -182,7 +182,7 @@ public void TemplateBodyIncludesMessageTemplateInBody() const string messageTemplate = "Hello, {Name}"; var properties = new List { new("Name", new ScalarValue("World")) }; - var logRecord = LogRecordBuilder.ToLogRecord(Some.SerilogEvent(messageTemplate, properties), null, IncludedData.TemplateBody); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(Some.SerilogEvent(messageTemplate, properties), null, IncludedData.TemplateBody); Assert.NotNull(logRecord.Body); Assert.Equal(messageTemplate, logRecord.Body.StringValue); } @@ -192,7 +192,7 @@ public void NoRenderingsIncludedWhenNoneInTemplate() { var logEvent = Some.SerilogEvent(messageTemplate: "Hello, {Name}", properties: new [] { new LogEventProperty("Name", new ScalarValue("World"))}); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute); Assert.DoesNotContain(SemanticConventions.AttributeMessageTemplateRenderings, logRecord.Attributes.Select(a => a.Key)); } @@ -202,7 +202,7 @@ public void RenderingsIncludedWhenPresentInTemplate() { var logEvent = CollectingSink.CollectSingle(log => log.Information("{First:0} {Second} {Third:0.00}", 123.456, 234.567, 345.678)); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute); var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateRenderings, Value = new() { @@ -223,7 +223,7 @@ public void RenderingsNotIncludedWhenIncludedDataDoesNotSpecifyThem() { var logEvent = CollectingSink.CollectSingle(log => log.Information("{First:0}", 123.456)); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData); + var (logRecord, _) = LogRecordBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData); Assert.DoesNotContain(SemanticConventions.AttributeMessageTemplateRenderings, logRecord.Attributes.Select(a => a.Key)); } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs index 17e9068..1839e50 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using OpenTelemetry.Proto.Logs.V1; using Serilog.Sinks.OpenTelemetry.ProtocolHelpers; -using Serilog.Sinks.OpenTelemetry.Tests.Support; using Xunit; namespace Serilog.Sinks.OpenTelemetry.Tests; @@ -21,28 +21,23 @@ namespace Serilog.Sinks.OpenTelemetry.Tests; public class RequestTemplateFactoryTests { [Fact] - // Ensure that logs are not carried over from one clone of the - // request template to another. - public void TestNoDuplicateLogs() + public void ResourceLogsAreClonedDeeply() { - var logEvent = Some.DefaultSerilogEvent(); - var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.None); + var template = RequestTemplateFactory.CreateResourceLogs(new Dictionary()); - var requestTemplate = RequestTemplateFactory.CreateRequestTemplate(new Dictionary()); + var request = template.Clone(); - var request = requestTemplate.Clone(); - - var n = request.ResourceLogs.ElementAt(0).ScopeLogs.ElementAt(0).LogRecords.Count; + var n = request.ScopeLogs.Count; Assert.Equal(0, n); - request.ResourceLogs[0].ScopeLogs[0].LogRecords.Add(logRecord); + request.ScopeLogs.Add(new ScopeLogs()); - n = request.ResourceLogs.ElementAt(0).ScopeLogs.ElementAt(0).LogRecords.Count; + n = request.ScopeLogs.Count; Assert.Equal(1, n); + + request = template.Clone(); - request = requestTemplate.Clone(); - n = request.ResourceLogs.ElementAt(0).ScopeLogs.ElementAt(0).LogRecords.Count; - + n = request.ScopeLogs.Count; Assert.Equal(0, n); } } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt b/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt index 56be0e4..ef513e8 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt @@ -26,6 +26,7 @@ namespace Serilog.Sinks.OpenTelemetry SpecRequiredResourceAttributes = 16, TemplateBody = 32, MessageTemplateRenderingsAttribute = 64, + SourceContextAttribute = 128, } public class OpenTelemetrySinkOptions { From b85a910826f62a344833d4f51b3412f17f05c9fe Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 23 Oct 2023 16:13:01 +1000 Subject: [PATCH 2/4] Source context name extraction tests --- .../ProtocolHelpers/RequestTemplateFactory.cs | 2 -- .../LogRecordBuilderTests.cs | 26 +++++++++++++++++++ ...ests.cs => RequestTemplateFactoryTests.cs} | 12 ++++----- 3 files changed, 32 insertions(+), 8 deletions(-) rename test/Serilog.Sinks.OpenTelemetry.Tests/{OpenTelemetryUtilsTests.cs => RequestTemplateFactoryTests.cs} (81%) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs index 5e67e55..84183b9 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Reflection; using Google.Protobuf.Collections; -using OpenTelemetry.Proto.Collector.Logs.V1; using OpenTelemetry.Proto.Common.V1; using OpenTelemetry.Proto.Logs.V1; using OpenTelemetry.Proto.Resource.V1; diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs index d01edce..fb7c674 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs @@ -16,6 +16,7 @@ using OpenTelemetry.Proto.Common.V1; using OpenTelemetry.Proto.Logs.V1; using OpenTelemetry.Trace; +using Serilog.Core; using Serilog.Events; using Serilog.Sinks.OpenTelemetry.ProtocolHelpers; using Serilog.Sinks.OpenTelemetry.Tests.Support; @@ -227,4 +228,29 @@ public void RenderingsNotIncludedWhenIncludedDataDoesNotSpecifyThem() Assert.DoesNotContain(SemanticConventions.AttributeMessageTemplateRenderings, logRecord.Attributes.Select(a => a.Key)); } + + [Fact] + public void SourceContextIsInstrumentationScope() + { + var contextType = typeof(LogRecordBuilderTests); + var logEvent = CollectingSink.CollectSingle(log => log.ForContext(contextType).Information("Hello, world!")); + + var (logRecord, scopeName) = LogRecordBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData); + + Assert.Equal(contextType.FullName, scopeName); + Assert.DoesNotContain(Constants.SourceContextPropertyName, logRecord.Attributes.Select(a => a.Key)); + } + + [Fact] + public void SourceContextCanBePreservedAsAttribute() + { + var contextType = typeof(LogRecordBuilderTests); + var logEvent = CollectingSink.CollectSingle(log => log.ForContext(contextType).Information("Hello, world!")); + + var (logRecord, scopeName) = LogRecordBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData | IncludedData.SourceContextAttribute); + + Assert.Equal(contextType.FullName, scopeName); + var ctx = Assert.Single(logRecord.Attributes.Where(a => a.Key == Constants.SourceContextPropertyName)); + Assert.Equal(contextType.FullName, ctx.Value.StringValue); + } } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/RequestTemplateFactoryTests.cs similarity index 81% rename from test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs rename to test/Serilog.Sinks.OpenTelemetry.Tests/RequestTemplateFactoryTests.cs index 1839e50..2c567e2 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryUtilsTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/RequestTemplateFactoryTests.cs @@ -25,19 +25,19 @@ public void ResourceLogsAreClonedDeeply() { var template = RequestTemplateFactory.CreateResourceLogs(new Dictionary()); - var request = template.Clone(); + var clone = template.Clone(); - var n = request.ScopeLogs.Count; + var n = clone.ScopeLogs.Count; Assert.Equal(0, n); - request.ScopeLogs.Add(new ScopeLogs()); + clone.ScopeLogs.Add(new ScopeLogs()); - n = request.ScopeLogs.Count; + n = clone.ScopeLogs.Count; Assert.Equal(1, n); - request = template.Clone(); + clone = template.Clone(); - n = request.ScopeLogs.Count; + n = clone.ScopeLogs.Count; Assert.Equal(0, n); } } From 1628cfa67e99d58d6ab6bc1d27b2f998f1454751 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 23 Oct 2023 16:14:31 +1000 Subject: [PATCH 3/4] Major version bump --- .../Serilog.Sinks.OpenTelemetry.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj b/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj index 83b2a43..8a61b52 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj +++ b/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj @@ -2,7 +2,7 @@ This Serilog sink transforms Serilog events into OpenTelemetry logs and sends them to an OTLP (gRPC or HTTP) endpoint. - 1.2.0 + 2.0.0 Serilog Contributors net6.0;netstandard2.1;netstandard2.0;net462 serilog;sink;opentelemetry From 623ec8f5b8f4d6cfb05d5c260fce88e3951135ca Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 23 Oct 2023 16:48:01 +1000 Subject: [PATCH 4/4] More tests, fix a bug --- ...nTelemetryLoggerConfigurationExtensions.cs | 23 +++++--- .../Sinks/OpenTelemetry/Exporters/Exporter.cs | 32 ++++++++++ .../{ => Exporters}/GrpcExporter.cs | 2 +- .../{ => Exporters}/HttpExporter.cs | 2 +- .../Sinks/OpenTelemetry/OpenTelemetrySink.cs | 20 ++----- .../OpenTelemetrySinkTests.cs | 58 +++++++++++++++++++ .../Support/CollectingExporter.cs | 19 ++++++ .../Support/CollectingSink.cs | 15 ++++- 8 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/Exporter.cs rename src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/{ => Exporters}/GrpcExporter.cs (98%) rename src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/{ => Exporters}/HttpExporter.cs (98%) create mode 100644 test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetrySinkTests.cs create mode 100644 test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingExporter.cs diff --git a/src/Serilog.Sinks.OpenTelemetry/OpenTelemetryLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.OpenTelemetry/OpenTelemetryLoggerConfigurationExtensions.cs index a38c52a..35fe449 100644 --- a/src/Serilog.Sinks.OpenTelemetry/OpenTelemetryLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.OpenTelemetry/OpenTelemetryLoggerConfigurationExtensions.cs @@ -14,6 +14,7 @@ using Serilog.Configuration; using Serilog.Sinks.OpenTelemetry; +using Serilog.Sinks.OpenTelemetry.Exporters; using Serilog.Sinks.PeriodicBatching; namespace Serilog; @@ -39,15 +40,18 @@ public static LoggerConfiguration OpenTelemetry( var options = new BatchedOpenTelemetrySinkOptions(); configure(options); - var openTelemetrySink = new OpenTelemetrySink( + var exporter = Exporter.Create( endpoint: options.Endpoint, protocol: options.Protocol, - formatProvider: options.FormatProvider, - resourceAttributes: new Dictionary(options.ResourceAttributes), headers: new Dictionary(options.Headers), - includedData: options.IncludedData, httpMessageHandler: options.HttpMessageHandler); + var openTelemetrySink = new OpenTelemetrySink( + exporter: exporter, + formatProvider: options.FormatProvider, + resourceAttributes: new Dictionary(options.ResourceAttributes), + includedData: options.IncludedData); + var sink = new PeriodicBatchingSink(openTelemetrySink, options.BatchingOptions); return loggerSinkConfiguration.Sink(sink, options.RestrictedToMinimumLevel, options.LevelSwitch); @@ -97,15 +101,18 @@ public static LoggerConfiguration OpenTelemetry( configure(options); - var sink = new OpenTelemetrySink( + var exporter = Exporter.Create( endpoint: options.Endpoint, protocol: options.Protocol, - formatProvider: options.FormatProvider, - resourceAttributes: new Dictionary(options.ResourceAttributes), headers: new Dictionary(options.Headers), - includedData: options.IncludedData, httpMessageHandler: options.HttpMessageHandler); + var sink = new OpenTelemetrySink( + exporter: exporter, + formatProvider: options.FormatProvider, + resourceAttributes: new Dictionary(options.ResourceAttributes), + includedData: options.IncludedData); + return loggerAuditSinkConfiguration.Sink(sink, options.RestrictedToMinimumLevel, options.LevelSwitch); } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/Exporter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/Exporter.cs new file mode 100644 index 0000000..1e431cb --- /dev/null +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/Exporter.cs @@ -0,0 +1,32 @@ +// Copyright 2022 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog.Sinks.OpenTelemetry.Exporters; + +static class Exporter +{ + public static IExporter Create( + string endpoint, + OtlpProtocol protocol, + IReadOnlyDictionary headers, + HttpMessageHandler? httpMessageHandler) + { + return protocol switch + { + OtlpProtocol.HttpProtobuf => new HttpExporter(endpoint, headers, httpMessageHandler), + OtlpProtocol.Grpc => new GrpcExporter(endpoint, headers, httpMessageHandler), + _ => throw new NotSupportedException($"OTLP protocol {protocol} is unsupported.") + }; + } +} diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/GrpcExporter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/GrpcExporter.cs similarity index 98% rename from src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/GrpcExporter.cs rename to src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/GrpcExporter.cs index 999d37b..75b8162 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/GrpcExporter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/GrpcExporter.cs @@ -16,7 +16,7 @@ using Grpc.Net.Client; using OpenTelemetry.Proto.Collector.Logs.V1; -namespace Serilog.Sinks.OpenTelemetry; +namespace Serilog.Sinks.OpenTelemetry.Exporters; /// /// Implements an IExporter that sends OpenTelemetry Log requests diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/HttpExporter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/HttpExporter.cs similarity index 98% rename from src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/HttpExporter.cs rename to src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/HttpExporter.cs index e085491..fcd95e8 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/HttpExporter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Exporters/HttpExporter.cs @@ -15,7 +15,7 @@ using Google.Protobuf; using OpenTelemetry.Proto.Collector.Logs.V1; -namespace Serilog.Sinks.OpenTelemetry; +namespace Serilog.Sinks.OpenTelemetry.Exporters; /// /// Implements an IExporter that sends OpenTelemetry Log requests diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs index 2328bbd..87c2977 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySink.cs @@ -29,21 +29,12 @@ class OpenTelemetrySink : IBatchedLogEventSink, ILogEventSink, IDisposable readonly IncludedData _includedData; public OpenTelemetrySink( - string endpoint, - OtlpProtocol protocol, - IFormatProvider? formatProvider, - IReadOnlyDictionary resourceAttributes, - IReadOnlyDictionary headers, - IncludedData includedData, - HttpMessageHandler? httpMessageHandler) + IExporter exporter, + IFormatProvider? formatProvider, + IReadOnlyDictionary resourceAttributes, + IncludedData includedData) { - _exporter = protocol switch - { - OtlpProtocol.HttpProtobuf => new HttpExporter(endpoint, headers, httpMessageHandler), - OtlpProtocol.Grpc => new GrpcExporter(endpoint, headers, httpMessageHandler), - _ => throw new NotSupportedException($"OTLP protocol {protocol} is unsupported.") - }; - + _exporter = exporter; _formatProvider = formatProvider; _includedData = includedData; @@ -93,6 +84,7 @@ public Task EmitBatchAsync(IEnumerable batch) if (!namedScopes.TryGetValue(scopeName, out var namedScope)) { namedScope = RequestTemplateFactory.CreateScopeLogs(scopeName); + namedScopes.Add(scopeName, namedScope); resourceLogs.ScopeLogs.Add(namedScope); } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetrySinkTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetrySinkTests.cs new file mode 100644 index 0000000..04a52d3 --- /dev/null +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetrySinkTests.cs @@ -0,0 +1,58 @@ +using OpenTelemetry.Proto.Collector.Logs.V1; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.OpenTelemetry.Tests.Support; +using Xunit; + +namespace Serilog.Sinks.OpenTelemetry.Tests; + +public class OpenTelemetrySinkTests +{ + [Fact] + public async Task DefaultScopeIsNull() + { + var events = CollectingSink.Collect(log => log.Information("Hello, world!")); + var request = await ExportAsync(events); + var resourceLogs = Assert.Single(request.ResourceLogs); + var scopeLogs = Assert.Single(resourceLogs.ScopeLogs); + Assert.Null(scopeLogs.Scope); + } + + [Fact] + public async Task SourceContextNameIsInstrumentationScope() + { + var contextType = typeof(LogRecordBuilderTests); + var events = CollectingSink.Collect(log => log.ForContext(contextType).Information("Hello, world!")); + var request = await ExportAsync(events); + var resourceLogs = Assert.Single(request.ResourceLogs); + var scopeLogs = Assert.Single(resourceLogs.ScopeLogs); + Assert.Equal(contextType.FullName, scopeLogs.Scope.Name); + } + + [Fact] + public async Task ScopeLogsAreGrouped() + { + var events = CollectingSink.Collect(log => + { + log.ForContext(Constants.SourceContextPropertyName, "A").Information("Hello, world!"); + log.ForContext(Constants.SourceContextPropertyName, "B").Information("Hello, world!"); + log.ForContext(Constants.SourceContextPropertyName, "A").Information("Hello, world!"); + log.Information("Hello, world!"); + }); + var request = await ExportAsync(events); + var resourceLogs = Assert.Single(request.ResourceLogs); + Assert.Equal(3, resourceLogs.ScopeLogs.Count); + Assert.Equal(4, resourceLogs.ScopeLogs.SelectMany(s => s.LogRecords).Count()); + Assert.Equal(2, resourceLogs.ScopeLogs.Single(r => r.Scope?.Name == "A").LogRecords.Count); + Assert.Single(resourceLogs.ScopeLogs.Single(r => r.Scope?.Name == "B").LogRecords); + Assert.Single(resourceLogs.ScopeLogs.Single(r => r.Scope == null).LogRecords); + } + + static async Task ExportAsync(IEnumerable events) + { + var exporter = new CollectingExporter(); + var sink = new OpenTelemetrySink(exporter, null, new Dictionary(), OpenTelemetrySinkOptions.DefaultIncludedData); + await sink.EmitBatchAsync(events); + return Assert.Single(exporter.Requests); + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingExporter.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingExporter.cs new file mode 100644 index 0000000..9d7e1bb --- /dev/null +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingExporter.cs @@ -0,0 +1,19 @@ +using OpenTelemetry.Proto.Collector.Logs.V1; + +namespace Serilog.Sinks.OpenTelemetry.Tests.Support; + +class CollectingExporter: IExporter +{ + public List Requests { get; } = new(); + + public void Export(ExportLogsServiceRequest request) + { + Requests.Add(request); + } + + public Task ExportAsync(ExportLogsServiceRequest request) + { + Export(request); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingSink.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingSink.cs index 08d8e61..a2692bb 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingSink.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/Support/CollectingSink.cs @@ -14,10 +14,19 @@ public void Emit(LogEvent logEvent) } public static LogEvent CollectSingle(Action emitter) + { + return Assert.Single(Collect(emitter)); + } + + public static IReadOnlyList Collect(Action emitter) { var sink = new CollectingSink(); - var logger = new LoggerConfiguration().WriteTo.Sink(sink).CreateLogger(); - emitter(logger); - return Assert.Single(sink._emitted); + var collector = new LoggerConfiguration() + .WriteTo.Sink(sink) + .CreateLogger(); + + emitter(collector); + + return sink._emitted; } }