Apache SkyWalking 日志分析语言(LAL)完全指南:从 DSL 语法到日志、链路与指标联动
【免费下载链接】skywalkingAPM, Application Performance Monitoring System项目地址: https://gitcode.com/gh_mirrors/sky/skywalking
导读
Log Analysis Language(LAL,日志分析语言)是 Apache SkyWalking OAP 服务端内置的一门领域特定语言(DSL),用于对上报到后端的原始日志进行解析、字段抽取、按需持久化,并在此基础上实现日志与链路追踪(Trace)、指标(Metrics)系统的联动。阅读本文后,你将掌握 LAL 规则文件的结构与激活方式、Filter / Parser / Extractor / Sink 四大组件的完整语法与配置项,并能独立编写出"解析日志 → 抽取 traceId → 生成指标 → 采样落库"的完整分析规则。
LAL 是什么:SkyWalking 的日志分析 DSL
在 SkyWalking 的架构中,日志链路为:各类 Agent / 接收器将日志上报到 OAP 的 log-analyzer 模块,而 LAL 就是该模块用于加工日志的脚本语言。通过 LAL 可以完成四类工作:
- 解析(Parse):把非结构化或半结构化的原始日志解析为结构化字段;
- 抽取(Extract):从解析结果中抽出服务名、实例名、端点名、traceId、segmentId、spanId、时间戳等元数据;
- 保存(Save):决定哪些日志被持久化到存储中,以及以何种采样策略保存;
- 联动(Correlate):通过 traceId / segmentId / spanId 将日志与现有链路关联,通过 metrics extractor 将日志转化为指标送入 Meter 系统。
LAL 规则文件是 YAML 格式,存放在 OAP 的lal目录下。在默认发行包中,该目录位于 oap-server/server-starter/src/main/resources/lal,内含default.yaml、envoy-als.yaml、k8s-service.yaml、mesh-dp.yaml、mysql-slowsql.yaml、nginx.yaml、pgsql-slowsql.yaml、redis-slowsql.yaml等内置规则。
激活 LAL 规则文件
可以通过两种方式指定要加载的 LAL 配置文件:
- 在
application.yml中设置log-analyzer/default/lalFiles; - 设置环境变量
SW_LOG_LAL_FILES。
对应关系在 LogAnalyzerModuleConfig.java 中定义:默认的lalPath为lal,默认的lalFiles为default.yaml,多个文件用逗号分隔。同样地,由 metrics extractor 生成的指标交由 MAL(Meter Analysis Language,参见 mal.md)做进一步分析,对应的malFiles配置项默认目录为log-mal-rules。
# application.yml # ... log-analyzer: selector: ${SW_LOG_ANALYZER:default} default: lalFiles: ${SW_LOG_LAL_FILES:my-lal-config} # 文件位于 "lal" 目录下 malFiles: ${SW_LOG_MAL_FILES:my-lal-mal-config, folder1/another-lal-mal-config, folder2/*} # 文件位于 "log-mal-rules" 目录下LAL 规则文件结构
一个 LAL 规则文件由rules列表组成,每条规则包含name、layer与dsl。以默认规则 default.yaml 为例:
rules: - name: default layer: GENERAL dsl: | filter { sink { } }这条规则的行为是"保存所有日志",其行为与 8.5.0 之前的版本一致——空sink即代表将日志全部持久化。可见 LAL 语法本身是嵌入在 YAML 的dsl多行字符串中的类 Groovy 脚本。
Layer:日志的分析范围声明
每条 LAL 规则都通过layer声明其日志分析范围。Layer 定义在 Layer.java 中,例如GENERAL、MYSQL、MESH等。layer 不仅用于路由日志到对应的规则,还会被抽取为 LogData 的一部分并与服务(service)关联。
Filter:处理流水线的编排单元
Filter 是 LAL 的核心编排单元,一个 filter 是 parser、extractor、sink 三者的组合。一条规则可以声明一个或多个 filter 来组织处理逻辑,每一条日志都会被发送到规则内所有 filter。在 filter 内部,日志以属性log的形式暴露,因此可以通过log.service访问日志所属服务名。log的完整字段集合以 Logging.proto 中的 LogData 协议定义为准。
filter 内所有组件严格按照声明顺序依次执行。
全局函数
以下函数在 parser、extractor、sink 等所有组件中都可以使用。
abort:快速失败机制
默认情况下,无论设置了dropped、saved等标志,所有已声明的组件都会被执行。但有些场景希望在满足特定条件时提前终止整个 filter 链。abort函数会从声明处中止剩余的 filter 链,其后的所有组件都不会再执行,是 LAL 中的快速失败(fast-fail)机制:
filter { if (log.service == "TestingService") { // 不为 TestingService 浪费资源 abort {} // 剩余组件全部不再执行 } // ... parsers, extractors, sinks }注意:当把regexp用在if语句中时,必须用()将表达式括起来,写成regexp(<表达式>)而不是regexp <表达式>。
tag:读取日志标签值
tag函数提供了一种便捷的方式来获取日志标签(tag)中指定 key 的值。假设上报的日志带有如下标签:
[ { "tags":{ "data":[ { "key":"TEST_KEY", "value":"TEST_VALUE" } ] }, "body":{ ... } ... } ]那么在 LAL 中即可这样读取TEST_KEY的值:
filter { if (tag("TEST_KEY") == "TEST_VALUE") { ... } }tag与parsed不同:parsed访问的是解析器从 body 中解析出的字段,而tag访问的是日志协议中tags部分的键值对(例如用于标识慢 SQL 的LOG_KIND标签)。
Parser:把原始日志解析为结构化数据
Parser 负责把原始日志解析为 SkyWalking 可进一步处理的结构化数据。目前共有 3 类解析器:json、yaml、text。
日志被解析后,LAL 会注入一个名为parsed的属性。parsed通常是一个 Map:
- 当解析器是
json/yaml时,parsed包含原始日志中的所有键值; - 当解析器是
text(使用regexp/grok)时,parsed包含所有捕获组及其值。
所有解析器共享以下选项:
| 选项 | 类型 | 描述 | 默认值 |
|---|---|---|---|
abortOnFailure | boolean | 解析/匹配失败时是否中止整个 filter 链 | true |
json解析器
filter { json { abortOnFailure true // 可省略,因为这是默认行为 } }yaml解析器
filter { yaml { abortOnFailure true // 可省略,因为这是默认行为 } }text解析器
对于非结构化日志,提供了若干text解析器。
regexp
regexp解析器使用正则表达式解析日志,利用正则的捕获组(captured groups),所有捕获组都可以在后续的 extractor 或 sink 中使用。regexp返回一个boolean表示日志是否匹配该模式:
filter { text { abortOnFailure true // 可省略,因为这是默认行为 // 这只是演示用的模式 regexp "(?<timestamp>\\d{8}) (?<thread>\\w+) (?<level>\\w+) (?<traceId>\\w+) (?<msg>.+)" } extractor { tag level: parsed.level // 添加一个名为 level 的标签,值为 regexp 捕获的 parsed.level traceId parsed.traceId // 从解析结果中抽取 trace id,用于将日志与链路关联 } // ... }grok(TODO)
官方已知悉 grok Java 库存在一定性能问题,目前正在进行调研与基准测试(benchmark),并欢迎社区贡献。
解析器的安全性设计
从实现上看,LAL 脚本由 Groovy 编译执行。在 DSL.java 中可以看到:
- 通过
SecureASTCustomizer禁止了while、do-while、for等循环语句,避免恶意或失控脚本造成无限循环; - 通过白名单限制了允许的接收者类型(
Object、Map、List、Array、String、ProcessRegistry等); - 使用
CompileStatic进行静态编译,并预导入了ProcessRegistry类(供sampledTrace生成虚拟进程 ID 使用)。
这意味着 LAL 是一门受限的、安全的脚本子集,你无法在规则中写任意 Java 代码或循环,只能使用 DSL 提供的组件与函数。
Extractor:从日志中抽取元数据
Extractor 的目的是从日志中抽取元数据,包括服务名、服务实例名、端点名,甚至 trace ID,这些元数据可用于与现有链路和指标关联。下面逐一介绍。
service
从parsed结果中抽取服务名,写入LogData。该值会被持久化(如果未被 drop),并用于关联链路 / 指标。
instance
从parsed结果中抽取服务实例名,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。
endpoint
从parsed结果中抽取端点名,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。
traceId
从parsed结果中抽取 trace ID,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。
segmentId
从parsed结果中抽取 segment ID,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。
spanId
从parsed结果中抽取 span ID,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。
timestamp
从parsed结果中抽取时间戳,写入LogData。会被持久化(如果未被 drop),并用于关联链路 / 指标。timestamp的参数可以是毫秒值:
filter { // ... parser extractor { timestamp parsed.time as String } }也可以是带指定格式的日期时间字符串:
filter { // ... parser extractor { timestamp parsed.time as String, "yyyy-MM-dd HH:mm:ss" } }layer
从parsed结果中抽取 layer,写入LogData。会被持久化(如果未被 drop),并用于关联服务。
tag
从parsed结果中抽取标签并写入LogData。形式为tag key1: value, key2: value2,key 和 value 都可以使用parsed的属性:
import javax.swing.text.LayeredHighlighter filter { // ... parser extractor { tag level: parsed.level, (parsed.statusCode): parsed.statusMsg tag anotherKey: "anotherConstantValue" layer 'GENERAL' } }metrics:日志转指标
metrics从日志中抽取 / 生成指标,并发送到 Meter 系统。可以配置 MAL 对这些指标做进一步分析。专用的 MAL 配置文件位于log-mal-rules目录,通过log-analyzer/default/malFiles启用(见前文 application.yml 示例)。
一个生成两类指标的 extractor 示例如下:
filter { // ... extractor { service parsed.serviceName metrics { name "log_count" timestamp parsed.timestamp labels level: parsed.level, service: parsed.service, instance: parsed.instance value 1 } metrics { name "http_response_time" timestamp parsed.timestamp labels status_code: parsed.statusCode, service: parsed.service, instance: parsed.instance value parsed.duration } } // ... }上面的 extractor 生成了一个名为log_count的指标,带标签 keylevel、值为1。随后可以在 MAL 规则中按日志级别聚合统计日志数量:
# ... MAL 的其他配置 metrics: - name: log_count_debug exp: log_count.tagEqual('level', 'DEBUG').sum(['service', 'instance']).increase('PT1M') - name: log_count_error exp: log_count.tagEqual('level', 'ERROR').sum(['service', 'instance']).increase('PT1M')生成的另一个指标是http_response_time,可以配置 MAL 规则生成百分位数等更有价值的指标:
# ... MAL 的其他配置 metrics: - name: response_time_percentile exp: http_response_time.sum(['le', 'service', 'instance']).increase('PT5M').histogram().histogram_percentile([50,70,90,99])slowSql:日志转慢 SQL 语句
slowSql用于将 LogData 转换为 DatabaseSlowStatement:从parsed结果中抽取数据并保存为慢 SQL 语句记录。slowSql不会中止或修改日志,你可以使用其他 LAL 规则做进一步处理。它会复用 extractor 中设置的service、layer和timestamp,因此必须在这三者设置之后使用slowSql。
OAP 依赖日志标签"LOG_KIND" = "SLOW_SQL"来区分慢 SQL 日志与其他日志上报。
注意:慢 SQL 采样只会把该 SQL 标记进候选列表。OAP 会按服务维度做统计,默认每 10 分钟(由
topNReportPeriod: ${SW_CORE_TOPN_REPORT_PERIOD:10}控制)只持久化前 50 条。
上报到 OAP 的 JSON 示例:
[ { "tags":{ "data":[ { "key":"LOG_KIND", "value":"SLOW_SQL" } ] }, "layer":"MYSQL", "body":{ "json":{ "json":"{\"time\":\"1663063011\",\"id\":\"cb92c1a5b-2691e-fb2f-457a-9c72a392d9ed\",\"service\":\"root[root]@[localhost]\",\"statement\":\"select sleep(2);\",\"layer\":\"MYSQL\",\"query_time\":2000}" } }, "service":"root[root]@[localhost]" } ]statement
从parsed结果中抽取 SQL 语句,写入DatabaseSlowStatement。会被持久化(如果未被 drop),用于关联 TopNDatabaseStatement。
latency
从parsed结果中抽取延迟,写入DatabaseSlowStatement。会被持久化(如果未被 drop),用于关联 TopNDatabaseStatement。
id
从parsed结果中抽取 id,写入DatabaseSlowStatement。会被持久化(如果未被 drop),用于关联 TopNDatabaseStatement。
识别慢日志的完整 LAL 示例:
filter { json{ } extractor{ layer parsed.layer as String service parsed.service as String timestamp parsed.time as String if (tag("LOG_KIND") == "SLOW_SQL") { slowSql { id parsed.id as String statement parsed.statement as String latency parsed.query_time as Long } } } }这条规则与发行包内置的 mysql-slowsql.yaml 完全一致,是生产环境验证过的真实模板。类似的模板还有pgsql-slowsql.yaml、redis-slowsql.yaml,可分别用于 PostgreSQL 与 Redis 的慢语句分析。
sampledTrace:日志转采样链路记录
sampledTrace用于将 LogData 转换为 SampledTraceRecord:从parsed结果中抽取数据并保存为采样链路记录。sampledTrace不会中止或修改日志,可以使用其他 LAL 规则继续处理。
OAP 依赖日志标签"LOG_KIND" = "NET_PROFILING_SAMPLED_TRACE"来区分采样慢链路日志与其他日志上报。
上报到 OAP 的 JSON 示例:
[ { "tags":{ "data":[ { "key":"LOG_KIND", "value":"NET_PROFILING_SAMPLED_TRACE" } ] }, "layer":"MESH", "body":{ "json":{ "json":"{\"uri\":\"/provider\",\"reason\":\"slow\",\"latency\":2048,\"client_process\":{\"process_id\":\"c1519f4555ec11eda8df0242ac1d0002\",\"local\":false,\"address\":\"\"},\"server_process\":{\"process_id\":\"\",\"local\":false,\"address\":\"172.31.0.3:443\"},\"detect_point\":\"client\",\"component\":\"http\",\"ssl\":true}" } }, "service":"test-service", "serviceInstance":"test-service-instance", "timestamp": 1666916962406, } ]处理该日志的 LAL 示例:
filter { json { } if (tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE") { sampledTrace { latency parsed.latency as Long uri parsed.uri as String reason parsed.reason as String if (parsed.client_process.process_id as String != "") { processId parsed.client_process.process_id as String } else if (parsed.client_process.local as Boolean) { processId ProcessRegistry.generateVirtualLocalProcess(parsed.service as String, parsed.serviceInstance as String) as String } else { processId ProcessRegistry.generateVirtualRemoteProcess(parsed.service as String, parsed.serviceInstance as String, parsed.client_process.address as String) as String } if (parsed.server_process.process_id as String != "") { destProcessId parsed.server_process.process_id as String } else if (parsed.server_process.local as Boolean) { destProcessId ProcessRegistry.generateVirtualLocalProcess(parsed.service as String, parsed.serviceInstance as String) as String } else { destProcessId ProcessRegistry.generateVirtualRemoteProcess(parsed.service as String, parsed.serviceInstance as String, parsed.server_process.address as String) as String } detectPoint parsed.detect_point as String if (parsed.component as String == "http" && parsed.ssl as Boolean) { componentId 129 } else if (parsed.component as String == "http") { componentId 49 } else if (parsed.ssl as Boolean) { componentId 130 } else { componentId 110 } } } }注意这里的ProcessRegistry就是 DSL.java 中通过 ImportCustomizer 预导入的类,用于为缺少进程 ID 的本地 / 远程端点生成虚拟进程 ID,从而把采样到的网络链路关联到虚拟进程上。
Sink:决定日志的持久化方式
Sink 是 LAL 的持久化层。默认情况下,每个 filter 处理的日志都会被持久化到存储中。但有些机制允许你选择性地保存部分日志,甚至在抽取完有用信息(如指标)后丢弃全部日志。
Sampler:按采样策略保存
Sampler 允许以采样方式保存日志。目前支持以下采样策略:
rateLimit:以每分钟最多n条的速度采样。rateLimit("SamplerID")需要为 sampler 指定一个 ID,拥有相同 ID 的 sampler 声明共享同一个 sampler 实例,从而共享相同的rpm与重置逻辑;possibility:每条日志有percentage的伪概率被采样,该概率由 Java 随机数生成器生成并与给定的percentage比较得出。
如果指定了多个 sampler,最后一个 sampler 决定最终的采样结果。官方欢迎社区贡献更多采样策略。
示例 1,rateLimit:
filter { // ... parser sink { sampler { if (parsed.service == "ImportantApp") { rateLimit("ImportantAppSampler") { rpm 1800 // 服务 "ImportantApp" 每分钟采样 1800 条日志 } } else { rateLimit("OtherSampler") { rpm 180 // 其他服务每分钟采样 180 条日志 } } } } }示例 2,possibility:
filter { // ... parser sink { sampler { if (parsed.service == "ImportantApp") { possibility(80) { // 服务 "ImportantApp" 采样 80% 的日志 } } else { possibility(30) { // 其他服务采样 30% 的日志 } } } } }Dropper:丢弃全部日志
Dropper 是一种特殊的 sink,表示无条件丢弃所有日志。这在你想丢弃调试日志时非常有用:
filter { // ... parser sink { if (parsed.level == "DEBUG") { dropper {} } else { sampler { // ... 采样配置 } } } }或者当你有多个 filter、其中一些只用于抽取指标时,可以只让其中一个 filter 持久化日志:
filter { // filter A:负责持久化 // ... parser sink { sampler { // .. sampler 配置 } } } filter { // filter B:负责生成大量指标 // ... extractors to generate many metrics extractors { metrics { // ... 指标 } } sink { dropper {} // 丢弃所有日志,因为它们已在 "filter A" 中被保存 } }Enforcer:强制采样
Enforcer 是另一种特殊的 sink,用于强制采样日志。典型场景是:已经配置了 sampler,但仍希望某些日志被强制保存,例如即使配置了采样机制,也一定要保存错误日志:
filter { // ... parser sink { sampler { // ... sampler 配置 } if (parsed.level == "ERROR" || parsed.userId == "TestingUserId") { // 即使配置了采样策略,也强制采样错误日志或测试用户(userId == "TestingUserId")的日志 enforcer { } } } }一个完整的实战组合示例
将以上组件串联起来,一个典型的"解析结构化日志 → 抽取链路上下文 → 生成指标 → 分级采样"的规则如下:
rules: - name: my-app-log layer: GENERAL dsl: | filter { json { abortOnFailure true } extractor { service parsed.service instance parsed.instance endpoint parsed.endpoint traceId parsed.traceId timestamp parsed.timestamp as String, "yyyy-MM-dd HH:mm:ss.SSS" tag level: parsed.level metrics { name "log_count" timestamp parsed.timestamp labels level: parsed.level, service: parsed.service, instance: parsed.instance value 1 } } sink { sampler { if (parsed.level == "DEBUG") { possibility(10) } else { rateLimit("normal") { rpm 600 } } } if (parsed.level == "ERROR") { enforcer {} } } }这段规则实现了:JSON 解析失败即中止链路;抽取服务、实例、端点与 traceId 实现日志与链路关联;按级别统计日志量送入指标系统;DEBUG 日志只采样 10%、普通日志每分钟限速 600 条、ERROR 日志强制全量保存。
总结与参考
LAL 把"解析、抽取、保存、联动"四件事统一到了一门安全受限的 DSL 中,配合 YAML 规则文件即可完成精细化的日志治理,无需为每种日志格式编写独立的 Java 处理代码。实践中建议:
- 将不同业务、不同 layer 的日志拆分为独立规则文件,通过
lalFiles逗号分隔加载; - 对仅用于指标抽取的 filter 使用
dropper,避免日志重复存储; - 对高吞吐日志务必配置 sampler,并结合
enforcer保留错误日志等关键数据; - 需要将日志关联到现有链路时,优先抽取
traceId、segmentId、spanId三个字段。
更深入的内容可继续阅读:
- MAL 指标分析语言:docs/en/concepts-and-designs/mal.md
- log-analyzer 模块配置说明:docs/en/setup/backend/log-analyzer.md
- 配置词汇表:docs/en/setup/backend/configuration-vocabulary.md
- LAL 语法实现:oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/dsl/DSL.java
- 内置 LAL 规则:oap-server/server-starter/src/main/resources/lal
【免费下载链接】skywalkingAPM, Application Performance Monitoring System项目地址: https://gitcode.com/gh_mirrors/sky/skywalking
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考