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]