Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用指南

WarpParse 核心概念速查

本文档帮助零基础用户快速理解 WarpParse 的核心概念和术语。

什么是 ETL?

ETL 是 Extract(提取)、Transform(转换)、Load(加载)的缩写。简单来说:

  • Extract(提取):从日志文件、数据库、消息队列等地方读取原始数据
  • Transform(转换):把非结构化的文本转换成结构化的数据(如 JSON)
  • Load(加载):把处理好的数据存储到目标位置(文件、数据库、ES 等)

类比理解

就像一个智能快递分拣系统:

  • Extract = 收件(从各个地方收集包裹)
  • Transform = 分类(按地址、类型分类打标签)
  • Load = 派送(送到对应的目的地)

WarpParse 核心术语

术语英文通俗解释类比
输入源Source数据从哪里来水源(文件、TCP、Kafka)
输出源Sink数据到哪里去水池(文件、数据库、ES)
连接器Connector如何连接数据源/目标水管接头(配置连接参数)
WPLWarp Processing Language数据提取规则语言筛子(定义如何从文本中提取字段)
OMLObject Modeling Language数据组装规则语言模具(定义如何组装成目标格式)
规则Rule一条解析规则一个筛子的设计图纸
字段Field要提取的数据项筛子上的一个孔
管道Pipe数据处理流程水管(数据流动的路径)

数据流转过程

原始日志 → [Source 读取] → [WPL 解析] → [OML 转换] → [Sink 输出] → 目标存储
   ↓              ↓              ↓              ↓              ↓
文件/TCP      connectors     提取字段       组装对象      文件/DB/ES

详细说明

  1. Source 读取:从配置的数据源读取原始数据

    • 文件:读取日志文件
    • TCP:监听网络端口接收数据
    • Kafka:从消息队列消费数据
  2. WPL 解析:使用 WPL 规则从原始文本中提取字段

    • 识别 IP 地址、时间、数字等类型
    • 解析 JSON、KV 等结构化数据
    • 验证和过滤字段
  3. OML 转换:使用 OML 规则组装输出格式

    • 字段重命名和映射
    • 类型转换(字符串→数字→时间)
    • 条件判断和计算
  4. Sink 输出:将处理后的数据写入目标

    • 文件:保存为 JSON/CSV 等格式
    • 数据库:写入 MySQL/PostgreSQL
    • 搜索引擎:写入 Elasticsearch

快速入门路径

阶段一:
1、跑通 Getting Started,看到数据流转起来
2、学习 WPL 基础类型(ip、digit、time、chars)
3、学习 OML 数据组装
4、在 editor 学习了解 demo 样例

阶段二:
1、在 editor 上开始完成 T1 题目
2、在自己空间下创建 wpl和oml
3、配置 Source 和 Sink
4、使用 wpgen 生成样例数据,wparse 进行执行查看输出


项目目录结构

工作目录/
├── conf/              # 配置文件
│   ├── wparse.toml   # 解析引擎配置
│   └── wpgen.toml    # 数据生成器配置
│
├── connectors/        # 连接器定义
│   ├── sink.d/       # 输出连接器(数据去哪里)
│   └── source.d/     # 输入连接器(数据从哪来)
│
├── data/              # 数据目录
│   ├── in_dat/       # 输入数据(原始日志)
│   │   └── gen.dat  # 生成的测试数据
│   ├── out_dat/      # 输出数据(解析结果)
│   │   ├── demo.json       # 所有成功解析的记录
│   │   ├── default.dat     # 命中wpl未命中oml异常数据
│   │   ├── error.dat       # 异常数据
│   │   ├── miss.dat        # 未命中wpl异常数据
│   │   ├── monitor.dat     # 监控数据
│   │   └── residue.dat     # 残留数据(部分解析成功)
│   ├── logs/         # 运行日志
│   └── rescue/       # 失败数据(用于排查问题)
│
├── models/            # 模型定义
│   ├── wpl/          # WPL 解析规则
│   │   └── wp-space/
│   │       ├── parse.wpl    # 解析规则文件
│   │       └── sample.dat   # 规则测试样本
│   ├── oml/          # OML 转换规则
│   │   └── wp-space/
│   │       └── adm.oml      # 对象映射规则
│   └── knowledge/    # 知识库(SQL 查询等)
│
└── topology/          # 拓扑配置
    ├── sources/      # 数据源配置
    └── sinks/        # 数据目标配置

目录说明

  • conf/:全局配置,如日志级别、性能参数
  • connectors/:定义如何连接数据源和目标
  • data/:所有数据文件的存放位置
    • in_dat/:存放原始输入数据
      • gen.dat:wpgen 生成的测试数据
    • out_dat/:存放解析后的输出数据
      • all.json:所有成功解析的记录(JSON 格式)
      • default.dat:默认输出文件
      • error.dat:解析过程中出错的记录
      • ignore.dat:根据规则被忽略的记录
      • miss.dat:缺失必需字段的记录
      • monitor.dat:监控和统计数据
      • residue.dat:部分字段解析成功的残留数据
    • logs/:运行时日志,用于调试和排查问题
    • rescue/:解析失败的原始数据,用于重新处理
  • models/:核心业务逻辑(WPL 规则和 OML 规则)
    • wpl/:WPL 解析规则,按命名空间组织
      • wp-space/parse.wpl:具体的解析规则文件
      • wp-space/sample.dat:规则对应的测试样本
    • oml/:OML 转换规则,按命名空间组织
      • wp-space/adm.oml:对象映射和转换规则
    • knowledge/:知识库,如 SQL 查询、IP 库等
  • topology/:数据流拓扑配置

常用命令速查

命令功能示例
wproj init初始化项目wproj init --mode full
wproj check检查配置wproj check
wproj data stat统计数据wproj data stat
wpgen sample生成测试数据wpgen sample -n 3000
wparse batch批处理模式wparse batch --stat 3 -p
wparse daemon守护进程模式wparse daemon --stat-print
wprescue batch恢复失败数据wprescue batch

下一步

阅读完本文档后,建议按以下顺序学习:

  1. 快速入门:10 分钟跑通第一个示例
  2. WPL 语言基础:学习数据提取规则
  3. OML 语言基础:学习数据组装规则

相关文档

CLI 工具集

本文档集合介绍 WarpParse提供的完整命令行工具集,包括数据解析、生成、项目管理等功能。

工具总览

核心工具

工具功能描述主要用途
wparse数据解析引擎实时数据流处理、批处理分析
wpgen数据生成器基于规则或样本生成测试数据
wproj项目管理工具项目初始化、配置管理、数据统计
wprescue数据恢复工具从救援目录恢复处理失败的数据

快速参考

wparse - 数据解析引擎

# 守护进程模式(持续运行)
wparse daemon --work-root ./myproject --stat-print

# 批处理模式
wparse batch --work-root ./myproject --max-line 10000 --stat 5

wpgen - 数据生成器

# 基于规则生成数据
wpgen rule --work-root ./myproject --print-stat --line-cnt 10000

# 基于样本生成数据
wpgen sample --work-root ./myproject --line-cnt 5000

# 配置管理
wpgen conf init
wpgen conf check
wpgen conf clean

# 数据管理
wpgen data clean
wpgen data check

wproj - 项目管理工具

# 项目初始化
wproj init --mode full

# 项目检查
wproj check

# 数据清理
wproj data clean

# 统计功能
wproj stat file
wproj stat file --output json

# 模型管理
wproj model list
wproj model validate

# 规则工具
wproj rule parse --rule-id myrule
wproj rule test --input sample.log

# 配置管理
wproj sinks list
wproj sinks validate
wproj sinks route

wprescue - 数据恢复工具

# 批处理模式恢复数据
wprescue batch --work-root ./myproject

工具关系图

graph TD
    A[wproj] --> B[初始化项目]
    A --> C[检查配置]
    A --> D[统计数据]

    B --> E[生成配置文件]
    B --> F[创建目录结构]

    C --> G[验证配置有效性]

    D --> H[源数据统计]
    D --> I[输出数据统计]

    J[wpgen] --> K[生成测试数据]
    J --> L[管理生成器配置]
    J --> M[管理输出数据]

    N[wparse] --> O[实时流处理]
    N --> P[批处理分析]
    N --> Q[性能调优]

    R[wprescue] --> S[数据恢复]
    R --> T[故障恢复]

学习路径

  1. 初学者:从 快速入门指南 开始,了解完整的配置和使用流程
  2. 进阶用户:深入学习 wparse 运行模式,理解两种运行模式的区别和适用场景
  3. 数据工程师:掌握 wpgen 使用指南,能够生成各种测试数据
  4. 运维人员:使用 wproj 项目管理 进行日常的项目管理和监控
  5. 故障处理:参考 wprescue 数据恢复 处理异常情况

常见使用场景

实时流处理

# 启动守护进程,持续处理数据流
wparse daemon \
  --work-root ./myproject \
  --stat-print \
  --robust online

批量数据分析

# 批处理分析历史数据
wparse batch \
  --work-root ./myproject \
  --max-line 100000 \
  --check-continue 1000 \
  --stat 10

开发测试

# 开发模式,详细日志
wparse batch \
  --work-root ./myproject \
  --log-profile dev \
  --max-line 100 \
  --stat 1

数据生成测试

# 生成特定场景的测试数据
wpgen rule \
  --work-root ./myproject \
  --conf-name test.toml \
  --line-cnt 10000 \
  --gen-speed 1000

故障排除

常见问题

  1. 配置文件找不到

    • 确保在正确的工作目录下运行命令
    • 使用 --work-root 参数指定工作目录
  2. 权限错误

    • 检查工作目录和日志目录的写权限
    • 确保有足够的磁盘空间
  3. 内存不足

    • 减少 --max-line--parse-workers 参数值
    • 使用 --robust 参数设置合适的鲁棒模式
  4. 数据源连接失败

    • 检查 connectors/source.d/ 目录下的连接器配置
    • 验证网络连接和认证信息

日志分析

# 查看实时日志
tail -f logs/wparse.log

# 搜索错误日志
grep -i error logs/*.log

# 使用 wproj 进行项目检查
wproj check --work-root ./myproject

版本信息

查看工具版本:

wparse --version
wpgen --version
wproj --version
wprescue --version

环境变量

变量名描述
WP_PARSE_ROBUST设置全局鲁棒模式
WP_PARSE_LOG_LEVEL覆盖日志级别
RUST_LOGRust 日志级别(调试用)

退出码

退出码含义
0成功
1通用错误
2配置错误
3数据错误
4网络错误
5权限错误

技术架构

CLI 工具采用 Rust 编写,使用 clap 框架处理命令行参数,tokio 异步运行时。核心功能通过 wp-engine 库的 facade 模式对外暴露,确保 API 稳定性和向后兼容性。

项目结构

warp-parse/
├── src/
│   ├── wparse/          # 数据解析引擎
│   │   └── main.rs
│   ├── wpgen/            # 数据生成器
│   │   ├── main.rs
│   │   ├── cli.rs       # CLI 定义
│   │   ├── rule.rs      # 规则生成逻辑
│   │   ├── sample.rs    # 样本生成逻辑
│   │   ├── conf.rs      # 配置管理
│   │   └── data.rs      # 数据管理
│   ├── wproj/            # 项目管理
│   │   └── main.rs
│   └── wprescue/         # 数据恢复
│       └── main.rs
└── wp-engine/            # 核心引擎库
    └── facade/args.rs    # 参数定义

这种设计确保了:

  1. 模块化:每个工具职责单一,易于维护
  2. 复用性:核心功能通过库共享
  3. 可扩展性:便于添加新的 CLI 工具

相关链接

GettingStarted

前置准备

  • 下载 wparse
  • copy到可执行路径下。如 /usr/local/bin 或 /${HOME}/bin

一、初始化工作目录

  • 清理并初始化配置与模板
    wproj init --mode full
    wproj check 
    

执行完成后,工作目录将包含:

├── conf
│   ├── wparse.toml
│   └── wpgen.toml
├── connectors
│   ├── sink.d
│   └── source.d
├── data
│   ├── in_dat
│   ├── logs
│   ├── out_dat
│   └── rescue
├── models
│   ├── knowledge
│   ├── oml
│   └── wpl
└── topology
    ├── sinks
    └── sources

二、生成数据与清理

wproj data clean 
wpgen data clean 

# 生成样本(示例 3000 行,3 秒统计间隔)
wpgen sample -n 3000 --stat 3

三、运行解析

# 批处理(-n 指定条数,-p 打印统计;失败时查看 ./logs/ 下日志)
wparse batch --stat 3 -p  

四、统计与校验

# 同时统计源与文件型 sink
wproj  data stat

Wproj

wproj 是 Warp Parse 项目管理工具,提供完整的项目生命周期管理功能,包括项目初始化和配置管理、数据源的检查和统计、模型管理和知识库创建维护。

命令概览

wproj <COMMAND>

Commands:
  rule   规则工具:解析规则的管理和调试 | Rule tools: management and debugging of parsing rules
  init   一键初始化完整工程骨架 | Initialize complete project skeleton
  check  批量检查项目配置和文件完整性 | Batch check project configuration and file integrity
  data   数据管理工具:清理、统计、验证 | Data management tools: cleanup, statistics, validation
  model  模型管理工具:规则、源、汇、知识库 | Model management tools: rules, sources, sinks, knowledge base

init - 项目初始化

一键创建项目目录结构和默认配置。

wproj init [OPTIONS]
参数短选项长选项默认值说明
mode-m--modeconf初始化模式

初始化模式:

模式说明
full完整项目(配置+模型+数据+示例+链接器)
normal完整项目(配置+模型+数据+示例)
model仅模型文件
conf仅配置文件
data仅数据目录

示例:

# 初始化配置(默认)
wproj init -w /project

# 初始化完整项目
wproj init -w /project --mode full

check - 项目检查

批量检查项目配置和文件完整性。

wproj check [OPTIONS]
参数短选项长选项默认值说明
work_root-w--work-root.根目录
what---whatall检查项
console---consolefalse控制台日志输出
fail_fast---fail-fastfalse首次失败即退出
json---jsonfalseJSON 格式输出
only_fail---only-failfalse仅输出失败项

检查项(–what):

说明
conf主配置文件
connectors连接器配置
sources数据源配置
sinks数据汇配置
wplWPL 规则语法
omlOML 模型语法
all全部检查(默认)

示例:

# 全面检查
wproj check -w /project --what all

# 仅检查配置和规则,首次失败即退出
wproj check -w /project --what conf,wpl --fail-fast

# JSON 输出,仅显示失败项
wproj check -w /project --json --only-fail

data - 数据管理

wproj data <SUBCOMMAND>

Subcommands:
  clean     清理本地输出文件
  check     检查数据源连通性
  stat      统计数据量和性能
  validate  验证数据分布和比例

data clean

清理项目输出数据。

wproj data clean

data stat

统计数据量。

wproj data stat 

data validate

验证数据分布和比例。

wproj data validate [OPTIONS]
参数短选项长选项默认值说明
input_cnt---input-cnt-输入总数(分母)

示例:

# 清理输出数据
wproj data clean 

# 统计源+SINK文件行数
wproj data stat 

# 验证数据分布
wproj data validate 

model - 模型管理

TODO!


rule - 规则工具

离线解析测试,验证 WPL 规则。

wproj rule parse [OPTIONS]

示例:

# 使用规则执行离线解析测试
wproj rule parse 

Wparse

运行模式

  • 两种运行模式:wparse daemon(常驻服务)和 wparse batch(批处理)
  • 批处理模式读完文件后自动退出,daemon 模式需信号触发退出

命令行参数

wparse <COMMAND>

Commands:
  daemon  守护进程模式(常驻服务)
  batch   批处理模式(读完即退)

通用参数

参数短选项长选项默认值说明
parse_workers-w--parse-workers-解析线程数
stat_sec---stat-统计输出间隔(秒)
stat_print-p--print_statfalse周期打印统计信息
wpl_dir---wpl-WPL 规则目录覆盖

使用示例

# 批处理模式:处理 3000 条后退出,每 2 秒输出统计
wparse batch -n 3000 --stat 2 -p

# 批处理模式:指定工作目录和多线程
wparse batch  -w 4 --parse-workers 4

# 守护进程模式:常驻服务,每 5 秒输出统计
wparse daemon --stat 5 -p

# 自定义日志和规则目录
wparse daemon --log-profile custom.toml --wpl /custom/rules

退出策略

批处理模式(batch)

单源(picker)结束条件(任一满足):

  • 上游 EOF(文件读取完毕)
  • 收到 Stop 指令
  • 致命错误(触发全局停机)

进程退出流程:

  1. 所有数据源的 picker 结束
  2. 主组完成
  3. sink/infra 组依序下线
  4. 进程退出

关键日志:

  • 每个源结束:数据源 '...' picker 正常结束
  • 全局收尾:all routine group await end!

守护进程模式(daemon)

  • 启动 acceptor(网络监听等)
  • 进程保持常驻运行
  • 退出触发方式:
    • SIGTERM/SIGINT/SIGQUIT 信号
    • 控制总线 Stop 指令(企业版)

错误与重试策略

错误类型策略说明
EOFTerminate优雅结束当前源
断线/可重试FixRetry指数退避后继续
数据/业务可容忍Tolerant记录后继续
致命错误Throw触发全局停机

常见问题

Q:为什么 batch 下不启动 acceptor?

A:acceptor 是常驻组件(监听网络),会阻塞主组完成。batch 目标是“源结束 → 主组完成 → 进程退出“。

Wpgen

wpgen 是 WarpParse 数据生成器,用于基于WPL规则或样本文件生成测试数据。

命令概览

wpgen <COMMAND>

Commands:
  rule    Generate data by rule/基于规则生成数据
  sample  Generate data from sample files/基于样本文件生成数据
  conf    Configuration commands/配置相关命令
  data    Data management commands/数据管理相关命令

子命令详解

rule - 基于规则生成

基于 WPL 规则生成测试数据,支持规则验证和性能分析。

wpgen rule [OPTIONS]
参数短选项长选项默认值说明
wpl_dir---wpl-WPL 规则目录覆盖
conf_name-c--confwpgen.toml配置文件名
stat_print-p--print_statfalse周期打印统计信息
line_cnt-n--总行数覆盖
speed-s--生成速度(行/秒)覆盖
stat_sec---stat1统计输出间隔(秒)

sample - 基于样本生成

基于样本文件(sample.dat)生成测试数据。

wpgen sample [OPTIONS]

conf - 配置管理

wpgen conf <SUBCOMMAND>

Subcommands:
  init   初始化生成器配置(conf/wpgen.toml)
  clean  清理生成器配置
  check  检查配置有效性

data - 数据管理

wpgen data <SUBCOMMAND>

Subcommands:
  clean  Clean generated output data according to wpgen config/根据 wpgen 配置清理已生成输出数据
  check  Not supported; reserved for future/暂不支持;保留供未来使用
参数短选项长选项默认值说明
conf_name-c--confwpgen.toml配置文件名

运行语义

count(总产出条数)

启动时按 parallel 精确均分到每个 worker,余数前置分配。各 worker 跑完本地任务量即退出,总量严格等于 count

speed(全局速率)

  • speed = 0:无限制(不等待)
  • speed > 0:每 worker 速率为 floor(speed / parallel)

parallel(并行数)

生成 worker 的并行数。对 blackhole_sink 消费端也会并行,其它 sink 默认单消费者。

使用示例

# 配置初始化
wpgen conf init -w .
wpgen conf check -w .

# 基于规则生成 10000 条数据
wpgen sample -n 10000 -p

# 自定义规则目录和生成速度
wpgen rule 
    --wpl nginx \
    -c custom.toml \
    -s 1000 \
    --stat 2 \
    -p

# 基于样本文件生成
wpgen sample  -n 50000 -s 5000 --stat 5 -p

# 清理生成的数据
wpgen data clean -c wpgen.toml --local

常见问题

Q:产出不足预期?

A:count 被精确分配给每个 worker。检查日志中 limit : … 是否符合预期。

配置文件

默认配置文件路径:conf/wpgen.toml

主要配置项:

[generator]
count = 10000      # 总生成条数
speed = 1000       # 生成速度(行/秒),0 表示无限制
parallel = 4       # 并行 worker 数

[output]
# 输出配置...

生成文件通常位于 ./data/in_dat/,可在配置中调整目标路径。

wprescue

wprescue 是数据恢复工具,用于从救援目录中恢复数据并按照项目配置的 Sink 路由输出到目标。

命令概览

wprescue 

重要: wprescue 仅支持 batch 模式。

命令行参数

wprescue [OPTIONS]

工作原理

  1. 读取救援目录(./data/rescue)中的数据
  2. 按照项目配置的 Sink 路由进行处理
  3. 输出到目标位置
  4. 处理完成后自动退出

配置指南

本文聚焦 wparse 运行所依赖的配置。建议从“运行主配置(wparse.toml)”开始,随后按需阅读源/汇与连接器章节。

推荐阅读顺序

相关参考

  • 参考参数与规格:docs/80-reference 下各 Source/Sink/Spec 文档
  • CLI:docs/cli/wparse.md(快速查看常用选项)

提示

  • 使用 wproj conf init --work-root . 可初始化标准目录与模板(conf/、connectors/ 与部分 models 目录)。若需要知识库(KnowDB)模板,请另行执行 wproj knowdb init
  • 修改场景流程后,建议运行 usecase/core/getting_started/case_verify.sh 验证端到端产出。

Wparse配置

完整示例(推荐默认)

version = "1.0"
robust  = "normal"           # debug|normal|strict

[models]
wpl     = "./models/wpl"
oml     = "./models/oml"

[topology]
sources = "./topology/sources"
sinks   = "./topology/sinks"

[performance]
rate_limit_rps = 10000        # 限速(records/second)
parse_workers  = 2            # 解析并发 worker 数

[rescue]
path = "./data/rescue"        

[log_conf]
output = "File"               # Console|File|Both
level  = "warn,ctrl=info"

[log_conf.file]
path = "./data/logs"          # 文件输出目录;文件名自动取可执行名(wparse.log)

[stat]

[[stat.pick]]                 # 采集阶段统计
key    = "pick_stat"
target = "*"

[[stat.parse]]                # 解析阶段统计
key    = "parse_stat"
target = "*"

[[stat.sink]]                 # 下游阶段统计
key    = "sink_stat"
target = "*"

Sources配置

概览

Source(源)是 warp-parse 系统中负责数据输入的组件,支持多种数据源和协议。采用统一的连接器架构,提供灵活的数据接入能力。

定位与目录

  • 配置文件$WORK_ROOT/topology/sources/wpsrc.toml
  • 连接器定义:从 $WORK_ROOT/models/sources 起向上查找最近的 connectors/source.d/*.toml

核心概念

  • 连接器:可复用的输入连接定义,包含 id/type/params/allow_override
  • 参数覆写:通过白名单机制安全覆写连接器参数
  • 标签系统:支持为数据源添加标签,便于路由和过滤

支持的 Source 类型

内置 Source

  • file:文件输入,支持监控和轮询
  • syslog:Syslog 协议输入(UDP/TCP)
  • tcp:协议输入

扩展 Source

  • kafka:Apache Kafka 消息队列输入

配置规则

基本规则

  • 仅支持 [[sources]] + connect/params 格式
  • 覆写键必须 ∈ connector allow_override 白名单;超出即报错
  • enable 字段控制是否启用(默认 true)
  • tags 字段支持添加数据源标签

配置结构

[[sources]]
key = "source_identifier"           # 源的唯一标识
connect = "connector_id"            # 引用的连接器 ID
enable = true                       # 是否启用(可选,默认 true)
tags = ["source:tag1", "type:log"]  # 标签(可选)
params = {                 # 参数覆写(可选)
    # 覆写连接器参数
}

配置示例

最小示例

[[sources]]
key = "file_1"
connect = "file_src"
params = { base = "data/in_dat", file = "gen.dat" }

文件输入示例

# models/sources/wpsrc.toml
[[sources]]
key = "access_log"
connect = "file_src"
params = {
    base = "./logs",
    file = "access.log",
    encode = "text"
}
tags = ["type:access", "env:prod"]

Syslog 输入示例


# models/sources/wpsrc.toml
[[sources]]
key = "syslog_udp"
connect = "syslog_udp_src"
params = {
    port = 1514,
    header_mode = "parse",
    prefer_newline = true
}
tags = ["protocol:syslog", "transport:udp"]

TCP 输入示例(通用 TCP 行/长度分帧)


# models/sources/wpsrc.toml
[[sources]]
key = "tcp_in"
connect = "tcp_src"
enable = true
params= {
  port = 19000,
  framing = "auto",
  prefer_newline = true
}

Sink 配置

目录与文件组织

  • sink_root:用例内通常为 <case>/sink

    • business.d/**/*.toml:业务组路由(场景输出,支持子目录)
    • infra.d/**/*.toml:基础组路由(default/miss/residue/intercept/error/monitor,支持子目录)
    • defaults.toml:默认组级期望 [defaults.expect]
  • connectors/sink.d/*.toml:连接器定义(loader 自 sink_root 向上查找最近的该目录)

路由文件格式

  • 顶层
    • version(可选)
    • sink_group
      • name:组名(字符串)
      • oml / rule:推荐扁平写法;均可为字符串或字符串数组;用于匹配模型或规则。
      • expect:可选,组级期望(覆盖 defaults)
      • sinks:数组,每项为单个 sink 定义
  • 单个 sink 字段
    • name:该 sink 的名称(组内唯一);未提供则按 [index] 回退
    • connect:引用连接器 id(兼容读取 use/connector
    • params:对连接器默认参数的白名单覆盖(keys 必须在连接器 allow_override 列表中)
    • expect:可选,单 sink 期望(仅 ratio/tol/min/max,互斥关系:ratio/tol 与 min/max 不可混用)
    • filter:可选,拦截条件文件路径;命中 true 时丢弃该 sink 并发送至 intercept

配置示例:

基础组

version = "2.0"
[sink_group]
name = "intercept"

[[sink_group.sinks]]
name = "intercept"
connect = "file_kv_sink"
params = { base = "./out", file = "intercept.dat" }

业务组(filter)

version = "2.0"

[sink_group]
name = "/sink/filter"
oml  = ["/oml/sh*"]

[[sink_group.sinks]]
name = "all"
connect = "file_kv_sink"
params = { base = "./out/sink", file = "all.dat" }

[[sink_group.sinks]]
name = "safe"
connect = "file_kv_sink"
filter = "./sink/business.d/filter.conf"   # 命中 -> 拦截,不写 safe
params = { base = "./out/sink", file = "safe.dat" }

说明

  • 标识规则
    • 组名:sink_group.name(例如 /sink/example/simple)
    • sink 名:name(组内唯一;未显式提供时按索引回退为 [0]/[1]/…)
  • 过滤语义(filter)
    • filter 是“拦截条件”:表达式求值为 true 时,该条数据不写入该 sink,而是转发到基础组 intercept(framework/intercept)
    • 每个 sink 可独立设置 filter;与 expect 相互独立

校验提示

  • 分母决定:
    • basis = total_input:总输入
    • basis = group_input:该组各 sink 行数之和(或 stats 中该组输入)
    • basis = model:按模型粒度统计(目前以组内 sinks 行数之和替代)
  • min_samples:当分母为 0 或小于该值时,组校验被忽略(打印提示,不 fail)
  • 当 route 为非文件类写入 fmt 时,validate 会提示“fmt 由后端决定,已忽略”。

常见排错

  • 连接器未找到:检查 connectors/sink.d 是否存在对应 id;wproj sinks list 可查看引用关系
  • 覆盖参数不生效:检查 allow_override 白名单
  • filter 未生效:
    • 路径解析相对当前工作目录(建议写相对 sink_root 的相对路径)
    • 日志中会打印“found path/not found filter …”
    • 表达式语法需通过 TCondParser;可先用简单表达式试验

Wpgen配置

wpgen 是数据生成工具,用于按照规则或样本生成测试数据。

基础配置

配置文件路径:conf/wpgen.toml

version = "1.0"

[generator]
mode = "sample"          # 生成模式:rule | sample
count = 1000             # 生成总条数(可选)
duration_secs = 60       # 生成持续时间(秒,可选,与 count 二选一)
speed = 1000             # 恒定速率(行/秒),0 为无限速
parallel = 1             # 并行度
rule_root = "./rules"    # 规则目录(mode=rule 时使用)
sample_pattern = "*.txt" # 样本文件匹配模式(mode=sample 时使用)

[output]
# 引用 connectors/sink.d 中的连接器 id
connect = "file_kv_sink"
name = "gen_out"
# 覆写连接器参数(仅 allow_override 白名单内的键)
params = { base = "./src_dat", file = "gen.dat" }

[logging]
level = "warn"
output = "file"
file_path = "./data/logs/"

动态速度模型

除了使用 speed 字段指定恒定速率外,还可以使用 speed_profile 配置动态速度变化模型。 当 speed_profile 存在时,speed 字段将被忽略。

恒定速率 (constant)

固定速率生成数据。

[generator.speed_profile]
type = "constant"
rate = 5000              # 每秒生成行数

正弦波动 (sinusoidal)

速率按正弦曲线周期性波动,模拟周期性负载变化。

[generator.speed_profile]
type = "sinusoidal"
base = 5000              # 基准速率(行/秒)
amplitude = 2000         # 波动幅度(行/秒)
period_secs = 60.0       # 周期长度(秒)

速率范围:[base - amplitude, base + amplitude],即上例中为 3000-7000 行/秒。

阶梯变化 (stepped)

速率按预定义的阶梯序列变化,适合模拟分阶段负载测试。

[generator.speed_profile]
type = "stepped"
# 格式:[[持续时间(秒), 速率], ...]
steps = [
    [30.0, 1000],        # 前 30 秒:1000 行/秒
    [30.0, 5000],        # 接下来 30 秒:5000 行/秒
    [30.0, 2000]         # 最后 30 秒:2000 行/秒
]
loop_forever = true      # 是否循环执行(默认 false)

突发模式 (burst)

在基准速率上随机触发高速突发,模拟突发流量场景。

[generator.speed_profile]
type = "burst"
base = 1000              # 基准速率(行/秒)
burst_rate = 10000       # 突发时速率(行/秒)
burst_duration_ms = 500  # 突发持续时间(毫秒)
burst_probability = 0.05 # 每秒触发突发的概率(0.0-1.0)

渐进模式 (ramp)

速率从起始值线性变化到目标值,适合压力递增测试。

[generator.speed_profile]
type = "ramp"
start = 100              # 起始速率(行/秒)
end = 10000              # 目标速率(行/秒)
duration_secs = 300.0    # 变化持续时间(秒)

达到目标速率后将保持该速率。支持正向(递增)和反向(递减)。

随机波动 (random_walk)

速率在基准值附近随机波动,模拟不规则负载。

[generator.speed_profile]
type = "random_walk"
base = 5000              # 基准速率(行/秒)
variance = 0.3           # 波动范围(0.0-1.0),0.3 表示 ±30%

速率范围:[base * (1 - variance), base * (1 + variance)]

复合模式 (composite)

组合多个速度模型,支持多种组合方式。

[generator.speed_profile]
type = "composite"
combine_mode = "average" # 组合方式:average | max | min | sum

# 子模型列表
[[generator.speed_profile.profiles]]
type = "sinusoidal"
base = 5000
amplitude = 2000
period_secs = 60.0

[[generator.speed_profile.profiles]]
type = "random_walk"
base = 5000
variance = 0.1

组合方式说明:

  • average:取所有子模型速率的平均值(默认)
  • max:取所有子模型速率的最大值
  • min:取所有子模型速率的最小值
  • sum:累加所有子模型速率

配置示例

示例 1:简单恒定速率

version = "1.0"

[generator]
mode = "sample"
count = 10000
speed = 5000
parallel = 2

[output]
connect = "file_json_sink"
params = { base = "./data", file = "output.dat" }

[logging]
level = "info"
output = "file"
file_path = "./logs"

示例 2:渐进压力测试

version = "1.0"

[generator]
mode = "rule"
duration_secs = 600      # 运行 10 分钟
parallel = 4
rule_root = "./rules"

[generator.speed_profile]
type = "ramp"
start = 100
end = 20000
duration_secs = 300.0    # 5 分钟内从 100 提升到 20000

[output]
connect = "kafka_sink"
params = { topic = "test-topic" }

[logging]
level = "warn"
output = "file"
file_path = "./logs"

示例 3:模拟真实业务负载

version = "1.0"

[generator]
mode = "sample"
duration_secs = 3600     # 运行 1 小时
parallel = 8

[generator.speed_profile]
type = "composite"
combine_mode = "average"

# 基础周期性波动(模拟日间/夜间流量差异)
[[generator.speed_profile.profiles]]
type = "sinusoidal"
base = 10000
amplitude = 5000
period_secs = 300.0

# 叠加随机噪声
[[generator.speed_profile.profiles]]
type = "random_walk"
base = 10000
variance = 0.15

[output]
connect = "tcp_sink"
params = { host = "127.0.0.1", port = 9000 }

[logging]
level = "info"
output = "both"
file_path = "./logs"

运行规则

  • wpgen 会在加载 conf/wpgen.toml 时,若检测到 [output].connect

    • ENGINE_CONF.sink_root 向上查找最近的 connectors/sink.d/ 目录
    • 读取目标连接器并与 params 合并(仅允许 allow_override 中的键)
  • 当配置了 parallel > 1 时,速度模型会自动按并行度分配,确保总速率符合预期

  • countduration_secs 二选一:

    • 设置 count 时,生成指定条数后停止
    • 设置 duration_secs 时,运行指定秒数后停止
    • 两者都未设置时,将持续运行直到手动停止

KnowDB 配置

本指南描述知识库(KnowDB)的目录式配置与装载规范。

适用范围

  • 初始化权威库(CSV → SQLite),用于 wparse/wproj 等工具在启动时装载

核心原则

  • SQL 外置:每张表的 DDL/DML 均放在对应目录下的 .sql 文件中
  • 安全:运行期只允许访问配置里声明过的表名;SQL 仅支持 {table} 占位符
  • 默认可用:多数字段可省略,内置默认值与自动探测能满足常见场景

目录布局(推荐)

models/knowledge/
  knowdb.toml                     # 本配置
  example/
    create.sql
    insert.sql
    data.csv                     # 单一数据文件(表目录根)
  address/
    create.sql
    insert.sql
    data.csv

顶层配置(models/knowledge/knowdb.toml)

version = 2

[[tables]]
name = "example"
# dir 省略时等于 name;此示例即使用目录 models/knowledge/example
# data_file 省略时使用表目录下的 data.csv
columns.by_header = ["name", "pinying"]

# 如需更多表,追加 [[tables]] 段落

SQL 文件规范

  • create.sql:建表语句,必须存在;可使用占位符 {table};允许包含多条语句(如 CREATE INDEX
  • insert.sql:插入语句,必须存在;参数位置用 ?1..?N;允许 {table}
  • clean.sql:可选;若不存在,装载前默认执行 DELETE FROM {table}

列映射(columns)

  • 推荐 by_header=[..],按 CSV 表头名映射到 insert.sql 中的列
  • has_header=false,必须提供 by_index=[..]
  • 可选增强(实现层):若未配置 columns,且 insert.sql 显式了列清单,可解析 insert 的列名作为 by_header

装载策略(默认可省略)

  • 默认:transaction=truebatch_size=2000on_error="fail"
  • on_error:
    • fail:遇到坏行(缺列/解析失败)即失败回滚
    • skip:跳过坏行并计数告警

自动探测(当 data_file 未配置)

  • 使用 {base_dir}/{tables.dir}/data.csv
  • 不存在则报错

安全约束

  • 运行时(facade/query_cipher/SQL 评估)仅允许使用 [[tables]].name 中声明的表名
  • SQL 模板仅允许 {table} 占位符;禁止其它动态拼接

最小可运行示例

  1. 目录
models/knowledge/knowdb.toml
models/knowledge/example/{create.sql, insert.sql, data.csv}
  1. create.sql
CREATE TABLE IF NOT EXISTS {table} (
  id      INTEGER PRIMARY KEY,
  name    TEXT NOT NULL,
  pinying TEXT NOT NULL
);
  1. insert.sql
INSERT INTO {table} (name, pinying) VALUES (?1, ?2);
  1. data.csv
name,pinying
令狐冲,linghuchong
任盈盈,renyingying
  1. knowdb.toml(最小化)
version = 2
base_dir = "./models/knowledge"
[[tables]]
name = "example"
dir  = "example"
columns.by_header = ["name", "pinying"]
[tables.expected_rows]
min = 1
max = 100

常见错误与排障

  • 缺少 create.sql / insert.sql:启动时失败并指向缺失文件
  • has_header=false 但未提供 by_index:装载报错
  • expected_rows.min 未满足:数据不足,装载失败
  • 数据源未找到:既未配置 data_file,也不存在默认路径 data.csv
  • 运行期 SQL 访问未声明的表:安全校验失败

与应用的关系

  • wparse/wproj 等会在启动处加载 knowdb:创建权威库并设置 Query Provider
  • 曾用于隐私模块的 query_cipher(table)(加载单列表词表)在当前版本默认不启用;如需脱敏请在业务侧实现

内置 SQL 函数(UDF)

  • 运行时注册:
    • 导入阶段(权威库写连接)与查询阶段(线程克隆的只读连接)均自动注册。
    • 可在 INSERT/SELECT/WHERE 中直接使用(DDL 不涉及)。
  • 签名与语义:
    • ip4_int(text) -> integer:点分 IPv4 转 32 位整数;容忍空白/引号;非法返回 0
    • ip4_between(ip_text, start_text, end_text) -> integer:是否在闭区间 [start,end] 内(1/0)。
    • cidr4_min(text) -> integer:CIDR 起始地址(含),如 10.0.0.0/8
    • cidr4_max(text) -> integer:CIDR 结束地址(含)。
    • cidr4_contains(ip_text, cidr_text) -> integer:IP 是否落在 CIDR 段内(1/0)。
    • ip4_text(integer|string) -> text:32 位整数转点分 IPv4(便于调试/展示)。
    • trim_quotes(text) -> text:去除两端成对引号(’ 或 “),容忍前后空白;未成对则原样返回(去掉空白)。
  • 导入示例(insert.sql):
    INSERT INTO {table} (ip_start_int, ip_end_int, zone)
    VALUES (ip4_int(?1), ip4_int(?2), trim_quotes(?3));
    
  • 查询示例(普通 SQL):
    -- 区间命中(推荐整数比较写法,避免在 WHERE 中直接比较函数返回)
    SELECT zone FROM zone
    WHERE ip_start_int <= ip4_int(:ip)
      AND ip_end_int   >= ip4_int(:ip)
    LIMIT 1;
    
    -- CIDR 命中
    SELECT zone FROM zone
    WHERE cidr4_contains(:ip, :cidr) = 1;
    
    -- 调试回显
    SELECT ip4_text(ip_start_int) AS ip_start, ip4_text(ip_end_int) AS ip_end, zone
    FROM zone
    LIMIT 5;
    
  • OML 中的 SQL 精确求值:
    • OML 的 select … from … where …; 语法对列段做了标识符白名单限制,不建议在列段直接写函数。
    • 推荐在上游产出数值型 IP(如 src_ip_int),在 OML 的 where 中用整数比较:
      from_zone: chars = sql(
        select zone from zone
        where ip_start_int <= read(src_ip_int)
          and ip_end_int   >= read(src_ip_int);
      )
      
  • 注意事项:
    • 目前非法 IPv4/CIDR 输入返回 0(或匹配失败),为提高导入韧性;如需严格行为可定制。
    • SQLite 原生已提供 lower/upper/trim 等字符串函数,可与上述 UDF 组合使用。

日志配置

本文面向使用者与一线开发,给出在 Warp Parse 中开启/调整日志与进行常见问题定位的实操指南。

  • 目标域(targets)见《开发者文档:Logging Targets And Levels》。
  • 文件日志默认滚动:10MB/10 份,gzip 压缩。

快速上手

wparse / wprescue (conf/wparse.toml)

生产推荐(文件输出 + 低噪声):

[log_conf]
output = "File"   # Console|File|Both
level  = "warn,ctrl=info,dfx=info"
[log_conf.file]
path = "./data/logs"   # 目录会自动创建;文件名按可执行名自动命名,如 wparse.log

本地联调(同时输出到控制台):

[log_conf]
output = "Both"
level  = "debug"
[log_conf.file]
path = "./data/logs"

WPL 规则语言

WPL (Warp Processing Language) 是 Warp Parse 解析子系统(warp-parse)使用的规则语言,用于描述字段抽取、协议解析与简单判定逻辑。


📚 文档导航

按学习路径

🆕 新手入门
   ↓
01-quickstart.md ────→ 5分钟上手,复制即用
   ↓
07-complete-types-examplemd ──→ 🌟 完整功能演示(强烈推荐)
   ↓
02-core-concepts.md ──→ 理解设计理念和核心概念
   ↓
03-practical-guide.md → 按任务查找解决方案
   ↓
04-language-reference.md → 查阅类型和语法
   ↓
05-functions-reference.md → 查阅函数

按用户角色

我是…推荐阅读
WPL 新手01-quickstart.md02-core-concepts.md
日常使用者03-practical-guide.md - 按任务查找
开发者/集成04-language-reference.md + 05-functions-reference.md
编译器开发06-grammar-reference.md - EBNF 语法

按任务查找

我想…查看文档
🚀 快速上手01-quickstart.md
🎯 查看完整类型示例07-complete-types-example.md
💡 理解概念02-core-concepts.md
📝 解析 Nginx 日志03-practical-guide.md § 1
📊 解析 JSON 数据03-practical-guide.md § 2
🔑 解析 KV 键值对03-practical-guide.md § 3
🔐 处理 Base64 编码03-practical-guide.md § 4
✅ 验证字段03-practical-guide.md § 5
🔍 查某个类型04-language-reference.md § 类型系统
⚙️ 查某个函数05-functions-reference.md
📖 查语法规则06-grammar-reference.md

📖 文档列表

文档内容适合人群
01-quickstart.md5 分钟快速入门 + 3 个最常用场景 + 练习所有人
07-complete-types-example.md完整类型系统示例 - 23 种类型速查所有人
02-core-concepts.md设计理念 + 类型系统 + 匹配语义 + 管道系统想深入理解的用户
03-practical-guide.md按任务组织的实战示例 + 常见问题日常使用者
04-language-reference.md完整类型列表 + 语法元素 + 速查表开发者
05-functions-reference.md所有函数的标准化参考开发者
06-grammar-reference.mdEBNF 形式化语法定义编译器开发者

⚡ 快速示例

Nginx 访问日志

package nginx {
  rule access_log {
    (
      ip:client_ip,
      2*_,
      time/clf<[,]>:time,
      http/request":request,
      digit:status,
      digit:bytes
    )
  }
}

JSON API 响应

package api {
  rule response {
    (json(
      chars@user,
      digit@code,
      chars@message
    ))
  }
}

华为防火墙日志(Base64)

package firewall {
  rule huawei_log {
    |decode/base64|
    (
      digit:id,
      time:timestamp,
      sn:serial,
      chars:type\:,
      opt(kvarr),
      kvarr
    )
  }
}

更多示例请查看:01-quickstart.md03-practical-guide.md


🎯 完整类型系统示例

想快速了解 WPL 支持的所有数据类型?

👉 查看完整类型示例 - 一个示例展示 23 种主要数据类型

该文档包含:

  • 完整可运行的输入数据 + WPL 规则 + 输出结果
  • 23 种类型:基础、时间、网络、结构化、协议、编码
  • 每种类型详解:语法、示例、使用场景
  • 常见组合模式:复制即用的类型组合

适合:

  • 🆕 新手快速了解 WPL 能力
  • 📚 开发者作为类型速查手册
  • 🔍 遇到陌生数据格式时快速查找对应类型

🎯 核心特性

  • 声明式:描述“是什么“,而非“怎么做“
  • 类型安全:自动验证和转换(IP、时间、JSON 等)
  • 组合性:小规则组合成复杂规则
  • 强大的管道:预处理(Base64/Hex 解码)+ 字段级验证
  • 灵活的匹配:顺序、择一、可选、重复
  • 子字段提取:JSON/KV 嵌套字段

💬 快速帮助

常见问题

Q: 从哪里开始学习? A: 从 01-quickstart.md 开始,5 分钟即可上手。

Q: 如何解析我的日志格式? A: 查看 03-practical-guide.md,找到相似的场景并调整。

Q: 某个类型/函数怎么用? A: 查看 04-language-reference.md05-functions-reference.md

Q: 解析失败怎么调试? A: 参考 01-quickstart.md § 调试技巧03-practical-guide.md § 常见问题


开始学习: 01-quickstart.md - 5分钟快速入门

WPL 完整类型系统示例

一个示例展示 WPL 支持的所有主要数据类型

本文档提供一个完整的、可运行的 WPL 规则示例,展示 23 种主要数据类型的使用方法。这是学习 WPL 类型系统的最佳参考


📑 文档导航

章节说明
类型覆盖清单本示例包含的所有类型
完整示例输入数据、WPL 规则、输出结果
类型详解每种类型的详细说明
使用建议学习路径和实战建议
快速导航快速跳转到需要的内容

📋 类型覆盖清单

本示例包含以下类型:

类别包含类型数量
基础类型peek_symbolsymbolboolcharsdigitfloat_7
时间类型time_3339time_2822time/clftime_timestamp4
网络类型ipip_netport3
结构化类型kvarrjson2
协议类型http/requesthttp/statushttp/agenthttp/method4
编码类型hexbase642
特殊类型sn1
总计23 种类型

🚀 完整示例

输入数据

peek_symbol symbol true "hello world" 123 3.14 2026-01-19T12:34:56Z 2022-03-21T12:34:56+00:00 Mon, 07 Jul 2025 09:20:32 +0000 [06/Aug/2019:12:12:19 +0800] 1647849600 192.168.1.100 192.168.0.0/24 name=test {"strict":true} "GET /api/users HTTP/1.1" 200 "Mozilla/5.0" "POST" 8080 ABC123XYZ 0x1A2B YmFzZTY0ZGF0YQ==

WPL 规则

package wpl_example {
  rule full_types {
    (
      // 1. peek_symbol - 预读符号(不消费)
      peek_symbol(peek_symbol):peek_sym,

      // 2. symbol - 精确匹配符号
      symbol(symbol):sym,

      // 3. bool - 布尔值
      bool:bool_val,

      // 4. chars - 字符串(带引号)
      chars":quoted_str,

      // 5. digit - 整数
      digit:integer,

      // 6. float - 浮点数
      float:float_val,

      // 7. time_3339 - ISO 8601 / RFC3339
      time_3339:time_iso,

      // 8. time_3339 - RFC3339(带时区)
      time_3339:time_rfc3339,

      // 9. time_2822 - RFC2822(邮件时间格式)
      time_2822:time_rfc2822,

      // 10. time/clf - Common Log Format(Apache/Nginx)
      time/clf<[,]>:time_clf,

      // 11. time_timestamp - Unix 时间戳
      time_timestamp:timestamp,

      // 12. ip - IP 地址
      ip:ip_addr,

      // 13. ip_net - IP 网段(CIDR)
      ip_net:ip_network,

      // 14. kvarr - 键值对
      kvarr(chars@name):kv_data,

      // 15. json - JSON 对象
      json(bool@strict):json_data,

      // 16. http/request - HTTP 请求行
      http/request":http_req,

      // 17. http/status - HTTP 状态码
      http/status:http_status,

      // 18. http/agent - User-Agent
      http/agent":user_agent,

      // 19. http/method - HTTP 方法
      http/method":http_method,

      // 20. port - 端口号
      port:port_num,

      // 21. sn - 序列号
      sn:serial,

      // 22. hex - 十六进制
      hex:hex_data,

      // 23. base64 - Base64 编码
      base64:base64_data
    )
  }
}

输出结果

{
  "peek_sym": "peek_symbol",
  "sym": "symbol",
  "bool_val": true,
  "quoted_str": "hello world",
  "integer": 123,
  "float_val": 3.14,
  "time_iso": "2026-01-19 12:34:56",
  "time_rfc3339": "2022-03-21 12:34:56",
  "time_rfc2822": "2025-07-07 09:20:32",
  "time_clf": "2019-08-06 12:12:19",
  "timestamp": 1647849600,
  "ip_addr": "192.168.1.100",
  "ip_network": "192.168.0.0/24",
  "kv_data": {"name": "test"},
  "json_data": {"strict": true},
  "http_req": "GET /api/users HTTP/1.1",
  "http_status": 200,
  "user_agent": "Mozilla/5.0",
  "http_method": "POST",
  "port_num": 8080,
  "serial": "ABC123XYZ",
  "hex_data": "0x1A2B",
  "base64_data": "YmFzZTY0ZGF0YQ=="
}

📖 类型详解

基础类型

1. peek_symbol - 预读符号

peek_symbol(peek_symbol):peek_sym
  • 作用:预读匹配但不消费输入
  • 用途:用于向前查看而不影响后续解析

2. symbol - 精确匹配符号

symbol(symbol):sym
  • 作用:精确匹配指定字符串
  • 用途:匹配固定关键字、分隔符等

3. bool - 布尔值

bool:bool_val
  • 匹配truefalse
  • 输出:布尔类型

4. chars - 字符串

chars":quoted_str
  • 格式:支持引号包裹 "hello" 或裸字符串 hello
  • 用途:提取任意字符串

5. digit - 整数

digit:integer
  • 匹配:整数,如 123-456
  • 输出:整数类型

6. float - 浮点数

float:float_val
  • 匹配:浮点数,如 3.14-0.5
  • 输出:浮点数类型

时间类型

7-8. time_3339 - RFC3339/ISO 8601

time_3339:time_iso           // 2026-01-19T12:34:56Z
time_3339:time_rfc3339       // 2022-03-21T12:34:56+00:00
  • 格式:ISO 8601 / RFC3339 标准
  • 支持:带时区或不带时区

9. time_2822 - RFC2822(邮件时间)

time_2822:time_rfc2822
  • 格式Mon, 07 Jul 2025 09:20:32 +0000
  • 用途:邮件头、RSS 等

10. time/clf - Common Log Format

time/clf<[,]>:time_clf
  • 格式[06/Aug/2019:12:12:19 +0800]
  • 用途:Apache/Nginx 日志

11. time_timestamp - Unix 时间戳

time_timestamp:timestamp
  • 格式:秒级时间戳,如 1647849600
  • 输出:时间类型

网络类型

12. ip - IP 地址

ip:ip_addr
  • 支持:IPv4(192.168.1.100)和 IPv6(::1

13. ip_net - IP 网段

ip_net:ip_network
  • 格式:CIDR 表示法,如 192.168.0.0/24

14. port - 端口号

port:port_num
  • 范围:1-65535

结构化类型

15. kvarr - 键值对

kvarr(chars@name):kv_data
  • 格式key=value
  • 支持:自动解析或指定键提取
  • 示例name=test{name: "test"}

16. json - JSON 对象

json(bool@strict):json_data
  • 支持:完整 JSON 解析
  • 子字段:可提取嵌套字段
  • 示例{"strict":true}{strict: true}

协议类型

17. http/request - HTTP 请求行

http/request":http_req
  • 格式"GET /api/users HTTP/1.1"
  • 提取:方法 + 路径 + 协议版本

18. http/status - HTTP 状态码

http/status:http_status
  • 格式200404500

19. http/agent - User-Agent

http/agent":user_agent
  • 格式"Mozilla/5.0..."
  • 用途:浏览器标识

20. http/method - HTTP 方法

http/method":http_method
  • 支持GETPOSTPUTDELETE

编码类型

21. hex - 十六进制

hex:hex_data
  • 格式0x1A2B1A2B
  • 输出:十六进制字符串

22. base64 - Base64 编码

base64:base64_data
  • 格式YmFzZTY0ZGF0YQ==
  • 输出:Base64 编码字符串

特殊类型

23. sn - 序列号

sn:serial
  • 格式:字母数字组合,如 ABC123XYZ
  • 用途:设备序列号、订单号等

💡 使用建议

学习路径

  1. 第一步:先理解基础类型(digitcharsip
  2. 第二步:掌握时间类型(根据日志格式选择)
  3. 第三步:学习结构化类型(jsonkvarr
  4. 第四步:了解协议类型(http/*

实战建议

  • 快速查阅:遇到不认识的数据格式,先在本页面搜索
  • 复制即用:直接复制对应类型的语法到你的规则中
  • 类型组合:多种类型可以自由组合使用

常见组合

// Web 日志
(ip, time/clf<[,]>, http/request", http/status, digit)

// API 日志
(time_3339, chars, json(chars@user, digit@code))

// 防火墙日志
(ip, port, time, kvarr)

🔗 相关文档


📌 快速导航

我想…查看章节
查看完整示例代码完整示例
了解某个类型详情类型详解
学习如何使用使用建议
查找常见组合常见组合

提示:将本页面加入书签,作为 WPL 类型系统的速查手册!

WPL 快速入门

5 分钟上手 WPL,立即解析你的日志数据。


📚 快速导航

主题内容
什么是 WPLWPL 简介、核心特点、适用场景
完整类型系统23 种数据类型总览
最简示例Nginx 日志解析
3 个最常用场景空格分隔、JSON、KV 键值对
基本语法速览结构、类型、匹配模式
常见模式速查引号字段、可选字段、重复字段等
快速调试技巧调试方法
实战练习3 个练习题

什么是 WPL

WPL (Warp Processing Language) 是一种声明式规则语言,用于描述如何从日志、消息等文本数据中提取字段解析结构

核心特点

  • 声明式:描述“数据是什么“,而非“如何提取“
  • 类型安全:自动验证和转换(IP、时间、JSON 等 37 种类型)
  • 强大灵活:支持 JSON/KV 嵌套提取、Base64 解码、字段验证等
  • 易于学习:5 分钟即可上手基础用法

适用场景

  • 解析 Web 服务器日志(Nginx、Apache)
  • 提取 JSON/KV 结构化数据
  • 处理编码数据(Base64、Hex)
  • 防火墙、安全设备日志解析
  • 自定义日志格式解析

📚 完整类型系统

WPL 支持 23 种主要数据类型,涵盖基础类型、时间、网络、结构化数据、协议和编码等。

👉 查看完整示例: 07-complete-types-example.md

该文档包含:

  • ✅ 所有 23 种类型的完整示例代码
  • ✅ 可运行的输入数据和 WPL 规则
  • ✅ 每种类型的详细说明和使用建议
  • ✅ 常见类型组合模式

快速预览主要类型:

  • 基础digitfloatcharsbool
  • 时间time/clftime_3339time_2822time_timestamp
  • 网络ipip_netportdomainurl
  • 结构化jsonkvarrarray
  • 协议http/requesthttp/statushttp/methodhttp/agent
  • 编码hexbase64

最简示例:Nginx 日志

输入数据:

192.168.1.2 - - [06/Aug/2019:12:12:19 +0800] "GET /index.html HTTP/1.1" 200 1024

WPL 规则:

package nginx {
  rule access_log {
    (
      ip:client_ip,
      2*_,
      time/clf<[,]>:time,
      http/request":request,
      digit:status,
      digit:bytes
    )
  }
}

输出结果:

client_ip: 192.168.1.2
time: 2019-08-06 12:12:19
request: GET /index.html HTTP/1.1
status: 200
bytes: 1024

说明:

  • ip:client_ip - 提取 IP 地址,命名为 client_ip
  • 2*_ - 忽略 2 个字段(两个 -
  • time/clf<[,]> - 提取方括号包裹的 CLF 时间
  • http/request" - 提取引号包裹的 HTTP 请求
  • digit - 提取数字

3 个最常用场景

场景 1:空格分隔的日志

输入:

200 192.168.1.1 2023-01-01T12:00:00 login_success

WPL:

package demo {
  rule simple_log {
    (digit:code, ip:client, time:ts, chars:action)
  }
}

输出:

code: 200
client: 192.168.1.1
ts: 2023-01-01 12:00:00
action: login_success

场景 2:JSON 数据

输入:

{"user":"admin","code":200,"message":"success"}

WPL:

package api {
  rule json_response {
    (json(
      chars@user,
      digit@code,
      chars@message
    ))
  }
}

输出:

user: admin
code: 200
message: success

场景 3:KV 键值对

输入:

host=server1;port=8080;user=admin;status=online

WPL:

package config {
  rule kv_log {
    (kvarr)
  }
}

输出:

host: server1
port: 8080
user: admin
status: online

基本语法速览

结构

package 包名 {
  rule 规则名 {
    (字段列表)
  }
}

常用字段类型

类型说明示例
digit整数200, 8080
chars字符串"hello", admin
ipIP 地址192.168.1.1
time时间2023-01-01 12:00:00
time/clfCLF 时间[06/Aug/2019:12:12:19 +0800]
jsonJSON 对象{"key":"value"}
kv键值对key=value
http/requestHTTP 请求GET /path HTTP/1.1
http/statusHTTP 状态码200

字段命名

type:name              # 命名字段
digit:status           # status = 数字
ip:client_ip           # client_ip = IP地址

忽略字段

_                      # 忽略 1 个字段
2*_                    # 忽略 2 个字段
5*_                    # 忽略 5 个字段

格式控制

<[,]>                  # 方括号包裹:[content]
<{,}>                  # 花括号包裹:{content}
"                      # 引号包裹:"content"
^N                     # 固定 N 个字符

重复模式

kvarr                  # 自动解析所有KV
3*ip                   # 重复 3 次
12*digit               # 重复 12 次

子字段提取

# JSON 子字段
json(chars@name, digit@age)

# KV 子字段
kvarr(chars@host, digit@port)

# 嵌套字段
json(chars@user/name, digit@user/age)

常见模式速查

解析带引号的字段

chars":url             # "http://example.com"
http/agent":ua         # "Mozilla/5.0..."

解析带特殊分隔符的数据

# 逗号分隔
(digit, ip, chars)\,

# 分号分隔
(digit, ip, chars)\;

# 字段级分隔符(优先级更高)
digit\,, ip\;, chars\s

可选字段

opt(chars)             # 可选字段
(digit, opt(chars), time)

Base64 解码

|decode/base64|
(json(chars@data))

快速调试技巧

1. 从简单开始

# 第 1 步:最简单
(digit)

# 第 2 步:添加字段
(digit, ip)

# 第 3 步:添加命名
(digit:status, ip:client)

# 第 4 步:添加复杂类型
(digit:status, ip:client, json(chars@name))

2. 使用 opt() 定位问题

# 如果某个字段导致失败,用 opt 包裹
(digit, opt(ip), time, chars)
# 如果 ip 解析失败,其他字段仍然可以解析

3. 检查分隔符

打印原始数据,确认字段间的分隔符:

数据:200,192.168.1.1,admin
分隔符:逗号
规则:(digit, ip, chars)\,

下一步

理解概念

02-core-concepts.md - 理解 WPL 的设计理念

你将学到:

  • 为什么 WPL 这样设计?
  • 类型系统的作用
  • 匹配语义(seq/alt/opt/some_of)
  • 管道系统原理
  • 分隔符优先级

解决实际问题

03-practical-guide.md - 按任务查找解决方案

你将学到:

  • 解析 Web 服务器日志(Nginx/Apache)
  • 解析 JSON 数据(嵌套、反转义)
  • 解析 KV 键值对(多种分隔符)
  • 处理编码数据(Base64/Hex)
  • 字段验证与过滤
  • 复杂场景(重复模式、可选字段)

查阅参考

04-language-reference.md - 完整类型和语法参考 → 05-functions-reference.md - 所有函数参考


实战练习

练习 1:解析自定义日志

数据:

[2023-01-01 12:00:00] INFO 192.168.1.1 user=admin action=login

提示:

  • 时间在方括号中
  • INFO 可以忽略
  • 后面是 IP 和 KV
查看答案
package practice {
  rule custom_log {
    (
      time<[,]>:timestamp,
      _,
      ip:client,
      kvarr
    )
  }
}

练习 2:解析嵌套 JSON

数据:

{"user":{"name":"Alice","age":25},"status":"active"}

提示:

  • 使用 @path/to/field 提取嵌套字段
查看答案
package practice {
  rule nested_json {
    (json(
      chars@user/name,
      digit@user/age,
      chars@status
    ))
  }
}

练习 3:解析 Base64 编码日志

数据(Base64):

eyJ1c2VyIjoiYWRtaW4iLCJjb2RlIjoyMDB9

解码后:

{"user":"admin","code":200}

提示:

  • 使用 |decode/base64| 预处理
查看答案
package practice {
  rule base64_log {
    |decode/base64|
    (json(
      chars@user,
      digit@code
    ))
  }
}

相关资源

WPL 核心概念

本文档帮助你理解 WPL 的设计理念和核心概念,建立正确的思维模型。


📚 文档导航

快速导航

主题内容
设计理念为什么需要 WPL、核心思想、声明式设计
类型系统类型的作用、类型层次、类型组合
匹配语义seq 顺序、alt 择一、opt 可选、some_of 重复
管道系统预处理管道、字段级管道、两者区别
子字段与嵌套JSON 子字段、KV 子字段、数组
分隔符优先级优先级规则、分隔符类型、实际应用
设计原则声明式、类型安全、组合性、明确性
常见误解WPL vs 正则、字段连续性、格式灵活性

WPL 设计理念

为什么需要 WPL?

问题: 如何从非结构化文本中提取结构化数据?

输入(文本):192.168.1.1 - - [06/Aug/2019:12:12:19 +0800] "GET /index.html" 200
输出(结构):
  client_ip: 192.168.1.1
  time: 2019-08-06 12:12:19
  request: GET /index.html
  status: 200

传统方法的问题:

  • 正则表达式:难写、难读、难维护
  • 手写解析器:代码冗长、容易出错
  • 固定格式解析:不够灵活

WPL 的解决方案:

  • 声明式:描述“是什么“,而非“怎么做“
  • 类型安全:自动验证和转换
  • 组合性:小的规则组合成复杂规则

核心思想:规则 = 模式匹配 + 字段提取

package demo {
  rule example {
    (ip:client, digit:status, time:ts)
  }
}

这个规则表达:

  1. 模式:数据格式是“IP 数字 时间“
  2. 提取:提取 3 个字段,分别命名为 client、status、ts
  3. 验证:自动验证 IP 格式、数字格式、时间格式
  4. 转换:自动转换为对应类型

类型系统

类型的作用

类型在 WPL 中有三个作用:

  1. 验证:确保数据符合预期格式
  2. 转换:自动转换为标准格式
  3. 语义:表达数据的含义

示例:

# 输入:06/Aug/2019:12:12:19 +0800
time/clf:access_time

# 类型 time/clf 做了 3 件事:
# 1. 验证:是否符合 CLF 时间格式
# 2. 转换:转换为标准时间格式 2019-08-06 12:12:19
# 3. 语义:表达"这是访问时间"

类型的层次

基础类型 ────→ 结构化类型 ────→ 协议类型
   ↓              ↓               ↓
 digit          json          http/request
 chars           kvarr        http/status
  ip           array          time/clf
 time           obj

基础类型:原子数据

  • digit - 整数
  • chars - 字符串
  • ip - IP 地址
  • time - 时间

结构化类型:复合数据

  • json - JSON 对象(包含多个字段)
  • kvarr - 键值对
  • array - 数组

协议类型:领域特定格式

  • http/request - HTTP 请求行
  • http/status - HTTP 状态码
  • time/clf - CLF 时间格式

类型组合示例

# 简单组合
(digit, ip, time)

# 嵌套组合
(digit, json(chars@name, digit@age), time)

# 数组组合
array/digit              # 数字数组
array/array/chars        # 二维字符串数组

匹配语义

WPL 提供 4 种匹配语义,满足不同场景需求。

seq(顺序匹配)- 默认

语义:按顺序依次匹配每个字段

# 显式写法
seq(ip, digit, time)

# 隐式写法(默认)
(ip, digit, time)

匹配过程:

输入:192.168.1.1 200 2023-01-01
      ↓           ↓   ↓
     ip         digit time

何时使用: 字段顺序固定(90% 的场景)


alt(择一匹配)

语义:尝试多种类型,匹配其中一个

alt(ip, digit)

匹配过程:

输入:192.168.1.1
尝试:ip ✓ → 成功,返回 ip
      digit ✗ → 不尝试

输入:12345
尝试:ip ✗ → 失败
      digit ✓ → 成功,返回 digit

何时使用: 同一位置可能是不同类型

示例场景:

# 日志中 user_id 可能是数字或字符串
(time, alt(digit, chars):user_id, chars:action)

opt(可选匹配)

语义:字段可选,失败不报错

opt(chars)

匹配过程:

输入:有内容 → 尝试匹配,成功则提取
输入:无内容 → 跳过,继续下一个字段

何时使用: 某些字段可能不存在

示例场景:

# HTTP 日志中 referer 可能为空
(ip, time, http/request, digit, opt(chars):referer)

some_of(尽可能多)

语义:循环匹配,尽可能多地消费字段

some_of(kvarr, ip, digit)

匹配过程:

输入:k1=v1 192.168.1.1 200 k2=v2 300

循环 1:尝试 kvarr ✓ → 提取 k1=v1
循环 2:尝试 kvarr ✗, 尝试 ip ✓ → 提取 192.168.1.1
循环 3:尝试 kvarr ✗, 尝试 ip ✗, 尝试 digit ✓ → 提取 200
循环 4:尝试 kvarr ✓ → 提取 k2=v2
循环 5:尝试 kvarr ✗, 尝试 ip ✗, 尝试 digit ✓ → 提取 300
循环 6:全部失败 → 停止

何时使用: 不确定数量和顺序的混合字段


匹配语义对比

语义用途示例匹配次数
seq顺序固定(ip, digit, time)每个字段 1 次
alt类型不定alt(ip, digit)其中 1 个
opt可选字段opt(chars)0 或 1 次
some_of混合重复some_of(kvarr, ip)尽可能多

管道系统

WPL 的管道系统分为两层:预处理管道(整行级)和字段级管道(字段级)。

预处理管道(整行级)

语法:

|step1|step2|
(字段列表)

作用域: 整行原始输入

执行时机: 在字段解析之前

常用场景:

# Base64 解码
|decode/base64|
(json)

# 多步处理
|decode/base64|unquote/unescape|
(json(chars@path))

为什么需要预处理管道?

  • 某些日志整行都是 Base64 编码(如华为防火墙)
  • 需要先解码,才能进行字段解析
  • 预处理一次,所有字段都受益

字段级管道(字段级)

语法:

(fields) |function1| |function2|

作用域: 解析后的字段集合

执行时机: 在字段解析之后

常用场景:

# 验证字段
(json |f_has(status) |f_digit_in(status, [200, 201]))

# 转换字段
(json(chars@message) |take(message) |json_unescape())

为什么需要字段级管道?

  • 需要验证某个字段是否存在
  • 需要验证字段值是否符合条件
  • 需要对特定字段进行转换

两种管道的区别

特性预处理管道字段级管道
作用域整行输入解析后字段
执行时机解析前解析后
语法|step||function()|
典型用途解码、反转义验证、转换

示例对比:

# 预处理管道:整行 Base64 解码
|decode/base64|
(json(chars@user))

# 字段级管道:单字段 Base64 解码
(json(chars@payload) |take(payload) |base64_decode())

子字段与嵌套

为什么需要子字段?

问题: JSON/KV 等结构化数据包含多个字段,如何提取?

{"user":"admin","code":200,"data":{"result":"ok"}}

解决方案: 使用子字段语法

json(
  chars@user,              # 提取 user 字段
  digit@code,              # 提取 code 字段
  chars@data/result        # 提取 data.result 字段
)

JSON 子字段

基本语法:

json(type@key, type@key, ...)

示例:

# 提取指定字段
json(chars@name, digit@age)

# 嵌套路径
json(chars@user/name, digit@user/age)

# 可选字段
json(chars@name, opt(chars)@email)

输入:

{"user":{"name":"Alice","age":25},"status":"active"}

输出:

user/name: Alice
user/age: 25
status: active

KV 子字段

基本语法:

kvarr(type@key, type@key, ...)

示例:

kvarr(chars@hostname, digit@port, opt(chars)@user)

输入:

hostname=server1 port=3306 user=root

输出:

hostname: server1
port: 3306
user: root

数组

基本语法:

array[/subtype]

示例:

array/digit:nums          # [1,2,3] → nums/[0]=1, nums/[1]=2, nums/[2]=3
array/chars:items         # ["a","b"] → items/[0]="a", items/[1]="b"
array/array/digit         # [[1,2],[3,4]] → 嵌套数组

分隔符优先级

为什么需要优先级?

问题: 不同来源的分隔符可能冲突

# 字段级分隔符
digit\,

# 组级分隔符
(digit, ip)\;

# 上游分隔符(来自 json/kvarr 等)

解决方案: 定义优先级规则


优先级规则

字段级(3) > 组级(2) > 上游(1)

示例:

# 字段级覆盖组级
(digit\;, ip, chars)\,
# digit 用分号,ip 和 chars 用逗号

# 组级覆盖上游
json(...) (digit, ip)\;
# 即使 json 内部默认空格,组级分号生效

分隔符类型

分隔符写法说明
逗号\,最常用
分号\;常用于 KV
空格\s默认
冒号\:键值分隔
行尾\0读到行尾

实际应用

场景 1:不同字段不同分隔符

(digit\;, ip\,, chars\s)
# digit 用分号,ip 用逗号,chars 用空格

场景 2:组级统一分隔符

(digit, ip, time)\,
# 所有字段都用逗号

场景 3:最后一个字段读到行尾

(digit, ip, chars\0)
# chars 读取所有剩余内容

设计原则总结

1. 声明式优于命令式

# WPL(声明式)
(ip, digit, time)

# 命令式伪代码
ip = parse_ip(input)
digit = parse_digit(input)
time = parse_time(input)

2. 类型安全优于字符串匹配

# 带类型验证
ip:client_ip              # 自动验证 IP 格式

# 纯字符串
chars:client_ip           # 不验证格式

3. 组合优于重复

# 可组合
rule base_fields { (ip, time) }
rule extended { (ip, time, json) }   # 复用基础部分

# 不可组合
rule log1 { (ip, time, chars) }
rule log2 { (ip, time, json) }       # 重复 ip, time

4. 明确优于隐含

# 明确指定
time/clf<[,]>:access_time

# 隐含(可能失败)
time:access_time

常见误解

误解 1:WPL 是正则表达式

错误认知: WPL 和正则表达式类似

正确理解: WPL 是类型化的模式匹配语言

  • 正则:字符级匹配
  • WPL:类型级匹配 + 验证 + 转换

误解 2:所有字段必须连续

错误认知: 字段之间不能有空隙

正确理解: 使用 _ 跳过不需要的字段

(ip, 3*_, time)           # 跳过 3 个字段

误解 3:只能解析固定格式

错误认知: WPL 只能解析固定格式的数据

正确理解: 支持可选、重复、择一等灵活模式

opt(chars)                # 可选
kvarr                     # 自动解析KV
alt(ip, digit)            # 择一

下一步

实战应用

03-practical-guide.md - 按任务查找解决方案

你将学到:

  • 解析各种 Web 服务器日志
  • 处理 JSON 和 KV 数据
  • 使用预处理管道
  • 字段验证与过滤
  • 复杂场景处理

深入参考

04-language-reference.md - 完整类型和语法 → 05-functions-reference.md - 所有函数详解


相关资源

WPL 实战指南

本文档采用任务导向的方式,帮助你快速找到解决方案。


📚 任务导航

任务类型跳转
解析 Web 服务器日志Nginx/Apache 访问日志、错误日志
解析 JSON 数据提取 JSON 字段、嵌套 JSON
解析 KV 键值对基础 KV、嵌套 KV、混合格式
处理编码数据Base64、Hex 解码
字段验证与过滤检查字段、IP 范围、端口范围
复杂场景可变字段、多格式、嵌套结构
常见问题调试技巧、性能优化

📋 快速参考

常用模式速查

模式语法说明
可选字段opt(type:name)字段可能不存在
重复字段some_of(type:name)匹配 1 到多个
跳过字段_n*_跳过 1 个或 n 个字段
JSON 提取json(type@path:name)提取 JSON 字段
KV 提取kvarr解析键值对
Base64 解码|decode/base64|预处理管道
字段验证type/check验证字段值
择一匹配one_of(...)多个模式选一个

常用类型速查

类型说明示例
ipIP 地址192.168.1.1
digit整数8080
chars字符串hello
time/clfApache 时间格式[06/Aug/2019:12:12:19 +0800]
http/requestHTTP 请求GET /index.html HTTP/1.1
jsonJSON 数据{"key":"value"}
kvarr键值对数组key1=val1;key2=val2

📖 如何使用本指南

根据你的任务,找到对应章节,复制规则并根据实际情况调整。


1. 解析 Web 服务器日志

任务 1.1:解析 Nginx/Apache 访问日志

场景: 标准 Nginx/Apache 访问日志

输入:

192.168.1.2 - - [06/Aug/2019:12:12:19 +0800] "GET /index.html HTTP/1.1" 200 1024 "http://example.com/" "Mozilla/5.0"

WPL 规则:

package nginx {
  rule access_log {
    (
      ip:client_ip,
      2*_,
      time/clf<[,]>:time,
      http/request":request,
      http/status:status,
      digit:bytes,
      chars":referer,
      http/agent":user_agent
    )
  }
}

输出:

client_ip: 192.168.1.2
time: 2019-08-06 12:12:19
request: GET /index.html HTTP/1.1
status: 200
bytes: 1024
referer: http://example.com/
user_agent: Mozilla/5.0

要点:

  • 2*_ 忽略两个 - 字段
  • time/clf<[,]> 解析方括号包裹的 CLF 时间
  • http/request" 自动解析引号包裹的 HTTP 请求并提取方法、路径、协议
  • chars" 提取引号包裹的字符串

任务 1.2:解析带变量的 Nginx 日志

场景: 自定义 Nginx log_format

输入:

2023-01-01T12:00:00+08:00|INFO|192.168.1.1|GET|/api/users|200|0.123

WPL 规则:

package nginx {
  rule custom_log {
    (
      time_3339:timestamp,
      chars:level,
      ip:client_ip,
      http/method:method,
      chars:path,
      http/status:status,
      float:response_time
    )\|
  }
}

输出:

timestamp: 2023-01-01 12:00:00
level: INFO
client_ip: 192.168.1.1
method: GET
path: /api/users
status: 200
response_time: 0.123

要点:

  • )\| 指定组级分隔符为管道符 |
  • time_3339 解析 RFC 3339 时间格式
  • http/method 专门解析 HTTP 方法
  • float 解析浮点数(响应时间)

任务 1.3:解析带 referer 为空的日志

场景: referer 可能为 - 或空

输入:

192.168.1.1 [06/Aug/2019:12:12:19 +0800] "GET /index.html" 200 1024 "-"

WPL 规则:

package nginx {
  rule access_log_optional {
    (
      ip:client_ip,
      time/clf<[,]>:time,
      http/request":request,
      http/status:status,
      digit:bytes,
      opt(chars"):referer
    )
  }
}

输出:

client_ip: 192.168.1.1
time: 2019-08-06 12:12:19
request: GET /index.html
status: 200
bytes: 1024
referer: -

要点:

  • opt(chars") 将 referer 标记为可选字段
  • 即使 referer 解析失败,其他字段仍能正常提取

2. 解析 JSON 数据

任务 2.1:提取 JSON 字段

场景: API 响应日志

输入:

{"user":"admin","code":200,"message":"success","timestamp":"2023-01-01T12:00:00"}

WPL 规则:

package api {
  rule response {
    (json(
      chars@user,
      digit@code,
      chars@message,
      time_3339@timestamp
    ))
  }
}

输出:

user: admin
code: 200
message: success
timestamp: 2023-01-01 12:00:00

要点:

  • json(type@key) 语法提取指定键的值
  • 类型自动验证和转换(time_3339 转换时间格式)

任务 2.2:处理嵌套 JSON

场景: 嵌套的 JSON 结构

输入:

{"user":{"name":"Alice","age":25,"profile":{"city":"Beijing"}},"status":"active"}

WPL 规则:

package api {
  rule nested_json {
    (json(
      chars@user/name,
      digit@user/age,
      chars@user/profile/city,
      chars@status
    ))
  }
}

输出:

user/name: Alice
user/age: 25
user/profile/city: Beijing
status: active

要点:

  • 使用 / 分隔嵌套路径:@user/name, @user/profile/city
  • 路径层级无限制

任务 2.3:JSON 反转义

场景: JSON 字符串包含转义字符

输入:

{"path":"c:\\users\\admin\\file.txt","message":"line1\nline2"}

WPL 规则:

package api {
  rule json_unescape {
    (json(chars@path, chars@message) |json_unescape())
  }
}

输出:

path: c:\users\admin\file.txt
message: line1
line2

要点:

  • |json_unescape()\\n 转换为实际换行符
  • \\\\ 转换为 \
  • \\\" 转换为 "

任务 2.4:可选 JSON 字段

场景: 某些字段可能不存在

输入:

{"user":"admin","code":200}

WPL 规则:

package api {
  rule optional_fields {
    (json(
      chars@user,
      digit@code,
      opt(chars)@message,
      opt(chars)@data
    ))
  }
}

输出:

user: admin
code: 200

要点:

  • opt(type)@key 标记字段为可选
  • 不存在的字段不会导致解析失败

3. 解析 KV 键值对

任务 3.1:基础 KV 解析(分号分隔)

场景: 简单的 KV 格式日志

输入:

host=server1;port=8080;user=admin;status=online

WPL 规则:

package config {
  rule kv_semicolon {
    (kvarr)
  }
}

输出:

host: server1
port: 8080
user: admin
status: online

要点:

  • kvarr 自动解析所有KV对
  • 自动识别分隔符

任务 3.2:固定数量 KV(逗号分隔)

场景: 华为防火墙日志(12 个固定 KV)

输入:

k1=v1,k2=v2,k3=v3,k4=v4,k5=v5,k6=v6,k7=v7,k8=v8,k9=v9,k10=v10,k11=v11,k12=v12

WPL 规则:

package firewall {
  rule fixed_kv {
    (kvarr)
  }
}

输出:

k1: v1
k2: v2
...
k12: v12

要点:

  • kvarr 自动解析所有KV对
  • 不需要指定数量

任务 3.3:提取指定 KV 字段

场景: 只提取需要的字段

输入:

hostname=server1 port=3306 user=root db=test timeout=30

WPL 规则:

package database {
  rule extract_kv {
    (kvarr(
      chars@hostname,
      digit@port,
      chars@user,
      opt(chars)@db
    ))
  }
}

输出:

hostname: server1
port: 3306
user: root
db: test

要点:

  • kvarr(type@key) 提取指定键的值
  • 未列出的键(如 timeout)被忽略
  • opt(type)@key 标记可选字段

任务 3.4:混合 KV 格式

场景: 可选 KV + 多个 KV

输入:

1234,2023-01-01T12:00:00,ABC123,LOGIN:host=server;user=admin,port=8080,action=success

WPL 规则:

package firewall {
  rule mixed_kv {
    (
      digit:id,
      time:timestamp,
      sn:serial,
      chars:type\:,
      opt(kvarr),
      kvarr
    )
  }
}

输出:

id: 1234
timestamp: 2023-01-01 12:00:00
serial: ABC123
type: LOGIN
host: server
user: admin
port: 8080
action: success

要点:

  • chars:type\: 冒号作为分隔符
  • opt(kvarr) 可选的KV
  • kvarr 自动解析KV

4. 处理编码数据

任务 4.1:Base64 解码

场景: 华为防火墙日志(整行 Base64 编码)

输入(Base64):

MTIzNCwyMDIzLTAxLTAxVDEyOjAwOjAwLEFCQzEyMyxMT0dJTjpob3N0PXNlcnZlcjt1c2VyPWFkbWluLHBvcnQ9ODA4MCxhY3Rpb249c3VjY2Vzcw==

解码后:

1234,2023-01-01T12:00:00,ABC123,LOGIN:host=server;user=admin,port=8080,action=success

WPL 规则:

package firewall {
  rule huawei_log {
    |decode/base64|
    (
      digit:id,
      time:timestamp,
      sn:serial,
      chars:type\:,
      opt(kvarr),
      kvarr
    )
  }
}

输出:

id: 1234
timestamp: 2023-01-01 12:00:00
serial: ABC123
type: LOGIN
host: server
user: admin
port: 8080
action: success

要点:

  • |decode/base64| 预处理管道,对整行进行 Base64 解码
  • 解码后再进行字段解析

任务 4.2:十六进制解码

场景: 二进制数据的十六进制表示

输入:

48656c6c6f20576f726c64

WPL 规则:

package binary {
  rule hex_decode {
    |decode/hex|
    (chars:data)
  }
}

输出:

data: Hello World

要点:

  • |decode/hex| 将十六进制字符串解码为原始文本

任务 4.3:组合多步预处理

场景: Base64 + JSON 反转义

输入(Base64):

eyJwYXRoIjoiY1xcXFx1c2Vyc1xcXFxmaWxlIiwidGV4dCI6ImxpbmUxXG5saW5lMiJ9

解码后:

{"path":"c:\\users\\file","text":"line1\nline2"}

WPL 规则:

package security {
  rule multi_step {
    |decode/base64|unquote/unescape|
    (json(chars@path, chars@text))
  }
}

输出:

path: c:\users\file
text: line1
line2

要点:

  • 可以链接多个预处理步骤
  • 执行顺序:从左到右

5. 字段验证与过滤

任务 5.1:检查字段存在

场景: 确保必需字段存在

输入:

{"status":"ok","message":"success","data":null}

WPL 规则:

package api {
  rule check_required {
    (json |f_has(status) |f_has(message))
  }
}

输出:

status: ok
message: success
data: null

要点:

  • |f_has(field) 检查字段是否存在
  • 字段不存在时解析失败

任务 5.2:验证状态码

场景: 只处理成功的响应(200/201/204)

输入:

{"code":200,"status":"success","data":"result"}

WPL 规则:

package api {
  rule validate_success {
    (json |f_digit_in(code, [200, 201, 204]))
  }
}

输出:

code: 200
status: success
data: result

要点:

  • |f_digit_in(field, [list]) 验证数字字段值在列表中
  • 不在列表中时解析失败

任务 5.3:过滤特定方法

场景: 只处理 GET/POST 请求

输入:

{"method":"GET","path":"/api/users","status":200}

WPL 规则:

package api {
  rule filter_methods {
    (json |f_chars_in(method, [GET, POST]))
  }
}

输出:

method: GET
path: /api/users
status: 200

要点:

  • |f_chars_in(field, [list]) 验证字符串字段值在列表中

任务 5.4:链式验证

场景: 多个条件组合

输入:

{"user":"admin","age":25,"status":"active"}

WPL 规则:

package api {
  rule chain_validation {
    (json(chars@user, digit@age, chars@status)
      |take(user)
      |chars_has(admin)
      |take(age)
      |digit_in([18, 25, 30])
      |take(status)
      |chars_has(active)
    )
  }
}

输出:

user: admin
age: 25
status: active

要点:

  • take(field) 选择字段为活跃字段
  • 然后对活跃字段进行验证
  • 可以链式调用多个验证

6. 复杂场景

任务 6.1:可变数量字段(some_of)

场景: 字段数量和顺序不固定

输入:

192.168.1.1 k1=v1 200 k2=v2 300 k3=v3

WPL 规则:

package mixed {
  rule variable_fields {
    some_of(ip, kv, digit)
  }
}

输出:

ip: 192.168.1.1
k1: v1
digit: 200
k2: v2
digit: 300
k3: v3

要点:

  • some_of(...) 循环匹配所有可能的类型
  • 尽可能多地消费字段

任务 6.2:择一匹配(alt)

场景: 某个位置可能是不同类型

输入:

user_id:12345 action:login
user_id:admin action:logout

WPL 规则:

package auth {
  rule flexible_user_id {
    (
      chars:key1,
      alt(digit, chars):user_id,
      chars:key2,
      chars:action
    )
  }
}

输出(输入 1):

key1: user_id
user_id: 12345
key2: action
action: login

输出(输入 2):

key1: user_id
user_id: admin
key2: action
action: logout

要点:

  • alt(type1, type2) 尝试多种类型
  • 匹配第一个成功的类型

任务 6.3:读到行尾

场景: 最后一个字段包含所有剩余内容

输入:

2023-01-01 ERROR This is a very long error message with many details

WPL 规则:

package log {
  rule read_to_end {
    (
      time:timestamp,
      chars:level,
      chars\0:message
    )
  }
}

输出:

timestamp: 2023-01-01
level: ERROR
message: This is a very long error message with many details

要点:

  • \0 表示读到行尾
  • 常用于日志的 message 字段

7. 常见问题

Q1: 如何处理可变数量的字段?

答案: 使用 N*typesome_of

# KV 自动解析
kvarr

# 混合类型
some_of(ip, digit, kvarr)

Q2: 如何忽略某些字段?

答案: 使用 _N*_

# 忽略 1 个字段
(ip, _, time)

# 忽略 3 个字段
(ip, 3*_, time)

Q3: 分隔符不一致怎么办?

答案: 使用字段级分隔符优先级

# 不同字段不同分隔符
(digit\;, ip\,, chars\s)

# 或使用组级分隔符
(digit, ip, chars)\,

Q4: JSON 字段可能不存在怎么办?

答案: 使用 opt(type)@key

json(
  chars@user,
  opt(chars)@email,
  opt(digit)@age
)

Q5: 如何提取嵌套 JSON 字段?

答案: 使用路径语法 @path/to/field

json(
  chars@user/name,
  digit@user/age,
  chars@data/result
)

Q6: 预处理管道失败怎么办?

答案: 检查以下几点

  1. 管道名称是否正确
  2. 是否以 | 结尾
  3. 输入数据格式是否正确
# 正确
|decode/base64|

# 错误:缺少结尾 |
|decode/base64

Q7: 如何调试解析失败的规则?

步骤:

  1. 简化规则,从最简单的字段开始
  2. 使用 opt() 标记可疑字段
  3. 检查分隔符是否正确
  4. 检查格式控制符(引号、括号等)
# 原规则(失败)
(digit, time, chars, json)

# 调试规则
(digit, opt(time), opt(chars), opt(json))
# 逐步确定哪个字段导致失败

下一步

查阅参考

04-language-reference.md - 完整类型和语法 → 05-functions-reference.md - 所有函数详解

深入理解

02-core-concepts.md - 理解 WPL 设计理念


相关资源

WPL 语言参考

本文档提供 WPL 的完整语言元素参考,用于快速查阅类型、语法和结构。


📑 文档导航

章节说明
类型系统所有数据类型速查表
语法元素基本结构、字段定义、格式控制
子字段语法JSON、KV、数组子字段
注解tag、copy_raw 注解
语法速查常用模式快速参考

📋 类型系统

基础类型

#类型标识符样例说明
1预读符号peek_symbol(xxx)peek_symbol(GET)预读但不消费
2忽略__, 2*_, 3*_忽略该字段
3符号symbol(xxx)symbol(HTTP)精确匹配
4布尔booltrue, false布尔值
5字符串chars"hello"任意字符串
6整数digit123, 8080整数
7浮点float3.14, 0.01浮点数
8序列号snABC123XYZ自动生成序列号

时间类型

#类型标识符样例说明
9通用时间time2023-05-15 07:09:12自动识别多种格式
10ISO 8601time_iso2023-05-15T07:09:12ZISO 8601 标准
11RFC 3339time_33392022-03-21T12:34:56+00:00RFC3339 标准
12RFC 2822time_2822Mon, 07 Jul 2025 09:20:32 +0000邮件时间格式
13CLF 时间time/clf06/Aug/2019:12:12:19 +0800Apache/Nginx 日志
14Unix时间戳time_timestamp1647849600Unix 秒级时间戳

网络类型

#类型标识符样例说明
15IP地址ip192.168.1.100, ::1IPv4/IPv6 地址
16IP网段ip_net192.168.0.0/24CIDR 网段
17域名domainexample.com域名
18邮箱emailuser@example.com邮箱地址
19端口port8080, 443端口号
20URLurlhttp://example.com/path完整 URL

编码类型

#类型标识符样例说明
21十六进制hex48656c6c6f十六进制数据
22Base64base64aGVsbG8=Base64 编码

结构化类型

#类型标识符样例说明
23键值对kvarrkey=valueKV 格式
24JSONjson{"k":"v"}JSON 对象
25严格JSONexact_json{"k":"v"}严格验证 JSON
26对象obj嵌套对象通用对象
27数组array[1,2,3]数组
28数字数组array/digit[1,2,3]数字数组
29字符串数组array/chars["a","b"]字符串数组

协议类型

#类型标识符样例说明
30HTTP请求http/requestGET /path HTTP/1.1HTTP 请求行
31HTTP状态http/status200, 404HTTP 状态码
32User-Agenthttp/agentMozilla/5.0...浏览器 UA
33HTTP方法http/methodGET, POSTHTTP 方法

特殊类型

#类型标识符样例说明
34身份证id_card11010119900101123418 位身份证
35手机号mobile_phone1380013800011 位手机号
36协议文本proto_text协议格式协议文本解析
37自动识别auto自动推断自动类型推断

🔧 语法元素

基本结构

package 包名 {
  rule 规则名 {
    |预处理管道|
    (字段列表)
  }
}

字段定义完整语法

[N*] DataType [ (symbol) ] [ (subfields) ] [:name] [ [len] ] [ format ] [ sep ] { | pipe }
部分说明示例
[N*]重复计数kvarr, 3*ip
DataType数据类型digit, ip, json
(symbol)符号内容symbol(GET)
(subfields)子字段列表json(chars@name)
:name字段命名:status, :client_ip
[len]长度限制[100]
format格式控制<[,]>, ", ^10
sep分隔符\,, \;, \0
| pipe管道函数|f_has(name)|

格式控制

格式语法示例说明
范围定界<beg,end><[,]>, <{,}>起止符号
引号"chars"引号包裹
字符计数N*10*chars, 5*_按字符数

分组元信息

元信息说明示例匹配行为
seq顺序匹配(默认)seq(ip, digit, time)按顺序依次匹配
alt择一匹配alt(ip, digit)匹配其中一个
opt可选匹配opt(chars)失败不报错
some_of尽可能多some_of(ip, digit)循环匹配

分隔符优先级

字段级(3) > 组级(2) > 上游(1)
分隔符写法说明优先级
逗号\,逗号分隔字段级 (3)
分号\;分号分隔字段级 (3)
冒号\:冒号分隔字段级 (3)
空格\s空格分隔字段级 (3)
行尾\0读到行尾字段级 (3)
组分隔(...)\sep组级分隔组级 (2)
默认继承上游上游 (1)

🎯 子字段语法

JSON 子字段

json(type@key, type@key, ...)

示例:

# 基本提取
json(chars@name, digit@age)

# 嵌套路径
json(chars@user/name, digit@user/age)

# 可选字段
json(chars@name, opt(chars)@email)

KV 子字段

kvarr(type@key, type@key, ...)

示例:

kvarr(chars@hostname, digit@port, opt(chars)@user)

数组

array[/subtype]

示例:

array/digit:nums          # [1,2,3]
array/chars:items         # ["a","b"]
array/array/digit         # 嵌套数组

📝 注解

tag 注解

#[tag(key1:"value1", key2:"value2")]
package demo {
  rule x { (digit, time) }
}

copy_raw 注解

#[copy_raw(name:"raw_log")]
rule nginx_log {
  (ip, time, chars)
}

原始字符串(避免转义)

#[tag(path:r#"C:\Program Files\App"#)]
package demo {
  rule x { (digit) }
}

💡 语法速查

常用模式

# 字段命名
type:name              # digit:status

# 忽略字段
_                      # 忽略 1 个
N*_                    # 忽略 N 个

# 重复模式
kvarr                  # 自动解析KV
N*ip                   # 固定 N 次

# 可选字段
opt(chars)             # 单个可选
opt(kvarr)             # 可选 KV

# 格式控制
<[,]>                  # 方括号包裹
"                      # 引号包裹
^10                    # 固定 10 字符

# 分隔符
\,                     # 逗号
\;                     # 分号
\0                     # 行尾

# 子字段
json(chars@key)        # JSON 字段
kvarr(digit@port)      # KV 字段
@path/to/field         # 嵌套路径

相关文档

WPL 函数参考

本文档提供所有 WPL 函数的标准化参考。


📋 WPL 所有函数速查

预处理管道函数

函数说明示例
decode/base64对整行 Base64 解码|decode/base64|
decode/hex对整行十六进制解码|decode/hex|
unquote/unescape移除引号并还原转义|unquote/unescape|
plg_pipe/<name>自定义预处理扩展|plg_pipe/dayu|

选择器函数

函数说明示例
take(name)选择指定字段为活跃字段|take(name)|
last()选择最后一个字段为活跃字段|last()|

字段集操作函数(f_ 前缀)

函数说明示例
f_has(name)检查字段是否存在|f_has(status)|
f_chars_has(name, val)检查字段值等于字符串|f_chars_has(status, success)|
f_chars_not_has(name, val)检查字段值不等于字符串|f_chars_not_has(level, error)|
f_chars_in(name, [...])检查字段值在字符串列表|f_chars_in(method, [GET, POST])|
f_digit_has(name, num)检查字段值等于数字|f_digit_has(code, 200)|
f_digit_in(name, [...])检查字段值在数字列表|f_digit_in(status, [200, 201])|
f_ip_in(name, [...])检查 IP 在列表|f_ip_in(client_ip, [127.0.0.1])|

活跃字段操作函数

函数说明示例
has()检查活跃字段存在|take(name)| has()|
chars_has(val)检查活跃字段等于字符串|take(status)| chars_has(success)|
chars_not_has(val)检查活跃字段不等于字符串|take(level)| chars_not_has(error)|
chars_in([...])检查活跃字段在字符串列表|take(method)| chars_in([GET, POST])|
digit_has(num)检查活跃字段等于数字|take(code)| digit_has(200)|
digit_in([...])检查活跃字段在数字列表|take(status)| digit_in([200, 201])|
ip_in([...])检查活跃 IP 在列表|take(client_ip)| ip_in([127.0.0.1])|

转换函数

函数说明示例
json_unescape()JSON 反转义|take(message)| json_unescape()|
base64_decode()Base64 解码|take(payload)| base64_decode()|

常用场景速查

我想做什么使用方法
对整行 Base64 解码|decode/base64|
对整行十六进制解码|decode/hex|
移除引号和转义|unquote/unescape|
检查字段是否存在|f_has(field_name)|
检查字段值等于某字符串|f_chars_has(status, success)|
检查字段值在列表中|f_chars_in(method, [GET, POST])|
检查状态码是否为 200|f_digit_has(code, 200)|
检查 IP 是否在白名单|f_ip_in(client_ip, [127.0.0.1, 192.168.1.1])|
选择特定字段验证|take(status)| chars_has(ok)|
对字段 JSON 反转义|take(message)| json_unescape()|
对字段 Base64 解码|take(payload)| base64_decode()|
链式验证多个条件|f_has(method)| f_chars_in(method, [GET, POST])|

预处理管道函数

decode/base64

说明: 对整行输入进行 Base64 解码

语法:

|decode/base64|

参数:

示例:

rule base64_log {
  |decode/base64|
  (json(chars@user, digit@code))
}

输入(Base64):

eyJ1c2VyIjoiYWRtaW4iLCJjb2RlIjoyMDB9

解码后:

{"user":"admin","code":200}

注意:

  • 作用于整行原始输入
  • 必须在字段解析前执行
  • 解码失败会报错

decode/hex

说明: 对整行输入进行十六进制解码

语法:

|decode/hex|

参数:

示例:

rule hex_log {
  |decode/hex|
  (chars:data)
}

输入:

48656c6c6f20576f726c64

输出:

data: Hello World

unquote/unescape

说明: 移除外层引号并还原反斜杠转义

语法:

|unquote/unescape|

参数:

示例:

rule unescape_log {
  |unquote/unescape|
  (chars:message)
}

转换效果:

输入输出
\"hello\"hello
path\\to\\filepath\to\file

plg_pipe/

说明: 自定义预处理扩展

语法:

|plg_pipe/<name>|

参数: <name> - 注册的扩展名称

示例:

rule custom_preproc {
  |plg_pipe/dayu|
  (json(_@_origin, _@payload/packet_data))
}

注意:

  • 需要通过代码注册扩展
  • 注册接口:wpl::register_wpl_pipe!

选择器函数

take

说明: 选择指定字段为活跃字段

语法:

|take(<field_name>)|

参数:

  • field_name - 要选择的字段名

返回: 无(改变活跃字段状态)

示例:

rule take_example {
  (
    json(chars@name, digit@age)
    |take(name)              # 选择 name 为活跃字段
    |chars_has(admin)        # 验证 name 的值
  )
}

使用场景:

  • 需要对特定字段进行验证
  • 链式验证多个字段

last

说明: 选择最后一个字段为活跃字段

语法:

|last()|

参数:

返回: 无(改变活跃字段状态)

示例:

rule last_example {
  (
    json(chars@a, chars@b, chars@c)
    |last()                  # 选择 c 为活跃字段
    |chars_has(value)        # 验证 c 的值
  )
}

字段集操作函数

f_has

说明: 检查指定字段是否存在

语法:

|f_has(<field_name>)|

参数:

  • field_name - 要检查的字段名

返回: 布尔值(字段存在返回 true,否则失败)

示例:

rule check_field {
  (json |f_has(status) |f_has(message))
}

f_chars_has

说明: 检查指定字段值是否等于字符串

语法:

|f_chars_has(<field_name>, <value>)|

参数:

  • field_name - 字段名(或 _ 表示活跃字段)
  • value - 要匹配的字符串值

返回: 布尔值

示例:

rule check_string {
  (json |f_chars_has(status, success))
}

f_chars_not_has

说明: 检查指定字段值是否不等于字符串

语法:

|f_chars_not_has(<field_name>, <value>)|

参数:

  • field_name - 字段名
  • value - 不应匹配的字符串值

返回: 布尔值

示例:

rule exclude_error {
  (json |f_chars_not_has(level, error))
}

f_chars_in

说明: 检查指定字段值是否在字符串列表中

语法:

|f_chars_in(<field_name>, [<value1>, <value2>, ...])|

参数:

  • field_name - 字段名
  • [...] - 允许的字符串值列表

返回: 布尔值

示例:

rule check_method {
  (json |f_chars_in(method, [GET, POST, PUT]))
}

f_digit_has

说明: 检查指定字段数值是否等于指定数字

语法:

|f_digit_has(<field_name>, <number>)|

参数:

  • field_name - 字段名
  • number - 要匹配的数字

返回: 布尔值

示例:

rule check_status {
  (json |f_digit_has(code, 200))
}

f_digit_in

说明: 检查指定字段数值是否在数字列表中

语法:

|f_digit_in(<field_name>, [<num1>, <num2>, ...])|

参数:

  • field_name - 字段名
  • [...] - 允许的数字值列表

返回: 布尔值

示例:

rule check_success {
  (json |f_digit_in(status, [200, 201, 204]))
}

f_ip_in

说明: 检查指定 IP 字段是否在 IP 列表中

语法:

|f_ip_in(<field_name>, [<ip1>, <ip2>, ...])|

参数:

  • field_name - 字段名
  • [...] - 允许的 IP 地址列表(支持 IPv4/IPv6)

返回: 布尔值

示例:

rule check_trusted {
  (json |f_ip_in(client_ip, [127.0.0.1, 192.168.1.1]))
}

活跃字段操作函数

has

说明: 检查活跃字段是否存在

语法:

|has()|

参数:

返回: 布尔值

示例:

rule check_active {
  (
    json(chars@name)
    |take(name)
    |has()                   # 检查 name 是否存在
  )
}

chars_has

说明: 检查活跃字段值是否等于指定字符串

语法:

|chars_has(<value>)|

参数:

  • value - 要匹配的字符串值

返回: 布尔值

示例:

rule check_value {
  (
    json(chars@status)
    |take(status)
    |chars_has(success)
  )
}

chars_not_has

说明: 检查活跃字段值是否不等于指定字符串

语法:

|chars_not_has(<value>)|

参数:

  • value - 不应匹配的字符串值

返回: 布尔值

示例:

rule exclude_value {
  (
    json(chars@level)
    |take(level)
    |chars_not_has(error)
  )
}

chars_in

说明: 检查活跃字段值是否在字符串列表中

语法:

|chars_in([<value1>, <value2>, ...])|

参数:

  • [...] - 允许的字符串值列表

返回: 布尔值

示例:

rule check_method {
  (
    json(chars@method)
    |take(method)
    |chars_in([GET, POST, PUT])
  )
}

digit_has

说明: 检查活跃字段数值是否等于指定数字

语法:

|digit_has(<number>)|

参数:

  • number - 要匹配的数字

返回: 布尔值

示例:

rule check_code {
  (
    json(digit@code)
    |take(code)
    |digit_has(200)
  )
}

digit_in

说明: 检查活跃字段数值是否在数字列表中

语法:

|digit_in([<num1>, <num2>, ...])|

参数:

  • [...] - 允许的数字值列表

返回: 布尔值

示例:

rule check_success {
  (
    json(digit@status)
    |take(status)
    |digit_in([200, 201, 204])
  )
}

ip_in

说明: 检查活跃 IP 字段是否在 IP 列表中

语法:

|ip_in([<ip1>, <ip2>, ...])|

参数:

  • [...] - 允许的 IP 地址列表(支持 IPv4/IPv6)

返回: 布尔值

示例:

rule check_client {
  (
    json(ip@client_ip)
    |take(client_ip)
    |ip_in([127.0.0.1, 192.168.1.1])
  )
}

转换函数

json_unescape

说明: 对活跃字段进行 JSON 反转义

语法:

|json_unescape()|

参数:

转换效果:

输入输出
hello\\nworldhello + 换行 + world
path\\\\topath\to
say\\\"hi\\\"say"hi"

示例:

rule parse_json_log {
  (
    json(chars@message)
    |take(message)
    |json_unescape()
  )
}

base64_decode

说明: 对活跃字段进行 Base64 解码

语法:

|base64_decode()|

参数:

转换效果:

输入输出
aGVsbG8=hello
Zm9vYmFyfoobar

示例:

rule decode_payload {
  (
    json(chars@payload)
    |take(payload)
    |base64_decode()
  )
}

📊 函数对照表

字段集 vs 活跃字段

功能字段集操作(f_ 前缀)活跃字段操作(无前缀)
检查存在f_has(name)take(name) | has()
字符串相等f_chars_has(name, val)take(name) | chars_has(val)
字符串不等f_chars_not_has(name, val)take(name) | chars_not_has(val)
字符串在列表f_chars_in(name, [a, b])take(name) | chars_in([a, b])
数字相等f_digit_has(name, 200)take(name) | digit_has(200)
数字在列表f_digit_in(name, [200, 201])take(name) | digit_in([200, 201])
IP 在列表f_ip_in(name, [1.1.1.1])take(name) | ip_in([1.1.1.1])

💡 使用模式

链式调用

rule chain {
  (json
    |f_has(method)
    |f_chars_in(method, [GET, POST])
    |f_digit_in(status, [200, 201])
    |f_ip_in(client_ip, [10.0.0.1])
  )
}

选择器 + 验证

rule selector {
  (json(chars@name, digit@age)
    |take(name)
    |chars_has(admin)
    |take(age)
    |digit_in([18, 19, 20])
  )
}

预处理 + 字段级管道

rule full {
  |decode/base64|                      # 预处理:整行解码
  (json |f_has(name) |f_digit_in(age, [18, 25]))  # 字段级:验证
}

相关文档

WPL 语法参考(EBNF)

本文档提供 WPL 的形式化语法定义,适合:

  • 编译器/解析器开发者
  • 精确理解语法规则
  • 工具集成开发

普通用户请参考:


📑 文档导航

章节说明
完整 EBNF 定义形式化语法定义
语义说明语法规则的语义解释
实现参考源代码位置

完整 EBNF 定义

权威实现以 crates/wp-lang 解析器为准;此处与源代码保持同步。

; WPL 语法(EBNF)
; 基于 crates/wp-lang 下解析实现(winnow)整理
; 说明:本文件给出语法产生式与必要的词法约定。除显式标注外,token 之间允许可选空白 `ws`。

wpl_document     = { package_decl } ;

package_decl     = [ annotation ] "package" ws? ident ws? "{" ws? rule_decl+ ws? "}" ;

rule_decl        = [ annotation ] "rule" ws? rule_name ws? "{" ws? statement ws? "}" ;

statement        = plg_pipe_block | express ;

plg_pipe_block   = ["@"]? "plg_pipe" ws? "(" ws? "id" ws? ":" ws? key ws? ")" ws? "{" ws? express ws? "}" ;

express          = [ preproc ] group { ws? "," ws? group } ;

preproc          = "|" ws? preproc_step { ws? "|" ws? preproc_step } ws? "|" ;   ; 至少一个步骤,且以 '|' 结尾
preproc_step     = builtin_preproc | plg_pipe_step ;
builtin_preproc  = ns "/" name ;
plg_pipe_step    = "plg_pipe" ws? "/" ws? key ;                   ; 通过注册表查找自定义扩展
ns               = "decode" | "unquote" ;                        ; 命名空间白名单
name             = ("base64" | "hex") | "unescape" ;             ; 步骤名白名单

group            = [ group_meta ] ws? "(" ws? field_list_opt ws? ")" [ ws? group_len ] [ ws? group_sep ] ;
group_meta       = "alt" | "opt" | "some_of" | "seq" ;
group_len        = "[" number "]" ;
group_sep        = sep ;

; 列表:允许空、允许尾随逗号
field_list_opt   = [ field { ws? "," ws? field } [ ws? "," ] ] ;

field            = [ repeat ] data_type [ symbol_content ]
                   [ subfields ]
                   [ ":" ws? var_name ]
                   [ length ]
                   [ format ]
                   [ sep ]
                   { pipe } ;                              ; 允许多个管道

repeat           = [ number ] "*" ;                        ; "*ip" 或 "3*ip"
length           = "[" number "]" ;                       ; 仅顶层字段支持(子字段不支持)

; 复合字段(如 kvarr/json 等)的子字段列表
subfields        = "(" ws? subfields_opt ws? ")" ;
subfields_opt    = [ subfield { ws? "," ws? subfield } [ ws? "," ] ] ;
subfield         = [ opt_datatype | data_type ]
                   [ symbol_content ]
                   [ "@" ref_path ]
                   [ ":" ws? var_name ]
                   [ format ]
                   [ sep ]
                   { pipe } ;

opt_datatype     = "opt" "(" ws? data_type ws? ")" ;     ; 声明该子字段为可选

; 字段数据类型(与外部 crate wp-model-core::DataType 对应)
data_type        = builtin_type | ns_type | array_type ;

builtin_type     = "auto" | "bool" | "chars" | "symbol" | "peek_symbol"
                   | "digit" | "float" | "_" | "sn"
                   | "time" | "time/clf" | "time_iso" | "time_3339" | "time_2822" | "time_timestamp"
                   | "ip" | "ip_net" | "domain" | "email" | "port"
                   | "hex" | "base64"
                   | "kv" | "kvarr" | "json" | "exact_json"
                   | "url"
                   | "proto_text" | "obj"
                   | "id_card" | "mobile_phone" ;

ns_type          = path_ident ;                               ; 例如 http/request、http/status 等
; 注:实现层面建议对白名单前缀(如 "http/")做校验,以避免任意路径膨胀语言面。

array_type       = "array" [ "/" key ] ;                 ; 如:"array" 或 "array/ip"

; 仅当 data_type 为 symbol/peek_symbol 时允许携带内容
symbol_content   = "(" symbol_chars ")" ;

; 字段显示/抽取格式
format           = scope_fmt | quote_fmt | field_cnt ;
scope_fmt        = "<" any_chars "," any_chars ">" ;   ; 作用域首尾定界,如 <[,]>
quote_fmt        = '"' ;                                ; 等价首尾均为 '"'
field_cnt        = "^" number ;                          ; 仅 chars/_ 合法(实现约束)

; 分隔符(高/中优先级,原样拼接)。语法为反斜杠转义的字符序列,长度>=1
sep              = sep_char , { sep_char } ;             ; 例:"\\," => ",";"\\!\\|" => "!|"
sep_char         = '\\' , any_char ;

; 字段级管道:函数调用或嵌套分组
pipe             = "|" ws? ( fun_call | group ) ;

; 预置函数(wpl_fun.rs):
; - 选择器函数:take, last
; - f_ 前缀表示字段集合操作(需指定字段名)
; - 无前缀表示活跃字段操作
; - 转换函数:json_unescape, base64_decode
fun_call         = selector_fun | target_fun | active_fun | transform_fun ;

; 选择器函数
selector_fun     = take_fun | last_fun ;
take_fun         = "take" "(" ws? key ws? ")" ;
last_fun         = "last" "(" ws? ")" ;

; 字段集合操作函数(f_ 前缀)
target_fun       = f_has | f_chars_has | f_chars_not_has | f_chars_in
                 | f_digit_has | f_digit_in | f_ip_in ;
f_has            = "f_has" "(" ws? key ws? ")" ;
f_chars_has      = "f_chars_has" "(" ws? key ws? "," ws? path ws? ")" ;
f_chars_not_has  = "f_chars_not_has" "(" ws? key ws? "," ws? path ws? ")" ;
f_chars_in       = "f_chars_in" "(" ws? key ws? "," ws? path_array ws? ")" ;
f_digit_has      = "f_digit_has" "(" ws? key ws? "," ws? number ws? ")" ;
f_digit_in       = "f_digit_in" "(" ws? key ws? "," ws? number_array ws? ")" ;
f_ip_in          = "f_ip_in" "(" ws? key ws? "," ws? ip_array ws? ")" ;

; 活跃字段操作函数(无前缀)
active_fun       = has_fun | chars_has | chars_not_has | chars_in
                 | digit_has | digit_in | ip_in ;
has_fun          = "has" "(" ws? ")" ;
chars_has        = "chars_has" "(" ws? path ws? ")" ;
chars_not_has    = "chars_not_has" "(" ws? path ws? ")" ;
chars_in         = "chars_in" "(" ws? path_array ws? ")" ;
digit_has        = "digit_has" "(" ws? number ws? ")" ;
digit_in         = "digit_in" "(" ws? number_array ws? ")" ;
ip_in            = "ip_in" "(" ws? ip_array ws? ")" ;

; 转换函数
transform_fun    = json_unescape | base64_decode ;
json_unescape    = "json_unescape" "(" ws? ")" ;
base64_decode    = "base64_decode" "(" ws? ")" ;

path_array       = "[" ws? path { ws? "," ws? path } ws? "]" ;
number_array     = "[" ws? number { ws? "," ws? number } ws? "]" ;
ip_array         = "[" ws? ip_addr { ws? "," ws? ip_addr } ws? "]" ;

annotation       = "#[" ws? ann_item { ws? "," ws? ann_item } ws? "]" ;
ann_item         = tag_anno | copy_raw_anno ;
tag_anno         = "tag" "(" ws? tag_kv { ws? "," ws? tag_kv } ws? ")" ;
tag_kv           = ident ":" ( quoted_string | raw_string ) ;      ; 键为标识符;值为字符串
copy_raw_anno    = "copy_raw" "(" ws? "name" ws? ":" ws? ( quoted_string | raw_string ) ws? ")" ;

; 词法与辅助记号 --------------------------------------------------------
field_name       = var_name ;
rule_name        = exact_path ;
key              = key_char { key_char } ;              ; [A-Za-z0-9_./-]+
var_name         = var_char { var_char } ;              ; [A-Za-z0-9_.-]+
ref_path         = ref_char { ref_char } ;              ; [A-Za-z0-9_./\-.[\]*]+
; 标识符与路径标识符(推荐写法)
ident            = ( letter | '_' ) { letter | digit | '_' | '.' | '-' } ;
path_ident       = ident { "/" ident } ;

exact_path       = exact_path_char { exact_path_char } ; ; 不含 '[' ']' '*'
exact_path_char  = letter | digit | '_' | '.' | '/' | '-' ;
path             = key | ref_path ;

number           = digit { digit } ;
digit            = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

key_char         = letter | digit | '_' | '.' | '/' | '-' ;
var_char         = letter | digit | '_' | '.' | '-' ;
ref_char         = key_char | '[' | ']' | '*' ;

letter           = 'A'..'Z' | 'a'..'z' ;

quoted_string    = '"' { escaped | char_no_quote_backslash } '"' ;
raw_string       = 'r' '#' '"' { any_char } '"' '#' ;          ; r#"..."#,内部不处理转义(内容可包含 '"')
char_no_quote    = ? any char except '"' ? ;
escaped          = '\\' ( '"' | '\\' | 'n' | 't' | 'r' | 'x' hex hex ) ;
char_no_quote_backslash = ? any char except '"' and '\\' ? ;
hex              = '0'..'9' | 'a'..'f' | 'A'..'F' ;

free_string      = { fchar } ;                          ; 直至 ',' 或 ')'(不含)
fchar            = ? any char except ',' and ')' ? ;

symbol_chars     = { schar } ;                          ; 允许除 ')' 与 '\\' 外字符,或使用 '\)' 转义
schar            = char_no_close_paren_backslash | '\\' ')' ;
char_no_close_paren_backslash = ? any char except ')' and '\\' ? ;
any_chars        = { any_char } ;
any_char         = ? any character ? ;

ip_addr          = quoted_string | ipv4 | ipv6 ;        ; 支持 IPv4/IPv6 裸字面量或带引号
ipv4             = digit1 "." digit1 "." digit1 "." digit1 ;
digit1           = digit { digit } ;
ipv6             = ? valid IPv6 literal (RFC 4291), including compressed forms like "::1" ? ;

ws               = { ' ' | '\t' | '\n' | '\r' } ;

;保留关键字(不可作为标识符使用;由实现侧进行冲突校验)
ReservedKeyword  = "package" | "rule" | "alt" | "opt" | "some_of" | "seq" | "order"
                 | "tag" | "copy_raw" | "include" | "macro" ;



语义说明

预处理管道

  • preproc 管道(例如 |decode/base64|unquote/unescape|)出现在 express 起始处,独立于字段级 pipe
  • 作用于整行输入,在字段解析前执行
  • 执行顺序:从左到右
  • 必须以 | 结尾

分组长度和分隔符

  • group 后可跟 [n] 与分隔符 sep
  • 长度会应用到组内所有字段
  • sep 仅存储在组上,具体组合策略见实现

格式控制

  • format 中的 field_cnt^n)仅适用于 chars/_ 类型
  • 其它类型将被拒绝(实现约束)

符号类型

  • symbol/peek_symbol 可携带 symbol_content,如 symbol(boy)
  • peek_symbol 等价于 symbol,且仅改变“窥探“语义(预读但不消费)

子字段

  • subfields 中未显式 "@ref" 时,键默认为 "*"(通配键)
  • 子字段支持 opt(type) 标记为可选

分隔符

  • sep 写法需以反斜杠转义每个字符
  • 例如 \\!\\| 代表字符串 "!|"
  • 优先级:字段级 > 组级 > 上游

注解

  • annotation 可用于 packagerule
  • 若同时存在,会在 rule 侧合并(rule 优先)

相关文档


实现参考

  • 语法实现:crates/wp-lang/src/parser/
  • 管道函数:crates/wp-lang/src/parser/wpl_fun.rs
  • 数据类型:外部 crate wp-model-core

WPL Field Functions 函数索引

本文档列出了 WP-Motor WPL 语言中所有可用的 field function(字段函数)。

字段选择函数 (Field Selectors)

函数语法说明文档
taketake(field_name)选择指定名称的字段作为活动字段-
lastlast()选择最后一个字段作为活动字段-

字符串匹配函数 (String Matching)

函数语法说明文档
chars_haschars_has(value)检查字符串字段是否等于指定值-
chars_not_haschars_not_has(value)检查字符串字段是否不等于指定值-
chars_inchars_in([value1, value2, ...])检查字符串字段是否在值列表中-
f_chars_hasf_chars_has(target, value)检查指定字段是否等于指定值-
f_chars_not_hasf_chars_not_has(target, value)检查指定字段是否不等于指定值-
f_chars_inf_chars_in(target, [values])检查指定字段是否在值列表中-
starts_withstarts_with('prefix')检查字符串是否以指定前缀开始,否则转为 ignore📖 详细文档
regex_matchregex_match('pattern')使用正则表达式匹配字符串字段📖 详细文档

数值匹配函数 (Numeric Matching)

函数语法说明文档
digit_hasdigit_has(value)检查数值字段是否等于指定值-
digit_indigit_in([value1, value2, ...])检查数值字段是否在值列表中-
digit_rangedigit_range(begin, end)检查数值是否在指定范围内(闭区间)📖 详细文档
f_digit_hasf_digit_has(target, value)检查指定字段是否等于指定数值-
f_digit_inf_digit_in(target, [values])检查指定字段是否在数值列表中-

IP 地址匹配函数 (IP Matching)

函数语法说明文档
ip_inip_in([ip1, ip2, ...])检查 IP 地址是否在列表中-
f_ip_inf_ip_in(target, [ips])检查指定字段的 IP 地址是否在列表中-

字段存在性检查 (Field Existence)

函数语法说明文档
hashas()检查当前活动字段是否存在-
f_hasf_has(target)检查指定字段是否存在-

包装函数 (Wrapper Functions)

函数语法说明文档
notnot(inner_function)反转(取反)内部管道函数的成功/失败结果📖 详细文档

字符串转换函数 (String Transformation)

函数语法说明文档
json_unescapejson_unescape()解码 JSON 转义字符(\n, \t, \", \\ 等)-
base64_decodebase64_decode()Base64 解码字符串字段-
chars_replacechars_replace(target, replacement)替换字符串中的子串📖 详细文档

函数分类总览

按功能分类

1. 条件检查函数

用于检查字段是否满足特定条件,不修改字段值。

  • 字符串检查:chars_has, chars_not_has, chars_in, starts_with, regex_match
  • 数值检查:digit_has, digit_in, digit_range
  • IP 检查:ip_in
  • 存在性检查:has

2. 转换函数

修改字段值的函数。

  • 解码:json_unescape, base64_decode
  • 替换:chars_replace

3. 字段选择函数

用于选择特定字段作为活动字段。

  • take: 按名称选择
  • last: 选择最后一个字段

4. 包装函数

包装其他函数以改变其行为。

  • not: 反转内部函数的成功/失败结果

按目标字段支持分类

操作当前活动字段

  • chars_has, chars_not_has, chars_in, starts_with
  • digit_has, digit_in, digit_range
  • ip_in
  • has
  • json_unescape, base64_decode, chars_replace
  • regex_match

可指定目标字段 (带 f_ 前缀)

  • f_chars_has, f_chars_not_has, f_chars_in
  • f_digit_has, f_digit_in
  • f_ip_in
  • f_has

使用示例

基本流水线

rule example_pipeline {
    # 1. 选择字段
    | take(message)

    # 2. 检查条件
    | chars_has(error)

    # 3. 转换处理
    | chars_replace(error, ERROR)
}

复杂条件组合

rule complex_filter {
    # 检查状态码范围
    | take(status)
    | digit_range(200, 299)  # 2xx 成功状态码

    # 检查消息内容
    | take(message)
    | regex_match('(?i)(success|ok|complete)')
}

分支逻辑

rule branching_logic {
    # 分支 1:错误日志
    (
        | take(level)
        | chars_in([ERROR, FATAL])
    )
    |
    # 分支 2:警告日志
    (
        | take(level)
        | chars_in([WARN, WARNING])
    )
}

性能参考

函数类型典型性能说明
字段选择< 100ns极快,基于索引查找
字符串匹配< 1μs简单字符串比较
数值匹配< 100ns简单数值比较
范围检查< 500ns线性扫描多个范围
正则匹配1-100μs取决于模式复杂度
Base64 解码1-10μs取决于字符串长度
字符串替换1-10μs取决于字符串长度

最佳实践

1. 合理使用函数类型

# ✅ 推荐:简单匹配使用 chars_has
| chars_has(error)

# ⚠️ 过度使用:简单匹配不需要正则
| regex_match('^error$')  # 性能较差

2. 优先使用专用函数

# ✅ 推荐:数值范围使用 digit_range
| digit_range(200, 299)

# ⚠️ 不推荐:用正则匹配数字
| regex_match('^2\d{2}$')  # 性能较差

3. 先选择字段再处理

# ✅ 正确
| take(message)
| chars_replace(old, new)

# ❌ 错误:没有活动字段
| chars_replace(old, new)  # 会失败

4. 组合使用条件函数

# ✅ 推荐:先用简单条件过滤,再用复杂条件
| chars_has(error)          # 快速过滤
| regex_match('error:\d+')  # 精确匹配

函数对比

chars_has vs regex_match

特性chars_hasregex_match
用途精确字符串匹配模式匹配
性能极快相对较慢
灵活性
适用场景已知固定值复杂模式
# 简单匹配:使用 chars_has
| chars_has(ERROR)

# 复杂匹配:使用 regex_match
| regex_match('(?i)error:\s*\d+')

digit_in vs digit_range

特性digit_indigit_range
用途离散值检查范围检查
参数值列表范围列表
适用场景特定值(如状态码)连续范围
# 离散值:使用 digit_in
| digit_in([200, 404, 500])

# 连续范围:使用 digit_range
| digit_range(200, 299)

目标字段函数 vs 活动字段函数

特性活动字段函数目标字段函数
前缀f_
字段选择需要先 take自动选择
性能稍快稍慢(需查找)
便利性需要额外步骤一步到位
# 活动字段函数
| take(status)
| digit_has(200)

# 目标字段函数(更简洁)
| f_digit_has(status, 200)

相关文档

版本历史

  • 1.15.1 (2026-02-07)

    • 新增 not() 包装函数
    • 修复 f_chars_not_haschars_not_has 类型检查bug
  • 1.13.4 (2026-02-03)

    • 新增 starts_with 函数
    • 完善文档体系
  • 1.13.1 (2026-02-02)

    • 新增 digit_range 函数
    • 新增 regex_match 函数
    • 完善文档体系
  • 1.11.0 (2026-01-29)

    • 新增 chars_replace 函数
    • 完善 Base64 和 JSON 转义支持

提示: 选择合适的函数类型可以显著提升性能。优先使用简单的专用函数,仅在需要复杂模式匹配时使用正则表达式。

WPL 字段引用使用指南

概述

在 WPL 中,使用 @ 符号来引用集合中的字段。支持两种字段名格式:

  • 普通标识符@field_name
  • 单引号字符串@'@special-field'(用于包含特殊字符的字段名)

快速开始

基本语法

# 引用普通字段
@field_name

# 引用带特殊字符的字段
@'special-field-name'

# 指定字段类型和别名
datatype@field_name: alias_name

简单示例

# JSON 解析 - 提取字段
rule parse_json {
    json(
        @src_ip: source_ip,
        @dst_ip: dest_ip,
        @message: msg
    )
}

# 使用单引号处理特殊字段名
rule parse_json_special {
    json(
        @'@client-ip': client,
        @'event.type': event,
        @'log/level': level
    )
}

普通字段引用

支持的字符

普通字段名(不带引号)支持以下字符:

  • 字母和数字(a-z, A-Z, 0-9)
  • 下划线(_
  • 斜杠(/
  • 连字符(-
  • 点号(.
  • 方括号([, ])- 用于数组索引
  • 星号(*)- 用于通配符

示例

# 简单字段名
@user_id
@username
@ip_address

# 路径式字段名
@process/name
@parent/process/pid
@network/protocol

# 数组索引
@items[0]
@data[5]/value
@process[0]/path

# 通配符
@items[*]
@logs/*/message

单引号字段引用

何时使用

当字段名包含以下特殊字符时,必须使用单引号:

  • @ 符号
  • 空格
  • 逗号(,
  • 等号(=
  • 括号((, )
  • 尖括号(<, >
  • 井号(#
  • 其他非标准字符

基本语法

@'field name with spaces'
@'@field-with-at-sign'
@'field,with,commas'

转义字符

双引号字符串支持以下转义序列:

转义序列含义示例
\"双引号@"field\"name"
\\反斜杠@"path\\to\\file"
\n换行符@"multi\nline"
\t制表符@"tab\tseparated"
\r回车符@"carriage\rreturn"
\xHH十六进制字节@"hex\x41value"

单引号字符串原始字符串(raw string):

  • 只支持 \' 转义单引号本身
  • 其他所有反斜杠 \ 都按字面意思处理
  • \n\t\\ 等不会被转义

示例

# 双引号 - 支持完整转义
@"field\"name"      # 结果: field"name
@"path\\file"       # 结果: path\file
@"line\nbreak"      # 结果: line换行break

# 单引号 - 原始字符串,只转义 \'
@'field\'s name'    # 结果: field's name
@'path\to\file'     # 结果: path\to\file (字面反斜杠)
@'raw\nstring'      # 结果: raw\nstring (字面 \n)
@'C:\Users\test'    # 结果: C:\Users\test (Windows 路径)

推荐使用场景

  • 单引号:Windows 路径、Unix 路径、正则表达式、包含反斜杠的字符串
  • 双引号:需要换行符、制表符等转义字符的场景

实际应用场景

场景 1:解析 Elasticsearch 日志

# Elasticsearch 字段通常使用 @ 前缀
rule elasticsearch_log {
    json(
        @'@timestamp': timestamp,
        @'@version': version,
        @message: msg,
        @'log.level': level,
        @'event.action': action
    )
}

场景 2:解析带空格的字段名

# CSV 或其他格式可能包含带空格的列名
rule csv_with_spaces {
    (
        @'First Name': first_name,
        @'Last Name': last_name,
        @'Email Address': email,
        @'Phone Number': phone
    )
}

场景 3:解析嵌套 JSON 字段

# JSON 字段路径包含特殊字符
rule nested_json {
    json(
        @'user.id': uid,
        @'user.profile.name': username,
        @'event#metadata': metadata,
        @'geo.location.lat': latitude,
        @'geo.location.lon': longitude
    )
}

场景 4:处理 Prometheus 指标

# Prometheus 指标名称包含多种特殊字符
rule prometheus_metrics {
    (
        @'http_requests_total{method="GET"}': get_requests,
        @'http_requests_total{method="POST"}': post_requests,
        @'process_cpu_seconds_total': cpu_usage
    )
}

场景 5:Windows 事件日志

# Windows 路径包含反斜杠
rule windows_events {
    json(
        @'Event.System.Provider': provider,
        @'Event.EventData.CommandLine': cmdline,
        @'Process\\Path': process_path
    )
}

场景 6:混合使用普通和特殊字段名

rule mixed_fields {
    json(
        # 普通字段名
        @username: user,
        @ip_address: ip,
        @timestamp: time,

        # 特殊字段名
        @'@client-ip': client,
        @'user.email': email,
        @'event#type': event_type,
        @'log level': level
    )
}

场景 7:KV 解析带特殊字段

# 键值对中包含特殊字符的键
rule kv_special_keys {
    kv(
        @'@timestamp': time,
        @'event-type': type,
        @'user/name': username,
        @'session#id': session
    )
}

take() 函数引号支持

take() 函数用于选择当前字段,同样支持单引号和双引号来处理包含特殊字符的字段名。

基本语法

# 普通字段名
| take(field_name)

# 双引号字段名
| take("@special-field")

# 单引号字段名
| take('@special-field')

使用场景

1. 选择带特殊字符的字段

rule select_special_fields {
    # 双引号
    | take("@timestamp")
    | take("field with spaces")
    | take("field,with,commas")

    # 单引号
    | take('@client-ip')
    | take('event.type')
    | take('log/level')
}

2. 转义字符支持

rule escaped_fields {
    # 双引号内转义
    | take("field\"name")
    | take("path\\with\\backslash")

    # 单引号内转义
    | take('field\'s name')
    | take('path\\to\\file')
}

3. 实际应用

# Elasticsearch 日志处理
rule elasticsearch {
    | take("@timestamp")
    | take("@version")
    | take("log.level")
}

# CSV 数据处理
rule csv_processing {
    | take('First Name')
    | take('Last Name')
    | take('Email Address')
}

# 混合使用
rule mixed_usage {
    | take(user_id)          # 普通字段
    | take("@timestamp")     # 双引号
    | take('event.type')     # 单引号
}

支持的转义字符

转义序列含义双引号单引号
\"双引号❌ (字面 \")
\'单引号❌ (字面 \')
\\反斜杠❌ (字面 \\)
\n换行符❌ (字面 \n)
\t制表符❌ (字面 \t)

说明

  • 双引号:支持完整转义,类似 C/Java/JavaScript 字符串
  • 单引号:原始字符串(raw string),只支持 \' 转义单引号本身,其他反斜杠都是字面字符

最佳实践

# ✅ 推荐 - 优先使用不带引号
| take(field_name)

# ✅ 推荐 - 特殊字符使用引号
| take("@timestamp")
| take('@client-ip')

# ✅ 推荐 - 根据内容选择引号类型
| take("field with spaces")         # 双引号,适合简单字符串
| take('it\'s a field')              # 单引号,只需转义 \'
| take('C:\Windows\System32')       # 单引号,Windows 路径
| take("line\nbreak")                # 双引号,需要换行符转义

字段类型指定

可以为字段指定数据类型:

# 不带引号的字段
ip@source_ip: src
digit@port: port_num
time@timestamp: time

# 带引号的字段
ip@'@client-ip': client
digit@'user.age': age
chars@'event message': msg

支持的类型包括:

  • ip - IP 地址
  • digit - 整数
  • float - 浮点数
  • time - 时间戳
  • chars - 字符串
  • json - JSON 对象
  • kv - 键值对
  • 等等

字段别名

使用 : 为字段指定别名:

# 普通字段别名
@source_ip: src
@destination_ip: dst
@user_id: uid

# 特殊字段别名
@'@timestamp': time
@'event.type': event
@'log/level': level

# 带类型和别名
ip@'@client-ip': client_ip
digit@'user.age': age
chars@'user name': username

使用限制

1. 不支持双引号

只支持单引号,不支持双引号:

# ✅ 正确
@'@field-name'

# ❌ 错误
@"@field-name"

2. 转义字符限制

转义字符只在单引号字符串内有效:

# ✅ 正确 - 单引号内转义
@'user\'s name'

# ❌ 错误 - 普通字段名不支持转义
@user\'s_name

3. 空字段名

字段名不能为空:

# ❌ 错误
@''

# ✅ 正确
@'_'  # 使用下划线作为字段名

4. 嵌套引用

单引号不支持嵌套:

# ❌ 错误
@'field\'nested\''

# ✅ 正确 - 使用转义
@'field\'nested'

性能说明

解析性能

  • 普通字段名:零拷贝,性能最优

    @field_name  # 直接引用,无需分配
    
  • 单引号字段名:需要解码转义字符

    @'@field'    # 无转义字符,性能接近普通字段
    @'field\'s'  # 有转义字符,需要额外处理
    

性能对比

字段名类型解析时间内存分配推荐场景
普通字段名~10ns零拷贝优先使用
单引号(无转义)~15ns一次分配特殊字符
单引号(有转义)~30ns一次分配必要时使用

优化建议

  1. 优先使用普通字段名

    # ✅ 推荐
    @user_id
    @timestamp
    
    # ⚠️ 仅在必要时使用
    @'@timestamp'
    
  2. 避免不必要的转义

    # ✅ 推荐
    @'simple-field'
    
    # ⚠️ 避免
    @'field\twith\tescape'  # 仅在确实需要制表符时使用
    
  3. 批量操作时考虑性能

    # 大量字段解析时,优先使用普通字段名
    json(
        @user_id,      # 快
        @username,     # 快
        @'@metadata'   # 稍慢
    )
    

错误处理

常见错误

1. 字段名包含特殊字符但未使用引号

错误: 解析失败,意外的字符 '@'
原因: 字段名包含 @ 但未使用单引号
解决: @'@field-name'

2. 单引号未闭合

错误: 字符串未闭合
原因: 缺少结束的单引号
解决: 确保引号成对出现 @'field-name'

3. 转义字符错误

错误: 无效的转义序列
原因: 使用了不支持的转义字符
解决: 只使用支持的转义序列 \', \\, \n, \t, \r, \xHH

4. 空字段名

错误: 字段名不能为空
原因: @'' 或 @ 后无内容
解决: 提供有效的字段名

最佳实践

1. 命名规范

# ✅ 推荐 - 使用下划线分隔
@user_id
@client_ip
@event_timestamp

# ⚠️ 避免 - 除非必要
@'user id'
@'client-ip'

2. 保持一致性

# ✅ 推荐 - 统一风格
rule consistent_naming {
    json(
        @user_id,
        @user_name,
        @user_email
    )
}

# ⚠️ 避免 - 混合风格
rule inconsistent_naming {
    json(
        @user_id,
        @'user name',
        @userEmail
    )
}

3. 文档化特殊字段

# ✅ 推荐 - 添加注释说明
rule documented {
    json(
        # Elasticsearch 的 @timestamp 字段
        @'@timestamp': time,

        # 日志级别(包含空格)
        @'log level': level
    )
}

4. 使用类型前缀

# ✅ 推荐 - 明确指定类型
time@'@timestamp': time
ip@'@client-ip': client
chars@'event message': msg

5. 别名使用规范

# ✅ 推荐 - 使用简短的别名
@'very.long.nested.field.name': short_name
@'@timestamp': time
@'event.action': action

# ⚠️ 避免 - 别名过长
@'@timestamp': timestamp_value_from_elasticsearch

调试技巧

1. 逐步验证字段引用

# 第一步:验证单个字段
rule test_single {
    json(@'@timestamp')
}

# 第二步:添加更多字段
rule test_multiple {
    json(
        @'@timestamp',
        @'event.type'
    )
}

# 第三步:添加类型和别名
rule test_complete {
    time@'@timestamp': time,
    chars@'event.type': event
}

2. 检查字段名拼写

# 使用 JSON 工具查看原始字段名
echo '{"@timestamp": "2024-01-01"}' | jq 'keys'

# 输出: ["@timestamp"]
# WPL 中使用: @'@timestamp'

3. 测试转义字符

# 逐个测试转义字符
@'test\'quote'      # 单引号
@'test\\backslash'  # 反斜杠
@'test\nnewline'    # 换行符

4. 使用调试模式

# 使用 WP-Motor 调试模式查看解析结果
wp-motor --debug rule.wpl < test.log

常见问题 (FAQ)

Q1: 何时必须使用单引号?

当字段名包含以下字符时必须使用单引号:

  • @、空格、逗号、等号、括号、尖括号、井号等特殊字符

Q2: 单引号和双引号有什么区别?

WPL 只支持单引号 ' 用于字段名引用。双引号 " 用于其他语法元素(如作用域标记)。

Q3: 如何在字段名中包含单引号?

使用反斜杠转义:@'user\'s name'

Q4: 性能影响有多大?

对于大多数应用场景,性能影响可忽略不计(纳秒级差异)。只在极高性能要求时才需要考虑。

Q5: 可以使用变量作为字段名吗?

不可以,字段名必须是静态的字面量。

Q6: 如何处理动态字段名?

使用通配符或字段组合:

@items[*]/name     # 匹配所有数组元素的 name 字段
@'prefix*'         # 匹配以 prefix 开头的字段(如果支持)

Q7: 支持 Unicode 字符吗?

支持,字段名可以包含任意 Unicode 字符:

@'用户名称'
@'événement'
@'フィールド'

更多资源

  • 分隔符使用指南: docs/usage/wpl/separator.md
  • chars_replace 使用指南: docs/usage/wpl/chars_replace.md
  • WPL Field Function 开发指南: docs/guide/wpl_field_func_development_guide.md
  • 源代码:
    • crates/wp-lang/src/parser/utils.rs (take_ref_path_or_quoted)
    • crates/wp-lang/src/parser/wpl_field.rs (wpl_id_field)

版本历史

  • 1.11.0 (2026-01-29)
    • 新增单引号字段名支持(@'@special-field'
    • 新增 take() 函数单引号和双引号支持
    • 支持 take("@field")take('@field') 语法
    • 添加转义字符支持(\", \', \\, \n, \t
    • 添加完整的测试覆盖

提示: 优先使用普通字段名以获得最佳性能,仅在字段名包含特殊字符时使用引号。

WPL 分隔符使用指南

概述

分隔符(Separator)是 WPL 中用于分割日志字段的关键语法元素。通过灵活使用分隔符,可以解析各种格式的结构化日志数据。

快速开始

基本语法

| take(field_name) separator

分隔符写在字段定义之后,用于指示该字段在何处结束。

简单示例

# 使用空格分隔
| take(ip) \s
| take(method) \s
| take(path)

# 使用逗号分隔
| take(name) ,
| take(age) ,
| take(city)

内置分隔符

1. 空格分隔符 \s

匹配单个空格字符。

# 输入: "192.168.1.1 GET /api/users"
rule parse_log {
    | take(ip) \s
    | take(method) \s
    | take(path)
}

适用场景

  • Apache/Nginx 访问日志
  • 空格分隔的简单日志
  • 标准格式日志文件

2. 制表符分隔符 \t

匹配单个制表符(Tab)字符。

# 输入: "user001\t25\tBeijing"
rule parse_tsv {
    | take(user_id) \t
    | take(age) \t
    | take(city)
}

适用场景

  • TSV(Tab-Separated Values)文件
  • 数据库导出文件
  • 制表符对齐的日志

3. 通用空白分隔符 \S

匹配空格制表符(二选一)。

# 输入: "field1 field2\tfield3"(混合空格和制表符)
rule parse_flexible {
    | take(col1) \S
    | take(col2) \S
    | take(col3)
}

适用场景

  • 格式不统一的日志(混合空格和 Tab)
  • 手工编辑过的配置文件
  • 宽松的数据解析

行为说明

  • 遇到空格或制表符都会停止
  • 灵活处理格式不一致的数据源

4. 行尾分隔符 \0

读取到行尾(换行符或字符串结束)。

# 输入: "prefix_value some remaining text until end"
rule parse_to_end {
    | take(prefix) _
    | take(remaining) \0
}

适用场景

  • 解析最后一个字段
  • 读取消息正文
  • 获取剩余所有内容

别名

  • \00 等价

5. 自定义字符分隔符

使用任意单个字符作为分隔符。

# 逗号分隔
| take(name) ,
| take(age) ,

# 竖线分隔
| take(id) |
| take(status) |

# 分号分隔
| take(key) ;
| take(value) ;

支持的字符

  • 逗号 ,
  • 竖线 |
  • 分号 ;
  • 冒号 :
  • 等号 =
  • 斜杠 /
  • 等任意单字符

6. 自定义字符串分隔符

使用多字符字符串作为分隔符。

# 使用 " | " 分隔
| take(field1) " | "
| take(field2) " | "

# 使用 " :: " 分隔
| take(module) " :: "
| take(function) " :: "

适用场景

  • 格式化输出的日志
  • 特定格式协议
  • 需要明确边界的数据

实际应用场景

场景 1:解析 Nginx 访问日志

# 日志格式: 192.168.1.1 - - [29/Jan/2024:10:30:45 +0800] "GET /api/users HTTP/1.1" 200 1234
rule nginx_access_log {
    | take(client_ip) \s
    | take(identity) \s
    | take(user) \s
    | take(timestamp) \s
    | take(request) \s
    | take(status_code) \s
    | take(bytes_sent) \0
}

场景 2:解析 TSV 数据

# 输入: "2024-01-29\t10:30:45\tERROR\tDatabase connection failed"
rule tsv_log {
    | take(date) \t
    | take(time) \t
    | take(level) \t
    | take(message) \0
}

场景 3:解析 CSV 数据

# 输入: "John Smith,30,New York,Engineer"
rule csv_parser {
    | take(name) ,
    | take(age) ,
    | take(city) ,
    | take(job) \0
}

场景 4:解析结构化日志

# 输入: "level=error | module=database | msg=Connection timeout"
rule structured_log {
    | take(level_prefix) =
    | take(level_value) " | "
    | take(module_prefix) =
    | take(module_value) " | "
    | take(msg_prefix) =
    | take(message) \0
}

场景 5:处理混合空白的日志

# 输入: "192.168.1.1 \tGET\t /api/data"(混合空格和制表符)
rule flexible_whitespace {
    | take(ip) \S
    | take(method) \S
    | take(path) \0
}

场景 6:解析 Syslog 格式

# 输入: "Jan 29 10:30:45 hostname app[1234]: Error message here"
rule syslog {
    | take(month) \s
    | take(day) \s
    | take(time) \s
    | take(hostname) \s
    | take(app_tag) ": "
    | take(message) \0
}

场景 7:解析键值对日志

# 输入: "user=admin;action=login;ip=192.168.1.1;status=success"
rule kv_log {
    | take(user_key) =
    | take(user_value) ;
    | take(action_key) =
    | take(action_value) ;
    | take(ip_key) =
    | take(ip_value) ;
    | take(status_key) =
    | take(status_value) \0
}

分隔符优先级

WPL 支持三个级别的分隔符设置:

1. 字段级分隔符(优先级 3,最高)

| take(field1) ,  # 该字段使用逗号
| take(field2) \s # 该字段使用空格

2. 组级分隔符(优先级 2)

group {
    sep = \t  # 组内所有字段默认使用制表符
    | take(field1)
    | take(field2)
}

3. 继承分隔符(优先级 1,最低)

从上游规则继承的默认分隔符。

优先级规则

字段级 > 组级 > 继承级

group {
    sep = \t         # 组级:制表符
    | take(f1)       # 使用 \t
    | take(f2) ,     # 使用 ,(字段级覆盖组级)
    | take(f3)       # 使用 \t
}

分隔符行为

全局替换 vs 单次匹配

分隔符只在当前字段结束位置匹配一次:

# 输入: "hello,world,test"
| take(first) ,   # 读取 "hello",消费第一个逗号
| take(second) ,  # 读取 "world",消费第二个逗号
| take(third) \0  # 读取 "test"

分隔符消费行为

默认情况下,分隔符会被消费(从输入中移除):

# 输入: "a,b,c"
| take(x) ,  # 读取 "a",消费 ",",剩余 "b,c"
| take(y) ,  # 读取 "b",消费 ",",剩余 "c"

分隔符不存在的情况

如果到达字符串末尾仍未找到分隔符,读取到末尾:

# 输入: "field1 field2"
| take(f1) ,  # 未找到逗号,读取全部 "field1 field2"

高级用法

组合使用多种分隔符

# 输入: "192.168.1.1:8080/api/users?id=123"
rule url_parse {
    | take(ip) :
    | take(port) /
    | take(api_path) /
    | take(resource) ?
    | take(query_string) \0
}

处理可选字段

# 输入可能是: "user,30,city" 或 "user,,city"(age 为空)
rule optional_fields {
    | take(name) ,
    | take(age) ,      # 可能为空字符串
    | take(city) \0
}

跳过不需要的字段

# 只提取第 1 和第 3 个字段
rule skip_fields {
    | take(field1) ,
    | take(_skip) ,    # 临时变量,不保存
    | take(field3) \0
}

使用限制

1. 分隔符不支持正则表达式

# ❌ 不支持正则
| take(field) [0-9]+

# ✅ 使用固定字符串
| take(field) \s

2. 分隔符区分大小写

# "ABC" 和 "abc" 是不同的分隔符
| take(field1) ABC
| take(field2) abc

3. 空字符串不能作为分隔符

# ❌ 不支持
| take(field) ""

# ✅ 使用 \0 读取到末尾
| take(field) \0

4. 转义字符限制

当前支持的转义字符:

  • \s - 空格
  • \t - 制表符
  • \S - 空格或制表符
  • \0 - 行尾

其他转义字符(如 \n\r)需要使用实际字符。

性能说明

单字符分隔符

性能最优,推荐优先使用:

| take(f1) ,
| take(f2) \s
| take(f3) \t
  • 时间复杂度:O(n)
  • 扫描速度:约 500 MB/s

多字符分隔符

性能略低,但仍然高效:

| take(f1) " | "
| take(f2) " :: "
  • 时间复杂度:O(n × m),m 为分隔符长度
  • 扫描速度:约 300-400 MB/s

通用空白分隔符 \S

需要逐字符检查,性能介于两者之间:

| take(f1) \S
  • 时间复杂度:O(n)
  • 扫描速度:约 400 MB/s

错误处理

常见错误

1. 分隔符未找到

错误: 未找到分隔符 ','
原因: 输入字符串中不包含指定的分隔符
解决: 检查输入格式或使用 \0 读取到末尾

2. 分隔符语法错误

错误: invalid separator
原因: 使用了不支持的分隔符语法
解决: 参考本文档使用正确的分隔符格式

3. 字段顺序错误

错误: 字段解析失败
原因: 字段定义顺序与实际数据不匹配
解决: 调整字段顺序以匹配输入格式

最佳实践

1. 优先使用内置分隔符

# ✅ 推荐
| take(f1) \s
| take(f2) \t

# ⚠️ 避免(除非必要)
| take(f1) " "
| take(f2) "\t"

2. 明确指定最后字段的分隔符

# ✅ 推荐(明确到行尾)
| take(message) \0

# ⚠️ 不清晰
| take(message)  # 依赖默认行为

3. 使用 \S 处理不规范数据

# ✅ 推荐(兼容性好)
| take(field1) \S
| take(field2) \S

# ⚠️ 可能失败(如果混合了空格和制表符)
| take(field1) \s
| take(field2) \s

4. 复杂格式使用多字符分隔符

# ✅ 清晰准确
| take(level) " | "
| take(message) " | "

# ⚠️ 可能误匹配
| take(level) |
| take(message) |

5. 组合使用字段级和组级分隔符

# ✅ 推荐(减少重复)
group {
    sep = ,
    | take(f1)
    | take(f2)
    | take(f3) \0  # 最后字段使用不同分隔符
}

调试技巧

1. 逐字段验证

# 先解析第一个字段
| take(field1) ,

# 确认成功后添加第二个
| take(field1) ,
| take(field2) ,

# 依次添加...

2. 使用临时字段查看中间结果

| take(field1) ,
| take(_debug) ,    # 临时字段,查看剩余内容
| take(field2) \0

3. 打印分隔符位置

在测试环境中,使用调试模式查看分隔符匹配情况:

wp-motor --debug rule.wpl < test.log

4. 验证分隔符字符

对于不可见字符(如制表符),使用十六进制查看器确认:

# 查看文件中的制表符
cat -A test.log
# 或
hexdump -C test.log | head

常见问题 (FAQ)

Q1: \s\S 有什么区别?

  • \s:只匹配空格 (space)
  • \S:匹配空格或制表符 (space OR tab)
# 输入: "a b"
| take(x) \s  # ✅ 匹配成功

# 输入: "a\tb"
| take(x) \s  # ❌ 匹配失败(这是制表符,不是空格)
| take(x) \S  # ✅ 匹配成功

Q2: 如何处理连续的分隔符?

WPL 会将连续分隔符视为多个空字段:

# 输入: "a,,c"
| take(f1) ,  # 读取 "a"
| take(f2) ,  # 读取 ""(空字符串)
| take(f3) \0 # 读取 "c"

Q3: 分隔符会影响性能吗?

单字符分隔符性能最优,多字符分隔符略慢,但对于大多数场景影响可忽略。

Q4: 如何解析嵌套结构?

使用多级分隔符:

# 输入: "k1=v1;k2=v2|k3=v3;k4=v4"
rule nested {
    | take(group1) |
    | take(group2) \0
}
# 然后在每个 group 内再用 ; 和 = 解析

Q5: 分隔符可以是中文吗?

可以,支持 Unicode 字符:

# 使用中文逗号分隔
| take(field1) ,
| take(field2) ,

Q6: \0 和省略分隔符有区别吗?

建议显式使用 \0,语义更清晰:

# ✅ 推荐(明确)
| take(message) \0

# ⚠️ 可以但不明确
| take(message)

Q7: 如何处理引号内的分隔符?

对于包含引号的复杂格式,建议使用专门的解析器(如 JSON、KV 解析器):

# 复杂 CSV(带引号)
# 输入: "field1","field with , comma","field3"
# 建议使用 CSV 解析器而非手动分隔符

更多资源

  • WPL 语法参考: docs/guide/wpl_syntax.md
  • 解析器开发指南: docs/guide/wpl_field_func_development_guide.md
  • chars_replace 使用指南: docs/usage/wpl/chars_replace.md
  • 源代码: crates/wp-lang/src/ast/syntax/wpl_sep.rs

版本历史

  • 1.11.0 (2026-01-29)

    • 新增 \t 制表符分隔符支持
    • 新增 \S 通用空白分隔符(空格或制表符)
    • 优化 Whitespace 分隔符性能
    • 添加完整的测试覆盖
  • 1.10.x 及更早版本

    • 支持 \s(空格)和 \0(行尾)
    • 支持自定义字符和字符串分隔符

提示: 分隔符是 WPL 解析的核心,选择合适的分隔符可以大大简化日志解析规则。

chars_replace 函数使用指南

概述

chars_replace 是 WPL (WP Language) 中的字符串替换函数,用于在日志字段中查找并替换指定的子字符串。

快速开始

基本语法

chars_replace(target, replacement)
  • target: 要查找并替换的字符串
  • replacement: 替换后的新字符串

简单示例

# 将 "error" 替换为 "warning"
chars_replace(error, warning)

# 将 "ERROR" 替换为 "WARN"
chars_replace(ERROR, WARN)

参数格式

1. 不带引号(简单标识符)

适用于简单的字段名或关键词:

chars_replace(old_value, new_value)
chars_replace(test-old, test-new)
chars_replace(错误, 警告)

支持的字符

  • 字母(a-z, A-Z)
  • 数字(0-9)
  • 下划线(_)
  • 点(.)
  • 斜杠(/)
  • 连字符(-)
  • Unicode 字符(中文、日文等)

2. 带引号(特殊字符)

适用于包含特殊字符的字符串:

chars_replace("test,old", "test,new")         # 包含逗号
chars_replace("hello world", "goodbye world") # 包含空格
chars_replace("status=error", "status=ok")    # 包含等号
chars_replace("[ERROR]", "[WARN]")            # 包含方括号

必须使用引号的场景

  • 包含逗号(,)
  • 包含空格
  • 包含等号(=)
  • 包含方括号([])
  • 包含其他特殊符号

3. 混合使用

可以混合使用带引号和不带引号的参数:

chars_replace("test,old", new_value)
chars_replace(old_value, "new,value")

4. 空字符串(删除文本)

使用空引号删除目标字符串:

# 删除 "DEBUG: " 前缀
chars_replace("DEBUG: ", "")

# 删除逗号
chars_replace(",", "")

实际应用场景

场景 1:标准化日志级别

# 统一大小写
chars_replace(error, ERROR)
chars_replace(warning, WARNING)

# 标准化格式
chars_replace("[ERROR]", "ERROR:")
chars_replace("[WARN]", "WARNING:")

场景 2:清理日志内容

# 删除调试前缀
chars_replace("DEBUG: ", "")

# 删除多余的空格
chars_replace("  ", " ")

# 删除换行符
chars_replace("\n", " ")

场景 3:URL 参数替换

chars_replace("status=error", "status=ok")
chars_replace("code=500", "code=200")

场景 4:CSV 字段处理

# 替换带逗号的名字
chars_replace("Smith, John", "John Smith")
chars_replace("Doe, Jane", "Jane Doe")

场景 5:路径标准化

# Windows 路径转 Unix 路径
chars_replace("\\", "/")

# 简化路径
chars_replace("/usr/local/", "/opt/")

场景 6:多语言支持

# 中文替换
chars_replace(错误, 警告)
chars_replace("错误:", "警告:")

# 日文替换
chars_replace(エラー, 警告)

场景 7:敏感信息脱敏

# 替换密码
chars_replace("password=12345", "password=***")

# 替换令牌
chars_replace("token=abc123xyz", "token=***")

使用限制

不支持的特性

  1. 转义字符

    # ❌ 不支持(会解析错误)
    chars_replace("say \"hello\"", "say 'hi'")
    
  2. 正则表达式

    # ❌ 不支持正则
    chars_replace("[0-9]+", "NUMBER")  # 会按字面匹配
    
  3. 通配符

    # ❌ 不支持通配符
    chars_replace("error*", "warning")  # 会按字面匹配
    

类型限制

chars_replace 只能处理字符串类型的字段:

# ✅ 正确 - 字段是字符串
message: "error occurred" -> chars_replace(error, warning)

# ❌ 错误 - 字段是数字
status_code: 500 -> chars_replace(500, 200)  # 会失败

# ❌ 错误 - 字段是 IP 地址
ip_address: 192.168.1.1 -> chars_replace(192, 10)  # 会失败

替换行为

  • 全局替换:替换字段中所有匹配的子字符串

    # 输入: "hello hello hello"
    chars_replace(hello, hi)
    # 输出: "hi hi hi"
    
  • 大小写敏感:区分大小写

    # 输入: "Error error ERROR"
    chars_replace(error, warning)
    # 输出: "Error warning ERROR"  # 只替换小写的 error
    

完整示例

示例 1:日志级别标准化流水线

rule log_normalization {
    # 标准化不同格式的 ERROR
    | chars_replace("[ERROR]", "ERROR:")
    | chars_replace("ERR:", "ERROR:")
    | chars_replace("Err:", "ERROR:")

    # 标准化 WARNING
    | chars_replace("[WARN]", "WARNING:")
    | chars_replace("Warn:", "WARNING:")

    # 删除调试信息
    | chars_replace("DEBUG: ", "")
}

示例 2:CSV 数据清理

rule csv_cleanup {
    # 标准化姓名格式(从 "Last, First" 到 "First Last")
    | chars_replace("Smith, John", "John Smith")
    | chars_replace("Doe, Jane", "Jane Doe")

    # 删除多余的引号
    | chars_replace("\"", "")

    # 标准化分隔符
    | chars_replace(";", ",")
}

示例 3:多步骤替换

rule multi_step_replace {
    # 第一步:替换日志级别
    | chars_replace(error, ERROR)

    # 第二步:添加时间戳前缀(通过替换空字符串)
    | chars_replace("", "[2024-01-29] ")

    # 第三步:替换服务名
    | chars_replace(old-service, new-service)
}

性能说明

  • 时间复杂度:O(n) - n 为字段长度
  • 空间复杂度:O(n) - 需要创建新字符串
  • 性能建议
    • 短字符串(< 1KB):性能优秀,延迟 < 1μs
    • 长字符串(1-10KB):仍然快速,延迟 < 10μs
    • 超长字符串(> 10KB):考虑性能影响

错误处理

常见错误

  1. 字段不存在

    错误: chars_replace | no active field
    原因: 当前没有活动字段
    解决: 使用 take() 或其他选择器先选择字段
    
  2. 字段类型不匹配

    错误: chars_replace
    原因: 字段不是字符串类型
    解决: 确保字段是 Chars 类型
    
  3. 语法错误

    错误: invalid symbol, expected need ','
    原因: 包含逗号的参数未使用引号
    解决: 使用引号包裹参数
    

与其他函数配合使用

与字段选择器配合

# 先选择字段,再替换
| take(message)
| chars_replace(error, warning)

与条件检查配合

# 只在特定条件下替换
| chars_has(error)
| chars_replace(error, warning)

与转换函数配合

# 先解码 Base64,再替换
| base64_decode()
| chars_replace(old_value, new_value)

最佳实践

1. 优先使用不带引号的格式

# ✅ 推荐(简洁)
chars_replace(error, warning)

# ⚠️ 可以但不必要
chars_replace("error", "warning")

2. 复杂字符串使用引号

# ✅ 正确
chars_replace("status=error", "status=ok")

# ❌ 错误(语法错误)
chars_replace(status=error, status=ok)

3. 空字符串删除文本

# ✅ 推荐(明确意图)
chars_replace("DEBUG: ", "")

# ⚠️ 不清晰
chars_replace("DEBUG: ", nothing)  # 不存在 nothing 关键字

4. 按顺序执行多次替换

# ✅ 正确(逐步替换)
| chars_replace(error, ERROR)
| chars_replace(ERROR, WARNING)
# 结果: error -> ERROR -> WARNING

# ⚠️ 注意顺序
| chars_replace(ERROR, WARNING)
| chars_replace(error, ERROR)
# 结果: error -> ERROR(第二步不会再变成 WARNING)

5. 测试边界情况

# 测试空字符串
chars_replace("", "prefix")  # 在每个字符间插入

# 测试单字符
chars_replace(",", ";")      # 简单替换

# 测试长字符串
chars_replace("very long string to find", "replacement")

调试技巧

1. 逐步测试

# 第一步:只做替换
| chars_replace(error, warning)

# 第二步:添加更多替换
| chars_replace(error, warning)
| chars_replace(warning, info)

2. 检查字段类型

# 使用 has() 确认字段存在
| has()

# 使用 chars_has() 确认是字符串类型
| chars_has(some_value)

3. 查看替换结果

在测试环境中打印替换前后的值:

# 使用 WP-Motor 的调试模式
wp-motor --debug rule.wpl < test.log

常见问题 (FAQ)

Q1: 如何替换换行符?

# 方法 1:使用实际的换行符(如果解析器支持)
chars_replace("\n", " ")

# 方法 2:根据实际编码处理
chars_replace("
", " ")  # 实际换行

Q2: 如何同时替换多个不同的字符串?

# 使用多个 chars_replace 调用
| chars_replace(error, ERROR)
| chars_replace(warning, WARNING)
| chars_replace(info, INFO)

Q3: 如何实现大小写不敏感的替换?

chars_replace 是大小写敏感的,需要多次调用:

| chars_replace(error, ERROR)
| chars_replace(Error, ERROR)
| chars_replace(ERROR, ERROR)

Q4: 替换会修改原始字段吗?

是的,chars_replace 会直接修改活动字段的值。

Q5: 性能够用吗?

对于大多数日志处理场景,性能完全足够:

  • 单条日志 < 10KB:几乎无感知
  • 高吞吐量场景:可处理 100K+ 日志/秒

更多资源

  • 开发指南: docs/guide/wpl_field_func_development_guide.md
  • 解析器实现: docs/guide/chars_replace_parser_tests.md
  • 性能分析: docs/guide/take_quoted_string_performance.md
  • 源代码: crates/wp-lang/src/ast/processor/function.rs

版本历史

  • 1.11.0 (2026-01-29)
    • 初始实现
    • 支持基本字符串替换
    • 支持带引号字符串(包含逗号、空格等)
    • 添加完整的测试覆盖

提示: 如果您在使用过程中遇到问题,请参考错误处理章节或查看开发指南。

regex_match 函数使用指南

概述

regex_match 是 WPL (WP Language) 中的正则表达式匹配函数,用于检查日志字段的字符串内容是否匹配指定的正则表达式模式。使用 Rust 的 regex 引擎,支持完整的正则表达式语法。

快速开始

基本语法

regex_match('pattern')
  • pattern: 正则表达式模式(推荐使用单引号
  • 匹配成功返回 Ok,失败返回 Err

简单示例

# 匹配纯数字
regex_match('^\d+$')

# 匹配邮箱格式
regex_match('^\w+@\w+\.\w+$')

# 匹配 IP 地址
regex_match('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')

# 匹配 HTTP 方法
regex_match('^(GET|POST|PUT|DELETE)$')

重要提示:引号使用

推荐:使用单引号(原始字符串)

# ✅ 推荐:单引号不处理转义,适合正则表达式
regex_match('^\d+$')           # \d 保持原样
regex_match('^\w+@\w+\.\w+$')  # \w, \. 保持原样
regex_match('^[A-Z]+\d+$')     # 完美工作

避免:双引号会导致转义问题

# ❌ 错误:双引号会尝试转义 \d
regex_match("^\d+$")  # 解析失败!\d 不是有效的转义序列

# ❌ 错误:\w 也会失败
regex_match("^\w+$")  # 解析失败!

原因:WPL 的双引号字符串解析器只支持 \", \\, \n, \t 这几种转义字符,而正则表达式中的 \d, \w, \s 等会导致解析错误。

正则表达式语法

regex_match 使用 Rust regex 引擎,支持以下特性:

1. 基本匹配

# 字面字符
regex_match('hello')          # 匹配 "hello"
regex_match('error')          # 匹配 "error"

2. 字符类

# 数字
regex_match('\d')             # 匹配任意数字 [0-9]
regex_match('\d+')            # 匹配一个或多个数字
regex_match('\d{3}')          # 匹配恰好3个数字

# 字母和数字
regex_match('\w')             # 匹配 [a-zA-Z0-9_]
regex_match('\w+')            # 匹配一个或多个单词字符

# 空白字符
regex_match('\s')             # 匹配空格、制表符、换行符
regex_match('\s+')            # 匹配一个或多个空白字符

# 自定义字符类
regex_match('[a-z]')          # 匹配小写字母
regex_match('[A-Z]')          # 匹配大写字母
regex_match('[0-9]')          # 匹配数字
regex_match('[a-zA-Z0-9]')    # 匹配字母或数字

3. 量词

# * - 0次或多次
regex_match('a*')             # 匹配 "", "a", "aa", "aaa"...

# + - 1次或多次
regex_match('a+')             # 匹配 "a", "aa", "aaa"... (不匹配空串)

# ? - 0次或1次
regex_match('colou?r')        # 匹配 "color" 或 "colour"

# {n} - 恰好n次
regex_match('\d{4}')          # 匹配4位数字

# {n,m} - n到m次
regex_match('\d{2,4}')        # 匹配2到4位数字

# {n,} - 至少n次
regex_match('\d{3,}')         # 匹配3位或更多数字

4. 锚点

# ^ - 字符串开始
regex_match('^\d+')           # 必须以数字开头

# $ - 字符串结束
regex_match('\d+$')           # 必须以数字结尾

# ^...$ - 完全匹配
regex_match('^\d+$')          # 整个字符串必须是数字

5. 分组和选择

# (...) - 分组
regex_match('(ab)+')          # 匹配 "ab", "abab", "ababab"...

# | - 选择(或)
regex_match('cat|dog')        # 匹配 "cat" 或 "dog"
regex_match('^(GET|POST)$')   # 匹配 "GET" 或 "POST"

# (?:...) - 非捕获分组
regex_match('(?:ab)+')        # 与 (ab)+ 功能相同,但不捕获

6. 特殊字符转义

# 转义元字符
regex_match('\.')             # 匹配点号 .
regex_match('\[')             # 匹配左方括号 [
regex_match('\(')             # 匹配左括号 (
regex_match('\$')             # 匹配美元符号 $
regex_match('\*')             # 匹配星号 *

7. 标志

# (?i) - 大小写不敏感
regex_match('(?i)error')      # 匹配 "error", "ERROR", "Error"

# (?m) - 多行模式
regex_match('(?m)^line')      # ^ 匹配每行开始

# (?s) - 单行模式(. 匹配换行符)
regex_match('(?s).*')         # . 可以匹配换行符

实际应用场景

场景 1:日志级别匹配

rule log_level_filter {
    # 选择日志消息字段
    | take(message)

    # 匹配包含 ERROR 或 FATAL 的消息(大小写不敏感)
    | regex_match('(?i)(error|fatal)')
}

# 示例数据:
# message: "Error occurred"     → ✅ 匹配
# message: "FATAL exception"    → ✅ 匹配
# message: "Warning message"    → ❌ 不匹配

场景 2:邮箱地址验证

rule email_validation {
    # 选择邮箱字段
    | take(email)

    # 验证邮箱格式
    | regex_match('^\w+(\.\w+)*@\w+(\.\w+)+$')
}

# 示例数据:
# email: "user@example.com"           → ✅ 匹配
# email: "john.doe@company.co.uk"     → ✅ 匹配
# email: "invalid-email"              → ❌ 不匹配
# email: "@example.com"               → ❌ 不匹配

场景 3:IP 地址匹配

rule ip_address_filter {
    # 选择 IP 地址字段
    | take(client_ip)

    # 匹配内网 IP(192.168.x.x)
    | regex_match('^192\.168\.\d{1,3}\.\d{1,3}$')
}

# 示例数据:
# client_ip: "192.168.1.1"    → ✅ 匹配
# client_ip: "192.168.0.100"  → ✅ 匹配
# client_ip: "10.0.0.1"       → ❌ 不匹配
# client_ip: "8.8.8.8"        → ❌ 不匹配

场景 4:URL 路径过滤

rule api_endpoint_filter {
    # 选择请求路径
    | take(path)

    # 匹配 API 端点(/api/v1/...)
    | regex_match('^/api/v\d+/')
}

# 示例数据:
# path: "/api/v1/users"         → ✅ 匹配
# path: "/api/v2/products"      → ✅ 匹配
# path: "/static/image.png"     → ❌ 不匹配

场景 5:时间戳格式验证

rule timestamp_validation {
    # 选择时间戳字段
    | take(timestamp)

    # 匹配 ISO 8601 格式(YYYY-MM-DD HH:MM:SS)
    | regex_match('^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$')
}

# 示例数据:
# timestamp: "2024-01-29 15:30:45"  → ✅ 匹配
# timestamp: "2024-1-9 5:3:5"       → ❌ 不匹配(缺少前导零)
# timestamp: "29/01/2024 15:30"     → ❌ 不匹配(格式不同)

场景 6:HTTP 方法验证

rule http_method_check {
    # 选择 HTTP 方法字段
    | take(method)

    # 只接受安全的 HTTP 方法
    | regex_match('^(GET|HEAD|OPTIONS)$')
}

# 示例数据:
# method: "GET"     → ✅ 匹配
# method: "HEAD"    → ✅ 匹配
# method: "POST"    → ❌ 不匹配
# method: "DELETE"  → ❌ 不匹配

场景 7:版本号匹配

rule version_check {
    # 选择版本字段
    | take(version)

    # 匹配语义化版本号(如 1.2.3)
    | regex_match('^\d+\.\d+\.\d+$')
}

# 示例数据:
# version: "1.0.0"     → ✅ 匹配
# version: "2.10.5"    → ✅ 匹配
# version: "1.0"       → ❌ 不匹配(缺少补丁版本)
# version: "v1.2.3"    → ❌ 不匹配(有前缀)

场景 8:SQL 注入检测

rule sql_injection_detection {
    # 选择用户输入字段
    | take(user_input)

    # 检测常见的 SQL 注入模式
    | regex_match('(?i)(union|select|insert|update|delete|drop|;|--|\|)')
}

# 示例数据:
# user_input: "SELECT * FROM users"  → ✅ 匹配(检测到)
# user_input: "'; DROP TABLE --"     → ✅ 匹配(检测到)
# user_input: "normal search query"  → ❌ 不匹配(安全)

场景 9:文件扩展名过滤

rule image_file_filter {
    # 选择文件名字段
    | take(filename)

    # 只匹配图片文件
    | regex_match('\.(?i)(jpg|jpeg|png|gif|bmp|svg)$')
}

# 示例数据:
# filename: "photo.jpg"      → ✅ 匹配
# filename: "image.PNG"      → ✅ 匹配(大小写不敏感)
# filename: "document.pdf"   → ❌ 不匹配

场景 10:MAC 地址验证

rule mac_address_validation {
    # 选择 MAC 地址字段
    | take(mac)

    # 匹配 MAC 地址格式(XX:XX:XX:XX:XX:XX)
    | regex_match('^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')
}

# 示例数据:
# mac: "00:1B:44:11:3A:B7"  → ✅ 匹配
# mac: "AA:BB:CC:DD:EE:FF"  → ✅ 匹配
# mac: "invalid-mac"        → ❌ 不匹配

使用限制

类型限制

regex_match 只能处理字符串类型的字段:

# ✅ 正确 - 字段是字符串
message: "error occurred" -> regex_match('error')

# ❌ 错误 - 字段是数字
status_code: 404 -> regex_match('\d+')  # 会失败

# ❌ 错误 - 字段是 IP 地址(非字符串类型)
ip: 192.168.1.1 -> regex_match('\d+')  # 会失败

性能考虑

  1. 正则表达式编译开销

    • 每次调用都会重新编译正则表达式
    • 复杂的正则表达式编译可能需要几微秒
  2. 匹配性能

    • 简单模式:微秒级
    • 复杂模式(大量回溯):可能较慢
    • 建议:避免过度复杂的正则表达式
  3. 优化建议

    # ✅ 推荐:简单直接的模式
    regex_match('^\d{4}$')
    
    # ⚠️ 慎用:复杂的回溯模式
    regex_match('^(a+)+b$')  # 可能导致性能问题
    

不支持的特性

  1. 不支持命名捕获组

    # ❌ 不支持(无法提取捕获的内容)
    regex_match('(?P<year>\d{4})')
    
  2. 不支持替换

    # ❌ regex_match 只做匹配,不做替换
    # 需要替换请使用 chars_replace
    
  3. 不支持多个模式

    # ❌ 不能传递多个模式
    regex_match('pattern1', 'pattern2')
    
    # ✅ 使用选择符 |
    regex_match('pattern1|pattern2')
    

完整示例

示例 1:日志分类流水线

rule log_classification {
    # 选择日志消息
    | take(message)

    # 分类为错误日志
    (
        | regex_match('(?i)(error|exception|failed|fatal)')
        | tag(level, ERROR)
    )
    |
    # 或分类为警告日志
    (
        | regex_match('(?i)(warn|warning|deprecated)')
        | tag(level, WARNING)
    )
    |
    # 或分类为普通日志
    (
        | tag(level, INFO)
    )
}

示例 2:安全审计过滤

rule security_audit {
    # 检查用户输入中的危险模式
    | take(user_input)

    # 检测脚本注入
    | regex_match('(?i)(<script|javascript:|onerror=)')

    # 或检测 SQL 注入
    | regex_match('(?i)(union|select.*from|insert.*into)')

    # 或检测路径遍历
    | regex_match('(\.\./|\.\.\\)')

    # 匹配到任何一个就记录为安全事件
    | tag(security_event, true)
}

示例 3:结构化日志解析

rule structured_log_parsing {
    # 验证 JSON 日志格式
    | take(raw_message)
    | regex_match('^\{.*\}$')

    # 验证包含必需字段
    | regex_match('"timestamp":\s*"\d{4}-\d{2}-\d{2}')
    | regex_match('"level":\s*"(INFO|WARN|ERROR)"')
    | regex_match('"message":\s*"[^"]+"')

    # 所有验证通过后继续处理
}

性能说明

  • 正则编译:每次调用都会编译,建议使用简单模式
  • 匹配速度
    • 简单模式(如 ^\d+$):< 1μs
    • 中等复杂度(如邮箱验证):1-10μs
    • 复杂模式(大量回溯):可能 > 100μs
  • 内存开销:每个正则表达式约 1-10KB

错误处理

常见错误

  1. 无效的正则表达式

    错误: regex_match | invalid regex pattern
    原因: 正则表达式语法错误
    解决: 检查正则表达式语法
    
  2. 字段不存在

    错误: regex_match | no active field
    原因: 当前没有活动字段
    解决: 使用 take() 先选择字段
    
  3. 字段类型不匹配

    错误: regex_match | field is not a string
    原因: 字段不是字符串类型
    解决: 确保字段是 Chars 类型
    
  4. 模式不匹配

    错误: regex_match | not matched
    原因: 字段内容不匹配正则表达式
    解决: 这是正常的过滤逻辑
    

与其他函数配合使用

与字段选择器配合

# 先选择字段,再匹配
| take(message)
| regex_match('error')

与条件检查配合

# 组合多个条件
| take(status)
| regex_match('^[45]\d{2}$')  # 4xx 或 5xx

与 chars_replace 配合

# 先匹配,再替换
| regex_match('error')
| chars_replace(error, ERROR)

与分支逻辑配合

# 不同模式走不同分支
(
    | regex_match('^2\d{2}$')  # 2xx
    | tag(status_class, success)
)
|
(
    | regex_match('^4\d{2}$')  # 4xx
    | tag(status_class, client_error)
)

最佳实践

1. 优先使用单引号

# ✅ 推荐
regex_match('^\d+$')

# ❌ 避免(会导致解析错误)
regex_match("^\d+$")

2. 使用锚点明确匹配范围

# ✅ 推荐:完全匹配
regex_match('^\d{4}$')  # 恰好4位数字

# ⚠️ 可能不符合预期:部分匹配
regex_match('\d{4}')    # 包含4位数字即可

3. 简化正则表达式

# ✅ 推荐:简单清晰
regex_match('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')

# ⚠️ 过度复杂
regex_match('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')

4. 使用注释文档化复杂模式

# 匹配邮箱:用户名@域名.后缀
regex_match('^\w+@\w+\.\w+$')

5. 测试边界情况

# 测试模式的边界
regex_match('^\d{2,4}$')
# 测试:1 ❌, 12 ✅, 123 ✅, 1234 ✅, 12345 ❌

正则表达式测试

在线测试工具

  1. regex101.com

    • 选择 Rust flavor
    • 测试你的正则表达式
    • 查看匹配详情和性能
  2. regexr.com

    • 可视化匹配过程
    • 提供备忘清单

命令行测试

# 使用 WP-Motor 测试
echo "test_value: 12345" | wp-motor test.wpl

# 查看匹配结果
wp-motor --debug test.wpl < test_data.log

常见问题 (FAQ)

Q1: 为什么必须使用单引号?

因为 WPL 的双引号字符串解析器只支持有限的转义序列(\", \\, \n, \t),而正则表达式需要 \d, \w, \s 等,这些会导致解析失败。

Q2: 如何匹配点号(.)?

# 使用反斜杠转义
regex_match('\.')  # 匹配字面的点号

Q3: 如何实现大小写不敏感匹配?

# 使用 (?i) 标志
regex_match('(?i)error')  # 匹配 error, ERROR, Error

Q4: 正则表达式是完全匹配还是部分匹配?

默认是部分匹配。使用 ^$ 实现完全匹配:

# 部分匹配
regex_match('\d+')     # "abc123def" → ✅ 匹配

# 完全匹配
regex_match('^\d+$')   # "abc123def" → ❌ 不匹配

Q5: 如何匹配多行文本?

# 使用 (?m) 多行模式
regex_match('(?m)^ERROR')  # 匹配任意行开头的 ERROR

# 使用 (?s) 单行模式让 . 匹配换行符
regex_match('(?s)start.*end')  # 跨行匹配

Q6: 性能如何?

  • 简单模式:非常快(微秒级)
  • 复杂模式:可能较慢
  • 建议:避免过度复杂的回溯模式

Q7: 能否提取匹配的内容?

不支持。regex_match 只做匹配判断,不提取内容。

正则表达式速查表

常用模式

模式说明示例
\d数字\d+ 匹配 “123”
\w单词字符\w+ 匹配 “hello”
\s空白字符\s+ 匹配 “ “
.任意字符.* 匹配任何内容
^行首^start 必须开头匹配
$行尾end$ 必须结尾匹配
*0或多次a* 匹配 “”, “a”, “aa”
+1或多次a+ 匹配 “a”, “aa”
?0或1次a? 匹配 “”, “a”
{n}恰好n次\d{4} 匹配 “2024”
{n,m}n到m次\d{2,4} 匹配 “12”, “123”
[abc]字符集[aeiou] 匹配元音
[^abc]非字符集[^0-9] 匹配非数字
|选择cat|dog 匹配 “cat” 或 “dog”
()分组(ab)+ 匹配 “ab”, “abab”

更多资源

  • Rust Regex 文档: https://docs.rs/regex/
  • 开发指南: docs/guide/wpl_field_func_development_guide.md
  • 源代码: crates/wp-lang/src/ast/processor/function.rs
  • 测试用例: crates/wp-lang/src/eval/builtins/pipe_fun.rs

版本历史

  • 1.13.1 (2026-02-02)
    • 初始实现
    • 支持完整的 Rust regex 语法
    • 支持所有标准正则表达式特性
    • 添加完整的测试覆盖

提示: regex_match 功能强大但也可能影响性能。对于简单的字符串匹配,优先考虑使用 chars_haschars_in;对于数值范围检查,使用 digit_rangedigit_in。正则表达式适合复杂的模式匹配场景。

digit_range 函数使用指南

概述

digit_range 是 WPL (WP Language) 中的数值范围检查函数,用于判断日志字段的数值是否在指定的单个范围内。这是一个简单高效的闭区间检查函数。

快速开始

基本语法

digit_range(begin, end)
  • begin: 范围下界(标量值)
  • end: 范围上界(标量值)
  • 检查方式:begin <= value <= end(闭区间)

简单示例

# 检查数值是否在 [0, 100] 范围内
digit_range(0, 100)

# 检查 HTTP 成功状态码(200-299)
digit_range(200, 299)

# 检查端口号是否在标准范围内(0-65535)
digit_range(0, 65535)

参数格式

1. 单个范围

检查单个连续范围:

# 检查端口号是否在标准范围内(0-65535)
digit_range(0, 65535)

# 检查 HTTP 成功状态码(200-299)
digit_range(200, 299)

# 检查年龄是否成年(18-150)
digit_range(18, 150)

2. 范围限制说明

注意: 从 1.13.1 版本开始,digit_range 仅支持单个范围检查。如需检查多个不连续的范围,请使用多个规则或分支逻辑(见下文“与分支逻辑配合“部分)。

# ✅ 单个范围检查
digit_range(200, 299)  # 2xx 状态码

# ❌ 不再支持多范围数组语法
# digit_range([200, 300], [299, 399])  # 旧版本语法,已废弃

3. 边界值处理

范围检查是闭区间,包含边界值:

# [100, 200] - 包含 100 和 200
digit_range(100, 200)

# 检查值 100: ✅ 通过(等于下界)
# 检查值 200: ✅ 通过(等于上界)
# 检查值 150: ✅ 通过(在范围内)
# 检查值 99:  ❌ 失败(小于下界)
# 检查值 201: ❌ 失败(大于上界)

4. 负数范围

支持负数范围:

# 温度范围检查(-20°C 到 40°C)
digit_range(-20, 40)

# 海拔范围(死海 -400m 到 珠峰 8848m)
digit_range(-400, 8848)

实际应用场景

场景 1:HTTP 状态码分类

rule http_success_check {
    # 选择状态码字段
    | take(status_code)

    # 检查是否为成功状态码(2xx)
    | digit_range(200, 299)
}

# 示例数据:
# status_code: 200  → ✅ 通过(在 [200,299] 范围内)
# status_code: 204  → ✅ 通过(在 [200,299] 范围内)
# status_code: 301  → ❌ 失败(不在范围内)
# status_code: 404  → ❌ 失败(不在范围内)

# 如需检查多个状态码范围(如 2xx 或 3xx),使用分支逻辑:
rule http_ok_or_redirect {
    | take(status_code)
    | (digit_range(200, 299) | digit_range(300, 399))
}

场景 2:性能指标监控

rule response_time_check {
    # 选择响应时间字段(毫秒)
    | take(response_time)

    # 检查响应时间是否在正常范围(0-500ms)
    | digit_range(0, 500)
}

# 示例数据:
# response_time: 50   → ✅ 通过(快速响应)
# response_time: 200  → ✅ 通过(正常响应)
# response_time: 1000 → ❌ 失败(超时)

场景 3:端口号验证

rule system_port_check {
    # 选择端口字段
    | take(port)

    # 检查是否为系统保留端口(1-1023)
    | digit_range(1, 1023)
}

# 示例数据:
# port: 80    → ✅ 通过(HTTP 默认端口)
# port: 443   → ✅ 通过(HTTPS 默认端口)
# port: 8080  → ❌ 失败(用户端口)

场景 4:时间段过滤

rule morning_hours_check {
    # 选择小时字段
    | take(hour)

    # 检查是否在上午工作时间(9-12)
    | digit_range(9, 12)
}

# 示例数据:
# hour: 10  → ✅ 通过(上午工作时间)
# hour: 11  → ✅ 通过(上午工作时间)
# hour: 15  → ❌ 失败(下午时间)

# 检查多个时间段,使用分支逻辑:
rule business_hours_check {
    | take(hour)
    | (digit_range(9, 12) | digit_range(14, 18))
}

场景 5:年龄分段

rule adult_age_check {
    # 选择年龄字段
    | take(age)

    # 成年人年龄段(18-65)
    | digit_range(18, 65)
}

# 示例数据:
# age: 30  → ✅ 通过(成年人)
# age: 50  → ✅ 通过(成年人)
# age: 15  → ❌ 失败(未成年)
# age: 70  → ❌ 失败(老年人)

场景 6:优先级过滤

rule high_priority_filter {
    # 选择优先级字段
    | take(priority)

    # 只处理高优先级(1-3)
    | digit_range(1, 3)
}

# 示例数据:
# priority: 1  → ✅ 通过(高优先级)
# priority: 3  → ✅ 通过(高优先级)
# priority: 5  → ❌ 失败(中优先级)

场景 7:数据质量检查

rule data_quality_check {
    # 检查温度传感器数据
    | take(temperature)

    # 正常温度范围(-40°C 到 80°C)
    | digit_range(-40, 80)
}

# 示例数据:
# temperature: 25   → ✅ 通过(正常室温)
# temperature: -10  → ✅ 通过(冬季温度)
# temperature: 100  → ❌ 失败(异常数据)
# temperature: -100 → ❌ 失败(传感器故障)

使用限制

类型限制

digit_range 只能处理数值类型的字段:

# ✅ 正确 - 字段是数字
status_code: 200 -> digit_range(200, 299)

# ❌ 错误 - 字段是字符串
level: "200" -> digit_range(200, 299)  # 会失败

# ❌ 错误 - 字段是 IP 地址
ip: 192.168.1.1 -> digit_range(192, 200)  # 会失败

参数要求

  1. 参数必须是标量值

    # ✅ 正确 - 使用标量值
    digit_range(1, 10)
    
    # ❌ 错误 - 不支持数组参数(旧版本语法)
    digit_range([1], [10])  # 已废弃
    
  2. 范围下界应小于等于上界

    # ✅ 正确
    digit_range(1, 10)      # 1 <= x <= 10
    digit_range(10, 10)     # x == 10(单点)
    
    # ⚠️ 逻辑错误(不会匹配任何值)
    digit_range(10, 1)      # 10 <= x <= 1(永远为假)
    

不支持的特性

  1. 不支持浮点数精确匹配

    # ⚠️ 注意:内部使用 i64,浮点数会被舍入
    digit_range(1, 10)  # 只能匹配整数
    
  2. 不支持无限范围

    # ❌ 不支持
    digit_range(0, infinity)  # 没有无限值
    
  3. 不支持多范围数组

    # ❌ 不支持(旧版本语法已废弃)
    digit_range([1, 100], [10, 200])  # 请使用分支逻辑替代
    

完整示例

示例 1:日志严重性过滤

rule log_error_filter {
    # 选择严重性级别字段
    | take(severity)

    # 过滤 ERROR 级别(1-2)
    | digit_range(1, 2)

    # 后续处理...
}

# 日志级别定义:
# 1 = CRITICAL
# 2 = ERROR
# 3 = WARNING
# 4 = WARN
# 5 = INFO
# 6 = DEBUG

示例 2:性能监控组合

rule performance_monitor {
    # 检查响应时间
    | take(response_ms)
    | digit_range(0, 1000)  # 0-1000ms 认为正常

    # 检查状态码
    | take(status)
    | digit_range(200, 299)  # 2xx

    # 两个条件都满足才通过
}

示例 3:时间窗口分析

rule weekday_check {
    # 工作日检查(1=周一, 7=周日)
    | take(day_of_week)
    | digit_range(1, 5)  # 周一到周五

    # 只分析工作日数据
}

性能说明

  • 时间复杂度:O(1) - 单次比较
  • 空间复杂度:O(1) - 原地检查
  • 性能特点
    • 纳秒级执行时间
    • 简单的整数比较操作
    • 性能极佳,适合高频调用

错误处理

常见错误

  1. 字段不存在

    错误: <pipe> | not in range
    原因: 当前没有活动字段
    解决: 使用 take() 先选择字段
    
  2. 字段类型不匹配

    错误: <pipe> | not in range
    原因: 字段不是数字类型(Digit)
    解决: 确保字段是数值类型
    
  3. 数值不在任何范围内

    错误: <pipe> | not in range
    原因: 字段值不满足任何一个范围条件
    解决: 检查范围设置是否正确
    

与其他函数配合使用

与字段选择器配合

# 先选择字段,再检查范围
| take(status_code)
| digit_range(200, 299)

与条件检查配合

# 先检查字段存在,再检查范围
| has()
| digit_range(0, 100)

与转换函数配合

# 组合使用进行复杂验证
| take(response_time)
| digit_range(0, 1000)  # 响应时间正常
| take(status_code)
| digit_range(200, 299)  # 状态码正常

与分支逻辑配合

# 使用 alt 实现"或"逻辑 - 检查多个不连续范围
(
    # 分支 1:检查是否为成功状态码
    | take(status)
    | digit_range(200, 299)
)
|
(
    # 分支 2:检查是否为重定向状态码
    | take(status)
    | digit_range(300, 399)
)

最佳实践

1. 范围设计原则

# ✅ 推荐:语义清晰的范围
digit_range(200, 299)  # HTTP 2xx 状态码

# ⚠️ 避免:使用旧的数组语法
# digit_range([200], [299])  # 已废弃

2. 简洁明了的单范围

# ✅ 推荐:简单直接的单范围
digit_range(0, 100)

# ✅ 推荐:多个范围使用分支逻辑
(digit_range(0, 50) | digit_range(100, 150))

3. 范围的可读性

# ✅ 推荐:添加注释说明范围含义
| digit_range(200, 299)  # HTTP 成功状态码

# ✅ 推荐:使用有意义的范围
| digit_range(18, 65)  # 工作年龄段

4. 边界值测试

# 测试边界值是否符合预期
digit_range(100, 200)
# 测试:100 ✅, 200 ✅, 99 ❌, 201 ❌

5. 使用分支处理不连续范围

# ✅ 推荐:清晰的分支逻辑
(
    digit_range(1, 10)   # 第一个范围
    |
    digit_range(50, 100) # 第二个范围
)

# ❌ 避免:使用已废弃的数组语法
# digit_range([1, 50], [10, 100])

调试技巧

1. 测试单个范围

# 从简单的单个范围开始
| digit_range(0, 100)

# 如需多个范围,使用分支逻辑
| (digit_range(0, 100) | digit_range(200, 300))

2. 验证字段类型

# 使用 digit_has() 确认字段是数字类型
| take(my_field)
| digit_has(0)  # 如果失败,说明不是数字字段

3. 检查边界值

# 准备测试数据
echo "value: 99" | wp-motor test.wpl    # 测试下界-1
echo "value: 100" | wp-motor test.wpl   # 测试下界
echo "value: 200" | wp-motor test.wpl   # 测试上界
echo "value: 201" | wp-motor test.wpl   # 测试上界+1

常见问题 (FAQ)

Q1: 如何检查单个特定值?

# 将下界和上界设为相同值
digit_range(200, 200)  # 只匹配 200

Q2: 范围可以倒序吗?

技术上可以,但逻辑上没有意义:

digit_range(100, 50)  # begin > end,永远不匹配

Q3: 如何实现“不在范围内“的逻辑?

WPL 中管道失败会中断,可以使用分支逻辑:

# 使用否定逻辑(复杂)
# 建议:使用其他字段函数或在应用层处理

Q4: 支持浮点数吗?

内部使用 i64,浮点数会被转换:

# 字段值 3.14 会被视为 3
digit_range(3, 4)  # 可能匹配 3.14(取决于解析方式)

Q5: 如何检查多个不连续的范围?

使用分支逻辑(alt 操作符):

# 检查 [1,10] 或 [100,200] 范围
(digit_range(1, 10) | digit_range(100, 200))
# 匹配:[1,10] 或 [100,200] 内的任何值

Q6: 性能够用吗?

非常快!范围检查是简单的数值比较:

  • O(1) 时间复杂度
  • 纳秒级执行时间
  • 适合高频调用场景

Q7: 旧的数组语法还能用吗?

不推荐使用,已废弃:

# ❌ 旧语法(已废弃)
digit_range([200], [299])

# ✅ 新语法(推荐)
digit_range(200, 299)

与 digit_in 的对比

特性digit_rangedigit_in
用途单个范围检查集合成员检查
参数两个标量(begin, end)一个数组(值列表)
适用场景连续范围离散值
示例digit_range(0, 100)digit_in([200, 404, 500])
复杂度O(1)O(n)
# digit_range - 适合连续范围
digit_range(200, 299)  # 200, 201, ..., 299

# digit_in - 适合离散值
digit_in([200, 404, 500])  # 只匹配这三个值

# 多个不连续范围 - 使用分支逻辑
(digit_range(200, 299) | digit_range(300, 399))

更多资源

  • 开发指南: docs/guide/wpl_field_func_development_guide.md
  • 源代码: crates/wp-lang/src/ast/processor/function.rs
  • 测试用例: crates/wp-lang/src/eval/builtins/pipe_fun.rs

版本历史

  • 1.13.1 (2026-02-02)
    • 重构为双参数形式:digit_range(begin, end)
    • 废弃旧的数组语法:digit_range([begins], [ends])
    • 简化为单范围检查,性能优化至 O(1)
    • 支持负数范围
    • 添加完整的测试覆盖

提示: digit_range 现在是一个简单高效的单范围检查函数,适合处理连续的数值范围验证场景,如状态码、性能指标、时间段等。对于多个不连续范围,请使用分支逻辑(alt 操作符)。对于离散值检查,请使用 digit_in 函数。

starts_with 函数使用指南

简介

starts_with 是一个 WPL pipe 函数,用于检查字符串字段是否以指定的前缀开始。

语法

field_name | starts_with('prefix')

参数

  • prefix: 字符串类型,要检查的前缀

行为

  • 如果字段值以指定前缀开始,字段保持不变并继续传递
  • 如果字段值不以指定前缀开始,字段转换为 ignore 类型,后续规则将忽略该字段
  • 如果字段不是字符串类型,字段转换为 ignore 类型
  • 前缀匹配是大小写敏感的
  • 空前缀 '' 匹配任何字符串

使用示例

示例 1: 过滤 HTTPS URL

rule https_filter {
    (chars:url) | starts_with('https://')
}

输入: https://example.com 输出: 匹配成功,提取 url = "https://example.com"

输入: http://example.com 输出: url 字段转换为 ignore,规则匹配失败

示例 2: 过滤 API 路径

rule api_filter {
    (chars:path) | starts_with('/api/')
}

输入: /api/users 输出: 匹配成功,提取 path = "/api/users"

输入: /home/users 输出: path 字段转换为 ignore,规则匹配失败

示例 3: 检查日志级别

rule error_log {
    (chars:log_level) | starts_with('ERROR'),
    (chars:message)
}

输入: ERROR: Database connection failed 输出: 匹配成功

  • log_level = "ERROR:"
  • message = "Database connection failed"

示例 4: 结合其他函数使用

rule secure_url {
    (chars:url) | starts_with('https://') | chars_has('secure')
}

只匹配以 https:// 开头且包含 secure 的 URL。

示例 5: 多分支条件

rule protocol_filter {
    (
        (chars:url) | starts_with('https://')
    ) | (
        (chars:url) | starts_with('wss://')
    )
}

匹配以 https://wss:// 开头的 URL。

与 regex_match 的对比

特性starts_withregex_match
性能更快(字符串前缀检查)较慢(正则表达式编译和匹配)
功能只能检查前缀支持复杂模式匹配
使用难度简单直观需要了解正则表达式
失败行为转换为 ignore解析失败

何时使用 starts_with:

  • 只需要检查字符串前缀
  • 性能要求高
  • 简单场景
  • 需要在管道中继续处理(通过 ignore 字段)

何时使用 regex_match:

  • 需要复杂的模式匹配
  • 需要检查中间或结尾内容
  • 需要使用正则表达式特性(捕获组等)

注意事项

  1. 大小写敏感: starts_with('HTTP') 不会匹配 http://example.com
  2. 完全匹配前缀: 前缀必须完全匹配,不支持通配符
  3. 仅支持字符串: 如果字段不是字符串类型,将转换为 ignore
  4. ignore 类型传播: 转换为 ignore 的字段在后续管道函数中会被跳过

实现细节

  • 定义位置: crates/wp-lang/src/ast/processor/function.rs
  • 实现位置: crates/wp-lang/src/eval/builtins/pipe_fun.rs
  • 解析器: crates/wp-lang/src/parser/wpl_fun.rs
  • 测试: crates/wp-lang/src/eval/builtins/pipe_fun.rs (tests 模块)

相关函数

  • regex_match(pattern): 正则表达式匹配
  • chars_has(value): 检查字段值是否等于指定字符串
  • chars_in([values...]): 检查字段值是否在列表中
  • chars_replace(target, replacement): 替换字符串中的子串

not() - 结果反转包装函数

概述

not() 是一个包装函数,用于反转(取反)内部管道函数的成功/失败结果。当内部函数匹配成功时,not() 返回失败;当内部函数匹配失败时,not() 返回成功。

语法

| not(inner_function)

参数

  • inner_function: 任何字段管道函数(如 f_chars_has, has, chars_has 等)

返回

  • 内部函数成功 → not() 失败
  • 内部函数失败 → not() 成功

重要特性

  • 保留字段值not() 只反转结果,不修改字段内容
  • 支持嵌套:可以使用 not(not(...)) 实现双重否定
  • 自动字段选择:继承内部函数的字段选择行为
  • 零性能开销:仅在执行时克隆单个字段进行测试

基本用法

1. 字符串不等于检查

# 检查 dev_type 不等于 "NDS"
(chars:dev_type) | not(chars_has(NDS))

# 等价于
(chars:dev_type) | chars_not_has(NDS)

2. 字段不存在检查

# 检查字段不存在
| not(f_has(optional_field))

3. 使用目标字段函数

# 检查指定字段不等于某值
| not(f_chars_has(status, ERROR))

# 检查指定字段不在列表中
| not(f_chars_in(level, [DEBUG, TRACE]))

高级用法

双重否定

双重否定等同于肯定断言:

# not(not(...)) 等同于直接使用内部函数
| not(not(f_chars_has(status, OK)))

# 等价于
| f_chars_has(status, OK)

组合复杂条件

# 字段存在但值不等于目标值
| f_has(dev_type)           # 确保字段存在
| not(chars_has(NDS))       # 确保值不等于 NDS

与数值函数结合

# 检查状态码不在成功范围内
| not(f_digit_range(status, 200, 299))

# 检查端口号不在常用端口列表中
| not(f_digit_in(port, [80, 443, 8080]))

与正则表达式结合

# 检查消息不匹配错误模式
| not(regex_match('(?i)error|fail|exception'))

与现有函数对比

not(chars_has) vs chars_not_has

虽然功能相似,但语义略有不同:

函数字段不存在非Chars类型值不等于值等于
not(chars_has(X))✅ 成功✅ 成功✅ 成功❌ 失败
chars_not_has(X)✅ 成功✅ 成功✅ 成功❌ 失败

推荐使用

  • 简单场景:使用 chars_not_has (更直观)
  • 复杂场景:使用 not() 包装其他函数(更灵活)
# ✅ 推荐:简单否定
| chars_not_has(ERROR)

# ✅ 推荐:复杂条件否定
| not(f_digit_range(code, 400, 499))

实际应用场景

场景 1:过滤非错误日志

rule filter_non_errors {
    # 解析日志级别
    (symbol(ERROR), symbol(WARN), symbol(INFO), symbol(DEBUG):level)

    # 只保留非 ERROR 和 WARN 级别的日志
    | take(level)
    | not(chars_in([ERROR, WARN]))
}

输入

INFO: Application started
ERROR: Connection failed
DEBUG: Processing request

输出

INFO: Application started     # ✅ 通过(非错误)
                              # ❌ 过滤掉 ERROR
DEBUG: Processing request     # ✅ 通过(非错误)

场景 2:排除特定设备类型

rule exclude_device_types {
    # 解析设备类型字段
    (chars:dev_type)

    # 排除 NDS 和 IDS 设备
    | not(f_chars_in(dev_type, [NDS, IDS]))
}

输入

dev_type=FIREWALL
dev_type=NDS
dev_type=ROUTER
dev_type=IDS

输出

dev_type=FIREWALL    # ✅ 通过
                     # ❌ 过滤掉 NDS
dev_type=ROUTER      # ✅ 通过
                     # ❌ 过滤掉 IDS

场景 3:非标准端口检查

rule non_standard_ports {
    # 解析端口号
    (digit:port)

    # 排除标准端口 80 和 443
    | not(f_digit_in(port, [80, 443]))

    # 同时必须在有效范围内
    | digit_range(1, 65535)
}

输入

80
8080
443
9000

输出

                # ❌ 过滤掉 80(标准端口)
8080            # ✅ 通过
                # ❌ 过滤掉 443(标准端口)
9000            # ✅ 通过

场景 4:排除测试环境日志

rule exclude_test_env {
    # 解析环境标识
    (chars:env)

    # 排除测试和开发环境
    | not(f_chars_in(env, [test, dev, staging]))
}

性能考虑

性能特征

操作性能影响
单层 not()< 200ns(克隆单个字段)
双层 not(not())< 400ns(两次克隆)
字段选择继承0ns(无额外开销)

性能优化建议

# ✅ 推荐:使用专用函数(更快)
| chars_not_has(ERROR)

# ⚠️ 可接受:使用 not() 包装(稍慢)
| not(chars_has(ERROR))

# ❌ 不推荐:过度嵌套
| not(not(not(chars_has(ERROR))))  # 无意义的多重否定

常见陷阱

陷阱 1:混淆管道级 not() 和组级 not()

# ❌ 错误:这是组级 not(),不是管道级
not(symbol(ERROR):test)

# ✅ 正确:管道级 not() 用于管道函数
(chars:status) | not(chars_has(ERROR))

陷阱 2:期望 not() 修改字段值

# ❌ 误解:以为 not() 会修改字段
(chars:status) | not(chars_has(ERROR))
# 字段值仍然是原始值,not() 只反转匹配结果

# ✅ 正确:如需修改值,使用转换函数
(chars:status) | chars_replace(ERROR, OK)

陷阱 3:not() 包装非字段函数

# ❌ 错误:take 不是字段管道函数
| not(take(field_name))
# 会报错:not() can only wrap field pipe functions

# ✅ 正确:包装字段管道函数
| not(f_has(field_name))

与组级 not() 的区别

WPL 中有两种 not()

特性管道级 not()组级 not()
用途反转管道函数结果反转字段组匹配
语法位置管道中 | not(...)字段组定义 not(...)
参数类型管道函数字段定义
返回结果成功/失败ignore 字段

示例对比

# 管道级 not():反转函数结果
(chars:status) | not(chars_has(ERROR))

# 组级 not():字段存在时失败
not(symbol(ERROR):error_marker)

最佳实践

1. 优先使用专用函数

# ✅ 推荐:使用 chars_not_has
| chars_not_has(ERROR)

# ⚠️ 次选:使用 not() 包装
| not(chars_has(ERROR))

2. not() 用于无专用函数的场景

# ✅ 推荐:没有 digit_not_in,使用 not()
| not(f_digit_in(port, [80, 443]))

# ✅ 推荐:没有 digit_not_range,使用 not()
| not(digit_range(200, 299))

3. 组合多个条件

# ✅ 推荐:清晰的逻辑组合
| f_has(status)              # 字段必须存在
| not(chars_in([ERROR, FATAL]))  # 且不是错误状态

4. 避免过度嵌套

# ❌ 不推荐:双重否定令人困惑
| not(not(chars_has(OK)))

# ✅ 推荐:直接使用肯定形式
| chars_has(OK)

故障排查

问题:not() 没有反转结果

可能原因:混淆了管道级和组级 not()

# 检查是否在正确位置使用
(chars:status) | not(chars_has(ERROR))  # ✅ 正确
not(chars:status)                        # ❌ 错误(这是组级)

问题:报错 “can only wrap field pipe functions”

解决方案:确保包装的是字段管道函数

| not(take(field))        # ❌ take 是选择器
| not(f_has(field))       # ✅ f_has 是管道函数

版本历史

  • 1.15.1 (2026-02-07)
    • 新增 not() 管道级包装函数
    • 支持反转任何字段管道函数结果
    • 支持嵌套和自动字段选择

相关文档


提示: not() 是一个强大的工具,但不要过度使用。大多数情况下,使用专用的否定函数(如 chars_not_has)更直观、性能更好。

WPL Group 逻辑

WPL 提供了多种 group 包装器,用于控制字段解析的逻辑行为。Group 可以包含一个或多个字段,并根据不同的逻辑语义决定解析成功或失败。

Group 类型

seq - 序列(默认)

默认的 group 类型,要求所有字段按顺序成功解析。

语法:

(field1, field2, field3)
seq(field1, field2, field3)

行为:

  • 按顺序解析所有字段
  • 所有字段都必须成功
  • 任一字段失败则整个 group 失败

示例:

(digit:id, chars:name, ip:addr)

opt - 可选

将 group 标记为可选,失败时不影响整体解析。

语法:

opt(field1, field2)

行为:

  • 尝试解析所有字段
  • 解析失败时不返回错误
  • 成功时返回解析结果,失败时跳过

示例:

opt(symbol([DEBUG]):level), chars:msg

alt - 选择

尝试多个解析选项,任一成功即可。

语法:

alt(field1, field2, field3)

行为:

  • 依次尝试每个字段
  • 第一个成功的字段被采用
  • 所有字段都失败时,group 失败

示例:

alt(ip:addr, chars:addr)  # 尝试解析 IP,失败则解析为字符串

some_of - 部分匹配

要求至少一个字段成功即可。

语法:

some_of(field1, field2, field3)

行为:

  • 尝试解析所有字段
  • 至少一个字段成功即可
  • 所有字段都失败时,group 失败

示例:

some_of(digit:port, chars:service)

not - 负向断言

反向逻辑,当内部字段解析失败时才成功。

语法:

not(field)

行为:

  • 尝试解析内部字段
  • 内部字段失败时,not() 成功
  • 内部字段成功时,not() 失败
  • 成功时返回 ignore 类型字段

输入消费:

  • not(symbol(...)) - 会消费输入(symbol 在失败时可能消费空白字符)
  • not(peek_symbol(...)) - 不消费输入(peek_symbol 永不消费)

示例:

# 确保不存在 ERROR 关键字
not(symbol(ERROR):check)

# 与 peek_symbol 配合,不消费输入
not(peek_symbol(ERROR):check), (chars:msg)

使用场景

1. 条件解析

# 解析可选的调试信息
opt(symbol([DEBUG]):level), chars:msg

2. 格式兼容

# 支持多种 IP 地址格式
alt(ip:addr, chars:addr)

3. 负向过滤

# 只处理非错误日志
not(symbol(ERROR)), (chars:msg)

4. 宽松匹配

# 至少匹配端口或服务名之一
some_of(digit:port, chars:service)

组合使用

Group 可以嵌套组合,实现复杂的解析逻辑:

# 可选的 IP 或域名
opt(alt(ip:addr, chars:domain))

# 确保不是 ERROR,然后解析消息
not(peek_symbol(ERROR)), (alt(json, kv, chars):msg)

注意事项

  1. Group 不能嵌套在 Group 内部

    # ❌ 错误:不支持嵌套
    (chars, (digit, chars))
    
    # ✓ 正确:使用多个并列 group
    (chars), (digit, chars)
    
  2. not() 只能包含单个字段

    # ✓ 正确
    not(symbol(ERROR):check)
    
    # ❌ 错误
    not(symbol(ERROR), symbol(FATAL))
    
  3. 输入消费行为取决于内部 parser

    • 使用 peek_symbol 等非消费 parser 可以实现前瞻断言
    • 使用 symboldigit 等消费 parser 会改变输入位置

WPL 学习目标与练习

实战教程 (T1-T4)

OML 对象模型语言

OML (Object Modeling Language) 是 Warp Parse 使用的数据转换语言,用于对 WPL 解析后的结构化数据进行转换、聚合和富化。


📚 文档导航

按学习路径

🆕 新手入门
   ↓
01-quickstart.md ────────→ 5分钟上手,复制即用
   ↓
07-complete-example.md ──→ 🌟 完整功能演示(强烈推荐)
   ↓
02-core-concepts.md ─────→ 理解设计理念和核心概念
   ↓
03-practical-guide.md ───→ 按任务查找解决方案
   ↓
04-functions-reference.md → 查阅函数
   ↓
05-integration.md ───────→ 集成到数据流

按用户角色

我是…推荐阅读
OML 新手01-quickstart.md07-complete-example.md
日常使用者03-practical-guide.md - 按任务查找
开发者/集成07-complete-example.md + 04-functions-reference.md
系统集成05-integration.md - WPL/OML/Sink 关联

按任务查找

我想…查看文档
🚀 快速上手01-quickstart.md
🌟 查看完整示例07-complete-example.md
💡 理解概念02-core-concepts.md
📝 提取字段03-practical-guide.md § 数据提取
🔄 类型转换03-practical-guide.md § 数据转换
📦 创建对象/数组03-practical-guide.md § 数据聚合
✅ 条件判断03-practical-guide.md § 条件处理
🔍 SQL 查询03-practical-guide.md § 数据富化
⚙️ 查某个函数04-functions-reference.md
🔗 集成到流水线05-integration.md
📖 查语法规则06-grammar-reference.md

📖 文档列表

文档内容适合人群
01-quickstart.md5 分钟快速入门 + 3 个最常用操作所有人
🌟 07-complete-example.md完整功能演示(强烈推荐)所有人
02-core-concepts.md设计理念 + 类型系统 + 读取语义想深入理解的用户
03-practical-guide.md按任务组织的实战示例日常使用者
04-functions-reference.md所有函数的标准化参考开发者
05-integration.mdWPL/OML/Sink 集成指南系统集成者
06-grammar-reference.mdEBNF 形式化语法定义编译器开发者

⚡ 快速示例

基础字段提取

name : nginx_access
rule : /nginx/access_log
---
user_id = read(user_id) ;
uri = read(request_uri) ;
status : digit = read(status) ;

数据聚合

name : system_metrics
rule : /system/metrics
---
metrics : obj = object {
    hostname : chars = read(hostname) ;
    cpu : float = read(cpu_usage) ;
    memory : float = read(mem_usage) ;
} ;

条件处理

name : log_classifier
rule : /app/logs
---
level = match read(status_code) {
    in (digit(200), digit(299)) => chars(success) ;
    in (digit(400), digit(499)) => chars(client_error) ;
    in (digit(500), digit(599)) => chars(server_error) ;
    _ => chars(unknown) ;
} ;

管道转换

name : data_transform
rule : /data/raw
---
# 时间转时间戳
ts = read(event_time) | Time::to_ts_zone(0, ms) ;

# URL 解析
domain = read(url) | url(domain) ;
path = read(url) | url(path) ;

# Base64 解码
decoded = read(base64_data) | base64_decode(Utf8) ;

SQL 数据富化

name : user_enrichment
rule : /app/user_activity
---
user_id = read(user_id) ;

# 从数据库查询用户信息
user_name, user_level =
    select name, level
    from users
    where id = read(user_id) ;

更多示例请查看:🌟 完整功能示例实战指南


🎯 核心特性

  • 声明式:描述“想要什么“,而非“怎么做“
  • 类型安全:8 种数据类型,自动推断或显式声明
  • WPL 关联:通过 rule 字段匹配 WPL 解析规则
  • 读取模式:read(非破坏性)vs take(破坏性)
  • 强大的管道:链式转换(时间/编解码/URL 解析等)
  • 条件匹配:match 表达式支持范围、否定、多源匹配
  • 数据聚合:object(对象)和 collect(数组)
  • SQL 集成:直接查询数据库进行数据富化

🔗 WPL 与 OML 关联

OML 通过 rule 字段与 WPL 的 package/rule 建立关联:

原始数据
    ↓
[WPL 解析] → 生成结构化数据 + rule 标识
    ↓
数据携带: rule = "/nginx/access_log"
    ↓
[查找匹配的 OML] → 匹配 rule 字段
    ↓
[执行 OML 转换]
    ↓
[输出到 Sink]

示例

WPL 规则:

package nginx {
    rule access_log {
        (ip:client_ip, chars:uri, digit:status)
    }
}

OML 配置:

name : nginx_handler
rule : /nginx/access_log    # 匹配 WPL 的 package/rule
---
client : ip = read(client_ip) ;
uri : chars = read(uri) ;
status : digit = read(status) ;

💬 快速帮助

常见问题

Q: 从哪里开始学习? A: 从 01-quickstart.md 开始,然后查看 🌟 完整功能示例

Q: 如何将 OML 与 WPL 关联? A: 使用 rule 字段匹配 WPL 的 package/rule 值,详见 05-integration.md

Q: read 和 take 有什么区别? A: read 是非破坏性的(可重复读取),take 是破坏性的(读取后移除),详见 02-core-concepts.md

Q: 某个函数怎么用? A: 查看 04-functions-reference.md🌟 完整功能示例

Q: 如何调试 OML 转换? A: 参考 05-integration.md § 故障排查


📝 相关文档


开始学习: 01-quickstart.md - 5分钟快速入门
完整示例: 🌟 07-complete-example.md - 所有功能演示

OML 完整功能示例

一个完整的示例,展示 OML 的所有核心功能

本文档提供一个全面的 OML 示例,涵盖所有核心功能,包括基础操作、内置函数、管道函数、高级匹配等。这是学习和参考 OML 功能的最佳起点。


📚 快速导航

章节内容
原始数据测试数据
WPL 解析规则数据解析规则
OML 配置完整的 OML 转换配置
功能详解每个功能的详细说明
关键要点WPL 与 OML 关联、功能清单

原始数据

222.133.52.20 simple_chars 80 192.168.1.10 select_one left 2025-12-29 12:00:00 {"msg":"hello"} "" aGVsbG8gd29ybGQ= ["val1","val2","val3"] /home/user/file.txt http://example.com/path/to/resource?foo=1&bar=2 [{"one":{"two":"nested"}}] foo bar baz qux 500 ext_value_1 ext_value_2 http://localhost:8080/bua/sync/health?a=test 525tab beijing shanghai 10.0.0.1 10.0.0.100 success enabled true sport:8080 dport:9090 details[0]/process_name:proc1 details[1]/process_name:proc2 optional_field:exists source_field:data another_field:value

WPL 解析规则

package T4 {
    rule case {
        (
            ip:sip,
            chars:simple_chars,
            digit:simple_port,
            ip:simple_ip,
            chars:select_one,
            chars:match_chars,
            time:timestamp_zone,
            json(chars@msg: json_msg),
            chars:empty_chars,
            base64 | (chars:base64),
            array/chars:array_str,
            chars:path,
            chars:url,
            array:obj,
            chars:one,
            chars:two,
            chars:three,
            chars:four,
            digit:num_range,
            chars:extend1,
            chars:extend2,
            chars:html,
            chars:str,
            chars:city1,
            chars:city2,
            ip:src_ip,
            ip:dst_ip,
            chars:status,
            chars:enabled,
            bool:enabled
        )
    }
}

说明:WPL 规则将原始数据解析为结构化字段,并附加 rule = T4/case 标识。


OML 配置

name : T4
rule : T4/case
---

// ==================== 1. 基础操作 ====================

// 1.1 直接赋值字面量
direct_chars = chars(13);
direct_digit = digit(13);

// 1.2 简单取值
simple_chars = read(simple_chars);
simple_port : digit = read(simple_port);
simple_ip : ip = read(simple_ip);

// 1.3 选择取值(按顺序尝试多个字段)
select_chars = read(option:[select_one, select_two]);

// 1.4 默认值处理(字段不存在时使用默认值)
field_with_default = read(optional_field) { _ : chars(DEFAULT_VALUE) };
version_fallback : chars = read(version) { _ : chars(v1.0.0) };

// 1.5 多目标同时赋值
target1, target2 : chars = read();
name_alias, name_copy = read(name);

// 1.6 匿名目标(丢弃不需要的返回值)
_, useful_field = read(option:[field1, field2]);

// 1.7 take vs read 区别(破坏性 vs 非破坏性)
field_taken = take(source_field);                                    // take 会移除源字段
field_taken_again = take(source_field) { _ : chars(already_taken) }; // 再次 take 失败
field_read1 = read(another_field);                                   // read 不移除
field_read2 = read(another_field);                                   // 可重复读取

// 1.8 通配符批量操作
all_fields = take();                      // 取所有字段
path_fields = take(keys:[*/path]);        // 批量匹配:所有以 /path 结尾
a_name_fields = read(keys:[A*/name]);     // 前缀匹配:A 开头、/name 结尾

// ==================== 2. 内置函数 ====================

// 2.1 时间函数
current_time = Now::time();  // 获取当前完整时间
current_date = Now::date();  // 获取当前日期(YYYYMMDD)
current_hour = Now::hour();  // 获取当前小时(YYYYMMDDHH)

// ==================== 3. 模式匹配 ====================

// 3.1 单源 match(简单匹配)
match_chars = match read(option:[match_chars]) {
    chars(left) => chars(1);
    chars(middle) => chars(2);
    chars(right) => chars(3);
};

// 3.2 范围判断(in 操作符)
num_range = match read(option:[num_range]) {
    in (digit(0), digit(1000)) => read(num_range);
    _ => digit(0);
};

// 3.3 双源 match(匹配两个字段的组合)
location : chars = match (read(city1), read(city2)) {
    (chars(beijing), chars(shanghai)) => chars(east_region);
    (chars(chengdu), chars(chongqing)) => chars(west_region);
    _ => chars(unknown_region);
};

region_by_ip : chars = match (read(src_ip), read(dst_ip)) {
    (ip(10.0.0.1), ip(10.0.0.100)) => chars(internal);
    _ => chars(external);
};

// 3.4 match 否定条件(! 操作符)
valid_status = match read(status) {
    !chars(error) => chars(ok);
    !chars(failed) => chars(success);
    _ => chars(unknown);
};

// 3.5 布尔类型 match
is_enabled : digit = match read(enabled) {
    bool(true) => digit(1);
    bool(false) => digit(0);
    _ => digit(-1);
};

// ==================== 4. 管道函数 ====================

// 4.1 时间转换
timestamp_zone = pipe read(timestamp_zone) | Time::to_ts_zone(0, ms);  // 修改时区
timestamp_s = pipe read(timestamp_zone) | Time::to_ts;                 // 转秒级时间戳
timestamp_ms = pipe @current_time | Time::to_ts_ms;                    // 转毫秒级时间戳
timestamp_us = pipe @current_time | Time::to_ts_us;                    // 转微秒级时间戳
timestamp_zone_8 = pipe @current_time | Time::to_ts_zone(8, s);        // UTC+8 时区

// 4.2 编码/解码
base64_decoded = pipe read(base64) | base64_decode(Utf8);  // Base64 解码
base64_encoded = pipe read(base64) | base64_encode;        // Base64 编码

// 4.3 转义/反转义
html_escaped = pipe read(html) | html_escape;              // HTML 转义
html_unescaped = pipe read(html) | html_unescape;          // HTML 反转义
json_escaped = pipe read(json_escape) | json_escape;       // JSON 转义
json_unescaped = pipe @json_escaped | json_unescape;       // JSON 反转义
str_escaped = pipe read(str) | str_escape;                 // 字符串转义

// 4.4 数据转换
to_str_result = pipe read(str) | to_str;                   // 转为字符串
array_json = pipe read(array_str) | to_json;               // 数组转 JSON
ip_to_int = pipe read(simple_ip) | ip4_to_int;             // IPv4 转整数

// 4.5 集合操作
array_first = pipe read(array_str) | nth(0);               // 获取数组第 0 个元素
obj_nested = pipe read(obj) | nth(0) | get(one/two);       // 对象嵌套取值

// 4.6 数据提取
file_name = pipe read(path) | path(name);                  // 提取文件名
file_path = pipe read(path) | path(path);                  // 提取文件路径
url_domain = pipe read(url) | url(domain);                 // 提取 URL domain
url_host = pipe read(url) | url(host);                     // 提取 URL host
url_uri = pipe read(url) | url(uri);                       // 提取 URL uri
url_path = pipe read(url) | url(path);                     // 提取 URL path
url_params = pipe read(url) | url(params);                 // 提取 URL params

// 4.7 其他管道函数
skip_empty_result = pipe read(empty_chars) | skip_empty;   // 跳过空值

// 4.8 省略 pipe 关键字(新语法)
simple_transform = read(data) | to_json;                   // 直接省略 pipe
chained_ops = read(array_data) | nth(0) | to_str;          // 链式调用
url_extract = read(url_field) | url(domain);               // 简化写法

// 4.9 链式管道操作
nested_extract = pipe read(complex_obj) | nth(0) | get(level1/level2/level3);
multi_transform = pipe read(raw_data) | base64_decode(Utf8) | to_json;

// ==================== 5. 字符串操作 ====================

// 5.1 字符串格式化(fmt 函数)
splice = fmt("{one}:{two}|{three}:{four}", read(one), read(two), read(three), read(four));

// ==================== 6. 对象与数组 ====================

// 6.1 对象创建(聚合多个字段)
extends = object {
    extend1, extend2 = read();
};

// 6.2 数组收集(collect)
collected_ports : array = collect read(keys:[sport, dport, extra_port]);
wildcard_items : array = collect take(keys:[details[*]/process_name]);  // 支持通配符收集

功能详解

1. 基础操作

1.1 字面量赋值

直接创建常量值:

direct_chars = chars(13);
direct_digit = digit(13);

1.2 简单取值

从输入数据读取字段:

simple_chars = read(simple_chars);
simple_port : digit = read(simple_port);  // 显式类型转换
simple_ip : ip = read(simple_ip);

1.3 选择取值

按优先级尝试多个字段:

select_chars = read(option:[select_one, select_two]);
// 先尝试 select_one,不存在则尝试 select_two

1.4 默认值处理

字段不存在时使用默认值:

field_with_default = read(optional_field) { _ : chars(DEFAULT_VALUE) };
version_fallback : chars = read(version) { _ : chars(v1.0.0) };

1.5 多目标赋值

一次赋值给多个目标:

target1, target2 : chars = read();
name_alias, name_copy = read(name);

1.6 匿名目标

丢弃不需要的返回值:

_, useful_field = read(option:[field1, field2]);
// 第一个返回值被丢弃

1.7 take vs read

  • take:破坏性读取,移除源字段
  • read:非破坏性读取,保留源字段
field_taken = take(source_field);      // 源字段被移除
field_taken_again = take(source_field) { _ : chars(already_taken) }; // 失败

field_read1 = read(another_field);     // 源字段保留
field_read2 = read(another_field);     // 可以再次读取

1.8 通配符批量操作

使用通配符匹配多个字段:

all_fields = take();                   // 取所有字段
path_fields = take(keys:[*/path]);     // 所有以 /path 结尾
a_name_fields = read(keys:[A*/name]);  // A 开头、/name 结尾

2. 内置函数

时间相关函数:

current_time = Now::time();  // 2025-12-29 12:00:00
current_date = Now::date();  // 20251229
current_hour = Now::hour();  // 2025122912

3. 模式匹配

3.1 单源 match

基于单个字段的值进行匹配:

match_chars = match read(option:[match_chars]) {
    chars(left) => chars(1);
    chars(middle) => chars(2);
    chars(right) => chars(3);
};

3.2 范围判断

使用 in 操作符判断范围:

num_range = match read(option:[num_range]) {
    in (digit(0), digit(1000)) => read(num_range);
    _ => digit(0);
};

3.3 双源 match

匹配两个字段的组合:

location : chars = match (read(city1), read(city2)) {
    (chars(beijing), chars(shanghai)) => chars(east_region);
    (chars(chengdu), chars(chongqing)) => chars(west_region);
    _ => chars(unknown_region);
};

3.4 否定条件

使用 ! 操作符进行否定匹配:

valid_status = match read(status) {
    !chars(error) => chars(ok);
    !chars(failed) => chars(success);
    _ => chars(unknown);
};

3.5 布尔类型 match

匹配布尔值:

is_enabled : digit = match read(enabled) {
    bool(true) => digit(1);
    bool(false) => digit(0);
    _ => digit(-1);
};

4. 管道函数

4.1 时间转换

timestamp_zone = pipe read(timestamp_zone) | Time::to_ts_zone(0, ms);  // UTC 毫秒
timestamp_s = pipe read(timestamp_zone) | Time::to_ts;                 // 秒级
timestamp_ms = pipe @current_time | Time::to_ts_ms;                    // 毫秒级
timestamp_us = pipe @current_time | Time::to_ts_us;                    // 微秒级
timestamp_zone_8 = pipe @current_time | Time::to_ts_zone(8, s);        // UTC+8

4.2 编码/解码

base64_decoded = pipe read(base64) | base64_decode(Utf8);
base64_encoded = pipe read(base64) | base64_encode;

4.3 转义/反转义

html_escaped = pipe read(html) | html_escape;
html_unescaped = pipe read(html) | html_unescape;
json_escaped = pipe read(json_escape) | json_escape;
json_unescaped = pipe @json_escaped | json_unescape;
str_escaped = pipe read(str) | str_escape;

4.4 数据转换

to_str_result = pipe read(str) | to_str;
array_json = pipe read(array_str) | to_json;
ip_to_int = pipe read(simple_ip) | ip4_to_int;

4.5 集合操作

array_first = pipe read(array_str) | nth(0);           // 获取第 0 个元素
obj_nested = pipe read(obj) | nth(0) | get(one/two);   // 嵌套取值

4.6 数据提取

file_name = pipe read(path) | path(name);      // file.txt
file_path = pipe read(path) | path(path);      // /home/user
url_domain = pipe read(url) | url(domain);     // example.com
url_host = pipe read(url) | url(host);         // example.com
url_uri = pipe read(url) | url(uri);           // /path/to/resource?foo=1&bar=2
url_path = pipe read(url) | url(path);         // /path/to/resource
url_params = pipe read(url) | url(params);     // foo=1&bar=2

4.7 控制函数

skip_empty_result = pipe read(empty_chars) | skip_empty;  // 跳过空值

4.8 简化语法

省略 pipe 关键字:

simple_transform = read(data) | to_json;
chained_ops = read(array_data) | nth(0) | to_str;
url_extract = read(url_field) | url(domain);

4.9 链式操作

nested_extract = pipe read(complex_obj) | nth(0) | get(level1/level2/level3);
multi_transform = pipe read(raw_data) | base64_decode(Utf8) | to_json;

5. 字符串操作

格式化字符串:

splice = fmt("{one}:{two}|{three}:{four}", read(one), read(two), read(three), read(four));
// 输出:foo:bar|baz:qux

6. 对象与数组

6.1 对象创建

聚合多个字段为对象:

extends = object {
    extend1, extend2 = read();
};

6.2 数组收集

收集多个字段为数组:

collected_ports : array = collect read(keys:[sport, dport, extra_port]);
// 输出:[8080, 9090, ...]

wildcard_items : array = collect take(keys:[details[*]/process_name]);
// 输出:["proc1", "proc2"]

关键要点

WPL 与 OML 关联

原始数据
    ↓
[WPL 解析] → 生成结构化数据 + rule 标识
    ↓
数据携带: rule = "T4/case"
    ↓
[查找匹配的 OML] → 匹配 rule 字段
    ↓
[执行 OML 转换] → 应用本示例的转换逻辑
    ↓
输出到 Sink

关键:OML 的 rule : T4/case 与 WPL 的 package T4 { rule case { ... } } 对应。

功能覆盖清单

  • ✅ 基础操作:字面量、取值、默认值、通配符
  • ✅ 内置函数:时间函数
  • ✅ 模式匹配:单源、双源、范围、否定、布尔
  • ✅ 管道函数:时间、编解码、转义、转换、集合、提取
  • ✅ 字符串操作:格式化
  • ✅ 对象与数组:聚合、收集

下一步


提示:这个示例是学习 OML 的最佳参考,建议收藏并在实际使用时对照查阅。

OML 快速入门

5 分钟快速上手 OML(Object Modeling Language)

💡 提示:想要查看所有功能的完整演示?请访问 完整功能示例


📚 快速导航

章节内容
什么是 OMLOML 简介
最小示例5 行代码上手
基本语法配置结构、WPL 关联、语法规则
三个最常用操作读取字段、类型转换、数据聚合
常用数据类型8 种数据类型速查
常用内置函数时间函数
read vs take两种读取模式对比
完整示例日志处理完整示例

什么是 OML

OML 是一种声明式的数据转换语言,用于将解析后的结构化数据转换为目标格式。它提供了简洁的语法来完成字段提取、类型转换、数据聚合等常见操作。

最小示例

name : my_first_oml
rule : /nginx/access_log
---
user_id = read(user_id) ;
timestamp : time = Now::time() ;

说明

  • name : my_first_oml - OML 配置名称声明
  • rule : /nginx/access_log - 匹配 WPL 的 package/rule 值(关键!)
  • --- - 分隔符,区分声明区和配置区
  • user_id = read(user_id) - 从输入读取 user_id 字段
  • timestamp : time = Now::time() - 调用内置函数获取当前时间

重要rule 字段用于关联 WPL 解析规则,只有当数据的 WPL rule 匹配时,这个 OML 配置才会被应用。

基本语法

1. 配置结构

name : <配置名称>
rule : <WPL 规则匹配模式>
---
<目标字段>[:<类型>] = <表达式> ;

2. WPL 与 OML 的关联

关键概念:一个 WPL 规则可以对应一个或多个 OML 配置。

WPL 解析 → 数据携带 rule 标识 → 匹配 OML 的 rule 字段 → 执行转换

示例

# OML 配置
name : nginx_access
rule : /nginx/access_log    # 匹配 WPL 的 package/rule 值
---
# 转换逻辑...

当 WPL 解析后的数据携带 rule = /nginx/access_log 时,这个 OML 配置会被自动应用。

支持通配符

  • rule : /nginx/* - 匹配所有 /nginx/ 开头的规则
  • rule : */access_log - 匹配所有以 /access_log 结尾的规则
  • rule : * - 匹配所有规则

3. 必须记住的规则

  • 每个配置条目必须以分号 ; 结束
  • 使用 --- 分隔配置的不同部分
  • rule 字段用于匹配 WPL 的 package/rule 值
  • 类型声明可选,默认为 auto 自动推断

三个最常用操作

操作 1:读取字段

场景:从输入数据中提取字段

name : read_example
---
# 读取单个字段
user_id = read(user_id) ;

# 读取并指定类型
port : digit = read(port) ;

# 读取时提供默认值
country = read(country) { _ : chars(CN) } ;

输入示例

user_id = "user123"
port = "8080"

输出

user_id = "user123"
port = 8080
country = "CN"  # 使用默认值

操作 2:类型转换

场景:转换字段类型

name : type_conversion
---
# 字符串转 IP
src_ip : ip = read(src_ip) ;

# 字符串转整数
port : digit = read(port) ;

# 字符串转浮点数
cpu_usage : float = read(cpu) ;

# 字符串转时间
event_time : time = read(time) ;

输入示例

src_ip = "192.168.1.100"
port = "8080"
cpu = "85.5"
time = "2024-01-15 14:30:00"

输出

src_ip = 192.168.1.100  # IP 类型
port = 8080              # 整数
cpu_usage = 85.5         # 浮点数
event_time = 2024-01-15 14:30:00  # 时间类型

操作 3:数据聚合

场景:将多个字段组合成对象或数组

创建对象

name : create_object
---
system_info : obj = object {
    hostname : chars = read(hostname) ;
    cpu : float = read(cpu_usage) ;
    memory : float = read(mem_usage) ;
} ;

输入

hostname = "web-server-01"
cpu_usage = "75.5"
mem_usage = "60.2"

输出

{
    "system_info": {
        "hostname": "web-server-01",
        "cpu": 75.5,
        "memory": 60.2
    }
}

创建数组

name : create_array
---
# 收集多个字段到数组
ports : array = collect read(keys:[sport, dport]) ;

输入

sport = "8080"
dport = "443"

输出

ports = [8080, 443]

常用数据类型

类型说明示例
auto自动推断(默认)value = read() ;
chars字符串name : chars = read() ;
digit整数count : digit = read() ;
float浮点数ratio : float = read() ;
ipIP 地址addr : ip = read() ;
time时间timestamp : time = Now::time() ;
obj对象info : obj = object { ... } ;
array数组items : array = collect read(...) ;

常用内置函数

name : builtin_functions
---
# 获取当前时间
now : time = Now::time() ;

# 获取当前日期(YYYYMMDD 格式)
today : digit = Now::date() ;

# 获取当前小时(YYYYMMDDHH 格式)
current_hour : digit = Now::hour() ;

read vs take:两种读取模式

read(非破坏性)

  • 可以多次读取同一字段
  • 不会从输入中移除字段
name : read_mode
---
field1 = read(data) ;
field2 = read(data) ;  # 仍可读取到 data

take(破坏性)

  • 读取后会从输入中移除
  • 后续无法再读取同一字段
name : take_mode
---
field1 = take(data) ;
field2 = take(data) ;  # 读取失败,data 已被移除

使用建议

  • 需要复用字段时使用 read
  • 确保字段只使用一次时使用 take

完整示例:日志处理

这个示例展示了 OML 的主要功能:字段提取、类型转换、条件判断、数据聚合。

输入数据(WPL 解析后):

user_id = "user123"
uri = "/api/users"
status = "200"
timestamp = "2024-01-15 14:30:00"

OML 配置:

name : access_log_processor
rule : /nginx/access_log
---
# 基础字段提取
user_id = read(user_id) ;
request_uri = read(uri) ;
status_code : digit = read(status) ;

# 时间处理
event_time : time = read(timestamp) ;
event_date : digit = Now::date() ;

# 条件转换(状态码分类)
status_level = match read(status_code) {
    in (digit(200), digit(299)) => chars(success) ;
    in (digit(400), digit(499)) => chars(client_error) ;
    in (digit(500), digit(599)) => chars(server_error) ;
    _ => chars(other) ;
} ;

# 数据聚合
log_entry : obj = object {
    user : chars = read(user_id) ;
    uri : chars = read(request_uri) ;
    status : digit = read(status_code) ;
    level : chars = read(status_level) ;
    time : time = read(event_time) ;
} ;

输出结果:

{
    "log_entry": {
        "user": "user123",
        "uri": "/api/users",
        "status": 200,
        "level": "success",
        "time": "2024-01-15 14:30:00"
    }
}

关键点:

  • rule : /nginx/access_log 匹配 WPL 的 package/rule 值
  • match 表达式实现条件分类
  • object 聚合多个字段为结构化输出
  • 类型自动转换(status 从字符串转为整数)

📚 完整类型系统与功能

OML 支持 8 种数据类型和丰富的函数库,涵盖数据提取、转换、聚合、条件处理等。

👉 查看完整示例: 07-complete-example.md

该文档包含:

  • ✅ 所有核心功能的完整示例代码
  • ✅ 可运行的原始数据、WPL 规则和 OML 配置
  • ✅ 每个功能的详细说明和使用建议
  • ✅ 基础操作、内置函数、管道函数、模式匹配等

快速预览主要功能:

  • 基础操作:字面量赋值、取值、默认值、通配符批量操作
  • 内置函数Now::time()Now::date()Now::hour()
  • 管道函数:Base64 编解码、HTML 转义、时间转换、URL 解析
  • 模式匹配:单源/双源 match、范围判断、否定条件
  • 数据聚合:对象创建、数组收集
  • SQL 集成:数据库查询和富化

下一步学习

理解概念

  • 核心概念 - 深入理解 OML 的设计理念
    • WPL 与 OML 协作关系
    • read vs take 读取语义
    • 类型系统和表达式

实战应用

  • 实战指南 - 按任务查找解决方案
    • 数据提取、转换、聚合
    • 条件处理、SQL 查询
    • 复杂场景示例

查阅参考


快速提示

  1. 从简单开始:先使用基本的 read 操作,熟悉后再使用高级特性
  2. 显式类型:对于重要字段,建议显式声明类型避免意外转换
  3. 提供默认值:对于可能缺失的字段,使用 { _ : <默认值> } 语法
  4. 使用对象组织数据:复杂数据用 object 聚合,便于理解和维护
  5. 分号不能省:每个配置条目必须以分号结束

相关资源

OML 核心概念

本文档介绍 OML 的核心设计理念和基础概念,帮助你深入理解 OML 的工作原理。


📚 文档导航

快速导航

主题内容
设计理念WPL 协作关系、声明式语法、不可变数据流
类型系统8 种基本类型、自动推断、类型转换
读取语义read vs take、破坏性与非破坏性、读取优先级
表达式类型值表达式、函数调用、管道、条件、聚合
默认值机制默认值语法、函数默认值、限制说明
通配符通配符语法、批量目标、使用限制
参数化读取option 优先级、keys 收集、JSON 路径
表达式组合嵌套对象、管道链、复杂 match
作用域规则目标字段作用域、全局字段
最佳实践读取模式选择、类型声明、默认值、通配符使用

OML 设计理念

WPL 与 OML 的协作关系

OML 不是独立工作的,它与 WPL 紧密配合:

1. WPL 解析原始数据
   ↓
2. 生成结构化数据 + rule 标识(如 /nginx/access_log)
   ↓
3. 系统查找匹配的 OML 配置(通过 rule 字段)
   ↓
4. 执行 OML 转换
   ↓
5. 输出到 Sink

关键点

  • 每个 OML 配置通过 rule 字段声明它处理哪些 WPL 规则的数据
  • 一个 WPL 规则可以对应多个 OML 配置
  • 支持通配符匹配,如 rule : /nginx/* 匹配所有 nginx 相关规则

示例

name : nginx_access_handler
rule : /nginx/access_log    # 只处理这个 WPL 规则的数据
---
# 转换逻辑...

声明式而非命令式

OML 采用声明式语法,你只需要描述想要什么结果,而不是如何实现

# 声明式:描述结果
user_info : obj = object {
    id : chars = read(user_id) ;
    name : chars = read(username) ;
} ;

对比命令式伪代码:

user_info = new Object()
user_info.id = get_field("user_id")
user_info.name = get_field("username")
convert_to_chars(user_info.id)
convert_to_chars(user_info.name)

不可变数据流

OML 中的数据转换是单向流动的:

输入数据 → OML 转换 → 输出数据
  • 输入数据不会被修改(除非使用 take
  • 每个转换步骤都产生新的值
  • 便于理解和调试

类型系统

基本类型

OML 提供 8 种基本数据类型:

name : types_example
---
# 字符串
text : chars = chars(hello) ;

# 整数
count : digit = digit(42) ;

# 浮点数
ratio : float = float(3.14) ;

# IP 地址
address : ip = ip(192.168.1.1) ;

# 时间
timestamp : time = Now::time() ;

# 布尔值
flag : bool = bool(true) ;

# 对象
info : obj = object { ... } ;

# 数组
items : array = collect read(keys:[...]) ;

自动类型推断

当不指定类型时,OML 会自动推断:

name : auto_type
---
# 自动推断为 chars
name = read(name) ;

# 自动推断为 digit
count = digit(100) ;

# 显式指定类型(推荐)
port : digit = read(port) ;

最佳实践:对于重要字段,建议显式声明类型以避免意外。

类型转换

OML 会在必要时自动进行类型转换:

name : type_cast
---
# 从字符串 "8080" 转换为整数 8080
port : digit = read(port) ;

# 从字符串 "192.168.1.1" 转换为 IP
ip_addr : ip = read(src_ip) ;

# 从字符串转换为时间
event_time : time = read(timestamp) ;

读取语义:read vs take

这是 OML 中最重要的概念之一。

read:非破坏性读取

特性

  • 从源数据克隆值到目标
  • 源数据保持不变
  • 可以多次读取同一字段

使用场景

  • 字段需要在多处使用
  • 需要保留原始数据

示例

name : read_example
---
# 假设输入:data = "hello"

field1 = read(data) ;  # field1 = "hello",src.data 仍存在
field2 = read(data) ;  # field2 = "hello",可以再次读取
field3 = read(data) ;  # field3 = "hello",仍然可以读取

take:破坏性读取

特性

  • 从源数据移走值到目标
  • 源数据中该字段被删除
  • 只能读取一次

使用场景

  • 字段只需要使用一次
  • 需要确保字段不被重复使用
  • 优化性能(避免数据复制)

示例

name : take_example
---
# 假设输入:data = "hello"

field1 = take(data) ;  # field1 = "hello",src.data 被移除
field2 = take(data) ;  # 失败!data 已经不存在

读取优先级

readtake 都遵循以下查找顺序:

  1. 先查找目标记录(dst)
  2. 如果未找到,再查找源记录(src)
name : lookup_priority
---
# 假设:src.value = "A"

field1 = read(value) ;     # "A" (从 src 读取)
field2 = read(field1) ;    # "A" (从 dst 读取,field1 已在目标中)

表达式类型

值表达式

直接构造常量值:

name : value_expr
---
# 字符串
text = chars(hello) ;

# 整数
count = digit(100) ;

# IP
ip_addr = ip(192.168.1.1) ;

函数调用

调用内置函数:

name : function_call
---
# 时间函数
now = Now::time() ;
today = Now::date() ;
hour = Now::hour() ;

管道表达式

链式处理数据:

name : pipe_expr
---
# 读取 → 转 JSON → Base64 编码
encoded = pipe read(data) | to_json | base64_encode ;

# 也可以省略 pipe 关键字
encoded2 = read(data) | to_json | base64_encode ;

条件表达式

基于条件选择值:

name : match_expr
---
level = match read(status) {
    in (digit(200), digit(299)) => chars(success) ;
    in (digit(400), digit(499)) => chars(error) ;
    _ => chars(other) ;
} ;

聚合表达式

创建复合数据结构:

name : aggregate_expr
---
# 对象聚合
info : obj = object {
    name = read(name) ;
    age = read(age) ;
} ;

# 数组聚合
items : array = collect read(keys:[a, b, c]) ;

默认值机制

默认值语法

当字段不存在或读取失败时,使用默认值:

name : default_value
---
# 语法:read(...) { _ : <默认值> }
country = read(country) { _ : chars(CN) } ;
version = read(version) { _ : chars(1.0.0) } ;
port = read(port) { _ : digit(8080) } ;

默认值可以是函数调用

name : default_with_function
---
# 如果 timestamp 不存在,使用当前时间
event_time = read(timestamp) { _ : Now::time() } ;

默认值可以是读取

name : default_with_read
---
# 如果 id 不存在,尝试读取 user_id
user_id = read(id) { _ : read(user_id) } ;

限制

  • @ref 语法糖不支持默认值
  • 默认值表达式不能是 matchobjectcollect 等复杂表达式

通配符与批量处理

通配符语法

使用 * 匹配多个字段:

name : wildcard
---
# 收集所有以 cpu_ 开头的字段
cpu_metrics = collect read(keys:[cpu_*]) ;

# 收集所有以 /path 结尾的字段
paths = collect read(keys:[*/path]) ;

批量目标

目标字段名包含 * 时进入批量模式:

name : batch_target
---
# 取走所有字段
* = take() ;

# 取走所有以 alert_ 开头的字段
alert* = take() ;

# 取走所有以 _log 结尾的字段
*_log = take() ;

限制:批量模式只支持 readtake,不支持其他表达式。

参数化读取

option:按优先级尝试

name : option_param
---
# 按顺序尝试读取 id、uid、user_id
user_id = read(option:[id, uid, user_id]) ;

行为

  • 从左到右尝试每个字段
  • 返回第一个存在的字段值
  • 如果都不存在,返回失败(可配合默认值)

keys:收集多个字段

name : keys_param
---
# 收集多个字段为数组
ports = collect read(keys:[sport, dport]) ;

行为

  • 读取所有指定的字段
  • 支持通配符 *
  • 返回数组

JSON 路径

读取嵌套数据:

name : json_path
---
# 读取 /user/info/name
username = read(/user/info/name) ;

# 读取数组元素 /items[0]/id
first_id = read(/items[0]/id) ;

表达式组合

嵌套对象

name : nested_objects
---
deployment : obj = object {
    app : obj = object {
        name = read(app_name) ;
        version = read(app_version) ;
    } ;
    env : obj = object {
        region = read(region) ;
        zone = read(zone) ;
    } ;
} ;

管道链

name : pipe_chain
---
# 多级转换
result = read(data) | to_json | base64_encode | html_escape ;

# 数组操作
first_user = read(users) | nth(0) | get(name) ;

match 中的复杂表达式

name : complex_match
---
status = match read(code) {
    in (digit(200), digit(299)) => collect read(keys:[a, b]) ;
    _ => read(default_value) ;
} ;

作用域规则

目标字段作用域

object 内部创建的字段,只在对象内可见:

name : scope_example
---
info : obj = object {
    name = read(username) ;  # name 只在 info 对象内
} ;

# 这里无法访问 name
other = read(name) ;  # 失败!name 不在外部作用域

全局目标字段

顶层定义的字段可以被后续读取:

name : global_scope
---
# 定义全局字段
temp = read(data) ;

# 后续可以读取
result = read(temp) ;

易错提醒

  1. 分号必需:每个顶层条目必须以 ; 结束
  2. 类型不匹配:显式类型声明与实际值不符会导致转换错误
  3. take 后再读:使用 take 后该字段被移除,无法再次读取
  4. @ref 限制@ref 只能在特定位置使用,不支持默认值
  5. 批量模式限制:目标名含 * 时,右值只能是 readtake

最佳实践

1. 选择合适的读取模式

# 推荐:字段复用时用 read
temp = read(data) ;
result1 = pipe read(temp) | to_json ;
result2 = pipe read(temp) | base64_encode ;

# 推荐:一次性使用时用 take
final = take(data) | to_json ;

2. 显式类型声明

# 推荐:明确类型
port : digit = read(port) ;
ip_addr : ip = read(src_ip) ;

# 可接受:简单场景自动推断
name = read(name) ;

3. 提供默认值

# 推荐:关键字段提供默认值
version = read(version) { _ : chars(1.0.0) } ;
timeout = read(timeout) { _ : digit(30) } ;

4. 合理使用通配符

# 推荐:明确的通配符模式
cpu_metrics = collect read(keys:[cpu_*]) ;

# 避免:过于宽泛的通配符
all = collect read(keys:[*]) ;  # 可能包含不需要的字段

下一步

OML 实战指南

按任务导向组织的实用指南,帮助你快速找到解决方案。


📚 任务导航

任务类型跳转
WPL 与 OML 关联理解关联机制、一对一/一对多关联
数据提取字段提取的各种方式
数据转换类型转换、时间、URL、Base64 等
数据聚合创建对象、数组
条件处理状态码分类、端口识别、IP 范围等
数据富化SQL 查询、多表关联
复杂场景Web 日志、系统监控完整处理

WPL 与 OML 关联

任务:理解关联机制

核心概念:OML 通过 rule 字段匹配 WPL 的 package/rule 路径来建立关联。

WPL 规则

package nginx {
  rule access_log {
    (ip:client_ip, time:timestamp, chars:request_uri, digit:status)
  }
}

完整路径/nginx/access_log(格式:/package/rule

OML 配置

name : nginx_processor
rule : /nginx/access_log    # 匹配 WPL 的 package/rule
---
client : ip = read(client_ip) ;
time : time = read(timestamp) ;
uri = read(request_uri) ;
status : digit = read(status) ;

说明:只有 WPL rule 为 /nginx/access_log 的数据会被这个 OML 处理。


任务:一对多关联(通配符匹配)

场景:一个 WPL 规则可以被多个 OML 配置处理

WPL 规则

package : nginx
rule : access_log
# 完整路径:/nginx/access_log

OML 配置 1(基础处理):

name : nginx_basic
rule : /nginx/*    # 匹配所有 nginx 相关规则
---
timestamp : time = Now::time() ;
source = chars(nginx) ;

OML 配置 2(访问日志专用):

name : nginx_access_detail
rule : /nginx/access_log    # 精确匹配访问日志
---
user_id = read(user_id) ;
uri = read(request_uri) ;
status : digit = read(status) ;

说明:同一条数据可以被多个 OML 配置处理(如果在不同的 Sink Group 中)。


任务:通配符模式匹配

场景:使用通配符处理多种类型的数据

支持的通配符模式

OML rule匹配的 WPL rule说明
/nginx/*/nginx/access_log
/nginx/error_log
前缀匹配
*/access_log/nginx/access_log
/apache/access_log
后缀匹配
/nginx/access*/nginx/access_log
/nginx/access_v2
部分匹配
*任意规则全匹配

示例:处理所有访问日志

name : all_access_logs
rule : */access_log    # 匹配所有 access_log
---
timestamp : time = Now::time() ;
uri = read(request_uri) ;
status : digit = read(status) ;

任务:多个 WPL 规则共享一个 OML

场景:不同来源的数据使用相同的转换逻辑

WPL 规则 1

package : nginx
rule : access_log
# 路径:/nginx/access_log

WPL 规则 2

package : apache
rule : access_log
# 路径:/apache/access_log

共享的 OML 配置

name : web_access_handler
rule : */access_log    # 匹配所有 access_log
---
# 统一的字段映射
timestamp : time = read(time) ;
client_ip : ip = read(option:[remote_addr, client_ip]) ;
uri = read(option:[request_uri, request]) ;
status : digit = read(option:[status, status_code]) ;

# 统一的输出格式
access : obj = object {
    time : time = read(timestamp) ;
    ip : ip = read(client_ip) ;
    uri : chars = read(uri) ;
    status : digit = read(status) ;
} ;

说明:使用 option 参数处理不同来源的字段名差异。


数据提取

综合示例:字段提取的各种方式

name : data_extraction
rule : /app/data
---
# 1. 简单提取
user_id = read(user_id) ;

# 2. 提供默认值
country = read(country) { _ : chars(CN) } ;

# 3. 按优先级尝试多个字段
user_id = read(option:[id, user_id, uid]) ;

# 4. 提取嵌套数据
username = read(/user/info/name) ;

# 5. 批量提取匹配模式
cpu_metrics = collect read(keys:[cpu_*]) ;

数据转换

综合示例:常用类型转换

name : type_conversion
rule : /app/data
---
# 字符串转各种类型
port : digit = read(port) ;                    # 转整数
ip : ip = read(ip_addr) ;                      # 转 IP
cpu : float = read(cpu_usage) ;                # 转浮点数
active : bool = read(is_active) ;              # 转布尔值

# 时间转时间戳
ts_sec = read(event_time) | Time::to_ts_zone(0, s) ;    # 秒
ts_ms = read(event_time) | Time::to_ts_zone(8, ms) ;    # 毫秒(UTC+8)

# URL 解析
domain = read(url) | url(domain) ;
path = read(url) | url(path) ;
params = read(url) | url(params) ;

# Base64 编解码
decoded = read(encoded) | base64_decode(Utf8) ;
encoded = read(message) | base64_encode ;

# IP 转整数
ip_int = read(src_ip) | ip4_to_int ;

数据聚合

任务:创建对象

name : create_object
rule : /system/metrics
---
system_info : obj = object {
    host : chars = read(hostname) ;
    cpu : float = read(cpu_usage) ;
    memory : float = read(mem_usage) ;
} ;

任务:创建嵌套对象

name : nested_object
rule : /app/deployment
---
deployment : obj = object {
    application : obj = object {
        name : chars = read(app_name) ;
        version : chars = read(version) ;
    } ;
    infrastructure : obj = object {
        region : chars = read(region) ;
        instance_id : chars = read(instance_id) ;
    } ;
} ;

任务:创建数组

name : create_array
rule : /network/ports
---
# 收集多个端口
ports : array = collect read(keys:[sport, dport]) ;

# 转换为 JSON 字符串
ports_json = read(ports) | to_json ;

# 获取数组元素
first_port = read(ports) | nth(0) ;

条件处理

任务:状态码分类

name : status_classification
rule : /http/response
---
status_level = match read(status_code) {
    in (digit(200), digit(299)) => chars(success) ;
    in (digit(400), digit(499)) => chars(client_error) ;
    in (digit(500), digit(599)) => chars(server_error) ;
    _ => chars(unknown) ;
} ;

任务:端口服务识别

name : port_service
rule : /network/traffic
---
service = match read(port) {
    digit(22) => chars(SSH) ;
    digit(80) => chars(HTTP) ;
    digit(443) => chars(HTTPS) ;
    digit(3306) => chars(MySQL) ;
    _ => chars(Unknown) ;
} ;

任务:IP 地址范围匹配

name : ip_zone_match
rule : /network/connection
---
zone = match read(src_ip) {
    in (ip(10.0.0.0), ip(10.255.255.255)) => chars(Private) ;
    in (ip(172.16.0.0), ip(172.31.255.255)) => chars(Private) ;
    in (ip(192.168.0.0), ip(192.168.255.255)) => chars(Private) ;
    _ => chars(Public) ;
} ;

任务:多条件组合判断

name : multi_condition
rule : /firewall/rule
---
traffic_type = match (read(protocol), read(port)) {
    (chars(tcp), digit(22)) => chars(SSH) ;
    (chars(tcp), digit(443)) => chars(HTTPS) ;
    (chars(udp), digit(53)) => chars(DNS) ;
    _ => chars(Other) ;
} ;

数据富化(SQL 查询)

任务:用户信息查询

场景:根据 user_id 查询用户详细信息

输入

user_id = "1001"

数据库表 (users)

idnameemaildepartment
1001张三zhangsan@example.com研发部

OML

name : user_lookup
---
user_name, user_email, user_dept =
    select name, email, department
    from users
    where id = read(user_id) ;

输出

user_name = "张三"
user_email = "zhangsan@example.com"
user_dept = "研发部"

任务:IP 地理位置查询

场景:查询 IP 地址的地理位置信息

输入

src_ip = "203.0.113.1"

数据库表 (ip_geo)

ip_start_intip_end_intcountrycity
34058037763405804031USLos Angeles

OML

name : ip_geolocation
---
# 先将 IP 转为整数
ip_int = pipe read(src_ip) | ip4_to_int ;

# 查询地理位置
country, city =
    select country, city
    from ip_geo
    where ip_start_int <= read(ip_int)
      and ip_end_int >= read(ip_int) ;

输出

ip_int = 3405803777
country = "US"
city = "Los Angeles"

任务:多表关联查询

场景:通过多次查询关联多个表的数据

输入

order_id = "ORD-2024-001"

OML

name : multi_table_lookup
---
# 第一步:查询订单信息
user_id, amount =
    select user_id, amount
    from orders
    where id = read(order_id) ;

# 第二步:查询用户信息
user_name, level =
    select name, level
    from users
    where id = read(user_id) ;

# 第三步:查询折扣信息
discount =
    select discount
    from user_levels
    where level = read(level) ;

输出

user_id = "U1001"
amount = "199.99"
user_name = "王五"
level = "VIP"
discount = "0.9"

复杂场景

场景:Web 访问日志完整处理

任务:处理 Web 访问日志,包含字段提取、类型转换、条件判断、数据聚合

输入

timestamp = "15/Jan/2024:14:30:00 +0800"
src_ip = "203.0.113.1"
method = "GET"
url = "/api/users?page=1"
status = "200"
size = "1234"

OML

name : web_log_processing
---
# 时间处理
event_ts = pipe read(timestamp) | Time::to_ts_zone(0, s) ;

# 字段提取
source_ip : ip = read(src_ip) ;
http_method = read(method) ;
status_code : digit = read(status) ;
response_size : digit = read(size) ;

# URL 解析
request_path = pipe read(url) | url(path) ;
query_params = pipe read(url) | url(params) ;

# 状态码分类
status_category = match read(status_code) {
    in (digit(200), digit(299)) => chars(Success) ;
    in (digit(400), digit(499)) => chars(Client_Error) ;
    in (digit(500), digit(599)) => chars(Server_Error) ;
    _ => chars(Unknown) ;
} ;

# 数据聚合
access_log : obj = object {
    timestamp : digit = read(event_ts) ;
    client : obj = object {
        ip : ip = read(source_ip) ;
    } ;
    request : obj = object {
        method : chars = read(http_method) ;
        path : chars = read(request_path) ;
        query : chars = read(query_params) ;
    } ;
    response : obj = object {
        status : digit = read(status_code) ;
        category : chars = read(status_category) ;
        size : digit = read(response_size) ;
    } ;
} ;

输出

{
    "access_log": {
        "timestamp": 1705318200,
        "client": {
            "ip": "203.0.113.1"
        },
        "request": {
            "method": "GET",
            "path": "/api/users",
            "query": "page=1"
        },
        "response": {
            "status": 200,
            "category": "Success",
            "size": 1234
        }
    }
}

场景:系统监控数据处理

任务:处理系统监控数据,包含数据提取、告警判断、嵌套对象创建

输入

hostname = "prod-web-01"
cpu_user = "65.5"
cpu_system = "15.2"
mem_used = "6144"
mem_total = "8192"

OML

name : system_monitoring
---
# 时间戳
event_time = Now::time() ;

# 告警判断
cpu_alert = match read(cpu_user) {
    in (digit(0), digit(60)) => chars(Normal) ;
    in (digit(60), digit(80)) => chars(Warning) ;
    _ => chars(Critical) ;
} ;

mem_alert = match read(mem_used) {
    in (digit(0), digit(6000)) => chars(Normal) ;
    in (digit(6000), digit(7000)) => chars(Warning) ;
    _ => chars(Critical) ;
} ;

# 数据聚合
metrics : obj = object {
    host : obj = object {
        name : chars = read(hostname) ;
        timestamp : time = read(event_time) ;
    } ;
    cpu : obj = object {
        user : float = read(cpu_user) ;
        system : float = read(cpu_system) ;
        alert : chars = read(cpu_alert) ;
    } ;
    memory : obj = object {
        used : digit = read(mem_used) ;
        total : digit = read(mem_total) ;
        alert : chars = read(mem_alert) ;
    } ;
} ;

输出

{
    "metrics": {
        "host": {
            "name": "prod-web-01",
            "timestamp": "2024-01-15 14:30:00"
        },
        "cpu": {
            "user": 65.5,
            "system": 15.2,
            "alert": "Warning"
        },
        "memory": {
            "used": 6144,
            "total": 8192,
            "alert": "Warning"
        }
    }
}


下一步

OML 函数参考

本文档提供所有内置函数和管道函数的完整参考,采用标准化格式便于查找。


📚 详细文档导航


📋 OML 所有函数速查

内置函数

函数说明示例
Now::time()获取当前时间event_time = Now::time() ;
Now::date()获取当前日期(YYYYMMDD)today = Now::date() ;
Now::hour()获取当前小时(YYYYMMDDHH)current_hour = Now::hour() ;

管道函数

功能分类函数说明示例
编码base64_encodeBase64 编码read(data) | base64_encode
base64_decodeBase64 解码(支持 Utf8/Gbk)read(data) | base64_decode(Utf8)
转义html_escapeHTML 转义read(text) | html_escape
html_unescapeHTML 反转义read(html) | html_unescape
json_escapeJSON 转义read(text) | json_escape
json_unescapeJSON 反转义read(json) | json_unescape
str_escape字符串转义read(str) | str_escape
时间Time::to_ts转时间戳(秒,UTC+8)read(time) | Time::to_ts
Time::to_ts_ms转时间戳(毫秒,UTC+8)read(time) | Time::to_ts_ms
Time::to_ts_us转时间戳(微秒,UTC+8)read(time) | Time::to_ts_us
Time::to_ts_zone转指定时区时间戳read(time) | Time::to_ts_zone(0, ms)
数据访问nth(index)获取数组元素read(arr) | nth(0)
get(key)获取对象字段read(obj) | get(name)
path(part)提取文件路径(name/path)read(path) | path(name)
url(part)提取 URL(domain/host/path/params/uri)read(url) | url(domain)
sxf_get(field)提取特殊格式字段read(log) | sxf_get(status)
转换to_str转换为字符串read(ip) | to_str
to_json转换为 JSONread(arr) | to_json
ip4_to_intIPv4 转整数read(ip) | ip4_to_int
控制skip_empty跳过空值read(field) | skip_empty

常用场景速查

我想做什么使用方法
获取当前时间event_time = Now::time() ;
时间转时间戳ts = read(time) | Time::to_ts_zone(0, ms) ;
Base64 解码decoded = read(data) | base64_decode(Utf8) ;
HTML 转义escaped = read(text) | html_escape ;
解析 URLdomain = read(url) | url(domain) ;
提取文件名filename = read(path) | path(name) ;
获取数组第一个元素first = read(arr) | nth(0) ;
获取对象字段name = read(obj) | get(name) ;
IP 转整数ip_int = read(ip) | ip4_to_int ;
跳过空值result = read(field) | skip_empty ;
链式处理result = read(data) | to_json | base64_encode ;
字符串格式化msg = fmt("{}:{}", @ip, @port) ;
条件匹配level = match read(status) { ... } ;
创建对象info : obj = object { ... } ;
创建数组items : array = collect read(keys:[...]) ;
提供默认值country = read(country) { _ : chars(CN) } ;
选择性读取id = read(option:[id, uid, user_id]) ;
批量收集metrics = collect read(keys:[cpu_*]) ;

内置函数

内置函数可以直接在赋值表达式中使用,无需 pipe 关键字。

Now::time()

获取当前时间。

语法

Now::time()

参数:无

返回类型time

示例

event_time : time = Now::time() ;
# 输出:2024-01-15 14:30:45

Now::date()

获取当前日期,格式为 YYYYMMDD 的整数。

语法

Now::date()

参数:无

返回类型digit

示例

today : digit = Now::date() ;
# 输出:20240115

Now::hour()

获取当前时间精确到小时,格式为 YYYYMMDDHH 的整数。

语法

Now::hour()

参数:无

返回类型digit

示例

current_hour : digit = Now::hour() ;
# 输出:2024011514

管道函数

管道函数通过 pipe 关键字和 | 操作符链式调用(pipe 关键字可省略)。

基本语法

# 使用 pipe 关键字
result = pipe read(field) | function1 | function2(param) ;

# 省略 pipe 关键字
result = read(field) | function1 | function2(param) ;

编码函数

base64_encode

将字符串进行 Base64 编码。

语法

| base64_encode

参数:无

输入类型chars 输出类型chars

示例

encoded = read(payload) | base64_encode ;
# 输入:"Hello, OML!"
# 输出:"SGVsbG8sIE9NTCE="

base64_decode

将 Base64 编码的字符串解码。

语法

| base64_decode
| base64_decode(<encoding>)

参数

  • encoding(可选):字符编码类型,默认为 Utf8

支持的编码

  • Utf8 - UTF-8 编码(默认)
  • Gbk - GBK 中文编码
  • Imap - IMAP Base64 变体(将非 ASCII 字节转义为 \xNN 格式)
  • 更多编码请参阅源码文档

输入类型chars 输出类型chars

示例

# 标准 UTF-8 解码
decoded = read(data) | base64_decode ;
# 输入:"SGVsbG8sIE9NTCE="
# 输出:"Hello, OML!"

# GBK 中文解码
gbk_text = read(gbk_data) | base64_decode(Gbk) ;

# IMAP 变体解码(处理二进制数据)
raw = read(binary_data) | base64_decode(Imap) ;

转义函数

html_escape

对 HTML 特殊字符进行转义。

语法

| html_escape

参数:无

转义规则

  • <&lt;
  • >&gt;
  • &&amp;
  • "&quot;
  • '&#x27;

输入类型chars 输出类型chars

示例

safe_html = read(user_input) | html_escape ;
# 输入:"<script>alert('xss')</script>"
# 输出:"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;"

html_unescape

将 HTML 实体还原为原始字符。

语法

| html_unescape

参数:无

输入类型chars 输出类型chars

示例

original = read(escaped_html) | html_unescape ;
# 输入:"&lt;div&gt;Hello&lt;/div&gt;"
# 输出:"<div>Hello</div>"

json_escape

对 JSON 字符串中的特殊字符进行转义。

语法

| json_escape

参数:无

输入类型chars 输出类型chars

示例

json_safe = read(text) | json_escape ;
# 转义引号、反斜杠、换行符等 JSON 特殊字符

json_unescape

将 JSON 转义序列还原为原始字符。

语法

| json_unescape

参数:无

输入类型chars 输出类型chars

示例

original = read(escaped_json) | json_unescape ;
# 还原 \n、\t、\"等转义序列

str_escape

对字符串中的特殊字符进行转义(主要是引号和反斜杠)。

语法

| str_escape

参数:无

输入类型chars 输出类型chars

示例

escaped = read(raw_string) | str_escape ;
# 输入:'hello"world'
# 输出:'hello\"world'

时间函数

Time::to_ts

将时间转换为 Unix 时间戳(秒),使用 UTC+8 时区。

语法

| Time::to_ts

参数:无

输入类型time 输出类型digit

示例

timestamp = read(occur_time) | Time::to_ts ;
# 输入:2024-01-15 14:30:00
# 输出:1705304400(UTC+8)

Time::to_ts_ms

将时间转换为 Unix 时间戳(毫秒),使用 UTC+8 时区。

语法

| Time::to_ts_ms

参数:无

输入类型time 输出类型digit

示例

timestamp_ms = read(occur_time) | Time::to_ts_ms ;
# 输入:2024-01-15 14:30:00
# 输出:1705304400000

Time::to_ts_us

将时间转换为 Unix 时间戳(微秒),使用 UTC+8 时区。

语法

| Time::to_ts_us

参数:无

输入类型time 输出类型digit

示例

timestamp_us = read(occur_time) | Time::to_ts_us ;
# 输入:2024-01-15 14:30:00
# 输出:1705304400000000

Time::to_ts_zone

将时间转换为指定时区的 Unix 时间戳。

语法

| Time::to_ts_zone(<timezone_offset>, <unit>)

参数

  • timezone_offset:时区偏移(小时)
    • 0:UTC
    • 8:UTC+8(北京时间)
    • -5:UTC-5(美东时间)
  • unit:时间戳单位
    • sss:秒
    • ms:毫秒
    • us:微秒

输入类型time 输出类型digit

示例

# UTC 时间戳(秒)
utc_ts = read(occur_time) | Time::to_ts_zone(0, s) ;

# UTC+8 时间戳(毫秒)
beijing_ts_ms = read(occur_time) | Time::to_ts_zone(8, ms) ;

# UTC-5 时间戳(秒)
eastern_ts = read(occur_time) | Time::to_ts_zone(-5, ss) ;

# UTC 时间戳(微秒)
utc_ts_us = read(occur_time) | Time::to_ts_zone(0, us) ;

数据访问函数

nth

获取数组中指定索引的元素。

语法

| nth(<index>)

参数

  • index:数组索引(从 0 开始)

输入类型array 输出类型:元素类型

示例

first_item = read(items) | nth(0) ;
second_item = read(items) | nth(1) ;
# 输入:[10, 20, 30]
# nth(0) 输出:10
# nth(1) 输出:20

get

获取对象中指定键的值。

语法

| get(<key>)

参数

  • key:对象的字段名

输入类型obj 输出类型:字段值类型

示例

# 获取对象的字段
name = read(user) | get(name) ;

# 链式调用
first_name = read(users) | nth(0) | get(name) ;
# 输入:[{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
# 输出:"John"

path

从文件路径中提取指定部分。

语法

| path(<part>)

参数

  • part:要提取的部分
    • name:文件名(含扩展名)
    • path:目录路径

输入类型chars 输出类型chars

示例

# 输入:"C:\Users\test\file.txt"
filename = read(file_path) | path(name) ;
# 输出:"file.txt"

parent = read(file_path) | path(path) ;
# 输出:"C:/Users/test"

url

从 URL 中提取指定部分。

语法

| url(<part>)

参数

  • part:要提取的部分
    • domain:域名(不含端口)
    • host:主机(含端口)
    • path:路径
    • uri:完整 URI(路径 + 查询 + 片段)
    • params:查询参数

输入类型chars 输出类型chars

示例

# 输入:"https://api.example.com:8080/v1/users?id=1&type=admin#section"

domain = read(http_url) | url(domain) ;
# 输出:"api.example.com"

host = read(http_url) | url(host) ;
# 输出:"api.example.com:8080"

path = read(http_url) | url(path) ;
# 输出:"/v1/users"

uri = read(http_url) | url(uri) ;
# 输出:"/v1/users?id=1&type=admin#section"

params = read(http_url) | url(params) ;
# 输出:"id=1&type=admin"

sxf_get

从特殊格式的文本中提取字段值。

语法

| sxf_get(<field_name>)

参数

  • field_name:要提取的字段名

输入类型chars 输出类型chars

示例

# 从格式化文本中提取字段
status = read(log_line) | sxf_get(statusCode) ;
username = read(log_line) | sxf_get(username) ;

转换函数

to_str

将值转换为字符串。

语法

| to_str

参数:无

输入类型:任意类型 输出类型chars

示例

ip_str = read(src_ip) | to_str ;
# 输入:192.168.1.100(IP 类型)
# 输出:"192.168.1.100"

num_str = read(count) | to_str ;
# 输入:42(digit 类型)
# 输出:"42"

to_json

将值转换为 JSON 字符串。

语法

| to_json

参数:无

输入类型:任意类型 输出类型chars

示例

# 数组转 JSON
ports_json = read(ports) | to_json ;
# 输入:[80, 443]
# 输出:"[80,443]"

# 对象转 JSON
user_json = read(user) | to_json ;
# 输入:{name: "John", age: 30}
# 输出:'{"name":"John","age":30}'

ip4_to_int

将 IPv4 地址转换为整数。

语法

| ip4_to_int

参数:无

输入类型ipchars 输出类型digit

示例

ip_int = read(src_ip) | ip4_to_int ;
# 输入:192.168.1.100
# 输出:3232235876

# 用于 IP 范围比较
ip_int = read(src_ip) | ip4_to_int ;
in_range = match read(ip_int) {
    in (digit(3232235776), digit(3232236031)) => chars(True) ;
    _ => chars(False) ;
} ;

控制函数

skip_empty

如果输入值为空,则跳过该字段的输出。

语法

| skip_empty

参数:无

输入类型:任意类型 输出类型:原类型或跳过

何时被视为“空“

  • 空字符串 ""
  • 空数组 []
  • 数值 0
  • 空对象 {}

示例

# 如果 optional_field 为空,则不输出 result 字段
result = read(optional_field) | skip_empty ;

# 常用于过滤空数组
items = read(items_array) | skip_empty ;


下一步

OML 集成指南

本文档介绍如何将 OML 集成到数据处理流水线中,包括配置关联、数据流向和路由规则。

目录


数据流概览

在 WP Engine 数据处理流水线中,数据的完整流向如下:

原始日志/数据
    ↓
[WPL 解析]
    ↓
结构化数据(携带 Rule 标识)
    ↓
[OML 转换](可选)
    ↓
目标格式数据
    ↓
[Sink 输出]
    ↓
目标存储(文件/数据库/消息队列等)

关键概念

  • WPL Rule:标识数据来源和类型的路径,如 /nginx/access_log
  • OML Model:定义数据转换逻辑的 OML 配置文件
  • Sink Group:输出组,定义数据的输出目标集合
  • Connector:连接器,定义具体的输出方式(文件、Kafka、数据库等)

OML 在数据流中的位置

OML 作为数据转换引擎,位于 WPL 解析和 Sink 输出之间:

graph LR
    A[原始数据] --> B[WPL 解析]
    B --> C[结构化数据<br/>rule: /nginx/access_log]
    C --> D{查找匹配的<br/>OML 模型}
    D -->|找到匹配| E[OML 转换]
    D -->|未找到| F[数据透传]
    E --> G[转换后的数据]
    F --> G
    G --> H[Sink 输出]

    style C fill:#e1f5ff
    style E fill:#fff4e1
    style G fill:#e8f5e9

工作原理

  1. WPL 解析器处理原始数据,生成结构化数据并附带 Rule 标识
  2. 系统根据 Sink Group 配置查找匹配的 OML 模型
  3. 如果找到匹配的 OML 模型,执行转换;否则数据透传
  4. 转换后的数据发送到 Sink 进行输出

配置文件结构

项目目录结构

project/
├── oml/                        # OML 模型目录
│   ├── nginx_access.oml        # Nginx 访问日志转换
│   ├── nginx_error.oml         # Nginx 错误日志转换
│   └── system_metrics.oml      # 系统监控数据转换
├── topology/
│   └── sinks/                  # Sink 配置目录
│       ├── web_logs.toml       # Web 日志输出组
│       └── metrics.toml        # 监控数据输出组
└── connectors/                 # 连接器定义
    ├── file_sink.toml
    ├── kafka_sink.toml
    └── mysql_sink.toml

OML 模型文件

OML 模型文件位于 oml/ 目录,使用 .oml 扩展名。

文件格式

name : <模型名称>
rule : <WPL 规则模式>
---
<字段转换定义>

示例oml/nginx_access.oml

name : nginx_access
rule : /nginx/access*
---
user_id = read(user_id) ;
timestamp : time = read(time) ;
status : digit = read(status_code) ;
uri = read(request_uri) ;

# 创建结构化输出
log : obj = object {
    user : chars = read(user_id) ;
    time : time = read(timestamp) ;
    status : digit = read(status) ;
    uri : chars = read(uri) ;
} ;

Sink Group 配置文件

Sink Group 配置文件位于 topology/sinks/ 目录,使用 TOML 格式。

文件格式

version = "2.0"

[sink_group]
name = "输出组名称"
oml = ["<OML 模型名称>", ...]  # 关联的 OML 模型
rule = ["<WPL 规则模式>", ...]  # 可选:限定处理的规则
parallel = 1                     # 可选:并行度
tags = ["key:value", ...]        # 可选:标签

[[sink_group.sinks]]
name = "输出目标名称"
connect = "<连接器 ID>"
tags = ["key:value", ...]

[sink_group.sinks.params]
# 连接器特定的参数

示例topology/sinks/web_logs.toml

version = "2.0"

[sink_group]
name = "web_logs"
oml = ["*"]           # 使用所有匹配的 OML 模型
rule = []             # 不限定规则
parallel = 1

[[sink_group.sinks]]
name = "access_logs"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "access.json"

[[sink_group.sinks]]
name = "error_logs"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "error.json"

OML 与 Sink 关联

关联方式

Sink Group 通过 oml 字段与 OML 模型建立关联,支持三种模式:

1. 通配符模式(推荐)

使用 ["*"] 自动匹配所有符合条件的 OML 模型。

[sink_group]
name = "all_logs"
oml = ["*"]  # 自动匹配所有 OML 模型

工作原理

  1. 数据携带 WPL Rule(如 /nginx/access_log
  2. 系统遍历所有 OML 模型
  3. 找到 rule 字段匹配的 OML 模型
  4. 执行转换

适用场景

  • 需要处理多种类型的数据
  • OML 模型经常变动
  • 希望自动发现新的 OML 模型

2. 指定模型名称

明确列出需要使用的 OML 模型。

[sink_group]
name = "nginx_logs"
oml = ["nginx_access", "nginx_error"]

工作原理

  • 只在指定的 OML 模型列表中查找匹配
  • 其他 OML 模型会被忽略

适用场景

  • 明确知道需要哪些 OML 模型
  • 需要精确控制转换逻辑
  • 避免意外使用其他 OML 模型

3. 空列表(数据透传)

不使用任何 OML 转换,数据直接输出。

[sink_group]
name = "raw_logs"
oml = []  # 不使用 OML 转换

适用场景

  • 不需要数据转换
  • 直接输出 WPL 解析后的原始数据

路由规则

OML 模型匹配规则

OML 模型通过 rule 字段定义匹配的 WPL Rule 模式,支持通配符。

匹配逻辑

数据的 WPL Rule 是否匹配 OML 模型的 rule 字段

通配符规则

  • * 匹配任意字符(包括 /
  • 支持前缀匹配、后缀匹配、完全匹配

示例

OML rule 字段数据 WPL Rule是否匹配
/nginx/*/nginx/access_log✅ 匹配
/nginx/*/nginx/error_log✅ 匹配
/nginx/access*/nginx/access_log✅ 匹配
/nginx/access*/nginx/error_log❌ 不匹配
*任意规则✅ 匹配
/apache/*/nginx/access_log❌ 不匹配

Sink Group 规则过滤

Sink Group 可以通过 rule 字段进一步限定处理的数据。

[sink_group]
name = "filtered_logs"
oml = ["*"]
rule = ["/nginx/*", "/apache/*"]  # 只处理 Nginx 和 Apache 日志

工作流程

1. 数据的 WPL Rule 是否匹配 Sink Group 的 rule?
   ├─ 是 → 继续
   └─ 否 → 跳过此 Sink Group

2. 查找匹配的 OML 模型
   ├─ 找到 → 执行转换
   └─ 未找到 → 数据透传(如果 oml=["*"] 或 oml=[])

3. 输出到 Sink

配置示例

示例 1:基础配置

处理 Nginx 访问日志并输出到文件。

OML 模型oml/nginx_access.oml

name : nginx_access
rule : /nginx/access_log
---
user_id = read(user_id) ;
timestamp : time = read(time) ;
status : digit = read(status_code) ;
uri = read(request_uri) ;

access_log : obj = object {
    user : chars = read(user_id) ;
    time : time = read(timestamp) ;
    status : digit = read(status) ;
    uri : chars = read(uri) ;
} ;

Sink Group 配置topology/sinks/nginx_logs.toml

version = "2.0"

[sink_group]
name = "nginx_access_logs"
oml = ["nginx_access"]

[[sink_group.sinks]]
name = "file_output"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "nginx_access.json"

示例 2:多 OML 模型配置

处理多种类型的日志。

OML 模型 1oml/nginx_access.oml

name : nginx_access
rule : /nginx/access*
---
user_id = read(user_id) ;
uri = read(request_uri) ;
status : digit = read(status_code) ;

OML 模型 2oml/apache_access.oml

name : apache_access
rule : /apache/access*
---
user = read(remote_user) ;
path = read(request) ;
code : digit = read(status) ;

Sink Group 配置topology/sinks/web_logs.toml

version = "2.0"

[sink_group]
name = "all_web_logs"
oml = ["*"]  # 自动匹配所有 OML 模型

[[sink_group.sinks]]
name = "unified_output"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "web_logs.json"

示例 3:条件路由

根据数据类型输出到不同目标。

Sink Group 1topology/sinks/error_logs.toml

version = "2.0"

[sink_group]
name = "error_logs"
oml = ["*"]
rule = ["*/error*"]  # 只处理错误日志

[[sink_group.sinks]]
name = "error_file"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "errors.json"

Sink Group 2topology/sinks/access_logs.toml

version = "2.0"

[sink_group]
name = "access_logs"
oml = ["*"]
rule = ["*/access*"]  # 只处理访问日志

[[sink_group.sinks]]
name = "access_file"
connect = "file_json_sink"

[sink_group.sinks.params]
file = "access.json"

示例 4:数据富化(SQL 集成)

从数据库查询额外信息。

OML 模型oml/user_activity.oml

name : user_activity
rule : /app/user_activity
---
user_id = read(user_id) ;
action = read(action) ;
timestamp : time = Now::time() ;

# 从数据库查询用户信息
user_name, user_level =
    select name, level
    from users
    where id = read(user_id) ;

# 聚合输出
activity : obj = object {
    user : obj = object {
        id : chars = read(user_id) ;
        name : chars = read(user_name) ;
        level : chars = read(user_level) ;
    } ;
    action : chars = read(action) ;
    time : time = read(timestamp) ;
} ;

Sink Group 配置topology/sinks/user_activities.toml

version = "2.0"

[sink_group]
name = "user_activities"
oml = ["user_activity"]

[[sink_group.sinks]]
name = "kafka_output"
connect = "kafka_sink"

[sink_group.sinks.params]
topic = "user-activities"
brokers = ["localhost:9092"]

工作流程

完整处理流程

1. 数据到达
   ↓
2. WPL 解析
   ├─ 生成结构化数据
   └─ 附加 Rule 标识(如 /nginx/access_log)
   ↓
3. Sink Group 匹配
   ├─ 检查 Sink Group 的 rule 字段
   └─ 匹配 → 继续;不匹配 → 跳过
   ↓
4. OML 模型查找
   ├─ 根据 Sink Group 的 oml 字段
   ├─ 查找匹配的 OML 模型
   └─ 找到 → 转换;未找到 → 透传
   ↓
5. 数据转换(如果有 OML)
   ├─ 执行 OML 转换逻辑
   └─ 生成目标格式数据
   ↓
6. Sink 输出
   ├─ 根据 Connector 配置
   └─ 输出到目标存储

多 Sink Group 并行处理

一条数据可以被多个 Sink Group 同时处理:

                数据(rule: /nginx/access_log)
                        |
        +---------------+---------------+
        |                               |
    Sink Group 1                   Sink Group 2
    (rule: ["/nginx/*"])          (rule: ["/nginx/access*"])
        |                               |
    OML 转换 A                      OML 转换 B
        |                               |
    输出到文件                       输出到 Kafka

最佳实践

1. OML 模型命名

使用清晰的命名规则:

<数据源>_<数据类型>.oml

示例:
- nginx_access.oml
- apache_error.oml
- system_metrics.oml

2. Rule 模式设计

使用有意义的层次结构:

# 好的示例
rule : /nginx/access_log
rule : /apache/access_log
rule : /system/cpu_metrics

# 避免过于宽泛
rule : *  # 会匹配所有数据

3. Sink Group 组织

按业务逻辑组织 Sink Group:

topology/sinks/
├── web_logs.toml           # Web 日志
├── system_metrics.toml     # 系统监控
├── application_logs.toml   # 应用日志
└── security_events.toml    # 安全事件

4. 使用通配符模式

对于灵活的配置,推荐使用通配符:

[sink_group]
oml = ["*"]  # 自动发现所有 OML 模型

5. 数据透传场景

不需要转换时,明确使用空列表:

[sink_group]
oml = []  # 明确表示不使用 OML

6. 测试配置

在部署前测试配置:

  1. 验证 OML 语法
  2. 检查 Rule 匹配逻辑
  3. 确认 Sink 连接
  4. 测试数据流向

故障排查

问题 1:数据未经过 OML 转换

可能原因

  • OML 模型的 rule 不匹配数据的 WPL Rule
  • Sink Group 的 oml 配置错误

排查步骤

  1. 检查数据的 WPL Rule
  2. 检查 OML 模型的 rule 字段
  3. 验证通配符匹配逻辑
  4. 检查 Sink Group 的 oml 配置

示例

# OML 模型
rule : /nginx/access_log

# 数据 Rule
/nginx/access  # ❌ 不匹配(缺少 _log)

# 修正
rule : /nginx/access*  # ✅ 匹配

问题 2:数据被重复处理

可能原因

  • 多个 Sink Group 匹配同一数据
  • 多个 OML 模型匹配同一 Rule

排查步骤

  1. 检查所有 Sink Group 的 rule 配置
  2. 确认是否需要并行处理
  3. 调整 Rule 模式避免重复

问题 3:OML 转换失败

可能原因

  • OML 语法错误
  • 字段不存在
  • 类型转换失败

排查步骤

  1. 检查 OML 语法
  2. 验证输入数据结构
  3. 使用默认值处理缺失字段
  4. 检查类型转换逻辑

示例

# 容错处理
port : digit = read(port) { _ : digit(80) } ;
user_id = read(user_id) { _ : chars(unknown) } ;

问题 4:Sink 输出失败

可能原因

  • Connector 配置错误
  • 目标存储不可用
  • 权限问题

排查步骤

  1. 检查 Connector 配置
  2. 验证目标存储连接
  3. 检查权限和凭证
  4. 查看错误日志

下一步

OML 语法参考

本文档提供 OML 的完整语法定义(EBNF 格式),用于精确理解语法规则。

基于源码 crates/wp-oml 的解析实现,词法细节复用 wp_parserwpl 的既有解析能力。


📚 文档导航

章节内容
EBNF 符号说明语法符号含义
顶层结构OML 文件结构
求值表达式表达式类型、值表达式、函数调用等
高级表达式格式化字符串、管道、match、聚合
SQL 表达式SQL 查询语法
隐私段数据脱敏语法
词法与约定标识符、字面量、注释
数据类型8 种数据类型
完整示例综合示例
管道函数速查常用管道函数
语法要点必需元素、可选元素、注意事项

EBNF 符号说明

  • = : 定义
  • , : 连接(序列)
  • | : 或(选择)
  • [ ] : 可选(0 或 1 次)
  • { } : 重复(0 或多次)
  • ( ) : 分组
  • "text" : 字面量
  • (* ... *) : 注释

顶层结构

oml              = header, sep_line, aggregate_items, [ sep_line, privacy_items ] ;

header           = "name", ":", name, eol,
                   [ "rule", ":", rule_path, { rule_path }, eol ] ;

sep_line         = "---" ;

name             = path ;                       (* 例如: test *)
rule_path        = wild_path ;                  (* 例如: wpx/abc, wpx/efg *)

aggregate_items  = aggregate_item, { aggregate_item } ;
aggregate_item   = target_list, "=", eval, ";" ;

target_list      = target, { ",", target } ;
target           = target_name, [ ":", data_type ] ;
target_name      = wild_key | "_" ;            (* 允许带通配符 '*';'_' 表示匿名/丢弃 *)
data_type        = type_ident ;                (* auto|ip|chars|digit|float|time|bool|obj|array *)

说明

  • name : <配置名称> - 必需的配置名称声明
  • rule : <规则路径> - 可选的规则关联
  • --- - 分隔符,区分声明区和配置区
  • 每个配置条目必须以 ; 结束

求值表达式

表达式类型

eval             = take_expr
                 | read_expr
                 | fmt_expr
                 | pipe_expr
                 | map_expr
                 | collect_expr
                 | match_expr
                 | sql_expr
                 | value_expr
                 | fun_call ;

读取表达式

(* 变量获取:take/read 支持统一参数形态;可跟缺省体 *)
take_expr        = "take", "(", [ arg_list ], ")", [ default_body ] ;
read_expr        = "read", "(", [ arg_list ], ")", [ default_body ] ;

arg_list         = arg, { ",", arg } ;
arg              = "option", ":", "[", key, { ",", key }, "]"
                 | ("in"|"keys"), ":", "[", key, { ",", key }, "]"
                 | "get",    ":", simple
                 | json_path ;                 (* 见 wp_parser::atom::take_json_path *)

default_body     = "{", "_", ":", gen_acq, [ ";" ], "}" ;
gen_acq          = take_expr | read_expr | value_expr | fun_call ;

说明

  • @ 仅作为变量获取语法糖用于 fmt/pipe/collect 的 var_get 位置
  • @ref 等价于 read(ref),但不支持缺省体
  • 不作为独立求值表达式

示例

# 基本读取
value = read(field) ;

# 带默认值
value = read(field) { _ : chars(default) } ;

# option 参数
value = read(option:[id, uid, user_id]) ;

# keys 参数
values = collect read(keys:[field1, field2]) ;

# JSON 路径
name = read(/user/info/name) ;

值表达式

(* 常量值:类型名+括号包裹的字面量 *)
value_expr       = data_type, "(", literal, ")" ;

示例

text = chars(hello) ;
count = digit(42) ;
address = ip(192.168.1.1) ;
flag = bool(true) ;

函数调用

(* 内置函数(零参占位):Now::* 家族 *)
fun_call         = ("Now::time"
                   |"Now::date"
                   |"Now::hour"), "(", ")" ;

示例

now = Now::time() ;
today = Now::date() ;
hour = Now::hour() ;

高级表达式

格式化字符串

(* 字符串格式化,至少 1 个参数 *)
fmt_expr         = "fmt", "(", string, ",", var_get, { ",", var_get }, ")" ;
var_get          = ("read" | "take"), "(", [ arg_list ], ")"
                 | "@", ident ;                  (* '@ref' 等价 read(ref),不支持缺省体 *)

示例

message = fmt("{}-{}", @user, read(city)) ;
id = fmt("{}:{}", read(host), read(port)) ;

管道表达式

(* 管道:可省略 pipe 关键字 *)
pipe_expr        = ["pipe"], var_get, "|", pipe_fun, { "|", pipe_fun } ;

pipe_fun         = "nth",           "(", unsigned, ")"
                 | "get",           "(", ident,   ")"
                 | "base64_decode", "(", [ encode_type ], ")"
                 | "sxf_get",       "(", alnum*,  ")"
                 | "path",          "(", ("name"|"path"), ")"
                 | "url",           "(", ("domain"|"host"|"uri"|"path"|"params"), ")"
                 | "Time::to_ts_zone", "(", [ "-" ], unsigned, ",", ("ms"|"us"|"ss"|"s"), ")"
                 | "base64_encode" | "html_escape" | "html_unescape"
                 | "str_escape" | "json_escape" | "json_unescape"
                 | "Time::to_ts" | "Time::to_ts_ms" | "Time::to_ts_us"
                 | "to_json" | "to_str" | "skip_empty" | "ip4_to_int" ;

encode_type      = ident ;                     (* 例如: Utf8/Gbk/Imap/... *)

示例

# 使用 pipe 关键字
result = pipe read(data) | to_json | base64_encode ;

# 省略 pipe 关键字
result = read(data) | to_json | base64_encode ;

# 时间转换
ts = read(time) | Time::to_ts_zone(0, ms) ;

# URL 解析
host = read(url) | url(host) ;

对象聚合

(* 聚合到对象:object 内部为子赋值序列;分号可选但推荐 *)
map_expr         = "object", "{", map_item, { map_item }, "}" ;
map_item         = map_targets, "=", sub_acq, [ ";" ] ;
map_targets      = ident, { ",", ident }, [ ":", data_type ] ;
sub_acq          = take_expr | read_expr | value_expr | fun_call ;

示例

info : obj = object {
    name : chars = read(name) ;
    age : digit = read(age) ;
    city : chars = read(city) ;
} ;

数组聚合

(* 聚合到数组:从 VarGet 收集(支持 keys/option 通配) *)
collect_expr     = "collect", var_get ;

示例

# 收集多个字段
ports = collect read(keys:[sport, dport]) ;

# 使用通配符
metrics = collect read(keys:[cpu_*]) ;

模式匹配

(* 模式匹配:单源/双源两种形态,支持 in/!= 与缺省分支 *)
match_expr       = "match", match_source, "{", case1, { case1 }, [ default_case ], "}"
                 | "match", "(", var_get, ",", var_get, ")", "{", case2, { case2 }, [ default_case ], "}" ;

match_source     = var_get ;
case1            = cond1, "=>", calc, [ "," ], [ ";" ] ;
case2            = "(", cond1, ",", cond1, ")", "=>", calc, [ "," ], [ ";" ] ;
default_case     = "_", "=>", calc, [ "," ], [ ";" ] ;
calc             = read_expr | take_expr | value_expr | collect_expr ;

cond1            = "in", "(", value_expr, ",", value_expr, ")"
                 | "!", value_expr
                 | value_expr ;                 (* 省略运算符表示等于 *)

示例

# 单源匹配
level = match read(status) {
    in (digit(200), digit(299)) => chars(success) ;
    in (digit(400), digit(499)) => chars(error) ;
    _ => chars(other) ;
} ;

# 双源匹配
result = match (read(a), read(b)) {
    (digit(1), digit(2)) => chars(case1) ;
    _ => chars(default) ;
} ;

SQL 表达式

sql_expr        = "select", sql_body, "where", sql_cond, ";" ;
sql_body        = sql_safe_body ;              (* 源码对白名单化:仅 [A-Za-z0-9_.] 与 '*' *)
sql_cond        = cond_expr ;

cond_expr       = cmp, { ("and" | "or"), cmp }
                 | "not", cond_expr
                 | "(", cond_expr, ")" ;

cmp             = ident, sql_op, cond_rhs ;
sql_op          = sql_cmp_op ;                 (* 见 wp_parser::sql_symbol::symbol_sql_cmp *)
cond_rhs        = read_expr | take_expr | fun_call | sql_literal ;
sql_literal     = number | string ;

严格模式说明

  • 严格模式(默认开启):当主体 <cols from table> 不满足白名单规则时,解析报错
  • 兼容模式:设置环境变量 OML_SQL_STRICT=0,若主体非法则回退原文(不推荐)
  • 白名单规则
    • 列清单:* 或由 [A-Za-z0-9_.]+ 组成的列名(允许点号作限定)
    • 表名:[A-Za-z0-9_.]+(单表,不支持 join/子查询)
    • from 大小写不敏感;多余空白允许

示例

# 正确示例
name, email = select name, email from users where id = read(user_id) ;

# 使用字符串常量
data = select * from table where type = 'admin' ;

# IP 范围查询
zone = select zone from ip_geo
    where ip_start_int <= ip4_int(read(src_ip))
      and ip_end_int >= ip4_int(read(src_ip)) ;

错误示例(严格模式)

# ❌ 表名含非法字符
data = select a, b from table-1 where ... ;

# ❌ 列清单含函数
data = select sum(a) from t where ... ;

# ❌ 不支持 join
data = select a from t1 join t2 ... ;

隐私段

注:引擎默认不启用运行期隐私/脱敏处理;以下为 DSL 语法能力说明,供需要的场景参考。

privacy_items   = privacy_item, { privacy_item } ;
privacy_item    = ident, ":", privacy_type ;

privacy_type    = "privacy_ip"
                 | "privacy_specify_ip"
                 | "privacy_id_card"
                 | "privacy_mobile"
                 | "privacy_mail"
                 | "privacy_domain"
                 | "privacy_specify_name"
                 | "privacy_specify_domain"
                 | "privacy_specify_address"
                 | "privacy_specify_company"
                 | "privacy_keymsg" ;

示例

name : privacy_example
---
field = read() ;
---
src_ip : privacy_ip
pos_sn : privacy_keymsg

词法与约定

path            = ident, { ("/" | "."), ident } ;
wild_path       = path | path, "*" ;          (* 允许通配 *)
wild_key        = ident, { ident | "*" } ;    (* 允许 '*' 出现在键名中 *)
type_ident      = ident ;                      (* 如 auto/ip/chars/digit/float/time/bool/obj/array *)
ident           = letter, { letter | digit | "_" } ;
key             = ident ;

string          = "\"", { any-but-quote }, "\""
                | "'", { any-but-quote }, "'" ;

literal         = string | number | ip | bool | datetime | ... ;
json_path       = "/" , ... ;                 (* 如 /a/b/[0]/1 *)
simple          = ident | number | string ;
unsigned        = digit, { digit } ;
eol             = { " " | "\t" | "\r" | "\n" } ;

letter          = "A" | ... | "Z" | "a" | ... | "z" ;
digit           = "0" | ... | "9" ;
alnum           = letter | digit ;

数据类型

OML 支持以下数据类型:

类型说明示例
auto自动推断(默认)field = read() ;
chars字符串name : chars = read() ;
digit整数count : digit = read() ;
float浮点数ratio : float = read() ;
ipIP 地址addr : ip = read() ;
time时间timestamp : time = Now::time() ;
bool布尔值flag : bool = read() ;
obj对象info : obj = object { ... } ;
array数组items : array = collect read(...) ;

完整示例

name : csv_example
rule : /csv/data
---
# 基本取值与缺省
version : chars = Now::time() ;
pos_sn = read() { _ : chars(FALLBACK) } ;

# object 聚合
values : obj = object {
    cpu_free, memory_free : digit = read() ;
} ;

# collect 数组聚合 + 管道
ports : array = collect read(keys:[sport, dport]) ;
ports_json = pipe read(ports) | to_json ;
first_port = pipe read(ports) | nth(0) ;

# 省略 pipe 关键字的管道写法
url_host = read(http_url) | url(host) ;

# match
quarter : chars = match read(month) {
    in (digit(1), digit(3))   => chars(Q1) ;
    in (digit(4), digit(6))   => chars(Q2) ;
    in (digit(7), digit(9))   => chars(Q3) ;
    in (digit(10), digit(12)) => chars(Q4) ;
    _ => chars(QX) ;
} ;

# 双源 match
X : chars = match (read(city1), read(city2)) {
    (ip(127.0.0.1), ip(127.0.0.100)) => chars(bj) ;
    _ => chars(sz) ;
} ;

# SQL(where 中可混用 read/take/Now::time/常量)
name, pinying = select name, pinying from example where pinying = read(py) ;
_, _ = select name, pinying from example where pinying = 'xiaolongnu' ;

---
# 隐私配置(按键绑定处理器枚举)
src_ip : privacy_ip
pos_sn : privacy_keymsg

管道函数速查

函数语法说明
base64_encodebase64_encodeBase64 编码
base64_decodebase64_decode / base64_decode(编码)Base64 解码
html_escapehtml_escapeHTML 转义
html_unescapehtml_unescapeHTML 反转义
json_escapejson_escapeJSON 转义
json_unescapejson_unescapeJSON 反转义
str_escapestr_escape字符串转义
Time::to_tsTime::to_ts时间转时间戳(秒,UTC+8)
Time::to_ts_msTime::to_ts_ms时间转时间戳(毫秒,UTC+8)
Time::to_ts_usTime::to_ts_us时间转时间戳(微秒,UTC+8)
Time::to_ts_zoneTime::to_ts_zone(时区,单位)时间转指定时区时间戳
nthnth(索引)获取数组元素
getget(字段名)获取对象字段
pathpath(name|path)提取文件路径部分
urlurl(domain|host|uri|path|params)提取 URL 部分
sxf_getsxf_get(字段名)提取特殊格式字段
to_strto_str转换为字符串
to_jsonto_json转换为 JSON
ip4_to_intip4_to_intIPv4 转整数
skip_emptyskip_empty跳过空值

语法要点

必需元素

  1. 配置名称name : <名称>
  2. 分隔符---
  3. 分号:每个顶层条目必须以 ; 结束

可选元素

  1. 类型声明field : <type> = ...(默认为 auto
  2. rule 字段rule : <规则路径>
  3. 默认值read() { _ : <默认值> }
  4. pipe 关键字pipe read() | func 可简写为 read() | func

注释

# 单行注释(使用 # 或 //)
// 也支持 C++ 风格注释

目标通配

* = take() ;           # 取走所有字段
alert* = take() ;      # 取走所有以 alert 开头的字段
*_log = take() ;       # 取走所有以 _log 结尾的字段

读取语义

  • read:非破坏性(可反复读取,不从 src 移除)
  • take:破坏性(取走后从 src 移除,后续不可再取)

下一步

OML Pipe Functions 函数索引

本文档列出了 WP-Motor OML 语言中所有可用的 pipe function(管道函数)。

字段访问函数 (Field Accessors)

函数语法说明文档
taketake(field_name)从输入数据中提取指定字段-
getget(key)从嵌套结构中获取指定键的值-
nthnth(index)从数组中获取指定索引的元素-

字符串匹配函数 (String Matching)

函数语法说明文档
starts_withstarts_with('prefix')检查字符串是否以指定前缀开始,否则转为 ignore📖 详细文档

值转换函数 (Value Transformation)

函数语法说明文档
map_tomap_to(value)将非 ignore 字段映射到指定值(支持多种类型)📖 详细文档
to_strto_str将字段值转换为字符串-
to_jsonto_json将字段值转换为 JSON 字符串-

编码/解码函数 (Encoding/Decoding)

函数语法说明文档
base64_encodebase64_encodeBase64 编码字符串-
base64_decodebase64_decode(encoding)Base64 解码字符串(可指定编码)-
html_escapehtml_escapeHTML 转义字符串-
html_unescapehtml_unescapeHTML 反转义字符串-
json_escapejson_escapeJSON 转义字符串-
json_unescapejson_unescapeJSON 反转义字符串-
str_escapestr_escape通用字符串转义-

时间转换函数 (Time Conversion)

函数语法说明文档
Time::to_tsTime::to_ts将时间转换为秒级时间戳-
Time::to_ts_msTime::to_ts_ms将时间转换为毫秒级时间戳-
Time::to_ts_usTime::to_ts_us将时间转换为微秒级时间戳-
Time::to_ts_zoneTime::to_ts_zone(zone, unit)将时间转换为指定时区的时间戳-

网络函数 (Network)

函数语法说明文档
ip4_to_intip4_to_int将 IPv4 地址转换为整数-

URL/路径解析函数 (URL/Path Parsing)

函数语法说明文档
pathpath(type)从路径字符串中提取指定部分-
urlurl(type)从 URL 字符串中提取指定部分-

控制流函数 (Control Flow)

函数语法说明文档
skip_emptyskip_empty跳过空值字段-

函数分类总览

按功能分类

1. 数据提取函数

从输入数据或嵌套结构中提取字段。

  • take(field): 提取顶层字段
  • get(key): 提取嵌套字段
  • nth(index): 提取数组元素

2. 条件过滤函数

检查条件,不满足时转换为 ignore。

  • starts_with(prefix): 前缀匹配

3. 值映射函数

修改字段值或类型。

  • map_to(value): 通用值映射
  • to_str: 转字符串
  • to_json: 转 JSON

4. 编码转换函数

在不同编码格式之间转换。

  • Base64: base64_encode, base64_decode
  • HTML: html_escape, html_unescape
  • JSON: json_escape, json_unescape

5. 时间处理函数

处理时间相关的转换。

  • 时间戳转换: Time::to_ts*
  • 时区转换: Time::to_ts_zone

6. 网络处理函数

处理网络相关数据。

  • IP 地址: ip4_to_int
  • URL 解析: url(type)
  • 路径解析: path(type)

使用示例

基本管道

name : basic_pipeline
---
# 1. 提取字段
result = pipe take(message)
    # 2. 过滤条件
    | starts_with('ERROR')
    # 3. 值映射
    | map_to('error_type');

复杂转换

name : complex_transformation
---
# 提取并转换时间
timestamp_ms = pipe take(time_str)
    | Time::to_ts_ms;

# Base64 编码
encoded = pipe take(message)
    | base64_encode;

# URL 解析
host = pipe take(url)
    | url(host);

条件分类

name : conditional_classification
---
# 根据 URL 前缀分类安全级别
high_security = pipe take(url)
    | starts_with('https://')
    | map_to(3);

low_security = pipe take(url)
    | starts_with('http://')
    | map_to(1);

数据清洗

name : data_cleaning
---
# 提取并清理数据
clean_message = pipe take(raw_message)
    | html_unescape
    | json_unescape
    | skip_empty;

性能参考

函数类型典型性能说明
字段访问< 100ns基于哈希表查找
字符串匹配< 1μs简单前缀比较
值映射< 100ns直接值替换
Base64 编码1-10μs取决于字符串长度
时间转换1-5μs时间解析和转换
URL 解析1-10μsURL 结构解析

最佳实践

1. 合理使用管道链

# ✅ 推荐:按逻辑顺序组织管道
result = pipe take(field)
    | starts_with('prefix')  # 先过滤
    | map_to('value');      # 再映射

# ⚠️ 避免:不必要的长管道
result = pipe take(field)
    | to_str
    | to_json
    | to_str  # 冗余操作

2. 利用 ignore 传播

# ✅ 推荐:利用 ignore 跳过后续处理
secure_flag = pipe take(url)
    | starts_with('https://')  # 失败返回 ignore
    | map_to(true);           # ignore 会跳过此步

# 这样可以安全地处理条件逻辑

3. 选择合适的函数

# ✅ 推荐:使用专用函数
result = pipe take(field) | map_to(123);  # 自动推断为整数

# ⚠️ 不推荐:手动转换
result = pipe take(field) | to_str | map_to('123');  # 额外开销

4. 避免重复提取

# ✅ 推荐:一次提取,多次使用
url_field = pipe take(url);
host = pipe take(url) | url(host);
path = pipe take(url) | url(path);

# ⚠️ 避免:每次都提取同一字段(性能影响小但不够清晰)

函数对比

starts_with vs 正则表达式

特性starts_with正则表达式
性能极快较慢
功能前缀匹配复杂模式
使用难度简单需要学习
失败行为转为 ignore-

map_to vs to_str

特性map_toto_str
功能值替换类型转换
类型支持多种仅字符串
ignore 保留
用途条件赋值类型转换

相关文档

版本历史

  • 1.13.4 (2026-02-03)

    • 新增 starts_with 函数
    • 新增 map_to 函数,支持多种类型自动推断
    • 完善文档体系
  • 1.13.3 (2026-02-03)

    • 修复编译错误
  • 1.13.2 (2026-02-03)

    • 完善 pipe 函数支持

提示: OML pipe 函数设计用于数据转换和映射。合理使用 ignore 机制可以实现灵活的条件逻辑。

OML Match 表达式函数匹配

本文档介绍 OML match 表达式中的函数匹配功能。

概述

从版本 1.13.4 开始,OML 的 match 表达式支持使用函数进行模式匹配,提供比简单值比较更灵活的匹配方式。

基本语法

field_name = match read(source_field) {
    function_name(arguments) => result_value,
    _ => default_value,
};

与 Pipe Function 的区别

特性Match 函数Pipe 函数
用途多分支条件判断二元过滤(保留/忽略)
返回根据条件返回不同的值匹配返回原值,不匹配返回 ignore
场景分类、路由、决策过滤、清洗

示例对比:

# Match: 根据前缀分类到不同结果
EventType = match read(log) {
    starts_with('[ERROR]') => chars(error),
    starts_with('[WARN]') => chars(warning),
    starts_with('[INFO]') => chars(info),
    _ => chars(other),
};

# Pipe: 过滤出 ERROR 日志,其他变为 ignore
ErrorLog = pipe take(log) | starts_with('[ERROR]');

支持的函数

字符串匹配函数

starts_with(prefix)

检查字段值是否以指定前缀开始。

语法: starts_with('prefix')

参数:

  • prefix: 字符串,要匹配的前缀(必须使用引号)

匹配规则:

  • 字段值以指定前缀开始 → 匹配成功
  • 字段值不以指定前缀开始 → 匹配失败
  • 字段不是字符串类型 → 匹配失败
  • 大小写敏感

示例:

EventType = match read(log_line) {
    starts_with('[ERROR]') => chars(error),
    starts_with('[WARN]') => chars(warning),
    _ => chars(info),
};

ends_with(suffix)

检查字段值是否以指定后缀结束。

语法: ends_with('suffix')

参数:

  • suffix: 字符串,要匹配的后缀(必须使用引号)

匹配规则:

  • 字段值以指定后缀结束 → 匹配成功
  • 字段值不以指定后缀结束 → 匹配失败
  • 字段不是字符串类型 → 匹配失败
  • 大小写敏感

示例:

FileType = match read(filename) {
    ends_with('.json') => chars(json),
    ends_with('.xml') => chars(xml),
    ends_with('.log') => chars(log),
    _ => chars(unknown),
};

contains(substring)

检查字段值是否包含指定子串。

语法: contains('substring')

参数:

  • substring: 字符串,要匹配的子串(必须使用引号)

匹配规则:

  • 字段值包含指定子串 → 匹配成功
  • 字段值不包含指定子串 → 匹配失败
  • 字段不是字符串类型 → 匹配失败
  • 大小写敏感

示例:

ErrorType = match read(message) {
    contains('exception') => chars(exception),
    contains('timeout') => chars(timeout),
    contains('failed') => chars(failure),
    _ => chars(normal),
};

regex_match(pattern)

使用正则表达式匹配字段值。

语法: regex_match('pattern')

参数:

  • pattern: 字符串,正则表达式模式(必须使用引号)

匹配规则:

  • 字段值匹配正则表达式 → 匹配成功
  • 字段值不匹配正则表达式 → 匹配失败
  • 正则表达式语法错误 → 匹配失败并记录警告
  • 字段不是字符串类型 → 匹配失败

注意: 使用标准 Rust regex 语法

示例:

EventPattern = match read(log_message) {
    regex_match('^\[\d{4}-\d{2}-\d{2}') => chars(timestamped),
    regex_match('^ERROR:.*timeout') => chars(error_timeout),
    regex_match('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') => chars(ip_address),
    _ => chars(unmatched),
};

is_empty()

检查字段值是否为空字符串。

语法: is_empty()

参数: 无

匹配规则:

  • 字段值为空字符串 → 匹配成功
  • 字段值非空 → 匹配失败
  • 字段不是字符串类型 → 匹配失败

示例:

Status = match read(field_value) {
    is_empty() => chars(missing),
    _ => chars(present),
};

iequals(value)

忽略大小写比较字段值。

语法: iequals('value')

参数:

  • value: 字符串,要比较的值(必须使用引号)

匹配规则:

  • 字段值与参数值在忽略大小写的情况下相等 → 匹配成功
  • 字段值与参数值不相等 → 匹配失败
  • 字段不是字符串类型 → 匹配失败

示例:

NormalizedStatus = match read(status) {
    iequals('success') => chars(ok),
    iequals('error') => chars(fail),
    iequals('warning') => chars(warn),
    _ => chars(unknown),
};

数值比较函数

gt(value)

检查字段值是否大于指定值。

语法: gt(100) (数值参数不需要引号)

参数:

  • value: 数值,要比较的阈值

匹配规则:

  • 字段值 > 参数值 → 匹配成功
  • 字段值 ≤ 参数值 → 匹配失败
  • 字段不是数值类型 → 匹配失败
  • 支持整数 (digit) 和浮点数 (float)

示例:

Level = match read(count) {
    gt(1000) => chars(critical),
    gt(500) => chars(high),
    gt(100) => chars(medium),
    _ => chars(low),
};

lt(value)

检查字段值是否小于指定值。

语法: lt(60) (数值参数不需要引号)

参数:

  • value: 数值,要比较的阈值

匹配规则:

  • 字段值 < 参数值 → 匹配成功
  • 字段值 ≥ 参数值 → 匹配失败
  • 字段不是数值类型 → 匹配失败
  • 支持整数和浮点数

示例:

Grade = match read(score) {
    lt(60) => chars(fail),
    lt(70) => chars(pass),
    lt(85) => chars(good),
    _ => chars(excellent),
};

eq(value)

检查字段值是否等于指定数值。

语法: eq(5) (数值参数不需要引号)

参数:

  • value: 数值,要比较的目标值

匹配规则:

  • 字段值等于参数值 → 匹配成功(浮点数比较容差 1e-10)
  • 字段值不等于参数值 → 匹配失败
  • 字段不是数值类型 → 匹配失败
  • 支持整数和浮点数

示例:

Status = match read(level) {
    eq(0) => chars(disabled),
    eq(5) => chars(max_level),
    eq(1) => chars(minimum),
    _ => chars(normal),
};

in_range(min, max)

检查字段值是否在指定范围内。

语法: in_range(20, 30) (数值参数不需要引号)

参数:

  • min: 数值,范围最小值
  • max: 数值,范围最大值

匹配规则:

  • min ≤ 字段值 ≤ max → 匹配成功
  • 字段值 < min 或 字段值 > max → 匹配失败
  • 字段不是数值类型 → 匹配失败
  • 支持整数和浮点数
  • 区间为闭区间 [min, max]

示例:

TempZone = match read(temperature) {
    lt(0) => chars(freezing),
    in_range(0, 10) => chars(cold),
    in_range(10, 20) => chars(cool),
    in_range(20, 30) => chars(comfortable),
    gt(30) => chars(warm),
    _ => chars(unknown),
};

使用示例

示例 1: 日志级别分类

name : classify_log_event
---
EventType = match read(Content) {
    starts_with('[ERROR]') => chars(error),
    starts_with('[WARN]') => chars(warning),
    starts_with('[INFO]') => chars(info),
    _ => chars(debug),
};

示例 2: 文件类型识别

name : file_type_detection
---
FileType = match read(filename) {
    ends_with('.json') => chars(json),
    ends_with('.xml') => chars(xml),
    ends_with('.log') => chars(log),
    ends_with('.txt') => chars(text),
    _ => chars(unknown),
};

示例 3: 错误类型检测

name : error_type_detection
---
ErrorType = match read(message) {
    contains('exception') => chars(exception),
    contains('timeout') => chars(timeout),
    contains('failed') => chars(failure),
    _ => chars(normal),
};

示例 4: 分数等级映射

name : score_grade_mapping
---
Grade = match read(score) {
    gt(90) => chars(A),
    in_range(80, 90) => chars(B),
    in_range(70, 80) => chars(C),
    in_range(60, 70) => chars(D),
    _ => chars(F),
};

示例 5: 温度区间分类

name : temperature_classification
---
TempZone = match read(temperature) {
    lt(0) => chars(freezing),
    in_range(0, 10) => chars(cold),
    in_range(10, 20) => chars(cool),
    in_range(20, 30) => chars(comfortable),
    in_range(30, 40) => chars(warm),
    gt(40) => chars(hot),
    _ => chars(unknown),
};

示例 6: 混合使用多种函数

name : log_classification
---
EventType = match read(log_line) {
    starts_with('[ERROR]') => chars(error),
    starts_with('[WARN]') => chars(warning),
    contains('exception') => chars(exception),
    ends_with('failed') => chars(failure),
    is_empty() => chars(empty),
    _ => chars(other),
};

示例 7: 正则表达式匹配

name : regex_pattern_match
---
EventPattern = match read(log_message) {
    regex_match('^\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\]') => chars(timestamped),
    regex_match('^ERROR:.*timeout') => chars(error_timeout),
    regex_match('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}') => chars(ip_address),
    _ => chars(unmatched),
};

示例 8: 大小写不敏感状态匹配

name : case_insensitive_status
---
NormalizedStatus = match read(status) {
    iequals('success') => chars(ok),
    iequals('error') => chars(fail),
    iequals('warning') => chars(warn),
    iequals('pending') => chars(wait),
    _ => chars(unknown),
};

注意事项

1. 参数引号规则

# ✅ 字符串参数必须加引号
starts_with('prefix')
iequals('value')

# ✅ 数值参数不需要引号
gt(100)
eq(5)
in_range(20, 30)

# ❌ 错误示例
starts_with(prefix)   # 缺少引号
gt('100')             # 不应该加引号

2. 大小写敏感性

# 大多数字符串函数都是大小写敏感的
starts_with('ERROR')  # 不会匹配 'error:'

# 使用 iequals 进行大小写不敏感匹配
iequals('success')    # 匹配 'SUCCESS', 'Success', 'success'

3. 匹配顺序

# match 按从上到下的顺序匹配,第一个匹配成功的分支会被执行
Grade = match read(score) {
    gt(90) => chars(A),        # 先检查 > 90
    gt(80) => chars(B),        # 再检查 > 80
    gt(70) => chars(C),        # 然后检查 > 70
    _ => chars(F),
};

# 如果 score = 95,只会匹配到第一个分支 (A)

4. 数值类型支持

# 支持多种数值类型
- digit(100)     # 整数
- float(3.14)    # 浮点数
- chars("123")   # 可解析的字符串

# 所有这些都可以用于数值比较函数
count = digit(150);
Level = match read(count) {
    gt(100) => chars(high),  # 匹配成功
    _ => chars(low),
};

5. 范围区间

# in_range 使用闭区间 [min, max]
in_range(10, 20)   # 包含 10 和 20

# 示例:score = 20 会匹配成功
Grade = match read(score) {
    in_range(10, 20) => chars(pass),  # 匹配!
    _ => chars(fail),
};

性能参考

函数类型典型性能说明
前缀/后缀匹配< 1μs简单字符串比较
子串匹配1-5μs取决于字符串长度
正则表达式5-50μs取决于模式复杂度
数值比较< 100ns直接数值比较
大小写转换1-2μs需要字符串复制

函数对比

Match 函数 vs Pipe 函数

特性Match starts_withPipe starts_with
返回值根据条件返回不同值匹配返回原值,不匹配返回 ignore
分支数支持多个分支仅二元(匹配/不匹配)
用途分类、决策树过滤、数据清洗
代码长度多条件时更简洁简单过滤时更简洁

示例对比:

# Match: 多分支分类
EventType = match read(log) {
    starts_with('[ERROR]') => chars(error),
    starts_with('[WARN]') => chars(warning),
    starts_with('[INFO]') => chars(info),
    _ => chars(debug),
};

# Pipe: 简单过滤
ErrorLog = pipe take(log) | starts_with('[ERROR]');
# 不匹配的变成 ignore,匹配的保留原值

数值函数对比

场景推荐函数示例
阈值判断gt / ltgt(100)
精确匹配eqeq(5)
区间判断in_rangein_range(10, 20)
分段分类组合使用见温度分类示例

最佳实践

1. 优先使用简单函数

# ✅ 推荐:使用简单的 starts_with
match read(url) {
    starts_with('https://') => chars(secure),
    _ => chars(insecure),
}

# ⚠️ 避免:不必要的正则表达式
match read(url) {
    regex_match('^https://') => chars(secure),  # 性能更差
    _ => chars(insecure),
}

2. 合理组织匹配顺序

# ✅ 推荐:从具体到一般
match read(log) {
    starts_with('[ERROR]') => chars(error),     # 最具体
    starts_with('[WARN]') => chars(warning),
    contains('exception') => chars(exception),   # 较宽泛
    _ => chars(other),                          # 默认
}

3. 利用数值范围

# ✅ 推荐:使用 in_range 简化多个条件
Grade = match read(score) {
    gt(90) => chars(A),
    in_range(80, 90) => chars(B),
    in_range(70, 80) => chars(C),
    _ => chars(D),
};

# ⚠️ 避免:重复的比较
Grade = match read(score) {
    gt(90) => chars(A),
    gt(80) => chars(B),  # 实际上是 80-90
    gt(70) => chars(C),  # 实际上是 70-80
    _ => chars(D),
};

4. 使用 iequals 处理用户输入

# ✅ 推荐:使用 iequals 处理大小写不确定的输入
Status = match read(user_input) {
    iequals('yes') => chars(confirmed),
    iequals('no') => chars(rejected),
    _ => chars(invalid),
};

与传统匹配的对比

传统值匹配

Status = match read(status_code) {
    digit(200) => chars(success),
    digit(404) => chars(not_found),
    digit(500) => chars(error),
    _ => chars(unknown),
};

特点: 精确匹配固定值

函数匹配

EventType = match read(log_line) {
    starts_with('ERROR:') => chars(error),
    starts_with('WARN:') => chars(warning),
    starts_with('INFO:') => chars(info),
    _ => chars(debug),
};

特点: 基于模式或条件匹配

相关文档

版本历史

  • 1.13.4 (2026-02-04)
    • 新增 match 表达式函数匹配支持
    • 字符串匹配:starts_with, ends_with, contains, regex_match, is_empty, iequals
    • 数值比较:gt, lt, eq, in_range
    • 完善函数文档

提示: Match 函数用于多分支条件判断,Pipe 函数用于二元过滤。根据实际场景选择合适的函数类型可以让代码更简洁清晰。

starts_with 函数使用指南

简介

starts_with 是一个 OML pipe 函数,用于检查字符串字段是否以指定的前缀开始。

语法

field_name = pipe take(source_field) | starts_with('prefix');

参数

  • prefix: 字符串类型,要检查的前缀(需使用引号)

行为

  • 如果字段值以指定前缀开始,字段保持不变并继续传递
  • 如果字段值不以指定前缀开始,字段转换为 ignore 类型
  • 如果字段不是字符串类型,字段转换为 ignore 类型
  • 前缀匹配是大小写敏感的

使用场景

场景 1: 过滤 HTTPS URL

name : filter_secure_urls
---
secure_url = pipe take(url) | starts_with('https://');

输入: url = "https://example.com" 输出: secure_url = "https://example.com" (Chars类型)

输入: url = "http://example.com" 输出: secure_url = (ignore)

场景 2: 提取 API 路径

name : extract_api_path
---
api_path = pipe take(request_path) | starts_with('/api/v1/');

输入: request_path = "/api/v1/users" 输出: api_path = "/api/v1/users" (Chars类型)

输入: request_path = "/admin/users" 输出: api_path = (ignore)

场景 3: 分类日志级别

name : classify_error_logs
---
error_message = pipe take(log_message) | starts_with('ERROR');
warning_message = pipe take(log_message) | starts_with('WARN');

输入: log_message = "ERROR: Connection failed" 输出:

  • error_message = "ERROR: Connection failed" (Chars类型)
  • warning_message = (ignore)

场景 4: 与 map_to 组合使用

name : classify_secure_requests
---
# 如果是 HTTPS,标记为安全
is_secure = pipe take(url) | starts_with('https://') | map_to(true);

# 如果是 HTTPS,设置安全级别
security_level = pipe take(url) | starts_with('https://') | map_to(3);

输入: url = "https://api.example.com" 输出:

  • is_secure = true (Bool类型)
  • security_level = 3 (Digit类型)

输入: url = "http://api.example.com" 输出:

  • is_secure = (ignore)
  • security_level = (ignore)

场景 5: 多条件过滤

name : extract_specific_prefix
---
# 提取特定前缀的字段
https_url = pipe take(url) | starts_with('https://');
ftp_url = pipe take(url) | starts_with('ftp://');
websocket_url = pipe take(url) | starts_with('wss://');

根据不同的协议前缀,将 URL 分类到不同的字段中。

场景 6: 路径规范化

name : normalize_paths
---
# 只接受绝对路径
absolute_path = pipe take(file_path) | starts_with('/');

# 只接受相对路径
relative_path = pipe take(file_path) | starts_with('./');

与其他函数的对比

函数检查位置性能用途
starts_with(prefix)字符串开头极快前缀匹配
regex_match(pattern)任意位置较慢复杂模式匹配
to_str-类型转换

常见用例

1. URL 协议过滤

name : url_protocol_filter
---
https_only = pipe take(url) | starts_with('https://');

2. 路径前缀提取

name : api_path_extract
---
v1_api = pipe take(path) | starts_with('/api/v1/');
v2_api = pipe take(path) | starts_with('/api/v2/');

3. 日志级别分类

name : log_level_classify
---
errors = pipe take(message) | starts_with('[ERROR]');
warnings = pipe take(message) | starts_with('[WARN]');
info = pipe take(message) | starts_with('[INFO]');

4. 文件扩展名检查(配合其他字段)

name : file_type_check
---
# 注意:这个示例假设 filename 已经被规范化
json_file = pipe take(filename) | starts_with('data_') | map_to('json');

实现细节

  • 定义位置: crates/wp-oml/src/language/syntax/functions/pipe/other.rs
  • 实现位置: crates/wp-oml/src/core/evaluator/transform/pipe/other.rs
  • 解析器: crates/wp-oml/src/parser/pipe_prm.rs
  • 测试: crates/wp-oml/src/core/evaluator/transform/pipe/other.rs (tests 模块)

注意事项

  1. 字符串必须加引号: starts_with('https://') 而非 starts_with(https://)
  2. 大小写敏感: starts_with('HTTP') 不会匹配 http://example.com
  3. ignore 字段传播: 转换为 ignore 的字段在后续管道函数中会保持 ignore 状态
  4. 与 map_to 配合: 常见模式是先用 starts_with 过滤,再用 map_to 赋值

性能特性

  • O(n) 时间复杂度: n 为前缀长度
  • 零拷贝: 不修改原始字符串
  • 短路优化: 发现不匹配立即返回

相关函数

  • map_to(value): 条件赋值,支持多种类型
  • skip_empty: 跳过空值
  • to_str: 转换为字符串
  • get(name): 从嵌套结构获取字段

map_to 函数使用指南

简介

map_to 是一个 OML pipe 函数,用于在字段存在(非 ignore)时将字段值映射到指定的值。支持多种类型:字符串、整数、浮点数、布尔值。

语法

field_name = pipe take(source_field) | map_to(value);

参数

map_to 支持多种类型的参数,解析器会自动推断类型:

  • 字符串: 使用引号包围,如 'text'"text"
  • 整数: 直接写数字,如 123-456
  • 浮点数: 带小数点的数字,如 3.14-2.5
  • 布尔值: truefalse

行为

  • 如果字段为非 ignore 类型,使用参数值替换字段值(并转换为相应类型)
  • 如果字段为 ignore 类型,保持不变
  • 自动进行类型转换

类型推断规则

输入推断类型结果字段类型
'text'"text"字符串Chars
123整数Digit
3.14浮点数Float
true / false布尔值Bool

使用场景

场景 1: 映射到字符串标记

name : classify_status
---
status_label = pipe take(http_code) | map_to('success');

输入: http_code = 200 输出: status_label = "success" (Chars类型)

场景 2: 映射到整数优先级

name : set_priority
---
priority = pipe take(log_level) | map_to(1);

输入: log_level = "ERROR" 输出: priority = 1 (Digit类型)

场景 3: 映射到浮点数阈值

name : set_threshold
---
threshold = pipe take(category) | map_to(0.95);

输入: category = "high" 输出: threshold = 0.95 (Float类型)

场景 4: 映射到布尔标记

name : mark_secure
---
is_secure = pipe take(protocol) | map_to(true);

输入: protocol = "https" 输出: is_secure = true (Bool类型)

场景 5: 与过滤函数组合

name : classify_secure_requests
---
security_level = pipe take(url) | starts_with('https://') | map_to(3);

输入: url = "https://api.example.com" 输出: security_level = 3 (Digit类型)

输入: url = "http://api.example.com" 输出: security_level = (ignore) (因为 starts_with 失败)

场景 6: 多级分类

name : multi_level_classification
---
# 检查 URL 是否为 HTTPS,如果是则设置安全级别为高优先级
priority = pipe take(url)
    | starts_with('https://')
    | map_to(10);

# 标记为已验证
verified = pipe take(url)
    | starts_with('https://')
    | map_to(true);

场景 7: 协议分类

name : protocol_classification
---
# HTTP 标记为 1
http_level = pipe take(protocol) | starts_with('http://') | map_to(1);

# HTTPS 标记为 3
https_level = pipe take(protocol) | starts_with('https://') | map_to(3);

# FTP 标记为 2
ftp_level = pipe take(protocol) | starts_with('ftp://') | map_to(2);

场景 8: 状态码分类

name : status_code_classification
---
# 2xx 成功
success_flag = pipe take(status_code) | digit_range(200, 299) | map_to('success');

# 4xx 客户端错误
client_error_flag = pipe take(status_code) | digit_range(400, 499) | map_to('client_error');

# 5xx 服务器错误
server_error_flag = pipe take(status_code) | digit_range(500, 599) | map_to('server_error');

与其他函数的对比

函数支持类型用途条件保留
map_to(value)字符串、整数、浮点数、布尔值通用映射,自动类型推断保留 ignore
to_str-类型转换为字符串不保留
to_json-转换为 JSON 字符串不保留

典型使用模式

1. 条件标记模式

name : conditional_marking
---
# 如果某个条件满足,标记为 true
is_valid = pipe take(field) | some_condition() | map_to(true);

2. 分类映射模式

name : classification_mapping
---
# 根据不同条件映射到不同的分类
category_a = pipe take(value) | condition_a() | map_to('A');
category_b = pipe take(value) | condition_b() | map_to('B');

3. 优先级赋值模式

name : priority_assignment
---
# 根据条件赋予不同优先级
high_priority = pipe take(level) | check_high() | map_to(10);
medium_priority = pipe take(level) | check_medium() | map_to(5);
low_priority = pipe take(level) | check_low() | map_to(1);

实现细节

  • 定义位置: crates/wp-oml/src/language/syntax/functions/pipe/other.rs
  • 实现位置: crates/wp-oml/src/core/evaluator/transform/pipe/other.rs
  • 解析器: crates/wp-oml/src/parser/pipe_prm.rs
  • 测试: crates/wp-oml/src/core/evaluator/transform/pipe/other.rs (tests 模块)

类型推断实现

解析器按以下顺序尝试解析参数:

  1. 布尔值: truefalse
  2. 数字:
    • 如果是整数形式(无小数部分),推断为 Digit
    • 如果有小数部分,推断为 Float
  3. 字符串: 单引号或双引号包围的文本

注意事项

  1. 字符串必须加引号: map_to(text) 会报错,应使用 map_to('text')
  2. 整数自动识别: map_to(100) 自动识别为整数,map_to(100.0) 识别为浮点数
  3. 布尔值不加引号: map_to(true) 而非 map_to('true')
  4. ignore 字段保持不变: 如果输入字段是 ignore,输出也是 ignore
  5. 类型转换: 无论输入字段的原始类型,输出字段类型由参数决定

性能特性

  • O(1) 时间复杂度: 简单的值替换操作
  • 类型安全: 编译时类型检查确保类型正确
  • 零开销: 直接值替换,无额外分配

调试技巧

1. 验证类型推断

name : debug_type_inference
---
# 测试不同类型
str_field = pipe take(input) | map_to('string');    # Chars
int_field = pipe take(input) | map_to(123);         # Digit
float_field = pipe take(input) | map_to(3.14);      # Float
bool_field = pipe take(input) | map_to(true);       # Bool

2. 检查 ignore 传播

name : debug_ignore_propagation
---
# 如果 starts_with 失败,result 应该是 ignore
result = pipe take(url) | starts_with('https://') | map_to('secure');

相关函数

  • starts_with(prefix): 检查字符串前缀,失败时返回 ignore
  • skip_empty: 跳过空值
  • to_str: 转换为字符串
  • to_json: 转换为 JSON 字符串
  • get(name): 从嵌套结构获取字段
  • nth(index): 从数组获取指定索引的元素

static 块:模型级常量与模板缓存

OML 允许在 --- 分隔线之后声明 static { ... } 区块,将“只初始化一次、在运行时复用”的结构写在这里。解析完成后,static 中的表达式会被执行一次,结果进入模型常量池;随后的所有数据转换都直接引用缓存对象,无需再生成临时字段。

适用场景

  • 事件模板、常量字典等纯字面量对象
  • 需要在多个转换步骤中复用的结构(例如 match 结果)

语法示例

name : /oml/apache_error_e1
rule : apache/error/e1
---
static {
    e1_template = object {
        id = chars(E1);
        tpl = chars("jk2_init() Found child <*> in scoreboard slot <*>")
    };
    e2_template = object {
        id = chars(E2);
        tpl = chars("workerEnv.init() ok <*>")
    };
}

Content = read(Content);

target_template = match Content {
    starts_with("jk2_init() Found child") => e1_template;
    starts_with("workerEnv.init() ok") => e2_template;
    _ => e1_template;
};

EventId = target_template | get(id);
EventTemplate = target_template | get(tpl);
  • static { ... } 中的赋值可使用任意合法表达式,但不得调用 read()/take() 等依赖输入数据的函数。
  • static 区块中直接写静态符号名即可引用缓存值,无需 read()

执行模型

  1. 解析阶段
    • 仅检查 static 语句和目标名称是否重复。
    • 生成 EvalExp AST 并登记符号名。
  2. 构建阶段
    • finalize_static_blocks 统一执行所有静态表达式,构建常量池 const_fields
    • 将 DSL 中的 StaticSymbol 占位符(如 match 结果、管道参数)重写为真实 DataField
  3. 运行阶段
    • 静态值来自常量池,不会再次执行 evaluator。

使用建议

  • 匹配/管道static 变量可出现在 match ... => symbolread(symbol)、管道起点等位置,解析器会自动识别。

限制

  • static 语句仅支持单目标赋值,不可批量定义多个字段。
  • 不允许在 static 中调用依赖输入记录的数据访问函数(read()/take() 等),否则编译期会报错。
  • 静态符号仅在定义所在模型内可见,不会跨模型共享。

连接器管理

本文档介绍 Warp Parse 系统中连接器(Connectors)的定义、结构和使用方法。

连接器概念

什么是连接器

连接器是数据源和数据输出的配置模板,定义了特定类型连接器的默认参数和行为。通过将连接器定义与实例配置分离,实现了配置的复用和统一管理。

连接器的作用

  1. 配置复用: 多个源可以引用同一个连接器
  2. 参数标准化: 统一同类数据源的配置规范
  3. 权限控制: 通过 allow_override 控制可覆盖的参数
  4. 版本管理: 便于连接器配置的版本控制

连接器定义结构

基础结构

# connectors/source.d/{connector_name}.toml
[[connectors]]
id = "unique_connector_id"
type = "connector_type"
allow_override = ["param1", "param2", "param3"]

[connectors.params]
param1 = "default_value1"
param2 = "default_value2"
param3 = "default_value3"

字段说明

id (必需)

  • 连接器的唯一标识符
  • 在源配置中通过 connect 字段引用
  • 命名规范:sources 连接器以 _src 结尾(如 file_src),sinks 连接器以 _sink 结尾(如 file_json_sink

type (必需)

  • 连接器类型,决定使用哪种数据源/输出实现
  • Sources 支持的类型:file, syslog, tcpkafka 暂未实现)
  • Sinks 支持的类型:file, syslog, tcp, blackholekafkaprometheus 暂未实现)

allow_override (可选)

  • 允许源/sink 配置覆盖的参数列表
  • 为空时表示不允许覆盖任何参数
  • 提供配置灵活性,同时保证安全性

params (必需)

  • 连接器的默认参数配置
  • allow_override 包含的参数可以在实例配置中覆盖

目录结构

connectors/
├── source.d/                  # 源连接器目录
│   ├── 00-file-default.toml   # 文件连接器
│   ├── 10-syslog-udp.toml     # UDP Syslog 连接器
│   ├── 11-syslog-tcp.toml     # TCP Syslog 连接器
│   ├── 12-tcp.toml            # TCP 连接器
│   └── 30-kafka.toml          # Kafka 连接器
└── sink.d/                    # 输出连接器目录
    ├── 00-blackhole-sink.toml # 黑洞连接器
    ├── 02-file-json.toml      # JSON 文件输出
    ├── 10-syslog-udp.toml     # UDP Syslog 输出
    ├── 11-syslog-tcp.toml     # TCP Syslog 输出
    ├── 12-tcp.toml            # TCP 输出
    ├── 30-kafka.toml          # Kafka 输出
    └── 30-prometheus.toml     # Prometheus 输出

连接器类型

Source 连接器

File 连接器

# connectors/source.d/00-file-default.toml
[[connectors]]
id = "file_src"
type = "file"
allow_override = ["base", "file", "encode"]

[connectors.params]
base = "data/in_dat"
file = "gen.dat"
encode = "text"

Kafka 连接器(暂未实现)

# connectors/source.d/30-kafka.toml
[[connectors]]
id = "kafka_src"
type = "kafka"
allow_override = ["topic", "group_id", "config"]

[connectors.params]
brokers = "localhost:9092"
topic = ["access_log"]
group_id = "wparse_default_group"

⚠️ Kafka 连接器当前暂未实现,请勿使用。

Syslog 连接器

# connectors/source.d/11-syslog-tcp.toml
[[connectors]]
id = "syslog_tcp_src"
type = "syslog"
allow_override = ["addr", "port", "protocol", "tcp_recv_bytes", "header_mode", "prefer_newline"]

[connectors.params]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"
header_mode = "strip"
tcp_recv_bytes = 256000

TCP 连接器

# connectors/source.d/12-tcp.toml
[[connectors]]
id = "tcp_src"
type = "tcp"
allow_override = ["addr", "port", "framing", "tcp_recv_bytes", "instances"]

[connectors.params]
addr = "0.0.0.0"
port = 9000
framing = "auto"
tcp_recv_bytes = 256000

Sink 连接器

File 连接器

# connectors/sink.d/02-file-json.toml
[[connectors]]
id = "file_json_sink"
type = "file"
allow_override = ["base", "file"]

[connectors.params]
fmt = "json"
base = "./data/out_dat"
file = "default.json"

Syslog 连接器

# connectors/sink.d/11-syslog-tcp.toml
[[connectors]]
id = "syslog_tcp_sink"
type = "syslog"
allow_override = ["addr", "port", "protocol", "app_name"]

[connectors.params]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"

TCP 连接器

# connectors/sink.d/12-tcp.toml
[[connectors]]
id = "tcp_sink"
type = "tcp"
allow_override = ["addr", "port", "framing"]

[connectors.params]
addr = "127.0.0.1"
port = 9000
framing = "line"

连接器最佳实践

1. 参数覆盖设计

# ✅ 好的设计:明确的覆盖权限
[[connectors]]
id = "file_main"
type = "file"
allow_override = ["base", "file", "encode"]

# ❌ 避免:过度开放覆盖权限
[[connectors]]
id = "file_too_open"
type = "file"
allow_override = ["*"]  # 不支持且不安全

2. 默认值设置

# ✅ 好的设计:合理的默认值
[[connectors]]
id = "syslog_secure"
type = "syslog"
allow_override = ["addr", "port", "protocol"]

[connectors.params]
addr = "127.0.0.1"    # 安全的默认地址
port = 1514           # 非特权端口
protocol = "tcp"      # 可靠的协议

相关文档

Sources 配置指南

本指南介绍如何配置和使用 Warp Parse 系统的各种数据源。

内容概览

快速开始

  1. 了解 源配置基础概念
  2. 根据你的数据源类型选择相应的配置指南
  3. 参考连接器管理文档了解连接器定义

支持的数据源类型

类型说明文档
file从本地文件读取数据文件源配置
kafka从 Kafka 消费消息Kafka 源配置
syslog接收 Syslog 协议数据 (UDP/TCP)Syslog 源配置
tcp通过 TCP 接收数据TCP 源配置

相关文档

Source 基础

本文档介绍 Warp Parse 系统中数据源配置的基础概念和用法。

概述

数据源(Source)是 Warp Parse 系统的数据输入端,负责从各种数据源接收数据并输入到处理流程中。

核心概念

1. 连接器(Connectors)

连接器定义了如何与特定类型的数据源建立连接和通信。系统内置了多种连接器类型:

  • File Connector: 从文件读取数据
  • Kafka Connector: 从 Kafka 消息队列消费数据
  • Syslog Connector: 接收 Syslog 协议数据
  • TCP Connector: 通过 TCP 套接字接收数据

2. 连接器定义

连接器定义存储在 connectors/source.d/ 目录下:

# connectors/source.d/kafka_src.toml
[[connectors]]
id = "kafka_src"
type = "kafka"
allow_override = ["topic", "group_id", "config"]

[connectors.params]
brokers = "localhost:9092"
topic = ["access_log"]
group_id = "wparse_default_group"

3. 源配置结构

# wpsrc.toml
[[sources]]
key = "main_source"
enable = true
connect = "kafka_src"
tags = ["env:production"]

[[sources.params]]
topic = "events"
# 或使用 params_override (别名)

配置步骤

  1. 定义连接器: 在 connectors/source.d/ 目录下创建连接器配置文件
  2. 配置源: 在源配置文件中引用连接器并指定参数
  3. 验证配置: 使用 CLI 工具验证配置正确性
  4. 启动系统: 启动 Warp Parse 系统开始接收数据

配置字段说明

通用字段

字段类型必需说明
keyString源的唯一标识符
enableBoolean是否启用该源(默认 true)
connectString引用的连接器 ID
tagsArray源的标签列表
paramsTable连接器参数(可覆盖连接器默认值)

参数覆盖规则

  • 只有在连接器的 allow_override 列表中的参数才能被覆盖
  • 未在列表中的参数使用连接器定义中的默认值

配置示例

基础文件源

# wpsrc.toml
[[sources]]
key = "file_access_log"
enable = true
connect = "file_src"

[sources.params]
base = "/var/log/nginx"
file = "access.log"

Kafka 源配置

# wpsrc.toml
[[sources]]
key = "kafka_access_logs"
enable = true
connect = "kafka_src"
tags = ["env:production", "type:access_log"]

[sources.params]
topic = "nginx_access_log"

Syslog 源配置

# wpsrc.toml
[[sources]]
key = "syslog_tcp_1"
enable = true
connect = "syslog_tcp_src"
tags = ["protocol:tcp", "env:production"]

[sources.params]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"

相关文档

File Source

本文档详细介绍如何配置和使用 Warp Parse 系统的文件数据源。

概述

文件源用于从本地文件系统读取数据,支持多种编码格式和灵活的路径配置。

连接器定义

基础文件连接器

# connectors/source.d/00-file-default.toml
[[connectors]]
id = "file_src"
type = "file"
allow_override = ["base", "file", "encode"]

[connectors.params]
base = "data/in_dat"
file = "gen.dat"
encode = "text"

支持的参数

路径配置

base + file 组合(推荐)

[[sources]]
key = "file_composed"
connect = "file_src"

[[sources.params]]
base = "/var/log"
file = "access.log"

instances(多实例并行读取)

[[sources.params]]
instances = 4   # 1-32,默认为 1

instances > 1 时,文件会被按行边界分割为多个范围,由多个实例并行读取。

多实例命名规则:当 instances > 1 时,每个实例的 key 会自动添加后缀,如 file_src-1file_src-2 等。

编码格式

text 编码(默认)

[[sources.params]]
encode = "text"

配置示例

基础文件读取

# wpsrc.toml
[[sources]]
enable = true
key = "access_log"
connect = "file_src"

[[sources.params]]
base = "/var/log/nginx"
file = "access.log"

多文件源配置

# wpsrc.toml
[[sources]]
enable = true
key = "nginx_access"
connect = "file_src"

[[sources.params]]
base = "/var/log/nginx"
file = "access.log"

[[sources]]
enable = true
key = "nginx_error"
connect = "file_src"

[[sources.params]]
base = "/var/log/nginx"
file = "error.log"

不同编码格式

# Base64 编码的文件
[[sources]]
key = "base64_data"
connect = "file_src"

[[sources.params]]
base = "./data"
file = "encoded.b64"
encode = "base64"

# 十六进制编码的文件
[[sources]]
key = "hex_data"
connect = "file_src"

[[sources.params]]
base = "./data"
file = "encoded.hex"
encode = "hex"

数据处理特性

1. 逐行读取

文件源采用逐行读取模式,每行作为独立的数据包处理。

2. 编码处理

  • text: 直接读取文本内容

示例:

{
  "data": "原始日志内容",
  "tags": {
    "access_source": "/var/log/nginx/access.log",
    "env": "production"
  }
}

相关文档

Kafka 源配置

本文档详细介绍如何配置和使用 Warp Parse 系统的 Kafka 数据源。

概述

Kafka 源用于从 Apache Kafka 消息队列消费数据,支持消费单个主题和灵活的配置选项。

注意:系统会自动创建配置的主题(如果不存在),消费者组 ID 可以通过 group_id 参数配置,默认为 wparse_default_group

连接器定义

基础 Kafka 连接器

# connectors/source.d/30-kafka.toml
[[connectors]]
id = "kafka_src"
type = "kafka"
allow_override = ["topic", "group_id", "config"]

[connectors.params]
brokers = "localhost:9092"
topic = ["access_log"]
group_id = "wparse_default_group"

支持的参数

基础连接参数

brokers (必需)

Kafka 集群地址,支持字符串格式

[[sources.params]]
brokers = "localhost:9092"

topic (必需)

消费的主题名称(数组形式)

[[sources.params]]
topic = ["access_log"]

group_id (可选)

消费者组 ID

[[sources.params]]
group_id = "my_consumer_group"

安全配置

所有安全相关参数必须通过 config 数组配置,格式为 key=value 字符串。

SSL/TLS 配置

[[sources.params]]
config = [
    "security_protocol=SSL",
    "ssl_ca_location=/path/to/ca.pem",
    "ssl_certificate_location=/path/to/client.pem",
    "ssl_key_location=/path/to/client.key",
    "ssl_key_password=key_password"
]

SASL 认证

[[sources.params]]
config = [
    "security_protocol=SASL_PLAINTEXT",
    "sasl_mechanisms=PLAIN",
    "sasl_username=consumer_user",
    "sasl_password=consumer_pass"
]

SASL/SCRAM 认证

[[sources.params]]
config = [
    "security_protocol=SASL_SSL",
    "sasl_mechanisms=SCRAM-SHA-256",
    "sasl_username=consumer_user",
    "sasl_password=consumer_pass"
]

高级配置

消费策略

[[sources.params]]
config = [
    "auto_offset_reset=earliest",
    "enable_auto_commit=false",
    "auto_commit_interval_ms=5000"
]

会话和心跳配置

[[sources.params]]
config = [
    "session_timeout_ms=30000",
    "heartbeat_interval_ms=3000",
    "max_poll_interval_ms=300000"
]

批量消费配置

[[sources.params]]
config = [
    "max_poll_records=500",
    "fetch_min_bytes=1",
    "fetch_max_wait_ms=500"
]

配置示例

基础配置

# wpsrc.toml
[[sources]]
enable = true
key = "kafka_access_logs"
connect = "kafka_src"

[[sources.params]]
topic = ["nginx_access_log"]

高级配置

# wpsrc.toml
[[sources]]
enable = true
key = "kafka_advanced"
connect = "kafka_src"

[[sources.params]]
topic = ["access_log"]
config = [
    "auto_offset_reset=earliest",
    "enable_auto_commit=false"
]

安全集群配置

# wpsrc.toml
[[sources]]
enable = true
key = "kafka_secure_logs"
connect = "kafka_src"
tags = ["env:production", "security:tls"]

[[sources.params]]
topic = ["secure_events"]
config = [
    "auto_offset_reset=latest",
    "enable_auto_commit=true",
    "auto_commit_interval_ms=1000"
]

数据处理特性

1. 消息结构

每个 Kafka 消息被转换为数据包,包含:

  • 消息体: 消息的实际内容(payload)

示例:

{
  "data": "原始消息内容",
  "tags": {
    "access_source": "access_log",
    "env": "production",
    "type": "access_log"
  }
}

3. 消费语义

  • 消费者组 ID: 通过 group_id 参数配置
  • Topic 自动创建: 配置的主题不存在时会自动创建(1 个分区,复制因子为 1)
  • 偏移量提交: 由底层 rdkafka 库处理,可通过 config 参数配置

性能优化

1. 批量消费

[[sources.params]]
config = [
    "max_poll_records=1000",
    "fetch_min_bytes=1024",
    "fetch_max_wait_ms=100"
]

2. 连接优化

[[sources.params]]
config = [
    "session_timeout_ms=60000",
    "heartbeat_interval_ms=5000",
    "max_poll_interval_ms=600000"
]

相关文档

Syslog 源配置

本文档详细介绍如何配置和使用 Warp Parse 系统的 Syslog 数据源。

概述

Syslog 源用于接收和解析标准的 Syslog 协议消息,支持 UDP 和 TCP 两种传输协议,以及多种 Syslog 格式。

连接器定义

UDP Syslog 连接器

# connectors/source.d/10-syslog-udp.toml
[[connectors]]
id = "syslog_udp_src"
type = "syslog"
allow_override = ["addr", "port", "protocol", "tcp_recv_bytes", "header_mode", "fast_strip"]

[connectors.params]
addr = "0.0.0.0"
port = 1514
protocol = "udp"
header_mode = "strip"
tcp_recv_bytes = 256000

TCP Syslog 连接器

# connectors/source.d/11-syslog-tcp.toml
[[connectors]]
id = "syslog_tcp_src"
type = "syslog"
allow_override = ["addr", "port", "protocol", "tcp_recv_bytes", "header_mode", "fast_strip"]

[connectors.params]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"
header_mode = "strip"
tcp_recv_bytes = 256000

支持的参数

基础网络参数

addr (必需)

监听地址

[[sources.params]]
addr = "0.0.0.0"    # 监听所有接口
addr = "127.0.0.1"   # 仅本地接口
addr = "10.0.0.100"  # 特定接口

port (必需)

监听端口

[[sources.params]]
port = 514           # 标准 syslog 端口 (需要 root 权限)

protocol (必需)

传输协议

[[sources.params]]
protocol = "tcp"     # TCP 协议 (可靠传输)

消息处理参数

header_mode

头部处理模式

[[sources.params]]
header_mode = "strip"   # 仅剥离头部,不注入标签
header_mode = "parse"   # 解析+注入标签+剥离头部(默认)
header_mode = "keep"    # 保留头部,原样透传

fast_strip

快速剥离模式(性能优化)

[[sources.params]]
fast_strip = true   # 启用快速剥离(性能更好)

TCP 专用参数

tcp_recv_bytes

TCP 接收缓冲区大小

[[sources.params]]
tcp_recv_bytes = 256000      # 256KB (默认)
tcp_recv_bytes = 10485760    # 10MB
tcp_recv_bytes = 104857600   # 100MB (高性能)

配置示例

基础 UDP 配置

# wpsrc.toml
[[sources]]
enable = true
key = "syslog_udp_1"
connect = "syslog_udp_src"
tags = ["protocol:udp", "env:production"]

[[sources.params]]
addr = "0.0.0.0"
port = 1514
protocol = "udp"

基础 TCP 配置

# wpsrc.toml
[[sources]]
enable = true
key = "syslog_tcp_1"
connect = "syslog_tcp_src"
tags = ["protocol:tcp", "env:production"]

[[sources.params]]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"

双协议配置

# wpsrc.toml
[[sources]]
enable = true
key = "syslog_udp_collector"
connect = "syslog_udp_src"

[[sources.params]]
addr = "0.0.0.0"
port = 1514
protocol = "udp"
header_mode = "strip"

[[sources]]
enable = true
key = "syslog_tcp_aggregator"
connect = "syslog_tcp_src"

[[sources.params]]
addr = "127.0.0.1"
port = 1515
protocol = "tcp"
header_mode = "parse"
tcp_recv_bytes = 1048576

数据处理特性

1. Syslog 格式支持

RFC3164 格式 (传统 BSD Syslog)

<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8

RFC5424 格式 (现代 Syslog)

<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] BOMAn application event log entry

2. 解析字段

header_mode = "parse" 时,系统会解析并添加以下标签:

{
  "data": "原始消息内容",
  "tags": {
    "source_type": "syslog",
    "syslog_priority": 34,        // 数值优先级
    "syslog_facility": 4,         // 设施代码
    "syslog_severity": 2,         // 严重性级别
    "syslog_hostname": "mymachine",
    "syslog_app_name": "su",
    "syslog_proc_id": "1234",     // 进程ID (RFC5424)
    "syslog_msg_id": "ID47",      // 消息ID (RFC5424)
    "syslog_timestamp": "Oct 11 22:14:15"
  }
}

3. 分帧/头部处理优化

# 高性能场景:
header_mode = "strip"         # 仅去头,减少解析与标签注入
fast_strip = true             # 启用快速剥离

# 分析场景:
header_mode = "parse"         # 解析并注入协议相关元信息

相关文档

TCP 源配置

本文档介绍通用 TCP 源(kind=tcp)的使用方式、分帧模式与与 TCP Sink 的联动示例。

功能概览

  • 支持三种分帧模式:
    • line:按换行符分帧;行末的 CR/空格/Tab 会被去除
    • len:长度前缀(RFC 6587 octet-counting):<len><SP><payload>
    • auto(默认):自动选择;默认优先 len,当 prefer_newline=true 时优先按行

连接器定义(source.d)

# connectors/source.d/12-tcp.toml
[[connectors]]
id = "tcp_src"
type = "tcp"
# 允许覆写的键,兼容 syslog 的常见命名
allow_override = ["addr", "port", "framing", "tcp_recv_bytes", "instances"]

[connectors.params]
addr = "0.0.0.0"
port = 9000
framing = "auto"          # auto|line|len
tcp_recv_bytes = 256000   # 256KB
# instances = 1             # 可选:多实例并行,默认 1,最大 16

源配置(wpsrc.toml)

[[sources]]
key = "tcp_in"
connect = "tcp_src"
enable = true
tags = ["source:tcp", "type:raw"]

[[sources.params]]
port = 19000
framing = "auto"
instances = 2

分帧模式详解

line(换行)

  • 适用:文本日志、人工/脚本推送、简单工具(nc/tail)链路
  • 行尾会去掉 \r/空格/Tab;建议发送端每条以 \n 结尾

len(长度前缀)

  • 形如:5 hello → 表示下一条 payload 长度为 5 字节(不包含前缀中的空格)
  • 适用:payload 可能包含换行/二进制的场景(例如多行日志、堆栈、压缩片段)
  • 接收端约束:长度最大 10MB、前缀最多 10 位十进制,异常时丢弃当前尝试,避免内存膨胀

auto(自动)

  • 默认优先尝试 len,若解析失败则回退按行
  • 若已检测到“长度前缀进行中“(读到 <digits><SP> 但 payload 未到齐),会继续等待,而不会回退按行,避免误切分

与 TCP Sink 联动(回环链路)

为了便于端到端联调,本项目提供了通用 TCP Sink(kind=tcp):

  • sink connectors:connectors/sink.d/12-tcp.toml
  • sink 参数:addr/port/framing(line|len)
  • 示例:wpgen 输出到 tcp_sinkwparsetcp_src 监听同端口,实现本机回环

示例 wpgen(conf/wpgen.toml)

[generator]
mode = "sample"
count = 10000

[output]
connect = "tcp_sink"

[output.params]
addr = "127.0.0.1"
port = 19000
framing = "line"  # 或 "len"

完整用例(usecase/core/tcp_roundtrip)

目录:usecase/core/tcp_roundtrip

  • 启动:./case_verify.sh
  • 步骤:启动 wparse(tcp 源)→ wpgen 推送(tcp sink)→ 校验文件输出

常见问题(FAQ)

  • 问:文本以“数字+空格“开头会不会被误判为长度前缀?
    • 答:在 auto 模式下,确有可能;可通过 framing="line" 避免
  • 问:为什么推荐生产中用 len
    • 答:边界明确、对二进制/多行更稳健;很多 syslog/TCP 等生产链路推荐/默认使用 octet‑counting

最佳实践

  • 仅文本:framing="line"
  • 多行/二进制:framing="len"(或 auto 默认)
  • 快速联调:framing="line",配合 nc -lk <port>

相关文档

Source Meta

概述

Warp Parse 系统在解析数据时,会自动向 DataRecord 追加一些机制数据字段,用于追踪数据的来源和处理路径。这些机制数据字段以 wp_ 前缀标识,为系统提供了数据溯源和调试能力。

机制数据字段列表

1. wp_event_id

  • 字段类型: 字符串 (String)
  • 描述: 事件的唯一标识符
  • 来源: 从 SourceEvent.event_id 获取 -用途: 追踪单个事件在系统中的完整处理流程

2. wp_src_key

  • 字段类型: 字符串 (String)
  • 描述: 数据源的标识符
  • 来源: 从 SourceEvent.src_key 获取
  • 用途: 标识数据来源于哪个数据源(如 “syslog_1”, “file_reader” 等)

3. wp_src_ip

  • 字段类型: IP 地址 (IP)
  • 描述: 数据源的客户端 IP 地址
  • 来源: 从 SourceEvent.ups_ip 获取
  • 用途: 记录发送数据的客户端 IP 地址,用于审计和定位

Sinks 配置指南

本指南介绍如何配置和使用 Warp Parse 系统的各种数据输出(Sink)。

支持的 Sink 类型

类型说明文档
blackhole黑洞输出(用于测试)-
file输出到本地文件文件 Sink 配置
syslog输出到 Syslog 服务器 (UDP/TCP)Syslog Sink 配置
tcp输出到 TCP 服务端TCP Sink 配置
kafka输出到 KafkaKafka Sink 配置
mysql输出到 MySQLMySQL Sink 配置
doris输出到 DorisDoris Sink 配置
prometheusPrometheus 指标暴露Prometheus Sink 配置

Sink 基础

本文档介绍 warpparse 系统中数据输出端 (Sink) 的基础概念和配置结构。

概述

Sink 是 warpparse 系统的数据输出端,负责将处理后的数据发送到各种目标系统。系统支持多种输出类型,包括文件、Syslog、Prometheus 等。

核心概念

1. 配置层次结构

warpparse 系统采用分层配置架构:

全局默认配置 (defaults.toml)
    ↓
路由组配置 (business.d/**/*.toml, infra.d/**/*.toml)
    ↓
连接器定义 (connectors/sink.d/*.toml)
    ↓
解析后的 Sink 实例 (ResolvedSinkSpec)

2. 核心数据结构

配置文件结构

1. 连接器定义 (connectors.toml)

# connectors/sink.d/file_raw_sink.toml
[[connectors]]
id = "file_raw_sink"
type = "file"
allow_override = ["base", "file", "fmt"]

[connectors.params]
base = "./data/out_dat"
file = "default.dat"
fmt = "json"

关键字段说明

  • id: 连接器唯一标识符
  • type: 连接器类型 (file, syslog, prometheus 等)
  • allow_override: 允许源配置覆盖的参数列表
  • params: 连接器默认参数

2. 路由配置 (business.d//*.toml, infra.d//*.toml)

# business.d/example.toml
version = "2.0"

[sink_group]
name = "/sink/example"
oml = ["example_pattern"]
parallel = 2
tags = ["env:production"]

[[sink_group.sinks]]
name = "example_sink"
connect = "file_raw_sink"
params = {
    base = "./output",
    file = "example.dat"
}
filter = "./filter.wpl"
tags = ["type:example"]

[sink_group.sinks.expect]
ratio = 1.0
tol = 0.01

3. 全局默认配置 (defaults.toml)

# defaults.toml
version = "2.0"

[defaults]
tags = ["env:default"]

[defaults.expect]
basis = "total_input"
min_samples = 100
mode = "error"

基础配置示例

1. 简单文件输出

# infra.d/simple_file.toml
version = "2.0"

[sink_group]
name = "simple_output"
oml = []
[[sink_group.sinks]]
connect = "file_raw_sink"
params = { file = "simple.log" }

2. 带过滤器的输出

# business.d/filtered_output.toml
version = "2.0"

[sink_group]
name = "/sink/filtered"
oml = ["/oml/logs/*"]

[[sink_group.sinks]]
name = "all_logs"
connect = "file_json_sink"
params = { file = "all_logs.json" }

[[sink_group.sinks]]
name = "error_logs"
connect = "file_json_sink"
filter = "./error_filter.wpl"
params = { file = "error_logs.json" }
[sink_group.sinks.expect]
ratio = 0.1
tol = 0.02

3. 并行输出配置(仅业务组)

# business.d/parallel_output.toml
version = "2.0"

[sink_group]
name = "/sink/parallel"
oml = ["high_volume"]
parallel = 4
tags = ["type:parallel"]

[[sink_group.sinks]]
name = "output_1"
connect = "file_proto_sink"
params = { file = "output_1.dat" }

[[sink_group.sinks]]
name = "output_2"
connect = "file_proto_sink"
params = { file = "output_2.dat" }

注:基础组(infra.d)不支持 parallel 与文件分片;如需提升吞吐与分片,请在业务组配置。

标签系统

1. 标签继承层次

标签系统支持三层继承:

  1. 默认标签 (来自 defaults.toml)
  2. 组级标签 (来自 sink_group)
  3. Sink 级标签 (来自具体 sink)

2. 标签配置示例

# defaults.toml
[defaults]
tags = ["env:production", "service:warpflow"]

# business.d/example.toml
[sink_group]
tags = ["region:us-west", "tier:processing"]

[[sink_group.sinks]]
tags = ["output:file", "compression:gzip"]

最终标签合并结果

["env:production", "service:warpflow", "region:us-west", "tier:processing", "output:file", "compression:gzip"]

期望值配置 (Expect)

1. 比例模式

[sink_group.sinks.expect]
ratio = 1.0    # 期望占比 100%
tol = 0.01     # 允许偏差 ±1%

2. 范围模式

[sink_group.sinks.expect]
min = 0.001    # 最小占比 0.1%
max = 2.0      # 最大占比 200%

3. 全局默认期望值

[defaults.expect]
basis = "total_input"  # 计算基准
min_samples = 100      # 最小样本数
mode = "error"         # 违规时处理模式

过滤器配置

1. 过滤器文件

过滤器文件使用 WPL (Warp Processing Language) 语法:

# filter.wpl
# 只处理错误级别的日志
level == "ERROR" || level == "FATAL"

# 或者复杂条件
(level == "ERROR" && source == "auth") ||
(level == "WARN" && message ~= "timeout")

2. 过滤器应用

[[sink_group.sinks]]
name = "filtered_output"
connect = "file_json_sink"
filter = "./error_filter.wpl"    # 应用过滤器
params = { file = "errors.json" }

配置验证

1. 参数覆盖验证

系统严格验证参数覆盖:

  • 只能覆盖 allow_override 中指定的参数
  • 不支持嵌套表结构覆盖

2. 唯一性验证

  • 同一 sink_group 内 sink 名称必须唯一
  • 连接器 ID 必须全局唯一

3. 文件存在性验证

  • 过滤器文件必须存在且语法正确
  • 文件路径参数必须有效

defaults

目标

  • 配置期望(比例/阈值/窗口)并理解分母口径与忽略规则;在 CLI 中进行校验。

核心概念(权威见 ../02-config/04-sinks_config.md

  • defaults:组的默认期望,$SINK_ROOT/defaults.toml 中的 [defaults.expect];可被组/单 sink 覆盖。
  • group expect:[sink_group].expect;优先于 defaults。
  • sink expect:[[sink_group.sinks]].expect;仅支持局部字段(ratio/tol 或 min/max,二者不可混用)。
  • 分母口径:total_input | group_input | model(具体定义见权威文档)。

示例(defaults)

version = "2.0"
[defaults]
tags = ["env:dev"]
[defaults.expect]
basis = "group_input"
min_samples = 100
mode = "warn"

示例(组级与单 sink)

[sink_group]
name = "/sink/demo"
[sink_group.expect]
basis = "group_input"
mode  = "fail"

[[sink_group.sinks]]
name = "ff"
connect = "file_json_sink"
[sink_group.sinks.expect]
ratio = 0.98   # 与 min/max 互斥
tol   = 0.01

Sinks 路由

目标

  • 基于目录式 routes(business.d/infra.d)配置路由分发;理解业务组与基础组的差异及命名规则。

核心概念(单点定义见 ../02-config/04-sinks_config.md

  • 业务组 business.d:面向场景输出.
  • 基础组 infra.d:系统级输出(default/miss/residue/error/monitor)。

目录与命名

  • 业务组:$SINK_ROOT/business.d/**/*.toml,支持子目录递归;每个路由文件一个组。
  • 基础组:$SINK_ROOT/infra.d/**/*.toml,支持子目录递归;固定组名(default/miss/…)。
  • 连接器:从 $WORK_ROOT/models/sinks 起向上查找最近的 connectors/sink.d/*.toml

最小示例(业务组)

version = "2.0"
[sink_group]
name = "/sink/demo"
oml  = ["/oml/example/*"]
parallel = 1

[[sink_group.sinks]]
name = "file_out"
connect = "file_json_sink"   # 连接器 id
params = { file = "demo.json" }

基础组示例(default)

version = "2.0"
[sink_group]
name = "default"

[[sink_group.sinks]]
name = "default_sink"
connect = "file_json_sink"

文件 Sink

本文档介绍文件型 Sink 的现行配置与能力,已与代码实现对齐。

概述

文件 Sink 将处理后的数据写入本地文件系统,支持多种输出格式和灵活的路径配置。常用于离线验收、归档与调试。

支持的输出格式(fmt):jsoncsvkvrawprotoproto-text(默认 json)。

连接器定义

推荐直接使用仓库自带模板(位于 connectors/sink.d/):

# JSON
[[connectors]]
id = "file_json_sink"
type = "file"
allow_override = ["base","file"]
[connectors.params]
fmt  = "json"
base = "./data/out_dat"
file = "default.json"

# Prototext
[[connectors]]
id = "file_proto_sink"
type = "file"
allow_override = ["base","file"]
[connectors.params]
fmt  = "proto-text"
base = "./data/out_dat"
file = "default.dat"

# Raw
[[connectors]]
id = "file_raw_sink"
type = "file"
allow_override = ["base","file"]
[connectors.params]
fmt  = "raw"
base = "./data/out_dat"
file = "default.raw"

可用参数(路由 params

  • base + file:目标目录与文件名(推荐写法)。
  • fmt:输出格式(见上)。

说明:文件 Sink 会自动创建父目录;内部使用缓冲写入并按批次刷新,无“手动缓冲大小/同步模式”等参数。

配置示例

  1. 基础 JSON 输出
# business.d/json_output.toml
version = "2.0"
[sink_group]
name = "/sink/json_output"
oml  = ["logs"]

[[sink_group.sinks]]
name = "json"
connect = "file_json_sink"
params = { base = "/var/log/warpflow", file = "application.json" }
  1. 错误日志分离(按过滤器)
version = "2.0"
[sink_group]
name = "/sink/error_logs"
oml  = ["application_logs"]

[[sink_group.sinks]]
name = "all"
connect = "file_json_sink"
params = { file = "all.json" }

[[sink_group.sinks]]
name = "err"
connect = "file_json_sink"
filter = "./error_filter.wpl"
params = { file = "err.json" }

Syslog Sink

本文档与代码实现对齐,描述 Syslog Sink 的实际可用参数与示例。

概述

Syslog Sink 将数据以 RFC3164 文本格式发送到 Syslog 服务器,支持 UDP 与 TCP。消息格式:<PRI>TIMESTAMP HOSTNAME APP_NAME: MESSAGE

连接器定义

使用仓库内置模板(connectors/sink.d/10-syslog-udp.toml11-syslog-tcp.toml):

[[connectors]]
id = "syslog_udp_sink"
type = "syslog"
allow_override = ["addr", "port", "protocol", "app_name"]
[connectors.params]
addr = "127.0.0.1"
port = 1514
protocol = "udp"

[[connectors]]
id = "syslog_tcp_sink"
type = "syslog"
allow_override = ["addr", "port", "protocol", "app_name"]
[connectors.params]
addr = "127.0.0.1"
port = 1514
protocol = "tcp"

可用参数(路由 params

  • addr:Syslog 服务器地址(IP 或主机名)。
  • port:端口(1–65535)。
  • protocoludptcp(大小写不敏感)。
  • app_name:应用名称,默认为当前进程名(如 wp-engine)。

配置示例

  1. 基础 UDP 输出
version = "2.0"
[sink_group]
name = "/sink/syslog_basic"
oml  = ["application_logs"]

[[sink_group.sinks]]
name = "syslog_output"
connect = "syslog_udp_sink"
params = { addr = "syslog.example.com", port = 1514, protocol = "udp" }
  1. 按条件分流
version = "2.0"
[sink_group]
name = "/sink/syslog_by_cond"

[[sink_group.sinks]]
name = "error"
connect = "syslog_tcp_sink"
filter = "./error_filter.wpl"
params = { addr = "syslog-errors.example.com", port = 514, protocol = "tcp" }

[[sink_group.sinks]]
name = "info"
connect = "syslog_udp_sink"
filter = "./info_filter.wpl"
params = { addr = "syslog-info.example.com", port = 1514, protocol = "udp" }

Prometheus Sink

⚠️ 注意:Prometheus Sink 当前暂未实现。本文档为计划功能的设计文档,实际使用时请以代码实现为准。

本文档与代码实现对齐。当前 Prometheus Sink 为“自暴露“型 Exporter,会在本地启动一个 HTTP 服务(默认仅支持 Counter 类计数),对外暴露 /metrics

连接器定义

使用仓库模板(connectors/sink.d/40-prometheus.toml):

[[connectors]]
id = "prometheus_sink"
type = "prometheus"
allow_override = ["endpoint", "source_key_format", "sink_key_format"]
[connectors.params]
endpoint = "127.0.0.1:35666"   # 监听地址(对外暴露 /metrics)
source_key_format = "(?P<source_type>.)_(?P<access_source>.)"
sink_key_format   = "(?P<rule>.)_(?P<sink_type>.)_sink"

说明:不支持 Pushgateway/自定义 metric_name/metric_type/labels。内置指标名固定:

  • wparse_receive_data(从数据源接收条数,带源标签)
  • wparse_parse_successwparse_parse_all(解析成功/总量)
  • wparse_send_to_sink(发送到 sink 的条数,带 sink 标签)

可用参数(路由 params

  • endpoint:Exporter 监听的 host:port(如 127.0.0.1:35666)。
  • source_key_format:可选,用于从 key 中提取 source_type/access_source 的正则(具名分组)。
  • sink_key_format:可选,用于从 key 中提取 rule/sink_type 的正则(具名分组)。

配置示例

启动 Exporter 并暴露指标:

version = "2.0"
[sink_group]
name = "/sink/prom_exporter"
oml  = ["metrics"]

[[sink_group.sinks]]
name = "prom"
connect = "prometheus_sink"
params = { endpoint = "0.0.0.0:35666" }

验证:curl http://127.0.0.1:35666/metrics

TCP Sink

TCP sink 用于将数据输出到任意 TCP 服务端,支持按行或长度前缀(RFC6587 风格)分帧。

连接器定义

[[connectors]]
id = "tcp_sink"
type = "tcp"
allow_override = ["addr", "port", "framing"]

[connectors.params]
addr = "127.0.0.1"
port = 9000
framing = "line"   # line|len

可用参数

  • addr:目标服务器地址(IP 或主机名)。
  • port:目标端口(1–65535),默认 9000。
  • framing:分帧模式,linelen,默认 line

使用示例(wpgen 输出到 TCP)

# conf/wpgen.toml
[generator]
mode = "sample"
count = 10000

[output]
connect = "tcp_sink"

[output.params]
addr = "127.0.0.1"
port = 9000
framing = "line"

分帧说明

  • line:追加 \n 作为消息结束符
  • len:发送 <len><space><payload>(不追加 \n

VictoriaLogs

VictoriaLogs sink 用于将日志数据输出到 VictoriaLogs 日志存储系统,通过 HTTP JSON Line 接口写入。

连接器定义

[[connectors]]
id = "victorialog_sink"
type = "victorialogs"
allow_override = ["endpoint", "insert_path", "fmt"]

[connectors.params]
endpoint = "http://localhost:8481"
insert_path = "/insert/json"
fmt = "json"

可用参数

参数类型默认值说明
endpointstringhttp://localhost:8481VictoriaLogs 服务地址(必填)
insert_pathstring/insert/json数据写入路径
create_time_fieldstring-自定义时间戳字段名,从数据记录中提取
fmtstringjson输出格式:jsoncsvkvraw

数据格式

Sink 会将每条数据记录转换为 JSON 对象发送,包含以下特殊字段:

  • _msg:格式化后的消息内容(根据 fmt 参数格式化)
  • _time:时间戳(纳秒精度),优先使用 create_time_field 指定字段,否则使用当前时间

配置示例

基础用法

version = "2.0"

[sink_group]
name = "/sink/victorialogs"
oml  = ["logs"]

[[sink_group.sinks]]
name = "vlogs"
connect = "victorialog_sink"
params = { endpoint = "http://victorialogs:9428" }

自定义时间字段

[[sink_group.sinks]]
name = "vlogs"
connect = "victorialog_sink"

[sink_group.sinks.params]
endpoint = "http://victorialogs:9428"
insert_path = "/insert/jsonline"
create_time_field = "timestamp"
fmt = "json"

注意事项

  • endpoint 参数不能为空,否则会校验失败
  • HTTP 请求超时时间为 5 秒
  • 如果 create_time_field 指定的字段不存在或非时间类型,将使用当前 UTC 时间

Doris Sink

Doris sink 通过 Doris 的 MySQL 协议入口建立连接,并使用 Stream Load 写入数据。相比 mysql sink,对 Doris 兼容性更好(mysql sink 不兼容 Doris,doris sink 兼容 MySQL)。

连接器定义

推荐使用仓库自带模板(位于 connectors/sink.d/50-doris.toml):

[[connectors]]
id = "doris_sink"
type = "doris"
allow_override = ["endpoint", "user", "password", "database", "table", "create_table"]

[connectors.params]
endpoint = "mysql://localhost:9030?charset=utf8mb4&connect_timeout=10"
user = "root"
password = ""
database = "wp_test"
table = "events_parsed"

可用参数

参数类型说明
endpointstringDoris FE 的 MySQL 访问地址(DSN 形式),如 mysql://host:9030?charset=utf8mb4(必填)
userstringDoris 用户名(必填)
passwordstringDoris 密码(可为空)
databasestring目标数据库(必填)
tablestring目标表(必填)
create_tablestring可选建表 SQL,库表不存在时自动执行
pool_sizeint连接池大小(可选,常用 4)
batch_sizeint单批写入事件数量(可选,常用 2048)

配置示例

基础用法

version = "2.0"

[sink_group]
name = "/sink/doris"
oml  = ["example2"]

[[sink_group.sinks]]
name = "doris_stream_load"
connect = "doris_sink"

[sink_group.sinks.params]
endpoint = "mysql://localhost:9030?charset=utf8mb4&connect_timeout=10"
database = "wp_test"
table = "events_parsed"
user = "root"
password = ""

自动建表

[[sink_group.sinks]]
name = "doris_stream_load"
connect = "doris_sink"

[sink_group.sinks.params]
endpoint = "mysql://localhost:9030?charset=utf8mb4&connect_timeout=10"
database = "wp_test"
table = "events_parsed"
create_table = """
CREATE DATABASE IF NOT EXISTS wp_test;
CREATE TABLE events_parsed (
    sn           VARCHAR(64) COMMENT '设备序列号',
    dev_name     VARCHAR(128) COMMENT '设备名称',
    sip          VARCHAR(45) COMMENT '源 IP',
    from_zone    VARCHAR(32) COMMENT '来源区域',
    from_ip      VARCHAR(45) COMMENT '来源 IP',
    requ_uri     VARCHAR(512) COMMENT '请求 URI',
    requ_status  SMALLINT COMMENT '请求状态码',
    resp_len     INT COMMENT '响应长度',
    src_city     VARCHAR(32) COMMENT '源城市'
)
    ENGINE=OLAP
    DUPLICATE KEY(sn)
COMMENT '设备请求事件解析表'
DISTRIBUTED BY HASH(sn) BUCKETS 8
PROPERTIES (
    "replication_num" = "1"
);
"""
user = "root"
password = ""

注意事项

  • 确保 Doris FE 的 MySQL 端口可访问,并开启 Stream Load 能力。
  • 需要为账号授予 SELECT/INSERT 以及 LOAD 权限。
  • 完整端到端示例可参考 wp-examples/extensions/doris/README.md

Kafka Sink

Kafka sink 用于将数据输出到 Apache Kafka。启动时会尝试创建目标 topic(使用 num_partitions/replication 作为分区与副本配置)。

连接器定义

推荐使用仓库自带模板(位于 connectors/sink.d/30-kafka.toml):

[[connectors]]
id = "kafka_sink"
type = "kafka"
allow_override = ["topic", "config", "num_partitions", "replication", "brokers"]

[connectors.params]
brokers = "localhost:9092"
topic = "wparse_output"
num_partitions = 1
replication = 1
# config = ["compression.type=snappy", "acks=all"]

可用参数

参数类型说明
brokersstringKafka bootstrap servers(逗号分隔,必填)
topicstring目标 topic(必填)
num_partitionsint自动创建 topic 的分区数(默认 1)
replicationint自动创建 topic 的副本数(默认 1)
configstring/array生产者配置列表,key=value 形式(可选)

配置示例

基础用法

version = "2.0"

[sink_group]
name = "/sink/kafka"
oml  = ["example2"]

[[sink_group.sinks]]
name = "kafka_out"
connect = "kafka_sink"

[sink_group.sinks.params]
brokers = "localhost:9092"
topic = "wp.testcase.events.parsed"

自定义生产者参数与格式

[[sink_group.sinks]]
name = "kafka_out"
connect = "kafka_sink"

[sink_group.sinks.params]
topic = "app.events"
num_partitions = 3
replication = 1
config = [
  "compression.type=snappy",
  "acks=all",
  "linger.ms=5"
]

注意事项

  • config 参数会透传给 Kafka producer(rdkafka),格式必须是 key=value 字符串。
  • 若集群禁用自动建 topic,请提前在 Kafka 中创建目标 topic。
  • 完整示例可参考 wp-examples/extensions/kafka/README.md

MySQL Sink

MySQL sink 用于将解析后的记录写入 MySQL 表。它会根据 columns 生成 INSERT IGNORE 语句,适合幂等重试场景;仅接受 Record 数据(不支持 raw 输入)。

连接器定义

推荐使用仓库自带模板(位于 connectors/sink.d/50-mysql.toml):

[[connectors]]
id = "mysql_sink"
type = "mysql"
allow_override = ["endpoint", "username", "password", "database", "table", "columns", "batch"]

[connectors.params]
endpoint = "localhost:3306"
username = "root"
password = "123456"
database = "wparse"
table = "nginx_logs"
columns = ["sip", "timestamp", "http/request", "status", "size", "referer", "http/agent", "wp_event_id"]
batch = 20

可用参数

参数类型说明
endpointstringMySQL 地址(host:port,必填)
usernamestring用户名(可选,默认 root
passwordstring密码(可选)
databasestring目标数据库(必填)
tablestring目标表名(可选,未设置时使用 sink 名称)
columnsarray列名列表,决定写入字段顺序;必须包含 wp_event_id(缺省会自动补齐)
batchint批量写入条数(可选)

配置示例

基础用法(参考 extensions/tcp_mysql

version = "2.0"

[sink_group]
name = "all"
rule = ["/*"]
parallel = 8

[[sink_group.sinks]]
name = "main"
connect = "mysql_sink"

[sink_group.sinks.params]
endpoint = "localhost:3306"
username = "root"
password = "123456"
database = "wparse"
table = "nginx_logs"
columns = ["sip", "timestamp", "http/request", "status", "size", "referer", "http/agent", "wp_event_id"]
batch = 20

注意事项

  • 表结构必须包含 wp_event_id(建议为 BIGINT 主键),否则写入会失败或产生重复。
  • columns 中的字段名需与 OML 输出字段一致;缺失字段会以 NULL 写入。
  • 可通过环境变量 MYSQL_URL 覆盖连接串(格式:mysql://user:pass@host:port/db)。
  • 端到端示例可参考 wp-examples/extensions/tcp_mysql/README.md

排障指南(Troubleshooting)

常见问题

  • 未找到 connectors 目录:确认 connectors/source.dconnectors/sink.d 是否存在;遵守“从 models/<side> 向上查找最近目录(≤32 层)“。
  • 工厂未注册:确保调用 plugins::register_sinks()register_sources_factory_only()wproj 默认会做。
  • 覆写报错:检查 allow_override 白名单;覆写表禁止嵌套 params/params_override
  • tags 校验未过:数量/字符集/长度是否符合;减少高基数。
  • feature 缺失:Kafka/DB 等需按 Cargo feature 启用。

定位建议

  • 使用 wproj sinks validate|list|routewproj sources list|route 先“看清配置解析结果”。
  • 打开 conf/wparse.toml,确认 sink_root/src_root 指向的 models 目录存在。

延伸阅读

  • 文档导航:docs/README.md(Quick Triage)

BenchMark Report

4 种日志 * 3 种拓扑 * 2 种能力 * 2 种平台 * 2 种引擎。

范畴与指标

  • 拓扑:File -> BlackHole、TCP -> BlackHole、TCP -> File。
  • 能力:解析(Parse);解析+转换(Parse+Transform)。
  • 平台:Mac M4 Mini、Linux VPS。
  • 引擎:WarpParse、Vector。
  • 指标:EPS=Events Per Second,MPS(MiB/s, Messages Per Seconds);CPU/MEM 取进程平均/峰值;规则大小为对应规则文件体积。
  • 样本大小:Nginx 239B、AWS 411B、Sysmon 1K、APT 3K(单条日志)。

覆盖矩阵

维度取值
日志类型Nginx、AWS、Sysmon、APT
拓扑File -> BlackHole、TCP -> BlackHole、TCP -> File
能力解析、解析+转换
平台Mac M4 Mini(数据就绪)、Linux VPS(数据筹备中)
引擎WarpParse、Vector

日志解析测试

Mac M4 Mini

Nginx(239B)

WarpParse
WPL
package /nginx/ {
   rule nginx {
        (ip:sip,_^2,chars:timestamp<[,]>,http/request:http_request",chars:status,chars:size,chars:referer",http/agent:http_agent",_")
   }
}
output
{
	"wp_event_id": 1764645169882925000,
	"sip": "180.57.30.148",
	"timestamp": "21/Jan/2025:01:40:02 +0800",
	"http_request": "GET /nginx-logo.png HTTP/1.1",
	"status": "500",
	"size": "368",
	"referer": "http://207.131.38.110/",
	"http_agent": "Mozilla/5.0(Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 ",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
VRL
source = '''
  parsed = parse_regex!(.message, r'^(?P<client>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<request>[^"]*)" (?P<status>\d{3}) (?P<size>\d+) "(?P<referer>[^"]*)" "(?P<agent>[^"]*)" "(?P<extra>[^"]*)"')
  .sip = parsed.client
  .http_request = parsed.request
  .status = parsed.status
  .size = parsed.size
  .referer = parsed.referer
  .http_agent = parsed.agent
  .timestamp = parsed.time
  del(.message)
'''
output
{
	"host": "127.0.0.1",
	"http_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
	"http_request": "GET /nginx-logo.png HTTP/1.1",
	"port": 58102,
	"referer": "http://207.131.38.110/",
	"sip": "180.57.30.148",
	"size": "368",
	"source_type": "socket",
	"status": "500",
	"timestamp": "21/Jan/2025:01:40:02 +0800"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole2,456,100559.81684.40 % / 824.50 %107.36 MB / 120.44 MB174B
TCP -> BlackHole1,737,200395.96506.85 % / 650.90 %426.09 MB /450.23 MB
TCP -> File1,084,600247.21541.19 % / 722.40 %696.63 MB / 699.62 MB
VectorFile -> BlackHole540,540123.20341.51 % / 404.50 %230.67 MB / 251.14 MB416B
TCP -> BlackHole974,100222.02530.76 % / 660.60 %233.23 MB / 238.45 MB
TCP -> File91,20020.79186.35 % / 194.70 %230.62 MB / 244.22 MB

https://github.com/vectordotdev/vector/issues/20739?utm_source=chatgpt.com

AWS(411B)

WarpParse
WPL
package /aws/ {
   rule aws {
        (
            symbol(http),
            chars:timestamp,
            chars:elb,
            chars:client_host,
            chars:target_host,
            chars:request_processing_time,
            chars:target_processing_time,
            chars:response_processing_time,
            chars:elb_status_code,
            chars:target_status_code,
            chars:received_bytes,
            chars:sent_bytes,
            chars:request | (chars:request_method, chars:request_url, chars:request_protocol),
            chars:user_agent,
            chars:ssl_cipher,
            chars:ssl_protocol,
            chars:target_group_arn,
            chars:trace_id,
            chars:domain_name,
            chars:chosen_cert_arn,
            chars:matched_rule_priority,
            chars:request_creation_time,
            chars:actions_executed,
            chars:redirect_url,
            chars:error_reason,
            chars:target_port_list,
            chars:target_status_code_list,
            chars:classification,
            chars:classification_reason,
            chars:traceability_id,
        )
   }
   }
output
{
	"wp_event_id": 1764646097464011000,
	"symbol": "http",
	"timestamp": "2018-11-30T22:23:00.186641Z",
	"elb": "app/my-lb",
	"client_host": "192.168.1.10:2000",
	"target_host": "10.0.0.15:8080",
	"request_processing_time": "0.01",
	"target_processing_time": "0.02",
	"response_processing_time": "0.01",
	"elb_status_code": "200",
	"target_status_code": "200",
	"received_bytes": "100",
	"sent_bytes": "200",
	"request_method": "POST",
	"request_url": "https://api.example.com/u?p=1&sid=2&t=3",
	"request_protocol": "HTTP/1.1",
	"user_agent": "Mozilla/5.0 (Win) Chrome/90",
	"ssl_cipher": "ECDHE",
	"ssl_protocol": "TLSv1.3",
	"target_group_arn": "arn:aws:elb:us:123:tg",
	"trace_id": "Root=1-test",
	"domain_name": "api.example.com",
	"chosen_cert_arn": "arn:aws:acm:us:123:cert/short",
	"matched_rule_priority": "1",
	"request_creation_time": "2018-11-30T22:22:48.364000Z",
	"actions_executed": "forward",
	"redirect_url": "https://auth.example.com/r",
	"error_reason": "err",
	"target_port_list": "10.0.0.1:80",
	"target_status_code_list": "200",
	"classification": "cls",
	"classification_reason": "rsn",
	"traceability_id": "TID_x1",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
VRL
source = '''
  parsed = parse_regex!(.message, r'^(?P<type>\S+) (?P<timestamp>\S+) (?P<elb>\S+) (?P<client_host>\S+) (?P<target_host>\S+) (?P<request_processing_time>[-\d\.]+) (?P<target_processing_time>[-\d\.]+) (?P<response_processing_time>[-\d\.]+) (?P<elb_status_code>\S+) (?P<target_status_code>\S+) (?P<received_bytes>\d+) (?P<sent_bytes>\d+) "(?P<request_method>\S+) (?P<request_url>[^ ]+) (?P<request_protocol>[^"]+)" "(?P<user_agent>[^"]*)" "(?P<ssl_cipher>[^"]*)" "(?P<ssl_protocol>[^"]*)" (?P<target_group_arn>\S+) "(?P<trace_id>[^"]*)" "(?P<domain_name>[^"]*)" "(?P<chosen_cert_arn>[^"]*)" (?P<matched_rule_priority>\S+) (?P<request_creation_time>\S+) "(?P<actions_executed>[^"]*)" "(?P<redirect_url>[^"]*)" "(?P<error_reason>[^"]*)" "(?P<target_port_list>[^"]*)" "(?P<target_status_code_list>[^"]*)" "(?P<classification>[^"]*)" "(?P<classification_reason>[^"]*)" (?P<traceability_id>\S+)$')
  .timestamp = parsed.timestamp
  .symbol = parsed.type
  .elb = parsed.elb
  .client_host = parsed.client_host
  .target_host = parsed.target_host
  .request_processing_time = parsed.request_processing_time
  .target_processing_time = parsed.target_processing_time
  .response_processing_time = parsed.response_processing_time
  .elb_status_code = parsed.elb_status_code
  .target_status_code = parsed.target_status_code
  .received_bytes = parsed.received_bytes
  .sent_bytes = parsed.sent_bytes
  .request_method = parsed.request_method
  .request_url = parsed.request_url
  .request_protocol = parsed.request_protocol
  .user_agent = parsed.user_agent
  .ssl_cipher = parsed.ssl_cipher
  .ssl_protocol = parsed.ssl_protocol
  .target_group_arn = parsed.target_group_arn
  .trace_id = parsed.trace_id
  .domain_name = parsed.domain_name
  .chosen_cert_arn = parsed.chosen_cert_arn
  .matched_rule_priority = parsed.matched_rule_priority
  .request_creation_time = parsed.request_creation_time
  .actions_executed = parsed.actions_executed
  .redirect_url = parsed.redirect_url
  .error_reason = parsed.error_reason
  .target_port_list = parsed.target_port_list
  .target_status_code_list = parsed.target_status_code_list
  .classification = parsed.classification
  .classification_reason = parsed.classification_reason
  .traceability_id = parsed.traceability_id
  del(.message)
'''
output
{
	"actions_executed": "forward",
	"chosen_cert_arn": "arn:aws:acm:us:123:cert/short",
	"classification": "cls",
	"classification_reason": "rsn",
	"client_host": "192.168.1.10:2000",
	"domain_name": "api.example.com",
	"elb": "app/my-lb",
	"elb_status_code": "200",
	"error_reason": "err",
	"host": "127.0.0.1",
	"matched_rule_priority": "1",
	"port": 58786,
	"received_bytes": "100",
	"redirect_url": "https://auth.example.com/r",
	"request_creation_time": "2018-11-30T22:22:48.364000Z",
	"request_method": "POST",
	"request_processing_time": "0.01",
	"request_protocol": "HTTP/1.1",
	"request_url": "https://api.example.com/u?p=1&sid=2&t=3",
	"response_processing_time": "0.01",
	"sent_bytes": "200",
	"source_type": "socket",
	"ssl_cipher": "ECDHE",
	"ssl_protocol": "TLSv1.3",
	"symbol": "http",
	"target_group_arn": "arn:aws:elb:us:123:tg",
	"target_host": "10.0.0.15:8080",
	"target_port_list": "10.0.0.1:80",
	"target_processing_time": "0.02",
	"target_status_code": "200",
	"target_status_code_list": "200",
	"timestamp": "2018-11-30T22:23:00.186641Z",
	"trace_id": "Root=1-test",
	"traceability_id": "TID_x1",
	"user_agent": "Mozilla/5.0 (Win) Chrome/90"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole1,012,400396.82826.53 % / 937.80 %237.05 MB / 263.53 MB1153B
TCP -> BlackHole846,000331.60554.38 % / 710.90 %323.86 MB / 326.97 MB
TCP -> File347,800136.32495.90 % / 615.00 %481.30 MB /847.70 MB
VectorFile -> BlackHole158,73062.22633.77 % / 730.30 %296.87 MB / 307.42 MB2289B
TCP -> BlackHole163,60064.12628.67 % / 674.60 %264.21 MB / 275.98 MB
TCP -> File74,70029.28374.47 % / 409.50 %264.70 MB / 273.64 MB

Sysmon(1K,JSON)

WarpParse
WPL
package /sysmon/ {
   rule sysmon {
        (_:pri<<,>>,3*_,_),(_\S\y\s\m\o\n\:,
        json(
            @Id:id,
            @Description/ProcessId:process_id,
            @Level:severity,
            @Opcode:Opcode,
            @ProcessId:ProcessId,
            @Task:Task,
            @ThreadId:ThreadId
            @Version:Version,
            @Description/CommandLine:cmd_line,
            @Description/ParentCommandLine:parent_cmd_line,
            @Description/LogonGuid:logon_guid,
            @Description/LogonId:logon_id,
            @Description/Image:process_path,
            @Description/ParentImage:parent_process_path,
            @Description/ParentProcessGuid:parent_process_guid,
            @Description/ParentProcessId:parent_process_id,
            @Description/ParentUser:parent_process_user,
            @Description/ProcessGuid:process_guid,
            @Description/Company:product_company,
            @Description/Description:process_desc,
            @Description/FileVersion:file_version,
            chars@Description/Hashes:Hashes
            @Description/IntegrityLevel:integrity_level,
            @Description/OriginalFileName:origin_file_name,
            @Description/Product:product_name,
            @Description/RuleName:rule_name,
            @Description/User:user_name,
            chars@Description/UtcTime:occur_time,
            @Description/TerminalSessionId:terminal_session_id,
            @Description/CurrentDirectory:current_dir,
            @Keywords:keywords
            )
        )
    }
   }
output
{
	"wp_event_id": 1764657738662604000,
	"cmd_line": "a.exe",
	"product_company": "C",
	"current_dir": "C:\\\\",
	"process_desc": "D",
	"file_version": "1",
	"Hashes": "H",
	"process_path": "C:\\\\Windows\\\\a.exe",
	"integrity_level": "M",
	"logon_guid": "{LG}",
	"logon_id": "1",
	"origin_file_name": "a.exe",
	"parent_cmd_line": "b.exe",
	"parent_process_path": "C:\\\\Windows\\\\b.exe",
	"parent_process_guid": "{PG}",
	"parent_process_id": "1",
	"parent_process_user": "U",
	"process_guid": "{G}",
	"process_id": "1",
	"product_name": "P",
	"rule_name": "R",
	"terminal_session_id": "1",
	"user_name": "U",
	"occur_time": "2025-04-10 06:17:28.503",
	"DescriptionRawMessage": "Process Create\\r\\nRuleName: R",
	"id": "1",
	"keywords": "0",
	"severity": "4",
	"LevelDisplayName": "信息",
	"LogName": "L",
	"MachineName": "A",
	"Opcode": "0",
	"OpcodeDisplayName": "信息",
	"ProcessId": "1",
	"ProviderId": "PID",
	"ProviderName": "P",
	"Task": "1",
	"TaskDisplayName": "Process Create",
	"ThreadId": "1",
	"TimeCreated": "2025-04-10T14:17:28.693228+08:00",
	"Version": "1",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
VRL
source = '''
  parsed_msg = parse_regex!(.message, r'^[^{]*(?P<body>\{.*)$')
  parsed = parse_regex!(parsed_msg.body, r'(?s)\{"Id":(?P<Id>[^,]+),"Version":(?P<Version>[^,]+),"Level":(?P<Level>[^,]+),"Task":(?P<Task>[^,]+),"Opcode":(?P<Opcode>[^,]+),"Keywords":(?P<Keywords>[^,]+),"RecordId":(?P<RecordId>[^,]+),"ProviderName":"(?P<ProviderName>[^"]*)","ProviderId":"(?P<ProviderId>[^"]*)","LogName":"(?P<LogName>[^"]*)","ProcessId":(?P<ProcessId>[^,]+),"ThreadId":(?P<ThreadId>[^,]+),"MachineName":"(?P<MachineName>[^"]*)","TimeCreated":"(?P<TimeCreated>[^"]*)","ActivityId":(?P<ActivityId>[^,]+),"RelatedActivityId":(?P<RelatedActivityId>[^,]+),"Qualifiers":(?P<Qualifiers>[^,]+),"LevelDisplayName":"(?P<LevelDisplayName>[^"]*)","OpcodeDisplayName":"(?P<OpcodeDisplayName>[^"]*)","TaskDisplayName":"(?P<TaskDisplayName>[^"]*)","Description":\{"RuleName":"(?P<RuleName>[^"]*)","UtcTime":"(?P<UtcTime>[^"]*)","ProcessGuid":"(?P<ProcessGuid>[^"]*)","ProcessId":"(?P<DescProcessId>[^"]*)","Image":"(?P<Image>[^"]*)","FileVersion":"(?P<FileVersion>[^"]*)","Description":"(?P<Description>[^"]*)","Product":"(?P<Product>[^"]*)","Company":"(?P<Company>[^"]*)","OriginalFileName":"(?P<OriginalFileName>[^"]*)","CommandLine":"(?P<CommandLine>[^"]*)","CurrentDirectory":"(?P<CurrentDirectory>[^"]*)","User":"(?P<User>[^"]*)","LogonGuid":"(?P<LogonGuid>[^"]*)","LogonId":"(?P<LogonId>[^"]*)","TerminalSessionId":"(?P<TerminalSessionId>[^"]*)","IntegrityLevel":"(?P<IntegrityLevel>[^"]*)","Hashes":"(?P<Hashes>[^"]*)","ParentProcessGuid":"(?P<ParentProcessGuid>[^"]*)","ParentProcessId":"(?P<ParentProcessId>[^"]*)","ParentImage":"(?P<ParentImage>[^"]*)","ParentCommandLine":"(?P<ParentCommandLine>[^"]*)","ParentUser":"(?P<ParentUser>[^"]*)"\},"DescriptionRawMessage":"(?P<DescriptionRawMessage>[^"]*)"\}$')
  .cmd_line = parsed.CommandLine
  .product_company = parsed.Company
  .process_id = parsed.ProcessId
  .Opcode = parsed.Opcode
  .ProcessId = parsed.ProcessId 
  .Task = parsed.Task
  .ThreadId = parsed.ThreadId
  .Version = parsed.Version
  .current_dir = parsed.CurrentDirectory
  .process_desc = parsed.Description
  .file_version = parsed.FileVersion
  .Hashes = parsed.Hashes
  .process_path = parsed.Image
  .integrity_level = parsed.IntegrityLevel
  .logon_guid = parsed.LogonGuid
  .logon_id = parsed.LogonId
  .origin_file_name = parsed.OriginalFileName
  .parent_cmd_line = parsed.ParentCommandLine
  .parent_process_path = parsed.ParentImage
  .parent_process_guid = parsed.ParentProcessGuid
  .parent_process_id = parsed.ParentProcessId
  .parent_process_user = parsed.ParentUser
  .process_guid = parsed.ProcessGuid
  .product_name = parsed.Product
  .rule_name = parsed.RuleName
  .terminal_session_id = parsed.TerminalSessionId
  .user_name = parsed.User
  .occur_time = parsed.UtcTime
  .DescriptionRawMessage = parsed.DescriptionRawMessage
  .id = parsed.Id
  .keywords = parsed.Keywords
  .severity = parsed.Level
  .LevelDisplayName = parsed.LevelDisplayName
  .LogName = parsed.LogName
  .MachineName = parsed.MachineName
  .OpcodeDisplayName = parsed.OpcodeDisplayName
  .ProviderId = parsed.ProviderId
  .ProviderName = parsed.ProviderName
  .TaskDisplayName = parsed.TaskDisplayName
  .TimeCreated = parsed.TimeCreated
  del(.message)
output
{
	"DescriptionRawMessage": "Process Create\\r\\nRuleName: R",
	"Hashes": "H",
	"LevelDisplayName": "信息",
	"LogName": "L",
	"MachineName": "A",
	"Opcode": "0",
	"OpcodeDisplayName": "信息",
	"ProcessId": "1",
	"ProviderId": "PID",
	"ProviderName": "P",
	"Task": "1",
	"TaskDisplayName": "Process Create",
	"ThreadId": "1",
	"TimeCreated": "2025-04-10T14:17:28.693228+08:00",
	"Version": "1",
	"cmd_line": "a.exe",
	"current_dir": "C:\\\\",
	"file_version": "1",
	"host": "127.0.0.1",
	"id": "1",
	"integrity_level": "M",
	"keywords": "0",
	"logon_guid": "{LG}",
	"logon_id": "1",
	"occur_time": "2025-04-10 06:17:28.503",
	"origin_file_name": "a.exe",
	"parent_cmd_line": "b.exe",
	"parent_process_guid": "{PG}",
	"parent_process_id": "1",
	"parent_process_path": "C:\\\\Windows\\\\b.exe",
	"parent_process_user": "U",
	"port": 50558,
	"process_desc": "D",
	"process_guid": "{G}",
	"process_id": "1",
	"process_path": "C:\\\\Windows\\\\a.exe",
	"product_company": "C",
	"product_name": "P",
	"rule_name": "R",
	"severity": "4",
	"source_type": "socket",
	"terminal_session_id": "1",
	"timestamp": "2025-12-02T06:33:53.716258Z",
	"user_name": "U"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole440,000413.74852.01 % / 943.50 %223.52 MB / 338.05 MB1552B
TCP -> BlackHole418,900393.90720.42 % / 814.70 %455.91 MB / 461.02 MB
TCP -> File279,700263.01713.18 % / 789.30 %441.34 MB / 453.27 MB
VectorFile -> BlackHole76,71772.14462.81 % / 563.70 %294.87 MB / 312.77 MB3259B
TCP -> BlackHole111,900105.22720.04 % / 808.80 %362.95 MB / 376.90 MB
TCP -> File62,10058.39471.40 % / 543.40 %343.65 MB / 355.57 MB

APT(3K)

WarpParse
WPL
package /apt/ {
   rule apt {
        (
            _\#,
            time:timestamp,
            _,
            chars:Hostname,
            _\%\%, 
            chars:ModuleName\/,
            chars:SeverityHeader\/,
            symbol(ANTI-APT)\(,
            chars:type\),
            chars:Count<[,]>,
            _\:,
            chars:Content\(,
        ),
        (
            kv(chars@SyslogId),
            kv(chars@VSys),
            kv(chars@Policy),
            kv(chars@SrcIp),
            kv(chars@DstIp),
            kv(chars@SrcPort),
            kv(chars@DstPort),
            kv(chars@SrcZone),
            kv(chars@DstZone),
            kv(chars@User),
            kv(chars@Protocol),
            kv(chars@Application),
            kv(chars@Profile),
            kv(chars@Direction),
            kv(chars@ThreatType),
            kv(chars@ThreatName),
            kv(chars@Action),
            kv(chars@FileType),
            kv(chars@Hash)\),
        )\,
    }
   }

output
{
	"wp_event_id": 1764661811871722000,
	"timestamp": "2025-02-07 15:07:18",
	"Hostname": "USG1000E",
	"ModuleName": "01ANTI-APT",
	"SeverityHeader": "4",
	"symbol": "ANTI-APT",
	"type": "l",
	"Count": "29",
	"Content": "An advanced persistent threat was detected.",
	"SyslogId": "1",
	"VSys": "public-long-virtual-system-name-for-testing-extra-large-value-to-simulate-enterprise-scenario",
	"Policy": "trust-untrust-high-risk-policy-with-deep-inspection-and-layer7-protection-enabled-for-advanced-threat-detection",
	"SrcIp": "192.168.1.123",
	"DstIp": "182.150.63.102",
	"SrcPort": "51784",
	"DstPort": "10781",
	"SrcZone": "trust-zone-with-multiple-segments-for-internal-security-domains-and-access-control",
	"DstZone": "untrust-wide-area-network-zone-with-external-facing-interfaces-and-honeynet-integration",
	"User": "unknown-long-user-field-used-for-simulation-purpose-with-extra-description-and-tags-[tag1][tag2][tag3]-to-reach-required-size",
	"Protocol": "TCP",
	"Application": "HTTP-long-application-signature-identification-with-multiple-behavior-patterns-and-deep-packet-inspection-enabled",
	"Profile": "IPS_default_advanced_extended_profile_with_ml_detection-long",
	"Direction": "aaa-long-direction-field-used-to-extend-size-with-additional-info-about-traffic-orientation-from-client-to-server",
	"ThreatType": "File Reputation with additional descriptive context of multi-layer analysis engine including sandbox-behavioral-signature-ml-static-analysis-and-network-correlation-modules-working-together",
	"ThreatName": "bbb-advanced-threat-campaign-with-code-name-operation-shadow-storm-and-related-IOCs-collected-over-multiple-incidents-in-the-wild-attached-metadata-[phase1][phase2][phase3]",
	"Action": "ccc-block-and-alert-with-deep-scan-followed-by-quarantine-and-forensic-dump-generation-for-further-investigation",
	"FileType": "ddd-executable-binary-with-multiple-packed-layers-suspicious-import-table-behavior-and-evasion-techniques",
	"Hash": "eee1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef-long-hash-value-used-for-testing-purpose-extended-with-multiple-hash-representations-[MD5:aaa111bbb222ccc333]-[SHA1:bbb222ccc333ddd444]-[SHA256:ccc333ddd444eee555]-[SSDEEP:ddd444eee555fff666]-end-of-hash-section, ExtraInfo=\"This is additional extended information purposely added to inflate the total log size for stress testing of log ingestion engines such as Vector, Fluent Bit, self-developed ETL pipelines, and any high-throughput log processing systems. It contains repeated segments to simulate realistic verbose threat intelligence attachment blocks. [SEG-A-BEGIN] The threat was part of a coordinated multi-vector campaign observed across various geographic regions targeting enterprise networks with spear-phishing, watering-hole attacks, and supply-chain compromise vectors. Enriched indicators include C2 domains, malware families, behavioral clusters, sandbox detonation traces, and network telemetry correlation. [SEG-A-END] [SEG-B-BEGIN] Further analysis revealed that the payload exhibited persistence techniques including registry autoruns, scheduled tasks, masquerading, process injection, and lateral movement attempts leveraging remote service creation and stolen credentials. The binary contains multiple obfuscation layers, anti-debugging, anti-VM checks, and unusual API call sequences. [SEG-B-END] [SEG-C-BEGIN] IOC Bundle: Domains=malicious-domain-example-01.com,malicious-domain-example-02.net,malicious-update-service.info; IPs=103.21.244.0,198.51.100.55,203.0.113.77; FileNames=update_service.exe,winlog_service.dll,mscore_update.bin; RegistryKeys=HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run,HKLM\\\\System\\\\Services\\\\FakeService; Mutex=Global\\\\A1B2C3D4E5F6G7H8; YARA Matches=[rule1,rule2,rule3]. [SEG-C-END] EndOfExtraInfo\"",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
VRL
source = '''
  parsed_log = parse_regex!(.message, r'(?s)^#(?P<timestamp>\w+\s+\d+\s+\d{4}\s+\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\s+(?P<hostname>\S+)\s+%%(?P<ModuleName>\d+[^/]+)/(?P<SeverityHeader>\d+)/(?P<symbol>[^(]+)\((?P<type>[^)]+)\)\[(?P<count>\d+)\]:\s*(?P<content>[^()]+?)\s*\(SyslogId=(?P<SyslogId>[^,]+),\s+VSys="(?P<VSys>[^"]+)",\s+Policy="(?P<Policy>[^"]+)",\s+SrcIp=(?P<SrcIp>[^,]+),\s+DstIp=(?P<DstIp>[^,]+),\s+SrcPort=(?P<SrcPort>[^,]+),\s+DstPort=(?P<DstPort>[^,]+),\s+SrcZone=(?P<SrcZone>[^,]+),\s+DstZone=(?P<DstZone>[^,]+),\s+User="(?P<User>[^"]+)",\s+Protocol=(?P<Protocol>[^,]+),\s+Application="(?P<Application>[^"]+)",\s+Profile="(?P<Profile>[^"]+)",\s+Direction=(?P<Direction>[^,]+),\s+ThreatType=(?P<ThreatType>[^,]+),\s+ThreatName=(?P<ThreatName>[^,]+),\s+Action=(?P<Action>[^,]+),\s+FileType=(?P<FileType>[^,]+),\s+Hash=(?P<Hash>.*)\)$')
  .Hostname = parsed_log.hostname
  .SrcPort = parsed_log.SrcPort
  .SeverityHeader = parsed_log.SeverityHeader
  .type = parsed_log.type
  .Count = parsed_log.count
  .Content = parsed_log.content
  .VSys = parsed_log.VSys
  .DstPort = parsed_log.DstPort
  .Policy = parsed_log.Policy
  .SrcIp = parsed_log.SrcIp
  .DstIp = parsed_log.DstIp
  .SrcZone = parsed_log.SrcZone
  .DstZone = parsed_log.DstZone
  .User = parsed_log.User
  .Protocol = parsed_log.Protocol
  .ModuleName = parsed_log.ModuleName
  .symbol = parsed_log.symbol
  .timestamp = parsed_log.timestamp
  .SyslogId = parsed_log.SyslogId
  .Application = parsed_log.Application
  .Profile = parsed_log.Profile
  .Direction = parsed_log.Direction
  .ThreatType = parsed_log.ThreatType
  .ThreatName = parsed_log.ThreatName
  .Action = parsed_log.Action
  .FileType = parsed_log.FileType
  .Hash = parsed_log.Hash
  del(.message)
'''
output
{
	"Action": "ccc-block-and-alert-with-deep-scan-followed-by-quarantine-and-forensic-dump-generation-for-further-investigation",
	"Application": "HTTP-long-application-signature-identification-with-multiple-behavior-patterns-and-deep-packet-inspection-enabled",
	"Content": "An advanced persistent threat was detected.",
	"Count": "29",
	"Direction": "aaa-long-direction-field-used-to-extend-size-with-additional-info-about-traffic-orientation-from-client-to-server",
	"DstIp": "182.150.63.102",
	"DstPort": "10781",
	"DstZone": "untrust-wide-area-network-zone-with-external-facing-interfaces-and-honeynet-integration",
	"FileType": "ddd-executable-binary-with-multiple-packed-layers-suspicious-import-table-behavior-and-evasion-techniques",
	"Hash": "eee1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef-long-hash-value-used-for-testing-purpose-extended-with-multiple-hash-representations-[MD5:aaa111bbb222ccc333]-[SHA1:bbb222ccc333ddd444]-[SHA256:ccc333ddd444eee555]-[SSDEEP:ddd444eee555fff666]-end-of-hash-section, ExtraInfo=\"This is additional extended information purposely added to inflate the total log size for stress testing of log ingestion engines such as Vector, Fluent Bit, self-developed ETL pipelines, and any high-throughput log processing systems. It contains repeated segments to simulate realistic verbose threat intelligence attachment blocks. [SEG-A-BEGIN] The threat was part of a coordinated multi-vector campaign observed across various geographic regions targeting enterprise networks with spear-phishing, watering-hole attacks, and supply-chain compromise vectors. Enriched indicators include C2 domains, malware families, behavioral clusters, sandbox detonation traces, and network telemetry correlation. [SEG-A-END] [SEG-B-BEGIN] Further analysis revealed that the payload exhibited persistence techniques including registry autoruns, scheduled tasks, masquerading, process injection, and lateral movement attempts leveraging remote service creation and stolen credentials. The binary contains multiple obfuscation layers, anti-debugging, anti-VM checks, and unusual API call sequences. [SEG-B-END] [SEG-C-BEGIN] IOC Bundle: Domains=malicious-domain-example-01.com,malicious-domain-example-02.net,malicious-update-service.info; IPs=103.21.244.0,198.51.100.55,203.0.113.77; FileNames=update_service.exe,winlog_service.dll,mscore_update.bin; RegistryKeys=HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run,HKLM\\\\System\\\\Services\\\\FakeService; Mutex=Global\\\\A1B2C3D4E5F6G7H8; YARA Matches=[rule1,rule2,rule3]. [SEG-C-END] EndOfExtraInfo\"",
	"Hostname": "USG1000E",
	"ModuleName": "01ANTI-APT",
	"Policy": "trust-untrust-high-risk-policy-with-deep-inspection-and-layer7-protection-enabled-for-advanced-threat-detection",
	"Profile": "IPS_default_advanced_extended_profile_with_ml_detection-long",
	"Protocol": "TCP",
	"SeverityHeader": "4",
	"SrcIp": "192.168.1.123",
	"SrcPort": "51784",
	"SrcZone": "trust-zone-with-multiple-segments-for-internal-security-domains-and-access-control",
	"SyslogId": "1",
	"ThreatName": "bbb-advanced-threat-campaign-with-code-name-operation-shadow-storm-and-related-IOCs-collected-over-multiple-incidents-in-the-wild-attached-metadata-[phase1][phase2][phase3]",
	"ThreatType": "File Reputation with additional descriptive context of multi-layer analysis engine including sandbox-behavioral-signature-ml-static-analysis-and-network-correlation-modules-working-together",
	"User": "unknown-long-user-field-used-for-simulation-purpose-with-extra-description-and-tags-[tag1][tag2][tag3]-to-reach-required-size",
	"VSys": "public-long-virtual-system-name-for-testing-extra-large-value-to-simulate-enterprise-scenario",
	"host": "127.0.0.1",
	"port": 55771,
	"source_type": "socket",
	"symbol": "ANTI-APT",
	"timestamp": "Feb  7 2025 15:07:18+08:00",
	"type": "l"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole314,2001062.84700.03 % / 826.30 %175.63 MB / 181.05 MB985B
TCP -> BlackHole298,2001008.72693.55 % / 762.10 %408.87 MB / 481.27 MB
TCP -> File179,600607.53605.69 % / 853.20 %1016.06 MB/ 1987.94 MB
VectorFile -> BlackHole33,614113.71563.18 % / 677.50 %261.19 MB / 278.39 MB1759B
TCP -> BlackHole46,100155.94849.30 % / 921.50 %421.18 MB / 445.80 MB
TCP -> File36,200122.45688.47 % / 754.70 %368.91 MB / 397.16 MB

日志解析转换测试

Mac M4 Mini

Nginx(239B)

WarpParse

WPL
package /nginx/ {
   rule nginx {
        (ip:sip,_^2,chars:timestamp<[,]>,http/request",chars:status,chars:size,chars:referer",http/agent",_")
   }
}
OML
name : nginx
rule : /nginx/*
---
size : digit = take(size);
status : digit = take(status);
str_status = match read(option:[status]) {
    digit(500) => chars(Internal Server Error);
    digit(404) => chars(Not Found); 
};
match_chars = match read(option:[wp_src_ip]) {
    ip(127.0.0.1) => chars(localhost); 
    !ip(127.0.0.1) => chars(attack_ip); 
};
* : auto = read();
output
{
	"host": "127.0.0.1",
	"http_agent": "Mozilla/5.0(Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 ",
	"http_request": "GET /nginx-logo.png HTTP/1.1",
	"match_chars": "localhost",
	"referer": "http://207.131.38.110/",
	"sip": "180.57.30.148",
	"size": 368,
	"source_type": "socket",
	"status": 500,
	"str_status": "Internal Server Error",
	"timestamp": "21/Jan/2025:01:40:02 +0800"
}
Vector
VRL
source = '''
  parsed = parse_regex!(.message, r'^(?P<client>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<request>[^"]*)" (?P<status>\d{3}) (?P<size>\d+) "(?P<referer>[^"]*)" "(?P<agent>[^"]*)" "(?P<extra>[^"]*)"')
  .sip = parsed.client
  .http_request = parsed.request
  .referer = parsed.referer
  .http_agent = parsed.agent
  .timestamp = parsed.time
  del(.message)
  .status = to_int!(parsed.status)
  .size = to_int!(parsed.size)
if .host == "127.0.0.1" {
    .match_chars = "localhost"
} else if .host != "127.0.0.1" {
    .match_chars = "attack_ip"
}  
if .status == 500 {
    .str_status = "Internal Server Error"
} else if .status == 404 {
    .str_status = "Not Found"
}  
'''
output
{
	"host": "127.0.0.1",
	"http_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
	"http_request": "GET /nginx-logo.png HTTP/1.1",
	"match_chars": "localhost",
	"port": 53894,
	"referer": "http://207.131.38.110/",
	"sip": "180.57.30.148",
	"size": 368,
	"source_type": "socket",
	"status": 500,
	"str_status": "Internal Server Error",
	"timestamp": "21/Jan/2025:01:40:02 +0800"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole1,749,200398.69762.65 %/865.70 %143.16 MB/159.22 MB521
WarpParseTCP -> BlackHole1,219,100277.87485.37 %/625.20 %415.22 MB/440.52 MB
WarpParseTCP -> File797,700181.82492.15 %/621.20 %523.97 MB/540.97 MB
VectorFile -> BlackHole470,312107.20372.09 %/423.00 %254.05 MB/280.03 MB682
VectorTCP -> BlackHole870,500198.41514.06 %/639.90 %238.50 MB/258.02 MB
VectorTCP -> File708,0016.14160.79 %/180.60 %226.58 MB/236.41 MB

AWS(411B)

WarpParse

WPL
package /aws/ {
   rule aws {
        (
            symbol(http),
            chars:timestamp,
            chars:elb,
            chars:client_host,
            chars:target_host,
            chars:request_processing_time,
            chars:target_processing_time,
            chars:response_processing_time,
            chars:elb_status_code,
            chars:target_status_code,
            chars:received_bytes,
            chars:sent_bytes,
            chars:request | (chars:request_method, chars:request_url, chars:request_protocol),
            chars:user_agent,
            chars:ssl_cipher,
            chars:ssl_protocol,
            chars:target_group_arn,
            chars:trace_id,
            chars:domain_name,
            chars:chosen_cert_arn,
            chars:matched_rule_priority,
            chars:request_creation_time,
            chars:actions_executed,
            chars:redirect_url,
            chars:error_reason,
            chars:target_port_list,
            chars:target_status_code_list,
            chars:classification,
            chars:classification_reason,
            chars:traceability_id,
        )
   }
   }
OML
name : aws
rule : /aws/*
---
sent_bytes:digit = take(sent_bytes) ;
target_status_code:digit = take(target_status_code) ;
elb_status_code:digit = take(elb_status_code) ;
extends : obj = object {
    ssl_cipher = read(ssl_cipher);
    ssl_protocol = read(ssl_protocol);
};
match_chars = match read(option:[wp_src_ip]) {
    ip(127.0.0.1) => chars(localhost); 
    !ip(127.0.0.1) => chars(attack_ip); 
};
str_elb_status = match read(option:[elb_status_code]) {
    digit(200) => chars(ok);
    digit(404) => chars(error); 
};
* : auto = read();
output
{
	"timestamp": "2018-11-30T22:23:00.186641Z",
	"actions_executed": "forward",
	"chosen_cert_arn": "arn:aws:acm:us:123:cert/short",
	"classification": "cls",
	"classification_reason": "rsn",
	"client_host": "192.168.1.10:2000",
	"domain_name": "api.example.com",
	"elb": "app/my-lb",
	"elb_status_code": 200,
	"error_reason": "err",
	"extends": {
		"ssl_cipher": "ECDHE",
		"ssl_protocol": "TLSv1.3"
	},
	"host": "127.0.0.1",
	"match_chars": "localhost",
	"matched_rule_priority": "1",
	"received_bytes": "100",
	"redirect_url": "https://auth.example.com/r",
	"request_creation_time": "2018-11-30T22:22:48.364000Z",
	"request_method": "POST",
	"request_processing_time": "0.01",
	"request_protocol": "HTTP/1.1",
	"request_url": "https://api.example.com/u?p=1&sid=2&t=3",
	"response_processing_time": "0.01",
	"sent_bytes": 200,
	"source_type": "socket",
	"ssl_cipher": "ECDHE",
	"ssl_protocol": "TLSv1.3",
	"str_elb_status": "ok",
	"target_group_arn": "arn:aws:elb:us:123:tg",
	"target_host": "10.0.0.15:8080",
	"target_port_list": "10.0.0.1:80",
	"target_processing_time": "0.02",
	"target_status_code": 200,
	"target_status_code_list": "200",
	"trace_id": "Root=1-test",
	"traceability_id": "TID_x1",
	"user_agent": "Mozilla/5.0 (Win) Chrome/90"
}
Vector
VRL
source = '''
  parsed = parse_regex!(.message, r'^(?P<type>\S+) (?P<timestamp>\S+) (?P<elb>\S+) (?P<client_host>\S+) (?P<target_host>\S+) (?P<request_processing_time>[-\d\.]+) (?P<target_processing_time>[-\d\.]+) (?P<response_processing_time>[-\d\.]+) (?P<elb_status_code>\S+) (?P<target_status_code>\S+) (?P<received_bytes>\d+) (?P<sent_bytes>\d+) "(?P<request_method>\S+) (?P<request_url>[^ ]+) (?P<request_protocol>[^"]+)" "(?P<user_agent>[^"]*)" "(?P<ssl_cipher>[^"]*)" "(?P<ssl_protocol>[^"]*)" (?P<target_group_arn>\S+) "(?P<trace_id>[^"]*)" "(?P<domain_name>[^"]*)" "(?P<chosen_cert_arn>[^"]*)" (?P<matched_rule_priority>\S+) (?P<request_creation_time>\S+) "(?P<actions_executed>[^"]*)" "(?P<redirect_url>[^"]*)" "(?P<error_reason>[^"]*)" "(?P<target_port_list>[^"]*)" "(?P<target_status_code_list>[^"]*)" "(?P<classification>[^"]*)" "(?P<classification_reason>[^"]*)" (?P<traceability_id>\S+)$')
  .timestamp = parsed.timestamp
  .symbol = parsed.type
  .elb = parsed.elb
  .client_host = parsed.client_host
  .target_host = parsed.target_host
  .request_processing_time = parsed.request_processing_time
  .target_processing_time = parsed.target_processing_time
  .response_processing_time = parsed.response_processing_time
  .received_bytes = parsed.received_bytes
  .request_method = parsed.request_method
  .request_url = parsed.request_url
  .request_protocol = parsed.request_protocol
  .user_agent = parsed.user_agent
  .ssl_cipher = parsed.ssl_cipher
  .ssl_protocol = parsed.ssl_protocol
  .target_group_arn = parsed.target_group_arn
  .trace_id = parsed.trace_id
  .domain_name = parsed.domain_name
  .chosen_cert_arn = parsed.chosen_cert_arn
  .matched_rule_priority = parsed.matched_rule_priority
  .request_creation_time = parsed.request_creation_time
  .actions_executed = parsed.actions_executed
  .redirect_url = parsed.redirect_url
  .error_reason = parsed.error_reason
  .target_port_list = parsed.target_port_list
  .target_status_code_list = parsed.target_status_code_list
  .classification = parsed.classification
  .classification_reason = parsed.classification_reason
  .traceability_id = parsed.traceability_id
  del(.message)
  .elb_status_code = to_int!(parsed.elb_status_code)
  .target_status_code = to_int!(parsed.target_status_code)
  .sent_bytes = to_int!(parsed.sent_bytes)
if .host == "127.0.0.1" {
    .match_chars = "localhost"
} else if .host != "127.0.0.1" {
    .match_chars = "attack_ip"
}   
if .elb_status_code == 200 {
    .str_elb_status = "ok"
} else if .elb_status_code == 404 {
    .str__elb_status = "error"
}
  .extends = {
    "ssl_cipher": .ssl_cipher,
    "ssl_protocol": .ssl_protocol,
}
'''
output
{
	"actions_executed": "forward",
	"chosen_cert_arn": "arn:aws:acm:us:123:cert/short",
	"classification": "cls",
	"classification_reason": "rsn",
	"client_host": "192.168.1.10:2000",
	"domain_name": "api.example.com",
	"elb": "app/my-lb",
	"elb_status_code": 200,
	"error_reason": "err",
	"extends": {
		"ssl_cipher": "ECDHE",
		"ssl_protocol": "TLSv1.3"
	},
	"host": "127.0.0.1",
	"match_chars": "localhost",
	"matched_rule_priority": "1",
	"port": 53995,
	"received_bytes": "100",
	"redirect_url": "https://auth.example.com/r",
	"request_creation_time": "2018-11-30T22:22:48.364000Z",
	"request_method": "POST",
	"request_processing_time": "0.01",
	"request_protocol": "HTTP/1.1",
	"request_url": "https://api.example.com/u?p=1&sid=2&t=3",
	"response_processing_time": "0.01",
	"sent_bytes": 200,
	"source_type": "socket",
	"ssl_cipher": "ECDHE",
	"ssl_protocol": "TLSv1.3",
	"str_elb_status": "ok",
	"symbol": "http",
	"target_group_arn": "arn:aws:elb:us:123:tg",
	"target_host": "10.0.0.15:8080",
	"target_port_list": "10.0.0.1:80",
	"target_processing_time": "0.02",
	"target_status_code": 200,
	"target_status_code_list": "200",
	"timestamp": "2018-11-30T22:23:00.186641Z",
	"trace_id": "Root=1-test",
	"traceability_id": "TID_x1",
	"user_agent": "Mozilla/5.0 (Win) Chrome/90"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole710,400278.45837.44 %/912.40 %230.00 MB/252.95 MB1694
WarpParseTCP -> BlackHole611,800239.80624.17 %/753.00 %478.25 MB/487.42 MB
WarpParseTCP -> File318,200124.72593.16 %/732.60 %409.21 MB/547.50 MB
VectorFile -> BlackHole129,74350.85593.45 %/665.00 %283.67 MB/298.16 MB2650
VectorTCP -> BlackHole152,90059.93611.53 %/677.50 %288.53 MB/294.42 MB
VectorTCP -> File582,0022.81331.78 %/373.90 %276.99 MB/288.42 MB

Sysmon(1K,JSON)

WarpParse

WPL
package /sysmon/ {
   rule sysmon {
        (_:pri<<,>>,3*_,_),(_\S\y\s\m\o\n\:,
        json(
            @Id:id,
            @Description/ProcessId:process_id,
            @Level:severity,
            @Opcode:Opcode,
            @ProcessId:ProcessId,
            @Task:Task,
            @ThreadId:ThreadId
            @Version:Version,
            @Description/CommandLine:cmd_line,
            @Description/ParentCommandLine:parent_cmd_line,
            @Description/LogonGuid:logon_guid,
            @Description/LogonId:logon_id,
            @Description/Image:process_path,
            @Description/ParentImage:parent_process_path,
            @Description/ParentProcessGuid:parent_process_guid,
            @Description/ParentProcessId:parent_process_id,
            @Description/ParentUser:parent_process_user,
            @Description/ProcessGuid:process_guid,
            @Description/Company:product_company,
            @Description/Description:process_desc,
            @Description/FileVersion:file_version,
            chars@Description/Hashes:Hashes
            @Description/IntegrityLevel:integrity_level,
            @Description/OriginalFileName:origin_file_name,
            @Description/Product:product_name,
            @Description/RuleName:rule_name,
            @Description/User:user_name,
            chars@Description/UtcTime:occur_time,
            @Description/TerminalSessionId:terminal_session_id,
            @Description/CurrentDirectory:current_dir,
            @Keywords:keywords
            )
        )
    }
   }
OML
name : sysmon
rule : /sysmon/*
---
Id:digit = take(id) ;
LogonId:digit = take(logon_id) ;
enrich_level = match read(option:[severity]) {
    chars(4) => chars(severity);
    chars(3) => chars(normal);
};
extends : obj = object {
    OriginalFileName = read(origin_file_name);
    ParentCommandLine = read(parent_cmd_line);
};
extends_dir = object {
    ParentProcessPath = read(parent_process_path);
    Process_path = read(process_path);
};
match_chars = match read(option:[wp_src_ip]) {
    ip(127.0.0.1) => chars(localhost); 
    !ip(127.0.0.1) => chars(attack_ip); 
};
num_range = match read(option:[Id]) {
    in ( digit(0), digit(1000) ) => read(Id) ;
    _ => digit(0) ;
};
* : auto = read();
output
{
	"Id": 1,
	"LogonId": 1,
	"enrich_level": "severity",
	"extends": {
		"OriginalFileName": "a.exe",
		"ParentCommandLine": "b.exe"
	},
	"extends_dir": {
		"ParentProcessPath": "C:\\\\Windows\\\\b.exe",
		"Process_path": "C:\\\\Windows\\\\a.exe"
	},
	"match_chars": "localhost",
	"num_range": 1,
	"wp_event_id": 1764813339134818000,
	"cmd_line": "a.exe",
	"product_company": "C",
	"current_dir": "C:\\\\",
	"process_desc": "D",
	"file_version": "1",
	"Hashes": "H",
	"process_path": "C:\\\\Windows\\\\a.exe",
	"integrity_level": "M",
	"logon_guid": "{LG}",
	"origin_file_name": "a.exe",
	"parent_cmd_line": "b.exe",
	"parent_process_path": "C:\\\\Windows\\\\b.exe",
	"parent_process_guid": "{PG}",
	"parent_process_id": "1",
	"parent_process_user": "U",
	"process_guid": "{G}",
	"process_id": "1",
	"product_name": "P",
	"rule_name": "R",
	"terminal_session_id": "1",
	"user_name": "U",
	"occur_time": "2025-04-10 06:17:28.503",
	"DescriptionRawMessage": "Process Create\\r\\nRuleName: R",
	"keywords": "0",
	"severity": "4",
	"LevelDisplayName": "信息",
	"LogName": "L",
	"MachineName": "A",
	"Opcode": "0",
	"OpcodeDisplayName": "信息",
	"ProcessId": "1",
	"ProviderId": "PID",
	"ProviderName": "P",
	"Task": "1",
	"TaskDisplayName": "Process Create",
	"ThreadId": "1",
	"TimeCreated": "2025-04-10T14:17:28.693228+08:00",
	"Version": "1",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
VRL
source = '''
  parsed_msg = parse_regex!(.message, r'^[^{]*(?P<body>\{.*)$')
  parsed = parse_regex!(parsed_msg.body, r'(?s)\{"Id":(?P<Id>[^,]+),"Version":(?P<Version>[^,]+),"Level":(?P<Level>[^,]+),"Task":(?P<Task>[^,]+),"Opcode":(?P<Opcode>[^,]+),"Keywords":(?P<Keywords>[^,]+),"RecordId":(?P<RecordId>[^,]+),"ProviderName":"(?P<ProviderName>[^"]*)","ProviderId":"(?P<ProviderId>[^"]*)","LogName":"(?P<LogName>[^"]*)","ProcessId":(?P<ProcessId>[^,]+),"ThreadId":(?P<ThreadId>[^,]+),"MachineName":"(?P<MachineName>[^"]*)","TimeCreated":"(?P<TimeCreated>[^"]*)","ActivityId":(?P<ActivityId>[^,]+),"RelatedActivityId":(?P<RelatedActivityId>[^,]+),"Qualifiers":(?P<Qualifiers>[^,]+),"LevelDisplayName":"(?P<LevelDisplayName>[^"]*)","OpcodeDisplayName":"(?P<OpcodeDisplayName>[^"]*)","TaskDisplayName":"(?P<TaskDisplayName>[^"]*)","Description":\{"RuleName":"(?P<RuleName>[^"]*)","UtcTime":"(?P<UtcTime>[^"]*)","ProcessGuid":"(?P<ProcessGuid>[^"]*)","ProcessId":"(?P<DescProcessId>[^"]*)","Image":"(?P<Image>[^"]*)","FileVersion":"(?P<FileVersion>[^"]*)","Description":"(?P<Description>[^"]*)","Product":"(?P<Product>[^"]*)","Company":"(?P<Company>[^"]*)","OriginalFileName":"(?P<OriginalFileName>[^"]*)","CommandLine":"(?P<CommandLine>[^"]*)","CurrentDirectory":"(?P<CurrentDirectory>[^"]*)","User":"(?P<User>[^"]*)","LogonGuid":"(?P<LogonGuid>[^"]*)","LogonId":"(?P<LogonId>[^"]*)","TerminalSessionId":"(?P<TerminalSessionId>[^"]*)","IntegrityLevel":"(?P<IntegrityLevel>[^"]*)","Hashes":"(?P<Hashes>[^"]*)","ParentProcessGuid":"(?P<ParentProcessGuid>[^"]*)","ParentProcessId":"(?P<ParentProcessId>[^"]*)","ParentImage":"(?P<ParentImage>[^"]*)","ParentCommandLine":"(?P<ParentCommandLine>[^"]*)","ParentUser":"(?P<ParentUser>[^"]*)"\},"DescriptionRawMessage":"(?P<DescriptionRawMessage>[^"]*)"\}$')
  .cmd_line = parsed.CommandLine
  .product_company= parsed.Company
  .Opcode = parsed.Opcode
  .process_id = parsed.ProcessId 
  .ProcessId = parsed.ProcessId 
  .Task = parsed.Task
  .ThreadId = parsed.ThreadId
  .Version = parsed.Version
  .current_dir = parsed.CurrentDirectory
  .process_desc = parsed.Description
  .file_version = parsed.FileVersion
  .Hashes = parsed.Hashes
  .process_path = parsed.Image
  .integrity_level = parsed.IntegrityLevel
  .logon_guid = parsed.LogonGuid
  .origin_file_name = parsed.OriginalFileName
  .parent_cmd_line = parsed.ParentCommandLine
  .parent_process_path = parsed.ParentImage
  .parent_process_guid = parsed.ParentProcessGuid
  .parent_process_id = parsed.ParentProcessId
  .parent_process_user = parsed.ParentUser
  .process_guid = parsed.ProcessGuid
  .product_name = parsed.Product
  .rule_name = parsed.RuleName
  .terminal_session_id = parsed.TerminalSessionId
  .user_name = parsed.User
  .occur_time = parsed.UtcTime
  .DescriptionRawMessage = parsed.DescriptionRawMessage
  .keywords = parsed.Keywords
  .severity = parsed.Level
  .LevelDisplayName = parsed.LevelDisplayName
  .LogName = parsed.LogName
  .MachineName = parsed.MachineName
  .OpcodeDisplayName = parsed.OpcodeDisplayName
  .ProviderId = parsed.ProviderId
  .ProviderName = parsed.ProviderName
  .TaskDisplayName = parsed.TaskDisplayName
  .TimeCreated = parsed.TimeCreated
  del(.message)
  .LogonId = to_int!(parsed.LogonId)
  .Id = to_int!(parsed.Id)
if .host == "127.0.0.1" {
    .match_chars = "localhost"
} else if .host != "127.0.0.1" {
    .match_chars = "attack_ip"
}   
if .severity == "4" {
    .enrich_level = "severity"
} else if .Level == "3" {
    .enrich_level = "normal"
} 
.extends = {
    "OriginalFileName": .origin_file_name,
    "ParentCommandLine": .parent_cmd_line,
}
.extends_dir = {
    "ParentProcessPath": .parent_process_path,
    "Process_path": .process_path,
}
.num_range = if .Id >= 0 && .Id <= 1000 {
    .Id
} else {
    0
}
'''
output
{
	"DescriptionRawMessage": "Process Create\\r\\nRuleName: R",
	"Hashes": "H",
	"Id": 1,
	"LevelDisplayName": "信息",
	"LogName": "L",
	"LogonId": 1,
	"MachineName": "A",
	"Opcode": "0",
	"OpcodeDisplayName": "信息",
	"ProcessId": "1",
	"ProviderId": "PID",
	"ProviderName": "P",
	"Task": "1",
	"TaskDisplayName": "Process Create",
	"ThreadId": "1",
	"TimeCreated": "2025-04-10T14:17:28.693228+08:00",
	"Version": "1",
	"cmd_line": "a.exe",
	"current_dir": "C:\\\\",
	"enrich_level": "severity",
	"extends": {
		"OriginalFileName": "a.exe",
		"ParentCommandLine": "b.exe"
	},
	"extends_dir": {
		"ParentProcessPath": "C:\\\\Windows\\\\b.exe",
		"Process_path": "C:\\\\Windows\\\\a.exe"
	},
	"file_version": "1",
	"host": "127.0.0.1",
	"integrity_level": "M",
	"keywords": "0",
	"logon_guid": "{LG}",
	"match_chars": "localhost",
	"num_range": 1,
	"occur_time": "2025-04-10 06:17:28.503",
	"origin_file_name": "a.exe",
	"parent_cmd_line": "b.exe",
	"parent_process_guid": "{PG}",
	"parent_process_id": "1",
	"parent_process_path": "C:\\\\Windows\\\\b.exe",
	"parent_process_user": "U",
	"port": 49838,
	"process_desc": "D",
	"process_guid": "{G}",
	"process_id": "1",
	"process_path": "C:\\\\Windows\\\\a.exe",
	"product_company": "C",
	"product_name": "P",
	"rule_name": "R",
	"severity": "4",
	"source_type": "socket",
	"terminal_session_id": "1",
	"timestamp": "2025-12-04T02:04:24.686378Z",
	"user_name": "U"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole354,800333.63880.24 %/935.40 %157.88 MB/170.69 MB2249
WarpParseTCP -> BlackHole299,500281.63664.56 %/749.00 %367.47 MB/377.25 MB
WarpParseTCP -> File219,900206.78719.29 %/817.00 %431.93 MB/457.17 MB
VectorFile -> BlackHole582,0054.73431.45 %/527.60 %296.28 MB/317.84 MB3782
VectorTCP -> BlackHole97,20091.40710.83 %/806.80 %399.64 MB/424.08 MB
VectorTCP -> File40,30037.90391.09 %/497.30 %394.67 MB/409.95 MB

APT(3K)

WarpParse

WPL
package /apt/ {
   rule apt {
        (
            _\#,
            time:timestamp,
            _,
            chars:Hostname,
            _\%\%, 
            chars:ModuleName\/,
            chars:SeverityHeader\/,
            symbol(ANTI-APT)\(,
            chars:type\),
            chars:Count<[,]>,
            _\:,
            chars:Content\(,
        ),
        (
            kv(chars@SyslogId),
            kv(chars@VSys),
            kv(chars@Policy),
            kv(chars@SrcIp),
            kv(chars@DstIp),
            kv(chars@SrcPort),
            kv(chars@DstPort),
            kv(chars@SrcZone),
            kv(chars@DstZone),
            kv(chars@User),
            kv(chars@Protocol),
            kv(chars@Application),
            kv(chars@Profile),
            kv(chars@Direction),
            kv(chars@ThreatType),
            kv(chars@ThreatName),
            kv(chars@Action),
            kv(chars@FileType),
            kv(chars@Hash)\),
        )\,
    }
   }
OML
name : apt
rule : /apt/*
---
count:digit = take(Count) ;
severity:digit = take(SeverityHeader) ;
match_chars = match read(option:[wp_src_ip]) {
    ip(127.0.0.1) => chars(localhost); 
    !ip(127.0.0.1) => chars(attack_ip); 
};
num_range = match read(option:[count]) {
    in ( digit(0), digit(1000) ) => read(count) ;
    _ => digit(0) ;
};
src_system_log_type = match read(option:[type]) {
    chars(l) => chars(日志信息);
    chars(s) => chars(安全日志信息);
};
extends_ip : obj = object {
    DstIp = read(DstIp);
    SrcIp = read(SrcIp);
};
extends_info : obj = object {
    hostname = read(Hostname);
    source_type = read(wp_src_key)
};
* : auto = read();
output
{
	"count": 29,
	"severity": 4,
	"match_chars": "localhost",
	"num_range": 29,
	"src_system_log_type": "日志信息",
	"extends_ip": {
		"DstIp": "182.150.63.102",
		"SrcIp": "192.168.1.123"
	},
	"extends_info": {
		"hostname": "USG1000E",
		"source_type": "socket"
	},
	"wp_event_id": 1764815397395451000,
	"timestamp": "2025-02-07 15:07:18",
	"Hostname": "USG1000E",
	"ModuleName": "01ANTI-APT",
	"symbol": "ANTI-APT",
	"type": "l",
	"Content": "An advanced persistent threat was detected.",
	"SyslogId": "1",
	"VSys": "public-long-virtual-system-name-for-testing-extra-large-value-to-simulate-enterprise-scenario",
	"Policy": "trust-untrust-high-risk-policy-with-deep-inspection-and-layer7-protection-enabled-for-advanced-threat-detection",
	"SrcIp": "192.168.1.123",
	"DstIp": "182.150.63.102",
	"SrcPort": "51784",
	"DstPort": "10781",
	"SrcZone": "trust-zone-with-multiple-segments-for-internal-security-domains-and-access-control",
	"DstZone": "untrust-wide-area-network-zone-with-external-facing-interfaces-and-honeynet-integration",
	"User": "unknown-long-user-field-used-for-simulation-purpose-with-extra-description-and-tags-[tag1][tag2][tag3]-to-reach-required-size",
	"Protocol": "TCP",
	"Application": "HTTP-long-application-signature-identification-with-multiple-behavior-patterns-and-deep-packet-inspection-enabled",
	"Profile": "IPS_default_advanced_extended_profile_with_ml_detection-long",
	"Direction": "aaa-long-direction-field-used-to-extend-size-with-additional-info-about-traffic-orientation-from-client-to-server",
	"ThreatType": "File Reputation with additional descriptive context of multi-layer analysis engine including sandbox-behavioral-signature-ml-static-analysis-and-network-correlation-modules-working-together",
	"ThreatName": "bbb-advanced-threat-campaign-with-code-name-operation-shadow-storm-and-related-IOCs-collected-over-multiple-incidents-in-the-wild-attached-metadata-[phase1][phase2][phase3]",
	"Action": "ccc-block-and-alert-with-deep-scan-followed-by-quarantine-and-forensic-dump-generation-for-further-investigation",
	"FileType": "ddd-executable-binary-with-multiple-packed-layers-suspicious-import-table-behavior-and-evasion-techniques",
	"Hash": "eee1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef-long-hash-value-used-for-testing-purpose-extended-with-multiple-hash-representations-[MD5:aaa111bbb222ccc333]-[SHA1:bbb222ccc333ddd444]-[SHA256:ccc333ddd444eee555]-[SSDEEP:ddd444eee555fff666]-end-of-hash-section, ExtraInfo=\"This is additional extended information purposely added to inflate the total log size for stress testing of log ingestion engines such as Vector, Fluent Bit, self-developed ETL pipelines, and any high-throughput log processing systems. It contains repeated segments to simulate realistic verbose threat intelligence attachment blocks. [SEG-A-BEGIN] The threat was part of a coordinated multi-vector campaign observed across various geographic regions targeting enterprise networks with spear-phishing, watering-hole attacks, and supply-chain compromise vectors. Enriched indicators include C2 domains, malware families, behavioral clusters, sandbox detonation traces, and network telemetry correlation. [SEG-A-END] [SEG-B-BEGIN] Further analysis revealed that the payload exhibited persistence techniques including registry autoruns, scheduled tasks, masquerading, process injection, and lateral movement attempts leveraging remote service creation and stolen credentials. The binary contains multiple obfuscation layers, anti-debugging, anti-VM checks, and unusual API call sequences. [SEG-B-END] [SEG-C-BEGIN] IOC Bundle: Domains=malicious-domain-example-01.com,malicious-domain-example-02.net,malicious-update-service.info; IPs=103.21.244.0,198.51.100.55,203.0.113.77; FileNames=update_service.exe,winlog_service.dll,mscore_update.bin; RegistryKeys=HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run,HKLM\\\\System\\\\Services\\\\FakeService; Mutex=Global\\\\A1B2C3D4E5F6G7H8; YARA Matches=[rule1,rule2,rule3]. [SEG-C-END] EndOfExtraInfo\"",
	"wp_src_key": "socket",
	"wp_src_ip": "127.0.0.1"
}
Vector
source = '''
parsed_log = parse_regex!(.message, r'(?s)^#(?P<timestamp>\w+\s+\d+\s+\d{4}\s+\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\s+(?P<hostname>\S+)\s+%%(?P<ModuleName>\d+[^/]+)/(?P<SeverityHeader>\d+)/(?P<symbol>[^(]+)\((?P<type>[^)]+)\)\[(?P<count>\d+)\]:\s*(?P<content>[^()]+?)\s*\(SyslogId=(?P<SyslogId>[^,]+),\s+VSys="(?P<VSys>[^"]+)",\s+Policy="(?P<Policy>[^"]+)",\s+SrcIp=(?P<SrcIp>[^,]+),\s+DstIp=(?P<DstIp>[^,]+),\s+SrcPort=(?P<SrcPort>[^,]+),\s+DstPort=(?P<DstPort>[^,]+),\s+SrcZone=(?P<SrcZone>[^,]+),\s+DstZone=(?P<DstZone>[^,]+),\s+User="(?P<User>[^"]+)",\s+Protocol=(?P<Protocol>[^,]+),\s+Application="(?P<Application>[^"]+)",\s+Profile="(?P<Profile>[^"]+)",\s+Direction=(?P<Direction>[^,]+),\s+ThreatType=(?P<ThreatType>[^,]+),\s+ThreatName=(?P<ThreatName>[^,]+),\s+Action=(?P<Action>[^,]+),\s+FileType=(?P<FileType>[^,]+),\s+Hash=(?P<Hash>.*)\)$')
  .Hostname = parsed_log.hostname
  .SrcPort = parsed_log.SrcPort
  .SeverityHeader = parsed_log.SeverityHeader
  .type = parsed_log.type
  .Content = parsed_log.content
  .VSys = parsed_log.VSys
  .DstPort = parsed_log.DstPort
  .Policy = parsed_log.Policy
  .SrcIp = parsed_log.SrcIp
  .DstIp = parsed_log.DstIp
  .SrcZone = parsed_log.SrcZone
  .DstZone = parsed_log.DstZone
  .User = parsed_log.User
  .Protocol = parsed_log.Protocol
  .ModuleName = parsed_log.ModuleName
  .symbol = parsed_log.symbol
  .timestamp = parsed_log.timestamp
  .SyslogId = parsed_log.SyslogId
  .Application = parsed_log.Application
  .Profile = parsed_log.Profile
  .Direction = parsed_log.Direction
  .ThreatType = parsed_log.ThreatType
  .ThreatName = parsed_log.ThreatName
  .Action = parsed_log.Action
  .FileType = parsed_log.FileType
  .Hash = parsed_log.Hash
  del(.message)
.severity = to_int!(parsed_log.SeverityHeader)
.count = to_int!(parsed_log.count)
if .host == "127.0.0.1" {
    .match_chars = "localhost"
} else if .host != "127.0.0.1" {
    .match_chars = "attack_ip"
}  
if .type == "l" {
.src_system_log_type = "日志信息"
} else if .type == "s" {
.src_system_log_type = "安全日志信息"
}
.extends_ip = {
    "DstIp": .DstIp,
    "SrcIp": .SrcIp,
}
.extends_info = {
    "hostname": .Hostname,
    "source_type": .source_type,
}
.num_range = if .count >= 0 && .count <= 1000 {
    .count
} else {
    0
}
'''
output
{
	"Action": "ccc-block-and-alert-with-deep-scan-followed-by-quarantine-and-forensic-dump-generation-for-further-investigation",
	"Application": "HTTP-long-application-signature-identification-with-multiple-behavior-patterns-and-deep-packet-inspection-enabled",
	"Content": "An advanced persistent threat was detected.",
	"Direction": "aaa-long-direction-field-used-to-extend-size-with-additional-info-about-traffic-orientation-from-client-to-server",
	"DstIp": "182.150.63.102",
	"DstPort": "10781",
	"DstZone": "untrust-wide-area-network-zone-with-external-facing-interfaces-and-honeynet-integration",
	"FileType": "ddd-executable-binary-with-multiple-packed-layers-suspicious-import-table-behavior-and-evasion-techniques",
	"Hash": "eee1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef-long-hash-value-used-for-testing-purpose-extended-with-multiple-hash-representations-[MD5:aaa111bbb222ccc333]-[SHA1:bbb222ccc333ddd444]-[SHA256:ccc333ddd444eee555]-[SSDEEP:ddd444eee555fff666]-end-of-hash-section, ExtraInfo=\"This is additional extended information purposely added to inflate the total log size for stress testing of log ingestion engines such as Vector, Fluent Bit, self-developed ETL pipelines, and any high-throughput log processing systems. It contains repeated segments to simulate realistic verbose threat intelligence attachment blocks. [SEG-A-BEGIN] The threat was part of a coordinated multi-vector campaign observed across various geographic regions targeting enterprise networks with spear-phishing, watering-hole attacks, and supply-chain compromise vectors. Enriched indicators include C2 domains, malware families, behavioral clusters, sandbox detonation traces, and network telemetry correlation. [SEG-A-END] [SEG-B-BEGIN] Further analysis revealed that the payload exhibited persistence techniques including registry autoruns, scheduled tasks, masquerading, process injection, and lateral movement attempts leveraging remote service creation and stolen credentials. The binary contains multiple obfuscation layers, anti-debugging, anti-VM checks, and unusual API call sequences. [SEG-B-END] [SEG-C-BEGIN] IOC Bundle: Domains=malicious-domain-example-01.com,malicious-domain-example-02.net,malicious-update-service.info; IPs=103.21.244.0,198.51.100.55,203.0.113.77; FileNames=update_service.exe,winlog_service.dll,mscore_update.bin; RegistryKeys=HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run,HKLM\\\\System\\\\Services\\\\FakeService; Mutex=Global\\\\A1B2C3D4E5F6G7H8; YARA Matches=[rule1,rule2,rule3]. [SEG-C-END] EndOfExtraInfo\"",
	"Hostname": "USG1000E",
	"ModuleName": "01ANTI-APT",
	"Policy": "trust-untrust-high-risk-policy-with-deep-inspection-and-layer7-protection-enabled-for-advanced-threat-detection",
	"Profile": "IPS_default_advanced_extended_profile_with_ml_detection-long",
	"Protocol": "TCP",
	"SeverityHeader": "4",
	"SrcIp": "192.168.1.123",
	"SrcPort": "51784",
	"SrcZone": "trust-zone-with-multiple-segments-for-internal-security-domains-and-access-control",
	"SyslogId": "1",
	"ThreatName": "bbb-advanced-threat-campaign-with-code-name-operation-shadow-storm-and-related-IOCs-collected-over-multiple-incidents-in-the-wild-attached-metadata-[phase1][phase2][phase3]",
	"ThreatType": "File Reputation with additional descriptive context of multi-layer analysis engine including sandbox-behavioral-signature-ml-static-analysis-and-network-correlation-modules-working-together",
	"User": "unknown-long-user-field-used-for-simulation-purpose-with-extra-description-and-tags-[tag1][tag2][tag3]-to-reach-required-size",
	"VSys": "public-long-virtual-system-name-for-testing-extra-large-value-to-simulate-enterprise-scenario",
	"count": 29,
	"extends_info": {
		"hostname": "USG1000E",
		"source_type": "socket"
	},
	"extends_ip": {
		"DstIp": "182.150.63.102",
		"SrcIp": "192.168.1.123"
	},
	"host": "127.0.0.1",
	"match_chars": "localhost",
	"num_range": 29,
	"port": 51272,
	"severity": 4,
	"source_type": "socket",
	"src_system_log_type": "日志信息",
	"symbol": "ANTI-APT",
	"timestamp": "Feb  7 2025 15:07:18+08:00",
	"type": "l"
}
引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole280,000947.15768.50 %/868.90 %172.72 MB/178.23 MB1638
WarpParseTCP -> BlackHole238,900808.12657.14 %/705.40 %364.73 MB/389.94 MB
WarpParseTCP -> File169,800574.38663.90 %/883.80 %871.77 MB/1500.22 MB
VectorFile -> BlackHole306,12103.55560.94 %/654.40 %248.00 MB/273.02 MB2259
VectorTCP -> BlackHole340,00115.01693.47 %/848.80 %408.92 MB/430.59 MB
VectorTCP -> File249,0084.23538.78 %/644.90 %393.26 MB/420.05 MB

Linux VPS

平台数据仍在采集中,先保留统一表格,便于后续补录。

Nginx(239B)

引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----
VectorFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----

AWS(411B)

引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----
VectorFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----

Sysmon(1K,JSON)

引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----
VectorFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----

APT(3K)

引擎拓扑EPSMPSCPU (avg/peak)MEM (avg/peak)规则大小
WarpParseFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----
VectorFile -> BlackHole-----
TCP -> BlackHole----
TCP -> File----

Connector 实现指南

架构总览

  • 运行时通过 connectors/registry 维护 Source/Sink Factory 的注册表,利用 OnceCell + RwLock 管理工厂实例,并在 register_* 时记录调用位置,方便诊断(src/connectors/registry.rs:1-99)。
  • 应用启动时统一调用 connectors/startup::init_runtime_registries:它一次性注册内置 Sink(file/syslog/tcp/test_rescue/blackhole)与 Source(syslog/tcp/file),随后打印最终的 kind 列表,确保外部动态工厂也可追踪(src/connectors/startup.rs:1-42)。
  • 若还需桥接 ConnectorKindAdapter,使用 connectors/adapter.rs 中的注册表;在 engine 内注册后,后续组件都能通过 kind 查到各自的 adapter(src/connectors/adapter.rs:1-43)。
         ┌────────────────────────────────────┐
         │     wp_connector_api (Traits)     │
         │ ┌───────────────────────────────┐ │
         │ │ SourceFactory / SinkFactory   │ │
         │ │ DataSource / Async* traits    │ │
         │ └───────────────────────────────┘ │
         └────────────────────────────────────┘
                         │ 实现
                         ▼
 ┌─────────────────────────────────────────────────────────────┐
 │              具体 Source / Sink 实现 (wp-engine)            │
 │ ┌─────────────────────────────────────────────────────────┐ │
 │ │ FileSourceSpec ──► FileSourceFactory ──► FileSource     │ │
 │ │ TcpSourceSpec  ──► TcpSourceFactory  ──► TcpSource      │ │
 │ │ SyslogSourceSpec ─► SyslogFactory     ─► Tcp/Udp Source │ │
 │ │ TcpSinkSpec    ──► TcpFactory         ─► TcpSink        │ │
 │ └─────────────────────────────────────────────────────────┘ │
 └─────────────────────────────────────────────────────────────┘
                         │ 注册
                         ▼
           ┌───────────────────────────────────┐
           │ connectors::registry / startup    │
           │  • register_*_factory(...)        │
           │  • log_registered_kinds()         │
           └───────────────────────────────────┘
                         │ 提供统一入口
                         ▼
           ┌───────────────────────────────────┐
           │ runtime / orchestrator            │
           │  读取 kind → 获取 Factory → Build │
           └───────────────────────────────────┘

必须实现的 Trait

  • SourceFactory/SinkFactory:来自 wp_connector_api。工厂需实现 kind(&self), validate_spec(&self, &Resolved*Spec)build(&self, &Resolved*Spec, &*Ctx)。示例参见 src/sources/file/factory.rs:65-123src/sinks/backends/tcp.rs:240-259
  • DataSource:Source 运行时实现 DataSource trait,提供 receive/try_receive/can_try_receive/identifier 等接口;FileSourcesrc/sources/file/source.rs 展示了如何在 receive 中返回批次并在 stop 时清理。
  • AsyncCtrl/AsyncRecordSink/AsyncRawDataSink:Sink 运行时需实现这些异步 trait 以接收结构化记录和原始字符串;TcpSinksrc/sinks/backends/tcp.rs:62-215 给出了完整实现(含批量方法)。
  • 可选:ConnectorKindAdapter:若需要在运行时动态选择不同工厂组合,实现 wp_connector_api::ConnectorKindAdapter 并通过 connectors/adapter.rs 注册;适用于同一 kind 在不同部署模式下映射到不同底层实现。
  • 工程工具(wp-proj)一致性crates/wp-proj 中的 SourcesSinksWplOml 管理器会读取 EngineConfig(即 conf/wparse.toml)中的 src_root/sink_root/rule_root/oml_root,因此 connector 初始化必须保证这些路径正确。wp-proj 的 CLI 在调用 init/check 时直接依赖这些路径,无需再手工推断目录。

Source/Sink 实现步骤

  1. 先建立静态 Spec
    • 在 Source 端,以 FileSourceSpec 为例,它负责从 ResolvedSourceSpec 中提取路径/编码/实例数并完成参数校验;validate_specbuild 都仅调用 from_resolved,防止重复解析(src/sources/file/factory.rs:15-123)。
    • 在 Sink 端同理,TcpSinkSpec 负责提取地址/端口/分帧信息并校验布尔开关;后续连接逻辑只接收 TcpSinkSpec,避免直接访问动态 Map(src/sinks/backends/tcp.rs:18-105)。
  2. validate_spec 中仅做转换:始终让 validate_spec 里只调用一次 Spec::from_*,把所有错误统一转成 SourceReason::from_confSinkError,确保 CLI/控制面可以直接提示参数问题(src/sources/file/factory.rs:73-82src/sinks/backends/tcp.rs:240-259)。
  3. build 中复用静态 Specbuild 里禁止再次从 params 中取值,直接使用 Spec 产物,并在需要时注入上下文(如 SourceBuildCtx 的路径/副本信息或 SinkBuildCtx 的限速值)。
  4. 注册工厂:实现完成后,在相应模块提供 register_* 函数并在 connectors/startup 中调用。FileSource 通过 register_factory_only 注册到全局表,是最简示例(src/sources/file/factory.rs:126-129)。
  5. 保持日志可读:网络类实现应在首次连接、首个包、错误等关键点打印 info!/warn!(可参考 TcpSink::connectSyslogSourceFactory 中的日志调用)。
  6. 确保 EngineConfig 可解析:因为 wp-proj 的管理命令和 CLI 检查都直接加载 conf/wparse.toml 来定位 topology/models 路径,connector 的默认模板、示例配置必须与 EngineConfig 中的路径保持一致。一旦添加新的默认目录或模板,需要同步更新 wparse.toml 示例和 EngineConfig::init 的默认字段。

参数校验与 Spec 转换建议

  • 一次性检查:尽量用 anyhow::ensure! 或模式匹配在 Spec 构造时完成所有合法性检查,再把 anyhow::Error 映射回连接器的 Reason(示例:TcpSourceSpec::from_params 对端口/缓冲区/帧模式/实例数的校验,见 src/sources/tcp/config.rs:4-63)。
  • 集中处理布尔或枚举参数:布尔开关使用 as_bool() 并在 Spec 层给出默认值;枚举按 to_ascii_lowercase() 匹配,防止大小写问题(示例:SyslogSourceSpec 中的 protocolheader_mode,见 src/sources/syslog/config.rs:7-74)。
  • 先校验标签:Source 实现通常需要在 validate_spec 开头调用 wp_data_model::tags::validate_tags,并在 build 时通过 parse_tags 生成 TagSetsrc/sources/file/factory.rs:73-123)。

启动与诊断

  • 集中注册:确保新工厂的 register_*connectors/startup::init_runtime_registries 中被调用,否则 CLI 虽能解析配置,但运行期无法找到对应 kind。
  • 列出注册结果:通过 connectors/startup::log_registered_kinds 可以快速查看当前进程加载的 Source/Sink,若出现找不到的 kind,优先检查是否忘记注册或重复注册(src/connectors/startup.rs:25-42)。
  • 适配器使用场景:如果需要把同一种 connector kind 映射到多个 factory(比如企业版扩展),在 adapter.rs 注册 ConnectorKindAdapter,再由业务层读取 list_kinds() 决定要启用的适配路径(src/connectors/adapter.rs:1-43)。

测试策略

  • 工厂级单元测试:所有新工厂都应像 File/Tcp/Syslog 一样包含 #[cfg(test)] 模块,验证参数校验、实例数量、Tag 注入等关键路径。例如 file::factory 中的 build_spec_with_instancescompute_file_ranges_aligns_to_line_boundaries 等用例(src/sources/file/factory.rs:188-266)。
  • 端到端验证:网络类 Source/Sink 建议提供受控的 e2e 测试,配合条件变量(如 WP_NET_TESTS)运行真实 TCP/UDP 循环,参考 src/sinks/backends/tcp.rs:287-356src/sources/tcp/conn/connection.rs:500-552 的用例。
  • 保持幂等:测试/工具函数不应依赖全局状态,使用 register_* 时若会污染全局 registry,要在测试结束后清理或使用隔离的 runner。

提交流程提示

  1. 文档更新:当新 connector 引入新的 CLI/配置参数,需同步更新 docswpgen 模板以及任何 CLI 帮助文本。
  2. 代码规范:遵守 Rustfmt、Clippy 以及仓库指引(宏/特性集中定义、错误提示使用 SourceReason/SinkReason)。
  3. 日志与可观测性:一旦连接建立、首包发送或异常发生应输出 info!/warn!,便于排查跨机问题。
  4. 注册核查:PR 提交前检查 connectors/startup.rs 是否包含新工厂的注册逻辑,并在日志里确认可见。

遵循以上步骤,新 connector 可以快速接入 engine,并保持配置、诊断与回归测试的统一体验。