見出し画像

OpenTelemetryについて

松原です。最近チーム内でオブザーバビリティ(可観測性)に真剣に取り組んでいこうという動きがあり、今回は比較的新しいツール/コミュニティである「Open Telemetry」を中心に、オブザーバビリティ周辺について書きます。

オブザーバビリティ(可観測性)とは

先に少し触れましたが、モニタリング(監視)は「何が起きているのかを見続けること」に対し、オブザーバビリティは「予期せぬことが起きたときになぜそれが起きたのかを把握すること」となります。

特にクラウドネイティブ環境においてはオブザーバビリティへのシフトが必須となりますが、モニタリングが不要というわけではありません。モニタリングとオブザーバビリティは相反するものではなく、適切に監視することも必要なものであるし、同時に観察することも求められます。

引用: オブザーバビリティとは何か?まずはその概念を理解しよう

予期せぬ出来事が発生したときに原因の追求を行うことが出来る能力がオブザーバビリティとなります。

オブザーバビリティを得るためには

オブザーバビリティ3本柱である「メトリクス」「トレース」「ログ」を収集することによって、障害の検知やエラーの原因追求などを行います。通常、収集したデータは何かしらのAPMツールやMetrics/Tracingツールを通じて可視化されます。(New Relic、DataDog、CloudWatch+X-ray、Jaeger、Prometheus+Grafana、OpenZipkinなど)

また、「メトリクス」「トレース」「ログ」といったテレメトリーデータを収集するために、実際のアプリケーション側で生成する必要があります。
このことを「計装(インストゥルメーション)」といいます。
計装には自動と手動の2通りあり、言語、フレームワーク、ライブラリによってサポートが異なります。

オブザーバビリティ3本柱 + Event = MELT

Metrics、Events、Logs、Tracesの頭文字をとってMELT(メルト)と呼びます。
イベントはログやトレース発生の起点となったり、メトリクスで計測される対象となったりするものです。

イベント | Events:
ある瞬間に発生する個別のアクション。「いつ何が起こった」
例: 2019年2月21日午後3時34分に、BBQチップのバッグが1ドルで購入されました。
すべてのイベントを収集しようとすると保有コストや帯域圧迫などが課題

メトリクス | Metrics:
定期的にグループ化または収集された測定値の集合(averageやmin/maxなど)。
例: 2019年2月21日の午後3時34分から3時35分まで、合計3回の購入があり、合計で$ 2.75でした。
保存するデータを少なくできるが、分析するデータを事前に決めておく必要がある(取得を決めていないものに対して、後からは遡ることはできない)

ログ | Logs:
特定のコードブロックが実行されたときにシステムが生成する単なるテキスト行。基本的にイベントよりも多めに出る。離散的、不規則的
例: 2/21/2019 15:33:21: User inserted $0.25 remaining balance is $0.75
例: 2/21/2019 15:34:03: { actionType: purchaseCompleted, machineId: 2099, itemName: ‘Tasty BBQ Chips’, itemValue: 1.00 }
構造化されていない場合、ツール側での解析が難しかったりする

トレース | Traces:
マイクロサービスエコシステムの異なるコンポーネント間のイベント(またはトランザクション)の因果連鎖のサンプル。離散的、不規則的
「スパン」という特別なイベントを生成し、各サービスは相互に「トレースコンテキスト」と呼ばれる相関識別子を渡す

引用: Metrics, Events, Logs, Traces ってなんだ? "トレースはどのように機能するか"

OpenTelemetryとは

2019年にOpenCensusとOpenTracingが合流してできた比較的新しいOSS/コミュニティです。各ベンダー、OSSとが発展させてきたオブザーバビリティへの知見を集約し、仕様の標準化や各種実装を提供しています。

OpenTelemetryが提供するもの

  • ベンダー非依存の各言語毎に用意された単一の計装ライブラリ(自動/手動の計装をサポート)

  • ベンダー非依存の単一のコレクター

  • テレメトリーデータを生成、発信、収集、処理、エクスポートするためのエンドツーエンドの実装

  • 設定によって複数の送信先に並行でデータ送信

  • オープンで標準化されたセマンティック規約

  • コンテキストの伝播(アプリケーション境界を超えた分散トレース)

OpenTelemetryのコンポーネント群

  • Specification
    三部門で構成される。データや送受信の仕様などを定義

    • API
      トレース、メトリクス、およびロギングデータを生成および相関させるためのデータ型と操作の定義

    • SDK
      APIを言語別に実装するための要件を定義する。設定、データ処理、エクスポートの概念もここで定義される

    • Data
      テレメトリバックエンドがサポートするOpenTelemetryプロトコル(OTLP)とベンダーに依存しないセマンティック規約を定義

  • Collector(OTelCol)
     
    テレメトリーデータをアプリケーション側から受信し、外部に送信するモジュール

  • Language SDKs
     各言語毎に提供される、テレメトリーデータを生成したりCollectorに送信するための具体的な実装を提供する。フレームワーク、ライブラリ、アプリケーションの開発者はSDKを使って計装を行う

