☰
OpenTelemetry Chroma 插桩实战:为 Chroma 向量数据库接入 OpenLLMetry 全链路可观测性
2026/10/15 17:52:25 网站建设 项目流程
  • LLMOps
  • 可观测性

【免费下载链接】openllmetry

Open-source observability for your GenAI or LLM application, based on OpenTelemetry

项目地址:https://gitcode.com/gh_mirrors/op/openllmetry
点击查看免费下载

本文以 openllmetry 仓库中的opentelemetry-instrumentation-chromadb包为蓝本,系统讲解如何用 OpenTelemetry 追踪 Chroma 向量数据库的客户端调用:从安装、一行代码接入,到 9 类 Collection 操作产生的 Span 属性、查询结果事件与错误处理语义。读完本文,你将能在 RAG 应用中为 Chroma 的增删改查自动生成标准化追踪数据,并看懂其后端语义约定与实现原理。

1. 包概览:追踪 Chroma 客户端调用的轻量插桩

opentelemetry-instrumentation-chromadb是 openllmetry 项目(基于 OpenTelemetry 的开源 GenAI/LLM 可观测性方案)中的向量数据库插桩包之一。它解决的问题是:使用官方 Chroma library 时,客户端侧对 Chroma 的每一次调用(写入、读取、查询、修改、删除)都处于可观测性盲区——既没有耗时、也没有参数和结果信息,无法和 LLM 调用串成完整链路。

该包通过wrapt对 Chroma 客户端对象做函数包装(wrapping),无需改动业务代码即可为每次调用创建 OpenTelemetry Span。从仓库结构看,包体由 5 个文件组成:

  • init.py:定义ChromaInstrumentor与待包装方法清单
  • wrapper.py:Span 创建、属性写入与事件生成的核心逻辑
  • config.py:Config类,目前仅承载exception_logger
  • utils.py:dont_throw装饰器,保证插桩逻辑自身异常不干扰业务
  • version.py:包版本号(仓库当前为0.62.4)

2. 安装与版本约束

按 README.md 提供的标准安装方式:

pip install opentelemetry-instrumentation-chromadb

从 pyproject.toml 可以确认更完整的依赖与约束信息:

  • Python 版本:>=3.10,<4
  • 核心依赖:opentelemetry-api>=1.38.0,<2、opentelemetry-instrumentation>=0.59b0、opentelemetry-semantic-conventions-ai>=0.5.1,<0.6.0(本包使用的db.chroma.*属性与db.query.*事件名均来自该语义约定包)、opentelemetry-semantic-conventions>=0.63b1
  • 可选依赖:instrumentsextra 对应chromadb本体
  • 插桩目标版本:_instruments = ("chromadb >= 0.3",)(定义于init.py),即支持 0.3 及以上的 Chroma 客户端;而测试环境使用chromadb>=0.5.0,<0.6.0

如果你使用 uv 管理依赖(仓库内各包均采用 uv,见 poetry.toml 与 uv.lock),也可以参考 project.json 中定义的 Nx 任务(uv sync --all-groups安装、uv run pytest tests/测试、uv run ruff check .静态检查)在包目录下完成开发环境搭建。

3. 快速开始:一行代码接入

README.md 给出的最小示例只有两行:

from opentelemetry.instrumentation.chromadb import ChromaInstrumentor ChromaInstrumentor().instrument()

在真实场景中,还需要先配置 TracerProvider 与 SpanProcessor,追踪数据才有去处。参照仓库测试的 conftest.py 写法,一个完整的最小接入示例是:

from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.chromadb import ChromaInstrumentor # 1. 配置 SDK 并注册导出器 provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) # 2. 插桩 Chroma(在创建 client / collection 之前执行) ChromaInstrumentor().instrument() # 3. 之后的 Chroma 调用会自动产生 Span import chromadb client = chromadb.PersistentClient(path="./chroma_data") collection = client.create_collection(name="docs") collection.add(ids=["id1"], documents=["hello chroma"]) collection.query(query_texts=["hello"], n_results=1)

