帮你快速理解、总结文档立即下载
文档中心>日志服务>Agent 可观测>接入指南>使用 Langfuse SDK 上报 Trace 数据到 CLS

使用 Langfuse SDK 上报 Trace 数据到 CLS

最近更新时间:2026-07-28 14:15:31

我的收藏

操作场景

Langfuse SDK 可用于追踪 LLM 应用中的请求链路、模型调用、输入输出、用户信息、会话信息、耗时和异常状态。您可以将 Langfuse SDK 生成的 OpenTelemetry Trace 数据上报到日志服务(Cloud Log Service,CLS),并在 CLS 中进行检索分析和链路排查。
本文介绍如何将 Langfuse SDK 生成的 Trace 数据上报到 CLS。本文提供以下接入方式:
通过 Skill 快速接入与分析:如果您使用支持 Skill 的 AI 工具,完成 CLS 日志主题准备后,可由 AI 自动识别项目语言并生成接入代码。
手动配置接入:按步骤完成环境变量配置、代码接入和上报验证。
接入方式
数据路径
推荐场景
OTLP/HTTP 标准直传
应用 > 标准 OTLP HTTP Exporter > CLS
手动接入推荐首选。适用于新项目、快速试用、希望减少自定义代码、希望按 OpenTelemetry 标准语义上报 Trace 的场景。
自定义 SpanExporter 直传
应用 > 自定义 SpanExporter > CLS LogItem > CLS
生产增强方案。适用于需要使用 CLS SDK 的压缩、批处理能力,或需要完全控制写入字段的高吞吐场景。Skill 可生成该方式的接入代码。
说明:
本方案不需要部署 Langfuse Server,也不需要将数据发送到 Langfuse Cloud。业务代码仍使用 Langfuse SDK 生成 Trace,数据出口改为 CLS。

前提条件

开始接入前,请确保已完成以下准备工作:
已开通 日志服务 CLS
已准备具备 CLS 写入权限的访问凭证,例如 CAM 子账号、CAM Role 或临时密钥。云 API 密钥信息请前往 API 密钥管理 获取。
应用使用 TypeScript/Node.js 或 Python,并已接入或计划接入 Langfuse SDK。

准备 CLS 日志主题

1. 登录 日志服务控制台,在左侧导航栏中,选择 Agent 可观测,然后单击应用接入

2. 填写应用名称,选择地域和日志集,单击确定

3. 在左侧导航栏中,选择日志主题,切换到您在上一步中选择的地域,找到名称为 {应用名称}-trace-topic(带有应用观测 tag 标签)的日志主题,复制其日志主题 ID。

4. 根据日志主题所在地域,获取 API 上传日志域名,详情请参见 地域和访问域名

通过 Skill 快速接入与分析

如果您使用支持 Skill 的 AI 工具,可使用 cls-agent-obs-setup Skill 自动完成接入。该 Skill 可生成自定义 SpanExporter 直传方案,并处理字段映射、时间单位、批量发送、退出 flush、resourceattribute 拆分和验证说明。
使用前请准备以下信息:
参数
说明
CLS_DEFAULT_REGION
日志主题所在地域,例如 ap-guangzhou
CLS_TOPIC_ID
准备 CLS 日志主题时复制的日志主题 ID。
TENCENTCLOUD_SECRET_ID
腾讯云访问凭证 SecretId。
TENCENTCLOUD_SECRET_KEY
腾讯云访问凭证 SecretKey。
在 AI 工具中输入以下提示词:
请使用腾讯云 CLS Agent 可观测接入 Skill:
https://skillhub.cn/skills/tencent-cls-agent-obs-setup

帮我把当前 AI 应用接入腾讯云 CLS Agent 可观测。

观测 SDK:Langfuse
CLS_DEFAULT_REGION={替换为你的地域,如 ap-guangzhou}
TENCENTCLOUD_SECRET_ID={替换为你的 SecretId}
TENCENTCLOUD_SECRET_KEY={替换为你的 SecretKey}
CLS_TOPIC_ID={替换为你的日志主题 ID}

