span 的创建

我们知道,在 OpenTelemetry 中 Span 的上下文关系是非常重要的。它直接影响了我们创建 span 的方式以及 span 的组织结构。
在 OpenTelemetry 中,我们可以直接使用 Tracer 类型的对象来创建 Span。
我们来看一下创建 Span 的方式:

  1. from opentelemetry import trace
  2. tracer = trace.get_tracer(__name__)
  3. with tracer.start_as_current_span("print") as span:
  4. print("foo")
  5. span.set_attribute("printed_string", "foo")

可以看到,上述代码中,我们演示了一种最常见的创建 span 的方式。
我们通过 with 方式创建了一个 span ,在 with 包体内:

  • 所有的操作都属于当前 span 的一部分;
  • 新创建的 span 都以当前 span 为父 span 进行创建

嵌套 span 的创建

with 体内

从上述的讲解中,在 with 体内我们创建的 span 默认都会读取当前的 span 作为新创建的 span 的父 span:

  1. from opentelemetry import trace
  2. import time
  3. tracer = trace.get_tracer(__name__)
  4. # Create a new span to track some work
  5. with tracer.start_as_current_span("parent"):
  6. time.sleep(1)
  7. # Create a nested span to track nested work
  8. with tracer.start_as_current_span("child"):
  9. time.sleep(2)
  10. # the nested span is closed when it's out of scope
  11. # Now the parent span is the current span again
  12. time.sleep(1)
  13. # This span is also closed when it goes out of scope

with 外

那如果我们创建的 span 的方式不是通过 with 创建的话,应该怎么进行处理呢?
我们来看几种场景:

  • 从 Propagator 恢复 Context;
  • 从 Span 中获取 Context;
  • 从 SpanContext 中获取 Context

从 Propagator 恢复 Context

当 Context 进行跨进程通信后,我们首先需要从传输包中获取信息并从中恢复 Context,并基于该 Context 来创建新的 Span,我们来看一个示例:

  1. from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
  2. # Extracting from carrier header
  3. carrier = {'traceparent': '00-a9c3b99a95cc045e573e163c3ac80a77-d99d251a8caecd06-01'}
  4. ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
  5. # 也可以写做:
  6. # ctx = extract(headers) 使用默认的 Propagator
  7. with tracer.start_as_current_span('child', context=ctx) as span:
  8. span.set_attribute('primes', [2, 3, 5, 7])

从 span 中恢复 context

我们来看一个示例:

  1. from opentelemetry import trace
  2. parent_span = tracer.start_span("bar")
  3. context = trace.set_span_in_context(parent_span)
  4. with tracer.start_as_current_span("baz", context=context):
  5. print("Hello world from OpenTelemetry Python!")
  6. parent_span.end()

我们可以通过 trace.set_span_in_context 得到一个 context 对象,并将该 context 作为参数传递给 start_as_current_span 函数即可。
start_as_current_span 函数除了接收 span 名称和上下文 context 对象之外,还可以接收如下参数:

  1. def start_as_current_span(
  2. self,
  3. name: str,
  4. context: Optional[Context] = None,
  5. kind: SpanKind = SpanKind.INTERNAL,
  6. attributes: types.Attributes = None,
  7. links: _Links = None,
  8. start_time: Optional[int] = None,
  9. record_exception: bool = True,
  10. set_status_on_exception: bool = True,
  11. end_on_exit: bool = True,
  12. ) -> Iterator["Span"]:
  13. """Context manager for creating a new span and set it
  14. as the current span in this tracer's context.
  15. Args:
  16. name: The name of the span to be created.
  17. context: An optional Context containing the span's parent. Defaults to the
  18. global context.
  19. kind: The span's kind (relationship to parent). Note that is
  20. meaningful even if there is no parent.
  21. attributes: The span's attributes.
  22. links: Links span to other spans
  23. start_time: Sets the start time of a span
  24. record_exception: Whether to record any exceptions raised within the
  25. context as error event on the span.
  26. set_status_on_exception: Only relevant if the returned span is used
  27. in a with/context manager. Defines wether the span status will
  28. be automatically set to ERROR when an uncaught exception is
  29. raised in the span with block. The span status won't be set by
  30. this mechanism if it was previously set manually.
  31. end_on_exit: Whether to end the span automatically when leaving the
  32. context manager.
  33. Yields:
  34. The newly-created span.
  35. """

从 SpanContext 中恢复 context

从 SpanContext 中恢复 context 与从 span 中恢复非常类似,只需要增加一步从 SpanContext 向 span 的转化即可:

  1. span = NonRecordingSpan(span_context)

修改 span

在我们创建了一个 span 对象后,我们就可以对其进行属性设置和事件添加,我们来看一些示例:

  1. from opentelemetry import trace
  2. current_span = trace.get_current_span()
  3. current_span.set_attribute("hometown", "seattle")
  4. current_span.add_event("this is event1", {"EventKey1": "EventValue1"})

