計装

OpenTelemetry Goのマニュアルインストルメンテーション

Instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

セットアップ

トレース

トレーサーの取得

スパンを作成するには、まずトレーサーを取得または初期化する必要があります。

適切なパッケージがインストールされていることを確認してください。

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/trace \
  go.opentelemetry.io/otel/sdk \

次に、エクスポーター、リソース、トレーサープロバイダー、最後にトレーサーを初期化します。

package app

import (
	"context"
	"fmt"
	"log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.32.0"
	"go.opentelemetry.io/otel/trace"
)

var tracer trace.Tracer

func newExporter(ctx context.Context)  /* (someExporter.Exporter, error) */ {
	// お好みのエクスポーター: console、jaeger、zipkin、OTLPなど
}

func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
	// デフォルトのSDKリソースと必要なサービス名が設定されていることを確認します
	r, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("ExampleService"),
		),
	)

	if err != nil {
		panic(err)
	}

	return sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(r),
	)
}

func main() {
	ctx := context.Background()

	exp, err := newExporter(ctx)
	if err != nil {
		log.Fatalf("failed to initialize exporter: %v", err)
	}

	// バッチスパンプロセッサーと指定されたエクスポーターで新しいトレーサープロバイダーを作成します
	tp := newTracerProvider(exp)

	// 何もリークしないようにシャットダウンを適切に処理します
	defer func() { _ = tp.Shutdown(ctx) }()

	otel.SetTracerProvider(tp)

	// 最後に、このパッケージで使用できるトレーサーを設定します
	tracer = tp.Tracer("example.io/package/name")
}

これでtracerにアクセスして、コードを手動計装できます。

スパンの作成

スパンはトレーサーによって作成されます。初期化されていない場合は、それを行う必要があります。

トレーサーでスパンを作成するには、context.Contextインスタンスのハンドルも必要です。 これらは通常、リクエストオブジェクトなどから取得され、計装ライブラリからの親スパンを既に含んでいる場合があります。

func httpHandler(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracer.Start(r.Context(), "hello-span")
	defer span.End()

	// hello-spanで追跡する作業を行います
}

Goでは、contextパッケージがアクティブなスパンを格納するために使用されます。 スパンを開始すると、作成されたスパンだけでなく、それを含む変更されたコンテキストのハンドルも取得できます。

スパンが完了すると、それは不変になり、もはや変更できません。

現在のスパンを取得

現在のスパンを取得するには、ハンドルを持っているcontext.Contextからそれを取り出す必要があります。

// このコンテキストは、抽出予定のアクティブなスパンを含む必要があります
ctx := context.TODO()
span := trace.SpanFromContext(ctx)

// 現在のスパンで何かを行い、オプションで終了したい場合は`span.End()`を呼び出します

これは、特定のタイミングで現在のスパンに情報を追加したい場合に役立ちます。

ネストしたスパンの作成

ネストした操作で作業を追跡するために、ネストしたスパンを作成できます。

ハンドルを持っている現在のcontext.Contextに既にスパンが含まれている場合、新しいスパンを作成するとそれがネストしたスパンになります。 例を挙げましょう。

func parentFunction(ctx context.Context) {
	ctx, parentSpan := tracer.Start(ctx, "parent")
	defer parentSpan.End()

	// 子関数を呼び出し、そこでネストしたスパンを開始します
	childFunction(ctx)

	// より多くの作業を行います - この関数が終了すると、parentSpanが完了します
}

func childFunction(ctx context.Context) {
	// `childFunction()`を追跡するスパンを作成します - これは親が`parentSpan`であるネストしたスパンです
	ctx, childSpan := tracer.Start(ctx, "child")
	defer childSpan.End()

	// ここで作業を行い、この関数が戻ると、childSpanが完了します
}

スパンが完了すると、それは不変になり、もはや変更できません。

スパン属性

