今天,我的朋友们,我们要好好地折磨一下自己,因为我们要把自动仪表的二进制工具放在一边,转而研究Python的超级手动OpenTelemetry仪表。由于我们没有自动监测作为我们的安全毯,我们将不得不学习如何做以下工作。
-
配置 OpenTelemetry for Python 来发送仪器数据到支持OTLPOTLP 的 Observability 后端。剧透一下:我们将使用LightstepLightstep作为我们的Observability后端。✅
-
在相关服务之间传播上下文,以便它们显示为同一跟踪的一部分 ✅
注意。我不会去讨论如何用OTel for Python创建Spans,因为OTel的官方文档已经做得很好了。
你害怕吗?不用怕,因为我已经想好了,所以你不需要害怕!
你准备好了吗?让我们开始吧!
前提条件
在我们开始教程之前,这里有一些你需要的东西。
如果你想运行第二部分的完整代码示例,你还需要。
第1部分 发生了什么?
我们将用一个客户端和服务器应用程序来说明用OpenTelemetry进行的Python手动仪表。客户端将调用一个由服务器托管的/ping 端点。
本教程中的例子可以在lightstep/opentelemetry-exampleslightstep/opentelemetry-examplesrepo中找到。我们将与三个主要文件一起工作。
在我们运行示例代码之前,我们必须首先了解它的作用。
1- OTel库
为了将OpenTelemetry数据发送到Observability后端(如Lightstep),你需要安装以下软件 OpenTelemetry软件包,这包括在requirements.txtrequirements.txt中。
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-grpc
正如你所看到的,我们正在安装 OpenTelemetry API 和 SDK 包,以及opentelemetry-exporter-otlp-proto-grpc ,它被用来通过gRPCgRPC 将 OTel 数据发送到你的 Observability 后端(例如 Lightstep)。
2- OTel 设置和配置 (common.py)
在我们的例子中,OTel 的设置和配置在common.pycommon.py 中完成。我们把事情分割到这个单独的文件中,这样我们就不必在client.pyclient.py和server.pyserver.py 中重复这些代码。
首先,我们必须导入所需的 OTel 包。
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
接下来,我们必须配置Exporter。Exporter是我们向OpenTelemetry发送数据的方式。正如我前面提到的,Lightstep接受OTLP格式的数据,所以我们需要定义一个OTLP Exporter。
注意。有些供应商不接受 OTLP 格式的数据,这意味着你需要使用一个供应商专用的导出器来向他们发送数据。
我们在Python中像这样配置我们的Exporter。
def get_otlp_exporter():
ls_access_token = os.environ.get("LS_ACCESS_TOKEN")
return OTLPSpanExporter(
endpoint="ingest.lightstep.com:443",
headers=(("lightstep-access-token", ls_access_token),),
)
一些值得注意的项目。
最后,我们配置Tracer Provider。一个TracerProvider ,作为OpenTelemetry API的入口点。它提供了对Tracers的访问。一个Tracer ,负责创建一个SpanSpan来追踪给定的操作。
我们在Python中这样配置我们的Tracer Provider。
有几个值得注意的项目。
-
我们定义了一个ResourceResource,为OpenTelemetry提供了一堆标识我们服务的信息,包括服务名称服务名称和服务版本服务版本。(你可以在这里看到你可以设置的资源属性的完整列表。)顾名思义,服务名称是你正在探测的微服务的名称,服务版本是你正在探测的服务的版本。在这个例子中,我们得到的服务名称和服务版本是作为环境变量OTEL_RESOURCE_ATTRIBUTESOTEL_RESOURCE_ATTRIBUTES中的键/值传递的(我们将在第二部分看到一些示例值)。如果该环境变量不存在,我们就设置一个默认的服务名称,
"test-py-manual-otlp"。 -
我们正在使用BatchSpanProcessorBatchSpanProcessor,这意味着我们正在告诉 OTel 分批导出数据。在这个例子中,除了基本的配置外,我们没有做任何事情。
3- 初始化(client.py 和 server.py)
我们终于准备好向Lightstep发送数据了!我们需要做的就是在client.py (第17-2017-20行)和server.py (第1717和2929行)中调用common.pycommon.py的get_tracer 函数,像这样。
from common import get_tracer
...
tracer = get_tracer()
...
4- 仪器化(client.py 和 server.py)
初始化完成后,我们需要对我们的代码进行检测,这意味着我们需要创建Spans。我不会在这里讨论创建 Span 的具体细节,因为OTel docs 在这方面做得很好,而且正如我在介绍中提到的,这不属于这篇文章的范围。
然而,我将简要地提到,在Python中,有几种方法可以检测我们的代码,你将在示例代码中看到两种创建Span的方法:使用with语句使用with语句,以及使用函数装饰器使用函数装饰器。
你可以在client.py, 第 23-32 行client.py, 第 23-32 行看到一个使用with语句创建 Span 的例子。下面是完整的函数列表。
def send_requests(url):
with tracer.start_as_current_span("client operation"):
try:
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
header = {"traceparent": carrier["traceparent"]}
res = requests.get(url, headers=header)
print(f"Request to {url}, got {len(res.content)} bytes")
except Exception as e:
print(f"Request to {url} failed {e}")
pass
Span被初始化的行,with tracer.start_as_current_span("client operation"): ,该行以下的所有内容都在该Span的范围内。
你可以在server.py第78行看到一个使用函数装饰器函数装饰器创建Span的例子server.py第78行。下面是完整的函数列表。
@tracer.start_as_current_span("pymongo_integration")
@app.route("/pymongo/")
def pymongo_integration(length):
with tracer.start_as_current_span("server pymongo operation"):
client = MongoClient("mongo", 27017, serverSelectionTimeoutMS=2000)
db = client["opentelemetry-tests"]
collection = db["tests"]
collection.find_one()
return _random_string(length)
有几个值得注意的地方。
-
@tracer.start_as_current_span("pymongo_integration")行开始了pymongo_integration函数的 Span。该函数中的所有内容都在该Span的范围内。 -
你可能还注意到,我们在其中初始化了另一个跨度,这一行是:
with tracer.start_as_current_span("server pymongo operation"):,(server.py,89行server.py,89行)。这意味着我们最终得到了嵌套的Spannested Spans(一个Span中的一个Span)。
5- 语境传播
正如我在介绍中提到的,使用Python自动仪表的好处之一是它可以为你处理跨服务的上下文传播问题。然而,如果你不使用自动仪表,你就必须自己处理上下文传播问题。很好。真是太好了。
但在我们研究如何做到这一点之前,我们需要首先了解上下文传播。
定义的时间!
上下文代表了与跨进程边界相关的信息。
传播是上下文被捆绑并在服务中和跨服务中传输的手段,通常是通过HTTP头文件。
这意味着,当一个服务调用另一个服务时,它们将作为同一个TraceTrace的一部分被联系在一起。然而,如果你走的是纯手工仪表路线(就像我们今天做的那样),你必须确保你的上下文在相互调用的服务之间传播,否则你就会出现单独的、不相关的(即使它们应该是相关的)Trace。
我不得不承认,我一直在绞尽脑汁地试图弄清这个上下文传播的问题。在花了很多时间在谷歌上搜索并向周围的人求证后,我终于明白了,所以我打算在这里与你分享这篇文章,希望能让你少些压力。
注意。尽管OpenTelemetry文档确实提供了一些关于如何在Python中进行手动上下文传播的见解在Python中进行手动上下文传播,但该文档还需要做一点工作。我实际上是OpenTelemetry Comms SIGOpenTelemetry Comms SIG的一部分,所以我正在用这个作为动力来改进围绕这个主题的文档......也请继续关注OTel文档的更新。😎
好吧,那么我们如何做这个手动上下文传播呢?首先,让我们提醒自己,在我们的例子应用中发生了什么。我们有一个客户端服务和一个服务器服务。客户端服务调用服务器服务上的/ping 端点,这意味着我们希望它们是同一个Trace的一部分。这又意味着我们必须确保它们都有相同的Trace ID,以便被Lightstep(和其他Observability后端)视为相关。
在高层次上,我们通过以下方式实现这一目标。
-
获取客户端的Trace ID
-
在客户端调用服务器之前,将跟踪ID注入到HTTP头中
-
在服务器端从HTTP头中提取客户端的跟踪ID
很简单!现在让我们来看看实现这一目的的代码。
首先,我们需要从一个叫做carrier 的东西开始。carrier 只是一个包含Trace ID的键-值对,它看起来像这样。
{'traceparent': '00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01'}
其中traceparent 是键,而值是你的跟踪ID。请注意,以上只是一个例子,说明跟踪ID可能是什么样子的。显然,你自己的Trace ID将是不同的(而且每次你运行代码时都会不同)。
好的,很好。现在我们如何获得上述carrier ?
首先,我们需要在client.pyclient.py中导入一个TraceContextTextMapPropagator 。
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
接下来,我们必须填充载体。
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
如果你在这一行之后检查carrier 的值,你会看到它看起来像这样。
{'traceparent': '00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01'}
看起来很熟悉吗?🤯
现在我们有了carrier ,我们需要在调用服务器之前把它放到我们的HTTP头中。
header = {"traceparent": carrier["traceparent"]}
res = requests.get(url, headers=header)
然后就可以了!你的载体就在HTTP请求中!
现在我们知道了所有这些片段的作用,让我们把它们放在一起。这就是我们的客户端代码的样子。
def send_requests(url):
with tracer.start_as_current_span("client operation"):
try:
carrier = {}
TraceContextTextMapPropagator().inject(carrier)
header = {"traceparent": carrier["traceparent"]}
res = requests.get(url, headers=header)
print(f"Request to {url}, got {len(res.content)} bytes")
except Exception as e:
print(f"Request to {url} failed {e}")
pass
关于完整的代码列表,请查看client.pyclient.py。
好了......我们已经把客户端的东西整理好了。耶!现在让我们到服务器端去,从HTTP请求中提取我们的carrier 。
在server.pyserver.py中,我们从我们的头中提取traceparent 的值,像这样。
traceparent = get_header_from_flask_request(request, "traceparent")
其中我们定义get_header_from_flask_request 为。
def get_header_from_flask_request(request, key):
return request.headers.get_all(key)
现在我们可以根据这些信息建立我们的carrier 。
carrier = {"traceparent": traceparent[0]}
我们用它来提取这个carrier 的上下文。
ctx = TraceContextTextMapPropagator().extract(carrier)
现在我们可以用上下文创建我们的Span,ctx 。
with tracer.start_as_current_span("/ping", context=ctx):
在这里,我们将ctx 传递给一个名为context 的命名参数。这确保了我们的"/ping" Span知道它是现有Trace的一部分(源自我们的客户端调用的那个)。
值得注意的是,"/ping" Span的任何子Span都不需要我们传入上下文,因为那是隐式传入的(见server.py,81行server.py,81行,例如)。
现在我们知道了所有这些片段的作用,让我们把它们放在一起。下面是我们的服务器代码的样子。
...
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
...
def get_header_from_flask_request(request, key):
return request.headers.get_all(key)
...
@app.route("/ping")
def ping():
traceparent = get_header_from_flask_request(request, "traceparent")
carrier = {"traceparent": traceparent[0]}
ctx = TraceContextTextMapPropagator().extract(carrier)
with tracer.start_as_current_span("/ping", context=ctx):
length = random.randint(1, 1024)
redis_integration(length)
pymongo_integration(length)
sqlalchemy_integration(length)
return _random_string(length)
...
关于完整的代码列表,请查看server.pyserver.py。
第二部分:试试吧
现在我们知道了所有这些背后的理论,让我们来运行我们的例子吧!
1- 克隆 repo
git clone https://github.com/lightstep/opentelemetry-examples.git
2- 设置
让我们先从设置我们的Python虚拟环境开始。
3- 运行服务器应用程序
我们已经准备好运行服务器了。请确保用你自己的Lightstep Access TokenLightstep Access Token替换 。
export LS_ACCESS_TOKEN=""
export OTEL_RESOURCE_ATTRIBUTES=service.name=py-opentelemetry-manual-otlp-server,service.version=10.10.9
python server.py
还记得我告诉你,我们会看到一个传入OTEL_RESOURCE_ATTRIBUTESOTEL_RESOURCE_ATTRIBUTES的值的例子吗?好吧,这就是了!在这里,我们传入了服务名称py-opentelemetry-manual-otlp-server ,以及服务版本10.10.9 。服务名称将在Lightstep资源管理器中显示出来。
你的输出将看起来像这样。
4- 运行客户端应用程序
打开一个新的终端窗口,并运行客户端应用程序。请确保用你自己的Lightstep Access TokenLightstep Access Token替换 。
PS:确保你在opentelemetry-examples repo根中的python/opentelemetry/manual_instrumentation 。
export LS_ACCESS_TOKEN=""
export OTEL_RESOURCE_ATTRIBUTES=service.name=py-opentelemetry-manual-otlp-client,service.version=10.10.10
python client.py test
注意我们是如何传入服务名称py-opentelemetry-manual-otlp-client ,以及服务版本10.10.10 。该服务名称将显示在Lightstep资源管理器中。
当你运行客户端应用程序时,它将持续调用/ping 端点。让它运行几次(也许5-6次左右),然后杀死它(à lactrl+c )。输出样本。
如果你偷看一下运行server.py 的终端,你可能会注意到一个超级丑陋的堆栈跟踪。不要惊慌!/ping 服务会调用RedisRedis和MongoDBMongoDB,由于这些服务都没有运行,你最终会得到一些讨厌的错误信息,比如这样。
5- 在Lightstep中看到它
如果你从资源管理器中选择py-opentelemetry-manual-otlp-client 服务,进入Lightstep中的跟踪视图(你也可以通过进入py-opentelemetry-manual-otlp-server 服务看到同样的东西),你会看到端到端的跟踪,显示客户端调用服务器,以及服务器内调用的其他函数。
还记得步骤4中的堆栈跟踪吗?嗯,它在你的跟踪中显示为一个错误。这很酷,因为它告诉你,你有一个问题,并指出了问题发生的地点!这有多酷?这有多酷呢?
还记得我们是如何将我们的上下文传递给redis_integration 和server redis operation 跨度的吗?你可以看到server redis operation 滚动到redis_integration ,然后再滚动到/ping ,就像我说的那样。神奇!🪄
最后的思考
今天我们学习了如何手动配置OpenTelemetry for Python以连接到Lightstep(这也适用于任何摄取OTLP格式的Observability后端OTLP格式)。我们还学习了如何通过手动上下文传播将相关服务连接在一起。
现在,如果你发现自己需要在不使用Python自动仪表二进制的情况下连接到Observability后端和/或需要在服务间手动传播上下文,你就会知道如何去做了