structured-logging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Structured Logging Skill

结构化日志技能

Two tiers. Tier 1 is non-negotiable. Tier 2 requires judgment. Rule 0 overrides both.
分为两个层级。第一层级为强制要求,第二层级需酌情判断。规则0优先级高于两者。

Rule 0 — Follow the project first, always

规则0 — 始终优先遵循项目现有规范

Before adding any log, check the codebase: existing logger (Winston/Pino, structlog/loguru, Serilog/zap…)? Existing field naming (
user_id
vs
userId
)? Existing redaction/PII-scrubbing utility? Existing audit-log pipeline (e.g. a dedicated
audit_logs
table/service instead of stdout)? If the project already has a pattern for something below, use it — it wins over every default in this skill. Only apply the defaults below on a new project, or where the project has no established pattern yet. Never rename an established field (
status_code
statusCode
); reuse it.
Preserve the pipeline: never bypass, replace, or wrap existing logging infrastructure — transports, formatters, tracing integrations, audit pipelines. Don't
JSON.stringify()
into a logger that already accepts structured objects, don't introduce a second logger, don't fall back to
console.log
/
print
/
fmt.Println
"just this once."
This skill covers backend/service code (APIs, jobs, workers). Client-side/UI logging is out of scope.
添加任何日志前,请先检查代码库:是否已有日志工具(Winston/Pino、structlog/loguru、Serilog/zap…)?是否已有字段命名规范(
user_id
还是
userId
)?是否已有脱敏/PII清理工具?是否已有审计日志流水线(例如专用的
audit_logs
表/服务而非标准输出)?**如果项目已针对以下内容制定了规范,请遵循该规范——它优先于本技能中的所有默认规则。**仅在新项目或项目无既定规范时,才应用以下默认规则。切勿重命名已有的字段(如
status_code
改为
statusCode
),请直接复用。
**保留现有流水线:**切勿绕过、替换或包装现有的日志基础设施——包括传输器、格式化器、追踪集成、审计流水线。若日志工具已支持结构化对象,请勿使用
JSON.stringify()
;切勿引入第二个日志工具;切勿“仅此一次”退而使用
console.log
/
print
/
fmt.Println
本技能适用于后端/服务代码(API、任务、工作进程)。客户端/UI日志不在本技能范围内。

Tier 1 — Mandatory: catch and log every exception

第一层级 — 强制要求:捕获并记录所有异常

  • Every exception that can occur MUST be caught (locally, or at a global unhandled-exception boundary) and logged. Never let it disappear silently or be re-thrown without a log.
  • Exception log must include:
    op
    (operation/function name),
    error
    (
    message
    ,
    type
    ,
    stack
    ),
    correlation_id
    , and a
    state_dump
    of the key variables present at failure time — redacted per the internal-state rule below (blocklist + auto-mask), not the external-input allowlist. The goal: explain why it failed, not just that it failed.
  • Level:
    ERROR
    for unhandled/system failures (5xx, DB/queue down, panics).
    WARN
    for expected/recoverable failures (validation errors, 4xx, failed-then-retried). Never log expected user-input errors as
    ERROR
    — it drowns real alerts.
  • This tier applies everywhere, always. No judgment call needed.
  • 所有可能发生的异常必须被捕获(本地捕获或通过全局未处理异常边界捕获)并记录。切勿让异常无声消失,或未记录就重新抛出。
  • 异常日志必须包含:
    op
    (操作/函数名称)、
    error
    message
    type
    stack
    )、
    correlation_id
    ,以及故障发生时关键变量的
    state_dump
    ——需遵循下文内部状态规则进行脱敏(黑名单+自动掩码),而非外部输入的白名单规则。目标是解释为何失败,而非仅记录发生了失败。
  • 日志级别:未处理/系统故障(5xx、数据库/队列宕机、程序崩溃)使用
    ERROR
    ;预期可恢复故障(验证错误、4xx、失败后重试)使用
    WARN
    。切勿将预期的用户输入错误记录为
    ERROR
    ——这会淹没真正的告警信息。
  • 本层级适用于所有场景,无需判断。

Tier 2 — Judgment-based: log what could actually cause hard-to-find bugs

第二层级 — 酌情判断:记录可能导致难以排查的Bug的内容

