OpenTelemetry 入门和部署

building-in-the-autumn

在维护现代分布式系统时,了解服务的运行状况和进行错误分析往往充满挑战。当客户请求横跨多个微服务、函数和基础设施时,如何能够清晰地洞察整个请求链路的健康状况与性能表现?这个问题的答案,就在于完善的服务可观测性

可观测性不仅仅是传统的监控(记录已知的故障模式),它赋予我们通过系统外部输出来探索、分析和理解系统内部状态的能力。这通常建立在三大支柱之上:指标(Metrics)链路(Traces)日志(Logs)

OpenTelemetry简介

在传统的可观测性实践中,应用程序需要分别使用不同的SDK对接各种监控平台,如Prometheus用于指标监控、Jaeger用于链路跟踪、Elasticsearch用于日志处理。在异构技术栈并存的微服务环境中,如何以一种统一、标准化的方式采集和关联这些运行数据,是一个复杂的难题。正是在这样的背景下,OpenTelemetry(简称OTel) 应运而生。它是一个由云原生计算基金会(CNCF)托管的开源项目,旨在提供一套与供应商无关的、统一的API、SDK和工具集,用于采集和导出遥测数据。

使用OpenTelemetry,实现可观测性目标的过程被大大简化。应用程序只需要对接OpenTelemetry,SDK将指标发送给OpenTelemetry,然后OpenTelemetry会负责进行数据处理,并转发给Prometheus、Jaeger和Elasticsearch等服务。OpenTelemetry的主要目标之一就是让任何编程语言、基础设施和运行环境中的应用程序和系统都易于进行观测。

具体来说,OpenTelemetry解决了3个核心问题:

  1. 定义一套标准——OTLP协议:OpenTelemetry Protocol (OTLP) 是OpenTelemetry项目定义的一种通用遥测数据传递协议。OTLP旨在为遥测数据(包括Traces、Metrics和Logs)的编码、传输和传递提供一个标准化的机制。它定义了数据从遥测源(如应用程序的SDK)到中间节点(如OpenTelemetry Collector收集器),再到最终遥测后端(如存储和分析系统)的整个过程,实现了标准化、高效性和通用性的metrics、traces、logs数据传递。OTLP推荐并主要使用Protocol Buffers (Protobuf)进行数据的序列化和gRPC传输,也支持其他序列化和HTTP传输方式。OTLP是OpenTelemetry生态系统的"通用语言",它通过定义标准的数据格式和传输机制,极大地简化了可观测性数据的采集和互操作性。

  2. 适配不同编程语言和框架的SDK:OpenTelemetry为各种主流编程语言(如Java、Go、Python、JavaScript等)提供了官方实现的SDK。开发者只需使用对应语言的SDK进行简单集成,就能自动或手动地从应用程序中采集遥测数据,并将其转换成符合OTLP标准的格式。这极大地简化了开发者的接入成本。

  3. 指标采集和处理服务——OpenTelemetry Collector:这是一个独立运行的代理服务,是OpenTelemetry架构中的"智能枢纽"。应用程序将OTLP格式的数据发送到Collector,由它来统一负责后续繁重的工作,例如:

    • 接收与转发:接收来自多个应用的数据,然后批量转发给一个或多个后端平台。
    • 处理与加工:对数据进行清洗、过滤、采样或富化(添加额外的标签信息)。
    • 格式转换:将OTLP数据转换成其他后端系统支持的格式(如Jaeger的格式、Prometheus的格式等)。

基于以上三点,OpenTelemetry构建了一个标准化的、端到端的、高性能的遥测数据采集与处理管道。

下图展示了OpenTelemetry的可观测性架构:

OpenTelemetry Arch

可以看到,OpenTelemetry提供了各类语言的SDK用于对接OpenTelemetry,也提供了中间用于数据处理和转发的OpenTelemetry Collector工具。而数据最终的归属,依旧是大家常用的那些Elasticsearch和Prometheus等平台。OpenTelemetry就像一座桥梁,左边连接应用软件,右边连接各个可观测平台,它提供的统一SDK和数据处理工具大大降低了对接的复杂性。

接下来我们将搭建一套极简版本的OpenTelemetry架构,基于以下的基础环境:

  • OS:Debian 13
  • Docker:docker-ce 29.1.2

OpenTelemetry部署

接下来使用Docker Compose部署一套基本可用的OpenTelemetry服务,包括以下4个服务:

  • OpenTelemetry Collector:接收应用程序发送过来的链路跟踪、指标和日志数据等,并处理和转发到各个后端服务。
  • Jaeger:2.12.0版本,接收OpenTelemetry Collector发送过来的OTLP标准的跟踪数据,提供链路跟踪和可视化查询能力。
  • Prometheus:接收OpenTelemetry Collector发送过来的metrics,提供指标存储和查询的能力。
  • Elasticsearch:8.19.7版本,作为存储后端,本案例中有两个作用,一方面用于保存OpenTelemetry Collector发送过来的日志,另一方面作为Jaeger的后端存储服务。

本文使用了比较常见的技术栈,实际上,在OpenTelemetry的生态系统中,有大量支持OTLP协议的服务可以替换上述各组件。此处不再一一赘述,详细清单可参见官方文档。

Docker Compose配置

为了简化部署,本文使用Docker Compose的方式部署4个服务。compose.yml的内容如下:

services:
  jaeger:
    image: jaegertracing/jaeger:2.12.0
    container_name: jaeger
    command:
      - "--config=file:/etc/jaeger/jaeger.yml"
    volumes:
      - ./conf/jaeger.yml:/etc/jaeger/jaeger.yml
    ports:
      - 16686:16686  # HTTP UI
      - 14317:4317   # GRPC
      - 14318:4318   # HTTP
    depends_on:
      elasticsearch:
        condition: service_healthy

  prometheus:
    container_name: prometheus
    image: prom/prometheus
    user: 1000:1000
    command:
      - --web.enable-otlp-receiver
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus/data/
    volumes:
      - ./conf/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prom_data/:/prometheus/data/
    ports:
      - 9090:9090

  elasticsearch:
    image: elasticsearch:8.19.7
    container_name: elasticsearch
    restart: always
    environment:
      - cluster.name=elasticsearch
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      - discovery.type=single-node
      - xpack.security.enabled=false
      - xpack.security.enrollment.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - ./es_data:/usr/share/elasticsearch/data
    healthcheck:
      interval: 5s
      retries: 60
      test: curl --fail -s -o /dev/null http://127.0.0.1:9200/

  otel-collector:
    image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.142.0
    container_name: otel-collector
    command:
      - --config=/etc/otelcol-config.yml
    volumes:
      - ./conf/otelcol-config.yml:/etc/otelcol-config.yml
    ports:
      - 4317:4317   # GRPC
      - 4318:4318   # HTTP
    depends_on:
      elasticsearch:
        condition: service_healthy
      jaeger:
        condition: service_started

上面的compose.yml文件看起来较长,但仔细观察会发现每个容器的配置都十分精简且易于理解,建议仔细阅读。

Jaeger配置解释

在上面定义的compose.yml中,Jaeger项目作为一个trace后端,接收trace类型的遥测数据,并提供对这些数据的处理、聚合、数据挖掘和可视化功能。

容器功能:

  • 容器内监听43174318端口,映射到宿主机的1431714318,避免与otel-collector监听的端口产生冲突。
  • 16686端口提供Jaeger的Web UI服务,用于查看应用的trace链路。
  • Jaeger会将trace数据保存到Elasticsearch服务中,所以需要依赖Elasticsearch先启动。
  • 本地的conf/jaeger.yml文件保存Jaeger的配置文件,里面定义了数据的接收和发送方式。

conf/jaeger.yml配置文件的内容如下,基于Jaeger v2版本:

extensions:
  jaeger_query:
    storage:
      traces: myes_storage
      metrics: myes_storage
    base_path: /jaeger/ui

  jaeger_storage:
    backends:
      myes_storage: &elasticsearch_config
        elasticsearch:
          server_urls:
            - http://elasticsearch:9200
          indices:
            index_prefix: "jaeger-main"

    metric_backends:
      myes_storage: *elasticsearch_config

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  jaeger_storage_exporter:
    trace_storage: myes_storage

service:
  extensions: [jaeger_storage, jaeger_query]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter]

配置说明:

  • extensions: 定义了jaeger_queryjaeger_storage两个扩展,前者配置了Jaeger的Web UI功能,后者是Jaeger的存储功能。存储功能中又配置了trace存储和metric存储,都使用Elasticsearch服务保存。在myes_storage中配置了Elasticsearch服务的API地址和使用的indices名字前缀。
  • receivers: 指定Jaeger作为接收者,监听gRPC和HTTP端口。otel-collector和应用可以把trace数据发往这两个端口。
  • processors: Jaeger具有处理指标的能力,这里仅配置了常用的batch处理器。
  • exporters: 定义了将数据发送到Elasticsearch存储服务中。
  • service: 引用了extensionsreceiversprocessorsexporters的配置。重要的是pipelines中的管道,配置了从OTLP协议接收traces数据,发送到exporter配置中定义的Elasticsearch存储服务。

上面配置的基本含义是:通过OTLP协议在4317 (gRPC) 和4318 (HTTP) 端口接收trace数据,使用批处理器对数据进行批量处理,通过exporter将数据存储到Elasticsearch,Jaeger Query扩展从相同的Elasticsearch后端读取数据并在Web UI展示。

Jaeger的OTLP端口和OpenTelemetry Collector一样,在同一个主机上监听相同的端口号会有冲突。如果修改Jaeger的端口号,会导致Jaeger的自身指标采集报错,虽然可以用环境变量OTEL_TRACES_SAMPLER=always_off禁用Jaeger的自监控,但是不建议这么做。

Jaeger的官方文档内容相对较少:https://www.jaegertracing.io/docs/2.12/ Jaeger仓库中的参考配置案例:https://github.com/jaegertracing/jaeger/tree/v2.12.0/cmd/jaeger

Prometheus配置解释

Prometheus一般使用Pull模式拉取指标,但它也支持作为OTLP接收器。打开CLI标志--web.enable-otlp-receiver后,就可以在Prometheus的/api/v1/otlp/v1/metrics路径上提供OTLP指标接收服务。

在Prometheus的Docker定义中做了如下配置:

  • 启用--web.enable-otlp-receiver CLI flag,开启Prometheus的OTLP指标接收功能。OpenTelemetry Collector会往这个服务发送metrics数据。
  • 另外将本地的conf/prometheus.yml配置文件和prom_data目录映射到容器中,映射prom_data目录用于提供数据持久化的能力。
  • Prometheus的9090端口提供接收指标和UI查询功能,暴露到主机上。

Prometheus配置文件conf/prometheus.yml

global:
  scrape_interval: 15s

otlp:
  keep_identifying_resource_attributes: true
  promote_resource_attributes:
    - service.instance.id
    - service.name
    - service.namespace
    - service.version
    - deployment.environment.name

Prometheus的配置相对简单:

  • 配置OTLP接收器处理来自应用程序的遥测数据
  • service.nameservice.instance.id等资源级别的属性提升为指标标签,便于查询和筛选。

Prometheus一般作为指标存储服务,常与Grafana可视化平台搭配使用,简单起见,本文不涉及Grafana的内容。

Elasticsearch配置解释

Elasticsearch提供日志存储和查询功能,可视化的查询服务需要部署Kibana。关于Kibana服务的内容,在之前的文章EFK日志体系快速入门中已经有所提及,为了简化演示,本次就不部署Kibana了。如果部署Kibana,要保持Kibana和Elasticsearch的版本一致。

由于Jaeger官方文档指定支持Elasticsearch的7.x和8.x版本,所以没有使用最新的Elasticsearch 9.x版本。

在compose.yml文件中,对Elasticsearch做了如下的简单配置:

  • 关闭了Elasticsearch安全功能,免去配置用户名和密码的麻烦。这种操作仅限于测试环境。
  • Elasticsearch的数据映射到es_data目录下,实现存储数据持久化。

OpenTelemetry Collector配置详解

OpenTelemetry Collector的Docker相对简单,主要是将接收数据的端口4317(gRPC)和4318(HTTP)暴露出来用于应用上报数据。

conf/otelcol-config.yml配置文件的内容如下,定义了数据接收、处理和转发的逻辑:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 2s
    send_batch_size: 1000

  transform:
    log_statements:
      - statements:
        - set(log.attributes["elasticsearch.index"], Concat(["logs", resource.attributes["service.name"]], "-"))

exporters:
  otlp:
    endpoint: "jaeger:4317"
    tls:
      insecure: true
  otlphttp/prometheus:
    endpoint: "http://prometheus:9090/api/v1/otlp"
    tls:
      insecure: true
  elasticsearch:
    endpoint: "http://elasticsearch:9200"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: []
      exporters: [otlp]
    metrics:
      receivers: [otlp]
      processors: []
      exporters: [otlphttp/prometheus]
    logs:
      receivers: [otlp]
      processors: [batch, transform]
      exporters: [elasticsearch]

otel-collector的配置文件和Jaeger有相似的结构,这套配置文件中定义了如下3个部分:

  • receivers:otel-collector监听的gRPC和HTTP服务端口,应用程序可以往这些端口上发送数据。
  • processors:创建了batch和transform 2个processor:
    • batch:批量处理数据,提升处理效率。
    • transform:为日志数据新增了elasticsearch.index 这个属性,格式是 logs-${service.name}service.name是客户端上报的属性名字。Elasticsearch会根据 elasticsearch.index的值自动创建索引。如此,就实现了不同应用使用不同索引的需求。transform proccessor 十分有用,可以很方便对日志元数据和内容进行编辑。
  • exporters:otel-collector将把数据发往这些后端服务,分别是Jaeger、Prometheus和Elasticsearch服务。
  • service:定义了数据处理的pipeline,trace类型的指标发往Jaeger、metric的指标发往Prometheus、日志类型的数据发往Elasticsearch。

OpenTelemetry Processor 文档 中可以查看所有的各类processor。有需要的请前往了解。

Note:Elasticsearch Exporter默认使用 Data Stream 将数据发送到Elasticsearch,Data Stream 类型的索引的前缀一般是 .ds- ,十分合适存储只会append数据的存储场景。使用Data Stream类型时,发送给Elasticsearch的索引(即elasticsearch.index属性)必须是 logs-metrics-或者traces-为prefix的,否则 Elasticsearch会返回报错index_not_found_exception。如果要自定义非Data Stream 格式的索引名字,必须显式的关闭Data Stream,方法为 elasticsearch.mapping::mode = none。这个地方踩了很久的坑,有太多隐藏的约定,值得注意。

数据的处理和存储形态可能是多种多样的,比如可能有Kafka消息队列应对大规模并发;也可能仅使用Elasticsearch技术栈,毕竟Elasticsearch+Kibana几乎可以承担所有的Trace、Metric和Log的存储、处理和展示。这里展现的只是一种常见的组合方式,而OpenTelemetry的强大之处也在于对各类平台的广泛支持。

另外,为了叙述方便,文中的Docker和各平台的配置都做了极简化的处理,能运行起来。但省略了安全、性能等方面的优化,在正式使用时,还需要再读读文档,对配置进行与环境适配性的改造。

启动服务

确保上面的compose.ymlconf/jaeger.ymlconf/prometheus.ymlconf/otelcol-config.yml文件已经都创建好了。

启动前先创建Elasticsearch和Prometheus的数据持久化目录:

$ mkdir es_data
$ mkdir prom_data
$ sudo chown -R 1000:1000 es_data prom_data

$ docker compose up -d

测试验证

虽然现在还没有开发上报数据的应用,但是Jaeger自身的指标也会被采集和发送到OpenTelemetry Collector,打开Jaeger的Web UI界面,多刷新几次就能看到Jaeger自身的trace数据了。

接下来使用curl命令向OpenTelemetry Collector的HTTP端口发送各类简单的测试数据。

测试Traces功能

向otel-collector上报一条trace数据,构造一条模拟耗时10s的trace数据(startTimeUnixNano和endTimeUnixNano相差10s):

$ curl -X POST http://localhost:4318/v1/traces   -H "Content-Type: application/json"   -d '{
  "resourceSpans": [{
    "resource": {
      "attributes": [{
        "key": "service.name",
        "value": {"stringValue": "test-service"}
      }]
    },
    "scopeSpans": [{
      "spans": [{
        "traceId": "'$(openssl rand -hex 16)'",
        "spanId": "'$(openssl rand -hex 8)'",
        "name": "curl-test-span",
        "kind": 1,                                    
        "startTimeUnixNano": '"$(date --date='10 seconds ago' +%s%N)"',
        "endTimeUnixNano": '"$(date +%s%N)"',
        "attributes": [{
          "key": "test.attribute",
          "value": {"stringValue": "42"}
        }]
      }]
    }]
  }]
}'

打开Jaeger的Web UI地址为http://127.0.0.1:16686,在打开页面的Service下拉框中应该能选中test-service,然后点击Find Traces按钮,右侧就能看到刚刚上报的trace的详情了。其中的duration显示为10s。

测试Metrics功能

向otel-collector上报一条测试的metric数据:

$ curl -X POST http://localhost:4318/v1/metrics   -H "Content-Type: application/json"   -d '{
  "resourceMetrics": [{
    "resource": {
      "attributes": [{
        "key": "service.name", 
        "value": {"stringValue": "test-service"}
      }]
    },
    "scopeMetrics": [{
      "metrics": [{
        "name": "curl_test_gauge",
        "gauge": {
          "dataPoints": [{
            "timeUnixNano": '"$(date +%s%N)"',
            "asInt": "42"
          }]
        }
      }]
    }]
  }]
}'

打开Prometheus的Web UI,本机地址为http://127.0.0.1:9090,在输入框中填入指标名curl_test_gauge并搜索,就能看到指标了。提交数据中的属性service.name在Prometheus中对应了service__name="test-service" label。

测试Logs功能

向otel-collector上报一条日志数据:

$ curl -X POST http://localhost:4318/v1/logs -H "Content-Type: application/json" -d '{
    "resourceLogs": [{
      "resource": {
        "attributes": [{
          "key": "service.name",
          "value": { "stringValue": "test-service" }
        }]
      },
      "scopeLogs": [{
        "logRecords": [{
          "timeUnixNano": "'$(date +%s%N)'",
          "severityText": "ERROR",
          "body": { "stringValue": "error occurred" }
        }]
      }]
    }]
  }'

查看Elasticsearch的indices和数据,省略了其中uuid的信息:

$ curl 127.0.0.1:9200/_cat/indices?v  # 查看索引
health status index                                     uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open   jaeger-main-jaeger-service-2025-12-14     ...   5   1         13            0     41.9kb         41.9kb       41.9kb
yellow open   .ds-logs-test-service-2025.12.14-000001   ...   1   1          1            0      5.8kb          5.8kb        5.8kb
yellow open   jaeger-main-jaeger-span-2025-12-14        ...   5   1        335            0    208.4kb        208.4kb      208.4kb

$ curl http://127.0.0.1:9200/logs-test-service/_search | jq .   # 查看index中的日志
...

查看indices时,jaeger-main-*格式的index是Jaeger发送过来的trace数据。

