-
Notifications
You must be signed in to change notification settings - Fork 769
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] Updates for specification changes #3677
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #3677 +/- ##
==========================================
- Coverage 87.74% 87.41% -0.33%
==========================================
Files 283 295 +12
Lines 10286 10542 +256
==========================================
+ Hits 9025 9215 +190
- Misses 1261 1327 +66
|
/// <summary> | ||
/// LoggerProvider is the entry point of the OpenTelemetry API. It provides access to <see cref="Logger"/>. | ||
/// </summary> | ||
public class LoggerProvider : BaseProvider |
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.
We're very confident that LoggerProvider
will be the final term? Seems very likely, but just calling out this is an area of risk if we release 1.4 stable and then it changes.
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.
I feel like yes, but can't say for certain! I know keeping it consistent with traces & metrics was one of the goals. If we want to make LoggerProvider
and LoggerProviderBuilder
internal what we would lose is the ability to release the Serilog & EventSource extensions to get feedback. For those to work you have to pass in the provider instance.
src/OpenTelemetry.Extensions.EventSource/OpenTelemetryEventSourceLogEmitter.cs
Outdated
Show resolved
Hide resolved
internal Logger GetLogger(string? name) | ||
=> this.GetLogger(new LoggerOptions(name)); | ||
|
||
internal Logger GetLogger(InstrumentationScope instrumentationScope) |
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.
Since metrics and traces do not yet support InstrumentationScope
would it make sense to just have
internal Logger GetLogger(string name, string version)
This mirrors .NET's support for meters and activities. When the runtime introduces support for schema url and attributes, we should follow the same pattern for the logger.
Also, I understand InstrumentationScope is internal right now, so not a big deal if this merges as is.
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.
This one was kind of an interesting journey ⛵
Check out: Get a Logger. There are a couple unique parameters event_domain
(required if you want to emit events) & include_trace_context
(optional, true by default). I think the first natural thing to do is like...
internal Logger GetLogger(string? name = null, string? version = null, string? eventDomain = null, bool includeTraceContext = true) {}
But then what do we do if we want to support the other scope things (schema_url
, attributes
) in the future? We have to drop in another giant overload, or have a whole bunch of non-defaulted versions to try and cover all the permutations. If the spec ever adds anything else, more trouble.
So in order to keep it simple and allow for expansion I originally just had:
Logger GetLogger(LoggerOptions options) {}
internal sealed class LoggerOptions
{
public LoggerOptions(string? name) {}
public string Name { get; }
public string? Version { get; init; }
public string? SchemaUrl { get; init; }
public IReadOnlyDictionary<string, object>? Attributes { get; init; }
public string? EventDomain { get; init; }
public bool IncludeTraceContext { get; init; } = true;
}
But then, Name
+ Version
+ SchemaUrl
+ Attributes
are really "instrumentation scope" which (by the spec) also apply to tracing & metrics. So to achieve potential re-use it became...
internal sealed class LoggerOptions
{
public LoggerOptions(InstrumentationScope instrumentationScope) {}
public InstrumentationScope InstrumentationScope { get; }
public string? EventDomain { get; init; }
public bool IncludeTraceContext { get; init; } = true;
}
I was thinking at some point we might do...
class LoggerProvider
{
public Tracer GetTracer(string name, string version = null) {}
+ public Tracer GetTracer(InstrumentationScope instrumentationScope) {}
}
Then I started to write tests and update the code to use this API. When you just want to pass a name having to new up LoggerOptions
I felt was too ceremonious. So I add some helpers for the common cases.
internal Logger GetLogger()
=> this.GetLogger(name: null);
internal Logger GetLogger(string? name)
=> this.GetLogger(new LoggerOptions(name));
internal Logger GetLogger(InstrumentationScope instrumentationScope)
=> this.GetLogger(new LoggerOptions(instrumentationScope));
Worth noting here is the InstrumentationScope
does end up on LogRecord
so exporters could actually be authored to be spec complaint w.r.t. "instrumentation scope".
So what should we do here? 🤷 It is all internal so we could just wait and see 😄
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.
It is all internal so we could just wait and see 😄
Yes. I think it would be ideal if these things were encapsulated in a .NET class not an OpenTelemetry class. Relevant to dotnet/runtime#63651 - the API here should be expanded to include the scope attributes.
configure!(resourceBuilder); | ||
} | ||
|
||
public void SetResourceBuilder(ResourceBuilder resourceBuilder) |
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.
I personally like the ConfigureResource
pattern over this. Does it make sense to just remove it from logging, or do you think the lack of parity with our trace/metric support is bad?
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.
I'm not totally convinced on the uselessness of SetResourceBuilder
. If you look at any individual signal in a vacuum, it totally makes sense to use ConfigureResource
. But if you are configuring a shared/global resource which will be used by two or more signals, or two or more providers of the same signal, isn't SetResourceBuilder
more useful?
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.
Yea good point. It may still be preferred by some. I've been shoving resource configuration into a method then using it like providerBuilder.ConfigureResource(ConfigureMyResource)
.
src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs
Outdated
Show resolved
Hide resolved
|
||
this.InstrumentationScope = null; | ||
|
||
this.Attributes = stateValues; |
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.
nit: rename local stateValues
-> attributes
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.
This ctor our code no longer uses. I'm maintaining it because I know of at least one user calling it to modify log records which were previously immutable. So I left it "stateValues" to be ultra-conservative as far as not breaking anyone's reflection. Unlikely they are looking at parameter names but I figured might as well be safe?
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.
I'm maintaining it because I know of at least one user calling it to modify log records which were previously immutable.
Hmm, now that they're mutable, maybe we're ok to break them now? If so, probably makes sense to do it in a separate PR for visibility.
{ | ||
options.AddConsoleExporter(); | ||
}); | ||
builder.AddOpenTelemetry().AddConsoleExporter(); |
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.
Regarding the sample in the PR description:
builder.Logging
.ClearProviders()
.AddEventLog() // Order must be changed
.AddOpenTelemetry(options => options.ParseStateValues = true)
.SetResourceBuilder(resourceBuilder)
.AddConsoleExporter();
For the sake of other reviewers, I found it helpful that you pointed out that while it is common for builder methods to be chainable like the Authorization extensions, even ASP.NET Core has examples where the extensions are not chainable like with the Authentication extensions (i.e., does not return the IServiceCollection
).
I think this is good precedent that requiring AddOpenTelemetry
be at the end is OK.
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.
Another good example: AddMvc. Returns IMvcBuilder.
Going to reopen this against a dedicated branch. |
Fixes #3637
[Apologies for the code bomb! 💣 💥 🤯]
Changes
The OpenTelemetry specification now has API definitions for
LoggerProvider
&Logger
. This PR adds those to the API project and moves some of what was in SDK into API (LogRecordData
+LogRecordAttributeList
). What is in API is very faithful to the spec, I didn't bring in any of the ILogger-specific stuff SDK has been using.internal
. Only the class definitions arepublic
at the moment. I tried to make everything internal but that became way more work than it was worth. Making the classes at least public lets all the standard extensions work.There is now a dedicated
LoggerProviderSdk
which contains much of whatOpenTelemetryLoggerProvider
contained previously. OurILogger
support is now more of an integration than a fundamental thing. The entry point to logging is theLoggerProvider
and that is fed into theILoggerProvider
integration in the same way it is fed into the Serilog or the EventSource adapter things.All of the extension methods added in [Logs] Support dependency injection in logging build-up #3504 have been moved to
LoggerProviderBuilder
. There really isn't a difference between logs, traces, & metrics now. All the options that were specific to the ILogger integration are now separate from the provider.LogEmitter
is gone.Logger
API now has methods for emitting logs + events. Serilog & EventSource projects now use those.Refactored some of the builder state & service registration helpers so all signals can use them.
Tried to improve the SDK
LogRecord
as far as its spec compliance.StateValues
in favor ofAttributes
.Body
which is now set to{OriginalFormat}
automatically for ILogger messages if it is detected as the last element in attributes.State
by makingAttributes
more useful (see next bullet).TraceState
onLogRecord
s. That wasn't in the spec. I added an option to turn it back on if desired.State
+StateValues
+ParseStateValues
These things evolved to be strange. Exporters are having to check
StateValues
&State
and some are forcingParseStateValues = true
.State
itself is not safe to access beyond the log lifecycle. I tweaked things so there is a more deterministic behavior. IfTState
isIReadOnlyList<KeyValuePair<string, object>>
(orIEnumerable<>
equivalent) thanLogRecord.Attributes
will now always be populated. That will cover most messages written throughILogger
using the source generator or the extensions. The only way to pass something that doesn't meet that requirement is callingILogger.Log<T>(...)
directly. In that case ifParseStateValues = true
than we will buildAttributes
dynamically using reflection.This allows for the deprecation of
LogRecord.State
. Exporters should now be able to look atLogRecord.Attributes
for everything and get a nice and consistent behavior.If users don't care for export of attributes at all there is now an option
IncludeState
to turn off all operations againstTState
.More coming soon.
Public API Changes
OpenTelemetry.Api
Kept most of the spec surface internal but needed a couple base classes exposed for extensions to bind to:
OpenTelemetry
More coming soon.
Backwards compatibility
The stable API in 1.3.1 uses OpenTelemetryLoggerOptions as the builder for logging. There isn't much to it, only
AddProcessor
andSetResourceBuilder
are available. Extensions are coded to use that API when building up the provider.This PR is essentially splitting things up so that
LoggingProviderBuilder
is responsible for building the specLoggerProvider
andOpenTelemetryLoggerOptions
is responsible for configuring theOpenTelemetryLoggerProvider
which is theILogger
integration.This was tricky and I spent a lot of time trying many different things. This is the best I could come up with! Open to suggestions 🤔
OpenTelemetryLoggerOptions.AddProcessor
&OpenTelemetryLoggerOptions.SetResourceBuilder
still exist. Code and extensions that rely on them will continue to function. How this works is anything set on the options will be applied to the provider when it is ready.LoggingProviderBuilder
has all the great stuff now. This is the API we want to use instead. To encourage migrationOpenTelemetryLoggerOptions.AddProcessor
&OpenTelemetryLoggerOptions.SetResourceBuilder
are now marked asObsolete
as are the extensions usingOpenTelemetryLoggerOptions
. New extensions have been added targetingLoggingProviderBuilder
instead.Here is some working 1.3.1 code:
That will continue to work but now generates warnings due to the obsoletions. Here is the new pattern:
AddOpenTelemetry
returnsOpenTelemetryLoggingBuilder
which implementsLoggerProviderBuilder
to configure the builder.This pattern works equally well:
As does...
The first pattern configures everything off of
ILoggingBuilder
in one spot. The second two configureLoggerProvider
independently and then tellILoggerBuilder
(ILogger integration) to use it. We don't have to provider a "one-spot" option, but if ILogger integration is our most common scenario it is kind of nice.Alternative ideas
This one feels close:
It makes our pattern...
Why I didn't go with this:
This one is tempting...
...but that has all the pitfalls this PR is trying to avoid.
LoggerProviderBuilder
needs theIServiceCollection
and is invoked immediately.OpenTelemetryLoggerOptions
is part of Options API so it doesn't really exist until theIServiceProvider
is ready. To try and combine them you have to do stuff like this. It is best to let builders be builders and let options be options 😄TODOs
CHANGELOG.md
updated for non-trivial changes