跳到主要内容

SDK 启动与 OTel 导出管道

30 秒导读: OpenLLMetry 的 traceloop-sdk 让你只写一行 Traceloop.init(),就把 OpenTelemetry(简称 OTel,业界标准的可观测数据协议与 SDK)那一堆零件——采样器、 处理器、导出器、资源属性、还有几十个 LLM 库的自动埋点——全部装配成一条能往后端发数据 的管道。这一章讲的就是这行 init() 背后到底发生了什么。

本章只讲装配:参数怎么分支、单例怎么建、exporter 怎么按 URL scheme 选、三条管道 (trace / metrics / logs)怎么并行起来。每次 LLM 调用怎么被拦下来记成 span(自动埋点的猴补丁) 是 下一章;span 上到底记了哪些字段第 3 章;手动 @workflow / @task 装饰器第 4 章


1. 这是什么(零基础也能懂)

一句话定义

Traceloop.init()一个把 OpenTelemetry 管道一次性配好的启动函数。你不用手动创建 TracerProvider、不用挑 HTTP 还是 gRPC exporter、不用逐个去 instrument() OpenAI / Anthropic / LangChain——init() 一把全干了。

解决什么问题 / 给谁用

假设你写了个用 OpenAI 和 LangChain 的 AI 应用,想知道:每次模型调用花了多少 token、 延迟多少、prompt 和 completion 是什么、一条 agent 链路里哪一步慢。

裸用 OpenTelemetry 你得手写几十行样板:建 provider、配 exporter、设 resource、装 processor, 再给每个库找对应的 instrumentation 包挨个 instrument()OpenLLMetry 把这段样板压成一行。 用户就是想给 LLM 应用加可观测性、但不想碰 OTel 底层细节的工程师。

用起来什么样

一个最小真实用法(教学示意,非源码):

from traceloop.sdk import Traceloop

# 只要一行:自动配好 provider + exporter + 给所有已安装的 LLM 库埋点
Traceloop.init(app_name="my-rag-app")

# 之后照常调你的 LLM,调用会被自动记成 span 发到 Traceloop 后端

只给 app_name,其余全走默认:数据发去 https://api.traceloop.com,鉴权 key 从环境变量 TRACELOOP_API_KEY 读。想换后端、换鉴权、只埋某几个库,都是 init() 的参数(见 §3)。

一句话直觉

init() 想成"总装车间的一键装配线":OTel 提供一堆零件(provider / processor / exporter / instrumentor),init() 按你给的几个开关,把零件拼成一条能跑的流水线, 再用一个全局单例把它焊死,保证整个进程里只有这一条。


2. 顶层全景(它大概怎么转)

三个入口文件

init() 本身住在 __init__.py,但真正的装配逻辑在 tracing.pyTracerWrapper 里。 metrics 和 logging 各有一个平行的 Wrapper。

部件干什么在哪个文件
Traceloop.init顶层入口:解析参数、校验、组装 header、依次点燃三条管道traceloop/sdk/__init__.py:49 init
TracerWrappertrace 管道的单例:建 provider + processor + exporter + 自动埋点traceloop/sdk/tracing/tracing.py:66 TracerWrapper
MetricsWrappermetrics 管道的单例:建 MeterProvider + 周期性 metric readertraceloop/sdk/metrics/metrics.py:22 MetricsWrapper
LoggerWrapperlogs 管道的单例:建 LoggerProvider + 把 Python logging 接进来traceloop/sdk/logging/logging.py:17 LoggerWrapper
Instruments 枚举"装哪些库"的开关清单traceloop/sdk/instruments.py:4 Instruments
config 环境开关四个 TRACELOOP_* 环境变量,总开关级别地关掉某条管道traceloop/sdk/config/__init__.py:4

主线走一遍(高层,不进代码)

一次 Traceloop.init(app_name="x") 从上到下的流向:

Traceloop.init(...)

│ 1. 归一化 use_attributes / 检查 enabled=False 早退
│ 2. 环境变量覆盖:TRACELOOP_BASE_URL / _API_KEY / _HEADERS 优先于入参
│ 3. is_tracing_enabled()? 否 → 打印 "Tracing is disabled" 直接 return
│ 4. 组装鉴权 header(见 §4):有 api_key 且没自带 exporter → Bearer token
│ 5. 缺 key 且用默认后端且没自带 exporter/processor → 打印报错 return

