OpenTelemetry Collector で任意のカスタムメトリクスを受信する

Blog posts are not updated after publication. This post is more than a year old, so its content may be outdated, and some links may be invalid. Cross-verify any information before relying on it.

OpenTelemetry(OTel)は “未知の未知” のトラブルシューティングや対処に役立つだけでなく、ディスク使用量、サーバーの可用性、SSL 証明書の有効期限といったシステムメトリクスの監視のような定型作業の管理にも有用です。 これは、OpenTelemetry Collector で利用可能な 90以上のレシーバーのいずれかを活用することで実現できます。 たとえば、Host Metrics Receiver や HTTP Check Receiver があります。

しかし、利用可能なレシーバーが特定のニーズを満たさない場合はどうすればよいでしょうか。 カスタムメトリクスを提供するシェルスクリプトのコレクションがあり、それらを OpenTelemetry Collector にエクスポートしたいとします。 独自のレシーバーを書くこともできますが、それには Go の知識が必要です。

その道を進む前に、利用可能なレシーバーをもう少し詳しく調べてみてください。 一部のレシーバーは、さまざまな形式のメトリクスを取り込むことができます。 たとえば、Carbon、StatsD、InfluxDB、Prometheus、さらには SNMP などがあり、これらを OpenTelemetry エコシステムに統合できます。 シェルスクリプトに少し手を加えるだけで、これらのレシーバーのいずれかを使って目的を達成できます。 たとえば、Carbon Receiver は、シンプルなプレーンテキストプロトコルを持つため、シェルスクリプトとの組み合わせに最適です。 そのプロトコルは非常にわかりやすいものです。

プレーンテキストプロトコルは、Carbon がサポートする最も単純なプロトコルです。 送信されるデータは次の形式でなければなりません。 <metric path> <metric value> <metric timestamp>

スクリプトの例: 証明書の有効期限の確認

次のシェルスクリプトは、ホスト名を引数として受け取り、openssl s_client を使用して証明書を取得し、証明書の有効期限までの残り時間を計算します。

#!/bin/bash
HOST=${1}
PORT=${2:-443}

now=$(date +%s)
notAfterString=$(echo q | openssl s_client -servername "${HOST}" "${HOST}:${PORT}" 2>/dev/null | openssl x509 -noout -enddate | awk -F"=" '{ print $2; }')
if [[ "$(uname)" == "Darwin" ]] ; then
  notAfter=$(date -j -f "%b %d %H:%M:%S %Y %Z" "${notAfterString}" +%s)
else
  notAfter=$(date -d "${notAfterString}" +%s)
fi

secondsLeft=$(($notAfter-$now))

echo ${secondsLeft}

このスクリプトは次のようにテストできます。

$ ./ssl_check.sh opentelemetry.io
4357523

Carbon のプレーンテキストプロトコルを使う

このスクリプトを Carbon のプレーンテキストプロトコルに対応させるには、スクリプトの最後の数行を変更して、Carbon 形式でメトリクスを出力する必要があります。

#!/bin/bash
HOST=${1}
PORT=${2:-443}

now=$(date +%s)
str=$(echo q | openssl s_client -servername "${HOST}" "${HOST}:${PORT}" 2>/dev/null | openssl x509 -noout -enddate | awk -F"=" '{ print $2; }')
if [[ "$(uname)" == "Darwin" ]] ; then
  notAfter=$(date -j -f "%b %d %H:%M:%S %Y %Z" "${notAfterString}" +%s)
else
  notAfter=$(date -d "${notAfterString}" +%s)
fi

secondsLeft=$(($notAfter-$now))

metricPath="tls.server.not_after.time_left;unit=s"
echo "${metricPath} ${secondsLeft} ${now}"

これにより、スクリプトは <metric path> を tls.server.not_after.time_left;unit=s、<metric value> を ${secondsLeft}、<metric timestamp> を ${now} として出力します。

これで、Carbon Receiver を有効にした OpenTelemetry Collector にメトリクスを送信する準備が整いました。

OTel Collector で任意のメトリクスを受信する

これをテストするには、次の設定で OpenTelemetry Collector を起動します。

receivers:
  carbon:
    endpoint: localhost:8080
    transport: tcp
    parser:
      type: plaintext
      config:

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    metrics:
      receivers: [carbon]
      exporters: [debug]

たとえば、このファイルを collector-config.yml として保存した場合、次のコマンドを実行します。

