ログの補足ガイドライン

この記事は英語の原文を日本語に翻訳したものです。原文: https://opentelemetry.io/docs/specs/otel/logs/supplementary-guidelines/

翻訳元: open-telemetry/opentelemetry-specification v1.60.0(コミット 29ae8c7

注: この文書は仕様ではなく、ログのAPISDKの仕様を補足するために提供されています。既存の仕様に追加の要件を課すものではありません。

使用方法

How to Create a Log4J Log Appender

ログアペンダーの実装を使って、ログをログSDKのOpenTelemetryLogRecordExporterへ橋渡しできます。このアプローチは、通常、ログの転送方法を変更してかまわないアプリケーションで使われ、サポートされているログ収集方法の1つです。

ログアペンダーの実装は、通常Loggerを取得し、アプリケーションから受け取ったLogRecordについてLogRecordの発行を呼び出します。

暗黙のコンテキスト注入明示的なコンテキスト注入は、アペンダーがTraceContextLogRecordへ注入する方法を説明しています。

アペンダーの図

同じアプローチは、例えば次のような場合にも使えます。

  • Handlerを作成することによるPythonのロギングライブラリ。
  • Coreインターフェースを実装することによるGoのzapロギングライブラリ。Goには暗黙のContextがないため、アクティブなSpanを取得して使うことはできない点に注意してください。

ログアペンダーは、OpenTelemetryのメンテナーによってOpenTelemetryの各言語ライブラリ内に作成されることもあれば、類似の拡張機構をサポートする任意のロギングライブラリに対してサードパーティによって作成されることもあります。本仕様書は、少なくとも1つの人気のロギングライブラリについて、すぐに使えるアペンダー実装を各OpenTelemetry言語ライブラリに含めることを推奨します。

Logger Name

ロギングライブラリに、OpenTelemetryのInstrumentation Scopeの名前の定義に類似した概念がある場合、アペンダーの実装は、Loggerの取得時のnameパラメータとしてその値を使うべきです。

これは例えば次のようなケースに当てはまります。

アペンダーは、Instrumentation Scopeのいずれの属性も設定しないようにすべきです。設定してしまうと、Loggerの同一のアイデンティティで取得された同じInstrumentation Scopeに対して、異なるアペンダーが異なる属性を設定する結果になる可能性があり、これは仕様上エラーとなります。

コンテキスト

暗黙のコンテキスト注入

Contextが暗黙的に利用可能な場合(Javaなど)、アペンダーはLogRecordの発行を呼び出す際にContextを明示的に設定しないことで、自動的なコンテキスト伝搬に依存できます。

一部のロギングライブラリには、Log4jのMDCのように、コンテキスト情報をログに注入するために特別に調整された機構があります。利用可能な場合は、こうした機構を使ってContextを設定する方が望ましいことがあります。ログアペンダーは、そのContextを取得し、LogRecordの発行を呼び出す際に明示的に設定できます。これにより、ログレコードが非同期に発行される場合でも正しいContextを含められます。そうしなければ、Contextが誤ったものになることがあります。

TODO: ログ呼び出しのコールサイトとログアペンダーが異なるスレッドで実行される場合の動作について明確化する。

明示的なコンテキスト注入

Contextを明示的に渡さなければならない言語(Goなど)でTraceContextLogRecordに記録するには、エンドユーザーがContextを取得し、ロギングサブシステムへ明示的に渡さなければなりません。ログアペンダーは、このContextを受け取り、LogRecordの発行を呼び出す際に明示的に設定しなければなりません。

これらの言語のロギングライブラリに対するOpenTelemetryのサポートは、通常、スパンが作成された時点で一度だけコンテキストを取得し、その後はラップされたロガーを通常の方法で使ってログ文を実行するロガーラッパーの形で実装できます。ラッパーは、取得したコンテキストをログに注入する責任を負います。

本仕様書は、実際の機構が言語や使用する特定のロギングライブラリに依存するため、それがどのように実現されるかを正確には定義しません。いずれの場合も、ラッパーは現在アクティブなスパンを取得するためにTrace Context APIを使うことが期待されます。

Go言語のzapロギングライブラリでこれをどのように実現できるかについてはを参照してください。

Advanced Processing

ログ処理を実装する方法と場所には多くの選択肢があり、次のようなものが含まれます。

  1. OpenTelemetry Collector
  2. OpenTelemetryログSDK
  3. 橋渡しされたロギングライブラリ
  4. バックエンドのオブザーバビリティシステム

それぞれのアプローチには異なる利点と欠点があります。この節では、OpenTelemetryログSDKを使って複数のプロセッサーによる高度な複合処理を行う方法に焦点を当てます。

以下は、ログSDKを使ってカスタムのログ処理を構築する方法の例です。これらの例は、説明のためにGo言語のログ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
}

[!NOTE] 前述の通り、これらの例は説明のためのものです。 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
}