结构化 JSON 日志:JSONFormatter + 时间戳/级别/来源/消息/异常统一格式

一句话:结构化日志是将日志信息以结构化格式(如JSON)输出,便于后续分析和处理。JSON格式包含时间戳、级别、来源、消息、异常等统一字段。

1. 结构化日志基础

1.1 为什么需要结构化日志?


graph TD

    A[传统日志] --> B[非结构化文本]

    B --> C[难以解析]

    B --> D[难以搜索]

    B --> E[难以分析]

    F[结构化日志] --> G[JSON格式]

    G --> H[易于解析]

    G --> I[易于搜索]

    G --> J[易于分析]

    style F fill:#e8f5e8

传统日志问题

  • 非结构化文本,难以解析

  • 难以搜索和过滤

  • 难以进行统计分析

1.2 结构化日志优势

优势说明
易于解析JSON格式,程序可直接解析
易于搜索可按字段搜索和过滤
易于分析可进行统计分析
易于存储可存储到Elasticsearch等系统

2. JSONFormatter实现

2.1 基础实现

 
import logging
 
import json
 
from datetime import datetime
 
class JSONFormatter(logging.Formatter):
 
    def format(self, record):
 
        log_entry = {
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "level": record.levelname,
 
            "logger": record.name,
 
            "message": record.getMessage(),
 
            "module": record.module,
 
            "function": record.funcName,
 
            "line": record.lineno
 
        }
 
        # 添加异常信息
 
        if record.exc_info:
 
            log_entry["exception"] = self.formatException(record.exc_info)
 
        # 添加额外字段
 
        if hasattr(record, 'extra_data'):
 
            log_entry["extra"] = record.extra_data
 
        return json.dumps(log_entry, ensure_ascii=False)
 

2.2 使用示例

 
import logging
 
# 创建logger
 
logger = logging.getLogger("myapp")
 
logger.setLevel(logging.INFO)
 
# 创建handler
 
handler = logging.StreamHandler()
 
handler.setFormatter(JSONFormatter())
 
# 添加handler
 
logger.addHandler(handler)
 
# 使用logger
 
logger.info("用户登录", extra={"user_id": 123, "ip": "192.168.1.1"})
 
logger.error("数据库连接失败", exc_info=True)
 

3. 统一日志格式

3.1 标准字段

 
class StandardJSONFormatter(logging.Formatter):
 
    def format(self, record):
 
        log_entry = {
 
            # 时间戳
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "level": record.levelname,
 
            # 来源信息
 
            "logger": record.name,
 
            "module": record.module,
 
            "function": record.funcName,
 
            "line": record.lineno,
 
            # 消息内容
 
            "message": record.getMessage(),
 
            # 异常信息
 
            "exception": None,
 
            # 额外字段
 
            "extra": {}
 
        }
 
        # 异常信息
 
        if record.exc_info:
 
            log_entry["exception"] = {
 
                "type": record.exc_info[0].__name__,
 
                "message": str(record.exc_info[1]),
 
                "traceback": self.formatException(record.exc_info)
 
            }
 
        # 额外字段
 
        extra_fields = {
 
            k: v for k, v in record.__dict__.items()
 
            if k not in logging.LogRecord("").__dict__.keys()
 
            and not k.startswith("_")
 
        }
 
        log_entry["extra"] = extra_fields
 
        return json.dumps(log_entry, ensure_ascii=False, default=str)
 

3.2 使用示例

 
# 配置日志
 
logging.basicConfig(
 
    level=logging.INFO,
 
    handlers=[logging.StreamHandler()]
 
)
 
# 设置自定义formatter
 
handler = logging.StreamHandler()
 
handler.setFormatter(StandardJSONFormatter())
 
logger = logging.getLogger("myapp")
 
logger.addHandler(handler)
 
# 使用
 
