📈

Alloyを使ったオブザーバビリティを試してみた

に公開

はじめに

こんにちは!SREエンジニアのKoseです!
Grafanaに最近ハマっている私ですが、Grafana Alloyってご存知ですか?
私は最近知ったのですが、Promtailの後継となる製品のようです。
そこで、今回はAlloyを使ったオブザーバビリティの基礎をまとめていきたいと思います。
(ちょっと長くなりそうだったので、ログとトレース情報に関してこの記事では説明しようと思います。)

前提知識と環境

  • 基本的なLinuxコマンドの知識
  • Kubernetesの基本的な操作
  • オブザーバビリティの基本的な概念
  • Kubernetesクラスタが構築されていること(この記事ではEKSで構築してます。)
  • Prometheus, Grafanaが環境に実装されていること

利用する技術

技術 説明 バージョン
Grafana Loki ログ管理システム 3.4.2
Grafana tempo 分散トレーシングバックエンド 2.7.1
Grafana Alloy テレメトリコレクターエージェント v4.0.1
Grafana 可視化ツール 11.6.1
Prometheus メトリック収集 v3.3.1
EKS コンテナオーケストレーションプラットフォーム v1.31
Helm Kubernetesパッケージマネージャー v3.17.3

本文

Grafana Alloyについて

Grafana Alloyは、Grafana社が開発したOSSのデータ収集ツールで、さまざまなシステムからログやトレースを効率的に集めることができます。OpenTelemetry技術をベースにしているため、特定のベンダーに依存せず、多様なシステムと連携できる柔軟性が特徴です。

Alloyの主な機能

Alloyには以下の3つの主要機能があります:

Collect

様々なコンポーネントを使用して、アプリケーション、データベース、Opentelemetryコレクターからテレメトリデータを収集します。
テレメトリデータは AlloyにPushすることも、AlloyがデータソースからPullすることもできるので、様々なテレメトリデータを取得することが可能です。
また、トレースデータに関しては、計装(アプリケーションコードにトレース収集機能を組み込むこと)が必要ですので、Opentelemetry SDKなどで計装を実装してください。

Transform

データを処理し、ラベル、メタデータを挿入したり、不要なデータを除外したりできます。

Write

OpenTelemetry 互換のデータベースまたはコレクター、Grafana スタック、または Grafana Cloud にデータを送信します。

Grafana Tempoについて

Grafana Tempoは、Grafana Labsが開発した高スケーラブルな分散トレーシングバックエンドです。Tempoは、Grafana AlloyやOpentelemetry Collectorなどから送られてくるトレースデータをコスト効率の高いオブジェクトストレージ(S3、GCS、Azure Blobなど)に保存し、トレースIDにより検索することに特化しています。
アーキテクチャはLokiと似たところがあり、比較的にわかりやすいマイクロサービスとなっているかと思います。
興味のある方は公式Documentをご確認ください

今回の検証

さぁ、ここからは手を動かしていきましょう!今回の検証ではログデータと、トレースデータを収集するところを実施したいと思います。
以下の図は今回構築する環境の全体像です。

構成図
検証環境構成図

主要機能 今回の検証で試すこと
Collect Kubernetes Podからログ、トレースデータを取得
Transform Kubernetes用のラベルを付与
Write Loki, Tempoにデータを送信

インストール手順

Alloyのインストール手順

ここでは、Alloyの基本的なインストール方法を説明します。インストールが完了するとAlloyはKubernetesのDaemonSetとしてデプロイされます。
それでは、Grafana Alloyをインストールしてみましょう。

1. リポジトリ追加

helm repo add grafana https://grafana.github.io/helm-charts

2. Values.yamlの取得

helm show values grafana/alloy > values.yaml

3. Values.yamlを変更

Alloyの設定には関しては後ほど説明しますのでこちらでは割愛します。values.yamlを設定しなくても、起動自体はできます。

4. インストール

helm upgrade --install alloy grafana/alloy --create-namespace --namespace alloy -f values.yaml

インストールの確認

以下のコマンドで、AlloyのPodが正常に起動していることを確認できます:

kubectl get pods -n alloy

正常にインストールできている場合、以下のような出力が表示されます:

NAMESPACE          NAME                                                 READY   STATUS    RESTARTS   AGE
alloy              alloy-2xx77                                          2/2     Running   0          3h13m
alloy              alloy-b6qhq                                          2/2     Running   0          3h13m
alloy              alloy-r2tbp                                          2/2     Running   0          3h13m
alloy              alloy-z4rq5                                          2/2     Running   0          3h13m

Tempoのインストール手順

