---
title: "结构化日志用能按字段搜索的日志格式和日志级别"
description: "结构化日志把每个值放进各自的字段，而不是塞在一句话里。我们把 1 笔订单用 3 种方式发到 LogBrew，并展示哪种日志格式能通过 order_id 再次找到。"
canonical: "https://logbrew.co/zh-Hans/guides/structured-logging"
last_updated: "2026-11-10"
---

# 结构化日志用能按字段搜索的日志格式和日志级别

结构化日志把每个值放进各自的字段，而不是塞在一句话里。我们把 1 笔订单用 3 种方式发到 LogBrew，并展示哪种日志格式能通过 `order_id` 再次找到。

2026年11月10日 用 Python 3.13.5和logbrew-sdk 0.1.29 实测

作者 [Furkan Erday](https://github.com/furkanerday). Furkan Erday 是 LogBrew 的创始人，每天用 Claude Code 和 Codex 开发 LogBrew。

Canonical: https://logbrew.co/zh-Hans/guides/structured-logging

## 哪一行日志能通过 `order_id` 再次找到

| id 放在哪里 | 文本搜索 | 字段筛选 |
| --- | --- | --- |
| 放在消息文本里 | 找到 | 未找到 |
| 放在 `extra={}` 字段里 | 未找到 | 找到 |
| 放在 `extra={}` 里嵌套的 dict 中 | 未找到 | 未找到 |

我们把 1 笔订单用 3 种方式经过 Python 处理器发送，然后去搜索它。满杯表示搜索找到了这一行。

## 选一种能筛选的日志格式

### 消息保持固定，把值放进字段

字段保留自己的名字和类型。消息文本里的值只是一个子串。在 Python 里，把字段放进 `extra={}`，这样每笔订单的消息都一样。

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### 有程序读日志时，就写 JSON 行

一个 `Formatter` 子类把每条记录变成每行 1 个 JSON 对象，包含标准字段和每个 `extra` 值。日志采集器不用正则就能解析这一行。

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

输出

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### 把字段发到 LogBrew

`LogBrewLoggingHandler` 把每条记录连同它的 `extra` 值作为字段发出去。我们把同一笔订单用 3 种方式发送，放在消息文本里、作为 `extra` 字段、作为 `extra` 里嵌套的 dict。

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

输出

```text
HTTP 200 accepted 3 logs
```

### 按字段再次找到这一行

`GET /api/logs` 接受 `attribute_filters`，由字段路径、类型、运算符和值组成。按 `order_id` 筛选只找到了用 `extra` 发送的那一行。`extra` 里嵌套的 dict 被丢弃了，因为处理器只保留字符串、数字、布尔值和 `None`。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

输出

```text
{"message":"order paid","order_id":"ord_42"}
```

## 在每个服务里用同样的方式使用日志级别

### 生产环境用 4 个级别

`info` 记录发生了什么，`warning` 记录出了问题但已处理的事，`error` 记录失败的请求或任务，`critical` 记录让服务停下的事。`debug` 留给本地运行。LogBrew 处理器把 DEBUG 记录作为 `info` 发送。

### 启动时设置 1 次级别

从环境变量读取级别，这样 1 台主机可以跑在 DEBUG，其余的保持 INFO。在 Python 里，`logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))` 就够了。

### 数字就按数字记录

`amount_cents: 1999` 保留了类型，所以筛选条件 `amount_cents >= 1000` 能匹配它。我们还把 `amount_cents: "1999"` 作为文本发送，数字筛选跳过了那一行。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

输出

```text
{"message":"order paid","amount_cents":1999}
```

## 我们实测看到的结果

我们把 1 笔订单用 3 种方式通过 Python 处理器发送，然后用 LogBrew API 搜索它。文本搜索 `search=ord_42` 匹配了 3 行中的 1 行，就是 id 在消息里的那行。

筛选条件 `order_id = ord_42` 匹配了 1 行，就是用 `extra={"order_id": "ord_42"}` 发送的那行。筛选条件 `amount_cents >= 1000` 匹配了同一行，因为处理器把 1999 保留为数字。

用 `extra={"order": {...}}` 发送的那行什么都没匹配到。处理器从 `extra` 里发送字符串、数字、布尔值和 `None`，跳过 dict 和 list。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

输出

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## 纯文本与 JSON 日志格式

纯 `%(message)s` 格式会丢掉所有 `extra` 值。下面这一行以 `INFO order paid` 结尾，没有订单 id，之后没人能找到这笔订单。

JSON 行保留字段及其类型，日志采集器不用正则就能解析。人读起来没那么顺，所以消息要短，字段名要稳定。

Python 没有内置的 JSON 格式器。python/cpython#77691 在 2018 年提出过这个需求，后来被关闭，因为文档已经展示了怎么自己写一个。上面那 15 行 `Formatter` 就是那份做法。

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

输出

```text
2026-11-10 08:34:04,955 INFO order paid
```

## LogBrew 记录了什么

每条日志保留级别、记录器名称和 `extra` 里的每个基本类型字段，再加上处理器 `metadata={}` 里的字段。API 按字段路径、类型和运算符筛选。文本接受 `eq`、`ne`、`contains` 和 `starts_with`，数字接受 `gt`、`gte`、`lt` 和 `lte`。

LogBrew 不会索引名字里含有 `password`、`secret`、`token`、`cookie` 或 `authorization` 的字段，所以筛选永远匹配不到它。不管怎样，别把敏感信息写进日志行。

在链路里发送的日志带着链路 ID 和 Span ID，所以按 `order_id` 筛选能找到产生这一行的那个请求。

## 常见问题

### 什么是结构化日志？

每个值都是带类型的命名字段的日志，以 JSON 或日志记录上的字段形式存在，而不是一句话里的词。搜索工具于是按字段筛选，而不是按子串。

### 日志级别按顺序是什么？

DEBUG、INFO、WARNING、ERROR、CRITICAL，在 Python 里对应数字 10 到 50。设为 INFO 的记录器放行 INFO 及以上，丢弃 DEBUG。

### 该用 JSON 还是纯文本记日志？

有程序读这些行时用 JSON，比如采集器、文件或搜索工具。只有人在终端读时用纯文本。两种格式保持相同的字段。

### 怎么给 Python 日志行添加字段？

在日志调用里传入 `extra={"order_id": "ord_42"}`。标准格式器会忽略 extra 字段。JSON 格式器或 LogBrew 处理器会保留它们。

### 在 LogBrew 里能按字段搜索日志吗？

能。`GET /api/logs` 最多接受 4 个 `attribute_filters`，每个由字段路径、类型和运算符组成，比如 `eq` 或 `gte`。

## 按任意字段筛选你的日志

LogBrew 保留你记录的每个字段，并按字段再次找到这一行，就在它所属的链路和问题旁边。免费额度充足，无需绑卡。

[免费开始](https://logbrew.co/zh-Hans)

## 参考来源

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## 更多智能体与 Python 指南

- [在 Claude Code 和 Codex 中添加 MCP 服务器](https://logbrew.co/zh-Hans/guides/mcp-server-claude-code-codex)
- [在 Claude Code 和 Codex 中使用 AGENTS.md](https://logbrew.co/zh-Hans/guides/agents-md)
- [解决 Claude overloaded 529 错误和 rate limit 429 错误](https://logbrew.co/zh-Hans/guides/claude-overloaded-rate-limit-error)
- [配置 Python 日志用对日志级别并捕获异常和回溯](https://logbrew.co/zh-Hans/guides/python-logging)
- [修复 Claude Code、Codex、JavaScript 和 Python 中的错误](https://logbrew.co/zh-Hans/guides)