请识别当前项目语言和框架,补齐依赖、初始化代码、环境变量模板,并接入 CLS SpanExporter。若需要 CLS_ENDPOINT,请根据地域和项目语言自动推导。完成后请告诉我如何验证 Trace 是否成功上报。

手动配置接入

步骤1:配置环境变量

请在应用项目中创建或更新 .env 文件。请确保 .env 已加入 .gitignore,避免密钥泄露。
OTLP/HTTP 标准直传:
CLS_DEFAULT_REGION=ap-guangzhou
CLS_TOPIC_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TENCENTCLOUD_SECRET_ID=AKIDxxxxxxxx
TENCENTCLOUD_SECRET_KEY=xxxxxxxx
SERVICE_NAME=my-llm-app

# Python 额外需要。Langfuse Python SDK 启动时会校验密钥是否存在,此处可填写任意非空占位值。
LANGFUSE_PUBLIC_KEY=pk-placeholder
LANGFUSE_SECRET_KEY=sk-placeholder
自定义 SpanExporter 直传:
TypeScript/Node.js
Python
CLS_ENDPOINT=ap-guangzhou.cls.tencentcs.com
CLS_TOPIC_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TENCENTCLOUD_SECRET_ID=AKIDxxxxxxxx
TENCENTCLOUD_SECRET_KEY=xxxxxxxx
TENCENTCLOUD_TOKEN=
CLS_DEFAULT_REGION=ap-guangzhou
SERVICE_NAME=my-llm-app

CLS_ENDPOINT=https://ap-guangzhou.cls.tencentcs.com
CLS_TOPIC_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TENCENTCLOUD_SECRET_ID=AKIDxxxxxxxx
TENCENTCLOUD_SECRET_KEY=xxxxxxxx
CLS_DEFAULT_REGION=ap-guangzhou
SERVICE_NAME=my-llm-app

# Python 额外需要。Langfuse Python SDK 启动时会校验密钥是否存在,此处可填写任意非空占位值。
LANGFUSE_PUBLIC_KEY=pk-placeholder
LANGFUSE_SECRET_KEY=sk-placeholder

说明:
OTLP/HTTP 标准直传不需要 CLS_ENDPOINT,Endpoint 由 CLS_DEFAULT_REGION 拼接而成。自定义 SpanExporter 使用 CLS SDK 写入日志,需要配置 CLS_ENDPOINT。不同语言 SDK 对 Endpoint 的格式要求不同,请按对应语言示例填写。
常见地域 Endpoint 如下:
地域
公网 Endpoint
内网 Endpoint
广州
ap-guangzhou.cls.tencentcs.com
ap-guangzhou.cls.tencentyun.com
上海
ap-shanghai.cls.tencentcs.com
ap-shanghai.cls.tencentyun.com
北京
ap-beijing.cls.tencentcs.com
ap-beijing.cls.tencentyun.com
新加坡
ap-singapore.cls.tencentcs.com
ap-singapore.cls.tencentyun.com
说明:
公网 Endpoint 适用于公网访问场景。内网 Endpoint 适用于同地域 VPC/CVM 环境,可降低网络延迟并避免公网流量。

步骤2:接入应用

手动接入时,建议先使用 OTLP/HTTP 标准直传 完成快速接入;如您需要使用 CLS SDK 的压缩、批处理或自定义字段能力,再选择 自定义 SpanExporter 直传。如果您通过 Skill 自动接入,可生成自定义 SpanExporter 方案。

方式一:OTLP/HTTP 标准直传

