☰
highlight.io React Native(beta)监控实战:基于 OpenTelemetry 接入日志、错误与链路追踪
2026/9/25 7:49:06 网站建设 项目流程
  • 可观测性
  • 后端

【免费下载链接】highlight

highlight.io: The open source, full-stack monitoring platform. Error monitoring, session replay, logging, distributed tracing, and more.

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

本文基于 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 导出器的工作,几个细节值得注意:

  1. export()方法:用fetch将整批 Span 序列化为 JSON 后 POST 到目标 URL,成功回调ExportResultCode.SUCCESS、失败回调FAILED并附带错误对象。它完全依赖 RN 运行时的fetch,这正是选择自定义实现以绕开 Metro 兼容性问题的原因。
  2. _buildResourceSpans():按 OTLP/JSON 结构组装{ resourceSpans: [{ resource, scopeSpans }] }载荷;Resource 属性取自批次内第一个 Span,instrumentation scope 同样取自第一个 Span 的instrumentationLibrary。
  3. _convertToOTLPFormat():完成 Span 到 OTLP 字段的映射,其中两处协议细节容易被忽略——kind因为 API 未定义“未设置”的枚举值需要偏移 1(span.kind == null ? 0 : span.kind + 1);startTime/endTime在 SDK 中是[秒, 纳秒余数]二元组,需要转换为startTime[0] * 1e9 + startTime[1]的纳秒时间戳。
  4. _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() } }

从源码逻辑看,钩子的设计要点有四处:

  1. 防重复注入:consoleHooked标志保证只覆写一次,重复调用直接返回;
  2. 先打印后上报:try/finally结构确保原始origWrite(...data)一定执行(devtools 输出不受影响),上报失败也不会打断业务逻辑;
  3. 错误增强:level === 'error'时额外注入exception.type、exception.message、exception.stacktrace(通过Error.captureStackTrace抓取现场调用栈),使console.error在 highlight 中呈现为带堆栈的错误;
  4. 安全序列化: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 面板中看到上述三类遥测数据。

注意事项与演进方向

  1. beta 状态:文档标题与 QuickStart 元信息均标注(beta),方案细节(如highlight.logSpan 名约定、OTLP 端点)以当前仓库版本为准。
  2. 自定义 Exporter 是过渡方案:文档与配套工程博客 how-to-instrument-your-react-native-app-with-opentelemetry.md 都明确指出,基于 bundler 的官方 OTLP 导出器方案在推进中,届时可用标准的OTLPTraceExporter替换ReactNativeOTLPTraceExporter。
  3. 双通道输出:直接用log()只发 highlight 不进 devtools;hookConsole()之后console.*才能两者兼得,建议在应用生命周期早期(如根布局useEffect)调用一次。
  4. 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.

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

相关推荐

上一篇:Formtastic表单数据预处理终极指南:如何实现智能默认值与动态选项
下一篇:VR控制器按键自定义教程:用OpenVR Advanced Settings打造专属操控体验

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

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

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

立即咨询