属性は、スパンにメタデータとして適用されるキーと値であり、トレースの集約、フィルタリング、グループ化に便利です。 属性はスパン作成時、または完了前のスパンのライフサイクル中の他の任意の時点で追加できます。

// 作成時に属性を設定...
ctx, span = tracer.Start(ctx, "attributesAtCreation", trace.WithAttributes(attribute.String("hello", "world")))
// ... そして作成後に
span.SetAttributes(attribute.Bool("isTrue", true), attribute.String("stringAttr", "hi!"))

属性キーは事前に計算することもできます。

var myKey = attribute.Key("myCoolAttribute")
span.SetAttributes(myKey.String("a value"))

セマンティック属性

セマンティック属性は、HTTPメソッド、ステータスコード、ユーザーエージェントなどの一般的な概念について、複数の言語、フレームワーク、ランタイム間で共有される属性キーのセットを提供するためにOpenTelemetry仕様によって定義された属性です。 これらの属性はgo.opentelemetry.io/otel/semconv/v1.32.0パッケージで利用できます。

詳細については、トレースセマンティック規約を参照してください。

イベント

イベントは、スパン上の人間が読める形式のメッセージで、そのライフタイム中に「何かが起こった」ことを表します。 たとえば、ミューテックスの下にあるリソースへの排他的アクセスを必要とする関数を想像してください。 イベントは2つのポイントで作成できます。 1つはリソースへのアクセスを取得しようとするとき、もう1つはミューテックスを取得するときです。

span.AddEvent("Acquiring lock")
mutex.Lock()
span.AddEvent("Got lock, doing work...")
// 作業を行います
span.AddEvent("Unlocking")
mutex.Unlock()

イベントの有用な特性は、そのタイムスタンプがスパンの開始からのオフセットとして表示されることで、それらの間に経過した時間を簡単に確認できることです。

イベントは独自の属性も持つことができます。

span.AddEvent("Cancelled wait due to external signal", trace.WithAttributes(attribute.Int("pid", 4328), attribute.String("signal", "SIGHUP")))

スパンステータスの設定

A Status can be set on a Span, typically used to specify that a Span has not completed successfully - Error. By default, all spans are Unset, which means a span completed without error. The Ok status is reserved for when you need to explicitly mark a span as successful rather than stick with the default of Unset (i.e., “without error”).

The status can be set at any time before the span is finished.

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operationThatCouldFail()
if err != nil {
	span.SetStatus(codes.Error, "operationThatCouldFail failed")
}

エラーの記録

失敗した操作があり、それが生成したエラーをキャプチャしたい場合は、そのエラーを記録できます。

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operationThatCouldFail()
if err != nil {
	span.SetStatus(codes.Error, "operationThatCouldFail failed")
	span.RecordError(err)
}

RecordErrorを使用する場合は、失敗した操作を追跡するスパンをエラースパンと見なしたくない場合を除き、スパンのステータスをErrorに設定することを強く推奨します。 RecordError関数は、呼び出されたときにスパンステータスを自動的に設定しません

プロパゲーターとコンテキスト

トレースは単一のプロセスを超えて拡張できます。 これには、トレースの識別子がリモートプロセスに送信されるメカニズムである コンテキストプロパゲーション が必要です。

ワイヤー経由でトレースコンテキストを伝播するために、プロパゲーターをOpenTelemetry APIに登録する必要があります。

import (
  "go.opentelemetry.io/otel"
  "go.opentelemetry.io/otel/propagation"
)
...
otel.SetTextMapPropagator(propagation.TraceContext{})

OpenTelemetryはまた、W3C TraceContext標準をサポートしない既存のトレーシングシステム(go.opentelemetry.io/contrib/propagators/b3)との互換性のためにB3ヘッダー形式もサポートしています。

コンテキストプロパゲーションを設定した後は、実際にコンテキストのシリアル化を管理する舞台裏の作業を処理するために、自動計装を使用したくなるでしょう。

メトリクス

