[Logs] OpenTelemetry.Extensions.EventSource extensions project (#3454)

* Add OpenTelemetryEventSourceLogEmitter.

* Project rename.

* Started tests.

* Added behavior comment in example.

* Add  --prerelease in READMEs.

* Example tweaks.

* Test improvements.

* Added activity_id/related_activity_id test.

* Warning cleanup.
This commit is contained in:
Mikel Blanchard 2022-07-22 15:40:39 -07:00 committed by GitHub
parent 1cd84d806a
commit bfabe9bc26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 876 additions and 9 deletions

View File

@ -235,6 +235,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Se
EndProject 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}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.EventSource", "src\OpenTelemetry.Extensions.EventSource\OpenTelemetry.Extensions.EventSource.csproj", "{7AFB4975-9680-4668-9F5E-C3F0CA41E982}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.EventSource.Tests", "test\OpenTelemetry.Extensions.EventSource.Tests\OpenTelemetry.Extensions.EventSource.Tests.csproj", "{304FCFFF-97DE-484B-8D8C-612C644426E5}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -481,6 +485,14 @@ Global
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.Build.0 = Release|Any CPU {6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.Build.0 = Release|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Release|Any CPU.Build.0 = Release|Any CPU
{304FCFFF-97DE-484B-8D8C-612C644426E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{304FCFFF-97DE-484B-8D8C-612C644426E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{304FCFFF-97DE-484B-8D8C-612C644426E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{304FCFFF-97DE-484B-8D8C-612C644426E5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -0,0 +1,33 @@
// <copyright file="ExampleEventSource.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.Diagnostics.Tracing;
namespace Examples.LoggingExtensions;
[EventSource(Name = EventSourceName)]
internal sealed class ExampleEventSource : EventSource
{
public const string EventSourceName = "OpenTelemetry-ExampleEventSource";
public static ExampleEventSource Log { get; } = new();
[Event(1, Message = "Example event written with '{0}' reason", Level = EventLevel.Informational)]
public void ExampleEvent(string reason)
{
this.WriteEvent(1, reason);
}
}

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.EventSource\OpenTelemetry.Extensions.EventSource.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -14,14 +14,14 @@
// limitations under the License. // limitations under the License.
// </copyright> // </copyright>
using System.Diagnostics.Tracing;
using Examples.LoggingExtensions;
using OpenTelemetry.Logs; using OpenTelemetry.Logs;
using OpenTelemetry.Resources; using OpenTelemetry.Resources;
using Serilog; using Serilog;
var resourceBuilder = ResourceBuilder.CreateDefault().AddService("Examples.LogEmitter"); var resourceBuilder = ResourceBuilder.CreateDefault().AddService("Examples.LoggingExtensions");
// 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 => var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options =>
{ {
options.IncludeFormattedMessage = true; options.IncludeFormattedMessage = true;
@ -30,18 +30,29 @@ var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options =>
.AddConsoleExporter(); .AddConsoleExporter();
}); });
// Creates an OpenTelemetryEventSourceLogEmitter for routing ExampleEventSource
// events into logs
using var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider, // <- Events will be written to openTelemetryLoggerProvider
(name) => name == ExampleEventSource.EventSourceName ? EventLevel.Informational : null,
disposeProvider: false); // <- Do not dispose the provider with OpenTelemetryEventSourceLogEmitter since in this case it is shared with Serilog
// Configure Serilog global logger // Configure Serilog global logger
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) // <- Register OpenTelemetry Serilog sink .WriteTo.OpenTelemetry(
openTelemetryLoggerProvider, // <- Register OpenTelemetry Serilog sink writing to openTelemetryLoggerProvider
disposeProvider: false) // <- Do not dispose the provider with Serilog since in this case it is shared with OpenTelemetryEventSourceLogEmitter
.CreateLogger(); .CreateLogger();
ExampleEventSource.Log.ExampleEvent("Startup complete");
// Note: Serilog ForContext API is used to set "CategoryName" on log messages // Note: Serilog ForContext API is used to set "CategoryName" on log messages
ILogger programLogger = Log.Logger.ForContext<Program>(); ILogger programLogger = Log.Logger.ForContext<Program>();
programLogger.Information("Application started {Greeting} {Location}", "Hello", "World"); programLogger.Information("Application started {Greeting} {Location}", "Hello", "World");
programLogger.Information("Message {Array}", new string[] { "value1", "value2" }); // Note: For Serilog this call flushes all logs
// Note: For Serilog this call flushes all logs and disposes
// OpenTelemetryLoggerProvider.
Log.CloseAndFlush(); Log.CloseAndFlush();
// Manually dispose OpenTelemetryLoggerProvider since it is being shared
openTelemetryLoggerProvider.Dispose();

