本文中,将会带你一步步掌握在 Python 中使用 opentelemetry-python 。
hello world: 在终端打印 trace 信息
首先,我们需要先安装 opentelemetry API 和 SDK:
pip install opentelemetry-api
pip install opentelemetry-sdk
其中:
- API 包提供了应用 Owner 需要使用的接口以及相关的辅助逻辑。
- SDK 包提供了这些接口的具体实现,这些实现被设计的具备足够的通用性和可扩展性。
当我们完成上述包的安装之后,就可以使用这些包在应用程序中生成和发送 span 数据。span 对应的就是应用程序中需要进行插桩的操作,例如一个 HTTP 请求或者一次数据库的调用等。通过插桩的方式,你可以获取到很多的有价值的信息,例如整个操作的耗时等。此外,你还可以在 span 中添加相关的属性信息,这些信息都可以用于后续的调试和分析等。
在下面的例子中,我们将会生成一个 trace ,其中包含三个命名的 span: foo、bar 和 baz。
from opentelemetry import trace # 引入 API
from opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Provider
from opentelemetry.sdk.trace.export import ( # 引入 SDK 中的 Processor 和 Exports
BatchSpanProcessor,
ConsoleSpanExporter,
)
provider = TracerProvider() # 实例化一个 Provider
# 实例化一个 Processor,该 Processor 采用批处理的方式,数据会被导出到 Console 终端
processor = BatchSpanProcessor(ConsoleSpanExporter())
# provider 与 processor 绑定
provider.add_span_processor(processor)
# 设置全局 trace 的 provider
trace.set_tracer_provider(provider)
# 获取一个 tracer 对象
tracer = trace.get_tracer(__name__)
# 创建一个 span
with tracer.start_as_current_span("foo"):
# 创建一个 span
with tracer.start_as_current_span("bar"):
# 创建一个 span
with 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 # 引入 API
from opentelemetry.sdk.trace import TracerProvider # 引入 SDK 中的 Provider
from opentelemetry.sdk.trace.export import BatchSpanProcessor # 引入 SDK 中的 Processor
from 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 Exporter
jaeger_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__)
# 创建一个 span
with tracer.start_as_current_span("foo"):
# 创建一个 span
with tracer.start_as_current_span("bar"):
# 创建一个 span
with 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-flask
pip install opentelemetry-instrumentation-requests
下述代码中,我们搭建了一个简单的 HTTP 服务,同时在访问该服务时,该服务会调用 requests 库发送 HTTP 请求访问 example 服务。
import flask
import requests
from opentelemetry import trace
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from 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_textmap
from opentelemetry.propagators.b3 import B3MultiFormat
set_global_textmap(B3Format())
此外,你还可以使用 Jaeger native propagation 协议:
安装依赖库:
pip install opentelemetry-propagator-jaeger
增加如下代码:
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.jaeger import JaegerPropagator
set_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: debug
processors:
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 trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
span_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 exporter
tracer = trace.get_tracer_provider().get_tracer(__name__)
with tracer.start_as_current_span("foo"):
print("Hello world!")
运行代码后,你就可以看到,我们已经在 collector 的日志中看到已经接收到了相关的数据。