说明:
不同语言的 OpenTelemetry SDK 对 Endpoint 参数的要求不同。本文 TypeScript/Node.js 和 Python 示例使用完整 OTLP Trace URL,即 https://{CLS_DEFAULT_REGION}.cls.tencentcs.com/v1/traces
TypeScript/Node.js
Python
1. 安装依赖。
npm install @langfuse/tracing @langfuse/otel @langfuse/openai \\
@opentelemetry/sdk-node \\
@opentelemetry/exporter-trace-otlp-http \\
dotenv
2. 创建 instrumentation.ts
import "dotenv/config";

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { LangfuseSpanProcessor } from "@langfuse/otel";

const secretId = process.env.TENCENTCLOUD_SECRET_ID!;
const secretKey = process.env.TENCENTCLOUD_SECRET_KEY!;
const region = process.env.CLS_DEFAULT_REGION!;
const topicId = process.env.CLS_TOPIC_ID!;

const auth = Buffer.from(`${secretId}:${secretKey}`).toString("base64");

export const sdk = new NodeSDK({
spanProcessors: [
new LangfuseSpanProcessor({
exporter: new OTLPTraceExporter({
url: `https://${region}.cls.tencentcs.com/v1/traces`,
headers: {
Authorization: `Basic ${auth}`,
topic_id: topicId,
},
}),
flushAt: 512,
flushInterval: 5000,
}),
],
});

sdk.start();

process.on("SIGTERM", async () => {
await sdk.shutdown();
});
3. 在业务入口文件顶部引入 instrumentation.ts。请确保该引入早于 OpenAI、LangChain 等会被 instrumentation patch 的模块。
import { sdk } from "./instrumentation";

import OpenAI from "openai";
import { observeOpenAI } from "@langfuse/openai";
import { startActiveObservation, propagateAttributes } from "@langfuse/tracing";

const openai = observeOpenAI(new OpenAI());

async function chat(userMessage: string) {
return startActiveObservation("chat", async (span) => {
span.update({ input: [{ role: "user", content: userMessage }] });

return propagateAttributes(
{ userId: "u-123", sessionId: "s-456", tags: ["model:gpt-4o"] },
async () => {
const res = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
});
span.update({ output: res.choices[0].message });
return res;
},
);
});
}

await chat("你好");
await sdk.shutdown();
1. 安装依赖。
pip install langfuse opentelemetry-sdk opentelemetry-exporter-otlp python-dotenv
2. 创建 instrumentation.py。该文件必须早于任何 langfuse 模块导入。
import os
import base64
from dotenv import load_dotenv

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

load_dotenv()

secret_id = os.environ["TENCENTCLOUD_SECRET_ID"]
secret_key = os.environ["TENCENTCLOUD_SECRET_KEY"]
region = os.environ["CLS_DEFAULT_REGION"]
topic_id = os.environ["CLS_TOPIC_ID"]

auth = base64.b64encode(f"{secret_id}:{secret_key}".encode()).decode()

provider = TracerProvider(
resource=Resource.create({
"service.name": os.environ.get("SERVICE_NAME", "my-llm-app")
})
)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=f"https://{region}.cls.tencentcs.com/v1/traces",
headers={
"Authorization": f"Basic {auth}",
"topic_id": topic_id,
},
),
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
trace.set_tracer_provider(provider)
3. 在业务入口中引入 instrumentation.py
import instrumentation

from langfuse import observe, propagate_attributes, get_client
from langfuse.openai import openai


@observe(name="chat", as_type="generation")
def chat(user_message: str):
with propagate_attributes(user_id="u-123", session_id="s-456", tags=["model:gpt-4o"]):
return openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
)


if __name__ == "__main__":
chat("你好")
get_client().flush()
进阶配置
采样配置:您可以在 TracerProvider 上配置采样策略,控制上报到 CLS 的 Trace 数据量。
采样器
说明
适用场景
ALWAYS_ON
全量采样
测试环境或低流量环境。
ALWAYS_OFF
全部不采样
临时关闭 Trace。
TraceIdRatioBased(0.1)
按比例采样 10%
生产环境控制写入量。
ParentBased(root)
根据父 Span 决定是否采样
分布式链路,保证整条链路采样一致。
TypeScript/Node.js
Python
1. 安装额外依赖。
npm install @opentelemetry/sdk-trace-base
2. instrumentation.ts 中配置采样器。
import {
ParentBasedSampler,
TraceIdRatioBasedSampler,
} from "@opentelemetry/sdk-trace-base";