├─► TracerWrapper.set_static_params(resource, content, endpoint, headers) ← 把配置塞进类变量
├─► TracerWrapper(...) ← 单例:建 provider+processor+exporter,启动所有自动埋点

├─► (若 metrics 未被关) MetricsWrapper.set_static_params → MetricsWrapper(...)
├─► (若 logging 开启) LoggerWrapper.set_static_params → LoggerWrapper(...)

└─► (若是 traceloop.com 官方后端且有 key) 建 Fetcher(可选) + Client,并 return client

一句话:init() 是编排层,只做"校验 + 组 header + 决定点燃哪几条管道";真正的 OTel 装配下沉在三个 Wrapper 里,各自是单例。


3. Traceloop.init() 的参数与分支

这节先把 init() 当黑盒,讲清"你能拨哪些开关、每个开关把管道拨向哪条岔路"。

3.1 参数清单(按用途分组)

init() 的完整签名在 traceloop/sdk/__init__.py:49-76 init。参数很多,按用途分组看:

身份与后端

参数默认作用
app_namesys.argv[0]服务名,写进 OTel resource 的 service.name
api_endpointhttps://api.traceloop.com数据发往哪;可被 TRACELOOP_BASE_URL 覆盖
api_keyNone鉴权 key;可被 TRACELOOP_API_KEY 覆盖
headers{}自定义鉴权 header;可被 TRACELOOP_HEADERS 覆盖
enabledTrue总开关,False 直接禁用

自定义导出管道(高级用户接管 OTel)

参数作用
exporter传入你自己的 SpanExporter,替换默认 OTLP exporter
processor传入 SpanProcessor 或其列表,完全接管处理层
sampler传入 OTel Sampler 控制采样
propagator传入 TextMapPropagator 设为全局
disable_batchTrueSimpleSpanProcessor(不攒批,来一条发一条)
span_postprocess_callbackspan 结束时回调,可改/脱敏属性(见 §5.3)

埋点范围

参数作用
instruments一个 Set[Instruments],埋这些库;None=全埋
block_instruments从待埋集合里减掉这些库
use_attributesprompt/completion 记成 span 属性(True,默认)还是 OTel log 事件(False)

三条管道各自的 exporter/header:metrics_exporter / metrics_headers / logging_exporter / logging_headers 让你给 metrics、logs 单独指定导出目标。

3.2 关键分支:六条早退路径

init() 里最值得记的是它在哪些情况会提前 return 或走岔路。按代码顺序:

#条件结果位置
1use_attributesuse_legacy_attributes 同时传TypeError__init__.py:92-96
2enabled=False标记 disabled,打印后 return(啥都不建)__init__.py:111-118
3is_tracing_enabled() 为假(环境变量关了)打印 "Tracing is disabled" return__init__.py:124-126
4缺 key + 用默认后端 + 没自带 exporter/processor打印"Missing API key"报错 return__init__.py:147-160
5exporterprocessor 同时传警告 exporter 会被忽略(不早退)__init__.py:130-137
6官方后端 + 有 key + 没自带管道Client返回它__init__.py:230-247

分支 4 是新手最常撞的墙——只 Traceloop.init() 不给 key、又没配环境变量时,SDK 不会崩, 而是打印一段红字提示去建 key,然后静默 return,什么都不记。真源码:

# __init__.py:147-160 init()
if (
not exporter
and not processor
and api_endpoint == "https://api.traceloop.com"
and not api_key
):
print(Fore.RED + "Error: Missing Traceloop API key, ...")
print("Set the TRACELOOP_API_KEY environment variable to the key")
return

注意这个校验带三个"逃生阀":只要你自带了 exporter、或自带 processor、或把 endpoint 换成了 自己的后端,缺 key 就不算错——因为那些场景根本不需要 Traceloop 的 key。

3.3 use_attributes 的三态归一

use_attributes 是个三态归一的小逻辑,在 __init__.py:92-110。它和已废弃的 use_legacy_attributes 是别名关系,归一规则:

  • 两个都传 → TypeError(__init__.py:92)。
  • 只传旧的 use_legacy_attributes → 发 DeprecationWarning,把值搬给 use_attributes
  • 最终仍是 None → 默认 True

