跳到主要内容

零侵入自动埋点与生态集成

30 秒导读: 前几章讲的 span 都要你手写 with logfire.span(...)。本章讲三条"不改业务代码也能有 span"的路子:①导入时偷偷改写模块的 AST,把每个函数体包进一个 span;②给函数贴一个 @instrument 装饰器;③把 FastAPI / httpx / OpenAI / Anthropic 这些第三方库的关键方法替换成会自动开 span 的版本。重点看 LLM 集成——它顺手把 OpenTelemetry 的 gen_ai.* 语义约定和 token 用量/成本也采下来,正好接上本货架"评估与可观测"的主题。

本章是采集侧:讲 span 怎么被自动产生。span 产生之后在导出前还会被二次改写(比如把 LLM 的大 payload 搬进日志、脱敏),那是 04-processing-and-export.md06-sampling-and-scrubbing.md 的事,这里不重复。


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

问题: 你想给一个已有项目加可观测性,但不想在几百个函数里手动插 with logfire.span(...)

Logfire 给的答案是三条自动化路径,按"侵入程度从低到高、控制力从高到低"排:

路径你要做什么效果适用场景
① 自动埋点(auto-tracing)启动时调一次 logfire.install_auto_tracing(['myapp'], min_duration=...)你的模块里每个函数调用都自动变成一个 span想给自己的一整个包"全都埋上"
② 装饰器(@instrument)在想埋的函数上贴 @logfire.instrument(...)这一个函数变成一个 span,还能记参数/返回值精挑几个关键函数
③ 第三方集成logfire.instrument_openai() / instrument_fastapi()别人写的库(你改不了源码)也能自动出 span追 Web 请求、DB 查询、LLM 调用

一句话直觉: 路径①是"给你自己的代码装了个隐形摄像头";路径③是"给别人的代码装摄像头"——因为你动不了别人的源码,只能在运行时把它的关键方法换成一个会先开 span、再调原方法的包装版。

用起来什么样(路径①):

# 示意,非源码
import logfire

logfire.configure()
# 关键:必须在导入 myapp 之前调用
logfire.install_auto_tracing(modules=['myapp'], min_duration=0.01)

import myapp # 从这里开始,myapp 里每个函数调用都自动变成 span
myapp.run()

注意那句注释:必须赶在被埋的模块被 import 之前调用——下一节会看到,整个魔法就挂在"import 这个动作"上。


2. 顶层全景(三条路怎么各自通往一个 span)

三条路径最终都汇到同一个终点:Logfire._fast_span(name, attributes)(logfire/_internal/main.py:278)——一个为高频调用而精简过的开 span 快捷方法。区别只在"谁在什么时机把函数体/方法包进这个 span"。

你写的一次配置调用
┌──────────────────────┬───────────────────────┬────────────────────────┐
│ ① install_auto_tracing│ ② @logfire.instrument │ ③ instrument_xxx() │
│ (改写 AST) │ (包一层装饰器) │ (替换第三方方法) │
└───────────┬──────────┴───────────┬───────────┴───────────┬────────────┘
│ │ │
import 时拦截模块导入 定义时包住函数 运行时"猴补"库方法
把函数体重写成 wrapper 里 request/handler
`with span: <原体>` `with open_span(): 调原函数` 外面套一层 span
│ │ │
└──────────────┬───────┴───────────┬───────────┘
▼ ▼
Logfire._fast_span(name, attributes) ← 统一开 span 的出口
(main.py:278;LLM 集成走 logfire.span 全功能版)


进入加工流水线 → 导出(见 04 章)

部件一句话职责:

