# ログの補足ガイドライン

> Source: https://www.ymotongpoo.com/works/otel-specs-ja/spec/logs/supplementary-guidelines/


注: この文書は仕様ではなく、ログの[API](../api/)と[SDK](../sdk/)の仕様を補足するために提供されています。既存の仕様に追加の要件を課すものではありません。

## 使用方法

### How to Create a Log4J Log Appender

[ログアペンダー](/works/otel-specs-ja/spec/glossary/#log-appender--bridge)の実装を使って、ログを[ログSDK](../sdk/)のOpenTelemetry[LogRecordExporter](../sdk/#logrecordexporter)へ橋渡しできます。このアプローチは、通常、ログの転送方法を変更してかまわないアプリケーションで使われ、[サポートされているログ収集方法の1つ](../#collectorへの直接送信)です。

ログアペンダーの実装は、通常[Logger](../api/#logger)を取得し、アプリケーションから受け取った`LogRecord`について[LogRecordの発行](../api/#logrecordの発行)を呼び出します。

[暗黙のコンテキスト注入](#暗黙のコンテキスト注入)と[明示的なコンテキスト注入](#明示的なコンテキスト注入)は、アペンダーが`TraceContext`を`LogRecord`へ注入する方法を説明しています。

![アペンダーの図](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/logs/img/appender.png)

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

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

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

### Logger Name

ロギングライブラリに、OpenTelemetryの[Instrumentation Scope](/works/otel-specs-ja/spec/common/instrumentation-scope/)の名前の定義に類似した概念がある場合、アペンダーの実装は、[Loggerの取得](../api/#loggerの取得)時のnameパラメータとしてその値を使うべきです。

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

- [Log4J](https://javadoc.io/doc/org.apache.logging.log4j/log4j-api/latest/org.apache.logging.log4j/org/apache/logging/log4j/Logger.html)のLogger name
- [Monolog](https://github.com/Seldaek/monolog/blob/3.4.0/doc/01-usage.md#leveraging-channels)のChannel name

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

### コンテキスト

#### 暗黙のコンテキスト注入

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

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

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

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

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

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

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

Go言語のzapロギングライブラリでこれをどのように実現できるかについては[例](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/aeb198d6de25588afef49545cfa06533d0e67f1d/bridges/otelzap/core.go#L194-L244)を参照してください。

### Advanced Processing

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

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

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

以下は、[ログSDK](../sdk/)を使ってカスタムのログ処理を構築する方法の例です。これらの例は、説明のために[Go言語のログSDK](https://pkg.go.dev/go.opentelemetry.io/otel/sdk/log)を使っています。他の言語での実現可能性については、その言語固有のドキュメントを参照してください。

<!-- markdownlint-disable no-hard-tabs -->

#### 変更

ログレコードは、プロセッサーに渡されたログレコードを変更することで書き換えられます。以下は、ログレコードの属性からトークンをマスクするプロセッサーの例です。

```go
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
}
```

#### フィルタリング

フィルタリングは、プロセッサーを[デコレート](https://refactoring.guru/design-patterns/decorator)することで実現できます。例えば、Severityに基づくフィルタリングは次のように実現できます。

```go
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`](https://pkg.go.dev/go.opentelemetry.io/contrib/processors/minsev)
> モジュールを利用できます。

#### 分離

処理パイプラインの分離は、プロセッサーを[合成](https://refactoring.guru/design-patterns/composite)することでサポートできます。以下の例は、各プロセッサーがログレコードのコピーに対して動作することを保証する、分離されたプロセッサーを示しています。

```go
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
}
```

#### ルーティング

ルーティングやファンアウトといった追加のケーパビリティは、プロセッサーを異なる方法で組み合わせることで実装できます。以下の例は、イベントレコードをログレコードとは別にルーティングするプロセッサーを示しています。

```go
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
}
```

#### セットアップ

以下の例は、これまでに説明したすべてのプロセッサーを使ってログ処理を設定します。

```go
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
}
```

<!-- markdownlint-enable no-hard-tabs -->

- [OTEP0150 ロギングライブラリSDKプロトタイプ仕様](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.60.0/oteps/logs/0150-logging-library-sdk.md)