True(默认)表示 prompt/completion 记成 span 属性(gen_ai.input.messages 等,符合当前 OTel GenAI 语义约定);False 表示改走 OTel log 事件路径,此时应用必须自己配好 EventLoggerProvider,否则数据无处可去(见 __init__.py:77-91 的 docstring)。这个开关最终被 一路透传给每个 instrumentor(见 §6)。


4. 鉴权 header 是怎么组装的

这节单独拎出来,因为它是"缺 key 报错"那条路径的另一半,也是初学者容易困惑的地方: 我到底该传 api_key 还是 headers?

组装顺序

init() 里 header 的确定分三步(__init__.py:142-175):

1. headers = os.getenv("TRACELOOP_HEADERS") or headers # 环境变量最优先
2. 若 headers 是字符串 → parse_env_headers() 解析成 dict
3. 若 (有 api_key) 且 (没自带 exporter/processor) 且 (headers 仍为空):
headers = {"Authorization": f"Bearer {api_key}"} # 用 key 拼 Bearer

用表格看清优先级(从高到低):

来源谁赢说明
TRACELOOP_HEADERS 环境变量最高存在则直接覆盖入参 headers,可为 "k1=v1,k2=v2" 字符串
入参 headers次之你手动传的自定义 header
api_key 拼的 Bearer兜底仅当上面都空时,才用 key 拼 Authorization: Bearer <key>

所以规则是:自定义 headers 一旦非空,api_key 就不会再拼 Bearer——两者互斥,header 赢。 parse_env_headers 来自 opentelemetry.util.re(__init__.py:15 导入),负责把 "key1=val1,key2=val2" 这种环境变量字符串解析成 dict。

组好的 header 去哪了

组好的 headers 连同 endpoint 一起,通过 TracerWrapper.set_static_params(...) 塞进 TracerWrapper类变量(__init__.py:181-183),之后 exporter 创建时从类变量取用。metrics 和 logs 各自 还能用 TRACELOOP_METRICS_HEADERS / TRACELOOP_LOGGING_HEADERS 覆盖,否则回落到 trace 的 headers(__init__.py:206-208219-221)。


5. TracerWrapper 单例:管道真正装配的地方

前面都是编排,这节进 tracing.py,看 trace 管道到底怎么建。这是本章的核心。

5.1 为什么用单例,单例怎么实现

要解决的小问题: 一个进程里只应该有一套 OTel provider/processor。如果 init() 被调两次、 或框架里别处也初始化了,不能装两条管道重复发数据。

思路:__new__ 而非 __init__ 实现单例——第一次创建时把实例挂到类属性 cls.instance, 以后再 TracerWrapper(...) 都返回同一个对象,装配代码只在第一次跑。

真源码骨架(tracing.py:75-92 TracerWrapper.__new__):

def __new__(cls, disable_batch=False, processor=None, ..., use_attributes=True):
if not hasattr(cls, "instance"): # 第一次才装配
obj = cls.instance = super().__new__(cls)
if not TracerWrapper.endpoint: # 没先 set_static_params → 空壳返回
return obj
... # 建 provider / processor / 埋点
return cls.instance # 之后永远返回同一个

注意那个 if not TracerWrapper.endpoint: return obj——endpoint 是通过 set_static_params(tracing.py:202)写进类变量的。必须先 set_static_params 再实例化, 否则拿到的是没接管道的空壳。这也是为什么 init() 里严格是"先 set_static_paramsTracerWrapper(...)" 的顺序(__init__.py:181-196)。

5.2 装配的四步

单例第一次创建时,__new__ 依次做四件事:

① init_tracer_provider(resource, sampler) → 拿到/新建 TracerProvider
② 装 span processor(三种情况,见 5.3) → provider.add_span_processor(...)
③ ThreadingInstrumentor().instrument() → 让 OTel context 跨线程传播
④ init_instrumentations(...) → 点燃所有自动埋点(第 2 章)
+ atexit.register(exit_handler) → 进程退出时强制 flush

① provider 的获取——不粗暴覆盖别人的(tracing.py:472-492 init_tracer_provider):