export const sdk = new NodeSDK({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1),
}),
// 其他配置保持不变
});
在 instrumentation.py 中配置采样器。
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

provider = TracerProvider(
sampler=ParentBased(TraceIdRatioBased(0.1)),
resource=Resource.create({
"service.name": os.environ.get("SERVICE_NAME", "my-llm-app")
})
)
跨地域上报:如果上报链路需要跨地域,可在 OTLP 请求 Header 中添加源地域标识。
TypeScript/Node.js
Python
new OTLPTraceExporter({
url: `https://${region}.cls.tencentcs.com/v1/traces`,
headers: {
Authorization: `Basic ${auth}`,
topic_id: topicId,
"x-cross-region": "ap-guangzhou", // 源地域标识
},
})
OTLPSpanExporter(
endpoint=f"https://{region}.cls.tencentcs.com/v1/traces",
headers={
"Authorization": f"Basic {auth}",
"topic_id": topic_id,
"x-cross-region": "ap-guangzhou", # 源地域标识
},
)
关闭 HTTPS(仅测试环境)
TypeScript/Node.js
Python
可传入 http:// 协议的 URL 来使用明文 HTTP。
url: `http://${region}.cls.tencentcs.com/v1/traces`,
可使用 insecure=True 参数。
OTLPSpanExporter(
endpoint=f"http://{region}.cls.tencentcs.com/v1/traces",
insecure=True, # 仅测试环境
# ...
)

方式二:自定义 SpanExporter 直传

如您需要使用 CLS SDK 的压缩、批处理能力,或需要完全控制写入 CLS 的字段,可使用自定义 SpanExporter。该方式会把 OTel Span 手动转换为 CLS LogItem 后写入 CLS。
注意:
自定义 SpanExporter 不是 OTLP 标准协议上报。请将 span.attributesspan.resource.attributes 分开映射,分别写入 attributeresource 字段,不要将 resource 合并到 attribute 中。
TypeScript/Node.js
Python
1. 安装依赖。
npm install @langfuse/tracing @langfuse/otel @langfuse/openai \\
@opentelemetry/sdk-node @opentelemetry/sdk-trace-base \\
@opentelemetry/api @opentelemetry/core \\
tencentcloud-cls-sdk-nodejs dotenv
2. 创建 cls-span-exporter.ts
import {
SpanExporter,
ReadableSpan,
} from "@opentelemetry/sdk-trace-base";
import { ExportResult, ExportResultCode, hrTimeToNanoseconds } from "@opentelemetry/core";
import {
Producer,
LogItem,
Content,
TencentCloudClsSDKException,
} from "tencentcloud-cls-sdk-nodejs";

export interface ClsExporterOptions {
endpoint: string;
topicId: string;
secretId: string;
secretKey: string;
token?: string;
}

