Open12

OpenTelemetryを使ってJavaのヒープサイズを AWSのCloudWatchMetricsに送信する

akym03akym03

Collectorの出力先は環境変数で定義している通りに、コンソールに出てくる。

$ export JAVA_TOOL_OPTIONS="-javaagent:PATH/TO/opentelemetry-javaagent.jar" \
  OTEL_TRACES_EXPORTER=logging \
  OTEL_METRICS_EXPORTER=logging \
  OTEL_LOGS_EXPORTER=logging \
  OTEL_METRIC_EXPORT_INTERVAL=15000

起動時に設定している環境変数のうち、Collectorの出力先に関わってくるのは下の3つ。

  • OTEL_TRACES_EXPORTER
  • OTEL_METRICS_EXPORTER
  • OTEL_LOGS_EXPORTER

役割は Environment Variable Specificationにある通り。

  • OTEL_TRACES_EXPORTER はトレースの出力先
  • OTEL_METRICS_EXPORTER はメトリクスの出力先
  • OTEL_LOGS_EXPORTER はログの出力先

値の意味は、ざっくり理解すると、 otlp は OpenTelemetry Protocol でバックエンドに送信、
console は 標準出力。 loggingも標準出力だけど、後方互換のために使われている。出力しないなら none を指定する。

akym03akym03

実際に出力されたメトリクスの一つ。長い・・・