View File

@ -0,0 +1,4 @@
#nullable enable
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func<string!, System.Diagnostics.Tracing.EventLevel?>! shouldListenToFunc, bool disposeProvider = true) -> void
override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void

View File

@ -0,0 +1,4 @@
#nullable enable
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func<string!, System.Diagnostics.Tracing.EventLevel?>! shouldListenToFunc, bool disposeProvider = true) -> void
override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void

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,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<Description>Extensions for using OpenTelemetry with System.Diagnostics.Tracing.EventSource</Description>
<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>
</Project>

View File

@ -0,0 +1,224 @@
// <copyright file="OpenTelemetryEventSourceLogEmitter.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 System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Logs
{
/// <summary>
/// Implements an <see cref="EventListener"/> which will convert <see
/// cref="EventSource"/> events into OpenTelemetry logs.
/// </summary>
public sealed class OpenTelemetryEventSourceLogEmitter : EventListener
{
private readonly bool includeFormattedMessage;
private readonly OpenTelemetryLoggerProvider openTelemetryLoggerProvider;
private readonly LogEmitter logEmitter;
private readonly object lockObj = new();
private readonly Func<string, EventLevel?> shouldListenToFunc;
private readonly List<EventSource> eventSources = new();
private readonly List<EventSource>? eventSourcesBeforeConstructor = new();
private readonly bool disposeProvider;
/// <summary>
/// Initializes a new instance of the <see
/// cref="OpenTelemetryEventSourceLogEmitter"/> class.
/// </summary>
/// <param name="openTelemetryLoggerProvider"><see
/// cref="OpenTelemetryLoggerProvider"/>.</param>
/// <param name="shouldListenToFunc">Callback function used to decide if
/// events should be captured for a given <see
/// cref="EventSource.Name"/>. Return <see langword="null"/> if no
/// events should be captured.</param>
/// <param name="disposeProvider">Controls whether or not the supplied
/// <paramref name="openTelemetryLoggerProvider"/> will be disposed when
/// the <see cref="EventListener"/> is disposed. Default value: <see
/// langword="true"/>.</param>
public OpenTelemetryEventSourceLogEmitter(
OpenTelemetryLoggerProvider openTelemetryLoggerProvider,
Func<string, EventLevel?> shouldListenToFunc,
bool disposeProvider = true)
{
Guard.ThrowIfNull(openTelemetryLoggerProvider);
Guard.ThrowIfNull(shouldListenToFunc);
this.includeFormattedMessage = openTelemetryLoggerProvider.IncludeFormattedMessage;
this.openTelemetryLoggerProvider = openTelemetryLoggerProvider!;
this.disposeProvider = disposeProvider;
this.shouldListenToFunc = shouldListenToFunc;
var logEmitter = this.openTelemetryLoggerProvider.CreateEmitter();
Debug.Assert(logEmitter != null, "logEmitter was null");
this.logEmitter = logEmitter!;
lock (this.lockObj)
{
foreach (EventSource eventSource in this.eventSourcesBeforeConstructor)
{
this.ProcessSource(eventSource);
}
this.eventSourcesBeforeConstructor = null;
}
}
/// <inheritdoc/>
public override void Dispose()
{
foreach (EventSource eventSource in this.eventSources)
{
this.DisableEvents(eventSource);
}
this.eventSources.Clear();
if (this.disposeProvider)
{
this.openTelemetryLoggerProvider.Dispose();
}
base.Dispose();
}
#pragma warning disable CA1062 // Validate arguments of public methods
/// <inheritdoc/>
protected override void OnEventSourceCreated(EventSource eventSource)
{
Debug.Assert(eventSource != null, "EventSource was null.");
try
{
if (this.eventSourcesBeforeConstructor != null)
{
lock (this.lockObj)
{
if (this.eventSourcesBeforeConstructor != null)
{
this.eventSourcesBeforeConstructor.Add(eventSource!);
return;
}
}
}
this.ProcessSource(eventSource!);
}
finally
{
base.OnEventSourceCreated(eventSource);
}
}
#pragma warning restore CA1062 // Validate arguments of public methods
#pragma warning disable CA1062 // Validate arguments of public methods
/// <inheritdoc/>
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
Debug.Assert(eventData != null, "EventData was null.");
string? rawMessage = eventData!.Message;
LogRecordData data = new(Activity.Current)
{
#if NETSTANDARD2_1_OR_GREATER
Timestamp = eventData.TimeStamp,
#endif
EventId = new EventId(eventData.EventId, eventData.EventName),
LogLevel = ConvertEventLevelToLogLevel(eventData.Level),
};
LogRecordAttributeList attributes = default;
attributes.Add("event_source.name", eventData.EventSource.Name);
if (eventData.ActivityId != Guid.Empty)
{
attributes.Add("event_source.activity_id", eventData.ActivityId);
}
if (eventData.RelatedActivityId != Guid.Empty)
{
attributes.Add("event_source.related_activity_id", eventData.RelatedActivityId);
}
int payloadCount = eventData.Payload?.Count ?? 0;
if (payloadCount > 0 && payloadCount == eventData.PayloadNames?.Count)
{
for (int i = 0; i < payloadCount; i++)
{
string name = eventData.PayloadNames[i];
if (!string.IsNullOrEmpty(rawMessage) && !this.includeFormattedMessage)
{
// TODO: This code converts the event message from
// string.Format syntax (eg: "Some message {0} {1}")
// into structured log format (eg: "Some message
// {propertyName1} {propertyName2}") but it is
// expensive. Probably needs a cache.
#if NETSTANDARD2_0
rawMessage = rawMessage.Replace($"{{{i}}}", $"{{{name}}}");
#else
rawMessage = rawMessage.Replace($"{{{i}}}", $"{{{name}}}", StringComparison.Ordinal);
#endif
}
attributes.Add(name, eventData.Payload![i]);
}
}
if (!string.IsNullOrEmpty(rawMessage) && this.includeFormattedMessage && payloadCount > 0)
{
rawMessage = string.Format(CultureInfo.InvariantCulture, rawMessage, eventData.Payload!.ToArray());
}
data.Message = rawMessage;
this.logEmitter.Emit(in data, in attributes);
}
#pragma warning restore CA1062 // Validate arguments of public methods
private static LogLevel ConvertEventLevelToLogLevel(EventLevel eventLevel)
{
return eventLevel switch
{
EventLevel.Informational => LogLevel.Information,
EventLevel.Warning => LogLevel.Warning,
EventLevel.Error => LogLevel.Error,
EventLevel.Critical => LogLevel.Critical,
_ => LogLevel.Trace,
};
}
private void ProcessSource(EventSource eventSource)
{
EventLevel? eventLevel = this.shouldListenToFunc(eventSource.Name);
if (eventLevel.HasValue)
{
this.eventSources.Add(eventSource);
this.EnableEvents(eventSource, eventLevel.Value, EventKeywords.All);
}
}
}
}

