diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln
index d661f7604c5..375d2633837 100644
--- a/OpenTelemetry.sln
+++ b/OpenTelemetry.sln
@@ -235,6 +235,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Se
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
+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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/examples/LoggingExtensions/ExampleEventSource.cs b/examples/LoggingExtensions/ExampleEventSource.cs
new file mode 100644
index 00000000000..7fa0b8cea95
--- /dev/null
+++ b/examples/LoggingExtensions/ExampleEventSource.cs
@@ -0,0 +1,33 @@
+//
+// 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.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);
+ }
+}
diff --git a/examples/LoggingExtensions/Examples.LoggingExtensions.csproj b/examples/LoggingExtensions/Examples.LoggingExtensions.csproj
index 8e502ea56d8..892a652ac78 100644
--- a/examples/LoggingExtensions/Examples.LoggingExtensions.csproj
+++ b/examples/LoggingExtensions/Examples.LoggingExtensions.csproj
@@ -10,6 +10,7 @@
+
diff --git a/examples/LoggingExtensions/Program.cs b/examples/LoggingExtensions/Program.cs
index 310e225337f..24ec26eadbf 100644
--- a/examples/LoggingExtensions/Program.cs
+++ b/examples/LoggingExtensions/Program.cs
@@ -14,14 +14,14 @@
// limitations under the License.
//
+using System.Diagnostics.Tracing;
+using Examples.LoggingExtensions;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
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 =>
{
options.IncludeFormattedMessage = true;
@@ -30,18 +30,29 @@
.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
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();
+ExampleEventSource.Log.ExampleEvent("Startup complete");
+
// 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.
+// Note: For Serilog this call flushes all logs
Log.CloseAndFlush();
+
+// Manually dispose OpenTelemetryLoggerProvider since it is being shared
+openTelemetryLoggerProvider.Dispose();
diff --git a/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Shipped.txt
new file mode 100644
index 00000000000..7dc5c58110b
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000000..32c7f36aea5
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,4 @@
+#nullable enable
+OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
+OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func! shouldListenToFunc, bool disposeProvider = true) -> void
+override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void
diff --git a/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Shipped.txt
new file mode 100644
index 00000000000..7dc5c58110b
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000000..32c7f36aea5
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt
@@ -0,0 +1,4 @@
+#nullable enable
+OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
+OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func! shouldListenToFunc, bool disposeProvider = true) -> void
+override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void
diff --git a/src/OpenTelemetry.Extensions.EventSource/AssemblyInfo.cs b/src/OpenTelemetry.Extensions.EventSource/AssemblyInfo.cs
new file mode 100644
index 00000000000..a51a83d9d1d
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/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.EventSource/CHANGELOG.md b/src/OpenTelemetry.Extensions.EventSource/CHANGELOG.md
new file mode 100644
index 00000000000..63bfc986bdc
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## Unreleased
+
+Initial release.
diff --git a/src/OpenTelemetry.Extensions.EventSource/OpenTelemetry.Extensions.EventSource.csproj b/src/OpenTelemetry.Extensions.EventSource/OpenTelemetry.Extensions.EventSource.csproj
new file mode 100644
index 00000000000..dfc6d2e0a89
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/OpenTelemetry.Extensions.EventSource.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.1;netstandard2.0
+ Extensions for using OpenTelemetry with System.Diagnostics.Tracing.EventSource
+ enable
+ AllEnabledByDefault
+ latest
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenTelemetry.Extensions.EventSource/OpenTelemetryEventSourceLogEmitter.cs b/src/OpenTelemetry.Extensions.EventSource/OpenTelemetryEventSourceLogEmitter.cs
new file mode 100644
index 00000000000..58b244ea428
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/OpenTelemetryEventSourceLogEmitter.cs
@@ -0,0 +1,224 @@
+//
+// 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 System.Diagnostics.Tracing;
+using System.Globalization;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry.Internal;
+
+namespace OpenTelemetry.Logs
+{
+ ///
+ /// Implements an which will convert events into OpenTelemetry logs.
+ ///
+ 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 shouldListenToFunc;
+ private readonly List eventSources = new();
+ private readonly List? eventSourcesBeforeConstructor = new();
+ private readonly bool disposeProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// .
+ /// Callback function used to decide if
+ /// events should be captured for a given . Return if no
+ /// events should be captured.
+ /// Controls whether or not the supplied
+ /// will be disposed when
+ /// the is disposed. Default value: .
+ public OpenTelemetryEventSourceLogEmitter(
+ OpenTelemetryLoggerProvider openTelemetryLoggerProvider,
+ Func 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;
+ }
+ }
+
+ ///
+ 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
+ ///
+ 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
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Extensions.EventSource/README.md b/src/OpenTelemetry.Extensions.EventSource/README.md
new file mode 100644
index 00000000000..5259aae3425
--- /dev/null
+++ b/src/OpenTelemetry.Extensions.EventSource/README.md
@@ -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/)
diff --git a/src/OpenTelemetry.Extensions.Serilog/README.md b/src/OpenTelemetry.Extensions.Serilog/README.md
index b702325cf14..39372b4e142 100644
--- a/src/OpenTelemetry.Extensions.Serilog/README.md
+++ b/src/OpenTelemetry.Extensions.Serilog/README.md
@@ -10,7 +10,7 @@ writing log messages to OpenTelemetry.
## Installation
```shell
-dotnet add package OpenTelemetry.Extensions.Serilog
+dotnet add package OpenTelemetry.Extensions.Serilog --prerelease
```
## Usage Example
diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs
index 0a79a36dfe5..46eb253152f 100644
--- a/src/OpenTelemetry/AssemblyInfo.cs
+++ b/src/OpenTelemetry/AssemblyInfo.cs
@@ -20,6 +20,7 @@
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.InMemory" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus" + 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.Serilog" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)]
diff --git a/test/OpenTelemetry.Extensions.EventSource.Tests/AssemblyInfo.cs b/test/OpenTelemetry.Extensions.EventSource.Tests/AssemblyInfo.cs
new file mode 100644
index 00000000000..11bfd5a2025
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.EventSource.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.EventSource.Tests/OpenTelemetry.Extensions.EventSource.Tests.csproj b/test/OpenTelemetry.Extensions.EventSource.Tests/OpenTelemetry.Extensions.EventSource.Tests.csproj
new file mode 100644
index 00000000000..72af555428c
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.EventSource.Tests/OpenTelemetry.Extensions.EventSource.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+ Unit test project for OpenTelemetry EventSource extensions
+
+ net6.0;netcoreapp3.1
+ $(TargetFrameworks);net462
+ enable
+ AllEnabledByDefault
+ latest
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenTelemetry.Extensions.EventSource.Tests/OpenTelemetryEventSourceLogEmitterTests.cs b/test/OpenTelemetry.Extensions.EventSource.Tests/OpenTelemetryEventSourceLogEmitterTests.cs
new file mode 100644
index 00000000000..e6a53aa6387
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.EventSource.Tests/OpenTelemetryEventSourceLogEmitterTests.cs
@@ -0,0 +1,391 @@
+//
+// 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 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 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 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 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 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 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 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 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 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 eventSources = new();
+
+ ///
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs
index ba22ba1fa8e..000a1e7c1d6 100644
--- a/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs
+++ b/test/OpenTelemetry.Extensions.Serilog.Tests/OpenTelemetrySerilogSinkTests.cs
@@ -16,6 +16,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
@@ -102,6 +103,46 @@ public void SerilogBasicLogTests(bool includeFormattedMessage)
Assert.NotNull(logRecord.StateValues);
Assert.Single(logRecord.StateValues);
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 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]