Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

OpenTelemetry Collector導入の実践編とその後

はじめまして。Gaudiyでエンジニアをしているあんどう(@Andoobomber)です。

以前、「OpenTelemetry Collector導入のPoCと今後に向けて」という記事を弊エンジニアの sato(@yusukesatoo06)より公開しました。簡単に記事を要約すると、

  1. OpenTelemetry及びOpenTelemetry Collectorの説明
  2. 実際にPoCを作ってみる
  3. 実導入を試みたがOpenTelemetry Collectorのホスティングに悩み、今後の課題として保留となった

といった内容でした。

あれから1年経ち、GaudiyではOpenTelemetry Collectorを本番環境に組み込み、OpenTelemetryの仕様に準拠して計装し、データの分析や監視を行っています。この記事では、前回からの進捗を紹介すると共にOpenTelemetryの導入方法を書きたいと思います。

1. 前回のPoCから変わったこと

1-1. インフラ: Cloud Run → Google Kubernetes Engine(GKE)

前回上がった導入の課題として、「CollectorのホスティングにCloud Runを使用する案が上がったが、Cloud Runの特性上、常時立ち上げておく必要があるCollectorとの相性が悪く断念した」といった課題がありました。(当時はCloud Run SidecarがPreviewでした)

これをどう解決したかというと、Gaudiyのアプリケーション(Gaudiy Fanlink)のインフラ環境をKubernetes(k8s)環境に乗せ替えることで解決しました。

※ただし、OpenTelemetry Collectorの問題を解消するためにk8sに移行した訳ではなく、複合的な要因でk8sに移行しました。詳細については「Kubernetes初学者が担当したGKE移行プロセスの全貌」に書かれているので是非みてください!

1-2. 監視: GCP → Datadog

また、MonitoringツールもGCPの監視サービス(Operations Suite, Cloud Trace/Metrics/Logging)を使っていたのですが、Datadogに乗り換えました。Datadogはとても高機能でGCPやGithubなど多種多様なアプリケーションへのIntegrationがあることや、直感的に操作できるUXが使いやすく乗り換えを決意しました。

元々、全てのアプリケーションはOpenTelemetryの仕様に準拠していたので、Datadogへの移行は、GCP OpenTelemetry Exporterを使ってGCPへExportしていたのを、Collector経由でDatadogにExportするだけで済みました。これがOpenTelemetryの強みであり、予め準拠しておいて本当によかったなと思います。

前回のPoCと現在の構成

2. OpenTelemetry Collectorの導入

2-1. Kubernetesリソースの定義

GaudiyではKubernetesのリソース設定にKustomize × Helmを使っています。以下のKustomize YAMLをArgoCDがGKEに適用しています。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

metadata:
  name: opentelemetry-collector

namespace: opentelemetry-system

helmCharts:
- name: opentelemetry-collector
  repo: https://open-telemetry.github.io/opentelemetry-helm-charts
  releaseName: opentelemetry-collector
    ...

patches:
    - patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/image
        value: ${CUSTOM_OTEL_COLLECTOR_PATH} # カスタムしたOTel Collectorを使用
  - path: ./configmap.yaml # ConfigMapにPatchを充てる
    target:
      version: v1
      kind: ConfigMap
        ~省略~

2-2. Custom OTel Collectorの用意

OpenTelemetry CollectorではDistributionとしてCoreContrib が用意されており、Coreはextensions/connectors/receivers/processors/exporters のベース部分が備わっており、Contribは加えてOTel開発者以外のサードパーティ等が開発した機能(Component)が備わっています。

しかし、OTel Collectorアンチパターンの記事にあるようにCoreもContribも本番環境で使うのは推奨されておらず、アプリケーションに必要なComponentを適時選んで独自でビルドして使うことが勧められています。Gaudiyも独自にビルドしたImageをCUSTOM_OTEL_COLLECTOR_PATH にPushして使っています。

以下はCollectorで使用するComponentのManifestになります。