Specificationが提供するのは仕様なので、実際に開発者が利用するのは「Collector」と各言語毎に提供される「SDK」の部分となります。計装はSDKを開発者が直接使って実装する他、フレームワークやライブラリのプラグインやフックなどが用意されていれば、自動で計装することも出来ます。

OpenTelemetryの進捗について

前身のOpenCensus(Metrics+Traces)とOpenTracing(Trace)があるためか、モジュール毎にバラつきがあります。

Traces: ほぼStableな状態です。
Metrics: 仕様はほぼStableですが、言語毎のSDKの実装の進捗にバラつきがあります。(Arpha/Beta)
Logging: かなり発展途上で、GitHub上で議論がアクティブに進められています。SDKはJava/C++など一部が実験的に提供している段階です。

コミュニティの動きとしては、MetricsのSDKの実装を揃える方が、Loggingの仕様策定よりも優先度が高そうな印象です。
LoggingのSDK実装はまだまだ先になりそうですが、今後に期待です。

OpenTelemetry Collector(OTelCol)について

OpenTelemetry SDK、Prometheus、Jaegerなど、アプリケーション側で生成したテレメトリーデータを受信し、加工を行ってから、APMツールなどに送信するモジュールです。
Recievers、Processors、Exportersの3部で構成されており、これらを組み合わせてPipelineを構成します。これらの設定はYAML形式で行います。

Recievers:
 アプリケーションから送出されたテレメトリーデータの受信方法を決める。OTLP/Prometheus/Jaeger/OpenZipkinなどのデータ形式に対応。
Processors:
 Recieverが受け取ったテレメトリーデータをバッチ処理/加工する
Exporters:
 APMツールや標準出力など、テレメトリーデータの送信先・送信方法を決める

設定ファイルの例

# Collector側がテレメトリーデータをどのように受信するか
# (PrometheusなどPull型の場合はスクレイピングの設定を書く)
receivers:
  otlp:
    protocols:
      grpc:

# 中間処理層
processors:
  batch:

# 外部のAPMツールなどに送信する際の設定
exporters:
  otlp:
    endpoint: otlp.nr-data.net:4317
    headers:
      api-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  prometheus:
    endpoint: prometheus:8889
    namespace: default
  logging:

# テレメトリーデータと直接関係の無いヘルスチェックなど
extensions:
  health_check:
    endpoint: :13133
  zpages:
    endpoint: :55679

# 実際にパイプラインとして組み立てることで有効になる
# traces, metrics, logsに対して複数のパイプラインを設定することも可能
service:
  extensions: [health_check, zpages]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp, jaeger, logging]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp, prometheus, logging]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp, logging]

ライブラリを利用したサーバーサイドのgRPCのTraceの自動計装の実装例(Golang)

open-telemetry-go-contribプロジェクトにgRPCの自動計装ライブラリがあるので、そちらを利用します。

TracerProviderはOpenTelemetryのTrace SDKが提供している実装です。TracerProviderの中にexporter(送信先)を設定します。OtelColにもexporterという用語が出てくるのでややこしいですが、ここでのexporterはアプリケーションから見たテレメトリーデータの送信先となります。OtelColで受信する場合はrecieversに設定したものとなります。サンプルの例では標準出力ですが、OTLPで送出する場合はgRPCもしくはHTTPリクエストのコネクションを設定します。

# config/config.go
package config

import (
	"go.opentelemetry.io/otel"
	stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/propagation"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// TracerProviderの生成。この中にexporterを追加する
// サンプルでは標準出力
func Init() (*sdktrace.TracerProvider, error) {
	exporter, err := stdout.New(stdout.WithPrettyPrint())
	if err != nil {
		return nil, err
	}
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithBatcher(exporter),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
	return tp, nil
}
# server/main.go
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/example/api"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/example/config"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

	"google.golang.org/grpc"
)

...(中略)

func main() {
	tp, err := config.Init()
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

    	# gRPCのインターセプターに追加すると、ライブラリがリクエストの受信/エラーなどのトレースデータを自動的に取得/送信してくれる
	s := grpc.NewServer(
		grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
		grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
	)

	api.RegisterHelloServiceServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}


終わりに

OpenTelemetryは比較的若いOSSですが、少しずつエコシステムが形成されており、APMベンダーやクラウドベンダーなども積極的にサポートをしつつあるな、と感じています。
ログなど仕様すら固まっていない部分もあるため、プロダクション環境への投入は現時点では早すぎる感がありますが、ブラウザ戦争におけるECMAScriptのような役回りになっていくのではないかと思っており、今後も注目していきたいと思います。

(情報技術本部・メディア研究開発センター 松原 浩平)