news 2026/9/24 14:39:49

Miller 日志处理实战:用 DKVP 格式对异构日志做临时分析与聚合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Miller 日志处理实战:用 DKVP 格式对异构日志做临时分析与聚合
  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

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

本文基于 Miller 官方文档《Log-processing examples》(对应仓库文档 docs/src/log-processing-examples.md),围绕"临时性(ad-hoc)处理日志文件数据"这一场景,系统讲解如何利用 Miller 的 DKVP 输入格式、filter/step/sec2gmt/group-like/stats1等动词(verb)与is_present等 DSL 函数,对字段结构不一致的异构日志进行提取、过滤、聚合、时间换算与分组重排。读完本文,你将掌握一条完整的"程序打日志 → grep 预处理 → Miller 分析"流水线,并能直接套用到自己的日志分析任务中。

DKVP:让每条日志行独立自洽的数据格式

日志处理是 Miller 的典型使用场景之一,而这里的主角是 DKVP(Key-Value Pair,键值对)格式。DKVP 之所以在日志场景中"大放异彩",官方文档给出的理由非常直白:

  • 每条记录的行内同时携带字段名和字段值,因此每一行都自洽独立,可以单独被grepsedawk等系统工具处理,也可以被 Miller 单独解析;
  • 因为 schema(字段名集合)内嵌在数据里,不是每一行都必须拥有相同的字段名列表——这正是日志数据的常态:不同事件类型携带不同属性。

在 Miller 源码中,DKVP 读取器位于 pkg/input/record_reader_dkvp_nidx.go,它与 NIDX 读取器共用同一套骨架(RecordReaderDKVPNIDX),唯一差异是行的切分方式:DKVP 按key=value对切分并解析为Mlrmap,NIDX 则按位置取序号字段。从源码结构可以推断,DKVP 读取器解析一行后,直接把key=value对还原成记录的字段集合,因此字段名天然随行而异。

生成与聚合日志输出:一个完整的实战流水线

官方文档设想了一个非常贴近现实的场景:你可以在任何语言的程序里边运行边打印日志行,把不同事件的条目混在一起输出。程序跑完后,再用 Miller 把混杂的数据"分拣"出来、过滤、分析、从中学习。

假设你的程序打印出了类似 docs/src/log.txt 的内容:

op=enter,time=1472819681 op=cache,type=A9,hit=0 op=cache,type=A4,hit=1 time=1472819690,batch_size=100,num_filtered=237 op=cache,type=A1,hit=1 op=cache,type=A9,hit=0 ...

注意这里共出现了三类不同"形状"的记录:

事件类型携带字段含义
op=enterop,time程序进入,记录起始时间戳(Unix 秒)
op=cacheop,type,hit缓存访问事件,记录缓存类型与是否命中
批处理汇总time,batch_size,num_filtered每批数据处理完后的统计信息

每条 print 语句只包含局部信息:当前时间戳、某个缓存是否命中、本批处理了多少条等等。这正是日志分析的起点——日志行无需统一 schema,Miller 负责在事后把它们整理成可分析的结构

按需挑选记录:grep、having-fields 与 is_present 三选一

要从混杂的记录中挑出我们关心的部分,官方文档给出了三种手段,各有适用场景:

  1. 系统grep命令:最简单直接,适合按文本内容粗筛,例如grep op=cache log.txt
  2. Miller 的having-fields动词:按记录的字段名做结构化筛选;
  3. DSL 的is_present函数:在put/filter表达式中判断某字段是否存在。

having-fields的实现位于 pkg/transformers/having_fields.go,它支持六种判定模式:--at-least(至少包含这些字段)、--which-are(字段名恰好是这些)、--at-most(至多包含这些字段)、--all-matching(全部字段名匹配正则)、--any-matching(任一字段名匹配正则)、--none-matching(没有字段名匹配正则)。源码中每种模式对应一个独立的transformHavingFieldsXxx方法,例如--any-matching通过遍历记录头指针逐一MatchString判定,命中即放行。