$ ./otelcol --config collector-config.yml
2023-11-24T12:52:51.340+0100	info	service@v0.89.0/telemetry.go:85	Setting up own telemetry...
2023-11-24T12:52:51.341+0100	info	service@v0.89.0/telemetry.go:202	Serving Prometheus metrics	{"address": ":8888", "level": "Basic"}
2023-11-24T12:52:51.341+0100	info	exporter@v0.89.0/exporter.go:275	Development component. May change in the future.	{"kind": "exporter", "data_type": "metrics", "name": "debug"}
2023-11-24T12:52:51.341+0100	info	service@v0.89.0/service.go:143	Starting otelcol-any-metric...	{"Version": "1.0.0", "NumCPU": 10}
2023-11-24T12:52:51.341+0100	info	extensions/extensions.go:34	Starting extensions...
2023-11-24T12:52:51.342+0100	info	service@v0.89.0/service.go:169	Everything is ready. Begin running and processing data.

OpenTelemetry Collector が動作したら、別のシェルを開いてメトリクスを送信します。

./ssl_check.sh opentelemetry.io | nc 127.0.0.1 8080

Debug Exporter がコンソールにメトリクスを表示します。

2023-11-24T12:54:51.369+0100	info	ResourceMetrics #0
Resource SchemaURL:
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope
Metric #0
Descriptor:
     -> Name: tls.server.not_after.time_left
     -> Description:
     -> Unit:
     -> DataType: Gauge
NumberDataPoints #0
Data point attributes:
     -> unit: Str(s)
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2023-11-24 11:54:51 +0000 UTC
Value: 4356471
	{"kind": "exporter", "data_type": "metrics", "name": "debug"}

これで完了です! 同じ手法を、カスタムメトリクスを報告する他のシェルスクリプトにも適用できます。

Transform Processor による微調整

Carbon Receiver は <metric path> を ; をデリミタとして分割し、メトリクス名(最初の項目)とデータポイント属性(それ以外の項目)を抽出します。 この例では、メトリクス名は tls.server.not_after.time_left となり、データポイント属性 unit: Str(s) が設定されます。

この方法はわかりやすいですが、リソース、メトリクスの説明、特にメトリクスの単位など、その他の詳細を設定することはできません。

しかし、Transform Processor がこれを解決してくれます。 OpenTelemetry Transformation Language(OTTL)ステートメントを collector-config.yml に組み込むことで、データポイント属性 unit をメトリクスの単位に変換できます。

receivers:
  carbon:
    endpoint: localhost:8080
    transport: tcp
    parser:
      type: plaintext
      config:

exporters:
  debug:
    verbosity: detailed

processors:
  transform:
    metric_statements:
      - context: datapoint
        statements:
          - set(metric.unit, attributes["unit"])
          - delete_key(attributes, "unit")
service:
  pipelines:
    metrics:
      receivers: [carbon]
      processors: [transform]
      exporters: [debug]

ssl_check.sh をもう一度実行します。

./ssl_check.sh opentelemetry.io | nc 127.0.0.1 8080

これで、Debug Exporter がメトリクスの記述子に単位も含めて表示します。

2023-11-24T12:54:51.369+0100	info	ResourceMetrics #0
Resource SchemaURL:
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope
Metric #0
Descriptor:
     -> Name: tls.server.not_after.time_left
     -> Description:
     -> Unit: s
     -> DataType: Gauge
NumberDataPoints #0
Data point attributes:
     -> unit: Str(s)
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2023-11-24 11:54:51 +0000 UTC
Value: 4356471
	{"kind": "exporter", "data_type": "metrics", "name": "debug"}

メトリクスを OpenTelemetry で計装したサービスに関連付けたい場合は、まずシェルスクリプトにデータポイント属性として service.name と service.namespace を追加します。

metricName="tls.server.not_after.time_left;unit=s;service.name=otel-webserver;service.namespace=opentelemetry.io"
echo "${metricName} ${secondsLeft} ${now}"

次に、別の OTTL ステートメントを追加して、これらのデータポイント属性からリソースを作成します。

processors:
  transform:
    metric_statements:
      - context: datapoint
        statements:
          - set(metric.unit, attributes["unit"])
          - set(resource.attributes["service.name"], attributes["service.name"])
          - set(resource.attributes["service.namespace"],
            attributes["service.namespace"])
          - delete_key(attributes, "unit")
          - delete_key(attributes, "service.name")
          - delete_key(attributes, "service.namespace")