メトリクスの生成を開始するには、Meterを作成できる初期化済みのMeterProviderが必要です。 メーターを使用すると、さまざまな種類のメトリクスを作成するために使用できる計装を作成できます。 OpenTelemetry Goは現在、次の計装をサポートしています。

  • Counter、非負の増分をサポートする同期計装
  • Asynchronous Counter、非負の増分をサポートする非同期計装
  • Histogram、ヒストグラム、サマリー、パーセンタイルなど、統計的に意味のある任意の値をサポートする同期計装
  • Synchronous Gauge、室温など、非加算値をサポートする同期計装
  • Asynchronous Gauge、室温など、非加算値をサポートする非同期計装
  • UpDownCounter、アクティブなリクエスト数など、増分と減分をサポートする同期計装
  • Asynchronous UpDownCounter、増分と減分をサポートする非同期計装

同期および非同期計装の詳細、およびユースケースに最適な種類については、補助ガイドラインを参照してください。

MeterProviderが計装ライブラリまたは手動で作成されていない場合、OpenTelemetryメトリクスAPIはno-op実装を使用し、データの生成に失敗します。

ここでより詳細なパッケージドキュメントを見つけることができます。

メトリクスの初期化

アプリでメトリクスを有効にするには、Meterを作成できる初期化済みのMeterProviderが必要です。

MeterProviderが作成されていない場合、メトリクス用のOpenTelemetry APIはno-op実装を使用し、データの生成に失敗します。 したがって、次のパッケージを使用してSDK初期化コードを含むようにソースコードを変更する必要があります。

適切なGoモジュールがインストールされていることを確認してください。

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/exporters/stdout/stdoutmetric \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/sdk/metric

次に、リソース、メトリクスエクスポーター、メトリクスプロバイダーを初期化します。

package main

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.32.0"
)

func main() {
	// リソースを作成します
	res, err := newResource()
	if err != nil {
		panic(err)
	}

	// メータープロバイダーを作成します
	// MeterProviderインスタンスを受け入れる場合は、このインスタンスを
	// 計装されたコードに直接渡すことができます
	meterProvider, err := newMeterProvider(res)
	if err != nil {
		panic(err)
	}

	// 何もリークしないようにシャットダウンを適切に処理します
	defer func() {
		if err := meterProvider.Shutdown(context.Background()); err != nil {
			log.Println(err)
		}
	}()

	// otel.Meter経由で使用でき、otel.GetMeterProviderを使用して
	// アクセスできるように、グローバルメータープロバイダーとして登録します
	// ほとんどのインストルメンテーションライブラリは、デフォルトとして
	// グローバルメータープロバイダーを使用します
	// グローバルメータープロバイダーが設定されていない場合、
	// no-op実装が使用され、データの生成に失敗します
	otel.SetMeterProvider(meterProvider)
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("my-service"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
	metricExporter, err := stdoutmetric.New()
	if err != nil {
		return nil, err
	}

	meterProvider := metric.NewMeterProvider(
		metric.WithResource(res),
		metric.WithReader(metric.NewPeriodicReader(metricExporter,
			// デフォルトは1mです。デモンストレーション目的で3sに設定します
			metric.WithInterval(3*time.Second))),
	)
	return meterProvider, nil
}

これでMeterProviderが設定されたので、Meterを取得できます。

メーターの取得

手動で計装されたコードがあるアプリケーション内の任意の場所で、otel.Meterを呼び出してメーターを取得できます。 例を挙げましょう。

import "go.opentelemetry.io/otel"

var meter = otel.Meter("example.io/package/name")

同期および非同期計装

OpenTelemetryの計装は、同期または非同期(観測可能)のいずれかです。

同期計装は、呼び出されたときに測定を行います。 測定は、他の関数呼び出しと同様に、プログラム実行中の別の呼び出しとして行われます。 定期的に、これらの測定の集約が設定されたエクスポーターによってエクスポートされます。 測定は値のエクスポートから切り離されているため、エクスポートサイクルには0または複数の集約された測定が含まれる場合があります。

一方、非同期計装は、SDKのリクエストで測定を提供します。 SDKがエクスポートするとき、作成時に計装に提供されたコールバックが呼び出されます。 このコールバックは、即座にエクスポートされる測定をSDKに提供します。 非同期計装でのすべての測定は、エクスポートサイクルごとに1回実行されます。

非同期計装は、次のようないくつかの状況で役立ちます。

  • カウンターの更新が計算上安価でなく、現在実行中のスレッドが測定を待つことを望まない場合
  • 観測がプログラム実行とは無関係な頻度で発生する必要がある場合(すなわち、リクエストライフサイクルに関連付けられているときに正確に測定できない場合)
  • 測定値に既知のタイムスタンプがない場合

このような場合、後処理で一連のデルタを集約するのではなく、累積値を直接観測する方が良いことがよくあります(同期例)。

カウンターの使用

カウンターは、非負の増加する値を測定するために使用できます。

たとえば、HTTPハンドラーの呼び出し数を報告する方法は次のとおりです。

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	apiCounter, err := meter.Int64Counter(
		"api.counter",
		metric.WithDescription("Number of API calls."),
		metric.WithUnit("{call}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		apiCounter.Add(r.Context(), 1)

		// API呼び出しで何らかの作業を行います
	})
}