Categories below are hints about where risk tends to live, not a checklist to log every instance of. Before adding a non-error log line, ask: "If this specific piece of code produces a wrong result, will I need this log line to figure out why?" Only add it if the honest answer is yes.
Lean toward logging: untrusted/unvalidated input, logic that can silently produce a wrong result without throwing, non-trivial multi-condition branching, an external call that can fail/be slow, irreversible or costly side effects (money, hard-to-undo state changes).
Lean toward not logging: getters/setters/simple mappers, a branch so simple the code already makes it obvious, per-iteration logging inside a loop.
When you do log, capture decision-relevant values, not the fact code ran (
branch="loyalty_bonus", discount=15
not
"entering calculateDiscount"
); for state mutations, capture before → after → trigger.
Loop policy: never log
INFO
/
DEBUG
per iteration.
WARN
/
ERROR
per item IS allowed when that item has its own try/catch and needs independent investigation (e.g. batch import). Always end the loop with one summary log (
total_processed
,
total_failed
, up to 5
sample_failed_ids
).
Audit vs operational: business actions with legal/compliance weight (money moved, permission changed, record deleted, admin action) are audit events — emit immediately,
INFO
/
WARN
, never sampled, never truncated beyond secret-masking. Everything else is operational and may be sampled/ truncated/batched into a wide event as below.
Self-check on log count, not a hard cap: if a function ends up with more than ~5 log lines, stop and re-justify each one against the question above — don't cut a legitimately-needed log just to hit a number, and don't keep a weak one just because you're under budget.
以下分类是风险高发区域的提示,而非需要逐一记录的清单。**添加非错误日志行前,请自问:“如果这段代码产生错误结果,我是否需要这条日志来排查原因?”**只有当答案为是时,才添加该日志。
倾向于记录:不可信/未验证的输入、可能无声产生错误结果而不抛出异常的逻辑、复杂的多条件分支、可能失败/缓慢的外部调用、不可逆或高成本的副作用(资金变动、难以撤销的状态变更)。
倾向于不记录:getter/setter/简单映射器、逻辑简单到代码已清晰体现的分支、循环内的逐次日志。
记录时,请捕获与决策相关的值,而非代码执行的事实(如记录
branch="loyalty_bonus", discount=15
而非“进入calculateDiscount函数”);对于状态变更,请捕获变更前→变更后→触发条件。
**循环规则:**切勿在循环体内记录
INFO
/
DEBUG
日志。当循环内的每个条目有独立的try/catch且需要单独排查时(如批量导入),允许为每个条目记录
WARN
/
ERROR
。循环结束后必须记录一条汇总日志(包含
total_processed
total_failed
,最多5个
sample_failed_ids
)。
审计日志 vs 操作日志:具有法律/合规权重的业务操作(资金转移、权限变更、记录删除、管理员操作)属于审计事件——需立即记录,级别为
INFO
/
WARN
,不得采样,除敏感信息掩码外不得截断。其他所有日志均为操作日志,可按下文所述进行采样/截断/合并为宽事件。
**日志数量自查(非硬性限制):**若一个函数最终包含超过约5条日志,请停止并重新根据上述问题验证每条日志的必要性——不要为了达标而删除真正需要的日志,也不要保留不必要的日志。

How to log (mechanics)

