Fluent Bit 内置的 CMetrics:用 C 语言构建轻量级指标处理管线的完整指南
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
CMetrics 是 Fluent Bit 仓库中内置的一个独立 C 指标库(位于 lib/cmetrics),负责指标的创建、变更、聚合、编码与解码,是 Fluent Bit 采集与输出 CPU、内存、网络等指标的核心数据层。读完本文,你将掌握 CMetrics 支持的全部指标类型与编解码格式、start_timestamp在 OTLP 累积流中的用法,并能基于公开 C API 编写一个完整的指标采集与 OTLP 编码程序。
说明:当前仓库中的 CMetrics 仍处于积极开发阶段(README 明确标注 "THIS LIBRARY IS STILL IN ACTIVE DEVELOPMENT"),本文描述的 API 与行为均以当前仓库源码为准。
CMetrics 是什么:指标上下文的一站式 C 库
CMetrics 是一个独立的 C 库,用于"创建、变更、聚合、编码和解码指标上下文(metrics contexts)"。它不依赖任何运行时或脚本语言,通过一套以cmt_前缀命名的 C API 对外提供服务。
从源码结构看,整个库遵循"公共 API 在头文件、实现分散在源文件"的经典布局:
- 公共 API 声明集中在 lib/cmetrics/include/cmetrics 下的头文件中(
cmt_counter.h、cmt_gauge.h、cmt_histogram.h等); - 具体实现位于 lib/cmetrics/src(
cmt_counter.c、cmt_gauge.c、cmt_encode_*.c、cmt_decode_*.c等); - 测试用例集中在 lib/cmetrics/tests(
counter.c、gauge.c、histogram.c、encoding.c、decoding.c、opentelemetry.c等)。
这种设计让 CMetrics 可以被 Fluent Bit 主程序、各 input/output 插件以及外部 C 项目直接静态链接使用。
核心数据模型:从上下文到数据点
理解 CMetrics 的用法,先要理清它的三层核心结构(详见 lib/cmetrics/docs/architecture.md):
struct cmt(上下文):顶层指标上下文,持有日志配置、元数据、静态标签,以及 counters/gauges/histograms/exp_histograms/summaries/untypeds 六类指标族链表(定义见 lib/cmetrics/include/cmetrics/cmetrics.h)。struct cmt_map(映射):每个指标族(metric family)拥有一个映射,负责管理带标签的数据点集合;无标签时通过静态 metric 直接访问(定义见 lib/cmetrics/include/cmetrics/cmt_map.h)。struct cmt_metric(数据点):单个带时间戳与标签的采样点,内部保存数值、直方图桶、指数直方图桶、摘要分位数、时间戳与start_timestamp等字段(定义见 lib/cmetrics/include/cmetrics/cmt_metric.h)。
每个指标族还对应一组选项struct cmt_opts(见 lib/cmetrics/include/cmetrics/cmt_opts.h),包含namespace、subsystem、name、description,并会自动拼接出全限定名fqname,格式为namespace_subsystem_name。
另外,cmetrics.h 还定义了指标类型的枚举常量与聚合类型:
#define CMT_COUNTER 0 #define CMT_GAUGE 1 #define CMT_HISTOGRAM 2 #define CMT_SUMMARY 3 #define CMT_UNTYPED 4 #define CMT_EXP_HISTOGRAM 5 #define CMT_AGGREGATION_TYPE_UNSPECIFIED 0 #define CMT_AGGREGATION_TYPE_DELTA 1 #define CMT_AGGREGATION_TYPE_CUMULATIVE 2其中聚合类型(DELTA/CUMULATIVE)是 OTLP 语义的重要组成部分,cumulative 流正是start_timestamp发挥作用的地方。
支持的指标类型(六种)
CMetrics 支持以下六种指标类型,覆盖了 Prometheus 数据模型与 OTLP 指标规范的主要类型:
| 指标类型 | 语义 | 对应头文件 |
|---|---|---|
| Counter | 只增不减的累计计数器(可配置允许重置) | cmt_counter.h |
| Gauge | 可增可减的瞬时值 | cmt_gauge.h |
| Untyped | 无类型语义的原始数值 | cmt_untyped.h |
| Histogram | 显式边界桶直方图 | cmt_histogram.h |
| Exponential Histogram | 指数桶直方图(OTLP 原生形态) | cmt_exp_histogram.h |
| Summary | 带分位数、sum、count 的摘要 | cmt_summary.h |
所有指标数据点(datapoint)都会保存一个纳秒精度的采样timestamp。
Counter:最常用的累计指标
Counter 的核心操作定义在 cmt_counter.h:
struct cmt_counter *cmt_counter_create(struct cmt *cmt, char *ns, char *subsystem, char *name, char *help, int label_count, char **label_keys); int cmt_counter_inc(struct cmt_counter *counter, uint64_t timestamp, int labels_count, char **label_vals); int cmt_counter_add(struct cmt_counter *counter, uint64_t timestamp, double val, int labels_count, char **label_vals); int cmt_counter_set(struct cmt_counter *counter, uint64_t timestamp, double val, int labels_count, char **label_vals); int cmt_counter_get_val(struct cmt_counter *counter, int labels_count, char **label_vals, double *out_val);cmt_counter_inc:计数器加 1;cmt_counter_add:计数器增加指定值;cmt_counter_set:直接设置计数器当前值(例如在解码、重启恢复或"允许重置"场景中使用);cmt_counter_allow_reset:允许计数器在检测到重置时回退。
Histogram:显式边界桶
直方图通过 cmt_histogram.h 提供,支持三种桶构建方式:
struct cmt_histogram_buckets *cmt_histogram_buckets_create(size_t count, ...); /* 可变参数直接指定边界 */ struct cmt_histogram_buckets *cmt_histogram_buckets_linear_create(double start, double width, size_t count); /* 线性边界 */ struct cmt_histogram_buckets *cmt_histogram_buckets_exponential_create(double start, double factor, size_t count); /* 指数边界 */ struct cmt_histogram_buckets *cmt_histogram_buckets_default_create(); /* 默认桶 */创建后通过cmt_histogram_observe(...)观察一个新观测值,库会按桶边界自动累加计数。
Exponential Histogram:OTLP 原生指数直方图
指数直方图(见 cmt_exp_histogram.h)使用 scale、zero_count、zero_threshold、正负两侧的 offset 与桶计数描述分布,是 OTLP 规范的原生形态,相比显式桶可以显著减少高基数桶的数量。CMetrics 还提供了cmt_exp_histogram_to_explicit(...)用于将指数直方图转换为显式边界桶表示,便于输出到不支持指数直方图的格式。
Summary:分位数摘要
Summary(见 cmt_summary.h)在设计上"只感知最终 quantile 值,不做百分位计算",创建时需要显式声明quantiles_count与quantiles数组(如 0、0.25、0.5、0.75、1),并通过cmt_summary_set_default(...)一次性写入分位数、sum 与 count。
数据点时间戳与 OTLP start_timestamp
CMetrics 的每个数据点都带有一个纳秒时间戳。除此之外,它还支持可选的原生start_timestamp(每个数据点一个),主要服务于 OTLP 累积(cumulative)指标流——在 OTLP 语义中,累积计数器的start_time_unix_nano表示该序列开始累积的时刻,对下游(如 Prometheus rate 计算)的正确性至关重要。
相关 API 全部声明在 cmt_metric.h:
| API | 作用 |
|---|---|
cmt_metric_set_start_timestamp(metric, start_ns) | 为数据点设置 start 时间戳 |
cmt_metric_unset_start_timestamp(metric) | 移除已设置的 start 时间戳 |
cmt_metric_has_start_timestamp(metric) | 判断数据点是否带 start 时间戳 |
cmt_metric_get_start_timestamp(metric) | 读取数据点的 start 时间戳 |
兼容性方面,README 明确保证:只使用timestamp的既有代码行为完全不变,start_timestamp是纯增量特性。
在编码/解码流程中,start_timestamp的传递规则为:
- OTLP 解码器:从
start_time_unix_nano字段填充原生start_timestamp; - OTLP 编码器:优先使用原生
start_timestamp,缺失时才回退到 OTLP 元数据; - CMetrics 内部 msgpack:通过可选的
start_ts字段在内部编码/解码流程中保留该值。
非 OTLP 格式(如 Prometheus text、Influx、Splunk HEC、CloudWatch EMF)没有 OTLP 风格的 start 时间戳字段,因此只序列化采样时间戳。这与各格式协议本身的能力边界一致,源码实现可参见 src/cmt_decode_opentelemetry.c 与 src/cmt_encode_opentelemetry.c。
编码器与解码器全景
CMetrics 的协议边界集中在cmt_encode_*.c与cmt_decode_*.c系列文件(lib/cmetrics/src),头文件一一对应。
支持的编码器(Encoder):
| 格式 | 头文件 | 说明 |
|---|---|---|
| OpenTelemetry Metrics(OTLP protobuf) | cmt_encode_opentelemetry.h | 生成 ExportMetricsServiceRequest protobuf |
| Prometheus text exposition | cmt_encode_prometheus.h | 生成# HELP/# TYPE+ 样本行的文本格式 |
| Prometheus Remote Write | cmt_encode_prometheus_remote_write.h | 生成 remote write protobuf |
| Influx line protocol | cmt_encode_influx.h | 生成 influx 行协议 |
| Splunk HEC | cmt_encode_splunk_hec.h | 生成 Splunk HEC JSON |
| CloudWatch EMF | cmt_encode_cloudwatch_emf.h | 生成 CloudWatch Embedded Metric Format |
| CMetrics msgpack(内部格式) | cmt_encode_msgpack.h | 用于内部流转/格式转换 |
| Text(人类可读) | cmt_encode_text.h | 便于调试输出 |
支持的解码器(Decoder):
| 格式 | 头文件 |
|---|---|
| OpenTelemetry Metrics(OTLP protobuf) | cmt_decode_opentelemetry.h |
| Prometheus text exposition | cmt_decode_prometheus.h |
| Prometheus Remote Write | cmt_decode_prometheus_remote_write.h |
| StatsD | cmt_decode_statsd.h |
| CMetrics msgpack(内部格式) | cmt_decode_msgpack.h |
值得注意的是,Prometheus text 解码器是基于 Flex/Bison 生成的词法/语法解析器(cmt_decode_prometheus.l、cmt_decode_prometheus.y),这解释了为何 lib/cmetrics/tests 中专门有prometheus_lexer.c与prometheus_parser.c两组测试。OTLP 与 Remote Write 则基于仓库内生成的 protobuf-C 定义(lib/cmetrics/src/external)。
在 Fluent Bit 生态中,这个编码器/解码器矩阵意味着:无论上游是 Prometheus 抓取、StatsD 还是 OTLP 上报,进入 CMetrics 后都可以被统一转换为任意下游格式输出——这正是 Fluent Bit 多格式互转能力的底层支撑。
完整 C 使用示例:创建 Counter 并编码 OTLP
下面是从 README 继承的完整示例,演示了 CMetrics 的典型使用闭环:创建上下文 → 创建 Counter → 写入采样点 → 附加start_timestamp→ 编码为 OTLP payload。该示例在结构上直接对应 Fluent Bit 内部"采集指标 → 聚合 → OTLP 输出"的调用链。
#include <stdint.h> #include <stdio.h> #include <cmetrics/cmetrics.h> #include <cmetrics/cmt_counter.h> #include <cmetrics/cmt_map.h> #include <cmetrics/cmt_metric.h> #include <cmetrics/cmt_encode_opentelemetry.h> int main(void) { struct cmt *ctx; struct cmt_counter *requests_total; struct cmt_metric *sample; cfl_sds_t otlp_payload; uint64_t start_ns; uint64_t sample_ns; ctx = cmt_create(); if (ctx == NULL) { return 1; } requests_total = cmt_counter_create(ctx, "demo", /* namespace */ "service", /* subsystem */ "requests_total", "Total requests", 0, /* label keys */ NULL); if (requests_total == NULL) { cmt_destroy(ctx); return 1; } start_ns = 1700000000000000000ULL; sample_ns = start_ns + 5000000000ULL; /* Write sample value (cumulative stream example). */ if (cmt_counter_set(requests_total, sample_ns, 42.0, 0, NULL) != 0) { cmt_destroy(ctx); return 1; } /* Access the same datapoint and attach native start timestamp. */ sample = cmt_map_metric_get(&requests_total->opts, requests_total->map, 0, NULL, CMT_FALSE); if (sample == NULL) { cmt_destroy(ctx); return 1; } cmt_metric_set_start_timestamp(sample, start_ns); /* Encode OTLP metrics payload. */ otlp_payload = cmt_encode_opentelemetry_create(ctx); if (otlp_payload == NULL) { cmt_destroy(ctx); return 1; } printf("Encoded OTLP payload size: %zu bytes\n", cfl_sds_len(otlp_payload)); cmt_encode_opentelemetry_destroy(otlp_payload); cmt_destroy(ctx); return 0; }示例要点逐行拆解
- 创建上下文:
cmt_create()返回顶层struct cmt *,所有指标族都挂载到该上下文上;结束使用后必须cmt_destroy(ctx)释放。 - 创建指标族:
cmt_counter_create的第 1~4 个参数依次是 namespace、subsystem、name、help。根据 cmt_opts.h 的fqname逻辑,这个示例最终会生成全限定指标名demo_service_requests_total。第 5、6 个参数声明标签数量与标签键数组,示例用0, NULL表示无标签。 - 写入采样:
cmt_counter_set(requests_total, sample_ns, 42.0, 0, NULL)以sample_ns为纳秒时间戳写入值 42.0。sample_ns = start_ns + 5_000_000_000意味着采样发生在开始后 5 秒。 - 定位数据点:
cmt_map_metric_get(&requests_total->opts, requests_total->map, 0, NULL, CMT_FALSE)返回该无标签系列对应的struct cmt_metric *,最后一个参数CMT_FALSE表示只读查找(不创建)。 - 附加 start 时间戳:
cmt_metric_set_start_timestamp(sample, start_ns)为该数据点设置 OTLP 累积流的开始时间。 - 编码 OTLP:
cmt_encode_opentelemetry_create(ctx)将整个上下文编码为一个 OTLP ExportMetricsServiceRequest protobuf,返回cfl_sds_t(CFL 动态字符串);结果通过cfl_sds_len()获取长度,最后用cmt_encode_opentelemetry_destroy()释放。编码失败码定义见 cmt_encode_opentelemetry.h(CMT_ENCODE_OPENTELEMETRY_ALLOCATION_ERROR、INVALID_ARGUMENT_ERROR、UNEXPECTED_METRIC_TYPE、DATA_POINT_INIT_ERROR)。
编码错误处理
OTLP 编码器可能返回以下错误码(定义于 cmt_encode_opentelemetry.h):
#define CMT_ENCODE_OPENTELEMETRY_SUCCESS 0 #define CMT_ENCODE_OPENTELEMETRY_ALLOCATION_ERROR 1 #define CMT_ENCODE_OPENTELEMETRY_INVALID_ARGUMENT_ERROR 2 #define CMT_ENCODE_OPENTELEMETRY_UNEXPECTED_METRIC_TYPE 3 #define CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR 4生产代码应当检查返回值,并在失败时逐层释放已分配的资源(参考示例中的cmt_destroy清理路径)。
大数据量下的 OTLP 分批编码
cmt_encode_opentelemetry.h 还额外提供了面向大数据量的分批 API:
cmt_encode_opentelemetry_split_payload(...):在不改变 metric/resource/scope 元数据的前提下,把已编码的 ExportMetricsServiceRequest 按max_data_points拆分为多个 batch;max_data_points为 0 时返回原始请求作为单个 batch;cmt_encode_opentelemetry_create_batches(...):直接从上下文按数据点上限分批生成;cmt_encode_opentelemetry_destroy_batches(...):释放批次集合,返回的 payload 由批次集合统一拥有。
当一次上报的数据点数量很大、需要控制单个 OTLP 请求大小时,这套 API 可以直接复用(Fluent Bit 的 OTLP 输出路径中即存在此类分批需求)。
指标过期清理与并发查找
除了 README 提到的内容,从源码还可以看到两个生产环境必须了解的机制:
- 过期清理:
cmt_expire(cmt, expiration)(声明于 cmetrics.h)配合cmt_map_metrics_expire(...)可以按时间戳淘汰过期数据点,防止动态标签系列无限增长——这对于高基数标签场景(如按 Pod/进程维度的指标)非常关键。对应的测试见 lib/cmetrics/tests/expire.c。 - 并发安全查找:
cmt_map_metric_get(...)内部对并发查找与创建做了序列化(头文件注释明确说明 "Concurrent lookups and metric creation are serialized internally"),返回的 metric 在 map 未过期或销毁前可用。struct cmt_metric中还带有内部查找索引字段(hash_indexed、map、_hash_head),配合 cmt_map.h 中的metric_buckets哈希桶结构提升带标签系列检索效率。
长标签值处理与安全边界(设计参考)
README 引用的设计文档 lib/cmetrics/docs/label-value-handling.md 记录了一次真实问题的完整复盘,对理解 CMetrics 的数据完整性策略很有价值:
- 背景:旧版本 CMetrics 在解码内部 MessagePack 时,拒绝任何超过 1024 字节的字符串,导致超过 1024 字节的 Prometheus 标签值(如
process_command_line)在"Prometheus 解析 → CMetrics msgpack 往返"过程中被整体丢弃(对应 Fluent Bit issue #9297)。 - 结论:不能通过静默截断来"修复"——因为 Prometheus 用"指标名 + 完整标签集"标识时间序列,两个共享 1024 字节前缀、仅尾部不同的标签值若都被截断为
...,会把不同序列合并成同一个,造成错误结果;固定字节偏移还可能切断多字节 UTF-8 字符。 - CMetrics 采取的原则:内部 MessagePack 解码器默认无损解码有效字符串;资源限制(如分配上限)与字符串内容/指标语义分离处理;解码前先校验声明的字节数是否真实存在,再一次性复制完整值,防止恶意长度字段触发超量分配,同时保证合法长字符串往返不被修改。
- 建议的摄入策略(由调用方实现,而非解码器):
preserve(完整保留,符合 Prometheus 默认行为)、reject(带诊断信息拒绝批次)、drop(仅丢弃违规序列并递增错误计数)、truncate_hash(在合法 UTF-8 边界截断并追加完整值哈希以尽量保持序列身份)。 - 回归测试覆盖:1023/1024/1025/2048/65536 字节值、相同前缀不同后缀的两个值、跨边界 UTF-8 字符、长字符串后的多字段同步性、声明长度大于实际输入、接近整数/分配上限的长度、分配失败清理路径以及端到端 Prometheus 抓取链路等,并建议配合 AddressSanitizer、UndefinedBehaviorSanitizer 与 Valgrind 做内存安全验证。
这个案例提醒所有集成方:呈现层需求不应通过修改内部数据模型来实现——编码器可以为了展示而缩写,但存储的值必须保持不变。
设计渊源:Go Prometheus Client
CMetrics 在 API 设计上深度借鉴了 Go Prometheus Client(prometheus/client_golang)的公开设计:namespace / subsystem / name的三段式命名、HELP文本、label 键值对、Counter/Gauge/Histogram/Summary 的家族划分等,都能在 Go 版客户端中找到对应物。这意味着熟悉 Prometheus 生态的开发者可以快速上手,而 Fluent Bit 的指标插件也因此天然贴合 Prometheus 的指标语义。Fluent Bit 内部"采集 → 指标上下文 → 多格式输出"的完整链路,正是围绕这一数据模型组织起来的。
小结
CMetrics 为 Fluent Bit 提供了统一的指标数据模型与协议转换能力:
- 六种指标类型(Counter/Gauge/Untyped/Histogram/Exponential Histogram/Summary)覆盖主流监控数据形态;
- 统一纳秒时间戳,并支持 OTLP 累积流所需的原生
start_timestamp(与既有timestamp完全向后兼容); - 八种编码器、五种解码器,打通 Prometheus、OTLP、Influx、Splunk、CloudWatch EMF 等格式之间的互转;
- 公开的 C API可直接在自定义插件中创建指标、写入采样、附加时间戳并编码输出;
- 同时提供指标过期清理、并发安全的 map 查找与安全的数据解码边界,适合作为长期运行的数据采集管线底座。
如需深入阅读实现与测试,推荐依次查看 lib/cmetrics/docs/architecture.md、lib/cmetrics/src 下的编码/解码源码,以及 lib/cmetrics/tests 中的counter.c、histogram.c、opentelemetry.c、encoding.c、decoding.c、expire.c等测试文件。
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考