From ab10e119ba055a746361e3368f5fdb911383837e Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 18 Jul 2022 09:38:02 -0700 Subject: [PATCH] [Logs] Serilog extensions project (#3438) * Add Serilog extensions project. * Improve provider disposal when using serilog. * Warning fixes. * Fix README. * Code review. * Move Serilog version to props. * Rename LogEmitter.Log -> Emit. * Added unit tests. * Warning cleanup. * Code review. * Standard README jazz. * Removed ApiCompat block. --- OpenTelemetry.sln | 19 ++ build/Common.props | 1 + .../Examples.LoggingExtensions.csproj | 15 + examples/LoggingExtensions/Program.cs | 47 ++++ examples/LoggingExtensions/README.md | 10 + .../netstandard2.0/PublicAPI.Shipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 3 + .../AssemblyInfo.cs | 35 +++ .../CHANGELOG.md | 5 + .../OpenTelemetry.Extensions.Serilog.csproj | 24 ++ .../OpenTelemetrySerilogExtensions.cs | 54 ++++ .../OpenTelemetrySerilogSink.cs | 135 +++++++++ .../README.md | 38 +++ src/OpenTelemetry/AssemblyInfo.cs | 1 + src/OpenTelemetry/Logs/LogEmitter.cs | 2 +- .../AssemblyInfo.cs | 19 ++ ...nTelemetry.Extensions.Serilog.Tests.csproj | 27 ++ .../OpenTelemetrySerilogSinkTests.cs | 260 ++++++++++++++++++ .../Logs/LogEmitterTests.cs | 8 +- 19 files changed, 699 insertions(+), 5 deletions(-) create mode 100644 examples/LoggingExtensions/Examples.LoggingExtensions.csproj create mode 100644 examples/LoggingExtensions/Program.cs create mode 100644 examples/LoggingExtensions/README.md create mode 100644 src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Shipped.txt create mode 100644 src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt create mode 100644 src/OpenTelemetry.Extensions.Serilog/AssemblyInfo.cs create mode 100644 src/OpenTelemetry.Extensions.Serilog/CHANGELOG.md create mode 100644 src/OpenTelemetry.Extensions.Serilog/OpenTelemetry.Extensions.Serilog.csproj create mode 100644 src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogExtensions.cs create mode 100644 src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogSink.cs create mode 100644 src/OpenTelemetry.Extensions.Serilog/README.md create mode 100644 test/OpenTelemetry.Extensions.Serilog.Tests/AssemblyInfo.cs create mode 100644 test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetry.Extensions.Serilog.Tests.csproj create mode 100644 test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 8e4c8bca6..d661f7604 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -229,6 +229,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "correlation", "docs\logs\co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress.Logs", "test\OpenTelemetry.Tests.Stress.Logs\OpenTelemetry.Tests.Stress.Logs.csproj", "{4298057B-24E0-47B3-BB76-C17E81AF6B39}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.LoggingExtensions", "examples\LoggingExtensions\Examples.LoggingExtensions.csproj", "{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Serilog", "src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj", "{0D85558E-15B9-4251-BDBD-9CB7933B57E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Serilog.Tests", "test\OpenTelemetry.Extensions.Serilog.Tests\OpenTelemetry.Extensions.Serilog.Tests.csproj", "{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -463,6 +469,18 @@ Global {4298057B-24E0-47B3-BB76-C17E81AF6B39}.Debug|Any CPU.Build.0 = Debug|Any CPU {4298057B-24E0-47B3-BB76-C17E81AF6B39}.Release|Any CPU.ActiveCfg = Release|Any CPU {4298057B-24E0-47B3-BB76-C17E81AF6B39}.Release|Any CPU.Build.0 = Release|Any CPU + {F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Release|Any CPU.Build.0 = Release|Any CPU + {0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Release|Any CPU.Build.0 = Release|Any CPU + {6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -500,6 +518,7 @@ Global {41B784AA-3301-4126-AF9F-1D59BD04B0BF} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} {6C7A1595-36D6-4229-BBB5-5A6B5791791D} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {9A07D215-90AC-4BAF-BCDB-73D74FD3A5C5} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} + {F5EFF065-7AF5-4D7D-8038-CC419ABD8777} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/build/Common.props b/build/Common.props index f9f7e5f15..510b4ab35 100644 --- a/build/Common.props +++ b/build/Common.props @@ -40,6 +40,7 @@ [1.0.0,2.0) [0.12.1,0.13) 1.3.0 + [2.8.0,3.0) [1.2.0-beta.354,2.0) 1.4.0 6.0.0 diff --git a/examples/LoggingExtensions/Examples.LoggingExtensions.csproj b/examples/LoggingExtensions/Examples.LoggingExtensions.csproj new file mode 100644 index 000000000..8e502ea56 --- /dev/null +++ b/examples/LoggingExtensions/Examples.LoggingExtensions.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/examples/LoggingExtensions/Program.cs b/examples/LoggingExtensions/Program.cs new file mode 100644 index 000000000..310e22533 --- /dev/null +++ b/examples/LoggingExtensions/Program.cs @@ -0,0 +1,47 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; +using Serilog; + +var resourceBuilder = ResourceBuilder.CreateDefault().AddService("Examples.LogEmitter"); + +// Note: It is important that OpenTelemetryLoggerProvider is disposed when the +// app is shutdown. In this example we allow Serilog to do that by calling CloseAndFlush. +var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => +{ + options.IncludeFormattedMessage = true; + options + .SetResourceBuilder(resourceBuilder) + .AddConsoleExporter(); +}); + +// Configure Serilog global logger +Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) // <- Register OpenTelemetry Serilog sink + .CreateLogger(); + +// Note: Serilog ForContext API is used to set "CategoryName" on log messages +ILogger programLogger = Log.Logger.ForContext(); + +programLogger.Information("Application started {Greeting} {Location}", "Hello", "World"); + +programLogger.Information("Message {Array}", new string[] { "value1", "value2" }); + +// Note: For Serilog this call flushes all logs and disposes +// OpenTelemetryLoggerProvider. +Log.CloseAndFlush(); diff --git a/examples/LoggingExtensions/README.md b/examples/LoggingExtensions/README.md new file mode 100644 index 000000000..adbb1df5b --- /dev/null +++ b/examples/LoggingExtensions/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry Logging Extensions Example + +This project contains examples of the `LogEmitter` API being used to extend +existing logging platforms to write into OpenTelemetry logs. + +* Serilog: Using OpenTelemetry.Extensions.Serilog + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..7dc5c5811 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..87a33a78e --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +Serilog.OpenTelemetrySerilogExtensions +static Serilog.OpenTelemetrySerilogExtensions.OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration! loggerConfiguration, OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, bool disposeProvider = true) -> Serilog.LoggerConfiguration! diff --git a/src/OpenTelemetry.Extensions.Serilog/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.Serilog/AssemblyInfo.cs new file mode 100644 index 000000000..a51a83d9d --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/AssemblyInfo.cs @@ -0,0 +1,35 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; +using System.Runtime.CompilerServices; + +[assembly: CLSCompliant(false)] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] + +#if SIGNED +internal static class AssemblyInfo +{ + public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898"; + public const string MoqPublicKey = ", PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"; +} +#else +internal static class AssemblyInfo +{ + public const string PublicKey = ""; + public const string MoqPublicKey = ""; +} +#endif diff --git a/src/OpenTelemetry.Extensions.Serilog/CHANGELOG.md b/src/OpenTelemetry.Extensions.Serilog/CHANGELOG.md new file mode 100644 index 000000000..63bfc986b --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +Initial release. diff --git a/src/OpenTelemetry.Extensions.Serilog/OpenTelemetry.Extensions.Serilog.csproj b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetry.Extensions.Serilog.csproj new file mode 100644 index 000000000..993c4cb48 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetry.Extensions.Serilog.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + Extensions to enable OpenTelemetry logging when using the Serilog library + $(PackageTags);serilog;logging + enable + AllEnabledByDefault + latest + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogExtensions.cs b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogExtensions.cs new file mode 100644 index 000000000..cf5262f00 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogExtensions.cs @@ -0,0 +1,54 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using Serilog.Configuration; + +namespace Serilog +{ + /// + /// Contains Serilog extension methods. + /// + public static class OpenTelemetrySerilogExtensions + { + /// + /// Adds a sink to Serilog which will + /// write to OpenTelemetry. + /// + /// . + /// . + /// Controls whether or not the supplied + /// will be disposed when + /// the logger is disposed. Default value: . + /// Supplied for chaining calls. + public static LoggerConfiguration OpenTelemetry( + this LoggerSinkConfiguration loggerConfiguration, + OpenTelemetryLoggerProvider openTelemetryLoggerProvider, + bool disposeProvider = true) + { + Guard.ThrowIfNull(loggerConfiguration); + Guard.ThrowIfNull(openTelemetryLoggerProvider); + +#pragma warning disable CA2000 // Dispose objects before losing scope + return loggerConfiguration.Sink(new OpenTelemetrySerilogSink(openTelemetryLoggerProvider, disposeProvider)); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + } +} diff --git a/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogSink.cs b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogSink.cs new file mode 100644 index 000000000..49e866532 --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogSink.cs @@ -0,0 +1,135 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using Serilog.Events; + +namespace OpenTelemetry.Logs +{ + internal sealed class OpenTelemetrySerilogSink : ILogEventSink, IDisposable + { + private readonly OpenTelemetryLoggerProvider openTelemetryLoggerProvider; + private readonly bool includeFormattedMessage; + private readonly LogEmitter logEmitter; + private readonly bool disposeProvider; + + public OpenTelemetrySerilogSink(OpenTelemetryLoggerProvider openTelemetryLoggerProvider, bool disposeProvider) + { + Debug.Assert(openTelemetryLoggerProvider != null, "openTelemetryLoggerProvider was null"); + + this.openTelemetryLoggerProvider = openTelemetryLoggerProvider!; + this.disposeProvider = disposeProvider; + + var logEmitter = this.openTelemetryLoggerProvider.CreateEmitter(); + Debug.Assert(logEmitter != null, "logEmitter was null"); + + this.logEmitter = logEmitter!; + + // TODO: This project can only access IncludeFormattedMessage + // because it can see SDK internals. At some point this is likely + // not to be the case. Need to figure out where to put + // IncludeFormattedMessage so that extensions can see it. Ideas: + // Make it public on OpenTelemetryLoggerProvider or expose it on + // LogEmitter instance. + this.includeFormattedMessage = this.openTelemetryLoggerProvider.IncludeFormattedMessage; + } + + public void Emit(LogEvent logEvent) + { + Debug.Assert(logEvent != null, "LogEvent was null."); + + LogRecordData data = new(Activity.Current) + { + Timestamp = logEvent!.Timestamp.UtcDateTime, + LogLevel = (LogLevel)(int)logEvent.Level, + Message = this.includeFormattedMessage ? logEvent.RenderMessage() : logEvent.MessageTemplate.Text, + Exception = logEvent.Exception, + }; + + LogRecordAttributeList attributes = default; + foreach (KeyValuePair property in logEvent.Properties) + { + // TODO: Serilog supports complex type logging. This is not yet + // supported in OpenTelemetry. + if (property.Key == Constants.SourceContextPropertyName + && property.Value is ScalarValue sourceContextValue) + { + data.CategoryName = sourceContextValue.Value as string; + } + else if (property.Value is ScalarValue scalarValue) + { + attributes.Add(property.Key, scalarValue.Value); + } + else if (property.Value is SequenceValue sequenceValue) + { + IReadOnlyList elements = sequenceValue.Elements; + if (elements.Count > 0) + { + // Note: The goal here is to build a typed array (eg + // int[]) if all the element types match otherwise + // fallback to object[] + + Type? elementType = null; + Array? values = null; + + for (int i = 0; i < elements.Count; i++) + { + if (elements[i] is ScalarValue value) + { + Type currentElementType = value.Value?.GetType() ?? typeof(object); + + if (values == null) + { + elementType = currentElementType; + values = Array.CreateInstance(elementType, elements.Count); + } + else if (!elementType!.IsAssignableFrom(currentElementType)) + { + // Array with mixed types detected + object[] newValues = new object[elements.Count]; + values.CopyTo(newValues, 0); + values = newValues; + elementType = typeof(object); + } + + values.SetValue(value.Value, i); + } + } + + if (values != null) + { + attributes.Add(property.Key, values); + } + } + } + } + + this.logEmitter.Emit(in data, in attributes); + } + + public void Dispose() + { + if (this.disposeProvider) + { + this.openTelemetryLoggerProvider.Dispose(); + } + } + } +} diff --git a/src/OpenTelemetry.Extensions.Serilog/README.md b/src/OpenTelemetry.Extensions.Serilog/README.md new file mode 100644 index 000000000..b702325cf --- /dev/null +++ b/src/OpenTelemetry.Extensions.Serilog/README.md @@ -0,0 +1,38 @@ +# OpenTelemetry.Extensions.Serilog + +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Extensions.Serilog.svg)](https://www.nuget.org/packages/OpenTelemetry.Extensions.Serilog) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Extensions.Serilog.svg)](https://www.nuget.org/packages/OpenTelemetry.Extensions.Serilog) + +This project contains a [Serilog](https://github.com/serilog/) +[sink](https://github.com/serilog/serilog/wiki/Configuration-Basics#sinks) for +writing log messages to OpenTelemetry. + +## Installation + +```shell +dotnet add package OpenTelemetry.Extensions.Serilog +``` + +## Usage Example + +```csharp +// Step 1: Configure OpenTelemetryLoggerProvider... +var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => +{ + options + .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyService")) + .AddConsoleExporter(); +}); + +// Step 2: Register OpenTelemetry sink with Serilog... +Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + +// Step 3: When application is shutdown flush all log messages and dispose provider... +Log.CloseAndFlush(); +``` + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs index 545b35a13..0a79a36df 100644 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ b/src/OpenTelemetry/AssemblyInfo.cs @@ -21,5 +21,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] +[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Serilog" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] [assembly: InternalsVisibleTo("Benchmarks" + AssemblyInfo.PublicKey)] diff --git a/src/OpenTelemetry/Logs/LogEmitter.cs b/src/OpenTelemetry/Logs/LogEmitter.cs index 30bbd916c..49e633604 100644 --- a/src/OpenTelemetry/Logs/LogEmitter.cs +++ b/src/OpenTelemetry/Logs/LogEmitter.cs @@ -42,7 +42,7 @@ namespace OpenTelemetry.Logs /// /// . /// . - public void Log(in LogRecordData data, in LogRecordAttributeList attributes = default) + public void Emit(in LogRecordData data, in LogRecordAttributeList attributes = default) { var provider = this.loggerProvider; var processor = provider.Processor; diff --git a/test/OpenTelemetry.Extensions.Serilog.Tests/AssemblyInfo.cs b/test/OpenTelemetry.Extensions.Serilog.Tests/AssemblyInfo.cs new file mode 100644 index 000000000..11bfd5a20 --- /dev/null +++ b/test/OpenTelemetry.Extensions.Serilog.Tests/AssemblyInfo.cs @@ -0,0 +1,19 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; + +[assembly: CLSCompliant(false)] diff --git a/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetry.Extensions.Serilog.Tests.csproj b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetry.Extensions.Serilog.Tests.csproj new file mode 100644 index 000000000..2a5954ade --- /dev/null +++ b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetry.Extensions.Serilog.Tests.csproj @@ -0,0 +1,27 @@ + + + Unit test project for OpenTelemetry Serilog extensions + + net6.0;netcoreapp3.1 + $(TargetFrameworks);net462 + enable + AllEnabledByDefault + latest + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs new file mode 100644 index 000000000..ba22ba1fa --- /dev/null +++ b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs @@ -0,0 +1,260 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using Serilog; +using Xunit; +using ILogger = Serilog.ILogger; + +namespace OpenTelemetry.Extensions.Serilog.Tests +{ + public class OpenTelemetrySerilogSinkTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SerilogDisposesProviderTests(bool dispose) + { + List exportedItems = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new WrappedOpenTelemetryLoggerProvider(options => + { + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: dispose) + .CreateLogger(); + + Log.CloseAndFlush(); + + Assert.Equal(dispose, openTelemetryLoggerProvider.Disposed); + + if (!dispose) + { + openTelemetryLoggerProvider.Dispose(); + } + + Assert.True(openTelemetryLoggerProvider.Disposed); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SerilogBasicLogTests(bool includeFormattedMessage) + { + List exportedItems = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => + { + options.IncludeFormattedMessage = includeFormattedMessage; + + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + + Log.Logger.Information("Hello {greeting}", "World"); + + Log.CloseAndFlush(); + + Assert.Single(exportedItems); + + LogRecord logRecord = exportedItems[0]; + + if (!includeFormattedMessage) + { + Assert.Equal("Hello {greeting}", logRecord.FormattedMessage); + } + else + { + Assert.Equal("Hello \"World\"", logRecord.FormattedMessage); + } + + Assert.NotEqual(DateTime.MinValue, logRecord.Timestamp); + Assert.Equal(DateTimeKind.Utc, logRecord.Timestamp.Kind); + Assert.Equal(LogLevel.Information, logRecord.LogLevel); + Assert.Null(logRecord.CategoryName); + + Assert.NotNull(logRecord.StateValues); + Assert.Single(logRecord.StateValues); + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "greeting" && (string?)kvp.Value == "World"); + } + + [Fact] + public void SerilogCategoryNameTest() + { + List exportedItems = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => + { + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + + // Note: Serilog ForContext API is used to set "CategoryName" on log messages + ILogger logger = Log.Logger.ForContext(); + + logger.Information("Hello {greeting}", "World"); + + Log.CloseAndFlush(); + + Assert.Single(exportedItems); + + LogRecord logRecord = exportedItems[0]; + + Assert.Equal("OpenTelemetry.Extensions.Serilog.Tests.OpenTelemetrySerilogSinkTests", logRecord.CategoryName); + } + + [Fact] + public void SerilogComplexMessageTemplateTest() + { + List exportedItems = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => + { + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + + ComplexType complexType = new(); + + Log.Logger.Information("Hello {greeting} {id} {@complexObj} {$complexStr}", "World", 18, complexType, complexType); + + Log.CloseAndFlush(); + + Assert.Single(exportedItems); + + LogRecord logRecord = exportedItems[0]; + + Assert.NotNull(logRecord.StateValues); + Assert.Equal(3, logRecord.StateValues!.Count); // Note: complexObj is currently not supported/ignored. + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "greeting" && (string?)kvp.Value == "World"); + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "id" && (int?)kvp.Value == 18); + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "complexStr" && (string?)kvp.Value == "ComplexTypeToString"); + } + + [Fact] + public void SerilogArrayMessageTemplateTest() + { + List exportedItems = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => + { + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + + ComplexType complexType = new(); + + var intArray = new int[] { 0, 1, 2, 3, 4 }; + var mixedArray = new object?[] { 0, null, "3", 18.0D }; + + Log.Logger.Information("Int array {data}", intArray); + Log.Logger.Information("Mixed array {data}", new object[] { mixedArray }); + + Log.CloseAndFlush(); + + Assert.Equal(2, exportedItems.Count); + + LogRecord logRecord = exportedItems[0]; + + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "data" && kvp.Value is int[] typedArray && intArray.SequenceEqual(typedArray)); + + logRecord = exportedItems[1]; + + Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "data" && kvp.Value is object?[] typedArray && mixedArray.SequenceEqual(typedArray)); + } + + [Fact] + public void SerilogExceptionTest() + { + List exportedItems = new(); + + InvalidOperationException ex = new(); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options => + { + options.AddInMemoryExporter(exportedItems); + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Log.Logger = new LoggerConfiguration() + .WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) + .CreateLogger(); + + ComplexType complexType = new(); + + Log.Logger.Information(ex, "Exception"); + + Log.CloseAndFlush(); + + Assert.Single(exportedItems); + + LogRecord logRecord = exportedItems[0]; + + Assert.Equal(ex, logRecord.Exception); + } + + private sealed class WrappedOpenTelemetryLoggerProvider : OpenTelemetryLoggerProvider + { + public WrappedOpenTelemetryLoggerProvider(Action configure) + : base(configure) + { + } + + public bool Disposed { get; private set; } + + protected override void Dispose(bool disposing) + { + this.Disposed = true; + + base.Dispose(disposing); + } + } + + private sealed class ComplexType + { + public override string ToString() => "ComplexTypeToString"; + } + } +} diff --git a/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs b/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs index 7b2b649db..7e086accf 100644 --- a/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs @@ -38,7 +38,7 @@ namespace OpenTelemetry.Logs.Tests Exception ex = new InvalidOperationException(); - logEmitter.Log( + logEmitter.Emit( new() { CategoryName = "LogEmitter", @@ -96,7 +96,7 @@ namespace OpenTelemetry.Logs.Tests activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; activity.TraceStateString = "key1=value1"; - logEmitter.Log(new(activity)); + logEmitter.Emit(new(activity)); Assert.Single(exportedItems); @@ -128,7 +128,7 @@ namespace OpenTelemetry.Logs.Tests new DateTime(2022, 6, 30, 16, 0, 0), DateTimeKind.Local); - logEmitter.Log(new() + logEmitter.Emit(new() { Timestamp = timestamp, }); @@ -159,7 +159,7 @@ namespace OpenTelemetry.Logs.Tests new DateTime(2022, 6, 30, 16, 0, 0), DateTimeKind.Unspecified); - logEmitter.Log(new() + logEmitter.Emit(new() { Timestamp = timestamp, });