Skip to content

System for managing own telemetry attributes within pipeline components #12217

Closed
@djaglowski

Description

@djaglowski

Some types of components are fully or partially shared across multiple instances. (e.g. otlp receiver / memory limiter)

I have previously pursued some optimistic models in which the collector would define certain ways in which sharing is "supported", but the reality is that sharing is an unbounded problem. e.g. Someone could write a processor that shares a data structure between logs and traces pipelines only if it's instantiated on a Wednesday and the component name is "foo". I am convinced that there are too many novel cases to model ahead of time, so here is a proposal to normalize attributes as much as possible, while still allowing component authors to both add or subtract attributes from their telemetry when appropriate.

I will use Loggers as the example here, but I believe the same general ideas can be applied to TracerProviders and MeterProviders.


General Idea:

Create a "Logger" struct specifically for use within components. This logger satisfies the same interface and behaves essentially the same as what we have today. However, it has the following important differences:

  1. The attributes are preset for the component as if it uses no sharing of parts with other instances of the component. (As defined in this RFC)
  2. A Without(key ...string) method is provided, which allows component authors to remove attributes as appropriate for portions of the code that are shared. This is opposite the typical pattern used by loggers, but necessary if we want our default set of attributes to be correct.

For example:

  • OTLP receiver: Since the entire component struct is shared, the factory will use Without("otelcol.signal") during construction to create a logger appropriate for the shared instance.
  • Memory limiter processor: For any code outside of the shared part, it can use the logger which it was given. For the shared part, is can create a new logger by calling Without("otelcol.signal", "otelcol.component.id", "otelcol.pipeline.id").
  • Custom component mentioned above: Typically uses the given logger, but if creating a logs or traces instance on a Wednesday and the component name is "foo", then it calls Without("otelcol.signal", "otelcol.component.id") to adjust the scope.

This implementation looks something like this:

import (
	"go.opentelemetry.io/otel/attribute"
	"go.uber.org/zap"
)

type ComponentLogger struct {
	*zap.Logger
	root *zap.Logger
	diff []attribute.KeyValue
}

func NewComponentLogger(root *zap.Logger, defaultAttrs map[string]string) *ComponentLogger {
	defaultLogger := root
	for k, v := range defaultAttrs {
		defaultLogger = defaultLogger.With(zap.String(k, v))
	}

	return &ComponentLogger{
		Logger: defaultLogger,
		root:   root,
		diff:   defaultAttrs,
	}
}

func (cl *ComponentLogger) Without(keys ...string) *zap.Logger {
	excludeKeys := make(map[string]struct{})
	for _, key := range keys {
		excludeKeys[key] = struct{}{}
	}

	logger := cl.root
	for _, attr := range cl.diff {
		if _, excluded := excludeKeys[string(attr.Key)]; !excluded {
			logger = logger.With(zap.String(k, v))
		}
	}
	return logger
}

func (cl *ComponentLogger) With(fields ...zap.Field) *ComponentLogger {
	// Create new diff by appending the new fields as attributes
	newDiff := make([]attribute.KeyValue, len(cl.diff))
	copy(newDiff, cl.diff)
	for _, f := range fields {
		newDiff = append(newDiff, attribute.Any(f.Key, f.Interface))
	}

	return &ComponentLogger{
		Logger: cl.Logger.With(fields...),
		root:   cl.root,
		diff:   newDiff,
	}
}

TracerProvider and MeterProvider can expose a similar With/Without interface, but the implementation will be different.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

Status

Done

Relationships

None yet

Development

No branches or pull requests

Issue actions