instrument()支持传入tracer_provider关键字参数,以覆盖全局 provider(见init.py):

ChromaInstrumentor().instrument(tracer_provider=provider)

对应的卸载接口为ChromaInstrumentor().uninstrument(),它会逐个unwrap已包装的方法,便于在测试或动态开关场景中恢复原始行为。

4. 被追踪的方法与 Span 命名

插桩的落点由init.py 中的WRAPPED_METHODS清单决定,共 9 个:

目标对象方法Span 名称
chromadb.api.segment.SegmentAPI_querychroma.query.segment._query
chromadb.Collectionaddchroma.add
chromadb.Collectiongetchroma.get
chromadb.Collectionpeekchroma.peek
chromadb.Collectionquerychroma.query
chromadb.Collectionmodifychroma.modify
chromadb.Collectionupdatechroma.update
chromadb.Collectionupsertchroma.upsert
chromadb.Collectiondeletechroma.delete

注意两个细节:

  1. _query是 Segment 层的内部方法。当用户调用collection.query()时,实际上会同时产生两个 Span:公开层chroma.query(记录查询参数与结果事件)和内部层chroma.query.segment._query(记录 collection_id 与查询向量事件)。仓库测试 test_query.py 对此有专门断言。
  2. peek(取少量样本)、modify(修改 collection 名称/元数据)这类低频操作也被覆盖,说明插桩目标是完整的方法面而非仅查询路径。

每个 Span 在创建时都会被打上两个基础属性(见 wrapper.py):

  • db.system = "chroma":标识数据库系统
  • db.operation = <方法名>:标识本次操作,如add、query

这两项对应 OpenTelemetry 语义约定中的DB_SYSTEM/DB_OPERATION常量,保证追踪数据可被任何兼容 OTel 的后端(如 Jaeger、Tempo、Traceloop)统一解析。

5. 各操作的 Span 属性详解

属性名全部来自语义约定包 opentelemetry-semantic-conventions-ai 中的SpanAttributes.CHROMADB_*常量,按操作分组如下。

5.1 add(写入)

在 wrapper.py 的_set_add_attributes中,对add()的 4 个核心入参统计数量:

属性含义
db.chroma.add.ids_count传入的ids数量
db.chroma.add.embeddings_count传入的embeddings数量
db.chroma.add.metadatas_count传入的metadatas数量
db.chroma.add.documents_count传入的documents数量

计数采用count_or_none逻辑(wrapper.py):参数存在时取len(),为空/缺失时返回None且不写入属性。这样可以在不复制文档全文的前提下,让分析端一眼看出单次写入的规模——这在批量灌库场景(如 sample-app 中每批 100 条写入 chroma_app.py)非常有用。

5.2 get(读取)

get的属性(_set_get_attributes,wrapper.py)直接反映筛选参数:

  • db.chroma.get.ids_count:按 id 获取时的数量
  • db.chroma.get.where:过滤条件,经_encode_where转为字符串(str(where))
  • db.chroma.get.where_document:文档内容过滤条件字符串
  • db.chroma.get.limit/db.chroma.get.offset:分页参数
  • db.chroma.get.include:返回内容选择(如["documents", "metadatas"])

5.3 peek(取样)

peek仅记录db.chroma.peek.limit,即采样条数(wrapper.py)。

5.4 query(查询,核心路径)

query的属性(_set_query_attributes,wrapper.py)是最丰富的:

属性含义
db.chroma.query.embeddings_countquery_embeddings数量(按向量检索)
db.chroma.query.texts_countquery_texts数量(按文本检索)
db.chroma.query.n_results返回结果条数(即 top-k)
db.chroma.query.where过滤条件字符串
db.chroma.query.where_document文档过滤条件字符串
db.chroma.query.include返回内容选择

此外还有 Segment 层的db.chroma.query.segment._query.collection_id,记录实际查询的 collection id(_set_segment_query_attributes,wrapper.py)。注意该函数兼容位置参数与关键字参数两种传参方式:collection_id既可能通过kwargs.get("collection_id")拿到,也可能作为位置参数args[0]传入——从源码结构看,这是为了适配不同 Chroma 版本的内部调用约定。

