补充指南

注意:本文档**不是**规范,它旨在支持日志 APISDK 规范,它**不**对现有规范增加任何额外要求。

用法

如何创建 Log4J 日志附加器

日志附加器实现可以用于将日志桥接到 OpenTelemetry 的 日志 SDKLogRecordExporter 中。这种方法通常用于对日志传输方式进行更改后仍能正常工作的应用程序,并且是 支持的 日志收集方法之一。

日志附加器实现通常会获取一个 Logger,然后为应用程序接收到的 LogRecord 调用 Emit LogRecord

隐式上下文注入显式上下文注入 描述了 Appender 如何将 TraceContext 注入 LogRecord 中。

Appender

同样的方法也可以用于,例如:

  • 通过创建 Handler 来处理 Python 的 logging 库。
  • 通过实现 Core 接口来处理 Go 的 zap logging 库。请注意,由于 Go 中不存在隐式上下文,因此无法获取和使用活动的 Span。

日志附加器可以由 OpenTelemetry 语言库的维护者创建,也可以由第三方为支持类似扩展机制的任何日志库创建。本规范建议每个 OpenTelemetry 语言库都包含至少一个流行日志库的开箱即用型 Appender 实现。

Logger 名称

如果日志库具有与 OpenTelemetry 对 Instrumentation Scope 名称的定义相似的概念,则 Appender 的实现应使用该值作为参数,在 获取 Logger 时使用。

这适用于例如:

Appender 应避免设置 Instrumentation Scope 的任何属性。这样做可能会导致不同的 Appender 为具有相同 Logger 标识的同一个 Instrumentation Scope 设置不同的属性,根据规范,这是错误的。

上下文

隐式上下文注入

当上下文隐式可用时(例如在 Java 中),Appender 可以依赖于自动上下文传播,即在调用 emit a LogRecord 时**不**显式设置 Context

一些日志库具有专门用于将上下文信息注入日志的机制,例如 Log4j 中的 MDC。如果可用,最好使用这些机制来设置 Context。然后,日志附加器可以获取 Context 并在调用 emit a LogRecord 时显式设置它。这可以确保即使在日志记录异步发出时也能包含正确的 Context,否则否则可能导致 Context 不正确。

待办:澄清当日志语句的调用站点和日志附加器在不同线程上执行时,其工作方式或不工作方式。

显式上下文注入

为了使 TraceContext 在需要显式提供 Context 的语言(例如 Go)的 LogRecord 中被记录,最终用户必须捕获 Context 并将其显式传递给日志子系统。日志附加器必须获取此 Context 并在调用 emit a LogRecord 时显式设置它。

对这些语言中的 OpenTelemetry 日志库的支持通常可以实现为 logger 包装器,这些包装器可以在 span 创建时捕获一次 Context,然后使用包装后的 logger 以正常方式执行日志语句。包装器将负责将捕获的 Context 注入日志中。

本规范没有明确定义具体实现方式,因为实际机制取决于语言和所使用的特定日志库。无论如何,预计包装器将利用 Trace Context API 来获取当前活动的 span。

请参阅 示例,了解如何在 Go 的 zap logging 库中实现。

高级处理

日志处理的实现方式和地点有很多,包括:

  1. OpenTelemetry Collector
  2. OpenTelemetry Logs SDK
  3. 桥接的日志库
  4. 后端可观测性系统

每种方法都有不同的优缺点。本节重点介绍如何使用 OpenTelemetry Logs SDK 进行多处理器的高级复合处理。

以下是使用 Logs SDK 构建自定义日志处理的示例。这些示例以 Go Logs SDK 为例进行说明。请查阅特定语言的文档以确定在其他语言中的可行性。

修改

可以通过修改传递给处理器的日志记录来修改日志记录。下面是一个用于从日志记录属性中删除令牌的处理器示例。

import (
	"context"
	"strings"

	"go.opentelemetry.io/otel/log"
	sdklog "go.opentelemetry.io/otel/sdk/log"
)

// RedactTokensProcessor redacts values from attributes
// that contain "token" in the key.
type RedactTokensProcessor struct{}

// OnEmit redacts values from attributes containing "token" in the key
// by replacing them with "REDACTED".
func (p *RedactTokensProcessor) OnEmit(ctx context.Context, record *sdklog.Record) error {
	record.WalkAttributes(func(kv log.KeyValue) bool {
		if strings.Contains(strings.ToLower(kv.Key), "token") {
			record.AddAttributes(log.String(kv.Key, "REDACTED"))
		}
		return true
	})
	return nil
}

过滤

可以通过 装饰 处理器来实现过滤。例如,下面是如何基于 Severity 进行过滤。

import (
	"context"

	"go.opentelemetry.io/otel/log"
	sdklog "go.opentelemetry.io/otel/sdk/log"
)

// SeverityProcessor filters out log records with severity below the given threshold.
type SeverityProcessor struct {
	sdklog.Processor
	Min log.Severity
}

// OnEmit passes ctx and record to the wrapped sdklog.Processor
// if the record's severity is greater than or equal to p.Min.
// Otherwise, the record is dropped (the wrapped processor is not invoked).
func (p *SeverityProcessor) OnEmit(ctx context.Context, record *sdklog.Record) error {
	if record.Severity() < p.Min {
		return nil
	}
	return p.Processor.OnEmit(ctx, record)
}