UpDownカウンターの使用

UpDownカウンターは増分と減分ができ、上下する累積値を観測できます。

たとえば、あるコレクションのアイテム数を報告する方法は次のとおりです。

import (
	"context"

	"go.opentelemetry.io/otel/metric"
)

var itemsCounter metric.Int64UpDownCounter

func init() {
	var err error
	itemsCounter, err = meter.Int64UpDownCounter(
		"items.counter",
		metric.WithDescription("Number of items."),
		metric.WithUnit("{item}"),
	)
	if err != nil {
		panic(err)
	}
}

func addItem() {
	// コレクションにアイテムを追加するコード

	itemsCounter.Add(context.Background(), 1)
}

func removeItem() {
	// コレクションからアイテムを削除するコード

	itemsCounter.Add(context.Background(), -1)
}

ゲージの使用

ゲージは、変更が発生したときに非加算値を測定するために使用されます。

たとえば、CPUファンの現在の速度を報告する方法は次のとおりです。

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
)

var (
  fanSpeedSubscription chan int64
  speedGauge metric.Int64Gauge
)

func init() {
	var err error
	speedGauge, err = meter.Int64Gauge(
		"cpu.fan.speed",
		metric.WithDescription("Speed of CPU fan"),
		metric.WithUnit("RPM"),
	)
	if err != nil {
		panic(err)
	}

	getCPUFanSpeed := func() int64 {
		// デモンストレーション目的でランダムなファン速度を生成します
		// 実際のアプリケーションでは、これを実際のファン速度を取得するように置き換えてください
		return int64(1500 + rand.Intn(1000))
	}

	fanSpeedSubscription = make(chan int64, 1)
	go func() {
		defer close(fanSpeedSubscription)

		for idx := 0; idx < 5; idx++ {
			// 同期ゲージは、測定サイクルが外部の変更と
			// 同期している場合に使用されます
			time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
			fanSpeed := getCPUFanSpeed()
			fanSpeedSubscription <- fanSpeed
		}
	}()
}

func recordFanSpeed() {
	ctx := context.Background()
	for fanSpeed := range fanSpeedSubscription {
		speedGauge.Record(ctx, fanSpeed)
	}
}

ヒストグラムの使用

ヒストグラムは、時間の経過に伴う値の分布を測定するために使用されます。

たとえば、HTTPハンドラーの応答時間の分布を報告する方法は次のとおりです。

