[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.
This commit is contained in:
Mikel Blanchard 2022-07-18 09:38:02 -07:00 committed by GitHub
parent a19d106e0c
commit ab10e119ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 699 additions and 5 deletions

View File

@ -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}

View File

@ -40,6 +40,7 @@
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
<OpenTracingPkgVer>[0.12.1,0.13)</OpenTracingPkgVer>
<OTelPreviousStableVer>1.3.0</OTelPreviousStableVer>
<SerilogPkgVer>[2.8.0,3.0)</SerilogPkgVer>
<StyleCopAnalyzersPkgVer>[1.2.0-beta.354,2.0)</StyleCopAnalyzersPkgVer>
<SystemCollectionsImmutablePkgVer>1.4.0</SystemCollectionsImmutablePkgVer>
<SystemDiagnosticSourcePkgVer>6.0.0</SystemDiagnosticSourcePkgVer>

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,47 @@
// <copyright file="Program.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<Program>();
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();

View File

@ -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/)

View File

@ -0,0 +1 @@
#nullable enable

View File

@ -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!

View File

@ -0,0 +1,35 @@
// <copyright file="AssemblyInfo.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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

View File

@ -0,0 +1,5 @@
# Changelog
## Unreleased
Initial release.

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Description>Extensions to enable OpenTelemetry logging when using the Serilog library</Description>
<PackageTags>$(PackageTags);serilog;logging</PackageTags>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Api\Internal\Guard.cs" Link="Includes\Guard.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="$(SerilogPkgVer)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,54 @@
// <copyright file="OpenTelemetrySerilogExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using Serilog.Configuration;
namespace Serilog
{
/// <summary>
/// Contains Serilog extension methods.
/// </summary>
public static class OpenTelemetrySerilogExtensions
{
/// <summary>
/// Adds a sink to Serilog <see cref="LoggerConfiguration"/> which will
/// write to OpenTelemetry.
/// </summary>
/// <param name="loggerConfiguration"><see
/// cref="LoggerSinkConfiguration"/>.</param>
/// <param name="openTelemetryLoggerProvider"><see
/// cref="OpenTelemetryLoggerProvider"/>.</param>
/// <param name="disposeProvider">Controls whether or not the supplied
/// <paramref name="openTelemetryLoggerProvider"/> will be disposed when
/// the logger is disposed. Default value: <see
/// langword="true"/>.</param>
/// <returns>Supplied <see cref="LoggerConfiguration"/> for chaining calls.</returns>
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
}
}
}

View File

@ -0,0 +1,135 @@
// <copyright file="OpenTelemetrySerilogSink.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, LogEventPropertyValue> 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<LogEventPropertyValue> 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();
}
}
}
}

View File

@ -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/)

View File

@ -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)]

View File

@ -42,7 +42,7 @@ namespace OpenTelemetry.Logs
/// </summary>
/// <param name="data"><see cref="LogRecordData"/>.</param>
/// <param name="attributes"><see cref="LogRecordAttributeList"/>.</param>
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;

View File

@ -0,0 +1,19 @@
// <copyright file="AssemblyInfo.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
[assembly: CLSCompliant(false)]

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Unit test project for OpenTelemetry Serilog extensions</Description>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
<TargetFrameworks>net6.0;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks Condition="$(OS) == 'Windows_NT'">$(TargetFrameworks);net462</TargetFrameworks>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPkgVer)" />
<PackageReference Include="xunit" Version="$(XUnitPkgVer)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioPkgVer)">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="$(DotNetXUnitCliVer)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,260 @@
// <copyright file="OpenTelemetrySerilogSinkTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<LogRecord> 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<LogRecord> 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<LogRecord> 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<OpenTelemetrySerilogSinkTests>();
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<LogRecord> 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<LogRecord> 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<LogRecord> 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<OpenTelemetryLoggerOptions> 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";
}
}
}

View File

@ -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,
});