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 日志 日志收集器 Filebeat / Fluentd Elasticsearch 存储 + 索引 实时监控 Kibana / Grafana 告警与自动化 ElastAlert / 自研脚本 字段解析:event_type → 过滤 | delta → 排序 | greeks → 风控计算

总结一下

结构化日志,说白了就是让日志从「文本」变成「数据」。JSON 格式是最简单、最通用的方式。

字段设计上,记住三个原则:

  1. 核心字段必须精炼:event_type、order_id、price、delta 这些是标配
  2. Greeks 采用「独立+嵌套」双模式:兼顾查询效率和数据的完整性
  3. event_type 用字符串命名:可读性优先,别用数字编号

一个小建议:刚开始做结构化日志时,不要追求完美。先定 5-6 个核心字段跑起来,后面再慢慢加。我见过太多团队在字段设计上纠结一个月,结果系统都上线了日志还没改好。

嗯,今天就聊到这里。下一节我们会聊聊日志的采集和传输,看看怎么把海量的 JSON 日志高效地送到存储系统里。


专注资料整理