Grafana Tempoには2種類のHelmインストールがありますが、今回はtempo-distributedを使用してます。
tempo-distributed Helm chart : microservices modeでデプロイするときに利用
tempo Helm chart : monolithic (single binary) modeでデプロイするときに利用

続いて、Tempoをインストールしてみます。

1. リポジトリ登録

helm repo add grafana https://grafana.github.io/helm-charts

2. valuesの取得

helm show values grafana/tempo-distributed > values.yaml

3. Values.yamlを変更

values.yamlは以下のように設定することで, S3にTraceデータを格納できるようなtempoをインストールすることができます。
当然、S3を利用する場合は、事前にS3バケットを用意しておき、irsaなどによりAWSのロールをKubernetesのServiceAccountに紐づける必要があります。
(Lokiの時と同じ設定なのでAWS側の設定に関してはこちらを参照ください)

serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<AccoutID>:role/tempo-irsa-role
ingester:
  persistence:
    enabled: true
    storageClass: gp2
    size: 10Gi
metricsGenerator:
  enabled: true

## トレースデータを受け付ける窓口
distributor:
  config:
    log_received_spans:
      enabled: true
    log_discarded_spans:
      enabled: true

## otlpを受け付けることにより,デフォルト4318/TCP,4317/TCPが有効になります
traces:
  otlp:
    http:
      enabled: true
    grpc:
      enabled: true


storage:
  trace:
    backend: s3
    s3:
      bucket: test-tempo-traces-bucket
      endpoint: s3.ap-northeast-1.amazonaws.com
      region: ap-northeast-1

4. インストール

helm upgrade --install tempo grafana/tempo-distributed --create-namespace -n tempo -f values.yaml

以下のコマンドで、tempoのPodが正常に起動していることを確認できます:

kubectl get pods -n tempo

正常にインストールできている場合、以下のような出力が表示されます:

NAMESPACE          NAME                                                 READY   STATUS    RESTARTS   AGE
tempo              tempo-compactor-6567d999-5gk5b                       1/1     Running   0          22h
tempo              tempo-distributor-75cc847cfd-mhnb2                   1/1     Running   0          22h
tempo              tempo-ingester-0                                     1/1     Running   0          21h
tempo              tempo-ingester-1                                     1/1     Running   0          21h
tempo              tempo-ingester-2                                     1/1     Running   0          22h
tempo              tempo-memcached-0                                    1/1     Running   0          22h
tempo              tempo-metrics-generator-6d94b7fdf6-fmc8x             1/1     Running   0          22h
tempo              tempo-querier-64b754d5d7-l44wt                       1/1     Running   0          22h
tempo              tempo-query-frontend-784d9986f9-lmbbl                1/1     Running   0          22h

Lokiのインストール手順

Lokiのインストール手順については、以前の記事で説明しているのでそちらを参考にしてください
https://zenn.dev/kose/articles/27f41edf465d26

Alloyの設定

Alloyは、様々なコンポーネントを使ってテレメトリデータを収集します。ここでは、K8sログの取得とトレースデータの取得の例を記載します。
他にも多くデータを取得できますので、Reference探してみることをお勧めします。

K8sログの取得

Kubernetesのログ収集設定です。

# Kubernetesから全てのPod情報を検出
discovery.kubernetes "pod" {
            role = "pod"
}

# 検出したPod情報にラベルを付与する設定
discovery.relabel "pod_logs" {
    targets = discovery.kubernetes.pod.targets
    
    # Kubernetes名前空間をnamespaceラベルとして追加
    rule {
        source_labels = ["__meta_kubernetes_namespace"]
        action = "replace"
        target_label = "namespace"
    }   
    # Pod名をpodラベルとして追加
    rule {
        source_labels = ["__meta_kubernetes_pod_name"]
        action = "replace"
        target_label = "pod"
    }
    # コンテナ名をcontainerラベルとして追加
    rule {
        source_labels = ["__meta_kubernetes_pod_container_name"]
        action = "replace"
        target_label = "container"
    }
}

# Kubernetesのログを取得してLokiに送信するための設定
loki.source.kubernetes "pod" {
    targets = discovery.relabel.pod_logs.output
    forward_to = [loki.process.pod_logs.receiver]
}

# loki.processは他のLokiコンポーネントからログエントリを受け取り、1つ以上の処理ステージを適用します
loki.process "pod_logs" {
    stage.static_labels {
        values = {
            cluster = "kose-eks-welcomestudy",
        }
    }
    forward_to = [loki.write.loki.receiver]
}
      
# Lokiへの送信設定
loki.write "loki" {
    endpoint {
        url = "http://loki-distributor.loki.svc.cluster.local:3100/loki/api/v1/push"
    }
}

トレースデータの取得