它先 get_tracer_provider() 看当前全局 provider 是什么,分三种情况:

当前全局 provider处理位置
ProxyTracerProvider(还没人设过,OTel 默认占位)新建 TracerProvider(带/不带 sampler)并设为全局tracing.py:478-483
已有真 provider 但不支持 add_span_processor记 error,返回 None(装不了)tracing.py:484-488
已有支持的真 provider复用它,只往上加自己的 processortracing.py:489-490

这个设计让 OpenLLMetry 能和别人已经配好的 OTel 共存——能挂就挂到现成 provider 上,不硬抢

③ ThreadingInstrumentor(tracing.py:169):OTel 的 context(里面装着 workflow 名、 association 属性等,见第 4 章)默认不跨线程。装上它,新起的线程会继承父线程的 context, 保证 span 归属正确。注释直言"we always want it"。

④ atexit flush(tracing.py:182):注册 exit_handlerflush()。BatchSpanProcessor 是攒批 异步发的,进程若直接退出,缓冲区里的 span 会丢。atexit 保证退出前 force_flush() 把攒下的 span 发出去——本地脚本、短命任务尤其需要。

5.3 装 processor 的三种情况

第 ② 步是整个 __new__ 最绕的地方,分三条互斥的岔路(tracing.py:100-163):

processor 参数是什么?

├─ 是 list ──────────► 多 processor:逐个把它们的 on_start 链上
│ Traceloop 自己的 on_start(chained),再 add

├─ 是单个 ──────────► 单 processor(向后兼容):同样 chain on_start 后 add

└─ 是 None(默认)──► get_default_span_processor():
按 endpoint 建默认 OTLP exporter + Batch/Simple 处理器
(若有 span_postprocess_callback,再包一层 on_end)

为什么要"链 on_start"? Traceloop 需要在每个 span 开始时往上打 workflow / agent / association 等属性(逻辑在 _span_processor_on_startdefault_span_processor_on_start,tracing.py:369)。 用户自带 processor 时,不能丢掉它原本的 on_start,于是把两者串起来:先/后调用原始的,再调 Traceloop 的(单/多两种情况里调用顺序略有差别,见 tracing.py:106-113123-128)。

span_postprocess_callback 的 unfreeze 技巧(tracing.py:138-160),这是本章最精巧的一处:

OTel 在 Span.end() 里会把 span 的属性冻结(设 _immutable = True),之后 on_end 再想改 属性就改不动了。可 Traceloop 想给用户一个"span 结束时脱敏/改写属性"的钩子。解法是包一层 wrapped_on_end:调回调临时把 _immutable 关掉,调完再恢复,最后才调原始 on_end

# tracing.py:142-158 wrapped_on_end(span)
attributes = getattr(span, "_attributes", None)
was_immutable = getattr(attributes, "_immutable", False)
if was_immutable:
attributes._immutable = False # 临时解冻
try:
span_postprocess_callback(span) # 用户回调可改属性
finally:
if was_immutable:
attributes._immutable = True # 恢复冻结
original_on_end(span) # 再走正常处理

这是在 OTel 官方 API 没给"改已结束 span"能力时,靠内部字段绕过限制的实用主义写法—— 也意味着它依赖 OTel 内部实现(_attributes._immutable),上游改了就会失效。

5.4 默认 processor:Batch vs Simple、is_notebook 特判

get_default_span_processor(tracing.py:439-469)负责在没自带 processor 时造一个默认的。 两个决策:

用哪种 processor?

条件选择语义
disable_batch=True is_notebook()SimpleSpanProcessor每条 span 结束立即同步导出
否则BatchSpanProcessor攒批异步导出,吞吐高、延迟低
# tracing.py:462-465
if disable_batch or is_notebook():
processor = SimpleSpanProcessor(spans_exporter)
else:
processor = BatchSpanProcessor(spans_exporter)

为什么 notebook 要特判? is_notebook()(utils/__init__.py:17)通过能否 from IPython import get_ipython() 且拿到非 None 的 ip 来判断是不是在 Jupyter 里跑。notebook 是交互式、可能随时中断的环境,BatchSpanProcessor 攒批可能让你在 cell 跑完还看不到 span; 换成 Simple 保证每 cell 立即出数据,交互体验更好。

