基于 OpenTelemetry 的 React 应用全链路追踪实战:从埋点到 SigNoz 可视化
在云原生架构中,前端应用的性能监控与故障排查往往面临跨服务追踪的难题。OpenTelemetry 作为 CNCF 孵化的标准化观测方案,为 React 应用提供了从浏览器到后端服务的完整链路追踪能力。本文将演示如何在 Kubernetes 环境中部署 SigNoz 作为观测后端,并完成 React 应用的埋点接入。
观测方案架构概览
OpenTelemetry 通过统一的 API 层解耦了数据采集与后端存储。React 应用借助 @opentelemetry/sdk-trace-web 生成符合 OTLP 格式的链路数据,经由 HTTP 协议上报至 SigNoz 的采集端点,最终在可视化界面呈现调用链路与性能指标。
SigNoz 区别于传统 ELK 方案的核心优势在于原生支持 OpenTelemetry 协议,无需额外的格式转换层,降低了架构复杂度与数据延迟。
Kubernetes 环境部署 SigNoz
以下基于 DigitalOcean 托管集群(版本 1.24.13)完成一键化部署:
helm repo add signoz https://charts.signoz.io
helm upgrade --install observability signoz/signoz \
--namespace monitoring --create-namespace \
--set otelCollector.config.receivers.otlp.protocols.http.cors.allowed_origins='https://demo.react-observability.io' \
--set otelCollector.config.receivers.otlp.protocols.http.include_metadata=true
关键配置说明:cors.allowed_origins 需匹配 React 应用的访问域名,避免浏览器跨域拦截。
验证服务状态:
kubectl get pods -n monitoring -l app.kubernetes.io/name=signoz
NAME READY STATUS
signoz-otel-collector-7d4f9b2c8a-3k5p9 1/1 Running
signoz-query-service-0 1/1 Running
signoz-frontend-9c7e5d2f1a-8m4n2 1/1 Running
配置公网采集入口
使用 Traefik 暴露 OTLP HTTP 接收端(默认端口 4318):
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: otlp-ingest
namespace: monitoring
spec:
entryPoints:
- websecure
routes:
- match: Host(`telemetry.react-observability.io`)
kind: Rule
services:
- name: signoz-otel-collector
port: 4318
React 应用埋点实现
创建追踪初始化模块 src/telemetry/tracer.ts:
import {
context,
trace,
Span,
SpanStatusCode,
Tracer
} from '@opentelemetry/api';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { Resource } from '@opentelemetry/resources';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
const SERVICE_IDENTIFIER = 'react-dashboard-ui';
const resourceAttributes = new Resource({
'service.name': SERVICE_IDENTIFIER,
'deployment.environment': 'production'
});
const traceProvider = new WebTracerProvider({
resource: resourceAttributes
});
const exporterConfig = {
url: 'https://telemetry.react-observability.io/v1/traces',
timeoutMillis: 30000
};
const spanExporter = new OTLPTraceExporter(exporterConfig);
traceProvider.addSpanProcessor(new BatchSpanProcessor(spanExporter));
traceProvider.register({ contextManager: new ZoneContextManager() });
const rootTracer = traceProvider.getTracer(SERVICE_IDENTIFIER);
// 用于服务端渲染场景下的上下文透传
let propagatedContext: Span | null = null;
(window as any).establishTraceContext = (
traceId: string,
spanId: string,
flags: number
) => {
propagatedContext = rootTracer.startSpan('server-boundary');
const ctx = propagatedContext.spanContext();
Object.assign(ctx, { traceId, spanId, traceFlags: flags });
};
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [new RegExp('.*?')],
clearTimingResources: true,
applyCustomAttributesOnSpan: (
activeSpan: Span,
request: RequestInit,
response: Response | Error
) => {
const attrs = (activeSpan as any).attributes || {};
if (attrs.component === 'fetch') {
activeSpan.updateName(`${attrs['http.method']} ${attrs['http.url']}`);
}
if (response instanceof Error) {
activeSpan.setStatus({
code: SpanStatusCode.ERROR,
message: response.message
});
activeSpan.recordException(response);
}
}
})
]
});
export function wrapWithTrace<T extends (...args: unknown[]) => unknown>(
operationName: string,
targetFn: T
): ReturnType<T> {
let currentSpan: Span;
if (propagatedContext) {
const parentCtx = trace.setSpan(context.active(), propagatedContext);
currentSpan = rootTracer.startSpan(operationName, undefined, parentCtx);
propagatedContext = null;
} else {
currentSpan = rootTracer.startSpan(operationName);
}
return context.with(
trace.setSpan(context.active(), currentSpan),
() => {
try {
const output = targetFn() as ReturnType<T>;
currentSpan.end();
return output;
} catch (thrownError) {
currentSpan.setStatus({ code: SpanStatusCode.ERROR });
currentSpan.end();
throw thrownError;
}
}
);
}
组件层集成示例
封装可复用的追踪按钮组件 src/components/TrackedAction/index.tsx:
import { Button, ButtonProps } from '@mui/material';
import { wrapWithTrace } from '../../telemetry/tracer';
interface TrackedButtonProps extends Omit<ButtonProps, 'onClick'> {
actionName: string;
handler: () => void;
}
export const TrackedButton = ({
actionName,
handler,
children,
...restProps
}: TrackedButtonProps) => {
const instrumentedHandler = () =>
wrapWithTrace(`UI interaction: ${actionName}`, handler);
return (
<Button {...restProps} onClick={instrumentedHandler}>
{children}
</Button>
);
};
验证端到端链路
访问演示站点触发 Fetch Links 操作后,SigNoz 面板将呈现以下观测数据:
- 服务依赖拓扑图,展示前端到后端 API 的调用关系
- P95/P99 延迟分位数,识别长尾请求
- 错误率趋势与异常堆栈关联
- TraceID 贯穿的完整请求生命周期
通过对比不同时间段的火焰图,可快速定位性能退化节点,例如某次部署后数据库查询层耗时突增的场景。
进阶优化方向
当前实现采用 BatchSpanProcessor 降低网络开销,生产环境建议进一步配置:
- 采样策略:基于尾延迟的概率采样或特定路由的强制采样
- 资源属性:注入 Git 提交版本、构建时间戳等部署元信息
- 错误边界:结合 React Error Boundary 实现组件级异常捕获与上报
- RUM 指标:扩展 WebVitals 采集,关联 LCP/FID 与业务链路
SigNoz 的查询引擎支持 PromQL 风格的指标聚合,可通过自定义仪表盘构建业务专属的可观测视图。