dist:
  module: github.com/gaudiy/**
  name: otelcol
  description: "Gaudiy OpenTelemetry Collector distribution"
  otelcol_version: "v0.95.0"
  output_path: out
  version: "v0.95.0"

receivers:
  - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.95.0

connector:
  - gomod: go.opentelemetry.io/collector/connector/forwardconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/datadogconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/routingconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/servicegraphconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.95.0

processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.95.0
  - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor v0.95.0

exporters:
  - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter v0.95.0

extensions:
  - gomod: go.opentelemetry.io/collector/extension/zpagesextension v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension v0.95.0

↑のmanifestをOpenTelemetry Collector Builder(ocb)を使ってビルドしています。

ビルドの手順については公式ドキュメントに書いてあるので、是非そちらを見てみてください。

2-3. CollectorのConfigを設定

ConfigMapにCollectorの設定値を書いています。以前の記事で紹介したreceiverやprocessorなどのフィールドを設定しています。

(一部のパラメータのみ載せています)

apiVersion: v1
kind: ConfigMap
metadata:
  name: opentelemetry-collector-**
data:
  relay: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318

        ~省略~

        processors:
      transform:
        error_mode: ignore
        log_statements:
          - context: scope
            statements:
              # DataDog cannot pick up the span name as resource on
              # Service Entry Spans. We append it here. This might have a
              # side effect to overwrite an application that is instrumented with
              # DataDog SDK. However, as long as OpenTelemetry SDK is used to
              # instrument, this transformation will help.
              - set(attributes["resource.name"], name)
          # - context: log
          #   statements:
          #     - set(attributes["env"], attributes["deployment.environment"])

        ~省略~

        exporters:
        datadog:
          api:
            key: ${env:DD_API_KEY}
            fail_on_invalid_key: true
          host_metadata:
            enabled: true
          traces:
            span_name_as_resource_name: true
            compute_stats_by_span_kind: true
            peer_service_aggregation: true
          metrics:
            resource_attributes_as_tags: true
            histograms:
              mode: counters
              send_aggregation_metrics: true
          logs:
            dump_payloads: true

        extensions:
      health_check: ~
      memory_ballast: ~
      zpages: ~

        service:
      telemetry:
        metrics:
          address: 0.0.0.0:8888

      extensions:
        - health_check
        - memory_ballast
        - zpages

      pipelines:
        traces:
          receivers:
            - otlp
          processors:
            - resourcedetection
            - k8sattributes
            - attributes/trace
            - transform
            - batch
          exporters:
            - datadog

        metrics:
                ~省略~

        logs:
                ~省略~

変わった設定は特にしていないのですが、特筆すべきところは以下の3つです。

2-3-1. exportersにdatadogを設定

DatadogのAPI KeyをSecretで定義しておいて、ENVに渡して設定しています。

そして、.service.pipelines.traces.exportersにもdatadogを使うことを指定します。tracesだけでなくmetrics/logsでも同様に指定しています。

exporters:
  datadog:
    api:
      key: ${env:DD_API_KEY}
      fail_on_invalid_key: true

service:
  pipelines:
    traces:
      exporters:
        - datadog

2-3-2. processorsでDatadog用にAttributeを設定する

DatadogではSpanにresource.nameの属性を設定する必要があるのですが、OTelの仕様ではname という属性で扱われています。解決方法として、processorsでDatadogのspecに合わせて加工しています。

先ほどのCustom OTel Collectorの所でtransformprocessorというcomponentを導入しており、ConfigMapにてtransformの処理を書いています。

processors:
    transform:
      error_mode: ignore
      log_statements:
        - context: scope
          statements:
            - set(attributes["resource.name"], name)

2-3-3. Loggingは社内独自ライブラリを使用

GoのLogging SDKは公式でもまだIn developmentのステータスですが、Javaなどは既にSDKが提供されている関係でProtocol Buffersの定義は存在したので、それをベースに社内でzapのadaptorを実装し先行して使っています。

この記事の公開時点では公式にもGoのSDK実装が徐々に入りつつあり、APIが安定した段階で移行する予定です。

以上の設定で、OTel Collectorを立てています。

2-4. アプリケーションにてExport先を設定する

実際のアプリケーションコードは複雑なので割愛しますが、OpenTelemetryのデモ用のコードサンプルを例にすると以下のようなコードになります。

// Collectorのアドレスを設定
// http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317
otelAgentAddr, ok := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
if !ok {
    otelAgentAddr = "0.0.0.0:4317"
}

// Metricsの設定
metricExp, err := otlpmetricgrpc.New(
    ctx,
    otlpmetricgrpc.WithInsecure(),
    otlpmetricgrpc.WithEndpoint(otelAgentAddr))
handleErr(err, "failed to create the collector metric exporter")

meterProvider := sdkmetric.NewMeterProvider(
    sdkmetric.WithResource(res),
    sdkmetric.WithReader(
        sdkmetric.NewPeriodicReader(
            metricExp,
            sdkmetric.WithInterval(2*time.Second),
        ),
    ),
)
otel.SetMeterProvider(meterProvider)

// Traceの設定
traceClient := otlptracegrpc.NewClient(
    otlptracegrpc.WithInsecure(),
    otlptracegrpc.WithEndpoint(otelAgentAddr),
    otlptracegrpc.WithDialOption(grpc.WithBlock()))
traceExp, err := otlptrace.New(ctx, traceClient)
handleErr(err, "failed to create the collector trace exporter")

bsp := sdktrace.NewBatchSpanProcessor(traceExp)
tracerProvider := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(sdktrace.AlwaysSample()),
    sdktrace.WithResource(res),
    sdktrace.WithSpanProcessor(bsp),
)

// set global propagator to tracecontext (the default is no-op).
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
otel.SetTracerProvider(tracerProvider)

OTel Trace/MetricsのGo SDKにExport先としてOTel Collectorのアドレスを渡すだけで設定は完了です。

TraceやMetricsはサンプルコードのようにして、好きな箇所で計装することができます。

// Metrics
meter := otel.Meter("demo-server-meter")
serverAttribute := attribute.String("server-attribute", "foo")
commonLabels := []attribute.KeyValue{serverAttribute}
requestCount, _ := meter.Int64Counter(
    "demo_server/request_counts",
    metric.WithDescription("The number of requests received"),
)
requestCount.Add(ctx, 1, metric.WithAttributes(commonLabels...))

// Trace
span := trace.SpanFromContext(ctx)
bag := baggage.FromContext(ctx)
var baggageAttributes []attribute.KeyValue
baggageAttributes = append(baggageAttributes, serverAttribute)
for _, member := range bag.Members() {
    baggageAttributes = append(baggageAttributes, attribute.String("baggage key:"+member.Key(), member.Value()))
}
span.SetAttributes(baggageAttributes...)

3. (おまけ)opentelemetry-go-instrumentationを試す

OTelではAuto Instrumentation(自動計装)という、組み込むとテレメトリーデータを自動計装してくれるライブラリを出しています。自動計装は言語毎にメカニズムが異なり、各言語毎にライブラリが作られています。

Go言語はまだ開発段階ではありますが opentelemetry-go-instrumentation が出ており、今回は開発環境で試験的に導入してみたのでその詳細について書こうと思います。

※ただし、まだ開発段階なのでLinuxディストリビューションやGoのバージョンによっては動作しない場合があります。

3-1. OpenTelemetry Operatorの利用

OpenTelemetry Operatorは、k8sクラスター内でOpenTelemetryコンポーネントを管理するoperatorです。今回は opentelemetry-go-instrumentation をSidecarで立てるためにoperatorを使います。

まずはhelmを使いリソースの定義をします。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

metadata:
  name: opentelemetry-operator

namespace: opentelemetry-operator-system

helmCharts:
- name: opentelemetry-operator
  repo: https://open-telemetry.github.io/opentelemetry-helm-charts
  releaseName: opentelemetry-operator
  version: 0.x
  namespace: opentelemetry-operator-system
  valuesInline:
    pdb:
      create: true
    manager:
      featureGates: "+operator.autoinstrumentation.go"
    kubeRBACProxy:
      image:
        tag: v0.14.3
  includeCRDs: true

featureGatesとして"operator.autoinstrumentation.go" を有効にしておきます。

3-2. アプリケーション側の設定

アプリケーション側にはSidecarをinjectするアノテーションを付けます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  replicas: 1
  template:
    metadata:
      labels:
        app: my-app
      annotations:
        instrumentation.opentelemetry.io/inject-go: "true"
        instrumentation.opentelemetry.io/otel-go-auto-target-exe: "/path/to/container/executable"

また、OTel OperatorがアプリケーションにSidecarをInjectできるようInstrumentationのリソースを作ります。

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: post-service
  namespace: post-service
spec:
  exporter:
    endpoint: http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317
  go:
    image: ${AUTO_INSTRUMENTATION_IMAGE_PATH}
    resourceRequirements:
      limits:
        cpu: 500m
        memory: 2Gi
      requests:
        cpu: 500m
        memory: 1Gi
    env:
      - name: OTEL_EXPORTER_OTLP_ENDPOINT
        value: http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317

これによりGoにてAuto Instrumentが可能になりました。

4. 今後の方針

以上により本番環境でOTel Collector経由でDatadogにExportすることができました。ただ、まだまだ課題もあるので今後の方針としていくつか挙げたいと思います。

  1. 可用性: Collectorの構成をAgent modeGateway mode両方使って可用性高い構成にしたいと思っているのですが、費用面と現状のアプリケーションの規模を見て意図的に対応していません。将来的には変えていきたいと思っています。(OTel Collectorアンチパターンの記事にも記載されています)
  2. 機能面: 今の所、最低限のSpanしか登録していない & Attributeも上手く使いこなせていないので、目的に合わせた計装の最適化を行っていきたいと思っています。

5. まとめ

今回はGCPからDatadogへの移行、OpenTelemetry Collectorの導入、そしてopentelemetry-go-instrumentationのテスト及びOpenTelemetry Operatorの利用といった一連のステップを通じて、効率的かつ効果的なテレメトリーデータの収集と分析方法を探求しました。この記事を通して、これからOpenTelemetryを導入しようとしている人の参考になると嬉しいです。

またGaudiyとしては、前回の記事が出てから1年経ち、紆余曲折ありましたがOpenTelemetryを実運用に取り込むことができてObservabilityの向上にむけてかなり前進したのではないかと思っています。

始めた当初、DevメンバーがTrace/Metricsを見て能動的にボトルネックの改善やアプリケーション監視に役立ててくれると良いなと思っていたのですが、その兆候が社内でも見られ始めているので、今回の取り組みは本当にやって良かったと思っています。

ボトルネック調査をして下さったSlackの投稿

Observablityの向上に取り組んでいる人や、OpenTelemetryに関心がある人がいたら、ぜひカジュアルにお話ししましょう。

site.gaudiy.com

Gaudiyではエンジニアを積極採用中なので、興味ある人はぜひこちらもご覧ください。

site.gaudiy.com