可以看到,我们可以通过 set_attribute 来传入对应属性的键和值。
同时,我可以通过 add_event 方法来传入对应的 events 事件,该方法接收一个字符串(必填)和一组属性。

Baggage 的写入和读取

下面,我们来看一下在 OpenTelemetry 中如何读取和写入 Baggage。
首先是写入 Baggage:

  1. from opentelemetry import baggage
  2. with tracer.start_as_current_span(name="root span") as root_span:
  3. parent_ctx = baggage.set_baggage("context", "parent")

可以看到,写入 baggage 的方式非常简单,可以直接使用 baggage.set_baggage 函数传入对应的 baggage 的键和值即可,它将会写入当前 context 对应的 span 中。
需要注意的是,set_baggage 还会返回一个注入的 Baggage 的 Context 对象,而不会在原有的 Context 在写入 Baggage,也不会影响当前生效的 Context。
如果想要向当前生效的 Context 中写入 Baggage,具体的方式是:

  1. from opentelemetry import context
  2. from opentelemetry import baggage
  3. # 其中 context.attach 可以将当前 context 切换到对应的 context 下
  4. context.attach(baggage.set_baggage("username", "easyenv_robot"))

下面,我们来一下如何读取 span 当中的 baggage 呢:

  1. with tracer.start_as_current_span(name="root span") as root_span:
  2. parent_ctx = baggage.set_baggage("context", "parent"))
  3. print(baggage.get_baggage("context", child_ctx))

可以看到,我们可以使用 baggage.get_baggage 方式并传入对应的 key 和 context 的方式来获取对应的 Baggage。

使用多个 tracer_provider

默认情况下,我们仅需要一个全局的 TracerProvider 就可以帮助我们来创建我们所需要的 Tracer 对象了。
不过有时我们也需要多个 TraceProvider 来提供不同的服务名称或者版本号,示例代码如下:

  1. from opentelemetry import trace
  2. from opentelemetry.sdk.trace import TracerProvider
  3. from opentelemetry.sdk.resources import Resource
  4. from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor
  5. # Global tracer provider which can be set only once
  6. trace.set_tracer_provider(
  7. TracerProvider(resource=Resource.create({"service.name": "service1"}))
  8. )
  9. trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
  10. tracer = trace.get_tracer(__name__)
  11. with tracer.start_as_current_span("some-name") as span:
  12. span.set_attribute("key", "value")
  13. another_tracer_provider = TracerProvider(
  14. resource=Resource.create({"service.name": "service2"})
  15. )
  16. another_tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
  17. another_tracer = trace.get_tracer(__name__, tracer_provider=another_tracer_provider)
  18. with another_tracer.start_as_current_span("name-here") as span:
  19. span.set_attribute("another-key", "another-value")

requests 库自动注入

在 Python 中,requests 库可以说是鼎鼎大名了。它可以说是最常用的 HTTP 客户端库。下面,我们来演示一下如何针对 requests 库实现自动插桩。
首先,我们需要安装自动插桩第三方库

  1. pip3 install opentelemetry-instrumentation-requests==0.27b0

接下来,我们可以在 tracer 全局初始化时增加如下代码:

  1. from opentelemetry.instrumentation.requests import RequestsInstrumentor
  2. RequestsInstrumentor().instrument(span_callback=http_span_callback)

这样一来,后续所有使用 requests 库发送的请求都将默认增加 trace 信息并记录 http url, code 等。
当然,如果你希望在 span 中增加更多的信息,例如响应 Header、响应体等等,那么,你可以通过传入一个 span_callback 的函数来自定义实现。
我们再来看一下代码:

  1. def http_span_callback(span, result):
  2. """
  3. # request 后置 span 写入操作
  4. :param span:
  5. :param result:
  6. :return:
  7. """
  8. if result.status_code >= 300:
  9. if len(str(result.request.body)) < 3000:
  10. span.set_attribute("http.request.body", str(result.request.body))
  11. content_length = len(result.text)
  12. span.set_attribute(SpanAttributes.HTTP_RESPONSE_CONTENT_LENGTH, content_length)
  13. if content_length < 10000:
  14. span.set_attribute("http.response.content", str(result.text))
  15. span.set_attribute("http.response.headers", str(result.request.headers))
  16. RequestsInstrumentor().instrument(span_callback=http_span_callback)

可以看到,我们定义了一个 http_span_callback 的函数,它接收两个参数:

  • span 对象,表示 http 请求对应的 Span 对象
  • result 对象,对应于 requests 库中的 Reponse 对象

你可以根据对 result 的结果分析来主动向 span 中增加 attribute、events 等一系列信息。