七种 instrument、日志记录的十二个字段,以及 schema URL
一句话总结
指标 API 的 Instrument 分为 Counter、UpDownCounter、Gauge、Histogram 及其异步(callback)变体,SDK 导出的 Sum 带有 delta 或 cumulative 这种 temporality。日志信号由含十二个字段的 LogRecord 数据模型,以及把现有日志库连接到该模型的桥接 API 构成。schema URL 是一种版本标记,让语义约定即使发生变化,消费者也能解读数据。出处是指标 API、指标数据模型、日志数据模型、遥测 schema这几份规范。
为什么需要它
“请求数”“队列长度”“房间温度”都是数字,性质却不同。请求数只增不减,队列长度时增时减,温度相加则没有意义。Instrument 分成不同类型,就是为了在 API 阶段把这种性质说明白,让 SDK 选出合适的默认聚合。规范概览把这一点表述为“记录原始测量值,聚合方式由最终用户选择”。
日志则是相反方向的问题。所有语言里早已有日志库,不能要求丢掉这些日志、用新 API 重写。因此日志信号从“如何把现有日志转入公共模型并与跟踪关联”出发。schema 也是同类问题:语义约定中的属性名称一旦改变,期待该名称的仪表板和后端就会出问题。
工作原理
七种 Instrument
Instrument 由名称、类型、单位(可选)、说明(可选)定义。综合概念文档与 API 规范的说明,如下所示。
| Instrument | 同步/异步 | 性质 | 记录操作 |
|---|---|---|---|
| Counter | 同步 | 单调递增(只增不减) | Add |
| Asynchronous Counter | 异步 | 单调递增,上报观察时刻的累积值 | 回调 |
| UpDownCounter | 同步 | 可增可减(队列长度) | Add |
| Asynchronous UpDownCounter | 异步 | 可以相加汇总的值(进程堆大小) | 回调 |
| Gauge | 同步 | 不能相加的值(噪声水平),变化时记录 | Record |
| Asynchronous Gauge | 异步 | 不能相加的值(房间温度),在观察时刻上报 | 回调 |
| Histogram | 同步 | 分布有意义的值(请求延迟) | Record |
同步 Instrument 直接在应用逻辑中调用,其测量值可以与 Context(即当前 span)关联。异步 Instrument 先注册回调,再由 SDK 只在采集(collection)时刻调用,规范中的例子是每 15 秒读取一次传感器温度。异步测量值不与 Context 关联。规范补充说,这里的同步与异步和编程中的 async 模式无关。UpDownCounter 的说明中有一条指引:“如果值是单调递增的,就使用 Counter”。
每个 Instrument 都有默认聚合,可以用 View 修改、忽略特定 Instrument,或选择要上报的属性。概念文档还说明了基数(cardinality)限制:每个指标流的唯一属性组合默认上限为 2000,超出后不会丢弃测量值,而是折叠成一个 otel.metric.overflow=true 属性。合计是对的,但被折叠的测量值会丢失其他属性,所以按该属性过滤的查询结果会偏少。
temporality:delta 与 cumulative
在数据模型中,Sum 由数据点构成,数据点带有 AggregationTemporality(delta 或 cumulative)、单调(monotonic)标志,以及起止时间。delta 在互不重叠的时间窗口内,上报该区间的值;cumulative 上报从起点(通常是进程启动)起的总和。单调 delta 的和不得为负,单调 cumulative 的和不得小于之前的值。
规范列出两者之间的取舍:进程重启检测、速率(rate)计算、推送/拉取方式;并说明 OTLP 两者都支持,必要时可以做 Delta-to-Cumulative 或 Cumulative-to-Delta 转换。直方图也有 temporality,min、max 在 delta 中更有用,delta→cumulative 可以转换,反过来则不行。概念文档还指出它与基数的关系:delta 每个周期清空状态,只统计一个周期内的组合;cumulative 则保留状态,所以一旦触及上限,就会持续溢出直到进程重启。Gauge 是某个时刻的采样值,所以没有 temporality。
日志记录的字段
日志数据模型出发于这样一个要求:“必须能把现有的日志格式无歧义地映射到这个模型”,常用字段作为有名字的顶层字段,其余放入 Attributes。顶层字段共十二个。
| 字段 | 含义 |
|---|---|
| Timestamp | 事件发生的时间(源头时钟) |
| ObservedTimestamp | 采集系统观察到的时间。由 SDK 创建的日志中与 Timestamp 相同 |
| TraceId / SpanId / TraceFlags | W3C Trace Context 的标识符与标志。附加在处理请求期间的日志上 |
| SeverityText | 源头的日志级别字符串 |
| SeverityNumber | 标准化的严重程度数字 |
| Body | 正文 |
| Resource | 产生日志的实体 |
| InstrumentationScope | 产生日志的范围(库、模块) |
| Attributes | 附加信息 |
| EventName | 表示事件种类的名称 |
转到只支持一个时间的地方时,有 Timestamp 就用它,没有就用 ObservedTimestamp。SeverityNumber 的范围是 1–24:1–4 为 TRACE,5–8 为 DEBUG,9–12 为 INFO,13–16 为 WARN,17–20 为 ERROR,21–24 为 FATAL,0 表示未指定。如规范中的例子所示,17 比 20 是更轻的错误。Events 是这种 LogRecord 的标准化形式,日志语义约定是以 Event 形式定义的。
桥接 API
日志 API在第一句话中就说明了对象:“提供给日志库作者,用来创建日志 appender;appender 通过这个 API,把现有日志库与 OpenTelemetry 日志数据模型连接起来。”结构就是从 LoggerProvider 获取 Logger(name,可选的 version、schema_url、attributes),再由 Logger 来 Emit LogRecord。Emit 接收 Timestamp、ObservedTimestamp、Context、SeverityNumber、SeverityText、Body、Attributes、EventName,不传 Context 时使用当前 Context——这就是日志自动带上 TraceId、SpanId 的原理。Enabled 只是在创建 LogRecord 开销很大时先行询问的优化,并非必须调用。
日志信号概览把采集方式分为两类。一类是由 Collector 读取并解析输出到文件或标准输出的日志(对应用几乎没有改动,但解析困难);另一类是挂上 appender,通过 OTLP 直接发送(结构化好,文件、轮转、解析都不再需要,但需要能接收 OTLP 的目的地)。无论哪种,应用开发者只需要在启动时配置 appender 和 SDK。
schema URL
语义约定在演进,遥测的来源与消费者的变化速度各不相同。为了把三方解耦,schema 规范规定:schema 有版本(MAJOR.MINOR.PATCH),版本之间的转换(例如属性重命名)由 schema 文件明确写出,每个版本都由唯一的 Schema URL 标识。URL 的最后一段路径是版本,前面是 schema 系列。schema 文件一经发布就不可变,所以可以永久缓存。来源(埋点库)会把 schema URL 写入导出的遥测——获取 Tracer、Meter、Logger 时的 schema_url 参数就是它的位置——消费者则查看收到的 schema 版本,必要时转换为自己期望的版本。文档中的例子是:1.2.0 中的 deployment.environment,被期望 1.1.0 的后端转换为 environment 后存储;如果后端不了解 schema,则由 Collector 的 schema 转换处理器代劳。OpenTelemetry 自己的 schema 发布在 /schemas/<version> 路径下。
在现场相遇的样子
一个团队把“当前活跃连接数”记成了 Counter,所以图表只会上升。连接关闭时数值应该减少,所以正确的是 UpDownCounter;而实际上,从连接池读取值的回调更自然,于是改成了 Asynchronous UpDownCounter。
另一个团队说发往 Prometheus 的指标和发往厂商后端的指标数值不同,原因是一边配置成 cumulative,另一边配置成 delta。同样是 Counter,temporality 不同,一个点上的数字所代表的区间就不同。
下一项测验要确认什么
测验会问:把 API 与 SDK 分离的原因、Batching processor 的默认值、同步与异步 Instrument 的区别及与 Context 的关联、delta 与 cumulative 的含义、LogRecord 的 Timestamp 与 ObservedTimestamp、桥接 API 的对象,以及 schema URL 的作用。