#operator #log-file #json #parse #aggregate #log-parser #cli

bin+lib ag

命令行应用程序,用于切割和切片日志文件

19 个版本 (破坏性)

0.19.4 2024年6月1日
0.19.3 2023年12月9日
0.19.2 2023年5月21日
0.18.0 2021年10月6日
0.6.2 2018年3月30日

#258 in 命令行工具

每月下载量 34

MIT 许可证

325KB
8K SLoC

angle-grinder 构建状态 Gitter 聊天

在命令行中切割和切片日志文件。

Angle-grinder 允许您解析、聚合、求和、平均、最小/最大、百分位数和排序您的数据。您可以在终端中实时查看这些数据。Angle grinder 设计用于当您没有石墨烯/honeycomb/kibana/sumologic/splunk 等数据时,但仍想进行复杂的分析。

Angle grinder 可以处理每秒超过 1M 行(简单的管道高达 5M),因此适用于相当有肉的数据聚合。随着数据的处理,结果将在您的终端中实时更新。Angle grinder 是一种骨感的函数式编程语言,与一个相当不错的终端 UI 相结合。

overview gif

安装

二进制文件适用于 Linux 和 OSX。如果您从源代码编译,还有更多平台(包括 Windows)可用。在以下所有命令中,生成的二进制文件将称为 agrind。从 v0.9.0 开始,agrind 可以通过 --self-update 标志进行自我更新。感谢许多志愿者在不同的包管理器与环境上维护 angle-grinder!

macOS

Brew

brew install angle-grinder

Macports

sudo port selfupdate
sudo port install angle-grinder

FreeBSD

pkg install angle-grinder

Linux (任何兼容MUSL的变体)

curl -L https://github.com/rcoh/angle-grinder/releases/download/v0.18.0/agrind-x86_64-unknown-linux-musl.tar.gz \
  | tar Ozxf - \
  | sudo tee /usr/local/bin/agrind > /dev/null && sudo chmod +x /usr/local/bin/agrind
  
agrind --self-update  

Cargo(大多数平台)

如果您已安装Cargo,您可以从源代码编译和安装:(适用于稳定版Rust >=1.26)

cargo install ag

查询语法

角磨机查询由一系列过滤器后跟一系列运算符组成。过滤器选择要由运算符转换的输入流中的行。通常,初始运算符将通过解析字段或日志行中的JSON以某种方式转换数据。随后,可以使用像sumaveragepercentile等运算符对数据进行聚合或分组。

agrind '<filter1> [... <filterN>] | operator1 | operator2 | operator3 | ...'

一个简单的查询,操作JSON日志并计算每个级别的日志数量可能是

agrind '* | json | count by log_level'

转义字段名称

包含空格、点或引号的字段名称必须使用["<字段名>"]进行转义

agrind '* | json | count by ["date received"], ["grpc.method"]

过滤器

有三个基本过滤器

  • *:匹配所有日志
  • filter-me*(不带引号)是一个不区分大小写的匹配,可以包含通配符
  • "filter-me"(带引号)是一个区分大小写的匹配(没有通配符,*匹配字面量*filter-me"filter me!")。

过滤器可以与ANDORNOT结合使用

("ERROR" OR WARN*) AND NOT staging | count

子表达式必须在括号中分组。只有与所有过滤器匹配的行才会传递给后续的运算符。filter.gif

别名

从v0.12.0开始,角磨机支持别名,预构建的管道可以简化常见的任务或格式。目前定义的唯一别名是apache,它可以解析Apache日志。添加更多别名贡献的最简单方法之一!

示例:

* | apache | count by status

运算符

非聚合运算符

这些运算符与输入数据和输出数据之间有1对1的对应关系。1行输入,0或1行输出。

JSON

json [from other_field]:将json序列化的行提取到字段中供以后使用。如果行不是有效的JSON,则将其删除。可以指定from other_field。开箱支持嵌套的JSON结构。只需使用.key[index]访问嵌套值,例如,.servers[6]。也支持负索引。

示例:

* | json
* | parse "INFO *" as js | json from js

给定输入如

{"key": "blah", "nested_key": {"this": "that"}}
* | json | count_distinct(nested_key.this)

json.gif

Logfmt

logfmt [from other_field]:将logfmt序列化的行提取到字段中供以后使用。如果行不是有效的logfmt,则将其删除。可以指定from other_field。Logfmt是Heroku和Splunk常用的输出格式,描述在https://www.brandur.org/logfmt

示例:

* | logfmt

给定输入如

{"key": "blah", "nested_key": "some=logfmt data=more"}
* | json | logfmt from nested_key | fields some
Split

split[(input_field)] [分隔符] [as new_field]:通过分隔符拆分输入(默认为,)。输出为数组类型。如果没有指定input_fieldnew_field,则内容将放在键_split中。