ssl_check.sh を再度実行します。

./ssl_check.sh opentelemetry.io | nc 127.0.0.1 8080

これで、Debug Exporter がリソースと属性 service.name および service.namespace も含めて表示します。

2023-11-24T14:49:03.806+0100	info	ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(otel-webserver)
     -> service.namespace: Str(opentelemetry.io)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope
Metric #0
Descriptor:
     -> Name: tls.server.not_after.time_left
     -> Description:
     -> Unit: s
     -> DataType: Gauge
NumberDataPoints #0
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2023-11-24 13:49:03 +0000 UTC
Value: 4349619
	{"kind": "exporter", "data_type": "metrics", "name": "debug"}

Transform Processor と OTTL は幅広い機能を提供します。 詳しくは以下を参照してください。

これで、OpenTelemetry Collector で任意のカスタムメトリクスを受信する準備が整いました!

ボーナス: OTLP の使用

Carbon Receiver と Transform Processor の使用はカスタムメトリクスを収集するための確実な方法ですが、OpenTelemetry Protocol(OTLP)が必要なものをすべて提供しているのに、外部形式を使ってメトリクスを OpenTelemetry にインポートするのは少し変に思えるかもしれません。

Carbon Receiver を使うかわりに、OTLP JSON を使ってカスタムメトリクスを送信することもできます。

#!/bin/bash
URL=${1}
PORT=${2:-443}

now=$(date +%s)
notAfterString=$(echo q | openssl s_client -servername "${URL}" "${URL}:${PORT}" 2>/dev/null | openssl x509 -noout -enddate | awk -F"=" '{ print $2; }')
if [[ "$(uname)" == "Darwin" ]] ; then
  notAfter=$(date -j -f "%b %d %H:%M:%S %Y %Z" "${notAfterString}" +%s)
else
  notAfter=$(date -d "${notAfterString}" +%s)
fi

secondsLeft=$(($notAfter-$now))

data="
{
    \"resourceMetrics\": [
      {
        \"resource\": {
          \"attributes\": [
            {
              \"key\": \"service.name\",
              \"value\": {
                \"stringValue\": \"${URL}\"
              }
            }
          ]
        },
        \"scopeMetrics\": [
          {
            \"metrics\": [
              {
                \"name\": \"tls.server.not_after.time_left\",
                \"unit\": \"s\",
                \"description\": \"\",
                \"gauge\": {
                  \"dataPoints\": [
                    {
                      \"asInt\": ${secondsLeft},
                      \"timeUnixNano\": ${now}000000000
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  }
"
curl -X POST -H "Content-Type: application/json" -d "${data}" -i localhost:4318/v1/metrics

collectors-config で OTLP Receiver を有効にします。

receivers:
  otlp:
    protocols:
      http:
      grpc:
exporters:
  debug:
    verbosity: detailed
service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [debug]

更新した ssl_check.sh を実行します。

./ssl_check.sh opentelemetry.io

今回は、正しい単位が設定され、JSON で定義したとおりのリソースが含まれたメトリクスが表示されます。

2023-11-24T15:28:51.212+0100	info	ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(opentelemetry.io)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope
Metric #0
Descriptor:
     -> Name: tls.server.not_after.time_left
     -> Description:
     -> Unit: s
     -> DataType: Gauge
NumberDataPoints #0
StartTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2023-11-24 14:28:51 +0000 UTC
Value: 4347231
	{"kind": "exporter", "data_type": "metrics", "name": "debug"}

シェルスクリプトで JSON を扱うのは、この例が明確に示しているように、あまり望ましくありません! 改善する方法はありますが、最終的には Python や Node.js のような言語を使うか、お気に入りの OTel CLI ツールにメトリクス(ゲージサポート付き)を組み込む方が効率的だと感じるかもしれません!

まとめ

この記事では、Carbon Receiver のような 汎用 レシーバーを使って任意のメトリクスを OpenTelemetry Collector に取り込む方法を学びました。 利用可能なレシーバーがニーズを満たさず、Go で独自のレシーバーを書きたくない場合にこのアプローチを使ってください。

OTLP と curl を使って直接 OpenTelemetry Collector にメトリクスを送信する方法も学びました。 OpenTelemetry Collector のパイプラインを変更できない場合にこのアプローチを使ってください。 OTLP 経由でメトリクスをエクスポートするコマンドラインツールが利用可能になれば、汎用 レシーバーに代わる有効な選択肢にもなるでしょう。