is_present定义于 pkg/bifs/types.go,实现为FromBool(!input1.IsAbsent())——即只要字段值不是"缺席(absent)"状态即为真。它与is_null的区别在于:is_null在字段缺席、空(void)、null 三种情况下都返回真,而is_present只关心字段是否存在。

实战一:按缓存类型统计命中率

下面的命令先用系统grep筛出所有缓存访问行,再用 Miller 以 DKVP 格式读入、按type分组统计hit字段的均值:

grep op=cache log.txt \ | mlr --idkvp --opprint stats1 -a mean -f hit -g type then sort -f type

输出:

type hit_mean A1 0.8571428571428571 A4 0.7142857142857143 A9 0.09090909090909091

这条命令把三种能力串成了一条链:

  • grep op=cache:文本级粗筛,只保留缓存事件;
  • --idkvp:告诉 Miller 输入是 DKVP 格式("i" = input);
  • --opprint:输出采用漂亮的表格(pprint)格式;
  • stats1 -a mean -f hit -g type:按type分组,对hit计算均值;
  • then sort -f typethen是 Miller 的链式动词分隔符,这里按type排序输出。

注意此处的关键点:混入批处理汇总行(batch_size等)并不影响分析——grep已把它们过滤掉,且 DKVP 读取器对每行独立解析,字段不同也无妨。

实战二:用 is_present 过滤 + step 计算时间/数量增量

官方文档给出的第二组命令是:只保留含batch_size字段的记录(即批处理汇总行),然后计算timenum_filtered相对上一条记录的增量,最后把 Unix 时间戳换算成 GMT:

mlr --from log.txt --opprint \ filter 'is_present($batch_size)' \ then step -a delta -f time,num_filtered \ then sec2gmt time

输出:

time batch_size num_filtered time_delta num_filtered_delta 2016-09-02T12:34:50Z 100 237 0 0 2016-09-02T12:35:05Z 100 348 15 111 2016-09-02T12:35:13Z 100 493 8 145 2016-09-02T12:35:20Z 100 554 7 61 2016-09-02T12:35:36Z 100 612 16 58 2016-09-02T12:35:42Z 100 728 6 116

这里出现了三个重要语法与动词,逐一拆解:

--from log.txt:Miller 的主标志(main flag),直接指定输入文件,等价于把文件重定向到 stdin。

filter 'is_present($batch_size)'filter动词按 DSL 表达式逐记录判定。由于异构日志中大量行没有batch_size字段,直接引用$batch_size会得到 absent 值,因此必须用is_present先做存在性检查。从 pkg/bifs/types.go 的实现看,它返回"字段非 absent"的布尔值,正好充当过滤条件。

step -a delta -f time,num_filteredstep动词计算"依赖前后记录的值"。-a delta表示求相邻记录差值,-f time,num_filtered指定参与计算的两个数值字段,输出自动追加time_deltanum_filtered_delta后缀列。第一行time_delta=0是因为首条记录没有前置记录。

step的完整选项可从 pkg/transformers/step.go 的stepOptions看到:

选项取值说明
-acounter,delta,ewma,from-first,ratio,rprod,rsum,shift,shift_lag,shift_lead,slwinstepper 名称,可逗号分隔或多个-a
-f字段名列表参与计算的数值字段
-g字段名列表可选分组字段(如按主机名分组求增量)
-d权重列表EWMA 平滑权重,1表示不平滑,默认0.5
-o后缀列表自定义 EWMA 输出字段后缀,须与-d数量一致
-F布尔兼容 Miller 5 的 no-op 标志(Miller 6 起整数计算自动按浮点处理)

值得注意的是,step的 stepper 名单里有shiftshift_lag的别名)、shift_lagshift_leadslwin(滑动窗口均值)等,并支持带计数后缀的形式如shift_lag_12(回溯 12 条记录)、slwin_7_2(回溯 7 条、前瞻 2 条)。从 pkg/transformers/step.go 的STEPPER_LOOKUP_TABLE可以看到全部内置 stepper 及其语义。