OpenTelemetryなどで計装したアプリケーションからトレースデータを取得します。

## OTLPレシーバーの設定
## このレシーバーは、アプリケーションからのトレースデータを受信します
otelcol.receiver.otlp "default" {
    grpc {} ## gRPCプロトコルでの受信を有効化
    http {} ## HTTPプロトコルでの受信も有効化
    output {
        traces = [otelcol.processor.k8sattributes.default.input] ## 受信したトレースをK8s属性プロセッサに転送
    }
}

## Kubernetes属性プロセッサの設定
## このプロセッサは、トレースデータにKubernetes関連の属性を追加します
otelcol.processor.k8sattributes "default" {
    extract {
        metadata = [
            "k8s.namespace.name",  ## Kubernetes名前空間
            "k8s.pod.name",        ## Pod名
            "k8s.container.name",  ## コンテナ名
        ]
    }
    output {
        traces = [otelcol.exporter.otlp.default.input] ## 属性が追加されたトレースをOTLPエクスポーターに転送
    }
}

## OTLPエクスポーターの設定
## このエクスポーターは、処理されたトレースデータをTempoに転送します
otelcol.exporter.otlp "default" {
    client {
        endpoint = "tempo-distributor.tempo.svc.cluster.local:4317" ## Tempoのエンドポイント
        tls {
            insecure = true ## 本番環境ではTLSを設定することをお勧めします
        }
    }
}

正常性の確認

Grafanaなどの可視化ツールでトレースデータを確認します:
alloy-trace

オブザーバビリティの実装

オブザーバビリティの実装はメトリック、ログ、トレースを収集することだけでは終わりません。システムの状態を理解する上では、この3つのデータを組み合わせて扱う必要があります。
ここでは、ログとトレースを組み合わせることにより、よりシステム内部を理解できるようにします。

Trace To Logs

Trace To Logsは、トレースからログへのリンクを作成する機能です。これにより、トレースを調査しているときに関連するログをワンクリックで確認できます。

設定方法

Grafanaでトレースデータソース(Tempo)の設定に以下を追加します:

apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-datasource
  namespace: grafana
  labels:
    grafana_datasource: "true"
data:
  tempo-datasource.yaml: |
    apiVersion: 1
    datasources:
      - name: Tempo
        type: tempo
        url: http://tempo-query-frontend.tempo.svc.cluster.local:3100/
        access: proxy
        editable: true
        jsonData:
          lokiSearch:
            datasourceUid: Loki
  # いきなりV2ですが、こちらを参考にしてます。 https://grafana.com/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/#example-file
          tracesToLogsV2:
            datasourceUid: Loki  # Lokiデータソースを指定(GrafanaのLokiデータソースUID)
            tags:
              - key: "k8s.container.name"  # Tempo trace field
                value: "container"         # Loki field
            spanStartTimeShift: "-1m"
            spanEndTimeShift: "1m"
            filterByTraceID: true
            filterBySpanID: false
  # 深く説明はしませんがtraceとMetricsを紐づけることもできます
          tracesToMetrics:
            datasourceUid: 'Prometheus'
            tags: 
              - key: "k8s.container.name"
                value: "container"
            spanStartTimeShift: "-10m"
            spanEndTimeShift: "10m"
            queries:
              - name: 'Memory Usage'
                query: avg(container_memory_usage_bytes{__ignore_usage__="",$$__tags} )       

結果

トレースビューでログへのリンクが表示されます:
trace_to_log

log to Traces

Log to Tracesは、ログからトレースへのリンクを作成する機能です。これにより、ログを調査しているときに関連するトレースをワンクリックで確認できます。

設定方法

Grafanaでログデータソース(Loki)の設定に以下を追加します:

apiVersion: v1
kind: ConfigMap
metadata:
  name: loki-datasource
  namespace: grafana
  labels:
    grafana_datasource: "true"
data:
  loki-datasource.yaml: |
    apiVersion: 1
    datasources:
      - name: Loki
        type: loki
        url: http://loki-query-frontend.loki.svc.cluster.local:3100/
        access: proxy
        editable: false
        jsonData:
          # Log to Tracesの設定
          derivedFields:
            - datasourceUid: 'Tempo'                  # Tempoデータソースの識別子
              matcherRegex: 'trace_id=([a-f0-9]{32})' # ログメッセージからトレースIDを抽出する正規表現
              name: 'TraceID'                         # UIに表示されるリンク名
              url: '$${__value.raw}'                  # トレースビューへのリンク

今回ログには以下のようにTraceidが出るようにアプリケーションを実装しているので、matcherRegexなどをそれに合うような値で設定してます。

