上线不是终点,是"运营"的起点。网站半夜挂了,谁第一个知道?------希望是你,而不是用户 。
本篇给早报站装上体检报告:日志、健康检查、Uptime监控、报警通知。从"能跑"到"知道它在跑",从"用户替你发现故障"到"你比用户先发现故障"。
🎯 本篇产出 :
/api/health深度健康检查 + 结构化日志打点 + watchdog监控脚本 + 报警纪律。含代码约100行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | 给上线后的早报站装上监控与告警 |
| 代码行数 | ~100行(含注释) |
| 依赖 | requests(已装)+ psutil(练习用) |
| 核心功能 | 健康检查 + 日志轮转 + 定时探活 + 报警去重 |
| 跑起来的命令 | curl /api/health + crontab 定时跑watchdog |
| 核心知识点 | 可观测性三支柱、深/浅健康检查、报警去重 |
| 做完你能得到 | 一套"你比用户先知道"的监控体系 |
一、概念课:可观测性三支柱与性价比哲学
📊 可观测性(Observability)三支柱
| 支柱 | 回答的问题 | 本篇做法 |
|---|---|---|
| 日志 Logs | 发生了什么(逐条记录) | logging配置 + 关键事件打点 |
| 指标 Metrics | 整体状态怎么样(数字) | 健康接口 + 命中率/耗时 |
| 追踪 Tracing | 请求经过了谁 | 微服务才需要,本篇一句带过 |
💰 性价比哲学
📌 别一上来就上Grafana全家桶。 早报站的体量,"结构化日志 + 健康接口 + 外部定时探测"三层就够了------监控的花费要和系统的价值成正比。
💡 等真有第二个服务、第十个接口时,再谈Prometheus(练习④)。
📋 监控三问
text
① 活着吗(进程在不在)
② 健康吗(依赖都通吗)
③ 出事了能查吗(日志能不能还原现场)
🎯 三问对应本篇三个产出。
二、第0步:结构化日志------出事能查的第一前提
core/logging_config.py:
python
"""core/logging_config.py ------ 应用日志:控制台 + 文件轮转双写"""
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
Path("logs").mkdir(exist_ok=True)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
# ① stdout → docker logs能看到
console = logging.StreamHandler()
console.setFormatter(fmt)
# ② 文件轮转:单文件10MB,留5份
file_handler = RotatingFileHandler(
"logs/daily.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8")
file_handler.setFormatter(fmt)
logging.basicConfig(level=logging.INFO, handlers=[console, file_handler])
logger = logging.getLogger("daily")
🔧 main.py里一行启用
python
# app/main.py 最顶部(在任何其他import之前)
import core.logging_config # noqa: F401 启用日志配置
⚠️ 必须在其他模块之前import------logging配置是全局的,晚import的模块可能已经用旧配置打过日志了。
📌 关键位置打点(监控的价值在"记对了地方")
python
logger.info("login ok user=%s", username)
logger.warning("login failed user=%s ip=%s", username, request.client.host)
logger.info("upload ok file=%s size=%s", name, file.size)
logger.info("cache hit key=%s", cache_key) # 练习④的正式上岗
logger.error("db error detail=%s", detail)
🎯 两个工程细节
| # | 细节 | 说明 |
|---|---|---|
| ① | 双写 | stdout给docker compose logs用,文件给排查归档用------只写文件会让容器日志变哑巴(第八节⑤) |
| ② | 轮转 | 单文件10MB自动切割,日志是磁盘杀手,不轮转迟早撑爆(第八节②) |
三、第1步:健康检查接口------浅检查 + 深检查
app/routers/health.py:
python
"""app/routers/health.py ------ 深度健康检查"""
import redis as redis_lib
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.deps import get_session
from core.cache import r as redis_client
router = APIRouter(prefix="/health", tags=["health"])
@router.get("")
def health(session: Session = Depends(get_session)):
# ① 深检查:依赖逐个探活,坏了不抛500,降级标记
db_ok, redis_ok = True, True
try:
session.execute(text("SELECT 1"))
except Exception:
db_ok = False
try:
redis_client.ping()
except redis_lib.RedisError:
redis_ok = False
# ② 汇总:ok / degraded(有依赖挂了但服务还活着)
status = "ok" if (db_ok and redis_ok) else "degraded"
return {"status": status, "db": db_ok, "redis": redis_ok}
📌 浅检查 vs 深检查
| 类型 | 回答什么问题 | 给谁用 |
|---|---|---|
| 浅检查 | 进程活着吗 | Docker healthcheck用(curl /docs可以换成它) |
| 深检查 | 依赖都通吗 | 监控报警用 |
🚨 深检查的核心纪律
📌 探活失败要降级返回
degraded,而不是抛500------健康接口自己崩了,监控就瞎了。
🔧 挂载路由(app/main.py)
python
from app.routers import health
app.include_router(health.router, prefix="/api")
四、第2步:watchdog------外部视角的定时探测
健康接口有了,还得有人定时来问。
scripts/watchdog.py (跑在任意一台别的机器上------外部视角才真实):
python
"""scripts/watchdog.py ------ 每5分钟探活,异常即报警(cron驱动)"""
import os
import time
import requests
URL = os.getenv("DAILY_URL", "https://daily.example.com")
WEBHOOK = os.getenv("ALERT_WEBHOOK", "") # 钉钉/飞书/Server酱的机器人地址
_last_alert = 0.0 # 报警去重:静默窗口
def send_alert(text: str) -> None:
global _last_alert
if time.time() - _last_alert < 1800: # 30分钟内不重复报(第八节④)
return
_last_alert = time.time()
if WEBHOOK:
requests.post(WEBHOOK, json={"msgtype": "text",
"text": {"content": f"[早报站告警] {text}"}},
timeout=5)
print("ALERT:", text) # 本地也留痕
def check() -> None:
try:
resp = requests.get(f"{URL}/api/health", timeout=5)
data = resp.json()
except Exception as e:
send_alert(f"健康接口不可达: {e}")
return
if resp.status_code != 200 or data.get("status") != "ok":
send_alert(f"服务异常: status={data.get('status')} "
f"db={data.get('db')} redis={data.get('redis')}")
if __name__ == "__main__":
check()
⏰ 挂到定时器(服务器上 crontab -e)
text
*/5 * * * * cd /opt/daily && /opt/daily/venv/bin/python scripts/watchdog.py >> /var/log/watchdog.log 2>&1
🚨 用venv里的python绝对路径 ------cron的PATH和你shell的PATH不一样,直接用
/usr/bin/python3会缺依赖(第八节③)。
📢 报警渠道三选一
| 渠道 | 获取方式 |
|---|---|
| 钉钉群机器人 | 群设置 → 机器人 → 自定义 → 拿webhook URL |
| 飞书群机器人 | 同上 |
| Server酱 | sct.ftqq.com 注册 → 拿SendKey → 拼URL |
💡 都是"创建机器人 → 拿到webhook URL → 填进
ALERT_WEBHOOK环境变量"三步。报警落地自选,本篇只管把URL用起来。
五、第3步:告警纪律------别把自己练成"狼来了"
✅ 该报
text
- 服务整体不可达
- db/redis不可达
- 磁盘超90%(练习②)
- 同一错误持续5分钟以上
❌ 别报
text
- 单次偶发超时
- 个别404/401(那可能只是爬虫或用户输错密码)
- 每5分钟必然重复的同一件事(去重+静默窗口解决,第4节已内置)
🐺 告警疲劳(alert fatigue)
📌 报警太多 → 你开始无视它们 → 真出事那天也没人看。
🎯 报警的价值 = 频率低 × 每次都真。 宁可少报漏一次,不可天天"狼来了"。
六、验收清单
bash
1. GET /api/health → {"status":"ok","db":true,"redis":true}
2. docker stop redis-daily容器 → health变degraded(db:true, redis:false),且接口本身不500
3. logs/daily.log出现login/upload/cache hit打点;单文件超10MB自动切割
4. 手动跑watchdog.py → 正常时无报警;停掉Redis再跑 → 收到webhook/钉钉消息
5. crontab里定时任务生效(*/5自动探测)
6. 全程无报错后提交Git
bash
git add .
git commit -m "监控告警:健康检查 + 结构化日志 + watchdog"
七、常见报错:这6个,监控上手的标配(重点!)
① health接口自己500(Redis没起时)
🔍 原因 :深检查没try/except------探活的初衷是"报告坏消息",结果自己成了坏消息。
✅ 解法 :每个依赖的探测独立try/except,降级返回degraded(第3节写法)。
② 日志把磁盘撑爆 / 写入报No space left on device
🔍 原因 :RotatingFileHandler没配,或maxBytes给了天文数字。
✅ 解法 :10MB × 5份是合理起点;du -sh logs/定期看一眼。
③ cron任务"跑着跑着就不跑了"
🔍 原因:cron的环境变量和PATH与你的shell不同------相对路径、python不在PATH、环境变量缺失。
✅ 解法 :全部用绝对路径(venv里的python、项目目录);需要的环境变量写进脚本或crontab行内。
④ Redis挂了1小时 → 收到12条一模一样的报警
🔍 原因:没做去重------每5分钟探测一次,每次都报。
✅ 解法 :静默窗口(第4节_last_alert逻辑):同一问题30分钟内只报第一次,恢复后可发一条"已恢复"。
⑤ docker compose logs里看不到应用日志
🔍 原因:只配了文件输出,stdout成了哑巴。
✅ 解法 :双写(console + file两个handler)------容器世界stdout是一等公民。
⑥ health全绿但用户照样报障
🔍 原因:健康检查覆盖不了所有故障(连接池耗尽、慢查询、业务逻辑bug......)。
✅ 解法 :认识监控的边界------健康检查是体检报告,不是全部真相;用户反馈渠道(哪怕一个QQ群)永远是监控的补充。
八、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 命中率打点 :cache_get/cache_set加日志(练习④的正式版),跑一天算真实命中率 |
⭐⭐ | 可观测性入门 |
| 2 | 磁盘监控:watchdog里加磁盘占用检查 | ⭐⭐ | psutil.disk_usage,超90%报警 |
| 3 | 报警去重升级:静默窗口内累积错误次数,恢复时发一条"已恢复(共失败N次)" | ⭐⭐⭐ | 状态机思维 |
| 4(选做) | Prometheus + Grafana全案:metrics接口暴露 + 两个容器 + 一块面板 | ⭐⭐⭐⭐ | 体验"监控全家桶",感受它与手写脚本的差别 |
📦 配套代码
health路由、日志配置与watchdog脚本已上传Git(python_daily/):【gitee仓库地址】