示例:

* | split on " "

给定输入如

INFO web-001 influxd[188053]: 127.0.0.1 "POST /write HTTP/1.0" 204

输出

[_split=[INFO, web-001, influxd[188053]:, 127.0.0.1, POST /write HTTP/1.0, 204]]

如果使用了input_field,但没有指定new_field,则input_field将被拆分的数据结构覆盖。例如:

* | parse "* *" as level, csv | split(csv)

给定输入如

INFO darren,hello,50
WARN jonathon,good-bye,100

将输出

[csv=[darren, hello, 50]]        [level=INFO]
[csv=[jonathon, good-bye, 100]]        [level=WARN]

其他示例

* | logfmt | split(raw) on "blah" as tokens | sum(tokens[1])
解析

parse "* 模式 * 其他模式 *" [来自字段] as a,b,c [nodrop]:将匹配模式的文本解析到变量中。如果不指定nodrop,则不匹配模式的行将被丢弃。*相当于正则表达式.*并且是贪婪的。默认情况下,parse操作消息的原始文本。使用from field_name,解析将处理来自特定列的输入。解析表达式中的任何空白都将与输入文本中的任何空白字符匹配(例如,一个字面制表符)。

示例:

* | parse "[status_code=*]" as status_code

parse.gif

解析正则表达式

parse regex "<regex-with-named-captures>" [来自字段] [nodrop]:将输入文本与正则表达式匹配,并使用命名捕获填充记录。如果不指定nodrop,则不匹配模式的行将被丢弃。默认情况下,parse操作消息的原始文本。使用from field_name,解析将处理来自特定列的输入。

注意:

  • 仅支持命名捕获。如果正则表达式包含任何未命名的捕获,将引发错误。
  • 使用Rust正则表达式语法
  • 转义序列不需要额外的反斜杠(即\w按原样工作)。

示例:解析短语"Hello, ...!"并捕获名字字段中的"..."值

* | parse regex "Hello, (?P<name>\w+)"
字段

fields [only|except|-|+] a, b:根据指定的模式丢弃字段a, b或仅包含a, b

示例:丢弃所有字段,除了eventtimestamp

* | json | fields + event, timestamp

仅丢弃event字段

* | fields except event
在哪里

where <bool-expr>:删除不满足条件的行。条件必须是一个返回布尔值的表达式。表达式可以是简单的字段名或字段和文字值(例如数字、字符串)之间的比较(即 ==, !=, <=, >=, <, >)。可以使用 '!' 运算符否定子表达式的结果。注意,None == None,因此当左右两边都匹配一个不存在的键时,该行将匹配。

示例

* | json | where status_code >= 400
* | json | where user_id_a == user_id_b
* | json | where url != "/hostname"
限制

limit #:限制行的数量为给定数量。如果数字为正,则返回前N行。如果数字为负,则返回最后N行。

示例

* | limit 10
* | limit -10
字段表达式

<expr> as <name>:计算给定的表达式并将结果存储在具有给定名称的字段中,用于当前行。表达式可以由以下组成

  • +-*/:具有正常优先级规则的数学运算符。这些运算符适用于数值和可以自动转换为数字的字符串。此外,在适当的情况下,这些运算符也适用于日期时间值和持续时间值。例如,您可以计算两个日期时间的差值,但不能将它们相加。
  • ==!=(或<>),<=>=<>:布尔运算符适用于大多数数据类型。
  • and&&or||:短路逻辑运算符。
  • <field>:当前行中的一个字段名称。如果行不包含给定的字段,将报告错误。
  • 括号用于分组操作

以下函数在表达式内受支持

  • 数学函数:abs()acos()asin()atan()atan2()cbrt(), ceil()cos()cosh()exp()expm1()floor()hypot()log()log10(), log1p()round()sin()sinh()sqrt()tan()tanh()toDegrees()toRadians()
  • concat(arg0, ..., argN) - 将参数连接成一个字符串
  • contains(haystack, needle) - 如果字符串中包含目标字符串,则返回 true。
  • length(str) - 返回字符串 "str" 中的字符数。
  • now() - 返回当前日期和时间。
  • num(value) - 将给定值作为数字返回。
  • parseDate(str) - 尝试从给定字符串中解析日期。
  • parseHex(str) - 尝试将十六进制字符串转换为整数。
  • substring(str, startOffset, [endOffset]) - 返回从给定的起始偏移量到结束偏移量(如果指定)指定的字符串部分。
  • toLowerCase(str) - 返回字符串的小写版本。
  • toUpperCase(str) - 返回字符串的大写版本。
  • isNull(value) - 如果值是 null,则返回 true,否则返回 false。
  • isEmpty(value) - 如果值是 null 或空字符串,则返回 true,否则返回 false。
  • isBlank(value) - 如果值是 null、空字符串或仅包含空白字符的字符串,则返回 true,否则返回 false。
  • isNumeric(str) - 如果给定的字符串是一个数字,则返回 true。