5.5 modify / update / upsert / delete

  • modify:记录db.chroma.modify.name(新名称);代码中留有# TODO: Add metadata attribute注释(wrapper.py),说明元数据属性尚未实现
  • update:与add对称,统计db.chroma.update.ids_count/embeddings_count/metadatas_count/documents_count
  • upsert:统计db.chroma.upsert.embeddings_count/metadatas_count/documents_count
  • delete:统计db.chroma.delete.ids_count,并记录db.chroma.delete.where/where_document

6. 查询级事件:向量与结果的可观测细节

仅靠属性只能描述“查了什么”,无法描述“查到了什么”。该包在 Span事件(Event)层面补充了两种关键数据,事件名与属性名定义在语义约定包的Events/EventAttributes枚举中(semconv_ai/init.py)。

6.1 db.query.embeddings(查询向量)

在 Segment 层_query的 Span 上,每次查询的每个query_embeddings都会产生一个db.query.embeddings事件,携带db.query.embeddings.vector属性——即向量本身的 JSON 序列化(json.dumps,兼容 numpy 数组的tolist(),见 wrapper.py)。

测试 test_query.py 验证了向量长度与数值范围(Chroma 默认 embedding 函数输出的归一化向量各分量落在 [-1, 1] 区间)。这意味着后端可以直接回放“用哪个向量做了检索”。

6.2 db.query.result(查询结果)

query的 Span 会为每一个返回结果生成一个db.query.result事件,属性包括:

  • db.query.result.id:命中文档 id
  • db.query.result.distance:距离值
  • db.query.result.metadata:元数据(dict 会被 JSON 序列化)
  • db.query.result.document:命中文档内容

实现位于_add_query_result_events(wrapper.py)。由于 Chroma 查询结果是"每个查询一个内层列表"的嵌套结构(ids、distances、metadatas、documents四个平行 list-of-lists),源码用两层itertools.zip_longest展开:外层按查询个数遍历,内层按n_results逐条产生事件。因此:

  • 单次查询n_results=2→ 恰好 2 个db.query.result事件
  • 2 个query_embeddings×n_results=2→ 共 4 个事件