import (
	"net/http"
	"time"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	histogram, err := meter.Float64Histogram(
		"task.duration",
		metric.WithDescription("The duration of task execution."),
		metric.WithUnit("s"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// API呼び出しで何らかの作業を行います

		duration := time.Since(start)
		histogram.Record(r.Context(), duration.Seconds())
	})
}

観測可能(非同期)カウンターの使用

観測可能カウンターは、加算的で非負の単調増加する値を測定するために使用できます。

たとえば、アプリケーションが開始してからの時間を報告する方法は次のとおりです。

import (
	"context"
	"time"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	start := time.Now()
	if _, err := meter.Float64ObservableCounter(
		"uptime",
		metric.WithDescription("The duration since the application started."),
		metric.WithUnit("s"),
		metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
			o.Observe(float64(time.Since(start).Seconds()))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

観測可能(非同期)UpDownカウンターの使用

観測可能UpDownカウンターは増分と減分ができ、加算的で非負の非単調増加累積値を測定できます。

たとえば、データベースメトリクスを報告する方法は次のとおりです。

import (
	"context"
	"database/sql"

	"go.opentelemetry.io/otel/metric"
)

// registerDBMetricsは、提供されたdbの非同期メトリクスを登録します
// 提供されたdbを閉じる前に、metric.Registrationの登録を解除してください
func registerDBMetrics(db *sql.DB, meter metric.Meter, poolName string) (metric.Registration, error) {
	max, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.max",
		metric.WithDescription("The maximum number of open connections allowed."),
		metric.WithUnit("{connection}"),
	)
	if err != nil {
		return nil, err
	}

	waitTime, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.wait_time",
		metric.WithDescription("The time it took to obtain an open connection from the pool."),
		metric.WithUnit("ms"),
	)
	if err != nil {
		return nil, err
	}

	reg, err := meter.RegisterCallback(
		func(_ context.Context, o metric.Observer) error {
			stats := db.Stats()
			o.ObserveInt64(max, int64(stats.MaxOpenConnections))
			o.ObserveInt64(waitTime, int64(stats.WaitDuration))
			return nil
		},
		max,
		waitTime,
	)
	if err != nil {
		return nil, err
	}
	return reg, nil
}

観測可能(非同期)ゲージの使用

観測可能ゲージは、非加算値を測定するために使用する必要があります。

たとえば、アプリケーションで使用されるヒープオブジェクトのメモリ使用量を報告する方法は次のとおりです。