export class TencentCLSSpanExporter implements SpanExporter {
private producer: Producer;

constructor(opts: ClsExporterOptions) {
this.producer = new Producer({
endpoint: opts.endpoint,
topic_id: opts.topicId,
credential: {
secretId: opts.secretId,
secretKey: opts.secretKey,
token: opts.token ?? "",
},
time: 2,
count: 1000,
maxMemLogCount: 10000,
onSendLogsError: (err) => console.warn("[cls] send error", err),
});
}

export(spans: ReadableSpan[], cb: (r: ExportResult) => void): void {
Promise.all(spans.map((span) => this.producer.send(this.spanToLogItem(span))))
.then(() => cb({ code: ExportResultCode.SUCCESS }))
.catch((err: Error) => {
if (err instanceof TencentCloudClsSDKException) {
console.warn("[cls] sdk exception:", err.toString());
}
cb({ code: ExportResultCode.FAILED, error: err });
});
}

private spanToLogItem(span: ReadableSpan): LogItem {
const item = new LogItem();
const endNs = hrTimeToNanoseconds(span.endTime);
const startNs = hrTimeToNanoseconds(span.startTime);
item.setTime(Math.floor(endNs / 1e6));

const ctx = span.spanContext();
item.pushBack(new Content("traceID", ctx.traceId));
item.pushBack(new Content("spanID", ctx.spanId));
item.pushBack(new Content("parentSpanID", span.parentSpanContext?.spanId ?? ""));
item.pushBack(new Content("name", span.name));
item.pushBack(new Content("kind", String(span.kind)));
item.pushBack(new Content("start", String(startNs)));
item.pushBack(new Content("end", String(endNs)));
item.pushBack(new Content("duration", String(endNs - startNs)));
item.pushBack(new Content("statusCode", String(span.status.code)));
item.pushBack(new Content("statusMessage", span.status.message ?? ""));
item.pushBack(new Content("attribute", JSON.stringify(span.attributes)));
item.pushBack(new Content("resource", JSON.stringify(span.resource.attributes)));
return item;
}

shutdown(): Promise<void> {
return Promise.resolve();
}

forceFlush(): Promise<void> {
return Promise.resolve();
}
}
3. 创建 instrumentation.ts
import "dotenv/config";

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";
import { TencentCLSSpanExporter } from "./cls-span-exporter";

export const sdk = new NodeSDK({
spanProcessors: [
new LangfuseSpanProcessor({
exporter: new TencentCLSSpanExporter({
endpoint: process.env.CLS_ENDPOINT!,
topicId: process.env.CLS_TOPIC_ID!,
secretId: process.env.TENCENTCLOUD_SECRET_ID!,
secretKey: process.env.TENCENTCLOUD_SECRET_KEY!,
token: process.env.TENCENTCLOUD_TOKEN,
}),
flushAt: 512, // 攒批阈值
flushInterval: 5, // 秒
}),
],
});

sdk.start();

process.on("SIGTERM", () => sdk.shutdown());
1. 安装依赖。
pip install langfuse opentelemetry-sdk tencentcloud-cls-sdk-python python-dotenv
2. 创建 cls_span_exporter.py
import json
from typing import Sequence

from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
from tencentcloud.log.cls_pb2 import LogGroupList
from tencentcloud.log.logclient import LogClient
from tencentcloud.log.logexception import LogException


class TencentCLSSpanExporter(SpanExporter):
def __init__(self, endpoint: str, topic_id: str, secret_id: str, secret_key: str):
self.client = LogClient(endpoint, secret_id, secret_key)
self.topic_id = topic_id

def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
group_list = LogGroupList()
group = group_list.logGroupList.add()
group.source = "langfuse-otel"

for span in spans:
ctx = span.get_span_context()
log = group.logs.add()
log.time = span.end_time // 1_000_000

def add(key: str, value: str):
content = log.contents.add()
content.key = key
content.value = value

add("traceID", format(ctx.trace_id, "032x"))
add("spanID", format(ctx.span_id, "016x"))
add("parentSpanID", format(span.parent.span_id, "016x") if span.parent else "")
add("name", span.name)
add("kind", span.kind.name)
add("start", str(span.start_time))
add("end", str(span.end_time))
add("duration", str(span.end_time - span.start_time))
add("statusCode", span.status.status_code.name)
add("statusMessage", span.status.description or "")
add("attribute", json.dumps(dict(span.attributes), ensure_ascii=False))
add("resource", json.dumps(dict(span.resource.attributes), ensure_ascii=False))

try:
self.client.put_log_raw(self.topic_id, group_list)
return SpanExportResult.SUCCESS
except LogException as err:
print("[cls] put_log_raw failed:", err)
return SpanExportResult.FAILURE
except Exception as err:
print("[cls] unexpected error:", err)
return SpanExportResult.FAILURE