// Enabled returns false if the severity is lower than p.Min.
func (p *SeverityProcessor) Enabled(ctx context.Context, param sdklog.EnabledParameters) bool {
	if param.Severity < p.Min {
		return false
	}
	if fp, ok := p.Processor.(sdklog.FilterProcessor); ok {
		// The wrapped processor is also a filtering processor.
		return p.Processor.Enabled(ctx, param)
	}
	return true
}
注意

如前所述,这些示例仅用于说明目的。Go 开发人员可以利用 go.opentelemetry.io/contrib/processors/minsev 模块。

隔离

可以通过 组合 处理器来支持隔离处理管道。下面的示例演示了一个隔离的处理器,它确保每个处理器在日志记录的副本上进行操作。

import (
	"context"
	"errors"

	"go.opentelemetry.io/otel/sdk/log"
)

// IsolatedProcessor composes multiple processors so that they are isolated.
type IsolatedProcessor struct {
	Processors []log.Processor
}

// OnEmit passes ctx and a clone of record to the each wrapped sdklog.Processor.
func (p *IsolatedProcessor) OnEmit(ctx context.Context, record *log.Record) error {
	var rErr error
	r := record.Clone()
	for _, proc := range p.Processors {
		if err := proc.OnEmit(ctx, &r); err != nil {
			rErr = errors.Join(rErr, err)
		}
	}
	return rErr
}

// Enabled honors Enabled of the wrapped processors.
func (p *IsolatedProcessor) Enabled(ctx context.Context, param sdklog.EnabledParameters) bool  {
	fltrProcessors := make([]sdklog.FilterProcessor, len(p.Processors))
	for i, proc := range p.Processors {
		fp, ok := proc.(sdklog.FilterProcessor)
		if !ok {
			// Processor not implementing Enabled.
			// We assume it will be processed.
			return true
		}
		fltrProcessors[i] = fp
	}

	for _, proc := range fltrProcessors {
		if proc.Enabled(ctx, param) {
			// At least one Processor will process the Record.
			return true
		}
	}
	// No processor will process the record.
	return false
}

路由

可以通过以不同的方式组合处理器来实现附加功能,例如路由或扇出。以下示例演示了一个将事件记录与日志记录分开路由的处理器。

import (
	"context"

	"go.opentelemetry.io/otel/sdk/log"
)

// LogEventRouteProcessor routes log records and event records separately.
type LogEventRouteProcessor struct {
	LogProcessor log.Processor
	EventProcessor log.Processor
}

// OnEmit calls EventProcessor if the record has a non-empty event name.
// Otherwise, it calls LogProcessor.
func (p *LogEventRouteProcessor) OnEmit(ctx context.Context, record *log.Record) error {
	if record.EventName() != "" {
		return p.EventProcessor.OnEmit(ctx, record)
	}
	return p.LogProcessor.OnEmit(ctx, record)
}

// Enabled honors Enabled of the wrapped processors.
func (p *LogEventRouteProcessor) Enabled(ctx context.Context, param sdklog.EnabledParameters) bool  {
	fp1, ok := p.EventProcessor.(sdklog.FilterProcessor)
	if !ok {
		// Processor not implementing Enabled.
		return true
	}
	fp2, ok := p.LogProcessor.(sdklog.FilterProcessor)
	if !ok {
		// Processor not implementing Enabled.
		return true
	}

	if fp1.Enabled(ctx, param) {
		return true
	}
	if fp2.Enabled(ctx, param) {
		return true
	}
	// No processor will process the record.
	return false
}

设置

以下示例设置了使用上述所有处理器进行的日志处理。

import (
	"context"

	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
	"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
	"go.opentelemetry.io/otel/log"
	sdklog "go.opentelemetry.io/otel/sdk/log"
)


// NewLoggerProvider creates a new logger provider.
// - Event records with severity not lower than Info are exported via OTLP.
// - Event records have token attributes redacted.
// - Non-event log records with severity not lower than Debug are synchronously emitted to stdout.
func NewLoggerProvider() (*sdklog.LoggerProvider, error) {
	// Events processing setup.
	otlpExp, err := otlploghttp.New(context.Background())
	if err != nil {
		return nil, err
	}
	evtProc := &SeverityProcessor{
		// This processing is isolated so that there is no chance
		// that log records send to stdout have their token attributes redacted.
		Processor: &IsolatedProcessor{
			Processors: []sdklog.Processor{
				&RedactTokensProcessor{},
				sdklog.NewBatchProcessor(otlpExp),
			},
		},
		Min: log.SeverityInfo,
	}

	// Logs processing setup.
	stdoutExp, err := stdoutlog.New()
	if err != nil {
		return nil, err
	}
	logProc := &SeverityProcessor{
		Processor: sdklog.NewSimpleProcessor(stdoutExp),
		Min:       log.SeverityDebug,
	}

	// Create logs provider with log/event routing.
	provider := sdklog.NewLoggerProvider(
		sdklog.WithProcessor(&LogEventRouteProcessor{
			LogProcessor:   logProc,
			EventProcessor: evtProc,
		}),
	)
	return provider, nil
}