示例value 乘以 100 以获得百分比

* | json | value * 100 as percentage
if 操作符

if(<condition>, <value-if-true>, <value-if-false>): 根据提供的条件选择两个值。

示例

获取成功请求的字节数

* | json | if(status == 200, sc_bytes, 0) as ok_bytes

聚合操作符

聚合操作符通过 0 个或多个键字段对您的数据进行分组和组合。同一个查询可以包含多个聚合。通用语法是

(operator [as renamed_column])+ [by key_col1, key_col2]

在最简单的形式中,键字段指的是列,但它们也可以是泛型表达式(参见示例)示例

* | count
* | json | count by status_code
* | json | count, p50(response_ms), p90(response_ms) by status_code
* | json | count as num_requests, p50(response_ms), p90(response_ms) by status_code
* | json | count, p50(response_ms), p90(response_ms), count by status_code >= 400, url

有几种聚合操作符可用。

计数

count[(condition)] [as count_column]:计算输入行的数量。输出列默认为 _count。您可以选择提供一个条件 - 这将计算所有条件评估为 true 的行。

示例:

source_host 计算行数

* | count by source_host

计算 source_hosts 的数量

* | count by source_host | count

计算 info 与 error 日志的数量

* | json | count(level == "info") as info_logs, count(level == "error") as error_logs
求和

sum(column) [as sum_column]:计算 column 中的值总和。如果 column 中的值不是数字,则忽略该行。示例

* | json | sum(num_records) by action
最小值

min(column) [as min_column] [by a, b] :计算 column 中值的最大值。如果 column 中的值不是数字,则忽略该行。

示例:

* | json | min(response_time)
平均值

average(column) [as average_column] [by a, b] :计算 column 中的平均值。如果 column 中的值不是数字,则忽略该行。

示例:

* | json | average(response_time)
最大值

max(column) [as max_column] [by a, b] :计算column中值的最大值。如果column中的值不是数字,该行将被忽略。

示例:

* | json | max(response_time)
百分位数

pXX(column):计算column的XX百分位数

示例:

* | json | p50(response_time), p90(response_time) by endpoint_url, status_code
排序

sort by a, [b, c] [asc|desc]:按照一组合并列排序聚合数据。默认为升序。

示例:

* | json | count by endpoint_url, status_code | sort by endpoint_url desc

除了列之外,sort还可以对任意表达式进行排序。

* | json | sort by num_requests / num_responses
* | json | sort by length(endpoint_url)
时间段

timeslice(<timestamp>) <duration> [as <field>]:将时间戳截断到指定的持续时间,以便将消息分割成时间段。参数timestamp必须是一个日期值,例如由parseDate()函数返回的值。持续时间是数量后跟以下单位之一

  • ns - 纳秒
  • us - 微秒
  • ms - 毫秒
  • s - 秒
  • m - 分钟
  • h - 小时
  • d - 天
  • w - 周

默认情况下,结果时间戳放在_timeslice字段中,或者放在as关键字后面的字段中。

示例:

* | json | timeslice(parseDate(ts)) 5m
总计

total(a) [as renamed_total]:计算给定字段的累计总和。总计目前不支持分组!

示例:

* | json | total(num_requests) as tot_requests
计数不重复

count_distinct(a):计算列a的不重复值数量。警告:这不是固定内存。请注意不要处理过多的组。

示例:

* | json | count_distinct(ip_address)

示例查询

  • 按发布(有特别嘉宾jq)计算angle-grinder的下载次数
curl  https://api.github.com/repos/rcoh/angle-grinder/releases  | \
   jq '.[] | .assets | .[]' -c | \
   agrind '* | json
         | parse "download/*/" from browser_download_url as version
         | sum(download_count) by version | sort by version desc'

输出

version       _sum
-----------------------
v0.6.2        0
v0.6.1        4
v0.6.0        5
v0.5.1        0
v0.5.0        4
v0.4.0        0
v0.3.3        0
v0.3.2        2
v0.3.1        9
v0.3.0        7
v0.2.1        0
v0.2.0        1
  • 按主机计算响应时间的第50百分位数
tail -F my_json_logs | agrind '* | json | pct50(response_time) by url'
  • 按URL计算状态码的数量
tail -F  my_json_logs | agrind '* | json | count status_code by url'

更多示例查询可以在测试文件夹中找到

渲染

非聚合数据按行写入终端,就像接收时那样

tail -f live_pcap | agrind '* | parse "* > *:" as src, dest | parse "length *" as length'
[dest=111.221.29.254.https]        [length=0]        [src=21:50:18.458331 IP 10.0.2.243.47152]
[dest=111.221.29.254.https]        [length=310]      [src=21:50:18.458527 IP 10.0.2.243.47152]