2025/05/21 02:48:07 Request received: path=/hello, method=GET, trace_id=4228e430a1f94dc866fb862c68189da7

結果

ログビューでトレースIDがクリック可能なリンクとして表示されます:

log_to_trace

まとめ

このチュートリアルでは、Alloyを使用してテレメトリデータの活用方法を実装する方法を学びました。
Alloyには、これ以外にも様々なコンポーネントがあり、多くのテレメトリデータの取得が可能です。バックエンドとして、Grafana Labsの製品を利用する場合は、利用選択肢として入ってくると思いますが、独自の書き方が多いのでちょっとだけ戸惑うことがあるかもしれません。

次のステップとしてはこのようなことに挑戦していきたいと思います(今後記事にしていきます)

  • 様々なコンポーネントを試してみる
  • OpenTelemetry Collectorとの違いの調査

お疲れ様でした〜🎉

参考資料

備考:サンプルアプリケーション

アプリケーションを計装するには様々な方法がありますが、より実践的なものとして、OpenTelemetryを利用します。Alloy自体がOpenTelemetryのディストリビューションであるため、相性が良く、シームレスな連携が可能です。

本記事ではGoアプリケーションを例にしていますが、JavaやPythonなど他の言語でも同様のアプローチが可能です。

環境準備

まず、新しいGoプロジェクトを作成します:

mkdir my-go-app
cd my-go-app
go mod init my-go-app

必要なライブラリのインストール

OpenTelemetryとその関連コンポーネントをインストールします:

go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp

以下はトレースを生成する簡単なHTTPサーバーの例です。各部分の役割を理解することが重要です:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

// トレーサーの初期化
// この関数は、OpenTelemetryトレーサーを設定し、Alloyにトレースデータを送信します
func initTracer(ctx context.Context) func() {
	// OTLP gRPCエクスポーターの作成
	// AlloyのエンドポイントURLを指定します。実際の環境に合わせて変更してください
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint("alloy.alloy.svc.cluster.local:4317"), // AlloyやCollectorのエンドポイント
		otlptracegrpc.WithInsecure(), // 本番環境ではTLSを使用することをお勧めします
	)
	if err != nil {
		log.Printf("failed to create exporter: %v", err)
	}

	// サービス名などのリソース情報を設定
	// これにより、トレースデータがどのサービスから発生したかが識別できます
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceName("hello-api"), // サービス名を適切に設定してください
		),
	)
	if err != nil {
		log.Printf("failed to create resource: %v", err)
	}

	// トレースプロバイダーの作成と設定
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter), // トレースデータのバッチ処理
		sdktrace.WithResource(res),     // リソース情報の追加
	)
	otel.SetTracerProvider(tp)

	// シャットダウン関数を返す(リソースリークを防ぐため)
	return func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("failed to shutdown TracerProvider: %v", err)
		}
	}
}

// Hello APIのハンドラー
// このハンドラーは、リクエストを処理し、トレースデータを生成します
func helloHandler(w http.ResponseWriter, req *http.Request) {
	// 手動でspanを作成(特定の処理を詳細に計測するため)
	_, span := otel.Tracer("hello-handler").Start(req.Context(), "handle-hello")
	defer span.End() // 必ずspanを終了させることが重要です

	// トレースIDを取得(ログとトレースの関連付けに使用)
	traceID := span.SpanContext().TraceID().String()

	// リクエスト情報とトレースIDをログ出力
	// このログはあとでトレースと関連付けられます
	log.Printf("Request received: path=%s, method=%s, trace_id=%s", req.URL.Path, req.Method, traceID)

	// スパンに属性を追加(フィルタリングや詳細な分析に役立つ)
	span.SetAttributes(attribute.String("custom.attribute", "hello-world"))

	// 実際の処理
	fmt.Fprintln(w, "Hello, World")

	// レスポンス送信をログ
	log.Printf("Response sent: trace_id=%s", traceID)
}

func main() {
	ctx := context.Background()
	shutdown := initTracer(ctx)
	defer shutdown() // アプリケーション終了時にトレーサーをクリーンアップ

	// 標準のHTTPハンドラーにOpenTelemetry Middlewareを追加
	// これにより、すべてのHTTPリクエストが自動的にトレースされます
	handler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "hello-endpoint")

	http.Handle("/hello", handler)

	fmt.Println("Starting server at :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Printf("server error: %v", err)
	}
}

アプリケーションの起動

以下のコマンドでアプリケーションを起動します:

go run ./

動作確認

アプリケーションが正常に動作しているか確認します:

curl http://localhost:8080/hello # localhostは各環境で読み替えてください

# 正常に動作していれば "Hello, World" と表示されます
GitHubで編集を提案

Discussion