sec2gmt time:把time字段从 Unix 秒换算为 GMT 时间戳(ISO 8601 格式)。这本质上是 pkg/transformers/sec2gmt.go 中sec2gmt动词对 DSLsec2gmt函数的"按键少字"封装——源码注释明确说明mlr sec2gmt time1,time2等价于mlr put '$time1 = sec2gmt($time1); $time2 = sec2gmt($time2)'。该动词还支持-1-9(保留 1~9 位小数)以及--millis/--micros/--nanos(输入为毫秒/微秒/纳秒时间戳)等选项。

异构数据分组重排:group-like 动词

前面的例子靠"先过滤再分析"回避了 schema 不一致问题。但官方文档还展示了一个更"偷懒"的思路:把相似形状的记录聚拢到一起,直接观察——这正是group-like动词的用途。

mlr --opprint group-like log.txt

输出(节选):

op time enter 1472819681 op type hit cache A9 0 cache A4 1 cache A1 1 ... time batch_size num_filtered 1472819690 100 237 1472819705 100 348 ...

可以看到,group-like把异构记录按字段名集合是否相同分成三批:op,time批、op,type,hit批、time,batch_size,num_filtered批,批间用空行分隔。

从源码 pkg/transformers/group_like.go 看,group-like的实现思路是:用inrec.GetKeysJoined()把每条记录的字段名拼成分组键,存入一个lib.OrderedMap(键为字段名串,值为记录列表);在流结束时(EndOfStream)按插入顺序把所有分组依次输出。使用有序映射而非普通哈希表,保证了输出分组的顺序确定性。

group-like动词没有任何选项参数,但它同样可以与then链式组合。官方文档紧接着给出了一个组合示例:先分组、再顺便把time字段换算成 GMT:

mlr --opprint group-like then sec2gmt time log.txt

输出(节选):

op time enter 2016-09-02T12:34:41Z op type hit cache A9 0 ... time batch_size num_filtered 2016-09-02T12:34:50Z 100 237 2016-09-02T12:35:05Z 100 348 ...

注意sec2gmt time作用于三条批:op=enter行的time字段(1472819681 → 2016-09-02T12:34:41Z)与批处理汇总行的time字段都被正确换算,而op,type,hit批没有time字段,行原样保留——这再次印证了 Miller 对异构记录"各取所需、缺字段不报错"的处理哲学。

解析日志输出:grep/sed 预处理后再交给 Miller

"生成与聚合"一节讨论的是结构化程度较高的key=value日志;但现实中的日志往往长这样(官方文档给出的示例):

2015-10-08 08:29:09,445 INFO com.company.path.to.ClassName @ [sometext] various/sorts/of data {& punctuation} hits=1 status=0 time=2.378

这是一行典型的 Java/Log4j 风格日志:时间戳、级别、类名、自由文本和结构化键值对混在一起。Miller 官方文档给出的处理策略是:先用grep和/或sed做文本级预处理,把结构化部分提取出来,再交给 Miller。示例命令:

grep 'various sorts' *.log \ | sed 's/.*} //' \ | mlr --fs space --repifs --oxtab stats1 -a min,p10,p50,p90,max -f time -g status

这条流水线包含几个值得拆解的要点:

  • grep 'various sorts' *.log:按关键字粗筛日志文件;
  • sed 's/.*} //':贪婪删除到最后一个}为止的文本,只保留后面的hits=1 status=0 time=2.378等键值对;
  • --fs space:将字段分隔符设为空格(此时行内是hits=1 status=0 time=2.378这类空格分隔的键值对);
  • --repifs:允许"重复的字段分隔符"——多个连续空格被视为一个分隔符,这对日志文本中不规则的空白至关重要;
  • --oxtab:以 XTAB(交叉表)格式输出,每条记录纵向展开为key: value多行,便于阅读;
  • stats1 -a min,p10,p50,p90,max -f time -g status:按status分组,对time字段求最小值、10/50/90 百分位和最大值——典型的延迟分析。