造好的 processor 还被打了个标记 setattr(processor, "_traceloop_processor", True) (tracing.py:467),供别处识别"这是 Traceloop 自己的默认 processor"。

5.5 exporter 按 URL scheme 选:HTTP / gRPC / gRPCs

默认 exporter 由 init_spans_exporter(tracing.py:327-366)根据 api_endpointURL scheme 决定用 HTTP 还是 gRPC。这是 §1 里承诺的"你不用自己挑 exporter"的兑现处:

urlparse(api_endpoint).scheme
├─ "http" / "https" ─► HTTPExporter,endpoint 补成 <base>/v1/traces
├─ "grpc" ─► GRPCExporter(insecure=True) 明文 gRPC
├─ "grpcs" ─► GRPCExporter(insecure=False) TLS gRPC
└─ 无 scheme ─► GRPCExporter(insecure=True) 向后兼容:默认明文 gRPC
schemeexporter细节
http / httpsHTTPExporter(OTLP over HTTP)自动把路径补成 /v1/traces(若还没以它结尾)
grpcGRPCExporter insecure=Trueparsed.netloc 作 endpoint,明文
grpcsGRPCExporter insecure=FalseTLS 加密 gRPC
无 schemeGRPCExporter insecure=True老行为:裸 host:port 当明文 gRPC

HTTP 分支补路径的真源码(tracing.py:347-353):

case "http" | "https":
base_url = api_endpoint.strip().rstrip('/')
if not base_url.endswith('/v1/traces'):
endpoint = f"{base_url}/v1/traces" # 补 OTLP HTTP 的标准路径
else:
endpoint = base_url
return HTTPExporter(endpoint=endpoint, headers=headers)

两个 exporter 类在文件顶部用别名导入以避免同名冲突(tracing.py:9-14):OTLPSpanExporter as HTTPExporteras GRPCExporter

顺带一提:Traceloop.get_default_span_processor()(__init__.py:256)是把这套默认 processor 暴露给用户的公开 API——想"默认管道 + 自己的自定义 processor 并存"时,用它拿到默认那条, 再一起塞进 processor=[...] 列表。


6. 埋点开关:Instruments 枚举与 init_instrumentations

装完 provider/processor,第 ④ 步 init_instrumentations(tracing.py:495-641)决定"给哪些库埋点"。 本章只讲开关逻辑,每个 instrumentor 内部的猴补丁是 第 2 章

6.1 Instruments 枚举 = 可埋的库清单

Instruments(instruments.py:4)是个枚举,每个成员对应一个能被自动埋点的库——OpenAI、 Anthropic、Bedrock、LangChain、LlamaIndex、CrewAI、以及一堆向量库(Chroma / Pinecone / Qdrant / Weaviate…) 和 HTTP 客户端(requests / urllib3)。它就是"装哪些库"的开关名录

(枚举里 CREW = "crewai"CREWAI 的已废弃别名,注释说未来大版本会删,instruments.py:12。)

6.2 集合运算决定最终埋哪些

init_instrumentations 开头用两行集合运算算出最终要埋的集合(tracing.py:502-507):

block_instruments = block_instruments or set()
# 显式判 None,因为空 set 也是 False:传空集合 = 一个都不埋
instruments = instruments if instruments is not None else set(Instruments)
instruments = instruments - block_instruments # 减去被 block 的

逻辑三态:

instruments 入参含义
None(默认)全部 set(Instruments)
非空集合只埋这几个
空集合 set()一个都不埋(注释特意强调"显式判 None,因为空 set 是 False")

再从中减去 block_instruments。之后一个大 if/elif 链(tracing.py:510-630)对每个选中的 instrument 调对应的 init_xxx_instrumentor()

6.3 每个 instrumentor 的防御式加载

每个 init_xxx_instrumentor 都是同一套防御模式,以 OpenAI 为例(tracing.py:644-665):

def init_openai_instrumentor(should_enrich_metrics, base64_image_uploader, use_attributes=True):
try:
if is_package_installed("openai"): # ① 库真装了才埋
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
instrumentor = OpenAIInstrumentor(..., use_attributes=use_attributes)
if not instrumentor.is_instrumented_by_opentelemetry: # ② 没被埋过才埋
instrumentor.instrument()
return True
except Exception as e:
logging.error(f"Error initializing OpenAI instrumentor: {e}") # ③ 失败只记日志不崩
return False