这一行为被 test_query_results.py 以"回归测试"的形式锁定(该文件开头注明对应 issue #1870,即修复过"一次查询只产生 1 个结果事件"的缺陷),其测试用例test_chromadb_multi_query_returns_all_chunks明确断言 2×2=4 个事件。

在 RAG 排查场景中,这套事件让工程师能直接回答三个问题:召回的是哪些 chunk?距离(相似度)是多少?这些 chunk 喂给了哪个 LLM prompt?——事件与 LLM Span 结合,即可还原完整的 RAG 链路。

7. 错误处理与异常语义

当被包装的 Chroma 调用抛异常时,wrapper.py 会执行三件事:

  1. 写入error.type属性(值为异常类名,如Exception)
  2. 记录exception事件(record_exception(e),含exception.message)
  3. 将 Span 状态置为ERROR,并携带异常信息字符串

随后重新抛出原始异常,保证业务代码的异常语义不被吞掉。测试 test_error_handling.py 通过 mock 让内部_add/_query抛错,验证了StatusCode.ERROR、error.type与exception事件三者齐备。

与上述"业务异常如实上报"互补的是dont_throw装饰器(utils.py):它包裹的是属性写入函数(_set_add_attributes等),保证即使插桩自身的取值/序列化逻辑出错,也只记 debug 日志而绝不影响真正的数据库调用——这是插桩库的健壮性底线。

8. 可配置项:exception_logger

插桩器目前只暴露一个配置点。构造ChromaInstrumentor(exception_logger=...)时,传入的可调用对象会被存入Config.exception_logger(config.py),并在dont_throw捕获到插桩层异常时被回调(utils.py)。

典型用法:把插桩自身的异常接到你的错误监控体系(如 Sentry、日志系统):

def my_exception_logger(exc): logging.getLogger("chroma_instrumentation").error("trace failed", exc_info=exc) ChromaInstrumentor(exception_logger=my_exception_logger).instrument()

9. 在真实 RAG 应用中的集成方式

仓库的 sample-app 提供了完整的落地参考。在 chroma_app.py 中,一个基于 SciFact 语料的 RAG 流程展示了本包的实际价值:

from traceloop.sdk import Traceloop Traceloop.init(app_name="chroma_app") # 自动装载包括 Chroma 在内的插桩 chroma_client = chromadb.Client() scifact_corpus_collection = chroma_client.create_collection( name="scifact_corpus", embedding_function=embedding_function ) # 批量灌库:每批 100 条,产生 chroma.add Span(带各计数属性) for i in range(0, len(corpus_df), batch_size): scifact_corpus_collection.add( ids=batch_df["doc_id"].apply(lambda x: str(x)).tolist(), documents=..., metadatas=[{"structured": structured} for structured in ...], ) # 检索:产生 chroma.query Span + db.query.result 事件 claim_query_result = scifact_corpus_collection.query( query_texts=claims, include=["documents", "distances"], n_results=3 )

该示例展示了两个要点:一是通过Traceloop.init可自动启用插桩(本包同时注册了opentelemetry_instrumentor入口点,见 pyproject.toml,因此也支持opentelemetry-instrument自动插桩方式);二是Chroma 查询 → OpenAI 补全的前后衔接——chroma.query的db.query.result事件与后续chat.completions的 LLM Span 处于同一 trace 上下文,检索质量与生成质量可以互相印证。

类似的集成还出现在 llama-index 生态中,见 llama_index_chroma_huggingface_app.py(通过ChromaVectorStore使用 Chroma),说明该插桩对上层框架的访问同样生效——只要最终调用落在chromadb.Collection的方法上。

10. 如何验证插桩效果

如果你想在本地快速验证 Span 产出,可以直接复用仓库测试的验证思路。最小验证脚本:

from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.instrumentation.chromadb import ChromaInstrumentor import chromadb exporter = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) trace.set_tracer_provider(provider) ChromaInstrumentor().instrument() client = chromadb.EphemeralClient() col = client.create_collection(name="demo") col.add(ids=["a", "b"], documents=["doc a", "doc b"]) col.query(query_texts=["doc"], n_results=2) for span in exporter.get_finished_spans(): print(span.name, dict(span.attributes), [e.name for e in span.events])

预期输出会包含:chroma.add(带db.chroma.add.*_count属性)、chroma.query(带db.chroma.query.*属性与 2 个db.query.result事件)、chroma.query.segment._query(带 collection_id 与db.query.embeddings事件)。仓库的 conftest.py 正是采用这套 InMemory 方案,可直接对照。

11. 小结:一套属性 + 两类事件的可观测模型

综合来看,opentelemetry-instrumentation-chromadb为 Chroma 建立了"基础标识属性(db.system / db.operation)+ 操作参数属性(db.chroma.*)+ 查询级事件(db.query.embeddings / db.query.result)+ 异常语义(error.type / exception 事件)"四层可观测模型:

  • 基础属性让所有 Chroma 调用可被统一筛选与聚合;
  • 参数属性忠实记录每次操作的规模与筛选条件,便于性能分析(如大批量 add、低效的 where 条件);
  • 查询事件把"召回内容"写入链路,支撑 RAG 质量归因;
  • 异常语义确保错误路径同样可观测且不改变业务行为。

这套模型与仓库中其他向量数据库插桩(Pinecone、Milvus、Qdrant、Weaviate 等,均共享opentelemetry-semantic-conventions-ai的db.*命名空间)保持一致,是 openllmetry 在向量检索可观测性上的统一设计。接入它只需pip install加一行ChromaInstrumentor().instrument(),即可让 Chroma 从"黑盒"变成链路中可查询、可对比、可回溯的一环。

  • LLMOps
  • 可观测性

【免费下载链接】openllmetry

Open-source observability for your GenAI or LLM application, based on OpenTelemetry

项目地址:https://gitcode.com/gh_mirrors/op/openllmetry
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询