logger.info("用户操作", extra={
 
    "user_id": 123,
 
    "action": "login",
 
    "ip": "192.168.1.1"
 
})
 

4. 高级功能

4.1 请求ID集成

 
import logging
 
import uuid
 
from contextvars import ContextVar
 
# 请求ID上下文变量
 
request_id_var: ContextVar[str] = ContextVar('request_id', default='')
 
class RequestContextFilter(logging.Filter):
 
    def filter(self, record):
 
        record.request_id = request_id_var.get('')
 
        return True
 
class RequestContextFormatter(logging.Formatter):
 
    def format(self, record):
 
        log_entry = {
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "level": record.levelname,
 
            "request_id": record.request_id,
 
            "logger": record.name,
 
            "message": record.getMessage()
 
        }
 
        if record.exc_info:
 
            log_entry["exception"] = self.formatException(record.exc_info)
 
        return json.dumps(log_entry, ensure_ascii=False)
 

4.2 性能日志

 
import time
 
import logging
 
class PerformanceLogger:
 
    def __init__(self, logger):
 
        self.logger = logger
 
    def log_operation(self, operation_name, func):
 
        """记录操作性能"""
 
        start_time = time.time()
 
        try:
 
            result = func()
 
            duration = time.time() - start_time
 
            self.logger.info(f"操作完成", extra={
 
                "operation": operation_name,
 
                "duration": duration,
 
                "status": "success"
 
            })
 
            return result
 
        except Exception as e:
 
            duration = time.time() - start_time
 
            self.logger.error(f"操作失败", extra={
 
                "operation": operation_name,
 
                "duration": duration,
 
                "status": "error",
 
                "error": str(e)
 
            })
 
            raise
 
# 使用
 
logger = logging.getLogger("performance")
 
perf_logger = PerformanceLogger(logger)
 
def slow_operation():
 
    time.sleep(1)
 
    return "result"
 
result = perf_logger.log_operation("slow_operation", slow_operation)
 

4.3 审计日志

 
import logging
 
import json
 
from datetime import datetime
 
class AuditLogger:
 
    def __init__(self, logger):
 
        self.logger = logger
 
    def log_user_action(self, user_id, action, details=None):
 
        """记录用户操作"""
 
        log_entry = {
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "event_type": "user_action",
 
            "user_id": user_id,
 
            "action": action,
 
            "details": details or {}
 
        }
 
        self.logger.info(json.dumps(log_entry, ensure_ascii=False))
 
    def log_system_event(self, event_type, details=None):
 
        """记录系统事件"""
 
        log_entry = {
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "event_type": event_type,
 
            "details": details or {}
 
        }
 
        self.logger.info(json.dumps(log_entry, ensure_ascii=False))
 
# 使用
 
audit_logger = AuditLogger(logging.getLogger("audit"))
 
audit_logger.log_user_action(123, "login", {"ip": "192.168.1.1"})
 

5. 实际案例

5.1 FastAPI集成

 
import logging
 
import json
 
from fastapi import FastAPI, Request
 
import uuid
 
app = FastAPI()
 
# 配置日志
 
logging.basicConfig(
 
    level=logging.INFO,
 
    format='%(message)s',
 
    handlers=[logging.StreamHandler()]
 
)
 
logger = logging.getLogger("fastapi")
 
@app.middleware("http")
 
async def logging_middleware(request: Request, call_next):
 
    # 生成请求ID
 
    request_id = str(uuid.uuid4())
 
    # 记录请求开始
 
    logger.info("请求开始", extra={
 
        "request_id": request_id,
 
        "method": request.method,
 
        "url": str(request.url)
 
    })
 
    # 处理请求
 
    response = await call_next(request)
 
    # 记录请求结束
 
    logger.info("请求结束", extra={
 
        "request_id": request_id,
 
        "status_code": response.status_code
 
    })
 
    return response
 

5.2 日志存储到Elasticsearch

 
from elasticsearch import Elasticsearch
 
