Skip to content
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

Open
pellared opened this issue Sep 10, 2024 · 11 comments
Open

Specify how Logs SDK implements Enabled #4207

pellared opened this issue Sep 10, 2024 · 11 comments
Labels
spec:logs Related to the specification/logs directory triage:deciding:community-feedback

Comments

@pellared
Copy link
Member

pellared commented Sep 10, 2024

Context: #4203 (comment)

Blocked by:

@pellared
Copy link
Member Author

pellared commented Sep 16, 2024

Proposal A - Extend Processor

Below is a proposal mostly based on how Go Logs SDK currently implements Logger.Enabled.
Spike: open-telemetry/opentelemetry-go#5816

The idea is to add an OnEnabled operation to the LogRecordProcessor.

The parameters of OnEnabled would be defined in the same way as Logger.Enabled. However, it is important to notice that in future the parameters may independently change (for instance, the LogRecordProcessor.OnEnabled may start accepting an instrumentation scope to allow dynamic filtering of loggers by name). Still, I propose to start with the minimal set of parameters.

The return value of OnEnabled would be defined in the same way as Logger.Enabled.

Implementation of OnEnabled operation is optional so that the specification and existing processors implementations remain backwards compatible. If a LogRecordProcessor does not implement OnEnabled then it defaults to returning true.

When the user calls Logger.Enabled, the SDK would call OnEnabled in the same order as processors were registered until any of the processors' returns true. Logger.Enabled returns false if none of registered processors returns true. Pseudocode:

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 wrapping/decorating the processors is already used by the isolating processor.

Personal remarks:
I start to feel that the isolating processors should be removed from the specification. Users can always create multiple processing pipelines by creating a logger (tracer/meter) provider composite (in Go we would call it MultiProvider, it could be also called Fanout Provider) which delegates the calls to wrapped providers. I find that it would be more cohesive with the existing design than creating decorators for processors or fan-out processors.
I am going to work on another proposal(s) which be more inspired on tracing sampling. I want to coin a simpler filtering mechanism - a new abstraction called Filterer that would be used by SDK's Logger in both Emit and Enabled implementations.

@pellared
Copy link
Member Author

pellared commented Sep 17, 2024

Proposal B - Add filtering via Filterer

Below is a proposal inspired by sampling design in tracing SDK.
Spike: open-telemetry/opentelemetry-go#5825

The idea is to add a new abstraction named Filterer with a Filter method.

The parameters of Filter would be defined in the same way as Logger.Enabled. However, it is important to notice that in future the parameters may independently change (for instance, the Filterer.Filter may start accepting an instrumentation scope to allow dynamic filtering of loggers by name). Still, I propose to start with the minimal set of parameters.

The return value of Filter would be defined in the same way as Logger.Enabled.

Pseudocode:

type Filterer interface {
	Filter(ctx context.Context, param FilterParameters) bool
}

When the user calls Logger.Enabled, the SDK would call Filter in the same order as filterers were registered until any processor returns false (then it breaks and returns false). Logger.Enabled returns true if none of registered filterers returned false. Pseudocode:

// 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 Logger.Emit, the SDK would first check if the record should be filtered out. Pseudocode:

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
}

This proposal assumes that each logger provider is used to define a separate processing pipeline. It is more aligned with the current specification design for all signals.

Filtering is coupled to emitting log records. Therefore, a custom processor filtering implementation does not need to implement filtering on both OnEmit and OnEnabled as in Proposal A. It makes also makes defining filters easier for users.

At last such design should be easier to be added to existing SDKs as it adds a new abstraction instead of adding more responsibilities to Processor.

@MSNev
Copy link

MSNev commented Sep 17, 2024

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...

@MSNev
Copy link

MSNev commented Sep 17, 2024

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.
If an end-user application wants to do something more complicated then the "better" option would be to have "them" (not OTel) implement their own "Filtered" logger thingy.

@pellared
Copy link
Member Author

pellared commented Sep 17, 2024

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 am not sure if I follow.

Logger.Enabled gives the possibility to for the user to check if it is even necessary to construct and emit a big and expensive log record.

Which part of the implementation proposal (A or B or both) are you concerned about?

@pellared
Copy link
Member Author

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.
If an end-user application wants to do something more complicated then the "better" option would be to have "them" (not OTel) implement their own "Filtered" logger thingy.

Agree. PTAL #4203 where I try to propose a minimal set of parameters.

@MSNev
Copy link

MSNev commented Sep 19, 2024

I am not sure if I follow.

Logger.Enabled gives the possibility to for the user to check if it is even necessary to construct and emit a big and expensive log record.

Which part of the implementation proposal (A or B or both) are you concerned about?

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.

@MrAlias
Copy link
Contributor

MrAlias commented Sep 19, 2024

@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

  1. https://github.com/open-telemetry/opentelemetry-specification/pull/4203#pullrequestreview-2292967830

@jack-berg
Copy link
Member

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 EnabledParameters, since requiring users to create a new struct / object to check if code should proceed to emitting a log seems non-optimal from a performance and ergonomics standpoint. I understand the desire to want to retain flexibility to evolve the API, but I think getting the arguments right in the initial stable version and not trying to retain the ability to evolve is likely to produce the most ergonomic API.

@pellared
Copy link
Member Author

pellared commented Sep 20, 2024

What about adding a severity level as part of the logger config?

I think that it is reasonable to improve the logging configuration than introducing a new concept.
It also does not mean that a feature like "filterers" cannot be added in future to allow more flexibility/customization if there would be a bigger community demand.

It seems to match the common log workflow of changing severity threshold for specific loggers.

I agree. I assume that setting a global severity threshold is also a common workflow.

I'm suspicious of the idea of introducing the concept of an expandable set of EnabledParameters, since requiring users to create a new struct / object to check if code should proceed to emitting a log seems non-optimal from a performance and ergonomics standpoint. I understand the desire to want to retain flexibility to evolve the API, but I think getting the arguments right in the initial stable version and not trying to retain the ability to evolve is likely to produce the most ergonomic API.

PTAL at #4203 (comment)

@XSAM
Copy link
Member

XSAM commented Sep 20, 2024

Per discussion with @pellared, if the logger can have an Enabled method, we probably don't need an Enabled method for the processor, as the logger can solve the memory allocation optimization at the beginning of the logging workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
spec:logs Related to the specification/logs directory triage:deciding:community-feedback
Projects
Status: Blocked
Development

No branches or pull requests

6 participants