日志记录实操规范

  • Correlation & trace propagation: if OpenTelemetry/APM context already exists (
    trace_id
    /
    span_id
    ), reuse it — don't create a parallel
    correlation_id
    . Otherwise extract in priority order: W3C
    traceparent
    X-Request-ID
    /
    X-Correlation-ID
    header → generate a UUID. Every entry point (HTTP middleware, queue consumer, cron/CLI main) sets this once and passes it through the whole call chain, including outgoing HTTP/DB/queue calls.
  • Structured, not string-concatenated: one JSON object per line, fixed field names (project convention, default
    snake_case
    ).
  • Emit timing: normal successful flow accumulates into one "wide event" at exit. Errors, irreversible state changes, audit events, and failed external calls emit immediately with everything accumulated so far — don't wait, the process may crash first.
  • Streaming / long-lived connections (WebSocket, SSE, gRPC stream): "wide event at exit" doesn't apply — a connection can live for hours. Emit
    connected
    at start and
    disconnected
    at end; WARN/ERROR immediately per message/segment failure; keepalive = metric, not log. Full pattern in
    reference/domain-specific.md
    .
  • Redaction — hybrid, not one rule for everything:
    • External input / request payload / user-facing data → allowlist (default to not logging; explicitly list safe fields).
    • Internal state / exception context / local variables at crash time → blocklist + auto-mask (
      password|token|secret|authorization| credit_card|ssn|api_key|private_key|otp|pin
      , case-insensitive) + truncate strings > ~400 chars with
      original_length
      . This is what makes Tier 1's
      state_dump
      useful instead of empty.
    • Never dump a whole object (
      logger.info(user)
      ) — extract fields.
  • Collections: log
    count
    + ≤5 sample ids, never the full array.
  • Logger must be injectable (parameter/DI), not a hardcoded global, so tests can swap in a no-op logger.
  • Performance-critical path (marked
    # performance-critical
    ): only
    ERROR
    + minimal fields.
  • High-frequency INFO (health checks, polling): respect a configurable sampling rate — 100% of WARN/ERROR/audit, sampled success.
  • Metrics vs logging: high-frequency numeric/boolean observations (cache hit/miss, queue depth, heartbeat, per-request counters) belong in metrics, not logs — unless folded as one field into a single wide event already being emitted. Logging these at high frequency is a common way AI silently 10x's log volume.
  • Non-blocking: logging must never block the hot path — serialize/truncate large
    state_dump
    before emission.
  • High-cardinality: don't use raw URLs with ids, full emails, filenames, prompts, or UUIDs as index/search labels — see
    reference/field-dictionary.md
    for the rule and safe alternatives.
  • Targeted debug (optional): prefer enabling
    DEBUG
    only for a specific
    correlation_id
    /
    user_id
    allow-list rather than globally in production.
  • Business events: name
    domain.entity.action
    (
    order.payment.completed
    ); add a short human-readable
    msg
    field so logs stay skimmable by eye.
  • Output: single-line JSON to stdout (stderr for ERROR). Append ERROR/FATAL to
    logs.err.txt
    only when
    LOG_TO_FILE=true
    (local non-containerized dev) — never a hard requirement.
  • Domain-specific fields (LLM calls, idempotency keys, retry/backoff, transaction boundaries, feature flags, concurrency ids): see
    reference/domain-specific.md
    — don't inline these into every function by default, apply only when that domain is actually in play.
  • 关联与追踪传播:若已存在OpenTelemetry/APM上下文(
    trace_id
    /
    span_id
    ),请
    复用该上下文
    ——不要创建并行的
    correlation_id
    。否则按以下优先级提取:W3C
    traceparent
    X-Request-ID
    /
    X-Correlation-ID
    请求头 → 生成UUID。每个入口点(HTTP中间件、队列消费者、定时任务/CLI主程序)只需设置一次,并在整个调用链中传递,包括对外的HTTP/数据库/队列调用。
  • **结构化而非字符串拼接:**每行一条JSON对象,字段名固定(遵循项目规范,默认使用
    snake_case
    )。
  • 记录时机:正常成功流程在退出时合并为一条“宽事件”。错误、不可逆状态变更、审计事件、失败的外部调用需立即记录已累积的所有信息——切勿等待,进程可能提前崩溃。
  • 流/长连接(WebSocket、SSE、gRPC流):“退出时记录宽事件”规则不适用——连接可能持续数小时。连接建立时记录
    connected
    ,断开时记录
    disconnected
    ;消息/段失败时立即记录
    WARN
    /
    ERROR
    ;心跳属于指标,而非日志。完整模式请参考
    reference/domain-specific.md
  • 脱敏——混合规则,而非一刀切:
    • 外部输入/请求负载/面向用户的数据 → 白名单规则(默认不记录,明确列出安全字段)。
    • 内部状态/异常上下文/崩溃时的局部变量 → 黑名单+自动掩码(匹配
      password|token|secret|authorization|credit_card|ssn|api_key|private_key|otp|pin
      ,不区分大小写)+ 截断超过约400字符的字符串并记录
      original_length
      。这能让第一层级的
      state_dump
      发挥作用,而非为空。
    • 切勿直接记录整个对象(如
      logger.info(user)
      )——请提取所需字段。
  • **集合类型:**记录
    count
    + 最多5个样本ID,切勿记录完整数组。
  • 日志工具必须可注入(通过参数/依赖注入),而非硬编码的全局实例,以便测试时可替换为无操作日志工具。
  • 性能关键路径(标记为
    # performance-critical
    ):仅记录
    ERROR
    级别及最少字段。
  • 高频INFO日志(健康检查、轮询):遵循可配置的采样率——
    WARN
    /
    ERROR
    /审计事件保持100%采样,成功事件可采样。
  • 指标 vs 日志:高频数值/布尔型观测数据(缓存命中/未命中、队列深度、心跳、逐请求计数器)属于指标,而非日志——除非作为一个字段合并到已有的单条宽事件中。高频记录此类数据是AI悄悄让日志量增加10倍的常见原因。
  • **非阻塞:**日志记录不得阻塞核心路径——在记录前序列化/截断大型
    state_dump
  • **高基数:**切勿将包含ID的原始URL、完整邮箱、文件名、提示词或UUID用作索引/搜索标签——规则及安全替代方案请参考
    reference/field-dictionary.md
  • 定向调试(可选):在生产环境中,优先针对特定
    correlation_id
    /
    user_id
    白名单启用
    DEBUG
    级别,而非全局启用。
  • **业务事件:**命名格式为
    domain.entity.action
    (如
    order.payment.completed
    );添加简短的人类可读
    msg
    字段,以便日志可快速浏览。
  • **输出:**单行JSON输出到标准输出(ERROR级别输出到标准错误)。仅当
    LOG_TO_FILE=true
    (本地非容器化开发环境)时,才将ERROR/FATAL级别日志追加到
    logs.err.txt
    ——切勿将此作为硬性要求。
  • 领域特定字段(LLM调用、幂等键、重试/退避、事务边界、功能标志、并发ID):请参考
    reference/domain-specific.md
    ——默认不要将这些字段内联到每个函数中,仅在实际涉及该领域时应用。