部件干什么在哪
LogfireFinder / LogfireLoader装进 sys.meta_path 拦截 import,对匹配模块施加 AST 改写_internal/auto_trace/import_hook.py
compile_source / AutoTraceTransformer把每个函数体包进 with _fast_span(...),并支持 min_duration 惰性升级_internal/auto_trace/rewrite_ast.py
install_auto_tracing装 finder、检查是否已被 import、把秒转纳秒_internal/auto_trace/__init__.py:16
instrument 装饰器给单个函数生成 wrapper,支持记参数/返回/新 trace/level_internal/instrument.py:52
instrument_* 方法群Logfire 类上一长串门面,委托给各适配器_internal/main.py:1044-2210
_internal/integrations/每个第三方库一个适配器_internal/integrations/**
llm_providers/LLM 专用适配器:gen_ai 语义约定 + token/成本_internal/integrations/llm_providers/

3. 路径①:AST 自动埋点(整个包一键埋上)

这是本章工程含量最高的一支。目标:不碰任何一行业务代码,却能让 myapp 里每个函数调用都变成 span。

3.1 思路:劫持"import"这个动作

Python 的 import 机制里有个扩展点:sys.meta_path——一串 finder(查找器)。每当你 import foo,解释器挨个问它们"你能定位 foo 吗?能的话给我一个能加载它的东西"。

Logfire 的花招是:往这串 finder 的最前面塞一个自己的 finder。当被埋的模块要被导入时,它抢先拿到模块的源码,改写源码的语法树(AST),再让改写后的版本去执行。

import myapp.foo


sys.meta_path: [ LogfireFinder , <标准 finder> , … ]

│ 1. 借标准 finder 拿到 foo 的源码与文件名
│ 2. 用 modules_filter 判断"要不要埋 foo"
│ 3. 要 → ast.parse → 改写 → compile

返回一个 LogfireLoader(执行改写后的代码)
不要 → 返回 None,让后面的 finder 走正常流程

3.2 finder:自己不干活,借别人的手找源码

LogfireFinder 是个 MetaPathFinder(import_hook.py:22)。它不自己去磁盘找文件——那太重。它遍历 sys.meta_path其它 finder,借它们把源码和文件名找出来:

  • _find_plain_specs(import_hook.py:88)逐个调用别的 finder 的 find_spec;遇到自己这一类就跳过,避免无限递归(import_hook.py:94)。
  • 拿到 spec 后,用 loader.get_source(fullname) 取源码(import_hook.py:47)。

只有当 modules_filter(AutoTraceModule(fullname, filename)) 返回 True 才继续;否则返回 None,把导入交还给正常机制(import_hook.py:63-64)。

一个关键的健壮性设计:改写在 finder 里就做完。 compile_source 被放在 find_spec 内、用 try/except 兜住(import_hook.py:77-83)。注释点破了原因:

一旦返回了 loader,就"承诺"要用它了;所以要在返回 loader 之前把可能失败的编译做完,失败就返回 None 让下一个 finder 接手——绝不能让自动埋点把用户整个应用搞崩。

改写成功,才包一个 LogfireLoader 返回(import_hook.py:85-86)。

3.3 loader:执行"改写后的代码"

LogfireLoader(import_hook.py:107)拿着两样东西:原始 spec(plain_spec)和一个 execute 闭包。它的 exec_module 只有一行——在模块自己的命名空间里执行改写后的代码(import_hook.py:121):

# 真实源码 import_hook.py:116-121
def exec_module(self, module: ModuleType):
self.execute(module.__dict__)

两个补丁式细节:

  • get_code(import_hook.py:128)为 python -m 场景服务:runpy 不走正常协议,所以这里返回一段 __loader__.execute(globals()) 让它也能触发改写后的执行。
  • __getattr__(import_hook.py:135)把其它属性访问都转发给原 loader(通常是 SourceFileLoader),假装自己就是它。

3.4 改写规则:每个函数体裹进一个 span

真正动 AST 的是 compile_source(rewrite_ast.py:20)。它给这个模块生成一个唯一全局名 logfire_<uuid>(rewrite_ast.py:39),指向一个 context_factories 列表;然后把每个函数体改成:

with context_factories[i]():
<原来的函数体>

i 是每个函数一个的常量下标(rewrite_ast.py:145-153)。执行时,execute 把这个列表塞进模块全局变量再 exec(rewrite_ast.py:46-50)。

with 的骨架逻辑在基类 BaseTransformer.rewrite_function(ast_utils.py:74),AutoTraceTransformer 继承它。基类有两个体贴的细节:

  • docstring 不裹:如果函数第一句是字符串字面量,保留在最前、不进 span,以免它不再被认作 docstring(ast_utils.py:80-89)。
  • 空/平凡函数不裹:函数体只有 pass... 或纯 docstring 时,直接跳过,不制造无意义 span(ast_utils.py:95-102)。

AutoTraceTransformer 自己再加两条豁免:

  • 生成器不埋:rewrite_function 里若 has_yield(node) 就原样返回(rewrite_ast.py:105-109)——把 with span 裹在生成器外面语义会错(span 会跨越多次 next())。has_yield(rewrite_ast.py:177)做的是"只在本函数体内"找 yield,不下钻到嵌套函数。
  • @no_auto_trace 豁免:见 §3.6。

3.5 min_duration:先掐表,够慢了才升级成真 span

如果每个函数都无条件开 span,对那些纳秒级、每秒被调百万次的小函数是灾难。min_duration 让埋点"先观察、后决定"。

logfire_method_call_node(rewrite_ast.py:111)。当 min_duration > 0 时,context_factories[i] 初始不是真 span 工厂,而是一个轻量的 MeasureTime 类(rewrite_ast.py:127-137):

# 真实源码 rewrite_ast.py:127-137(略简)
class MeasureTime:
__slots__ = 'start'
def __enter__(_self):
_self.start = timer()
def __exit__(_self, *_):
if timer() - _self.start >= min_duration:
self.context_factories[index] = span_factory # ← 就地"升级"

机制很妙:

  1. 头几次调用只掐表,不开 span——开销几乎为零。
  2. 某次运行时长 ≥ min_duration,就把列表里这一格原地换成真正的 span 工厂 partial(logfire_instance._fast_span, name, attributes)(rewrite_ast.py:114-117)。
  3. 之后再调这个函数,context_factories[i]() 拿到的就是真 span 了。

所以"够慢的函数才值得埋"这条策略,靠的是同一个列表槽位被惰性替换,而不是每次都判断——注释里也强调这段要尽量快,因为它是"永远不会被真正埋点的快函数"的固定成本(rewrite_ast.py:125-126)。min_duration=0 时直接用真 span 工厂,跳过掐表(rewrite_ast.py:138-139)。

span 的名字和属性由 logfire_method_arg_values(ast_utils.py:123)算出:文件路径、行号、code.function(限定名 qualname)、消息模板默认 Calling {module}.{qualname}

3.6 @no_auto_trace:导入期就识别的"别埋我"

有些小函数调用极频繁,埋了纯噪声。@no_auto_trace(rewrite_ast.py:159)让你把它择出去。

它的实现"什么都不做,原样返回参数"(rewrite_ast.py:174),零运行时开销。真正起作用的是 check_no_auto_trace(rewrite_ast.py:77)在改写 AST 时从装饰器列表里按名字识别它——只认 @no_auto_trace@logfire.no_auto_trace 这两种写法(rewrite_ast.py:79-91)。

docstring 说得很直白:改名、起别名、间接调用都不认(rewrite_ast.py:164-167)——因为它是在 AST 文本层面做识别,不是运行时行为。被它装饰的函数/类,以及其中嵌套定义的一切,都会被跳过(visit_ClassDef/visit_FunctionDef,rewrite_ast.py:93-103)。

3.7 安装入口:install_auto_tracing

面向用户的方法是 Logfire.install_auto_tracing(main.py:952),它直接转调 _internal/auto_trace/__init__.py:16 的同名函数。后者做四件事:

  1. modules 是字符串列表,转成一个"前缀匹配"函数(__init__.py:27-28,modules_func_from_sequence);匹配逻辑是 AutoTraceModule.parts_start_with(types.py:24),按点分边界匹配(foo 命中 foofoo.bar,但不命中 foo_bar)。
  2. check_imported_modules:若要埋的模块已经在 sys.modules(即已被 import),默认抛异常(__init__.py:44-49)——因为对已导入的模块改写已经晚了。可设 'warn' / 'ignore'
  3. min_duration 从秒转成纳秒(__init__.py:59)。
  4. LogfireFinder,插到 sys.meta_path 最前面(__init__.py:61-62)。

这解释了 §1 那句"必须在 import 之前调":finder 只能拦截之后发生的 import。


4. 路径②:@instrument 装饰器(精挑一个函数)

自动埋点是"整个包一把梭";@instrument 是"我就要这一个函数,而且要它多记点东西"。

入口是 Logfire.instrument(main.py:611/651/666,重载),核心实现是 _internal/instrument.py:52instrument。它返回一个 decorator,给目标函数生成 wrapper

wrapper 的骨架就是"开 span → 调原函数":

# 真实源码 instrument.py:112-117(同步分支)
def wrapper(*func_args, **func_kwargs):
with open_span(*func_args, **func_kwargs) as span:
result = func(*func_args, **func_kwargs)
if record_return:
set_user_attributes_on_raw_span(span._span, {'return': result})
return result

它按函数类型分四路生成 wrapper(instrument.py:75-117):普通函数、协程、生成器、异步生成器。生成器只有"当作上下文管理器用"才合理,否则会警告(GENERATOR_WARNING_MESSAGE,instrument.py:46)——原因和 §3.4 生成器不埋是同一个:with 裹生成器体语义会错。

两个关键辅助:

  • get_attributes(instrument.py:229)从函数身上抽 code.functioncode.lineno、文件路径、消息模板(默认 Calling {module}.{qualname})、tags。
  • get_open_span(instrument.py:125)是性能分级的核心。它按需返回不同的 open_span:
    • 无需抽参 → 最快路径,直接 _fast_span(instrument.py:174-177)。
    • extract_args=True 且函数有参数 → 用 inspect.signature 绑定实参,走 _instrument_span_with_args(instrument.py:183-191),它支持消息模板格式化与任意类型属性(main.py:290)。
    • extract_args 是名字列表 → 只抽这几个参数(instrument.py:212-224),缺失的名字会警告(instrument.py:203-208)。

还有两个巧思:

  • new_trace=True 时,不做当前 span 的子 span,而是开一条新 trace 并用 link 回指旧 span(extra_span_kwargs,instrument.py:158-167)。
  • get_logfire 特意避免把 logfire 作为闭包变量(instrument.py:144-156),这样用 cloudpickle 序列化被装饰函数时不会因带上 logfire 实例而失败——这是给分布式/多进程场景留的后路。

level 参数让 span 带日志级别,低于配置的 min_level 时直接返回 NoopSpan、不产生 span(instrument.py:175-176)。


5. 路径③:第三方集成(给别人的库装摄像头)

你改不了 openaifastapidjango 的源码,但你能在运行时把它们的关键方法换掉。这就是 instrument_* 家族干的事。

5.1 门面:一长串 instrument_xxx 委托给适配器

Logfire 类上有 ~40 个 instrument_xxx 方法,成片排在 main.py:1044-2210。它们几乎都是同一个模板:先 _warn_if_not_initialized_for_instrumentation(),再延迟 import 对应适配器并转调:

# 真实源码 main.py:1233-1236(instrument_fastapi 尾部)
from .integrations.fastapi import instrument_fastapi
self._warn_if_not_initialized_for_instrumentation()
return instrument_fastapi(self, app, capture_headers=..., ...)

延迟 import 是有意的:这些集成各自依赖 opentelemetry-instrumentation-fastapi 之类的可选包,不该在 import logfire 时就全拉进来。缺包时适配器顶部会抛出友好的安装提示(如 django.py:12-18)。

5.2 两种委托风格

看适配器内部,第三方集成其实分两派:

风格做法代表谁真正开 span
A. 转发给 OTel 现成 instrumentorXxxInstrumentor().instrument(...),只帮你把 provider/参数配好djangohttpxsqlalchemyredisOpenTelemetry contrib 库
B. Logfire 自己猴补直接替换库的某个方法为"先开 span 再调原方法"的包装llm_providersmcpclaude_agent_sdkLogfire 自己的 logfire.span

风格 A 例子:instrument_django 只是 DjangoInstrumentor().instrument(...) 加一句 maybe_capture_server_headers(django.py:34-40)。风格 B 是本章重点,下面细看 LLM。

5.3 LLM 集成:把 client.request 换成会开 span 的版本

这是风格 B 里最讲究的一支,也最贴本货架"评估与可观测"的主题。

instrument_openai(main.py:1247)与 instrument_anthropic(main.py:1351)几乎一样:都把三个 provider 专属函数(get_endpoint_config / on_response / is_async_client)喂给同一个通用引擎 instrument_llm_provider(llm_providers/llm_provider.py:24)。

instrument_openai / instrument_anthropic (main.py 门面,选好 provider 函数)
│ get_endpoint_config, on_response, is_async_client

instrument_llm_provider (通用引擎)
│ 猴补 client.request → instrumented_llm_request_(sync|async)

每次 LLM 调用:
get_endpoint_config(options) → 解析 url,给出 message_template + span_data(含 gen_ai.*)
with logfire_llm.span(...) → 开一个 CLIENT span
on_response(resp, span) → 把响应/usage/finish_reason 写回 span
(stream=True 时,包 stream_cls,流结束再补一条 info 日志)

猴补发生在哪: instrument_llm_provider 取出 client.request(旧版是 _request),存到 client._original_request_method,再 setattr 换成包装版(llm_provider.py:77-84168)。用 _is_instrumented_by_logfire 标志防重复(llm_provider.py:70-76)。返回一个可选的上下文管理器,退出时还原(llm_provider.py:170-185)。

包装版做什么: instrumented_llm_request_sync(llm_provider.py:138)先调 _instrumentation_setup 解析出模板与属性,再:

# 真实源码 llm_provider.py:143-149(略简)
with logfire_llm.span(message_template, _span_kind=SpanKind.CLIENT, **span_data) as span:
with maybe_suppress_instrumentation(suppress_otel):
if kwargs.get('stream'):
return original_request_method(*args, **kwargs)
else:
return on_response_fn(original_request_method(*args, **kwargs), span)

suppress_otel 那层(llm_provider.py:144188)是为了把 OpenAI 底层用 httpx 发的请求 span 抑制掉,免得一次 LLM 调用出两条重复 span。

流式响应是难点:响应还没返回时 span 就得结束,token 数要等流走完才知道。做法是子类化 provider 的 stream 类,在 __stream__ 里边转发 chunk 边喂给 StreamState(llm_provider.py:100-128),流结束后用 record_streaming 补一条 info 日志,带上时长与累计属性(llm_provider.py:197-224)。

5.4 gen_ai 语义约定:让 LLM span 说"通用语"

LLM span 的属性名不是 Logfire 自造的,而是遵循 OpenTelemetry Gen AI 语义约定——semconv.py 里一整套常量(llm_providers/semconv.py):

常量属性名含义
REQUEST_MODELgen_ai.request.model请求的模型
RESPONSE_MODELgen_ai.response.model实际响应的模型
OPERATION_NAMEgen_ai.operation.name操作类型(如 chat)
INPUT_MESSAGES / OUTPUT_MESSAGESgen_ai.input.messages / gen_ai.output.messages输入/输出消息
INPUT_TOKENS / OUTPUT_TOKENSgen_ai.usage.input_tokens / ...output_tokenstoken 用量
RESPONSE_FINISH_REASONSgen_ai.response.finish_reasons结束原因

provider_attrs(name)(semconv.py:46)统一产出 {gen_ai.system, gen_ai.provider.name}

版本切换是这里的一个设计点:SemconvVersion 取值 1'latest'(semconv.py:15)。1 用旧的 request_data/response_data 大属性;'latest'gen_ai.* 语义约定;也可传 [1, 'latest'] 同时发两套,方便迁移(见 instrument_openaiversion 参数说明,main.py:1308-1318)。Anthropic 适配器把消息转成语义约定格式的逻辑在 convert_messages_to_semconv(anthropic.py:213)。

5.5 token 用量与成本:usage.py

呼应"评估与可观测",Logfire 不止记 token,还算get_usage_attributes(llm_providers/usage.py:8)做三件事:

  1. gen_ai.usage.input_tokens / output_tokens(由各 provider 自己数好后传入)。
  2. 把原始 usage 对象 model_dump 存进 gen_ai.usage.raw(usage.py:34-36)。
  3. 用第三方库 genai_pricesextract_usage + calc_price 算出 operation.cost(美元)并写入(usage.py:39-53)。

一个稳健性细节:token 与成本互不牵连——成本计算包在独立 try/except 里静默吞掉(未知模型算不出价很正常),不会连累 token 属性(usage.py:33-56,docstring 明说)。

各 provider 抽 token 的口径不同。Anthropic 把缓存读/写的 input token 也并进 input_tokens(anthropic.py:314-318);OpenAI 则按 chat/responses/embeddings 三种 api_flavor 分流(openai.py:566-574)。on_response(anthropic.py:321)在非流式响应上把 usage、RESPONSE_MODELRESPONSE_IDfinish_reasons 全写回 span。

5.6 其它 agent 生态集成(点到)

同属风格 B、面向 agent 场景,还有几支值得知道位置:

  • claude_agent_sdk.py:instrument_claude_agent_sdk(main.py:1026)让之后创建的 ClaudeSDKClient 自动被追踪工具调用;它也复用 llm_providers/semconv 的 gen_ai 常量(claude_agent_sdk.py:23)。
  • mcp.py:instrument_mcp(main.py:1010mcp.py:28)同时给 MCP 客户端与服务端猴补 send_request 等方法,开 MCP request span;propagate_otel_context 控制是否把 traceparent 塞进请求 metadata 做分布式追踪。
  • pydantic_ai.py:instrument_pydantic_ai(main.py:1094pydantic_ai.py:14)把 Logfire 的 tracer/meter/logger provider 交给 pydantic-ai 自带的 instrumentor。

6. 巧妙之处(可带走的技术)

  • 改写放在 finder、不放在 loader:失败就退回正常 import,自动埋点绝不搞崩用户应用(import_hook.py:77-83)。这是"可观测性必须比业务更不容易出错"的体现。
  • min_duration 惰性升级:用"列表槽位就地替换"把慢函数才升级成真 span,快函数的常驻成本压到近乎为零(rewrite_ast.py:127-139)。
  • @no_auto_trace 在 AST 文本层识别:零运行时开销,但也因此"只认字面写法、不认别名"——把限制诚实写进 docstring(rewrite_ast.py:159-174)。
  • 一个通用引擎服务所有 LLM provider:provider 差异被收敛成三个函数(get_endpoint_config/on_response/is_async_client),引擎只管猴补与流式(llm_provider.py:24)。
  • token 与成本解耦:成本算不出不影响 token 落库(usage.py)。
  • get_open_span 按需返回最快的闭包:无参场景走 _fast_span,别为用不到的功能付代价(instrument.py:125-226)。

7. 边界与局限(诚实)

  • 生成器不自动埋:auto-tracing 与 @instrument 都对含 yield 的函数退避(rewrite_ast.py:105-109instrument.py:75-97),因为 with span 裹生成器体的时序语义是错的。
  • 时机敏感:install_auto_tracing 必须在被埋模块 import 之前调用;对已导入模块默认直接报错(__init__.py:36-49)。
  • 依赖源码可得:AST 改写要能拿到 .py 源码;交互式 shell、exec()、只有 .pyc 的场景取不到源码就失效(import_hook.py:42-52)。
  • 风格 A 集成不掌控 span 细节:转发给 OTel contrib instrumentor 的那些集成,span 形态由上游库决定,Logfire 只配 provider 与少量参数。
  • inspect_arguments 魔法有边界:@instrument 的 f-string 参数自省依赖 executing,取不到源码时会退化并告警(ast_utils.py:164-202)——这条与 02-fstring-magic-and-schema.md 同源。

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

主题文件路径符号名
import 拦截入口(MetaPathFinder)logfire/_internal/auto_trace/import_hook.pyLogfireFinder / find_spec / _find_plain_specs
执行改写后代码的 loaderlogfire/_internal/auto_trace/import_hook.pyLogfireLoader / exec_module / get_code
AST 编译与改写logfire/_internal/auto_trace/rewrite_ast.pycompile_source / rewrite_ast / AutoTraceTransformer
min_duration 惰性升级logfire/_internal/auto_trace/rewrite_ast.pylogfire_method_call_node / MeasureTime
生成器豁免logfire/_internal/auto_trace/rewrite_ast.pyhas_yield / rewrite_function
@no_auto_trace 及识别logfire/_internal/auto_trace/rewrite_ast.pyno_auto_trace / check_no_auto_trace
裹 span 的 AST 骨架logfire/_internal/ast_utils.pyBaseTransformer.rewrite_function / logfire_method_arg_values
安装自动埋点logfire/_internal/auto_trace/__init__.pyinstall_auto_tracing / modules_func_from_sequence
模块名前缀匹配logfire/_internal/auto_trace/types.pyAutoTraceModule.parts_start_with
面向用户的安装方法logfire/_internal/main.py:952Logfire.install_auto_tracing
@instrument 装饰器logfire/_internal/instrument.py:52instrument / get_open_span / get_attributes
开 span 快捷出口logfire/_internal/main.py:278Logfire._fast_span / _instrument_span_with_args
集成门面群logfire/_internal/main.py:1044-2210instrument_openai / instrument_anthropic / instrument_fastapi
LLM 通用引擎(猴补 + 流式)logfire/_internal/integrations/llm_providers/llm_provider.pyinstrument_llm_provider / record_streaming
Anthropic 适配器logfire/_internal/integrations/llm_providers/anthropic.pyget_endpoint_config / on_response / convert_messages_to_semconv
OpenAI 适配器logfire/_internal/integrations/llm_providers/openai.pyget_endpoint_config / get_openai_usage_attributes
gen_ai 语义约定常量logfire/_internal/integrations/llm_providers/semconv.pyREQUEST_MODEL / INPUT_TOKENS / provider_attrs / SemconvVersion
token 用量与成本logfire/_internal/integrations/llm_providers/usage.pyget_usage_attributes
MCP / Claude SDK / pydantic-ailogfire/_internal/integrations/{mcp,claude_agent_sdk,pydantic_ai}.pyinstrument_mcp / instrument_claude_agent_sdk / instrument_pydantic_ai

相邻章节:门面与一个 span 的诞生见 01-public-api-and-span.md;f-string 参数自省见 02-fstring-magic-and-schema.md;流水线组装见 03-config-and-pipeline-assembly.md;span 导出前的二次加工(含 LLM payload 搬运)见 04-processing-and-export.md;采样与脱敏见 06-sampling-and-scrubbing.md。总览见 index.md