def shutdown(self):
pass

def force_flush(self, timeout_millis: int = 30000) -> bool:
return True
3. 创建 instrumentation.py
import os
from dotenv import load_dotenv

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from cls_span_exporter import TencentCLSSpanExporter

load_dotenv()

provider = TracerProvider(
resource=Resource.create({
"service.name": os.environ.get("SERVICE_NAME", "my-llm-app")
})
)
provider.add_span_processor(
BatchSpanProcessor(
TencentCLSSpanExporter(
endpoint=os.environ["CLS_ENDPOINT"],
topic_id=os.environ["CLS_TOPIC_ID"],
secret_id=os.environ["TENCENTCLOUD_SECRET_ID"],
secret_key=os.environ["TENCENTCLOUD_SECRET_KEY"],
),
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
trace.set_tracer_provider(provider)
4. 在业务入口文件顶部引入 instrumentation.py,并在脚本退出前执行 flush()
import instrumentation

from langfuse import observe, get_client
from langfuse.openai import openai


@observe(name="chat", as_type="generation")
def chat(user_message: str):
return openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
)


if __name__ == "__main__":
chat("你好")
get_client().flush()

步骤3:验证上报结果

完成接入后,在应用中触发一次 LLM 调用,然后在 CLS 控制台中查询 Trace 数据。
1. 登录 日志服务控制台,在左侧导航栏中,选择检索分析
2. 选择用于存储 Trace 数据的日志主题。
3. 在检索框中输入以下语句,查询最新上报的 Trace 数据。

* | SELECT traceID, spanID, name, duration, statusCode ORDER BY __TIMESTAMP__ DESC LIMIT 10
也可以查询指定调用链:
traceID:"4bf92f3577b34da6a3ce929d0e0e4736"
如需按模型统计平均耗时,请确保模型字段已写入 attribute 并已配置索引或使用 JSON 提取函数:
* | SELECT json_extract_scalar(attribute,'$."gen_ai.request.model"') AS model,
AVG(duration) AS avg_duration_ns
GROUP BY model

字段说明

使用 OTLP/HTTP 标准直传时,resourcescopespan 等信息会按 OpenTelemetry 标准协议结构上报。使用自定义 SpanExporter 时,需要手动映射字段。
自定义 SpanExporter 建议按以下字段写入 CLS LogItem:
OTel Span 字段
CLS LogItem Key
说明
traceId
traceID
Trace ID,hex 字符串。
spanId
spanID
Span ID,hex 字符串。
parentSpanId
parentSpanID
父 Span ID,根 Span 为空。
span.name
name
Span 名称。
SpanKind
kind
Span 类型。
startTime
start
Span 开始时间,纳秒。
endTime
end
Span 结束时间,纳秒。
endTime - startTime
duration
Span 耗时,纳秒。
status.code
statusCode
Span 状态码。
status.message
statusMessage
Span 状态说明。
attributes
attribute
Span attributes,JSON 字符串。
resource.attributes
resource
Resource attributes,JSON 字符串。
events / links
logs / links
可选。如需保留 Span Events 或 Links,可序列化为 JSON 字符串写入。
注意:
自定义 SpanExporter 中的 attribute 只用于保存 span.attributes。请勿将 resource.attributes 写入 attribute.resourceAttribute,应将其作为独立 resource 字段写入。

注意事项

startendduration 字段需要保持纳秒单位。请勿将这些字段除以 1000 转成微秒,否则耗时会显示为实际值的千分之一。
自定义 SpanExporter 中,log.time 建议使用 Span 结束时间,例如 TypeScript 中使用 span.endTime 计算,Python 中使用 span.end_time // 1_000_000
短生命周期脚本退出前需要执行 flush()shutdown(),避免最后一批 Span 未完成上报。
访问凭证请通过环境变量或密钥管理工具注入,不要硬编码到代码中。