import (
	"context"
	"runtime"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	if _, err := meter.Int64ObservableGauge(
		"memory.heap",
		metric.WithDescription(
			"Memory usage of the allocated heap objects.",
		),
		metric.WithUnit("By"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			var m runtime.MemStats
			runtime.ReadMemStats(&m)
			o.Observe(int64(m.HeapAlloc))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

属性の追加

WithAttributeSetまたはWithAttributesオプションを使用して属性を追加できます。

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
	semconv "go.opentelemetry.io/otel/semconv/v1.32.0"
)

func init() {
	apiCounter, err := meter.Int64UpDownCounter(
		"api.finished.counter",
		metric.WithDescription("Number of finished API calls."),
		metric.WithUnit("{call}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// API呼び出しで何らかの作業を行い、応答HTTPステータスコードを設定します

		apiCounter.Add(r.Context(), 1,
			metric.WithAttributes(semconv.HTTPResponseStatusCode(statusCode)))
	})
}

ビューの登録

ビューは、SDKによって出力されるメトリクスをカスタマイズする柔軟性をSDKユーザーに提供します。 どのメトリクス計装を処理するか無視するかをカスタマイズできます。 また、集約とメトリクスで報告したい属性もカスタマイズできます。

すべての計装には、元の名前、説明、属性を保持し、計装のタイプに基づくデフォルトの集約を持つデフォルトビューがあります。 登録されたビューが計装と一致する場合、デフォルトビューは登録されたビューに置き換えられます。 計装と一致する追加の登録済みビューは累積的であり、計装に対して複数のエクスポートされたメトリクスが生成されます。

NewView関数を使用してビューを作成し、WithViewオプションを使用して登録できます。

たとえば、http計装ライブラリのv0.34.0バージョンからのlatency計装をrequest.latencyに名前を変更するビューを作成する方法は次のとおりです。

view := metric.NewView(metric.Instrument{
	Name: "latency",
	Scope: instrumentation.Scope{
		Name:    "http",
		Version: "0.34.0",
	},
}, metric.Stream{Name: "request.latency"})

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

たとえば、http計装ライブラリからのlatency計装を指数ヒストグラムとして報告するビューを作成する方法は次のとおりです。

view := metric.NewView(
	metric.Instrument{
		Name:  "latency",
		Scope: instrumentation.Scope{Name: "http"},
	},
	metric.Stream{
		Aggregation: metric.AggregationBase2ExponentialHistogram{
			MaxSize:  160,
			MaxScale: 20,
		},
	},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

SDKは、メトリクスをエクスポートする前にメトリクスと属性をフィルタリングします。たとえば、ビューを使用して高カーディナリティメトリクスのメモリ使用量を削減したり、機密データを含む可能性のある属性を削除したりできます。

http計装ライブラリからのlatency計装を削除するビューを作成する方法は次のとおりです。

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{Aggregation: metric.AggregationDrop{}},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

http計装ライブラリからのlatency計装によって記録されたhttp.request.method属性を削除するビューを作成する方法は次のとおりです。

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{AttributeFilter: attribute.NewDenyKeysFilter("http.request.method")},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

条件のNameフィールドはワイルドカードパターンマッチングをサポートしています。*ワイルドカードは0個以上の文字に一致するものとして認識され、?はちょうど1文字に一致するものとして認識されます。たとえば、*のパターンはすべての計装名に一致します。

名前のサフィックスが.msである任意の計装に対して単位をミリ秒に設定するビューを作成する方法の例を次に示します。

view := metric.NewView(
  metric.Instrument{Name: "*.ms"},
  metric.Stream{Unit: "ms"},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

NewView関数は、ビューを作成する便利な方法を提供します。NewViewが必要な機能を提供できない場合は、カスタムViewを直接作成できます。

たとえば、正規表現マッチングを使用して、すべてのデータストリーム名が使用する単位のサフィックスを持つことを保証するビューを作成する方法は次のとおりです。

re := regexp.MustCompile(`[._](ms|byte)$`)
var view metric.View = func(i metric.Instrument) (metric.Stream, bool) {
	// カスタムView関数では、名前、説明、単位を明示的にコピーする必要があります
	s := metric.Stream{Name: i.Name, Description: i.Description, Unit: i.Unit}
	// 単位サフィックスが定義されていないが、次元単位が定義されている
	// 任意のインストルメントに対して、名前を単位サフィックスで更新します
	if re.MatchString(i.Name) {
		return s, false
	}
	switch i.Unit {
	case "ms":
		s.Name += ".ms"
	case "By":
		s.Name += ".byte"
	default:
		return s, false
	}
	return s, true
}

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

ログ

ログは、ユーザー向けのOpenTelemetryログAPIが存在しない点で、メトリクスやトレースとは異なります。 かわりに、既存の人気のあるログパッケージ(slog、logrus、zap、logrなど)からOpenTelemetryエコシステムにログをブリッジするツールがあります。 この設計決定の根拠については、ログ仕様を参照してください。

以下で説明する2つの典型的なワークフローは、それぞれ異なるアプリケーション要件に対応しています。

Direct-to-Collector

ステータス: Experimental

Direct-to-Collectorワークフローでは、ログはネットワークプロトコル(OTLPなど)を使用してアプリケーションからコレクターに直接送信されます。 このワークフローは、追加のログ転送コンポーネントを必要とせず、設定が簡単で、ログデータモデルに準拠した構造化ログをアプリケーションが簡単に送信できます。 ただし、アプリケーションがネットワークの場所にログをキューイングおよびエクスポートするために必要なオーバーヘッドは、すべてのアプリケーションに適しているとは限りません。

このワークフローを使用するには。下記に従ってください。

  • 目的のターゲット宛先(コレクターまたはその他)にログレコードをエクスポートするように、OpenTelemetry Log SDKを設定します。
  • 適切なLog Bridgeを使用します。

ログSDK

ログSDKは、direct-to-Collectorワークフローを使用する際のログの処理方法を決定します。 ログ転送ワークフローを使用する場合、ログSDKは必要ありません。

典型的なログSDK設定では、OTLPエクスポーターを使用してバッチログレコードプロセッサーをインストールします。

アプリでログを有効にするには、Log Bridgeを使用できる初期化済みのLoggerProviderが必要です。

LoggerProviderが作成されていない場合、ログ用のOpenTelemetry APIはno-op実装を使用し、データの生成に失敗します。 したがって、次のパッケージを使用してSDK初期化コードを含むようにソースコードを変更する必要があります。

適切なGoモジュールがインストールされていることを確認してください。

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/sdk/log

次に、ロガープロバイダーを初期化します。

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
	"go.opentelemetry.io/otel/log/global"
	"go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.32.0"
)

func main() {
	ctx := context.Background()

	// リソースを作成します
	res, err := newResource()
	if err != nil {
		panic(err)
	}

	// ロガープロバイダーを作成します
	// ブリッジを作成するときに、このインスタンスを直接渡すことができます
	loggerProvider, err := newLoggerProvider(ctx, res)
	if err != nil {
		panic(err)
	}

	// 何もリークしないようにシャットダウンを適切に処理します
	defer func() {
		if err := loggerProvider.Shutdown(ctx); err != nil {
			fmt.Println(err)
		}
	}()

	// global.LoggerProviderにアクセスできるように、グローバルロガープロバイダーとして登録します
	// ほとんどのログブリッジは、デフォルトとしてグローバルロガープロバイダーを使用します
	// グローバルロガープロバイダーが設定されていない場合、no-op実装が使用され、
	// データの生成に失敗します
	global.SetLoggerProvider(loggerProvider)
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("my-service"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newLoggerProvider(ctx context.Context, res *resource.Resource) (*log.LoggerProvider, error) {
	exporter, err := otlploghttp.New(ctx)
	if err != nil {
		return nil, err
	}
	processor := log.NewBatchProcessor(exporter)
	provider := log.NewLoggerProvider(
		log.WithResource(res),
		log.WithProcessor(processor),
	)
	return provider, nil
}

これでLoggerProviderが設定されたので、それを使用してLog Bridgeを設定できます。

ログブリッジ

ログブリッジは、Logs Bridge APIを使用して、既存のログパッケージからOpenTelemetry Log SDKにログをブリッジするコンポーネントです。

利用可能なログブリッジの完全なリストは、OpenTelemetryレジストリで見つけることができます。

各ログブリッジパッケージのドキュメントには、使用例が含まれているはずです。

ファイルまたは標準出力経由

ファイルまたは標準出力ワークフローでは、ログはファイルまたは標準出力に書き込まれます。別のコンポーネント(FluentBitなど)がログの読み取り/テーリング、より構造化された形式への解析、およびコレクターなどのターゲットへの転送を担当します。このワークフローは、アプリケーション要件がdirect-to-Collectorからの追加のオーバーヘッドを許可しない状況で好まれる場合があります。ただし、下流で必要なすべてのログフィールドがログにエンコードされ、ログを読み取るコンポーネントがデータをログデータモデルに解析することが必要です。ログ転送コンポーネントのインストールと設定は、このドキュメントの範囲外です。

次のステップ

また、テレメトリーデータを1つ以上のテレメトリーバックエンドにエクスポートするために、適切なエクスポーターを設定することもできます。