本文中,将会带你一步步掌握在 Python 中使用 opentelemetry-python 。
hello world: 在终端打印 trace 信息
首先,我们需要先安装 opentelemetry API 和 SDK:
pip install opentelemetry-apipip install opentelemetry-sdk
其中:
- API 包提供了应用 Owner 需要使用的接口以及相关的辅助逻辑。
- SDK 包提供了这些接口的具体实现,这些实现被设计的具备足够的通用性和可扩展性。
当我们完成上述包的安装之后,就可以使用这些包在应用程序中生成和发送 span 数据。span 对应的就是应用程序中需要进行插桩的操作,例如一个 HTTP 请求或者一次数据库的调用等。通过插桩的方式,你可以获取到很多的有价值的信息,例如整个操作的耗时等。此外,你还可以在 span 中添加相关的属性信息,这些信息都可以用于后续的调试和分析等。
在下面的例子中,我们将会生成一个 trace ,其中包含三个命名的 span: foo、bar 和 baz。
from opentelemetry import trace # 引入 APIfrom opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Providerfrom opentelemetry.sdk.trace.export import ( # 引入 SDK 中的 Processor 和 ExportsBatchSpanProcessor,ConsoleSpanExporter,)provider = TracerProvider() # 实例化一个 Provider# 实例化一个 Processor,该 Processor 采用批处理的方式,数据会被导出到 Console 终端processor = BatchSpanProcessor(ConsoleSpanExporter())# provider 与 processor 绑定provider.add_span_processor(processor)# 设置全局 trace 的 providertrace.set_tracer_provider(provider)# 获取一个 tracer 对象tracer = trace.get_tracer(__name__)# 创建一个 spanwith tracer.start_as_current_span("foo"):# 创建一个 spanwith tracer.start_as_current_span("bar"):# 创建一个 spanwith tracer.start_as_current_span("baz"):print("Hello world from OpenTelemetry Python!")
当你运行代码后,应该能够得到如下结果:
Hello world from OpenTelemetry Python!{"name": "baz","context": {"trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72","span_id": "0x353cdcd853b3ce38","trace_state": "[]"},"kind": "SpanKind.INTERNAL","parent_id": "0x088ed028cf3caa74","start_time": "2022-01-21T11:14:10.262250Z","end_time": "2022-01-21T11:14:10.262275Z","status": {"status_code": "UNSET"},"attributes": {},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}{"name": "bar","context": {"trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72","span_id": "0x088ed028cf3caa74","trace_state": "[]"},"kind": "SpanKind.INTERNAL","parent_id": "0xc360e39eaf3fd362","start_time": "2022-01-21T11:14:10.262218Z","end_time": "2022-01-21T11:14:10.262287Z","status": {"status_code": "UNSET"},"attributes": {},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}{"name": "foo","context": {"trace_id": "0xaa17fbd61b7de4e2d94d8cd58c439f72","span_id": "0xc360e39eaf3fd362","trace_state": "[]"},"kind": "SpanKind.INTERNAL","parent_id": null,"start_time": "2022-01-21T11:14:10.262173Z","end_time": "2022-01-21T11:14:10.262296Z","status": {"status_code": "UNSET"},"attributes": {},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}
可以看到,除了 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 服务:
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 的能力,我们需要主动安装对应的包:
pip install opentelemetry-exporter-jaeger
安装完成后,我们可以修改代码如下:
from opentelemetry import trace # 引入 APIfrom opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Providerfrom opentelemetry.sdk.trace.export import BatchSpanProcessor # 引入 SDK 中的 Processorfrom opentelemetry.exporter.jaeger.thrift import JaegerExporter # 引入 Jaeger Exporter# 定义资源和服务名称from opentelemetry.sdk.resources import SERVICE_NAME, Resource# 设置一个 tracer Provider,定义对应的 Service 名称trace.set_tracer_provider(TracerProvider(resource=Resource.create({SERVICE_NAME: "my-helloworld-service"})))# 引入 Jaeger Exporterjaeger_exporter = JaegerExporter(agent_host_name="localhost",agent_port=6831,)# 将 Jaeger Exporter 作为 Processor 添加到 Provider 中trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(jaeger_exporter))# 获取一个 tracer 对象tracer = trace.get_tracer(__name__)# 创建一个 spanwith tracer.start_as_current_span("foo"):# 创建一个 spanwith tracer.start_as_current_span("bar"):# 创建一个 spanwith tracer.start_as_current_span("baz"):print("Hello world from OpenTelemetry Python!")
运行上述代码后,你可以在 Jaeger WebUI 页面中看到如下 Trace 数据:
Flask 自动插桩
在上述示例代码中,我们都是通过手动插桩的方式来创建 span 等。而针对如下一些通用的操作你可能会希望加入 trace 信息作为分布式 Trace 的一部分:
- Web 服务提供 HTTP 响应
- HTTP 客户端请求
- 数据库调用等
为了实现上述的这类通用的需求,OpenTelemetry 提供了一组自动插桩库。这些自动插桩库都是与特定的框架和库绑定的,例如 Flask Web框架等。你可以从 Contrib 代码库列表中查询目前支持的自动插桩库。
下面,我们来演示针对 Flask 和 requests 库的自动插桩库进行演示。首先,我们还是需要来安装这些依赖库:
pip install opentelemetry-instrumentation-flaskpip install opentelemetry-instrumentation-requests
下述代码中,我们搭建了一个简单的 HTTP 服务,同时在访问该服务时,该服务会调用 requests 库发送 HTTP 请求访问 example 服务。
import flaskimport requestsfrom opentelemetry import tracefrom opentelemetry.instrumentation.flask import FlaskInstrumentorfrom opentelemetry.instrumentation.requests import RequestsInstrumentorfrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import (BatchSpanProcessor,ConsoleSpanExporter,)trace.set_tracer_provider(TracerProvider())trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))app = flask.Flask(__name__)FlaskInstrumentor().instrument_app(app)RequestsInstrumentor().instrument()tracer = trace.get_tracer(__name__)@app.route("/")def hello():with tracer.start_as_current_span("example-request"):requests.get("http://www.example.com")return "hello"app.run(debug=True, port=5000)
可以看到,在上述代码中,我们使用了如下代码实现自动插桩库的自动插桩操作:
FlaskInstrumentor().instrument_app(app)RequestsInstrumentor().instrument()
下面,我们首先可以运行该脚本启动 HTTP 服务,然后就可以访问 http://localhost:5000/ 服务,观察命令行的输出,你就可以看到如下的输出内容:
{"name": "HTTP GET","context": {"trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff","span_id": "0xb3183dd4843d0956","trace_state": "[]"},"kind": "SpanKind.CLIENT","parent_id": "0x97e55e1de50c9aa5","start_time": "2022-01-21T12:53:39.582322Z","end_time": "2022-01-21T12:53:40.001677Z","status": {"status_code": "UNSET"},"attributes": {"http.method": "GET","http.url": "http://www.example.com","http.status_code": 200},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}{"name": "example-request","context": {"trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff","span_id": "0x97e55e1de50c9aa5","trace_state": "[]"},"kind": "SpanKind.INTERNAL","parent_id": "0x42e41a8dd24fba39","start_time": "2022-01-21T12:53:39.582004Z","end_time": "2022-01-21T12:53:40.001837Z","status": {"status_code": "UNSET"},"attributes": {},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}{"name": "/","context": {"trace_id": "0xe33fe6c9873aa3f3d1b51ea74af931ff","span_id": "0x42e41a8dd24fba39","trace_state": "[]"},"kind": "SpanKind.SERVER","parent_id": null,"start_time": "2022-01-21T12:53:39.577932Z","end_time": "2022-01-21T12:53:40.001991Z","status": {"status_code": "UNSET"},"attributes": {"http.method": "GET","http.server_name": "127.0.0.1","http.scheme": "http","net.host.port": 5000,"http.host": "127.0.0.1:5000","http.target": "/","net.peer.ip": "127.0.0.1","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","net.peer.port": 49974,"http.flavor": "1.1","http.route": "/","http.status_code": 200},"events": [],"links": [],"resource": {"telemetry.sdk.language": "python","telemetry.sdk.name": "opentelemetry","telemetry.sdk.version": "1.8.0","service.name": "unknown_service"}}
从上述日志中可以看到,整个过程包含了3个span,分别对应的是
- requests 库发送 HTTP 请求给 www.baidu.com (RequestsInstrumentor 自动插桩)
- 代码中自定义的 span: example-request (人工插桩)
- Flask 框架接收 / url 请求并处理(FlaskInstrumentor 自动插桩)
配置个性化HTTP传输器
分布式 Trace 的核心功能就是能够跨多个服务得到一组相互关联的 Trace 信息。然而,这样就必须要解决一个问题,就是在多个服务之间能够传递上下文信息。
为了能够在多个服务之间能够传递上下文信息,OpenTelemetry 中提供了一个 propagators 的概念,propagators 中提供了一个方法用于在请求和响应的过程中对上下文进行编解码。
默认情况下,opentelemetry-python 使用的是 W3C Trace Context 和 W3C Baggage 用于 HTTP Headers。当然,你也可以通过配置的方式改用其他的 propagation,例如可以使用 Zipkin 中定义的 b3 协议。首先,还是需要安装对应的依赖库:
pip install opentelemetry-propagator-b3
安装完成后,我们可以使用如下代码进行配置来修改 propagation:
from opentelemetry.propagate import set_global_textmapfrom opentelemetry.propagators.b3 import B3MultiFormatset_global_textmap(B3Format())
此外,你还可以使用 Jaeger native propagation 协议:
安装依赖库:
pip install opentelemetry-propagator-jaeger
增加如下代码:
from opentelemetry.propagate import set_global_textmapfrom opentelemetry.propagators.jaeger import JaegerPropagatorset_global_textmap(JaegerPropagator())
使用 OpenTelemetry Collector 收集 traces 数据
尽管在之前的示例中,我们可以直接通过插桩库插件将遥测数据导出到 Jaeger 后端服务,但是有时你可能会遇到一些更加负责的场景,这时,OpenTelemetry 更推荐使用 Collector 作为了一个 Proxy 来接收数据并进行处理后转发给指定的后端存储服务。
OpenTelemetry Collector 是一个独立、灵活的程序,它支持接收 Trace 数据并处理后转发给多个后端存储服务。
下面,我们来本地运行一个 Collector 服务进行演示。
首先,我们需要创建一个 collector 的配置文件(/tmp/otel-collector-config.yaml):
receivers:otlp:protocols:grpc:http:exporters:logging:loglevel: debugprocessors:batch:service:pipelines:traces:receivers: [otlp]exporters: [logging]processors: [batch]
通过该配置文件,collector 会接收 otlp 格式的数据并输出到日志中。
下面,我们通过 Docker 镜像来启动一个 Collector 服务:
docker run -p 4317:4317 \-v /tmp/otel-collector-config.yaml:/etc/otel-collector-config.yaml \otel/opentelemetry-collector:latest \--config=/etc/otel-collector-config.yaml
下面,我们需要安装一个 Collector Exporter:
pip install opentelemetry-exporter-otlp
最后,我们来执行代码看看:
from opentelemetry import tracefrom opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (OTLPSpanExporter,)from opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessorspan_exporter = OTLPSpanExporter(# optional# endpoint="myCollectorURL:4317",# credentials=ChannelCredentials(credentials),# headers=(("metadata", "metadata")),)tracer_provider = TracerProvider()trace.set_tracer_provider(tracer_provider)span_processor = BatchSpanProcessor(span_exporter)tracer_provider.add_span_processor(span_processor)# Configure the tracer to use the collector exportertracer = trace.get_tracer_provider().get_tracer(__name__)with tracer.start_as_current_span("foo"):print("Hello world!")
运行代码后,你就可以看到,我们已经在 collector 的日志中看到已经接收到了相关的数据。