三重防御:装了才埋(is_package_installed)、没埋过才埋(is_instrumented_by_opentelemetry)、 出错不炸(整个包一层 try/except 只记 error)。返回 True/False 汇总成 instrument_set—— 一个都没成功时打印 "No valid instruments set" 警告(tracing.py:632-639)。

use_attributesinit() 一路透传到支持它的 instrumentor(OpenAI / Anthropic / Bedrock / LangChain / LiteLLM / Groq / Together / Watsonx / SageMaker 等,见 tracing.py:510-621 的调用点), 决定 prompt/completion 记成属性还是 log 事件。


7. 三条并行管道:metrics 与 logging

trace 管道之外,init() 还会视配置点燃 metrics 和 logs 两条结构几乎一模一样的平行管道。

7.1 三者同构

三个 Wrapper 都是"单例 + set_static_params 先塞类变量 + 按 endpoint 选 HTTP/gRPC exporter"的同一套路:

管道Wrapperproviderexporter 选择
tracesTracerWrapper (tracing.py:66)TracerProviderscheme 四分支(§5.5)
metricsMetricsWrapper (metrics.py:22)MeterProvider + PeriodicExportingMetricReader"http" in endpoint → HTTP,否则 gRPC(metrics.py:61-65)
logsLoggerWrapper (logging.py:17)LoggerProvider + BatchLogRecordProcessor同上(logging.py:53-57)

注意 metrics/logs 的 exporter 选择比 trace :只做 "http" in endpoint.lower() 的字符串包含 判断(metrics.py:62logging.py:54),没有 trace 那套完整的 scheme 解析。

7.2 metrics 管道的两个亮点

MetricsWrapper(metrics.py:30)单例建 MeterProvider,用 PeriodicExportingMetricReader 周期性推送(metrics.py:76)。它带自定义 View(metrics.py:87-155 metric_views):给 token 用量、Pinecone 查询延迟、查询分数分别配了显式分桶的直方图聚合——比如 token 用量用 [1, 4, 16, 64, ... 67108864] 这种指数桶,让 token 分布在直方图里更有意义。

7.3 何时点燃 / 何时跳过

metrics 和 logs 不总是启动,init() 里有各自的守卫条件:

metrics 被跳过当(__init__.py:198-203):

  • is_metrics_enabled() 为假(TRACELOOP_METRICS_ENABLED != true),

  • 你自带了 trace 管道(processorexporter)却没给 metrics_exporter——此时打印 "Metrics are disabled"。

    逻辑变量名很自解释:custom_trace_without_custom_metrics = has_custom_spans_pipeline and not metrics_exporter

logs 才启动当(__init__.py:217):is_logging_enabled() 为真 (有 logging_exporter 或没自带 trace exporter)。LoggerWrapper 还会 LoggingInstrumentor().instrument(set_logging_format=True), 把标准库 logging 的日志也接进 OTel(logging.py:38)。

7.4 config 的四个环境开关

管道级别的总开关都在 config/__init__.py,四个函数各读一个环境变量:

函数环境变量默认控制
is_tracing_enabledTRACELOOP_TRACING_ENABLEDtrue关掉整条 trace 管道(§3.2 分支 3)
is_content_tracing_enabledTRACELOOP_TRACE_CONTENTtrue是否记 prompt/completion 正文
is_metrics_enabledTRACELOOP_METRICS_ENABLEDtrue关掉 metrics
is_logging_enabledTRACELOOP_LOGGING_ENABLEDfalse开启 logs(唯一默认关的)

注意 logging 默认关(config/__init__.py:16-17),其余三个默认开。is_content_tracing_enabled 的结果通过 set_static_params 存进 TracerWrapper.enable_content_tracing,配合 ContentAllowList 做按需的内容脱敏(tracing.py:189-200,细节属第 3 章)。


