-
Notifications
You must be signed in to change notification settings - Fork 889
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
Specify how Logs SDK implements Enabled #4207
Comments
Proposal A - Extend Processor Below is a proposal mostly based on how Go Logs SDK currently implements The idea is to add an The parameters of The return value of Implementation of When the user calls func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
newParam := l.newEnabledParameters(ctx, param)
for _, p := range l.provider.processors {
if enabled := p.Enabled(ctx, newParam); enabled {
return true
}
}
return false
} In order the implement filtering, the processor would need to wrap (decorate) an existing processor. Example pseudocode of a processor filtering log records by setting a minimum severity level: // LogProcessor is an [log.Processor] implementation that wraps another
// [log.Processor]. It will pass-through calls to OnEmit and Enabled for
// records with severity greater than or equal to a minimum. All other method
// calls are passed to the wrapped [log.Processor].
type LogProcessor struct {
log.Processor
Minimum api.Severity
}
// OnEmit passes ctx and r to the [log.Processor] that p wraps if the severity
// of record is greater than or equal to p.Minimum. Otherwise, record is
// dropped.
func (p *LogProcessor) OnEmit(ctx context.Context, record *log.Record) error {
if record.Severity() >= p.Minimum {
return p.Processor.OnEmit(ctx, record)
}
return nil
}
// Enabled returns if the [log.Processor] that p wraps is enabled if the
// severity of record is greater than or equal to p.Minimum. Otherwise false is
// returned.
func (p *LogProcessor) OnEnabled(ctx context.Context, param log.EnabledParameter) bool {
lvl, ok := param.Severity()
if !ok {
return true
}
return lvl >= p.Minimum && p.Processor.OnEnabled(ctx, param)
} I also want to call out that the idea of composing/wrapping/decorating the processors is already used by the isolating processor. Composing of processors is necessary to achieve complex processing pipelines in the SDK. Personal remarks: |
Proposal B - Add filtering via Filterer Below is a proposal inspired by sampling design in tracing SDK. The idea is to add a new abstraction named The parameters of The return value of Pseudocode: type Filterer interface {
Filter(ctx context.Context, param FilterParameters) bool
} When the user calls // Enabled returns false if at least one Filterer held by the LoggerProvider
// that created the logger will return false for the provided context and param.
func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
newParam := l.newEnabledParameters(param)
for _, flt := range l.provider.filterers {
if !flt.Filter(ctx, newParam) {
return false
}
}
return true
} When the user calls func (l *logger) Emit(ctx context.Context, r log.Record) {
param:= l.toEnabledParameters(r)
if !l.Enabled(ctx, param) {
return
}
newRecord := l.newRecord(ctx, r)
for _, p := range l.provider.processors {
if err := p.OnEmit(ctx, &newRecord); err != nil {
otel.Handle(err)
}
}
} Pseudocode of a custom minimum severity level filterer : type MinSevFilterer struct {
Minimum api.Severity
}
func (f *MinSevFilterer) Filter(ctx context.Context, param log.FilterParameters) bool {
lvl, ok := param.Severity()
if !ok {
return true
}
return lvl >= p.Minimum
} Filtering is coupled to emitting log records. Therefore, a custom processor filtering implementation does not need to implement filtering on both This design should be easier to be added to existing SDKs as it adds a new abstraction instead of adding more responsibilities to IMPORTANT: |
I'm a little concerned that by "defining" the way that "enabled" should be implemented (especially adding layers of filtering (as part of the required implementation)) would (potentially) worse from a perf perspective than just constructing and emitting the Log that eventually gets dropped. ie. the whole point of the "Is enabled" is for avoid perf impact... |
I think it's fine to define "what" options should be used (ie the parameters) to "check" whether something is enabled or not and this should be limited. |
I am not sure if I follow.
Which part of the implementation proposal (A or B or both) are you concerned about? |
Agree. PTAL #4203 where I try to propose a minimal set of parameters. |
Prescribing that it must be done via a specific method (A or B), rather than letting the language / environment determine the best way to provide the capability. |
@MSNev the issue with not specifying behavior is that it can be specified later in a non-compatible way. Meaning, if an implementation implements A and the spec later specifies B that language SIG is non-compliant. Not having some language about implementation both stops implementations from adopting this feature for that reason and causes specification maintainer to not approve any further changes1. This issue it trying to break out of this circular logic. If you think things should be left undefined, please work with @jack-berg to reach a consensus as his statements ask for the opposite. Footnotes |
Proposal C - Extend Logger config What about adding a severity level as part of the logger config? It seems to match the common log workflow of changing severity threshold for specific loggers. I'm suspicious of the idea of introducing the concept of an expandable set of |
I think that it is reasonable to improve the logging configuration than introducing a new concept.
I agree. I assume that setting a global severity threshold is also a common workflow.
PTAL at #4203 (comment) |
Per discussion with @pellared, if the logger can have an |
If we unify on the logger config, we would not be able to have different severity level support for different processors. If you have one processor that wants to send to short-term storage and hold all debug logs and a long-term storage that only store error logs and above. |
This issue is also present in Proposal B - Filterer. The possible workaround would be to compose logger providers as noted in proposal B. However, I do not feel like such workaround is good/acceptable. |
Wouldn't it be acceptable if the SDK supports to multiple ways to implement Enabled? E.g. Proposal A and Proposal C. I think that an ability to configure a logger to make it disabled or specify a minimum severity level would fulfill most use cases and may be well performing. While composing processors may be necessary to allow setting up complex processing pipelines. |
Given we already have a few proposals and a prototype in Go. Shouldn't this issue be no longer a blocker for #4208? CC @open-telemetry/technical-committee |
FWIW that's what .NET ILogger does - if any of the destinations have the logger + severity enabled, the logging is enabled. Since every provider owns it's own configuration via LoggerConfigurator, the composite provider could be responsible for merging them and creating the final It also means that filters would be an (optional) feature based on the LoggerConfig. Otherwise filtering processors and config may start to contradict each other and it'd be best if we could avoid it. More practical approach could be to let processors own/participate in LoggerConfig and this would merge proposals A and C together. |
@open-telemetry/technical-committee, can I propose a PR based on Proposal A and Go implementation? |
@open-telemetry/technical-committee FYI this is blocking Go stability of the Logs signal. |
[Meta] I am commenting to help with the process, not to express an opinion about the proposed approaches.
@pellared We definitely need a PR with a proposal and then if it gets approved we need to merge that PR to the spec in Development status first. To emphasize again, I don't have an opinion about which of the proposed approaches we should go ahead with (I think I can see 3 different proposals in this thread). |
@MrAlias I think based on the agreed process we have to do this:
I think you are saying this issue is blocking Step 5, which of course is true, I just want to make it clear there are a few additional steps before we get there. If you are expecting something else to happen then I may be misunderstanding what you are asking for. P.S. The requirement for 3 language implementations was discussed but I still don't see it anywhere in the spec. We need to add it. |
Please also see C++ and Rust |
Yeah, not expecting a direct jump. Just looking for continued progress here. |
There have been TC members taking an active role in blocking progress here:
I would appreciate a more active role from the TC in progressing this initiative. Is it possible to add @pellared to the TC? |
@tigrannajaryan, I am not working on PR as the label is
I do not want to waste the time of the reviewers and contributors until it is clear agreement that we can work on it. PS. I am not blaming anyone. There still was (and still is) many other issues that was waiting for my time 😉
@MrAlias, the mentioned issue and PR was related to metrics. I do not see anyone actively blocking this issue. |
I've been blocking progress on the grounds of an incomplete proposal. I'll happily concede (though I'll still disagree) if the consensus is that we can / should evolve the API features independently of how those will be used in the SDK.
Here's my take: While I understand why you might want the Logger, Meter, Tracer enabled methods to accept Let's take stock of some of the API / SDK facilities we have which are related to this problem:
If we extend Logger#enabled, Instrument#enabled to accept Context, then it follows that we should do the same for Tracer#enabled for the sake of symmetry. Adding a Context argument to these #enabled methods suggests TracerConfig, MeterConfig, LoggerConfig would be evolved to have some sort of feature where users can provide a function which accepts Context (and maybe other parameters) and return a boolean indicating whether the Logger, Instrument, Tracer is enabled. If the SDK corollary to the API isn't TracerConfig, MeterConfig, LoggerConfig, we'll have to make up some new concept, which I think we should try to avoid. Adding a programmable extension point to TracerConfig, MeterConfig, LoggerConfig is a sloppy design. Users now have to choose between writing code which turns things on and off in response to context in either a sampler, or in the {Signal}Config. When is it appropriate to use one or another? A cleaner design is to:
The benefit of keeping TracerConfig, MeterConfig, LoggerConfig static is that static config lends itself to declarative configuration, where as breaking this principle and accepting user-provided functions encourages bespoke, programmatic configuration. While more flexible, writing custom functions is a worse user experience. If we can carve out a portion of configuration which is static and rule-based, the ecosystem will have better user ergonomics and language interoperability in the long run. As for what a missing log sampler might look like, we need it to have some symmetry to trace SDK sampler, but also reflect the differences between logging and tracing. In particular, spans have a start and end stage, and trace Sampler can only receive the bits of the span available at span start. In contrast, all bits of a log record are known at a time when it is omitted. Reflecting these differences, log sampler's might consist of:
One thing that's missing is the equivalent programmable extension point for metrics. I would solve this by introducing something like what's been called a measurement processor. An extension point that allows users to intercept all measurements before they're aggregated and do things like: transform value (i.e. unit conversions), drop, enrich with attributes from context, baggage. If this type of capability existed, a measurement processor could make decisions on the basis of Context, which would surely be an argument provided as part of the measurement. Taking a step back, the proposal is to add a Context argument to the Logger#enabled, Instrument#enabled, Tracer#enabled methods. Since we make no guarantee that these methods are called by instrumentation, the only purpose they can possibly serve is to improve performance. By adding Context as an argument, we're implying some corresponding programmable SDK capability to respond based on the contents of Context. But its antithetical to call a user-provided function on an operation that is supposed to be blazing fast and eliminate overhead. Let's do the sensible thing and establish a design principle where Tracer#enabled, Instrument#enabled, and Logger#enabled are always statically resolved. |
I am not sure what "clean'" design means here but I think that the config design misses the fundamental functionality of supporting different log record processing pipelines. E.g. #4207 (comment) or different processing for events and bridged log records.
I find this statement as opinionated. Static configuration is less flexible and will be limiting the users.
I find this statement as not correct. Does it mean that we do not allow any customization for something that is supposed to be fast? Does it mean that the design of https://pkg.go.dev/log/slog or https://docs.rs/log/latest/log/ is antithetical? I never proposed a dynamically configured loggerconfig. My preferred proposal is to extend LogRecordProcessor with OnEnabled. We are able to model a lot of different log pipelines using this design that are not possible with other designs. We have this already implemented since a few months. So far we received none negative feedback regarding this aspect of OTel Go Logs API and SDK.
I think symmetry is good as a guideline. However, it should never be a requirement especially that each signal has its own unique properties and use cases. |
Yes. I'm taking an opinionated stance on what I think the design should be. Taking away flexibility (or put another way, adding guardrails) can be of great benefit to the user experience. After all, the removal of flexibility is what gives programming languages increased productivity and safety.
Still results in the ability for user code to run on the hot path. And the function has to run twice, right? Once when To be honest, the design to prioritize multiple independent logging pipelines, each with their own filter, doesn't resonate with me. Its too different from the other signals, and adds complexity for what I consider to be a niche use case. I think those types of use cases are better served by the collector, instead of adding the burden to all the language implementations.
Presumably there's parts of those systems which encourage the pattern of calling user code in a similar scenario as our Logger#enabled operation? If so, I'm not familiar with those systems and can't comment. This is all my personal opinion. If there's a consensus among community members to go in a different direction, the community should take that direction. |
Not sure whether calling "encourage" is a proper term in this context. I would say that it enables calling user code in a similar scenario. Regarding proposal A we have implementations in 2 languages (Go, Rust, yet the designs have little differences in details). There is also a precedence of a similar design in core logging libraries/facades in different languages (Go, Rust, .NET). I feel that adding such capability to the SDK is important as otherwise users would decorate the Logs API (e.g. open-telemetry/opentelemetry-go#5830) to achieve the similar functionality which would make the composition less user-friendly. It may also decrease the performance as it will introduce more overhead and other unwanted side-effects (e.g. more debug logging). Such design would lead to a situation that for complex pipelines the user would have to compose providers (instead of processors). |
We had chat with @tigrannajaryan. Here is a summary:
We feel that at this stage an OTEP would be a better format to follow-up. |
Context: #4203 (comment)
Blocked by:
The text was updated successfully, but these errors were encountered: