本文中,将会带你一步步掌握在 Python 中使用 opentelemetry-python 。

hello world: 在终端打印 trace 信息

首先,我们需要先安装 opentelemetry API 和 SDK:

  1. pip install opentelemetry-api
  2. pip install opentelemetry-sdk

其中:

  • API 包提供了应用 Owner 需要使用的接口以及相关的辅助逻辑。
  • SDK 包提供了这些接口的具体实现,这些实现被设计的具备足够的通用性和可扩展性。

当我们完成上述包的安装之后,就可以使用这些包在应用程序中生成和发送 span 数据。span 对应的就是应用程序中需要进行插桩的操作,例如一个 HTTP 请求或者一次数据库的调用等。通过插桩的方式,你可以获取到很多的有价值的信息,例如整个操作的耗时等。此外,你还可以在 span 中添加相关的属性信息,这些信息都可以用于后续的调试和分析等。

在下面的例子中,我们将会生成一个 trace ,其中包含三个命名的 span: foo、bar 和 baz。

  1. from opentelemetry import trace # 引入 API
  2. from opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Provider
  3. from opentelemetry.sdk.trace.export import ( # 引入 SDK 中的 Processor 和 Exports
  4. BatchSpanProcessor,
  5. ConsoleSpanExporter,
  6. )
  7. provider = TracerProvider() # 实例化一个 Provider
  8. # 实例化一个 Processor,该 Processor 采用批处理的方式,数据会被导出到 Console 终端
  9. processor = BatchSpanProcessor(ConsoleSpanExporter())
  10. # provider 与 processor 绑定
  11. provider.add_span_processor(processor)
  12. # 设置全局 trace 的 provider
  13. trace.set_tracer_provider(provider)
  14. # 获取一个 tracer 对象
  15. tracer = trace.get_tracer(__name__)
  16. # 创建一个 span
  17. with tracer.start_as_current_span("foo"):
  18. # 创建一个 span
  19. with tracer.start_as_current_span("bar"):
  20. # 创建一个 span
  21. with tracer.start_as_current_span("baz"):
  22. print("Hello world from OpenTelemetry Python!")

当你运行代码后,应该能够得到如下结果:

  1. Hello world from OpenTelemetry Python!
  2. {
  3. "name": "baz",
  4. "context": {
  5. "trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72",
  6. "span_id": "0x353cdcd853b3ce38",
  7. "trace_state": "[]"
  8. },
  9. "kind": "SpanKind.INTERNAL",
  10. "parent_id": "0x088ed028cf3caa74",
  11. "start_time": "2022-01-21T11:14:10.262250Z",
  12. "end_time": "2022-01-21T11:14:10.262275Z",
  13. "status": {
  14. "status_code": "UNSET"
  15. },
  16. "attributes": {},
  17. "events": [],
  18. "links": [],
  19. "resource": {
  20. "telemetry.sdk.language": "python",
  21. "telemetry.sdk.name": "opentelemetry",
  22. "telemetry.sdk.version": "1.8.0",
  23. "service.name": "unknown_service"
  24. }
  25. }
  26. {
  27. "name": "bar",
  28. "context": {
  29. "trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72",
  30. "span_id": "0x088ed028cf3caa74",
  31. "trace_state": "[]"
  32. },
  33. "kind": "SpanKind.INTERNAL",
  34. "parent_id": "0xc360e39eaf3fd362",
  35. "start_time": "2022-01-21T11:14:10.262218Z",
  36. "end_time": "2022-01-21T11:14:10.262287Z",
  37. "status": {
  38. "status_code": "UNSET"
  39. },
  40. "attributes": {},
  41. "events": [],
  42. "links": [],
  43. "resource": {
  44. "telemetry.sdk.language": "python",
  45. "telemetry.sdk.name": "opentelemetry",
  46. "telemetry.sdk.version": "1.8.0",
  47. "service.name": "unknown_service"
  48. }
  49. }
  50. {
  51. "name": "foo",
  52. "context": {
  53. "trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72",
  54. "span_id": "0xc360e39eaf3fd362",
  55. "trace_state": "[]"
  56. },
  57. "kind": "SpanKind.INTERNAL",
  58. "parent_id": null,
  59. "start_time": "2022-01-21T11:14:10.262173Z",
  60. "end_time": "2022-01-21T11:14:10.262296Z",
  61. "status": {
  62. "status_code": "UNSET"
  63. },
  64. "attributes": {},
  65. "events": [],
  66. "links": [],
  67. "resource": {
  68. "telemetry.sdk.language": "python",
  69. "telemetry.sdk.name": "opentelemetry",
  70. "telemetry.sdk.version": "1.8.0",
  71. "service.name": "unknown_service"
  72. }
  73. }

可以看到,除了 print() 的内容外,我们还会在 Console 中看到三个大的 JSON 输出。每个 JSON 对应的正是一个 Span 的信息。
通常而言,一个 span 通常都表示一个具体的操作或者一组任务。span 之间可以嵌套并且互相之间可以存在父子关系。当一个 span 还处于 active 状态时,新创建的 span 将会继承当前 span 的 TraceID、选项配置以及该上下文的其他属性。如果一个 span 没有父 span,那么我们称该 span 为 root span。一个 trace 会包含一个 root span 以及一组它的子 span。

配置 exports 将 span 发送到其他存储

在上述示例中,我们已经得到了 span 的数据,但是它的输出格式非常难以理解。通常,我们需要将 span 数据导出到一些能够支持性能监控或者报表可视化的后端服务中。此外,很多时候我们也希望多个服务的 span 和 trace 信息能够存储到同一个数据库中,从而可以在某个页面进行整体的可视化。
分布式 Trace 就是指将多个服务的 span 和 trace 信息进行聚合处理,一个流行为分布式 Trace 服务就是 Jaeger。 Jaeger 项目提供了一个 all-in-one 的 Docker 镜像,其中内置了 UI Web、数据库以及 Consumer。
我们可以通过如下命令来启动 Jaeger 服务:

  1. docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one

这个命令会启动一个 Jaeger 服务,同时监听了 16686 提供 Web UI 服务,6831 端口用于 Jaeger Thrift Agent 接收数据。你可以访问 http://localhost:16686 地址来访问对应服务。
当你完成后端服务的部署后,你还需要修改你的应用代码将数据导出到对应的后端服务。然而,opentelemetry-sdk 包中默认没有提供了 Jaeger Exporter 的能力,我们需要主动安装对应的包:

  1. pip install opentelemetry-exporter-jaeger

安装完成后,我们可以修改代码如下:

  1. from opentelemetry import trace # 引入 API
  2. from opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Provider
  3. from opentelemetry.sdk.trace.export import BatchSpanProcessor # 引入 SDK 中的 Processor
  4. from opentelemetry.exporter.jaeger.thrift import JaegerExporter # 引入 Jaeger Exporter
  5. # 定义资源和服务名称
  6. from opentelemetry.sdk.resources import SERVICE_NAME, Resource
  7. # 设置一个 tracer Provider,定义对应的 Service 名称
  8. trace.set_tracer_provider(
  9. TracerProvider(
  10. resource=Resource.create({SERVICE_NAME: "my-helloworld-service"})
  11. )
  12. )
  13. # 引入 Jaeger Exporter
  14. jaeger_exporter = JaegerExporter(
  15. agent_host_name="localhost",
  16. agent_port=6831,
  17. )
  18. # 将 Jaeger Exporter 作为 Processor 添加到 Provider 中
  19. trace.get_tracer_provider().add_span_processor(
  20. BatchSpanProcessor(jaeger_exporter)
  21. )
  22. # 获取一个 tracer 对象
  23. tracer = trace.get_tracer(__name__)
  24. # 创建一个 span
  25. with tracer.start_as_current_span("foo"):
  26. # 创建一个 span
  27. with tracer.start_as_current_span("bar"):
  28. # 创建一个 span
  29. with tracer.start_as_current_span("baz"):
  30. print("Hello world from OpenTelemetry Python!")

运行上述代码后,你可以在 Jaeger WebUI 页面中看到如下 Trace 数据:
image.png

Flask 自动插桩

在上述示例代码中,我们都是通过手动插桩的方式来创建 span 等。而针对如下一些通用的操作你可能会希望加入 trace 信息作为分布式 Trace 的一部分:

  • Web 服务提供 HTTP 响应
  • HTTP 客户端请求
  • 数据库调用等

为了实现上述的这类通用的需求,OpenTelemetry 提供了一组自动插桩库。这些自动插桩库都是与特定的框架和库绑定的,例如 Flask Web框架等。你可以从 Contrib 代码库列表中查询目前支持的自动插桩库。
下面,我们来演示针对 Flask 和 requests 库的自动插桩库进行演示。首先,我们还是需要来安装这些依赖库:

  1. pip install opentelemetry-instrumentation-flask
  2. pip install opentelemetry-instrumentation-requests

下述代码中,我们搭建了一个简单的 HTTP 服务,同时在访问该服务时,该服务会调用 requests 库发送 HTTP 请求访问 example 服务。

  1. import flask
  2. import requests
  3. from opentelemetry import trace
  4. from opentelemetry.instrumentation.flask import FlaskInstrumentor
  5. from opentelemetry.instrumentation.requests import RequestsInstrumentor
  6. from opentelemetry.sdk.trace import TracerProvider
  7. from opentelemetry.sdk.trace.export import (
  8. BatchSpanProcessor,
  9. ConsoleSpanExporter,
  10. )
  11. trace.set_tracer_provider(TracerProvider())
  12. trace.get_tracer_provider().add_span_processor(
  13. BatchSpanProcessor(ConsoleSpanExporter())
  14. )
  15. app = flask.Flask(__name__)
  16. FlaskInstrumentor().instrument_app(app)
  17. RequestsInstrumentor().instrument()
  18. tracer = trace.get_tracer(__name__)
  19. @app.route("/")
  20. def hello():
  21. with tracer.start_as_current_span("example-request"):
  22. requests.get("http://www.example.com")
  23. return "hello"
  24. app.run(debug=True, port=5000)

可以看到,在上述代码中,我们使用了如下代码实现自动插桩库的自动插桩操作:

  1. FlaskInstrumentor().instrument_app(app)
  2. RequestsInstrumentor().instrument()

下面,我们首先可以运行该脚本启动 HTTP 服务,然后就可以访问 http://localhost:5000/ 服务,观察命令行的输出,你就可以看到如下的输出内容:

  1. {
  2. "name": "HTTP GET",
  3. "context": {
  4. "trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff",
  5. "span_id": "0xb3183dd4843d0956",
  6. "trace_state": "[]"
  7. },
  8. "kind": "SpanKind.CLIENT",
  9. "parent_id": "0x97e55e1de50c9aa5",
  10. "start_time": "2022-01-21T12:53:39.582322Z",
  11. "end_time": "2022-01-21T12:53:40.001677Z",
  12. "status": {
  13. "status_code": "UNSET"
  14. },
  15. "attributes": {
  16. "http.method": "GET",
  17. "http.url": "http://www.example.com",
  18. "http.status_code": 200
  19. },
  20. "events": [],
  21. "links": [],
  22. "resource": {
  23. "telemetry.sdk.language": "python",
  24. "telemetry.sdk.name": "opentelemetry",
  25. "telemetry.sdk.version": "1.8.0",
  26. "service.name": "unknown_service"
  27. }
  28. }
  29. {
  30. "name": "example-request",
  31. "context": {
  32. "trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff",
  33. "span_id": "0x97e55e1de50c9aa5",
  34. "trace_state": "[]"
  35. },
  36. "kind": "SpanKind.INTERNAL",
  37. "parent_id": "0x42e41a8dd24fba39",
  38. "start_time": "2022-01-21T12:53:39.582004Z",
  39. "end_time": "2022-01-21T12:53:40.001837Z",
  40. "status": {
  41. "status_code": "UNSET"
  42. },
  43. "attributes": {},
  44. "events": [],
  45. "links": [],
  46. "resource": {
  47. "telemetry.sdk.language": "python",
  48. "telemetry.sdk.name": "opentelemetry",
  49. "telemetry.sdk.version": "1.8.0",
  50. "service.name": "unknown_service"
  51. }
  52. }
  53. {
  54. "name": "/",
  55. "context": {
  56. "trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff",
  57. "span_id": "0x42e41a8dd24fba39",
  58. "trace_state": "[]"
  59. },
  60. "kind": "SpanKind.SERVER",
  61. "parent_id": null,
  62. "start_time": "2022-01-21T12:53:39.577932Z",
  63. "end_time": "2022-01-21T12:53:40.001991Z",
  64. "status": {
  65. "status_code": "UNSET"
  66. },
  67. "attributes": {
  68. "http.method": "GET",
  69. "http.server_name": "127.0.0.1",
  70. "http.scheme": "http",
  71. "net.host.port": 5000,
  72. "http.host": "127.0.0.1:5000",
  73. "http.target": "/",
  74. "net.peer.ip": "127.0.0.1",
  75. "http.user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36",
  76. "net.peer.port": 49974,
  77. "http.flavor": "1.1",
  78. "http.route": "/",
  79. "http.status_code": 200
  80. },
  81. "events": [],
  82. "links": [],
  83. "resource": {
  84. "telemetry.sdk.language": "python",
  85. "telemetry.sdk.name": "opentelemetry",
  86. "telemetry.sdk.version": "1.8.0",
  87. "service.name": "unknown_service"
  88. }
  89. }

从上述日志中可以看到,整个过程包含了3个span,分别对应的是

  • requests 库发送 HTTP 请求给 www.baidu.com (RequestsInstrumentor 自动插桩)
  • 代码中自定义的 span: example-request (人工插桩)
  • Flask 框架接收 / url 请求并处理(FlaskInstrumentor 自动插桩)

配置个性化HTTP传输器

分布式 Trace 的核心功能就是能够跨多个服务得到一组相互关联的 Trace 信息。然而,这样就必须要解决一个问题,就是在多个服务之间能够传递上下文信息。
为了能够在多个服务之间能够传递上下文信息,OpenTelemetry 中提供了一个 propagators 的概念,propagators 中提供了一个方法用于在请求和响应的过程中对上下文进行编解码。
默认情况下,opentelemetry-python 使用的是 W3C Trace ContextW3C Baggage 用于 HTTP Headers。当然,你也可以通过配置的方式改用其他的 propagation,例如可以使用 Zipkin 中定义的 b3 协议。首先,还是需要安装对应的依赖库:

  1. pip install opentelemetry-propagator-b3

安装完成后,我们可以使用如下代码进行配置来修改 propagation:

  1. from opentelemetry.propagate import set_global_textmap
  2. from opentelemetry.propagators.b3 import B3MultiFormat
  3. set_global_textmap(B3Format())

此外,你还可以使用 Jaeger native propagation 协议:
安装依赖库:

  1. pip install opentelemetry-propagator-jaeger

增加如下代码:

  1. from opentelemetry.propagate import set_global_textmap
  2. from opentelemetry.propagators.jaeger import JaegerPropagator
  3. set_global_textmap(JaegerPropagator())

使用 OpenTelemetry Collector 收集 traces 数据

尽管在之前的示例中,我们可以直接通过插桩库插件将遥测数据导出到 Jaeger 后端服务,但是有时你可能会遇到一些更加负责的场景,这时,OpenTelemetry 更推荐使用 Collector 作为了一个 Proxy 来接收数据并进行处理后转发给指定的后端存储服务。
OpenTelemetry Collector 是一个独立、灵活的程序,它支持接收 Trace 数据并处理后转发给多个后端存储服务。
下面,我们来本地运行一个 Collector 服务进行演示。
首先,我们需要创建一个 collector 的配置文件(/tmp/otel-collector-config.yaml):

  1. receivers:
  2. otlp:
  3. protocols:
  4. grpc:
  5. http:
  6. exporters:
  7. logging:
  8. loglevel: debug
  9. processors:
  10. batch:
  11. service:
  12. pipelines:
  13. traces:
  14. receivers: [otlp]
  15. exporters: [logging]
  16. processors: [batch]

通过该配置文件,collector 会接收 otlp 格式的数据并输出到日志中。
下面,我们通过 Docker 镜像来启动一个 Collector 服务:

  1. docker run -p 4317:4317 \
  2. -v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \
  3. otel/opentelemetry-collector:latest \
  4. --config=/etc/otel-collector-config.yaml

下面,我们需要安装一个 Collector Exporter:

  1. pip install opentelemetry-exporter-otlp

最后,我们来执行代码看看:

  1. from opentelemetry import trace
  2. from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
  3. OTLPSpanExporter,
  4. )
  5. from opentelemetry.sdk.trace import TracerProvider
  6. from opentelemetry.sdk.trace.export import BatchSpanProcessor
  7. span_exporter = OTLPSpanExporter(
  8. # optional
  9. # endpoint="myCollectorURL:4317",
  10. # credentials=ChannelCredentials(credentials),
  11. # headers=(("metadata", "metadata")),
  12. )
  13. tracer_provider = TracerProvider()
  14. trace.set_tracer_provider(tracer_provider)
  15. span_processor = BatchSpanProcessor(span_exporter)
  16. tracer_provider.add_span_processor(span_processor)
  17. # Configure the tracer to use the collector exporter
  18. tracer = trace.get_tracer_provider().get_tracer(__name__)
  19. with tracer.start_as_current_span("foo"):
  20. print("Hello world!")

运行代码后,你就可以看到,我们已经在 collector 的日志中看到已经接收到了相关的数据。