skills/logging-best-practices/SKILL.md
应用程序日志记录最佳实践指南。涵盖日志目标设定、级别使用、结构化日志、有意义的日志条目、日志采样、规范日志行、日志聚合、保留策略、安全防护、敏感数据处理、性能影响、监控边界等全场景指导。当用户在项目中需要设计、优化或审查日志记录策略时使用此技能。
npx skillsauth add cruldra/skills logging-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
本技能提供应用程序日志记录的全面指导,从日志策略设计到具体实现,覆盖日志格式化、结构化、安全性、性能优化等实战场景。
| 实践 | 影响 | 难度 | |------|------|------| | 建立清晰的日志目标 | ⭐⭐⭐⭐⭐ | ⭐⭐ | | 正确使用日志级别 | ⭐⭐⭐⭐⭐ | ⭐ | | 结构化你的日志 | ⭐⭐⭐⭐⭐ | ⭐⭐ | | 编写有意义的日志条目 | ⭐⭐⭐ | ⭐⭐⭐⭐ | | 采样你的日志 | ⭐⭐⭐⭐ | ⭐⭐ | | 使用规范日志行 | ⭐⭐⭐⭐ | ⭐⭐ | | 聚合和集中化你的日志 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | | 建立日志保留策略 | ⭐⭐⭐ | ⭐⭐ | | 保护你的日志 | ⭐⭐⭐⭐⭐ | ⭐⭐ | | 不要记录敏感数据 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | 不要忽视日志的性能开销 | ⭐⭐⭐ | ⭐⭐⭐ | | 不要用日志做监控 | ⭐⭐⭐ | ⭐ |
在开始记录日志之前,先回答以下问题:
核心原则:日志的目的不仅是记录错误,而是让错误能被解决。记录错误详情和导致错误的事件链,提供帮助诊断根本原因的叙事。
# ❌ 错误:记录一切,没有目标
logger.info("进入函数")
logger.info("变量 x = 42")
logger.info("退出函数")
# ✅ 正确:围绕业务目标记录
logger.info("订单创建成功", order_id=order.id, user_id=user.id, amount=order.total)
logger.error("支付处理失败", order_id=order.id, error=str(e), retry_count=attempt)
建议:初期宁可多记一些日志,然后建立定期审查流程来调整日志级别,识别和修正过于冗长或缺失的日志。
| 级别 | 用途 | 示例 |
|------|------|------|
| TRACE | 最细粒度的调试信息 | 函数参数值、循环迭代 |
| DEBUG | 开发调试时的详细信息 | SQL 查询、缓存命中/未命中 |
| INFO | 重要的业务事件 | 用户登录、订单创建、服务启动 |
| WARN | 异常情况,可能预示未来问题 | 磁盘空间不足、重试操作、弃用 API 调用 |
| ERROR | 影响特定操作的不可恢复错误 | 数据库连接失败、第三方 API 超时 |
| FATAL | 影响整个程序的不可恢复错误 | 配置文件缺失、端口被占用 |
关键要点:
INFO 级别# ✅ 级别使用正确
logger.info("用户登录成功", user_id=user.id)
logger.warning("缓存未命中率过高", miss_rate=0.85, threshold=0.7)
logger.error("数据库连接超时", host=db_host, timeout_ms=5000)
# ❌ 级别使用错误
logger.error("用户登录成功") # 这不是错误
logger.info("数据库连接失败") # 这应该是 error
logger.debug("订单创建完成") # 重要业务事件应该是 info
非结构化日志(避免在生产环境使用):
[2023-11-03 08:45:33,123] ERROR: Database connection failed: Timeout exceeded.
半结构化日志:
2023-06-28 19:09:48.801818 I [969609:60] MyApp -- Starting application on port 3000
结构化日志(推荐):
{
"timestamp": "2023-06-28T17:20:19.409882Z",
"level": "info",
"pid": 982617,
"name": "MyApp",
"message": "Starting application on port 3000"
}
http {
log_format custom_json escape=json
'{'
'"timestamp":"$time_iso8601",'
'"pid":"$pid",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request":"$request",'
'"status": "$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time_ms":"$request_time",'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.json custom_json;
}
开发环境:可使用彩色化、易读的半结构化格式;生产环境:默认使用 JSON 结构化输出。
// ❌ 缺乏上下文
{
"timestamp": "2023-11-06T14:52:43.123Z",
"level": "INFO",
"message": "Login attempt failed"
}
// ✅ 包含足够上下文
{
"timestamp": "2023-11-06T14:52:43.123Z",
"level": "INFO",
"message": "Login attempt failed due to incorrect password",
"user_id": "12345",
"source_ip": "192.168.1.25",
"attempt_num": 3,
"request_id": "xyz-request-456",
"service": "user-authentication",
"device_info": "iPhone 12; iOS 16.1",
"location": "New York, NY"
}
原则:为未来的自己编写日志——消息要清晰、信息丰富,精确描述被捕获的事件。
对于每天产生数百 GB 或 TB 级数据的系统,日志采样是关键的成本控制策略。
// Go + zerolog 示例:每 5 条日志只保留 1 条
log := zerolog.New(os.Stdout).
With().
Timestamp().
Logger().
Sample(&zerolog.BasicSampler{N: 5})
| 策略 | 说明 | 适用场景 | |------|------|----------| | 固定比例采样 | 每 N 条保留 1 条 | 高流量、低差异的日志 | | 基于内容的采样 | 根据日志内容调整采样率 | 错误日志保留更多,信息日志保留更少 | | 基于级别的采样 | 不同级别不同采样率 | ERROR 全保留,INFO 采样 | | 选择性跳过 | 某些类别跳过采样 | 审计日志不采样 |
关键:尽早引入日志采样,不要等到成本已经成为问题。优先在应用层实现采样(如果框架支持),其次在日志管道中实现。
每个请求结束时创建一条唯一的、全面的日志条目,汇总该请求的所有关键信息。
{
"http_verb": "POST",
"path": "/user/login",
"source_ip": "203.0.113.45",
"user_agent": "Mozilla/5.0 ...",
"request_id": "req_98765",
"response_status": 500,
"error_id": "ERR500",
"error_message": "Internal Server Error",
"oauth_application": "AuthApp_123",
"user_id": "user_789",
"service_name": "AuthService",
"git_revision": "7f8ff286cda761c340719191e218fb22f3d0a72",
"request_duration_ms": 320,
"database_time_ms": 120,
"rate_limit_remaining": 99,
"rate_limit_total": 100
}
优势:排查失败请求时只需查看一条日志,包含输入参数、调用者身份、数据库查询次数、耗时信息、速率限制等。
在分布式系统中,将所有日志汇聚到集中式日志管理系统,创建单一、可搜索的真相来源:
| 日志类别 | 建议保留期限 | 说明 | |----------|-------------|------| | 审计日志 | 1-7 年 | 法规合规要求 | | 错误日志 | 30-90 天 | 故障排查需要 | | 应用日志 | 7-30 天 | 日常运维 | | 调试日志 | 1-7 天 | 临时排查 |
关键措施:
在应用层面隐藏敏感信息,即使包含敏感字段的对象被记录,也确保信息被省略或匿名化。
// Go slog 示例:实现 LogValuer 接口控制日志输出
type User struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
}
// 只暴露 ID,隐藏所有其他字段
func (u *User) LogValue() slog.Value {
return slog.StringValue(u.ID)
}
// 输出:{"user":"user-12234"} —— 密码和邮箱不会泄露
# Python structlog 示例:自定义处理器脱敏
import structlog
import re
SENSITIVE_PATTERNS = {
"password": r".*",
"token": r".*",
"email": r"(.).*@", # 保留首字母
"phone": r"(\d{3})\d{4}(\d{4})", # 保留前3后4
}
def redact_sensitive_fields(logger, method_name, event_dict):
for key, pattern in SENSITIVE_PATTERNS.items():
if key in event_dict:
if key in ("password", "token"):
event_dict[key] = "***REDACTED***"
elif key == "email":
event_dict[key] = re.sub(pattern, r"\1***@", event_dict[key])
elif key == "phone":
event_dict[key] = re.sub(pattern, r"\1****\2", event_dict[key])
return event_dict
structlog.configure(processors=[redact_sensitive_fields, ...])
| 方案 | 请求/秒 | 性能影响 | |------|---------|---------| | 无日志 | ~192k | 基准 | | Logrus(JSON) | ~153k | -20% | | Slog(JSON) | ~153k → ~187k | -3% |
日志只捕获预定义的事件和错误,不适合趋势分析和异常检测。应使用**指标(metrics)**回答以下问题:
| 用途 | 适合工具 | 不适合 | |------|----------|--------| | 趋势分析 | 指标(Prometheus、Grafana) | 日志 | | 异常检测 | 指标 + 告警规则 | 日志 | | 实时仪表盘 | 指标 | 日志 | | 故障排查 | 日志 + 链路追踪 | 仅指标 | | 审计追踪 | 日志 | 指标 |
| 实践 | 影响 | 难度 | |------|------|------| | 使用 JSON 结构化日志 | ⭐⭐⭐⭐⭐ | ⭐⭐ | | 统一使用字符串日志级别 | ⭐⭐⭐ | ⭐ | | 时间戳使用 ISO-8601 格式 | ⭐⭐⭐⭐ | ⭐⭐ | | 包含日志来源信息 | ⭐⭐⭐ | ⭐ | | 添加构建版本或 Git commit hash | ⭐⭐⭐ | ⭐ | | 错误日志包含堆栈追踪 | ⭐⭐⭐⭐⭐ | ⭐ | | 标准化上下文字段 | ⭐⭐⭐ | ⭐⭐⭐ | | 使用关联 ID 分组相关日志 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | | 选择性记录对象字段 | ⭐⭐⭐ | ⭐⭐⭐ |
不同框架的整数级别映射不同(如 Pino 中 60 = FATAL,Go slog 中 8 = ERROR),会造成混淆。
// ❌ 整数级别,不同框架含义不同
{"level": 60, "msg": "Fatal message"}
// ✅ 字符串级别,含义明确
{"level": "FATAL", "msg": "Fatal message"}
// ✅ ISO-8601 格式,人类可读且无歧义
2023-09-10T12:34:56.123456789Z
2023-11-17T12:34:56.123456789+08:00
// ❌ 其他格式
1687927940843 // Unix 时间戳,不可读
06/28/2023 04:49:48 // 美式日期,有歧义
规范:将时间戳统一标准化为 UTC,如需本地时区则带偏移量。
{
"time": "2023-05-24T19:39:27.005Z",
"level": "DEBUG",
"source": {
"function": "main.main",
"file": "app/main.go",
"line": 30
},
"msg": "Debug message"
}
在分布式系统中还应包含:主机名、容器 ID 等标识信息。
{
"time": "2023-06-29T06:37:38.429Z",
"level": "ERROR",
"msg": "an unexpected error",
"build_info": {
"go_version": "go1.20.2",
"commit_hash": "9b0695e1c4732a2ea2c8ac678472c4c3c235101b"
}
}
作用:代码重构后,仍能通过 commit hash 精确回溯到日志产生时的代码状态。
// ✅ 结构化堆栈追踪(优先选择)
{
"level": "error",
"message": "Cannot divide one by zero!",
"exception": [
{
"exc_type": "ZeroDivisionError",
"exc_value": "division by zero",
"frames": [
{
"filename": "/app/main.py",
"lineno": 16,
"name": "<module>"
}
]
}
]
}
// ✅ 字符串堆栈追踪(可接受)
{
"level": "ERROR",
"message": "division by zero",
"exc_info": "Traceback (most recent call last):\n File \"app/main.py\", line 24\n 1 / 0\nZeroDivisionError: division by zero"
}
# ✅ 使用键值对,而非嵌入消息字符串
slog.Info("User logged in", slog.Int("user_id", 42))
# ❌ 将上下文嵌入字符串
log.Println("User '" + user.id + "' logged in")
命名规范:
| 规则 | 示例 |
|------|------|
| 统一字段名,避免同义词 | user_id(不要混用 user、userId、userID) |
| 数值字段名包含单位 | execution_time_ms、response_size_bytes |
| 使用 snake_case | request_id、source_ip |
在基础设施边缘生成关联 ID,贯穿整个请求生命周期:
{
"timestamp": "2023-09-10T15:30:45.123456Z",
"correlation_id": "9ea8f2b4-639e-4de7-b406-f6cd3a155e9f",
"level": "INFO",
"message": "Received incoming HTTP request",
"request": {
"method": "GET",
"path": "/api/resource",
"remote_address": "192.168.1.100"
}
}
// Go:实现 LogValuer 接口
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
func (u *User) LogValue() slog.Value {
return slog.StringValue(u.ID)
}
// 只记录 ID,防止密码和邮箱泄露
# Python:自定义 __repr__ 或 __str__
class User:
def __init__(self, id, name, email, password):
self.id = id
self.name = name
self.email = email
self.password = password
def __repr__(self):
return f"User(id={self.id})"
设计或审查日志策略时逐项确认:
策略层面:
实现层面:
安全层面:
运维层面:
testing
智能体 UAT 验收测试技能。用于验证智能体在真实场景下的表现是否满足预期。支持任意智能体框架(langchain、langgraph、deepagents、crewai 等)。触发词:测试智能体、验收测试、agent test、UAT
tools
Use when you need to create a Gitea issue, update its spec/plan markers, read or merge an issue's state JSON, or post a PR review comment in a repo that uses the spx CLI (superpowers-vscode workflow).
development
Use when implementing, modifying, refactoring, or reviewing code and the agent must follow explicit coding standards for simplicity, readability, maintainability, testability, project conventions, and minimal safe changes.
development
Use when integrating the deepagents SDK into a Python project — creating agents, configuring backends, adding subagents, middleware, memory, or skills. Also use when debugging deepagents agents or choosing between StateBackend, FilesystemBackend, and LocalShellBackend.