View File

@ -0,0 +1,38 @@
# OpenTelemetry.Extensions.EventSource
[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Extensions.EventSource.svg)](https://www.nuget.org/packages/OpenTelemetry.Extensions.EventSource)
[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Extensions.EventSource.svg)](https://www.nuget.org/packages/OpenTelemetry.Extensions.EventSource)
This project contains an
[EventListener](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventlistener)
which can be used to translate events written to an
[EventSource](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventsource)
into OpenTelemetry logs.
## Installation
```shell
dotnet add package OpenTelemetry.Extensions.EventSource --prerelease
```
## Usage Example
```csharp
// Step 1: Configure OpenTelemetryLoggerProvider...
var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options =>
{
options
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyService"))
.AddConsoleExporter();
});
// Step 2: Create OpenTelemetryEventSourceLogEmitter to listen to events...
using var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name.StartsWith("OpenTelemetry") ? EventLevel.LogAlways : null,
disposeProvider: true);
```
## References
* [OpenTelemetry Project](https://opentelemetry.io/)

View File

@ -10,7 +10,7 @@ writing log messages to OpenTelemetry.
## Installation ## Installation
```shell ```shell
dotnet add package OpenTelemetry.Extensions.Serilog dotnet add package OpenTelemetry.Extensions.Serilog --prerelease
``` ```
## Usage Example ## Usage Example

View File

@ -20,6 +20,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.InMemory" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.InMemory" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.EventSource" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Serilog" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Serilog" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)]

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 EventSource 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.EventSource\OpenTelemetry.Extensions.EventSource.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,391 @@
// <copyright file="OpenTelemetryEventSourceLogEmitterTests.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 System.Diagnostics.Tracing;
using System.Globalization;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using Xunit;
namespace OpenTelemetry.Extensions.EventSource.Tests
{
public class OpenTelemetryEventSourceLogEmitterTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void OpenTelemetryEventSourceLogEmitterDisposesProviderTests(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
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => null,
disposeProvider: dispose))
{
}
Assert.Equal(dispose, openTelemetryLoggerProvider.Disposed);
if (!dispose)
{
openTelemetryLoggerProvider.Dispose();
}
Assert.True(openTelemetryLoggerProvider.Disposed);
}
[Theory]
[InlineData("OpenTelemetry.Extensions.EventSource.Tests", EventLevel.LogAlways, 2)]
[InlineData("OpenTelemetry.Extensions.EventSource.Tests", EventLevel.Warning, 1)]
[InlineData("_invalid_", EventLevel.LogAlways, 0)]
public void OpenTelemetryEventSourceLogEmitterFilterTests(string sourceName, EventLevel? eventLevel, int expectedNumberOfLogRecords)
{
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
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == sourceName ? eventLevel : null))
{
TestEventSource.Log.SimpleEvent();
TestEventSource.Log.ComplexEvent("Test_Message", 18);
}
Assert.Equal(expectedNumberOfLogRecords, exportedItems.Count);
}
[Fact]
public void OpenTelemetryEventSourceLogEmitterCapturesExistingSourceTest()
{
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
TestEventSource.Log.SimpleEvent();
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == "OpenTelemetry.Extensions.EventSource.Tests" ? EventLevel.LogAlways : null))
{
TestEventSource.Log.SimpleEvent();
}
Assert.Single(exportedItems);
}
[Fact]
public void OpenTelemetryEventSourceLogEmitterSimpleEventTest()
{
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
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == "OpenTelemetry.Extensions.EventSource.Tests" ? EventLevel.LogAlways : null))
{
TestEventSource.Log.SimpleEvent();
}
Assert.Single(exportedItems);
var logRecord = exportedItems[0];
Assert.NotEqual(DateTime.MinValue, logRecord.Timestamp);
Assert.Equal(TestEventSource.SimpleEventMessage, logRecord.FormattedMessage);
Assert.Equal(TestEventSource.SimpleEventId, logRecord.EventId.Id);
Assert.Equal(nameof(TestEventSource.SimpleEvent), logRecord.EventId.Name);
Assert.Equal(LogLevel.Warning, logRecord.LogLevel);
Assert.Null(logRecord.CategoryName);
Assert.Null(logRecord.Exception);
Assert.Equal(default, logRecord.TraceId);
Assert.Equal(default, logRecord.SpanId);
Assert.Null(logRecord.TraceState);
Assert.Equal(ActivityTraceFlags.None, logRecord.TraceFlags);
Assert.NotNull(logRecord.StateValues);
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "event_source.name" && (string?)kvp.Value == "OpenTelemetry.Extensions.EventSource.Tests");
}
[Fact]
public void OpenTelemetryEventSourceLogEmitterSimpleEventWithActivityTest()
{
using var activity = new Activity("Test");
activity.Start();
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
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == "OpenTelemetry.Extensions.EventSource.Tests" ? EventLevel.LogAlways : null))
{
TestEventSource.Log.SimpleEvent();
}
Assert.Single(exportedItems);
var logRecord = exportedItems[0];
Assert.NotEqual(default, logRecord.TraceId);
Assert.Equal(activity.TraceId, logRecord.TraceId);
Assert.Equal(activity.SpanId, logRecord.SpanId);
Assert.Equal(activity.TraceStateString, logRecord.TraceState);
Assert.Equal(activity.ActivityTraceFlags, logRecord.TraceFlags);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void OpenTelemetryEventSourceLogEmitterComplexEventTest(bool formatMessage)
{
List<LogRecord> exportedItems = new();
#pragma warning disable CA2000 // Dispose objects before losing scope
var openTelemetryLoggerProvider = new WrappedOpenTelemetryLoggerProvider(options =>
{
options.IncludeFormattedMessage = formatMessage;
options.AddInMemoryExporter(exportedItems);
});
#pragma warning restore CA2000 // Dispose objects before losing scope
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == "OpenTelemetry.Extensions.EventSource.Tests" ? EventLevel.LogAlways : null))
{
TestEventSource.Log.ComplexEvent("Test_Message", 18);
}
Assert.Single(exportedItems);
var logRecord = exportedItems[0];
Assert.NotEqual(DateTime.MinValue, logRecord.Timestamp);
if (!formatMessage)
{
Assert.Equal(TestEventSource.ComplexEventMessageStructured, logRecord.FormattedMessage);
}
else
{
string expectedMessage = string.Format(CultureInfo.InvariantCulture, TestEventSource.ComplexEventMessage, "Test_Message", 18);
Assert.Equal(expectedMessage, logRecord.FormattedMessage);
}
Assert.Equal(TestEventSource.ComplexEventId, logRecord.EventId.Id);
Assert.Equal(nameof(TestEventSource.ComplexEvent), logRecord.EventId.Name);
Assert.Equal(LogLevel.Information, logRecord.LogLevel);
Assert.Null(logRecord.CategoryName);
Assert.Null(logRecord.Exception);
Assert.Equal(default, logRecord.TraceId);
Assert.Equal(default, logRecord.SpanId);
Assert.Null(logRecord.TraceState);
Assert.Equal(ActivityTraceFlags.None, logRecord.TraceFlags);
Assert.NotNull(logRecord.StateValues);
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "event_source.name" && (string?)kvp.Value == "OpenTelemetry.Extensions.EventSource.Tests");
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "arg1" && (string?)kvp.Value == "Test_Message");
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "arg2" && (int?)kvp.Value == 18);
}
[Theory(Skip = "Not runnable in CI, see note.")]
[InlineData(true)]
[InlineData(false)]
public void OpenTelemetryEventSourceLogEmitterActivityIdTest(bool enableTplListener)
{
/*
* Note:
*
* To enable Activity ID the 'System.Threading.Tasks.TplEventSource'
* source must be enabled see:
* https://docs.microsoft.com/en-us/dotnet/core/diagnostics/eventsource-activity-ids#tracking-work-using-an-activity-id
*
* Once enabled, it cannot be turned off:
* https://github.com/dotnet/runtime/blob/0fbdb1ed6e076829e4693a61ae5d11c4cb23e7ee/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/ActivityTracker.cs#L208
*
* That behavior makes testing it difficult.
*/
using var tplListener = enableTplListener ? new TplEventSourceListener() : null;
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
using (var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider,
(name) => name == "OpenTelemetry.Extensions.EventSource.Tests" ? EventLevel.LogAlways : null))
{
TestEventSource.Log.WorkStart();
TestEventSource.Log.SubworkStart();
TestEventSource.Log.SubworkStop();
TestEventSource.Log.WorkStop();
}
Assert.Equal(4, exportedItems.Count);
var logRecord = exportedItems[1];
if (enableTplListener)
{
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "event_source.activity_id");
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "event_source.related_activity_id");
}
else
{
Assert.DoesNotContain(logRecord.StateValues, kvp => kvp.Key == "event_source.activity_id");
Assert.DoesNotContain(logRecord.StateValues, kvp => kvp.Key == "event_source.related_activity_id");
}
}
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);
}
}
[EventSource(Name = "OpenTelemetry.Extensions.EventSource.Tests")]
private sealed class TestEventSource : System.Diagnostics.Tracing.EventSource
{
public const int SimpleEventId = 1;
public const string SimpleEventMessage = "Warning event with no arguments.";
public const int ComplexEventId = 2;
public const string ComplexEventMessage = "Information event with two arguments: '{0}' & '{1}'.";
public const string ComplexEventMessageStructured = "Information event with two arguments: '{arg1}' & '{arg2}'.";
public static TestEventSource Log { get; } = new();
[Event(SimpleEventId, Message = SimpleEventMessage, Level = EventLevel.Warning)]
public void SimpleEvent()
{
this.WriteEvent(SimpleEventId);
}
[Event(ComplexEventId, Message = ComplexEventMessage, Level = EventLevel.Informational)]
public void ComplexEvent(string arg1, int arg2)
{
this.WriteEvent(ComplexEventId, arg1, arg2);
}
[Event(3, Level = EventLevel.Verbose)]
public void WorkStart()
{
this.WriteEvent(3);
}
[Event(4, Level = EventLevel.Verbose)]
public void WorkStop()
{
this.WriteEvent(4);
}
[Event(5, Level = EventLevel.Verbose)]
public void SubworkStart()
{
this.WriteEvent(5);
}
[Event(6, Level = EventLevel.Verbose)]
public void SubworkStop()
{
this.WriteEvent(6);
}
}
private sealed class TplEventSourceListener : EventListener
{
private readonly List<System.Diagnostics.Tracing.EventSource> eventSources = new();
/// <inheritdoc/>
public override void Dispose()
{
foreach (System.Diagnostics.Tracing.EventSource eventSource in this.eventSources)
{
this.DisableEvents(eventSource);
}
this.eventSources.Clear();
base.Dispose();
}
protected override void OnEventSourceCreated(System.Diagnostics.Tracing.EventSource eventSource)
{
if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
{
// Activity IDs aren't enabled by default.
// Enabling Keyword 0x80 on the TplEventSource turns them on
this.EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)0x80);
this.eventSources.Add(eventSource);
}
}
}
}
}

