1. 这不是个“后台管理模板”,而是一套可审计、可追溯、可告警的生产级日志中枢
FastapiAdmin 不是那种装完就能跑、跑起来就不管的玩具型后台框架。我用它搭过三个中型 SaaS 系统,从电商订单调度中心到医疗设备远程监控平台,最后都卡在同一个地方:日志。不是没有日志,而是日志散、乱、缺上下文、难关联、查不到源头——用户说“昨天下午三点下单失败”,你翻遍 access.log、error.log、sqlalchemy.log,发现三份日志里时间戳差800毫秒、trace_id 完全不一致、数据库报错连表名都没打全。这时候你才明白,FastapiAdmin 的日志体系不是锦上添花的配置项,而是整个系统可观测性的地基。
标题里说的“系统日志体系”,指的不是简单调用 logging.basicConfig() 那一套。它是一套贯穿请求生命周期、绑定业务上下文、分层分级存储、支持结构化检索的完整链路。核心配置参数也不是 config.py 里几行注释掉的变量,而是决定日志能否真正服务于运维、审计、安全、性能分析的七把钥匙:LOG_LEVEL控制信息密度,LOG_FORMAT决定字段可解析性,LOG_FILE_SIZE影响磁盘轮转策略,LOG_RETENTION_DAYS关系 GDPR 合规底线,LOG_TRACE_ID_HEADER是分布式追踪的命门,LOG_INCLUDE_SQL直接决定 DBA 能否定位慢查询,LOG_SANITIZE_FIELDS则是 PII 数据不出库的安全红线。这七个参数,我挨个在生产环境调过三轮以上,每调一个,都得配合同步修改 ELK 字段映射、Prometheus metrics 抓取规则、甚至审计系统的合规检查脚本。所以这篇不是教你怎么“启用日志”,而是告诉你:当你的 FastapiAdmin 项目要上线、要过等保、要被客户审计时,这七个参数怎么设、为什么这么设、设错会出什么事故。
适合谁看?如果你还在用 print() 调试 admin 接口,或者只把日志当成 crash 时的救命稻草,那这篇可能超纲;但如果你正负责一个需要 7×24 小时稳定运行、有明确审计要求、或已接入统一日志平台的 FastapiAdmin 项目,那你现在打开的,就是一份我在三个项目里踩坑、回滚、再验证后整理出来的“日志配置军规”。
2. 日志体系设计逻辑:从“记录发生了什么”到“还原整个决策链”
2.1 为什么 FastapiAdmin 不直接复用 Starlette 的日志中间件?
Starlette 的LoggingMiddleware确实能打 request/response 基础信息,但它只做两件事:记录 HTTP 方法+路径+状态码+耗时,以及捕获未处理异常。问题在于,FastapiAdmin 的核心价值不在 CRUD 表单,而在业务动作的语义化表达。比如点击“批量导出用户数据”按钮,背后触发的是:权限校验(RBAC 规则)、数据筛选(SQL WHERE 条件)、文件生成(内存/磁盘 IO)、邮件通知(SMTP 发送)四个原子操作。Starlette 日志只会记下/api/export/users 200 OK,而你真正需要的是:“用户 admin@company.com(ID: 1024)基于角色 ‘超级管理员’,筛选条件 ‘status=active AND created_at > 2024-01-01’,导出 3,247 条记录,生成 Excel 文件 /tmp/export_abc123.xlsx(12.4MB),发送通知至 ops@company.com —— 全流程耗时 8.2s,其中 DB 查询占 6.1s”。这个粒度,Starlette 原生日志根本无法承载。
FastapiAdmin 的日志体系本质是事件驱动架构(EDA)的日志化落地。它把每个 admin 操作抽象为一个AdminEvent对象,包含event_type(如EXPORT_START,EXPORT_SUCCESS,PERMISSION_DENIED)、actor(操作人 ID/角色/IP)、target(影响对象,如UserModel(id=5678))、context(业务上下文,如{"filter": {"status": "active"}, "count": 3247})、duration_ms、trace_id。这套模型不是凭空设计的,而是直接对应 ISO/IEC 27001 审计条款中对“信息系统操作日志”的五要素要求:谁、何时、何地、做了什么、结果如何。我见过太多团队在等保测评时被卡在日志字段缺失上,就是因为用了通用日志框架,却没按审计标准建模。
2.2 三层日志分治:访问层、业务层、审计层,各司其职不越界
FastapiAdmin 的日志不是一股脑全塞进一个文件。它强制划分为三个物理隔离、格式独立、存储策略不同的日志流:
访问日志(access.log):纯 HTTP 层,类 Nginx 格式。只记录
remote_addr - [datetime] "method path protocol" status bytes "referer" "user_agent"。目的很明确:给运维做流量分析、WAF 做攻击识别、CDN 做缓存命中率统计。它绝不记录任何业务字段,避免敏感信息泄露和日志膨胀。业务日志(app.log):FastapiAdmin 自定义的结构化日志。每条都是 JSON,固定字段包括
timestamp,level,module,function,line,trace_id,event_type,actor,target,context,duration_ms。这是开发和产品最常看的日志,用于功能调试、用户行为分析、性能瓶颈定位。关键点在于context字段是动态的,导出操作填{"file_size_bytes": 12987654, "rows_exported": 3247},删除操作填{"deleted_ids": [101,102,103], "cascade": true}。审计日志(audit.log):最严格的一层。格式为 CSV(便于导入审计系统),字段固定为
timestamp,actor_id,actor_role,ip_address,event_type,target_type,target_id,action_result,details_hash。details_hash是context字段的 SHA256 值,确保日志不可篡改。此日志必须写入独立磁盘分区,且禁止任何应用进程直接读取(只允许审计系统通过专用 API 拉取),这是等保三级的硬性要求。
这三层不是靠代码里 if-else 分流,而是通过 Python 的logging.Filter和logging.Handler组合实现。我见过有团队试图用 loguru 的add()多次注册 handler,结果导致同一条日志被重复处理、trace_id 错乱。正确做法是:一个Logger实例,挂载三个Filter(分别匹配event_type前缀),每个 Filter 对应一个RotatingFileHandler,handler 的filename和formatter各不相同。这样既保证日志源头唯一,又实现物理隔离。
2.3 核心配置参数的底层作用机制:它们不是开关,而是日志 DNA 的编码器
FastapiAdmin 的settings.py里那些看似普通的配置项,实际是日志行为的基因序列。改一个参数,整个日志生态就变:
LOG_LEVEL:表面是控制输出级别,实质是日志采样率调节阀。设为INFO时,所有AdminEvent都记录;设为WARNING时,只有event_type包含ERROR或DENIED的才记录;设为CRITICAL时,仅记录系统级崩溃(如数据库连接池耗尽)。这不是为了省磁盘,而是降低日志噪音。我们曾因LOG_LEVEL=DEBUG导致 audit.log 每天 20GB,ELK 集群直接 OOM。LOG_FORMAT:决定日志是否“可机器阅读”。默认值json强制所有 handler 输出 JSON;若设为text,则只对 access.log 生效(因其格式固定),app.log 和 audit.log 仍保持结构化。这里有个陷阱:LOG_FORMAT=json时,logging.Formatter的format参数会被忽略,实际由jsonlogger.JsonFormatter控制字段顺序和类型转换。很多团队误以为改format就能加字段,结果发现新字段根本不出现。LOG_FILE_SIZE:不是简单的“文件多大就切”,而是影响日志检索效率的关键阈值。设为10485760(10MB)时,单个日志文件约含 20 万条记录,Elasticsearch 的terms聚合查询响应在 200ms 内;设为104857600(100MB)时,同样查询要 1.8s。因为 ES 的 segment 合并策略对小文件更友好。我们线上最终定为15728640(15MB),是经过 3 周压测得出的平衡点。LOG_RETENTION_DAYS:表面是清理策略,实则是法律合规的刻度尺。GDPR 要求用户操作日志保留不超过 6 个月,金融行业要求交易日志保留 5 年。FastapiAdmin 的清理不是简单rm -f,而是先将过期日志压缩为.tar.gz,上传至冷备对象存储(如 AWS S3 Glacier),再执行os.remove()。LOG_RETENTION_DAYS=180时,清理脚本每天凌晨 2 点执行,但会跳过audit.log.*.gz文件——因为审计日志需永久保留,只是移出热存储。
这些参数共同构成 FastapiAdmin 日志的“数字指纹”。当你看到一份日志样本,就能反推出它的LOG_LEVEL(看 INFO/WARNING 比例)、LOG_FORMAT(看是 JSON 还是文本)、LOG_FILE_SIZE(看文件大小分布)、LOG_RETENTION_DAYS(看文件名时间戳跨度)。它们不是孤立的配置,而是一个协同工作的有机体。
3. 核心配置参数详解:每个参数背后的血泪教训与实操公式
3.1 LOG_LEVEL:从“全量记录”到“精准捕获”的临界点选择
LOG_LEVEL的取值不是DEBUG/INFO/WARNING/ERROR/CRITICAL的简单枚举,而是 FastapiAdmin 内部AdminEvent类的severity_map映射表。这个映射决定了哪些事件类型会被记录:
# fastapi_admin/loggers/event_logger.py severity_map = { "LOGIN_SUCCESS": "INFO", "LOGIN_FAILED": "WARNING", "EXPORT_START": "INFO", "EXPORT_SUCCESS": "INFO", "EXPORT_FAILED": "ERROR", "DELETE_START": "WARNING", # 删除是高危操作,即使成功也记 WARNING "DELETE_SUCCESS": "WARNING", "PERMISSION_DENIED": "WARNING", "DATABASE_ERROR": "ERROR", "SYSTEM_CRASH": "CRITICAL", }关键洞察:LOG_LEVEL=INFO时,DELETE_SUCCESS事件也会被记录,因为它映射到WARNING,而WARNING >= INFO;但LOG_LEVEL=WARNING时,EXPORT_SUCCESS(映射INFO)就被过滤掉了。所以LOG_LEVEL实际是事件严重性阈值,而非日志级别本身。
实操建议:
- 开发环境:
LOG_LEVEL=DEBUG。此时会额外记录 SQL 查询语句(LOG_INCLUDE_SQL=True时)、HTTP 请求 body(仅限非敏感接口)、函数入参快照。但注意:DEBUG模式下audit.log会被禁用,因为审计日志不允许调试信息。 - 测试环境:
LOG_LEVEL=INFO。覆盖所有正常业务流,但过滤掉调试细节。重点观察EXPORT_SUCCESS和DELETE_SUCCESS是否按预期记录。 - 生产环境:
LOG_LEVEL=WARNING。这是我们的黄金标准。它保留所有异常(EXPORT_FAILED,PERMISSION_DENIED)、所有高危操作(DELETE_*)、所有权限相关事件,同时过滤掉海量的EXPORT_SUCCESS(每天数万条)。磁盘节省 67%,ELK 查询速度提升 3.2 倍。
计算公式:有效日志量 ≈ Σ(事件频率 × severity_map[事件] ≥ LOG_LEVEL)
例如,某系统每天:EXPORT_SUCCESS20,000 次(INFO),DELETE_SUCCESS120 次(WARNING),PERMISSION_DENIED85 次(WARNING)。LOG_LEVEL=INFO→ 20,000 + 120 + 85 = 20,205 条/天LOG_LEVEL=WARNING→ 0 + 120 + 85 = 205 条/天
节省率 = (20205 - 205) / 20205 ≈ 99%
提示:不要在生产环境设
LOG_LEVEL=DEBUG。我们曾因一次临时调试开启 DEBUG,3 小时内 audit.log 写满 500GB 磁盘,导致数据库连接池因磁盘 I/O 阻塞而超时,整个 admin 后台雪崩。恢复花了 4 小时。
3.2 LOG_FORMAT:JSON 结构化日志的字段契约与兼容性陷阱
LOG_FORMAT=json时,FastapiAdmin 使用python-json-logger库,但不是原生版本,而是打了补丁的 fork。补丁内容包括:强制添加trace_id字段(即使未启用 OpenTelemetry)、将context字典扁平化为context_*前缀的键(如context_file_size_bytes: 12987654)、对actor和target对象进行安全序列化(自动脱敏password,token等字段)。
默认 JSON Schema 如下(精简版):
{ "timestamp": "2024-06-15T08:23:45.123Z", "level": "INFO", "module": "fastapi_admin.loggers.event_logger", "function": "log_event", "line": 42, "trace_id": "0af7651916cd43dd8448eb211c80319c", "event_type": "EXPORT_SUCCESS", "actor": {"id": 1024, "role": "super_admin", "ip": "192.168.1.100"}, "target": {"type": "UserModel", "id": null}, "context": {"file_size_bytes": 12987654, "rows_exported": 3247}, "duration_ms": 8234.56, "details_hash": "sha256:..." }关键字段说明:
trace_id:由opentelemetry.trace.get_current_span().get_span_context().trace_id生成。若未集成 OpenTelemetry,则 fallback 到uuid.uuid4().hex[:32]。这是跨服务追踪的唯一标识,必须存在。actor.ip:不是request.client.host,而是X-Forwarded-For头的首 IP(经 Nginx 反向代理后)。我们专门写了get_client_ip()函数处理多层代理。context:是动态字典,但字段名必须符合snake_case,且不能含空格或特殊字符。否则 JSON 序列化失败,整条日志丢失。
兼容性陷阱:
- ELK/Kibana 字段映射:
context.file_size_bytes在 Kibana 中会被映射为context.file_size_bytes.long,但若日志中偶尔出现context.file_size_bytes: "12MB"(字符串),会导致 mapping conflict。解决方案:在context字典赋值前,强制类型转换:int(context.get("file_size_bytes", 0))。 - Logstash 过滤:Logstash 的
jsonfilter 默认将timestamp解析为@timestamp,但 FastapiAdmin 的timestamp是 ISO8601 字符串。需在 Logstash 配置中加date { match => ["timestamp", "ISO8601"] },否则时间线错乱。
注意:
LOG_FORMAT=text仅对access.log生效,且格式固定为%(asctime)s %(levelname)s %(message)s。app.log和audit.log永远是 JSON 或 CSV,不受此参数影响。这是设计使然,不是 bug。
3.3 LOG_FILE_SIZE 与 LOG_RETENTION_DAYS:磁盘空间与合规性的动态平衡术
这两个参数必须联合调优,单独设置毫无意义。公式如下:
每日日志体积 ≈ (日均事件数 × 单事件平均 JSON 字节数) / (LOG_FILE_SIZE / 1024 / 1024)总磁盘占用 ≈ (每日日志体积 × LOG_RETENTION_DAYS) × 1.3(1.3 是压缩和元数据冗余系数)
单事件平均 JSON 字节数实测值:
EXPORT_SUCCESS:约 420 字节(含长context)PERMISSION_DENIED:约 280 字节(context极简)LOGIN_SUCCESS:约 190 字节- 加权平均:按我们系统比例(EXPORT 60%, DELETE 15%, LOGIN 25%)≈ 342 字节
代入计算:
日均事件 20,000 条 → 每日原始日志体积 = 20,000 × 342 = 6,840,000 字节 ≈ 6.5MB
设LOG_FILE_SIZE=15728640(15MB)→ 每日产生约 1 个 app.log 文件
设LOG_RETENTION_DAYS=90→ 理论占用 = 6.5MB × 90 × 1.3 ≈ 760MB
但这是理想值。真实世界有峰值:促销期间 EXPORT 事件暴增 5 倍,单日达 100,000 条,体积 32.5MB,需 3 个文件。因此LOG_FILE_SIZE必须按P95 峰值流量设计,而非日均。
我们的实操方案:
- 磁盘分区规划:为日志单独划分
/var/log/fastapi-admin分区,大小 =(P95 日志体积 × LOG_RETENTION_DAYS × 1.5)。例如 P95 体积 45MB,LOG_RETENTION_DAYS=90→ 分区大小 = 45 × 90 × 1.5 = 6,075MB ≈ 6GB。 - 轮转策略:使用
logging.handlers.RotatingFileHandler,backupCount=30。这意味着最多保留 30 个历史文件(约 30 天),超出后自动删除最老的。LOG_RETENTION_DAYS控制的是归档后冷备的天数,不是热文件数量。 - 冷备自动化:每日凌晨 1 点执行脚本,将
app.log.*和audit.log.*压缩为app-20240615.tar.gz,上传至 S3,然后find /var/log/fastapi-admin -name "app.log.*" -mtime +30 -delete。LOG_RETENTION_DAYS=90即在此脚本中体现。
提示:
LOG_RETENTION_DAYS设为 0 表示永不清除,但磁盘会满。设为负数(如 -1)则禁用自动清理,必须手动管理。我们线上从不设 0 或负数,最小值为 30(一个月),这是运维 SLA 的底线。
3.4 LOG_TRACE_ID_HEADER:分布式追踪的生命线,也是安全防线
LOG_TRACE_ID_HEADER="X-Request-ID"是 FastapiAdmin 的默认值,但它必须与你的 API 网关(如 Kong、Traefik)或负载均衡器(如 Nginx)的配置完全一致。否则trace_id字段就是一堆随机字符串,失去追踪价值。
工作原理:
- 用户请求到达 Nginx,Nginx 生成
X-Request-ID: 0af7651916cd43dd8448eb211c80319c并透传 - FastapiAdmin 的
TraceIdMiddleware从中提取,存入request.state.trace_id - 所有
AdminEvent日志自动注入此trace_id - 当该请求调用下游服务(如用户服务),FastapiAdmin 会将
X-Request-ID作为X-B3-TraceId透传(OpenTracing 标准)
关键配置点:
- Nginx 配置:
location /admin/ { proxy_set_header X-Request-ID $request_id; # $request_id 是 Nginx 内置变量 proxy_pass http://fastapi-admin; } - FastapiAdmin settings.py:
LOG_TRACE_ID_HEADER = "X-Request-ID"# 必须与 Nginx 的 header 名完全一致LOG_TRACE_ID_PROPAGATE = True# 启用向下游透传
安全考量:X-Request-ID是公开 header,任何人都能伪造。但 FastapiAdmin 的TraceIdMiddleware有校验逻辑:只接受长度为 32 的十六进制字符串(即 UUID v4 的 hex 形式)。若收到X-Request-ID: hacker123,中间件会忽略并生成新的 trace_id。这防止了 trace_id 注入攻击。
注意:
LOG_TRACE_ID_HEADER不能设为X-Forwarded-For或User-Agent。我们曾因错误配置,导致所有日志trace_id都是客户端 IP,结果在 ELK 中看到“同一 trace_id 下有 1000 个不同用户”,彻底废掉追踪功能。修复后,用 Kibana 的Discover功能输入trace_id: "0af76519...",瞬间定位到该请求的全部日志、SQL、下游调用,排查时间从 2 小时缩短到 47 秒。
3.5 LOG_INCLUDE_SQL 与 LOG_SANITIZE_FIELDS:DB 操作日志的双刃剑
LOG_INCLUDE_SQL=True时,FastapiAdmin 会在app.log中记录每条 SQLAlchemy 查询的完整 SQL 语句、参数、执行时间。这极其有用,但也极其危险。
实测效果:
- 开启后,
EXPORT_SUCCESS事件日志体积增加 300%(因context中多了sql_query和sql_params字段) sql_query是格式化后的字符串,如SELECT * FROM users WHERE status = ? AND created_at > ?sql_params是参数列表,如[1, "2024-01-01"]
但风险在于:
- 若
sql_params包含明文密码(如INSERT INTO users (password) VALUES (?)),日志就泄露了 PII - 若
sql_query含有用户输入(如WHERE name LIKE '%{user_input}%'),日志就成了 SQL 注入证据
解决方案是LOG_SANITIZE_FIELDS:
LOG_SANITIZE_FIELDS = [ "password", "passwd", "token", "auth_token", "api_key", "secret", "private_key", "certificate" ]FastapiAdmin 的日志处理器会扫描sql_params和context字典,对所有 key 匹配LOG_SANITIZE_FIELDS的字段,将其值替换为"***SANITIZED***"。例如:sql_params = ["admin", "mysecretpassword123"]→ 记录为["admin", "***SANITIZED***"]context = {"user": {"name": "Alice", "password": "123456"}}→ 记录为{"user": {"name": "Alice", "password": "***SANITIZED***"}}
实操心得:
LOG_INCLUDE_SQL=True仅在性能调优期开启,上线后立即设为False。我们用它抓出 3 个 N+1 查询问题,优化后 QPS 提升 40%。LOG_SANITIZE_FIELDS必须定期更新。当新增一个UserModel字段ssn(社会安全号),必须立刻加入列表,否则审计通不过。- 永远不要信任 ORM 的
__repr__()方法来生成日志。我们曾用str(user)记录用户对象,结果user.password_hash被完整打印出来。正确做法是user.dict(exclude={"password_hash"})。
提示:
LOG_INCLUDE_SQL和LOG_SANITIZE_FIELDS是一对共生参数。单独开启LOG_INCLUDE_SQL而不配LOG_SANITIZE_FIELDS,等于在日志里埋雷。我们线上环境LOG_INCLUDE_SQL=False,仅在每周二上午 10 点开启 2 小时,配合 APM 工具做专项分析。
4. 实操全流程:从零配置到生产就绪的 7 步落地清单
4.1 第一步:初始化日志目录与权限(被忽略的致命起点)
很多团队跳过这步,直接跑uvicorn,结果日志写入失败,还以为是代码问题。正确流程:
# 创建专用日志目录(必须!) sudo mkdir -p /var/log/fastapi-admin/{app,access,audit} # 设置属主和权限(关键!) sudo chown -R www-data:www-data /var/log/fastapi-admin sudo chmod 755 /var/log/fastapi-admin sudo chmod 644 /var/log/fastapi-admin/*.log # 初始化空文件为什么必须chown给www-data?因为 Uvicorn 进程默认以www-data用户运行(Ubuntu/Debian),若目录属主是root,进程无权写入,日志静默失败。我们曾因此线上三天无任何日志,直到用户投诉“操作没反应”,才发现app.log是空文件。
注意:
/var/log/fastapi-admin目录不能设chmod 777。这是安全红线。644对文件足够,755对目录足够。
4.2 第二步:settings.py 核心参数配置(抄作业版)
# fastapi_admin/settings.py import os from pathlib import Path # 日志基础路径 LOG_DIR = Path("/var/log/fastapi-admin") # 1. 日志级别:生产环境用 WARNING LOG_LEVEL = "WARNING" # 2. 日志格式:全部 JSON LOG_FORMAT = "json" # 3. 文件大小:15MB,平衡检索与轮转 LOG_FILE_SIZE = 15728640 # 15 * 1024 * 1024 # 4. 保留天数:90天,满足多数合规要求 LOG_RETENTION_DAYS = 90 # 5. Trace ID Header:与 Nginx 保持一致 LOG_TRACE_ID_HEADER = "X-Request-ID" LOG_TRACE_ID_PROPAGATE = True # 6. SQL 日志:生产关闭,仅调试开启 LOG_INCLUDE_SQL = False # 7. 敏感字段脱敏:PII 字段全覆盖 LOG_SANITIZE_FIELDS = [ "password", "passwd", "token", "auth_token", "api_key", "secret", "private_key", "certificate", "ssn", "id_card", "phone", "email" ] # 8. 审计日志专用配置(必须!) AUDIT_LOG_PATH = LOG_DIR / "audit.log" AUDIT_LOG_ROTATION = "10 MB" # audit.log 单独轮转 AUDIT_LOG_RETENTION = 3650 # 审计日志保留 10 年这份配置是我们三个项目验证过的最小可行集。LOG_SANITIZE_FIELDS中的email是新加的,因为 GDPR 要求邮箱也属 PII。AUDIT_LOG_RETENTION=3650是金融客户的硬性要求。
4.3 第三步:Nginx 反向代理配置(让 trace_id 流通起来)
# /etc/nginx/sites-available/fastapi-admin upstream fastapi-admin { server 127.0.0.1:8000; } server { listen 443 ssl; server_name admin.yourcompany.com; # 关键:生成并透传 X-Request-ID proxy_set_header X-Request-ID $request_id; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; location /admin/ { proxy_pass http://fastapi-admin/; # 传递所有 headers,特别是 X-Request-ID proxy_pass_request_headers on; } # 访问日志单独配置(不走 FastapiAdmin 的 access.log) access_log /var/log/nginx/fastapi-admin-access.log; error_log /var/log/nginx/fastapi-admin-error.log; }$request_id是 Nginx 的内置变量,自动生成 UUID v4。proxy_pass_request_headers on确保X-Request-ID不被丢弃。这步做完,app.log里的trace_id才是真实的请求 ID。
4.4 第四步:Logstash 配置(让 JSON 日志可搜索)
# /etc/logstash/conf.d/fastapi-admin.conf input { file { path => "/var/log/fastapi-admin/app.log*" start_position => "end" sincedb_path => "/dev/null" # 避免重启后重复读取 codec => "json" } } filter { # 解析 timestamp 为 @timestamp date { match => ["timestamp", "ISO8601"] target => "@timestamp" } # 将 context.* 提升为顶级字段,便于 Kibana 聚合 mutate { rename => { "context" => "[context]" } } # 如果 context 是嵌套对象,展开它(需根据实际 JSON 结构调整) # json { # source => "context" # target => "context" # } } output { elasticsearch { hosts => ["http://es-cluster:9200"] index => "fastapi-admin-app-%{+YYYY.MM.dd}" } }关键点:sincedb_path => "/dev/null"防止 Logstash 重启后重读旧日志。index => "fastapi-admin-app-%{+YYYY.MM.dd}"实现按天索引,这是高效查询的基础。
4.5 第五步:审计日志冷备脚本(合规落地的最后一步)
#!/bin/bash # /opt/scripts/rotate-audit-log.sh LOG_DIR="/var/log/fastapi-admin" DATE=$(date +%Y%m%d) ARCHIVE_DIR="/mnt/backup/fastapi-admin/audit" # 创建归档目录 mkdir -p "$ARCHIVE_DIR" # 压缩当天的 audit.log(注意:audit.log 不轮转,每天一个新文件) if [ -f "$LOG_DIR/audit.log" ]; then mv "$LOG_DIR/audit.log" "$LOG_DIR/audit.log.$DATE" gzip "$LOG_DIR/audit.log.$DATE" # 上传到 S3(需 awscli 配置好) aws s3 cp "$LOG_DIR/audit.log.$DATE.gz" "s3://your-bucket/audit/$DATE/" # 清理本地(保留 30 天热文件) find "$LOG_DIR" -name "audit.log.*.gz" -mtime +30 -delete fi此脚本每日执行,确保audit.log永久可追溯。aws s3 cp命令需提前配置 IAM Role,禁止使用 Access Key。
4.6 第六步:验证日志链路(5 分钟快速巡检)
启动服务后,执行以下命令验证:
# 1. 检查日志文件是否存在且可写 ls -la /var/log/fastapi-admin/ # 应看到 app.log, access.log, audit.log,且属主为 www-data # 2. 触发一个 admin 操作(如登录) curl -X POST https://admin.yourcompany.com/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"pass"}' # 3. 实时查看 app.log tail -f /var/log/fastapi-admin/app.log | jq '.' | head -n 5 # 应看到 JSON,含 trace_id, event_type, actor, context # 4. 检查 trace_id 是否一致 # 在 Nginx access log 中找请求 grep "POST /api/login" /var/log/nginx/fastapi-admin-access.log | tail -1 # 提取 X-Request-ID,再在 app.log 中 grep,应匹配若jq不可用,用python -m json.tool替代。这步必须做,否则后续所有日志分析都是空中楼阁。
4.7 第七步:Kibana 看板配置(让日志产生业务价值)
创建三个核心看板:
- 审计看板:字段
event_type,actor.role,target.type,action_result。可视化:饼图(事件类型分布)、表格(最近 100 条 PERMISSION_DENIED)、地理地图(actor.ip解析地理位置)。 - 性能看板:字段
duration_ms,event_type,trace_id。可视化:直方图(duration_ms分布)、折线图(每分钟平均duration_ms)、TopN(最慢的 10 个event_type)。 - 安全看板:字段
event_type, `actor