Self-check before finishing a function

完成函数前的自查清单

  • Every possible exception path is caught and logged with
    state_dump
    (blocklist-redacted, not allowlist).
  • Every non-error log I added — could I explain in one sentence what failure it would help diagnose? If not, remove it.
  • No
    INFO
    /
    DEBUG
    inside a loop body; per-item
    WARN
    /
    ERROR
    only if justified, plus a summary line after the loop.
  • Audit events (money/permission/delete/admin) emitted immediately, not sampled or truncated.
  • No secrets/PII logged; external input uses allowlist, internal state uses blocklist + auto-mask.
  • Field names match the project's existing convention (Rule 0).
  • Reused existing OpenTelemetry/trace context instead of inventing a parallel
    correlation_id
    ?
  • High-frequency counters/booleans logged as metrics, not log lines?
  • Could someone else debug a real failure here using only these logs, without a debugger?
  • 所有可能的异常路径均已被捕获,并记录了经过黑名单脱敏(而非白名单)的
    state_dump
  • 我添加的每条非错误日志——能否用一句话说明它能帮助排查何种故障?若不能,请删除。
  • 循环体内无
    INFO
    /
    DEBUG
    日志;仅在合理情况下为每个条目记录
    WARN
    /
    ERROR
    ,且循环结束后有汇总日志。
  • 审计事件(资金/权限/删除/管理员操作)已立即记录,未被采样或截断。
  • 未记录敏感信息/PII;外部输入使用白名单规则,内部状态使用黑名单+自动掩码规则。
  • 字段名符合项目现有规范(规则0)。
  • 复用了已有的OpenTelemetry/追踪上下文,而非创建并行的
    correlation_id
  • 高频计数器/布尔型数据已作为指标记录,而非日志行?
  • 仅通过这些日志,其他人能否在不使用调试器的情况下排查实际故障?

Review Mode (auditing existing code / a PR, not generating new code)

审核模式(审核现有代码/PR,而非生成新代码)

Same tiers and checks above apply, plus specifically look for:
  • A bypassed pipeline:
    console.log
    /
    print
    /
    fmt.Println
    , a second logger instance, or
    JSON.stringify()
    into a structured-capable logger.
  • A new
    correlation_id
    created where OpenTelemetry/trace context was already available and should have been reused instead.
  • High-cardinality values used as indexed/searchable labels.
除上述层级和检查项外,还需特别关注:
  • 绕过现有流水线的情况:使用
    console.log
    /
    print
    /
    fmt.Println
    、第二个日志实例,或向支持结构化的日志工具传入
    JSON.stringify()
    结果。
  • 在已有OpenTelemetry/追踪上下文的情况下,创建了新的
    correlation_id
    而非复用现有上下文。
  • 将高基数值用作可索引/搜索的标签。