[otel.javaagent 2024-11-03 17:49:12:993 +0900] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - metric: ImmutableMetricData{resource=Resource{schemaUrl=https://opentelemetry.io/schemas/1.24.0, attributes={host.arch="amd64", host.name="my-pc-name", os.description="Windows 10 10.0", os.type="windows", process.command_line="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe -javaagent:E:\open-telemetry\opentelemetry-javaagent.jar -Dfile.encoding=UTF-8 otel.DiceApplication", process.executable.path="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe", process.pid=5752, process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 11.0.3+7-LTS", process.runtime.name="OpenJDK Runtime Environment", process.runtime.version="11.0.3+7-LTS", service.instance.id="d9287b78-acc2-40fc-a6bb-7da8310978f6", service.name="unknown_service:java", telemetry.distro.name="opentelemetry-java-instrumentation", telemetry.distro.version="2.9.0", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.43.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=io.opentelemetry.runtime-telemetry-java8, version=2.9.0-alpha, schemaUrl=null, attributes={}}, name=jvm.memory.limit, description=Measure of max obtainable memory., unit=By, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1730623707947767900, epochNanos=1730623752967338300, attributes={jvm.memory.pool.name="G1 Old Gen", jvm.memory.type="heap"}, value=17125343232, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1730623707947767900, epochNanos=1730623752967338300, attributes={jvm.memory.pool.name="CodeHeap 'profiled nmethods'", jvm.memory.type="non_heap"}, value=122028032, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1730623707947767900, epochNanos=1730623752967338300, attributes={jvm.memory.pool.name="CodeHeap 'non-nmethods'", jvm.memory.type="non_heap"}, value=7602176, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1730623707947767900, epochNanos=1730623752967338300, attributes={jvm.memory.pool.name="CodeHeap 'non-profiled nmethods'", jvm.memory.type="non_heap"}, value=122028032, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1730623707947767900, epochNanos=1730623752967338300, attributes={jvm.memory.pool.name="Compressed Class Space", jvm.memory.type="non_heap"}, value=1073741824, exemplars=[]}], monotonic=false, aggregationTemporality=CUMULATIVE}}

akym03akym03

見ずらいので適宜、改行

Metric: jvm.memory.limitの値。

 ImmutableMetricData {
  resource=Resource {
    schemaUrl=https://opentelemetry.io/schemas/1.24.0,
    attributes={
      host.arch="amd64",
      host.name="my-pc-name",
      os.description="Windows 10 10.0",
      os.type="windows",
      process.command_line="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe -javaagent:E:\open-telemetry\opentelemetry-javaagent.jar -Dfile.encoding=UTF-8 otel.DiceApplication",
      process.executable.path="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe",
      process.pid=5752,
      process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 11.0.3+7-LTS",
      process.runtime.name="OpenJDK Runtime Environment",
      process.runtime.version="11.0.3+7-LTS",
      service.instance.id="d9287b78-acc2-40fc-a6bb-7da8310978f6",
      service.name="unknown_service:java",
      telemetry.distro.name="opentelemetry-java-instrumentation",
      telemetry.distro.version="2.9.0",
      telemetry.sdk.language="java",
      telemetry.sdk.name="opentelemetry",
      telemetry.sdk.version="1.43.0"
    }
  },
  instrumentationScopeInfo=InstrumentationScopeInfo{
    name=io.opentelemetry.runtime-telemetry-java8,
    version=2.9.0-alpha,
    schemaUrl=null,
    attributes={}
  },
  name=jvm.memory.limit,
  description=Measure of max obtainable memory.,
  unit=By,
  type=LONG_SUM,
  data=ImmutableSumData{
    points=[
      ImmutableLongPointData{
        startEpochNanos=1730623707947767900,
        epochNanos=1730623752967338300,
        attributes={
          jvm.memory.pool.name="G1 Old Gen",
          jvm.memory.type="heap"
        },
        value=17125343232,
        exemplars=[]
      },
      ImmutableLongPointData{
        startEpochNanos=1730623707947767900,
        epochNanos=1730623752967338300,
        attributes={
          jvm.memory.pool.name="CodeHeap 'profiled nmethods'",
          jvm.memory.type="non_heap"
        },
        value=122028032,
        exemplars=[]
      },
      ImmutableLongPointData{
        startEpochNanos=1730623707947767900,
        epochNanos=1730623752967338300,
        attributes={
          jvm.memory.pool.name="CodeHeap 'non-nmethods'",
          jvm.memory.type="non_heap"
        },
        value=7602176,
        exemplars=[]
      },
      ImmutableLongPointData{
        startEpochNanos=1730623707947767900,
        epochNanos=1730623752967338300,
        attributes={
          jvm.memory.pool.name="CodeHeap 'non-profiled nmethods'",
          jvm.memory.type="non_heap"
        },
        value=122028032,
        exemplars=[]
      },
      ImmutableLongPointData{
        startEpochNanos=1730623707947767900,
        epochNanos=1730623752967338300,
        attributes={
          jvm.memory.pool.name="Compressed Class Space",
          jvm.memory.type="non_heap"
        },
        value=1073741824,
        exemplars=[]
      }
    ],
    monotonic=false,
    aggregationTemporality=CUMULATIVE
  }
}
akym03akym03

1度の出力で得られたメトリクスは下の13個。
https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/Experimental を除いた12個 と tomcat-7

JVM Memory

  • jvm.memory.used
  • jvm.memory.committed
  • jvm.memory.limit
  • jvm.memory.used_after_last_gc

JVM Garbage Collection

  • jvm.gc.duration

JVM Threads

  • jvm.thread.count

JVM Classes

  • jvm.class.loaded
  • jvm.class.unloaded
  • jvm.class.count

JVM CPU

  • jvm.cpu.time
  • jvm.cpu.count
  • jvm.cpu.recent_utilization

tomcat-7

  • io.opentelemetry.tomcat-7.0

メトリクスのドキュメントは探せなかったけど、ソースは見つかった。 https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/tomcat

具体的な値は、これ。

ImmutableMetricData{
  resource=Resource{
    schemaUrl=https://opentelemetry.io/schemas/1.24.0,
    attributes={
      host.arch="amd64",
      host.name="DESKTOP-O9APPFJ",
      os.description="Windows 10 10.0",
      os.type="windows",
      process.command_line="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe -javaagent:E:\open-telemetry\opentelemetry-javaagent.jar -Dfile.encoding=UTF-8 otel.DiceApplication",
      process.executable.path="E:\amazon-corretto\jdk11.0.3_7\bin\java.exe",
      process.pid=5752,
      process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 11.0.3+7-LTS",
      process.runtime.name="OpenJDK Runtime Environment",
      process.runtime.version="11.0.3+7-LTS",
      service.instance.id="d9287b78-acc2-40fc-a6bb-7da8310978f6",
      service.name="unknown_service:java",
      telemetry.distro.name="opentelemetry-java-instrumentation",
      telemetry.distro.version="2.9.0",
      telemetry.sdk.language="java",
      telemetry.sdk.name="opentelemetry",
      telemetry.sdk.version="1.43.0"
    }
  },
  instrumentationScopeInfo=InstrumentationScopeInfo{
    name=io.opentelemetry.tomcat-7.0,
    version=2.9.0-alpha,
    schemaUrl=null,
    attributes={}
  },
  name=http.server.request.duration,
  description=Duration of HTTP server requests.,
  unit=s,
  type=HISTOGRAM,
  data=ImmutableHistogramData{
    aggregationTemporality=CUMULATIVE,
    points=[
      ImmutableHistogramPointData{
        getStartEpochNanos=1730623707947767900,
        getEpochNanos=1730623752967338300,
        getAttributes=FilteredAttributes{
          http.request.method=GET,http.response.status_code=404,http.route=/**,network.protocol.version=1.1,url.scheme=http
        },
        getSum=0.0639274,
        getCount=2,
        hasMin=true,
        getMin=0.006074,
        hasMax=true,
        getMax=0.0578534,
        getBoundaries=[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0],
        getCounts=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        getExemplars=[
          ImmutableDoubleExemplarData{
            filteredAttributes={
              client.address="127.0.0.1",
              network.peer.address="127.0.0.1",
              network.peer.port=62520,
              server.address="localhost",
              server.port=8080,
              url.path="/favicon.ico",
              user_agent.original="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0"
            },
            epochNanos=1730623749634000000,
            spanContext=ImmutableSpanContext{
              traceId=5fc5ed7f2be50133264377124a619657,
              spanId=0ffcf3c35ea514d7,
              traceFlags=01,
              traceState=ArrayBasedTraceState{
                entries=[]
              },
              remote=false,
              valid=true
            },
            value=0.006074
          }
        ]
      },
      ImmutableHistogramPointData{
        getStartEpochNanos=1730623707947767900,
        getEpochNanos=1730623752967338300,
        getAttributes=FilteredAttributes{
          http.request.method=GET,
          http.response.status_code=200,
          http.route=/rolldice,network.protocol.version=1.1,
          url.scheme=http
        },
        getSum=0.1518083,
        getCount=11,
        hasMin=true,
        getMin=0.0021692,
        hasMax=true,
        getMax=0.1254785,
        getBoundaries=[0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0],
        getCounts=[10, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        getExemplars=[
          ImmutableDoubleExemplarData{
            filteredAttributes={
              client.address="127.0.0.1",
              network.peer.address="127.0.0.1",
              network.peer.port=62521,
              server.address="localhost",
              server.port=8080,
              url.path="/rolldice",
              user_agent.original="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0"
            },
            epochNanos=1730623752703000000,
            spanContext=ImmutableSpanContext{
              traceId=19013ea924101886bdb21bc1c74b5c50,
              spanId=9bff9f2f5fb72848,
              traceFlags=01,
              traceState=ArrayBasedTraceState{
                entries=[]
              },
              remote=false,
              valid=true
            },
            value=0.0025629
          }
        ]
      }
    ]
  }
}
akym03akym03

-javaagent:PATH/TO/opentelemetry-javaagent.jar で指定したエージェントが Collectorに送信していることを確認できたので、AWsに送信したい。

エージェントは Auto-Instrumentation for Traces and Metrics with the Java agentInstallationlatest version. に設定されたリンクからダウンロード。リンク先はGitHubの リリースページ。

https://github.com/aws-observability/aws-otel-java-instrumentation/releases

Collectorは AWS Distro for OpenTelemetry を使う。

akym03akym03

Javaを起動する差分は jar コマンドの引数 -javaagen と 環境変数。

-javaagen オプションのjarファイルを事前にダウンロードした aws-opentelemetry-agent.jar に変更する。

-javaagent:E:\open-telemetry\aws-opentelemetry-agent.jar

環境変数は下の3つを削除してデフォルトの otlp で動作させるか、明示的にotlp を指定する。そうすることで エージェントが メトリクスを Collectorに送信する。

  • OTEL_TRACES_EXPORTER
  • OTEL_METRICS_EXPORTER
  • OTEL_LOGS_EXPORTER

追加する環境変数は1つ。

  • OTEL_RESOURCE_ATTRIBUTES="service.name=MyApp,service.namespace=MyTeam "

ここで指定した値はCloudWatchMetricsの名前空間に使われる。 Spring Boot starterを利用しているのなら、 application.yaml に定義することができる。

akym03akym03

Collectorの AWS Distro for OpenTelemetry をDcokerで起動するのは、docker-demo.mdを参考にして、下のコマンドで実行した

docker run --rm -p 4317:4317 -p 55680:55680 -p 8889:8888 \
      -e "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" \
      -e "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" \
      -e "AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" \
      -e AWS_REGION=ap-northeast-1 \
      -v .\otel-local-config.yaml:/otel-local-config.yaml \
      --name awscollector public.ecr.aws/aws-observability/aws-otel-collector:latest \
      --config /otel-local-config.yaml

otel-local-config.yamlhttps://github.com/aws-observability/aws-otel-collector/blob/main/config/ecs/ecs-default-config.yaml を参考にして、 awsemf の namespaceとlog_group_nameを削除した。

extensions:
  health_check:
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  awsxray:
    endpoint: 0.0.0.0:2000
    transport: udp
  statsd:
    endpoint: 0.0.0.0:8125
    aggregation_interval: 60s

processors:
  batch/traces:
    timeout: 1s
    send_batch_size: 50
  batch/metrics:
    timeout: 60s

exporters:
  awsxray:
  awsemf:

service:
  pipelines:
    traces:
      receivers: [otlp,awsxray]
      processors: [batch/traces]
      exporters: [awsxray]
    metrics:
      receivers: [otlp, statsd]
      processors: [batch/metrics]
      exporters: [awsemf]

  extensions: [health_check]
akym03akym03

CloudWatch Metricsに送られてきました。
CloudWatch Metricsのメトリクス一覧

メトリクス名は 環境変数で設定したサービスのnamespaceとnameが使われています。

OTEL_RESOURCE_ATTRIBUTES="service.name=MyApp,service.namespace=MyTeam"

MyTeam/MyApp を選択すると、いっぱいあります。

サービス名配下のメトリクス

今回必要なメトリクスは OTelLib にありました。

JVMのメトリクス

グラフにしてみます。1分より細かいメトリクスは取れていない(送られていない?)。これは要調査。
memoryのlimitとusage

akym03akym03

CloudWatch Logs の ロググループ /metrics/MyTeam/MyApp に メトリクスの値が記録されていた。

{
    "OTelLib": "io.opentelemetry.runtime-telemetry-java8",
    "Version": "1",
    "_aws": {
        "CloudWatchMetrics": [
            {
                "Namespace": "MyTeam/MyApp",
                "Dimensions": [
                    [
                        "pool",
                        "type",
                        "OTelLib"
                    ],
                    [
                        "OTelLib"
                    ],
                    [
                        "OTelLib",
                        "pool"
                    ],
                    [
                        "OTelLib",
                        "type"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "process.runtime.jvm.memory.usage_after_last_gc",
                        "Unit": "Bytes"
                    },
                    {
                        "Name": "process.runtime.jvm.memory.usage",
                        "Unit": "Bytes"
                    },
                    {
                        "Name": "process.runtime.jvm.memory.init",
                        "Unit": "Bytes"
                    },
                    {
                        "Name": "process.runtime.jvm.memory.committed",
                        "Unit": "Bytes"
                    }
                ]
            }
        ],
        "Timestamp": 1730722219636
    },
    "pool": "G1 Eden Space",
    "process.runtime.jvm.memory.committed": 0,
    "process.runtime.jvm.memory.init": 0,
    "process.runtime.jvm.memory.usage": 142606336,
    "process.runtime.jvm.memory.usage_after_last_gc": 0,
    "type": "heap"
}

GCのアルゴリズムが G1GC なので、メモリプールごとにメトリクスが記録されていた。

メトリクスの Attributeの意味を理解した。
OpenTelemetryのJVM-Memory

type と pool の組み合わせは下の通りであった。

type pool
heap G1 Eden Space
heap G1 Old Gen
heap G1 Survivor Space
non_heap CodeHeap 'non-nmethods'
non_heap CodeHeap 'non-profiled nmethods'
non_heap CodeHeap 'profiled nmethods'
non_heap Compressed Class Space
non_heap Metaspace
akym03akym03

メトリクスは量が多いので、CloudWatch Application Signals を使いたい所。

ひとまずは、ここまで。