import logging
 
import json
 
class ElasticsearchHandler(logging.Handler):
 
    def __init__(self, es_host, index_name):
 
        super().__init__()
 
        self.es = Elasticsearch(es_host)
 
        self.index_name = index_name
 
    def emit(self, record):
 
        log_entry = self.format(record)
 
        try:
 
            self.es.index(
 
                index=self.index_name,
 
                body=json.loads(log_entry)
 
            )
 
        except Exception as e:
 
            print(f"Failed to send log to Elasticsearch: {e}")
 
# 配置
 
es_handler = ElasticsearchHandler(
 
    es_host="localhost:9200",
 
    index_name="app-logs"
 
)
 
logger = logging.getLogger("myapp")
 
logger.addHandler(es_handler)
 

6. 常见坑点

1. 日志格式不一致

 
# 解决:统一使用JSONFormatter
 
handler = logging.StreamHandler()
 
handler.setFormatter(JSONFormatter())
 

2. 性能问题

 
# 解决:异步日志或批量发送
 
import asyncio
 
from concurrent.futures import ThreadPoolExecutor
 
executor = ThreadPoolExecutor(max_workers=4)
 
async def async_log(message):
 
    loop = asyncio.get_event_loop()
 
    await loop.run_in_executor(executor, logger.info, message)
 

3. 敏感信息泄露

 
# 解决:添加日志过滤器
 
class SensitiveDataFilter(logging.Filter):
 
    def filter(self, record):
 
        # 过滤敏感信息
 
        if hasattr(record, 'password'):
 
            record.password = '***'
 
        return True
 

核心要点

 
import logging
 
import json
 
class JSONFormatter(logging.Formatter):
 
    def format(self, record):
 
        log_entry = {
 
            "timestamp": datetime.utcnow().isoformat(),
 
            "level": record.levelname,
 
            "message": record.getMessage()
 
        }
 
        return json.dumps(log_entry)
 
# 使用
 
handler = logging.StreamHandler()
 
handler.setFormatter(JSONFormatter())
 
logger = logging.getLogger("myapp")
 
logger.addHandler(handler)
 
logger.info("测试消息")
 

速记卡(面试闪卡)

Q1:一句话讲清「结构化 JSON 日志:JSONFormatter + 时间戳/级别/来源/消息/异常统一格式」到底是什么?

A:结构化 JSON 日志是用统一字段的 JSON 格式输出日志,便于程序解析与检索分析。

Q2:2. JSONFormatter实现 —— 怎么理解?

A:像给日志发统一格式的名片:JSONFormatter 把时间/级别/来源/消息/异常打包成 JSON 字段(JSON Field),程序能直接解析,不像纯文本那样难搜。

Q3:3. 统一日志格式 —— 怎么理解?

A:像公司规定报销单模板:所有模块都用 timestamp/level/logger/message/exception 同一套标准字段(Standard Fields),跨服务才能统一检索。

Q4:4. 高级功能 —— 怎么理解?

A:像给每封信贴运单号:用 ContextVar 注入 request_id、加审计日志(Audit Log)、记耗时,让一次请求的全链路日志能串起来。

Q5:5. 实际案例 —— 怎么理解?

A:像中间件自动记账:FastAPI 中间件每次请求生成 request_id 并写日志,Elasticsearch(ES) 当仓库存 JSON 日志供检索。

Q6:核心速记主线有哪些?

  • 结构化日志:JSON 字段输出,便于解析/搜索/分析

  • JSONFormatter:自定义 format() 拼 timestamp/level/message/exception

  • 统一字段:标准键名让多服务日志可聚合检索

  • 高级玩法:request_id 串联链路、审计日志、落 ES

口诀

A:结构化,JSON 写,统一字段好检索

Formatter,自拼接,时间级别消息全

request_id 串链路,审计耗时也能记

落地 ES 可查询,日志分析不再难

相关链接