可以使用--output标志提供替代渲染格式。选项

  • --output json:JSON输出
  • --output logfmt:logfmt样式输出(k=v
  • --output format=<rust formatter>:此标志使用rust字符串格式化语法。例如
    tail -f live_pcap | agrind --format '{src} => {dst} | length={length}' '* | parse "* > *:" as src, dest | parse "length *" as length'
    21:50:18.458331 IP 10.0.2.243.47152 => 111.221.29.254.https | length=0
    21:50:18.458527 IP 10.0.2.243.47152 => 111.221.29.254.https | length=310
    

聚合数据将被写入终端,并将在流结束时进行实时更新。

k2                  avg
--------------------------------
test longer test    500.50
test test           375.38
alternate input     4.00
hello               3.00
hello thanks        2.00

渲染器将尽最大努力保持数据在变化时格式良好,并且输出行数限制为终端的长度。当前,它的刷新率约为20Hz。

渲染器可以检测输出是否为tty -- 如果您将其写入文件,则管道完成后将打印一次。

贡献

angle-grinder 使用Rust >= 1.26进行构建。提交PR时需要rustfmtrustup component add rustfmt)。

您可以通过多种方式做出贡献

  • 定义新的别名以供常见的日志格式或操作使用
  • 添加新的专用运算符
  • 改进现有运算符的文档并提供更多使用示例
  • 提供更多真实世界数据上的真实查询测试用例
  • 让更多人了解angle grinder!
cargo build
cargo test
cargo install --path .
agrind --help
... write some code!

cargo fmt

git commit ... etc.

在提交PR时,请运行cargo fmt -- 这是CI套件通过所必需的。您可以通过以下方式安装cargo fmtrustup component add rustfmt,如果它尚未包含在您的工具链中。

请参阅以下项目和开放问题以获取具体的潜在改进/错误。

项目:改进错误报告

通过准确且有用的错误消息,查询相关问题的可用性可以得到极大的改善。如果您在弄清楚为什么查询没有正常工作并难以解决问题时感到困扰,那么这是一个跳入并开始进行更改的好地方!

首先,您需要确定问题发生的地方。如果解析器拒绝查询,则语法可能需要一些调整以更接受某些语法。例如,如果parse运算符未提供字段名称,则查询仍然可以解析以生成语法树,并在下一阶段引发错误。如果查询通过了解析阶段,则问题可能在于语义分析阶段,其中解析树中的值需要验证其正确性。以parse为例,如果模式字符串中的捕获数与字段名称数不匹配,则错误将在这里引发。最后,如果查询到目前为止一直是有效的,您可能希望在执行时引发错误。例如,如果访问的字段名称不存在于传递给运算符的记录中,则可以引发错误以告知用户他们可能已输入错误的名称。

一旦您有了一个可能存在问题的地方的想法,您就可以开始深入研究代码。语法是用nom编写的,并包含在lang.rs模块中。构成解析树的枚举/结构也位于lang.rs模块中。为了使错误报告更容易,解析树中的值用Positioned对象包装,记录了值在查询字符串中的来源。这些对象由with_pos!解析组合器生成。然后,可以将这些对象传递到errors.rs模块中的SnippetBuilder以在错误消息中突出显示查询字符串的一部分。

语义阶段包含在typecheck.rs模块中,可能大部分工作都需要在这个模块中完成。该模块中的semantic_analysis()方法接收一个ErrorBuilder,可用于构建并发送错误报告给用户。

调整语法并添加问题检查后,将需要确定如何通知用户。理想情况下,任何错误都应解释问题、指向查询字符串的相关部分,并引导用户到解决方案。使用ErrorBuilder,您可以调用new_error_report_for()方法来为特定错误构造一个SnippetBuilder。为了突出显示查询字符串的一部分,使用带有指向查询字符串相关段落的Positioned对象的with_code_pointer()方法。最后,通过调用with_resolution()方法可以添加额外的帮助/示例。

完成所有操作后,您应该会看到一个格式良好的错误消息,如下所示

error: Expecting an expression to count
  |
1 | * | count_distinct
  |     ^^^^^^^^^^^^^^ No field argument given
  |
  = help: example: count_distinct(field_to_count)

类似项目

  • Angle Grinder是Sumoshell的重写,旨在更容易使用、可测试,并成为新特性的更好平台。
  • lnav是一个功能齐全的终端日志分析平台(比angle-grinder具有更多功能)。它包含对常见日志文件格式的原生支持、对日志的通用SQL查询、自动着色以及其他许多功能。
  • visidata是一个终端中的电子表格应用程序

依赖关系

~16–32MB
~539K SLoC