View File

@ -16,6 +16,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs; using OpenTelemetry.Logs;
@ -102,6 +103,46 @@ namespace OpenTelemetry.Extensions.Serilog.Tests
Assert.NotNull(logRecord.StateValues); Assert.NotNull(logRecord.StateValues);
Assert.Single(logRecord.StateValues); Assert.Single(logRecord.StateValues);
Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "greeting" && (string?)kvp.Value == "World"); Assert.Contains(logRecord.StateValues, kvp => kvp.Key == "greeting" && (string?)kvp.Value == "World");
Assert.Equal(default, logRecord.TraceId);
Assert.Equal(default, logRecord.SpanId);
Assert.Null(logRecord.TraceState);
Assert.Equal(ActivityTraceFlags.None, logRecord.TraceFlags);
}
[Fact]
public void SerilogBasicLogWithActivityTest()
{
using var activity = new Activity("Test");
activity.Start();
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();
Log.Logger.Information("Hello {greeting}", "World");
Log.CloseAndFlush();
Assert.Single(exportedItems);
var logRecord = exportedItems[0];
Assert.NotEqual(default, logRecord.TraceId);
Assert.Equal(activity.TraceId, logRecord.TraceId);
Assert.Equal(activity.SpanId, logRecord.SpanId);
Assert.Equal(activity.TraceStateString, logRecord.TraceState);
Assert.Equal(activity.ActivityTraceFlags, logRecord.TraceFlags);
} }
[Fact] [Fact]