4、结构化日志:JSON格式日志的优势,字段设计
日志这东西,很多做量化的人一开始都不重视。
我早年做期权做市系统时,也踩过这个坑。那时候日志全是自由文本,什么「订单已发送」、「成交了,价格不错」这种话。结果呢?线上出问题,grep 半天,眼睛都快看瞎了,还是找不到根因。
后来我痛定思痛,全面转向了 JSON 格式的结构化日志。今天就跟大家聊聊,为什么 JSON 日志是做市系统的标配,以及字段到底该怎么设计。
为什么是 JSON?
说白了,自由文本日志是给人看的,但生产环境里,日志主要是给机器和脚本看的。
你想想看,做市系统一秒钟可能产生上千条日志。人工一条条翻?不现实。我们需要的是程序能快速解析、过滤、聚合的数据。
JSON 的优势很明显:
- 自描述:每个字段都有名字,不用猜「第3列是什么」
- 易解析:几乎所有编程语言都有现成的 JSON 库
- 可扩展:加字段不影响老系统,兼容性好
- 嵌套结构:可以表达复杂的数据关系,比如 Greeks 作为一个子对象
核心观点:JSON 日志不是「锦上添花」,而是「雪中送炭」。没有结构化日志,做市系统的监控和排障基本靠猜。
字段设计:少即是多
我见过有人把日志字段搞到三四十个,结果大部分字段都是空的。嗯,这里要注意:字段不是越多越好。
我个人习惯,核心字段控制在 10 个以内。下面这几个是做市系统必须有的:
| 字段名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| timestamp | string (ISO 8601) | 事件发生时间,精确到毫秒 | 2025-03-15T10:30:00.123Z |
| event_type | string | 事件类型,如 order_new, trade_fill, greeks_update | order_new |
| order_id | string | 订单唯一标识 | ORD-20250315-001234 |
| symbol | string | 合约代码 | BTC-20250328-60000-C |
| side | string | 买卖方向 | buy / sell |
| price | float | 订单价格或成交价格 | 0.0450 |
| quantity | int | 数量 | 10 |
| delta | float | 期权 Delta 值 | 0.5234 |
| greeks | object | 完整的 Greeks 信息 | {"delta":0.52, "gamma":0.03, "vega":0.15, "theta":-0.02} |
| message | string | 人类可读的描述(可选) | 新订单已提交,等待成交 |
我的经验:timestamp 一定要用 ISO 8601 格式,带时区信息。我曾经因为时区问题,排查了整整一个下午才发现日志时间差了 8 小时。从那以后,我所有系统统一用 UTC 时间。
event_type 的设计原则
event_type 是日志的灵魂。它决定了你后续怎么过滤、怎么聚合。
我建议采用「动词_名词」的命名方式,清晰明了:
- order_new:新订单创建
- order_cancel:订单撤销
- order_reject:订单被交易所拒绝
- trade_fill:订单部分或全部成交
- greeks_update:Greeks 重新计算
- risk_check:风控检查通过或失败
- position_change:持仓变化
为什么不用数字编号?比如 event_type=1 代表订单创建?
我试过,后来发现每次查日志都要翻文档,太痛苦了。字符串虽然占点空间,但可读性提升是巨大的。
Greeks 字段:是展开还是嵌套?
这是一个常见的纠结点。Delta、Gamma、Vega、Theta,是拆成 4 个独立字段,还是放在一个 greeks 对象里?
我的建议是:两者都要。
- delta 作为独立字段:因为它是做市系统最常用的 Greeks 值,查询频率极高。独立出来方便快速过滤和排序。
- greeks 作为嵌套对象:包含完整的 Greeks 信息,方便需要全部 Greeks 的场景,比如风控计算。
这样做的好处是:
- 简单查询(比如「所有 delta > 0.5 的订单」)可以直接用 delta 字段,速度快
- 复杂分析(比如「计算整个组合的 Greeks」)可以用 greeks 对象,数据完整
避坑指南:我曾经把 Greeks 全部展开成独立字段,结果日志行变得巨长,而且每次加一个新 Greeks 指标都要改日志格式。后来改成嵌套对象,灵活多了。但注意,嵌套不要太深,两层就够了,否则解析起来也麻烦。
代码示例:Python 结构化日志
下面是一个简单的 Python 示例,展示如何输出 JSON 格式的日志:
import json
import logging
from datetime import datetime, timezone
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# 如果有额外的字段,合并进去
if hasattr(record, 'extra_fields'):
log_entry.update(record.extra_fields)
return json.dumps(log_entry)
# 使用示例
logger = logging.getLogger("option_maker")
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# 记录一条订单日志
logger.info("新订单已提交", extra={
"extra_fields": {
"event_type": "order_new",
"order_id": "ORD-20250315-001234",
"symbol": "BTC-20250328-60000-C",
"side": "buy",
"price": 0.0450,
"quantity": 10,
"delta": 0.5234,
"greeks": {
"delta": 0.5234,
"gamma": 0.0312,
"vega": 0.1523,
"theta": -0.0215
}
}
})
输出结果:
{"timestamp": "2025-03-15T10:30:00.123456+00:00", "level": "INFO", "logger": "option_maker", "message": "新订单已提交", "event_type": "order_new", "order_id": "ORD-20250315-001234", "symbol": "BTC-20250328-60000-C", "side": "buy", "price": 0.045, "quantity": 10, "delta": 0.5234, "greeks": {"delta": 0.5234, "gamma": 0.0312, "vega": 0.1523, "theta": -0.0215}}
结构化日志的核心逻辑
下面这张图,展示了从日志产生到最终监控的完整链路:
总结一下
结构化日志,说白了就是让日志从「文本」变成「数据」。JSON 格式是最简单、最通用的方式。
字段设计上,记住三个原则:
- 核心字段必须精炼:event_type、order_id、price、delta 这些是标配
- Greeks 采用「独立+嵌套」双模式:兼顾查询效率和数据的完整性
- event_type 用字符串命名:可读性优先,别用数字编号
一个小建议:刚开始做结构化日志时,不要追求完美。先定 5-6 个核心字段跑起来,后面再慢慢加。我见过太多团队在字段设计上纠结一个月,结果系统都上线了日志还没改好。
嗯,今天就聊到这里。下一节我们会聊聊日志的采集和传输,看看怎么把海量的 JSON 日志高效地送到存储系统里。