- 可观测性
- 后端
【免费下载链接】highlight
highlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.
本文基于 highlight 开源仓库中的 React Native 快速上手文档及其配套示例应用,完整讲解如何在 React Native(Expo)应用中使用 OpenTelemetry 标准 API 接入 highlight.io 的 Errors、Logs 与 Traces 三大产品。读完本文,你可以独立完成:OpenTelemetry 依赖安装、针对 Metro 打包器兼容问题的自定义 OTLP Exporter 实现、Tracer/Resource 配置、日志与错误上报封装、console 全局钩子(monkey patch),并使用仓库内的官方示例应用验证上报链路。
文档定位:一个通过 OpenTelemetry 覆盖三种遥测的 QuickStart
官方文档入口是 React Native 快速上手页,该页面本身是一个 MDX 壳,仅渲染quickStartContent["client"]["js"]["react-native"]指向的快捷步骤。其真实内容定义在 react-native.tsx,元信息声明如下:
- 标题:
React Native (beta)——当前方案处于 beta 阶段; - 产品覆盖:
['Errors', 'Logs', 'Traces']; - 副标题核心主张:使用 OpenTelemetry 在 React Native 应用中配置 highlight.io 的 errors、logs 和 traces。
也就是说,highlight 目前还没有独立的 React Native SDK,官方推荐路线是:直接采用 OpenTelemetry 的 trace API 产生 Span,通过一个自定义 Exporter 以 OTLP/HTTP JSON 格式 POST 到 highlight 的 OTLP 端点https://otel.highlight.io:4318/v1/traces,由 highlight 后端把 Span 解析为日志、错误和追踪。日志和错误也复用 trace 通道发送,靠约定的 Span 名称与属性区分类型。仓库中 e2e/react-native 目录下的 Expo 示例应用是该方案的完整可运行实现,本文所有代码均以 e2e/react-native/app/highlight.ts 的真实实现为准。
第一步:安装 OpenTelemetry npm 包
在终端中安装以下四个@opentelemetry包(文档提供 npm/yarn/pnpm 三种方式):
# with npm npm install @opentelemetry/api @opentelemetry/core @opentelemetry/resources @opentelemetry/sdk-trace-base# with yarn yarn add @opentelemetry/api @opentelemetry/core @opentelemetry/resources @opentelemetry/sdk-trace-base# with pnpm pnpm add @opentelemetry/api @opentelemetry/core @opentelemetry/resources @opentelemetry/sdk-trace-base官方示例应用 e2e/react-native/package.json 中的版本基线可供参考:
@opentelemetry/api:^1.9.0@opentelemetry/core:^1.30.0@opentelemetry/resources:^1.30.0@opentelemetry/sdk-trace-base:^1.30.0- 应用侧:
react-native 0.76.6、expo ~52.0.23、react 18.3.1
注意:方案刻意没有安装@opentelemetry/exporter-trace-otlp-http这类标准导出器,原因是 React Native 的 Metro 打包器与部分 OpenTelemetry 包存在浏览器兼容性问题,官方选择用自定义 Exporter 绕过(详见下文)。
第二步:自定义 OTLP Exporter,解决 Metro 打包器兼容问题
这是整个接入方案中最关键的一步。文档原文说明:部分 OpenTelemetry 包无法与 React Native 的 Metro bundler 一起使用,存在浏览器兼容性问题。作为变通方案,团队编写了一个自定义 Exporter 负责序列化数据。基于 bundler 的解决方案(即使用官方 OTLPTraceExporter)也在推进中。
自定义 Exporter 需要实现SpanExporter接口(来自@opentelemetry/sdk-trace-base),完整实现见 react-native.tsx 中的 exporter 代码块,示例应用中的实际版本为 e2e/react-native/app/highlight.ts#L17-L164:
import { BatchSpanProcessor, BasicTracerProvider, SpanExporter, ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; import type { Link, Attributes } from '@opentelemetry/api'; import { ExportResultCode } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; type KeyValue = { key: string; value: KeyValue }; class ReactNativeOTLPTraceExporter implements SpanExporter { url: string; constructor(options: { url: string; }) { this.url = options.url; this._buildResourceSpans = this._buildResourceSpans.bind(this); this._convertEvent = this._convertEvent.bind(this); this._convertToOTLPFormat = this._convertToOTLPFormat.bind(this); this._convertLink = this._convertLink.bind(this); this._convertAttributes = this._convertAttributes.bind(this); this._convertKeyValue = this._convertKeyValue.bind(this); this._toAnyValue = this._toAnyValue.bind(this); } export(spans: ReadableSpan[], resultCallback: any) { fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: this._buildResourceSpans(spans), }) .then((resp) => { resultCallback({ code: ExportResultCode.SUCCESS }); }) .catch((err) => { resultCallback({ code: ExportResultCode.FAILED, error: err }); }); } shutdown() { return Promise.resolve(); } _buildResourceSpans(spans: ReadableSpan[] = []) { const resource = spans[0]?.resource; const scope = spans[0]?.instrumentationLibrary; return JSON.stringify({ "resourceSpans": [ { "resource": { "attributes": resource.attributes ? this._convertAttributes(resource.attributes) : [], }, "scopeSpans": [ { "scope": { "name": scope?.name, "version": scope?.version }, "spans": spans.map(this._convertToOTLPFormat), }, ], }, ], }); } _convertToOTLPFormat(span: ReadableSpan) { const spanContext = span.spanContext(); const status = span.status; return { traceId: spanContext.traceId, spanId: spanContext.spanId, parentSpanId: span.parentSpanId, traceState: spanContext.traceState?.serialize(), name: span.name, // Span kind is offset by 1 because the API does not define a value for unset kind: span.kind == null ? 0 : span.kind + 1, startTimeUnixNano: span.startTime[0] * 1e9 + span.startTime[1], endTimeUnixNano: span.endTime[0] * 1e9 + span.endTime[1], attributes: span.attributes ? this._convertAttributes(span.attributes) : [], droppedAttributesCount: span.droppedAttributesCount || 0, events: span.events?.map(this._convertEvent) || [], droppedEventsCount: span.droppedEventsCount || 0, status: { code: status.code, message: status.message, }, links: span.links?.map(this._convertLink) || [], droppedLinksCount: span.droppedLinksCount, }; } _convertEvent(timedEvent: TimedEvent) { return { attributes: timedEvent.attributes ? this._convertAttributes(timedEvent.attributes) : [], name: timedEvent.name, timeUnixNano: timedEvent.time[0] * 1e9 + timedEvent.time[1], droppedAttributesCount: timedEvent.droppedAttributesCount || 0, }; } _convertLink(link: Link) { return { attributes: link.attributes ? this._convertAttributes(link.attributes) : [], spanId: link.context.spanId, traceId: link.context.traceId, traceState: link.context.traceState?.serialize(), droppedAttributesCount: link.droppedAttributesCount || 0, }; } _convertAttributes(attributes: Attributes) { return Object.keys(attributes).map(key => this._convertKeyValue(key, attributes[key])); } _convertKeyValue(key: string, value: any): KeyValue { return { key: key, value: this._toAnyValue(value), }; } _toAnyValue(value: any): any { const t = typeof value; if (t === 'string') return { stringValue: value as string }; if (t === 'number') { if (!Number.isInteger(value)) return { doubleValue: value as number }; return { intValue: value as number }; } if (t === 'boolean') return { boolValue: value as boolean }; if (value instanceof Uint8Array) return { bytesValue: value }; if (Array.isArray(value)) return { arrayValue: { values: value.map(this._toAnyValue) } }; if (t === 'object' && value != null) return { kvlistValue: { values: Object.entries(value as object).map(([k, v]) => this._convertKeyValue(k, v) ), }, }; return {}; } }从源码结构看,该实现手动完成了标准 OTLP 导出器的工作,几个细节值得注意:
export()方法:用fetch将整批 Span 序列化为 JSON 后 POST 到目标 URL,成功回调ExportResultCode.SUCCESS、失败回调FAILED并附带错误对象。它完全依赖 RN 运行时的fetch,这正是选择自定义实现以绕开 Metro 兼容性问题的原因。_buildResourceSpans():按 OTLP/JSON 结构组装{ resourceSpans: [{ resource, scopeSpans }] }载荷;Resource 属性取自批次内第一个 Span,instrumentation scope 同样取自第一个 Span 的instrumentationLibrary。_convertToOTLPFormat():完成 Span 到 OTLP 字段的映射,其中两处协议细节容易被忽略——kind因为 API 未定义“未设置”的枚举值需要偏移 1(span.kind == null ? 0 : span.kind + 1);startTime/endTime在 SDK 中是[秒, 纳秒余数]二元组,需要转换为startTime[0] * 1e9 + startTime[1]的纳秒时间戳。_toAnyValue():把任意 JS 值映射为 OTLP 的 AnyValue(stringValue/intValue/doubleValue/boolValue/bytesValue/arrayValue/kvlistValue),这是属性能无损到达后端的保证。
第三步:创建 Tracer 并配置 Resource
创建 Exporter 实例后,组装Resource、BasicTracerProvider与BatchSpanProcessor并注册全局 Tracer。文档给出的代码块为:
// create tracer with resource const resource = new Resource({ "highlight.project_id": "<YOUR_PROJECT_ID>", // add more resource attributes here for every trace/log/error "service.name": "reactnativeapp" // see more in @opentelemetry/semantic-conventions }); const tracerProvider = new BasicTracerProvider({resource}) const otlpExporter = new ReactNativeOTLPTraceExporter({ url: 'https://otel.highlight.io:4318/v1/traces' }); tracerProvider.addSpanProcessor(new BatchSpanProcessor(otlpExporter)); tracerProvider.register(); export const tracer = tracerProvider.getTracer('react-native-tracer');示例应用中的真实写法见 highlight.ts#L166-L177,与上面一致,仅project_id换成了演示值:
const resource = new Resource({ 'highlight.project_id': '1261', 'service.name': 'reactnativeapp', }) const tracerProvider = new BasicTracerProvider({ resource }) const otlpExporter = new ReactNativeOTLPTraceExporter({ url: 'https://otel.highlight.io:4318/v1/traces', }) tracerProvider.addSpanProcessor(new BatchSpanProcessor(otlpExporter)) tracerProvider.register() export const tracer = tracerProvider.getTracer('react-native-tracer')各要素的作用:
| 要素 | 作用 | 说明 |
|---|---|---|
Resource | 挂载到每一个trace/log/error 上的公共属性 | 必须包含highlight.project_id(替换为你在 highlight 中的项目 ID,演示值1261不可直接使用);service.name用于在服务维度识别应用;还可按 semantic conventions 追加environment等属性 |
BasicTracerProvider({resource}) | OpenTelemetry 的 Provider 入口,持有全部遥测配置 | 负责创建与管理 Span |
ReactNativeOTLPTraceExporter | 自定义导出器,指向https://otel.highlight.io:4318/v1/traces | 即 OTLP/HTTP 的 traces 端点(4318 端口) |
BatchSpanProcessor | 批处理处理器 | 将 Span 攒批后统一交给 Exporter,比逐条发送更高效 |
tracerProvider.register() | 将 Provider 注册为全局默认 | 之后通过getTracer()取出的 tracer 即可直接用于业务代码 |
第四步:封装日志上报函数(Log as a Trace)
highlight 的日志上报复用 trace 通道:文档说明通过创建 log trace 发送日志,参数可以根据使用场景简化或修改。有两个关键约定——Span 名称必须为highlight.log(后端据此判定这是一个日志),并在事件上携带log.severity与log.message属性:
const ConsoleLevels = { debug: 'debug', info: 'info', log: 'info', count: 'info', dir: 'info', warn: 'warn', assert: 'warn', error: 'error', trace: 'trace', } as const // send logs via trace export const log = (level: keyof typeof ConsoleLevels, message: string, attributes = {}) => { const span = tracer.startSpan('highlight.log') span.addEvent('log', { ...attributes, ['log.severity']: level, ['log.message']: message, }, new Date()) span.end() };ConsoleLevels把 console 的九种方法映射到 highlight 认可的日志级别(log/count/dir归入info,assert归入warn)。level与message之外传入的attributes会展开合并进事件属性,从而成为 highlight 中可检索的日志字段。示例应用中的对应实现见 highlight.ts#L192-L208。
一个注意点:单独使用这个log()函数时,消息只会上报到 highlight,不会出现在开发工具的控制台里——保留双通道输出的需求由下一步的 console 钩子解决。
第五步:封装错误上报函数
错误同样经由 trace 发送:Span 名沿用highlight.log约定,然后用recordException记录异常、setAttributes附加自定义属性:
// send errors via trace export const error = (message: string, attributes = {}) => { const span = tracer.startSpan('highlight.log') span.recordException( new Error(message), new Date(), ) span.setAttributes(attributes) span.end() };对应示例应用实现为 highlight.ts#L211-L216。其好处是错误名可自定义(不必依赖异常对象),且属性可携带任意上下文;recordException会保留标准exception.*语义字段,后端据此将 Span 处理为一条错误记录。
第六步:Monkey Patch console,实现自动上报
手动调用log()/error()灵活但侵入性强。如果只想“控制台里发生什么就上报什么”,文档提供了hookConsole():它覆写 console 各方法,使其在打印到 devtools 的同时默认发送到 highlight.io。完整代码:
// monkey patch console type ConsoleFn = (...data: any) => void let consoleHooked = false export function hookConsole() { if (consoleHooked) return consoleHooked = true for (const [level, highlightLevel] of Object.entries(ConsoleLevels)) { const origWrite = console[level as keyof Console] as ConsoleFn ;(console[level as keyof Console] as ConsoleFn) = function ( ...data: any[] ) { const date = new Date() try { return origWrite(...data) } finally { const o: { stack: any } = { stack: {} } Error.captureStackTrace(o) const message = data.map((o) => typeof o === 'object' ? safeStringify(o) : o, ) const attributes = data.filter((d) => typeof d === 'object').reduce((a, b) => ({ ...a, ...b }), {}) if (level === 'error') { attributes['exception.type'] = "Error" attributes['exception.message'] = message.join('') attributes['exception.stacktrace'] = JSON.stringify(o.stack) } log( highlightLevel, message.join(' '), attributes ) } } } } // https://stackoverflow.com/a/2805230 const MAX_RECURSION = 128 export function safeStringify(obj: any): string { function replacer(input: any, depth?: number): any { if ((depth ?? 0) > MAX_RECURSION) { throw new Error('max recursion exceeded') } if (input && typeof input === 'object') { for (let k in input) { if (typeof input[k] === 'object') { replacer(input[k], (depth ?? 0) + 1) } else if (!canStringify(input[k])) { input[k] = input[k].toString() } } } return input } function canStringify(value: any): boolean { try { JSON.stringify(value) return true } catch (e) { return false } } try { return JSON.stringify(replacer(obj)) } catch (e) { return obj.toString() } }从源码逻辑看,钩子的设计要点有四处:
- 防重复注入:
consoleHooked标志保证只覆写一次,重复调用直接返回; - 先打印后上报:
try/finally结构确保原始origWrite(...data)一定执行(devtools 输出不受影响),上报失败也不会打断业务逻辑; - 错误增强:
level === 'error'时额外注入exception.type、exception.message、exception.stacktrace(通过Error.captureStackTrace抓取现场调用栈),使console.error在 highlight 中呈现为带堆栈的错误; - 安全序列化:
safeStringify用MAX_RECURSION = 128限制递归深度,对无法直接JSON.stringify的叶子值降级为toString(),最终失败时整体回退为obj.toString(),避免深层/循环对象拖垮上报链路。
在应用中使用 tracer、log、error 与 hookConsole
所有函数集中在highlight.ts中导出,业务代码按需引入即可。文档给出的调用示例:
import * as H from "./highlight.ts" // path to highlight functions自定义 Span(追踪 + 异常记录):
const span = H.tracer.startSpan('Custom span name') ... span.recordException( new Error('this is a otel tracer error'), ) span.end()上报一条 warn 日志(附带可检索属性):
H.log('warn', 'Default sending information loaded', { sender: "spencer" })上报一条自定义错误:
H.error('Divide by 0 error', { numerator: 623 })启用 console 自动上报:
H.hookConsole() console.log("Hello World")官方示例应用展示了这些函数在真实工程中的用法。根布局 e2e/react-native/app/_layout.tsx#L25-L31 在字体资源加载完成后、应用真正开始渲染前调用钩子,保证首屏日志也能被捕获:
useEffect(() => { if (loaded) { H.hookConsole() SplashScreen.hideAsync() } else { } }, [loaded])首页 e2e/react-native/app/(tabs)/index.tsx#L10-L19/index.tsx#L10-L19) 则一次性演示了全部三类上报:
useEffect(() => { const span = H.tracer.startSpan('HomeScreen') console.log('A hooked console message') span.recordException(new Error('this is a otel tracer error')) span.end() }, []) H.log('warn', 'Default home screen loaded', { sender: 'spencer' }) H.error('type error', { code: 623 })其中console.log会在hookConsole()生效后同时出现在 devtools 与 highlight 中;HomeScreenSpan 上记录了一条异常,用于验证 trace 通道。
运行官方示例应用验证链路
仓库内置的示例应用 e2e/react-native 是一个基于 Expo(expo-router 文件路由)的标准项目,README 给出了启动方式:
# 1. 安装依赖 npm install # 2. 启动应用(使用隧道,便于真机/模拟器访问) npx expo start --tunnel启动后输出中会提供在 development build、Android 模拟器、iOS 模拟器或 Expo Go 中打开应用的选项。将 app/highlight.ts 中的highlight.project_id换成你自己的项目 ID 后,打开应用即可在 highlight 的 Logs、Errors、Traces 面板中看到上述三类遥测数据。
注意事项与演进方向
- beta 状态:文档标题与 QuickStart 元信息均标注
(beta),方案细节(如highlight.logSpan 名约定、OTLP 端点)以当前仓库版本为准。 - 自定义 Exporter 是过渡方案:文档与配套工程博客 how-to-instrument-your-react-native-app-with-opentelemetry.md 都明确指出,基于 bundler 的官方 OTLP 导出器方案在推进中,届时可用标准的
OTLPTraceExporter替换ReactNativeOTLPTraceExporter。 - 双通道输出:直接用
log()只发 highlight 不进 devtools;hookConsole()之后console.*才能两者兼得,建议在应用生命周期早期(如根布局useEffect)调用一次。 - Resource 是全局生效的:
highlight.project_id、service.name等 Resource 属性会附加到每条 trace/log/error 上,接入多环境时可用environment等语义属性区分,方便按维度过滤与告警。
整体调用链可以概括为:业务代码 → tracer(startSpan / log / error / hookConsole)→ BasicTracerProvider + BatchSpanProcessor 攒批 → ReactNativeOTLPTraceExporter 序列化并 fetch POST →https://otel.highlight.io:4318/v1/traces→ highlight 后端按 Span 名称与属性解析为 Logs、Errors、Traces。所有实现细节都能在 e2e/react-native/app/highlight.ts 与 highlight.io/components/QuickstartContent/frontend/react-native.tsx 中逐行核对。
- 可观测性
- 后端
【免费下载链接】highlight
highlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.
相关推荐
Windmill 可观测性实战:基于 Tempo、Grafana、Prometheus 与 Loki 的 OpenTelemetry 链路追踪与日志监控
Windmill 可观测性实战:基于 Tempo、Grafana、Prometheus 与 Loki 的 OpenTelemetry 链路追踪与日志监控 导读
后端工作流自动化任务调度低代码前端Bindu 可观测性实战指南:基于 OpenTelemetry 与 Sentry 的 AI Agent 全链路追踪与错误监控
Bindu 可观测性实战指南:基于 OpenTelemetry 与 Sentry 的 AI Agent 全链路追踪与错误监控 导读 本文是一份面向 Bindu
微服务链路追踪实战:基于nerdctl部署Jaeger与OpenTelemetry全链路监控
微服务链路追踪实战:基于nerdctl部署Jaeger与OpenTelemetry全链路监控 在微服务架构中,全链路监控是排查分布式系统问题的关键。但传统部署方
CLI云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考