8. 巧妙之处(可借鉴)

  • __new__ 单例 + 类变量传参。set_static_params 先把配置写进类变量,__new__ 再读—— 绕过"单例构造函数不好传参"的老问题,还能在 endpoint 没设时返回空壳(tracing.py:75-92)。
  • 不硬抢全局 provider。 只在当前是 ProxyTracerProvider 占位时才新建,否则挂到现成 provider 上, 与他人的 OTel 配置共存(tracing.py:472-492 init_tracer_provider)。
  • on_end 前临时解冻属性。 用内部 _immutable 字段绕开 OTel "结束即冻结"的限制,让脱敏回调能改 已结束 span——实用但耦合内部实现(tracing.py:142-158)。
  • notebook 自动切 Simple processor。 用能否 import IPython 判断环境,交互场景立即出数据 (tracing.py:462 + utils/__init__.py:17)。
  • 埋点三重防御。 装了才埋 / 没埋过才埋 / 出错只记日志,单个库缺失或报错绝不拖垮 init() (tracing.py:644-665 等所有 init_*_instrumentor)。
  • atexit 强制 flush。 短命进程退出前把 BatchSpanProcessor 缓冲的 span 发出去,避免数据丢失 (tracing.py:182)。

9. 边界与局限

  • 缺 key 静默 return,不抛异常。 用默认后端却忘了配 key 时,init() 只打印红字然后什么都不建 (__init__.py:147-160);后续代码里 TracerWrapper.verify_initialized() 会再提醒一次,但很容易被忽略。
  • 依赖 OTel 内部字段。 span_postprocess_callback 的 unfreeze 依赖 span._attributes._immutable 这个非公开实现;OTel 上游若重构,回调将静默失效(tracing.py:147-156)。
  • metrics/logs 的 exporter 判断较糙。 只用 "http" in endpoint 字符串包含,不解析 scheme, 不支持 trace 那套 grpc:// / grpcs:// 显式区分(metrics.py:62logging.py:54)。
  • 多次 init 无效。 单例一旦建立,第二次 TracerWrapper(...) 直接返回旧实例,新参数被忽略 (tracing.py:89)。想换配置只能重启进程。
  • use_attributes=False 需自备 EventLoggerProvider。 否则 prompt/completion 无处落地、静默丢失 (__init__.py:82-88 docstring)。

10. 代码地图(导航索引)

用符号名 grep 比行号更抗漂移。

主题文件符号
顶层入口、参数、分支、header 组装packages/traceloop-sdk/traceloop/sdk/__init__.pyTraceloop.init
缺 key 报错路径packages/traceloop-sdk/traceloop/sdk/__init__.py:147init(missing-key 分支)
暴露默认 processor 的公开 APIpackages/traceloop-sdk/traceloop/sdk/__init__.pyTraceloop.get_default_span_processor
trace 管道单例packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyTracerWrapperTracerWrapper.__new__
配置塞类变量packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyTracerWrapper.set_static_params
provider 获取/复用packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyinit_tracer_provider
默认 processor + Batch/Simple + notebookpackages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyget_default_span_processor
exporter 按 scheme 选 HTTP/gRPCpackages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyinit_spans_exporter
span 开始时打 workflow/association 属性packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pydefault_span_processor_on_start
埋点开关与集合运算packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyinit_instrumentations
单个库的防御式加载(模板)packages/traceloop-sdk/traceloop/sdk/tracing/tracing.pyinit_openai_instrumentor
可埋库枚举packages/traceloop-sdk/traceloop/sdk/instruments.pyInstruments
metrics 管道单例 + 直方图 Viewpackages/traceloop-sdk/traceloop/sdk/metrics/metrics.pyMetricsWrappermetric_views
logs 管道单例packages/traceloop-sdk/traceloop/sdk/logging/logging.pyLoggerWrapper
四个环境总开关packages/traceloop-sdk/traceloop/sdk/config/__init__.pyis_tracing_enabled
notebook 判定packages/traceloop-sdk/traceloop/sdk/utils/__init__.pyis_notebook

接着读哪一章: 想知道 init_instrumentations 点燃后,每次 LLM 调用具体怎么被拦成 span—— 读 02 自动埋点机制;想知道 span 上记了什么字段——读 03 GenAI 语义约定;想在代码里手动标注 workflow/task——读 04 手动埋点。回到全景看 index