文档在此处给出的输出为... output here ...,即结果取决于读者日志的实际内容;但命令结构本身即是可复用的模板:任何"一行里既有自由文本又有键值对"的日志,都可以用这条"grep → sed → Miller"三段式流水线转成结构化数据做统计

方法论小结:异构日志分析的三板斧

通读官方文档,可以提炼出 Miller 日志分析的三条核心方法论:

  1. 日志行自带 schema,天然适合 DKVP:写日志时无需约束所有事件使用同一组字段,每条日志"各说各话",事后统一交给 Miller 解析即可。这消除了传统 CSV 日志对字段顺序和表头的强约束。
  2. 三层筛选手段按需选用:文本层用系统grep/sed(简单、快、可处理自由文本);字段名层用having-fields(结构化、支持正则);字段值层用filter+is_present等 DSL 表达式(最灵活,可叠加任意逻辑)。
  3. 用 then 链把动词组装成流水线stats1聚合、step增量、sec2gmt时间换算、group-like分组重排,都可以用then自由组合;输出格式通过--o*系列标志(--opprint--oxtab等)切换,同一份数据可以"看"出多种形态。

更进一步,上述命令均可用 Miller 的回归测试体系复现与验证:例如group-like的最小复现命令记录在 test/cases/verb-group-like/0001/cmd,内容是mlr group-like test/input/het.dkvp——test/input/het.dkvp正是一个字段形状混杂的 DKVP 样本,其预期输出与该动词"按相同字段名集合分批输出"的语义完全一致。相关动词的更多使用细节可参考仓库文档 docs/src/reference-verbs.md(动词总览)与 docs/src/reference-dsl-builtin-functions.md(DSL 内建函数,含is_present的完整说明)。

参考:本文用到的示例数据与源码位置

  • 示例日志数据:docs/src/log.txt(op=enter/op=cache/ 批处理汇总三类异构记录)
  • 官方文档原文:docs/src/log-processing-examples.md
  • DKVP 读取器实现:pkg/input/record_reader_dkvp_nidx.go
  • group-like动词实现:pkg/transformers/group_like.go
  • step动词实现与 stepper 全表:pkg/transformers/step.go
  • sec2gmt动词实现:pkg/transformers/sec2gmt.go
  • having-fields动词实现:pkg/transformers/having_fields.go
  • is_present函数实现:pkg/bifs/types.go
  • 回归测试样例:test/cases/verb-group-like/0001/cmd
  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

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

相关推荐

上一篇:终极C排序算法指南:20+种排序方法深度对比与实战应用
下一篇:高级问题解决:contribute-to-scroll的复杂集成挑战与方案

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:38:12

还在费力去除AI生图水印吗?

背景重绘 局部修图 上一篇:免安装,免注册,免费token,niuma编程工具-CSDN博客

作者头像 李华
网站建设 2026/9/24 14:36:01

【计算机毕业设计单片机案例】基于 STM32 或 51 单片机的语音播报智能门窗控制装置设计 基于 STM32 或 51 单片机雨滴感应自动关窗控制系统设计(025608)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/24 14:35:45

在 Debian/Ubuntu 上通过 DEB 包安装 DocumentDB 扩展并部署 FerretDB

后端数据库文档数据库 【免费下载链接】FerretDB A truly Open Source MongoDB alternative 项目地址: https://gitcode.com/gh_mirrors/fe/FerretDB 点击查看 免费下载 本指南以 FerretDB 官方 v2.5 文档 website/versioned_docs/version-v2.5/installation/docum…

作者头像 李华
网站建设 2026/9/24 14:35:32

Java中方法,数组的使用

方法一,方法的概念与定义1.概念:将代码模块化(类似于c语言中的函数),使其能被直接调用2.定义:public static 返回值 方法名(形式参数列表){方法体}eg:public static boolean isLeapY…

作者头像 李华