-
Notifications
You must be signed in to change notification settings - Fork 790
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Logs] OpenTelemetry.Extensions.EventSource extensions project #3454
Changes from 1 commit
eeae428
44a3ad8
c6c606f
8397470
3c78cd4
dcb1c83
7446ec0
15f55ef
dae7579
f02ec69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -14,14 +14,13 @@ | |||||||
// limitations under the License. | ||||||||
// </copyright> | ||||||||
|
||||||||
using System.Diagnostics.Tracing; | ||||||||
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,9 +29,18 @@ | |||||||
.AddConsoleExporter(); | ||||||||
}); | ||||||||
|
||||||||
// Creates an OpenTelemetryEventSourceLogEmitter for routing EventSources with | ||||||||
// names matching OpenTelemetry* into logs | ||||||||
using var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter( | ||||||||
openTelemetryLoggerProvider, // <- Events will be written to openTelemetryLoggerProvider | ||||||||
(name) => name.StartsWith("OpenTelemetry") ? EventLevel.LogAlways : null, | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was it intentional that "OpenTelemetry" was used? any risk of cyclic/loops? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ya it was intentional. I just added this comment to explain the behavior: opentelemetry-dotnet/examples/LoggingExtensions/Program.cs Lines 56 to 58 in 8397470
I'm open to changing it LMK what you think about that. Doesn't have any cyclic or loop issues currently as OpenTelemetry.Extensions.EventSource doesn't have its own EventSource. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. If I enable this EventSourceLogEmitter, it'll subscribe to all ES starting with OpenTelemetry, and pipes them to OTel Logging SDK. OTel Logging SDK internally uses ES whose name starts with OpenTelemetry. So that EventSource Logs gets piped through this LoggingSDK again, which inturn produce more log.... (or did i got it wrong fully 🏃 ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The enduser for this package is people who already EventSource for their logging, and want those piped through OTel logging. For such use-cases, the best would be to show a custom EventSource, logging to it, but it showing up in OTel logging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
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(); | ||||||||
|
||||||||
// Note: Serilog ForContext API is used to set "CategoryName" on log messages | ||||||||
|
@@ -42,6 +50,8 @@ | |||||||
|
||||||||
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(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#nullable enable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#nullable enable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Changelog | ||
|
||
## Unreleased | ||
|
||
Initial release. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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</Description> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know the name My intention was this project would contain extensions for things in the Anyway open to alternatives just thought I would explain myself 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Then we could probably add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚡ ⚡ !! Haha, yea naming is a bummer. We should all just stop using words. Understood that
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we have separate packages for I prefer separate packages because they can be deprecated independently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK I'll rename this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [This reply is to @utpilla + @cijothomas I had it open for a while and didn't see @alanwest sneak in there. I'm good with punting on this discussion.] Hmm interesting. Hard time wrapping my mind around this. Is this definition from the spec somewhere? User plugging this stuff in is doing so at the host level so I'm not sure if the API/SDK distinction is useful. Just eyeballing this list... Which thing is not like the others? 🍎 🍎 🍎 🍌 🤣 OpenTelemetry.Instrumentation.AspNet - Emits telemetry about AspNet IMO the HOW it is done is less important than WHAT it is doing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No. My view ! We need to definitely discuss this more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💯 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, rename |
||
<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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
cijothomas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alanwest @cijothomas @utpilla One thing I realized working on tests. The Serilog + EventSource APIs ask you to pass in the
OpenTelemetryLoggerProvider
. If you are creating it manually, that is super easy to do. However if you are using theILoggingBuilder
extension it is tricky/non-obvious how you get the actualOpenTelemetryLoggerProvider
. Maybe as a follow-up I'll see if we can smooth that out somehow?