.ds-logs-test-service.*格式的是应用通过 otel-collector发送的日志数据,其中的doc数量是1,就是我们刚刚上报的一条日志。

proccessor.transform中设置的elasticsearch.index 明明是 logs-${service.name},现在为什么变成了 .ds-logs-test-service.* ?这个index名字可能有点奇怪,原因在于 logs-${service.name}的index格式匹配了Elasticsearch的内置模板,名字为 logs,会导致所有 logs-*-*的索引都保存为 Data Stream格式,并且添加日期后缀。可以通过Elasticsearch的 /_index_template/logs接口查看这个模板的信息。

具体可以参考 Elasticsearch Exporter

到此为止,OpenTelemetry的各个组件部署和测试就完成了。

Elasticsearch的Data Stream

在测试OpenTelemetry对接Elasticsearch是才第一次遇到Data Stream,解决了许多问题,这里对Data Stream做个介绍。

Data Stream在Elasticsearch 7.9版本中引入,用于优化时序数据(如Logs、Traces和Metrics)的存储,这类数据的特点是经常append,但是不会update、insert。Data Stream因此能更高效的处理时间范围过滤。当我们创建 logs-test-service这个Data Stream时,Elasticsearch会自动创建索引.ds-logs-test-service-2025.12.14-000001。这个索引的各字段含义为:

  • .ds-:固定前缀,表示这是 Data Stream 的后端索引,一个Data Stream索引可能由多个这样的后端索引构成,分散的存储数据。
    • .开头的索引被视为隐藏索引(Hidden Indices)
    • 不要手动向.ds-索引进行数据增删改操作。不要手动删除.ds-索引。
    • .ds-索引应该由ILM(索引生命周期管理)自动化管理,ILM可以通过/_ilm/policy/logs查看。
    • 用户应该始终使用logs-test-service,而不是使用.ds-logs-test-service-*
  • logs-test-service:定义的 Data Stream 名字。Data Stream 不是独立存在的,它必须匹配一个“开启了 Data Stream 功能”的索引模板。比如logs-test-service就符合logs索引模板的 pattern (logs--)。
  • 2025.12.14:该索引创建时的日期。
  • 000001:代际号(Generation)。这是一个 6 位填充的数字,从 000001 开始,每当发生一次 Rollover(滚动),这个数字就会加 1。rollover的时机可能是index的大小或者文档数量达到设定的阈值了。

以下是和 Data Stream相关的一些操作:

  • GET /_data_stream:查看所有数据流
  • GET /_data_stream/logs-test-service:查看特定数据流的详细信息
  • GET /_data_stream/_stats:查看数据流统计信息
  • POST /logs-test-service/_rollover:手动rollver,产生新的后端索引

此外,每次往data stream中写入数据时,必须指定为create index操作。比如

$ curl -X POST 'http://127.0.0.1:9200/_bulk?pretty'  -H "Content-Type: application/json" \
  -d '{"create":{"_index":"logs-test-service"}}
{"body":{"text":"error occurred"}}
'

Data Stream还有一个好处,在Kibana上创建data view时,可以直接指定 Data Stream的名字logs-test-service,而不需要使用 logs-test-service-*这种传统的索引匹配方法。

小结

本文中,我们首先介绍了OpenTelemetry通过制定标准、统一接入的方式,优化了可观测性方面的难题。然后,使用Docker部署了一套常见的OpenTelemetry架构。最后,我们使用curl工具向OpenTelemetry Collector的HTTP端口分别上报Traces、Metrics和日志数据,并进行逐一验证。

为了方便展示,文中的配置都是极简的,仅限于测试,在真正用于生产环境之时,以上所有的配置文件,甚至整体架构方面都有大量可以优化的地方。

在下一篇文章中,将使用Python的Flask框架,分享如何在应用程序中集成OpenTelemetry的SDK,并向OpenTelemetry Collector上报数据,最终展示到各个可观测平台。

Last updates at Dec 18,2025

Views (77)

Leave a Comment

total 0 comments