日志结构化与非结构化混合治理:JSON 与纯文本共存策略
日志结构化与非结构化混合治理JSON 与纯文本共存策略一、日志平台搜不出有用的因为一半是 JSON 一半是纯文本微服务架构的常见怪象新服务打 JSON 格式的结构化日志老服务打纯文本日志中间件的日志格式五花八门Nginx 用自定义模板、MySQL 用 error log、Redis 用纯文本。ELK 集群的索引 mapping 冲突不断同一个字段一会儿是string一会儿是object。更糟的是文本日志中嵌了 JSON 片段——比如Error: {code: 500, message: timeout}——日志解析器直接崩溃。这不是迁移到 JSON就能解决的问题。老系统不能停新系统不能等。必须设计一套混合治理策略在写入管道中实现统一解析让不同格式的日志都能被结构化查询。二、多格式日志的统一治理架构graph TD A[服务 Podbr/stdout/stderr] -- B[Fluentd / Vectorbr/日志采集] B -- C{Header 检测br/首字符判断} C --|{| D[JSON Parserbr/结构化解析] C --|[| E[JSON Arraybr/逐行解析] C --|其它| F[Regex Parserbr/模式匹配] D -- G[Schema 规范化br/- 字段重命名br/- 类型转换br/- 必填字段补全] E -- G F -- G G -- H[统一 Schemabr/{timestamp, level, service,br/message, trace_id, ...}] H -- I[写入 ClickHousebr/列式存储] H -- J[写入 Elasticsearchbr/全文搜索] style B fill:#4A90D9,color:#fff style C fill:#F5A623,color:#000 style G fill:#50B86C,color:#fff核心设计理念不要求所有服务改写日志格式而是在采集层做格式适配 Schema 归一化。不同格式的日志流入统一的管道解析出相同的 Schema 字段后写入存储。关键环节Header 检测日志采集器根据首字符判断格式。{→ JSON[→ JSON 数组其他 → 正则。这个简单的分路策略避免了先尝试 JSON 解析失败再 fallback 到正则的 try-catch 性能损失。Schema 规范化无论原始格式是什么最终写入存储的 Schema 必须统一。缺少的字段补默认值类型不匹配的强制转换。双写存储ClickHouse 存结构化字段做聚合分析Elasticsearch 存原始消息做全文搜索互补。三、生产级混合日志管道实现Fluentd 配置多格式自动路由source type tail path /var/log/containers/*.log pos_file /var/log/fluentd-containers.log.pos tag kube.* read_from_head true parse type multi_format # 先用 CRI 格式解析 K8s 容器日志 # K8s 容器日志格式timestamp stream tag message pattern format cri /pattern /parse /source # 多格式路由过滤器 filter kube.** type record_transformer enable_ruby true record # 从 K8s labels 中提取服务名 service_name ${record.dig(kubernetes, labels, app) || unknown} # 从文件名提取 pod 名 pod_name ${record.dig(kubernetes, pod_name) || unknown} /record /filter # 核心根据日志内容格式选择解析器 filter kube.** type rewrite_tag_filter rule key log # 以 { 开头的 → JSON 日志流 pattern /^\s*\{/ tag parsed.json.${tag} /rule rule key log # 以 [ 开头的 → JSON Array 日志流 pattern /^\s*\[/ tag parsed.json_array.${tag} /rule rule key log # 其余 → 纯文本日志流 pattern /^/ tag parsed.text.${tag} /rule /filter # JSON 日志流处理 filter parsed.json.** type parser key_name log parse type json /parse /filter filter parsed.json_array.** type parser key_name log parse type json /parse /filter # 纯文本日志流正则提取 filter parsed.text.** type parser key_name log parse type regexp # 匹配常见日志格式时间戳 级别 消息 expression /^(?timestamp\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}.\d).*?(?levelERROR|WARN|INFO|DEBUG)\s(?message.*)/ time_key timestamp time_format %Y-%m-%dT%H:%M:%S.%L /parse /filter # Schema 归一化 filter parsed.** type record_transformer enable_ruby true record # 统一字段命名 timestamp ${record[timestamp] || record[timestamp] || Time.now.iso8601} level ${record[level] || record[severity] || INFO} message ${record[message] || record[log] || record[msg] || } trace_id ${record[trace_id] || record[traceId] || record[dd.trace_id] || unknown} span_id ${record[span_id] || record[spanId] || unknown} # 补充必填字段 service ${record[service_name] || record[service] || unknown} environment ${record[env] || production} # 原始消息保存为 raw 字段方便排查解析错误 raw ${record[log] || } # 字段类型强制转换 status_code ${record[status_code].to_i || 0} duration_ms ${record[duration].to_f || 0} /record remove_keys log # JSON 解析后删除 raw log 字段 /filterClickHouse 表设计-- 统一日志 Schema 表 -- 为什么用 MergeTree 而非 ReplacingMergeTree -- 日志是追加写入的不需要去重MergeTree 即可 CREATE TABLE IF NOT EXISTS unified_logs ( -- 分区键按天分区方便删除旧数据 date Date DEFAULT toDate(timestamp), -- 核心字段 timestamp DateTime64(3), -- 毫秒精度 level LowCardinality(String), -- ERROR/WARN/INFO/DEBUG 低基数字符串 service LowCardinality(String), environment LowCardinality(String), message String, -- 链路追踪 trace_id String DEFAULT , span_id String DEFAULT , -- 扩展字段JSON 动态列 -- 为什么不固定 Schema -- 不同服务的扩展字段不同固定列会频繁 ALTER TABLE extra_fields Map(String, String), -- HTTP 相关可选 status_code UInt16 DEFAULT 0, duration_ms Float64 DEFAULT 0, http_method LowCardinality(String) DEFAULT , http_path String DEFAULT , -- 原始消息全文搜索用 raw_message String DEFAULT ) ENGINE MergeTree() PARTITION BY toYYYYMMDD(date) ORDER BY (service, level, timestamp) -- TTL: 90 天后自动删除 TTL date INTERVAL 90 DAY SETTINGS index_granularity 8192, ttl_only_drop_parts 1; -- 为 trace_id 创建跳数索引 ALTER TABLE unified_logs ADD INDEX idx_trace_id trace_id TYPE bloom_filter GRANULARITY 1; -- 物化视图聚合错误率统计 CREATE MATERIALIZED VIEW error_rate_view ENGINE AggregatingMergeTree() PARTITION BY toYYYYMM(date) ORDER BY (service, date) AS SELECT service, toDate(timestamp) as date, countState() as total_requests, countIfState(level ERROR) as error_count FROM unified_logs GROUP BY service, date;四、混合治理的陷阱缺点正则解析的性能瓶颈每条文本日志都要跑正则在百万 QPS 的系统中正则解析成为管道中最慢的环节。需要使用 Vector 等 Rust 实现的采集器比 Fluentd Ruby 快 10 倍。Schema 冲突难处理同一个status_code字段在服务 A 是int服务 B 是string如200。归一化层的类型转换可能导致静默的数据丢失。解决在归一化层加字段类型对齐配置表。JSON 嵌入文本的嵌套解析Error: {code:500, message:timeout}这种日志需要先正则提取 JSON 部分再 JSON parse。但正则可能被假 JSON如日志中{a: b}恰好出现在文本描述里误导。禁用场景可强制统一日志格式的新项目直接全部 JSON不需要混合治理。日志量极少 1000 条/天管道化治理的配置复杂度超过收益。五、总结日志混合治理的核心思路在采集层做格式适配在归一化层做 Schema 统一。通过首字符判断分流 JSON 和文本日志用正则提取文本日志的关键字段最终归一化到相同的写入 Schema。工程上注意两个要点采集器选型Rust 实现的 Vector 比 Fluentd 更适合高吞吐场景以及 Schema 字段的类型对齐配置。日志治理不是全改 JSON而是让不同格式的日志能被同一个查询语言检索。