【Web全栈进阶】监控与告警:线上系统的“体检报告”

上线不是终点,是"运营"的起点。网站半夜挂了,谁第一个知道?------希望是你,而不是用户 。

本篇给早报站装上体检报告:日志、健康检查、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仓库地址】

相关推荐
鬼手点金1 小时前
单轮对话:最简单的 API 调用
java·服务器·前端·javascript·react.js·servlet·tomcat
蜡笔羊驼1 小时前
ModuleNotFoundError: No module named ‘torchvision.transforms.functional_tensor‘
linux·人工智能·python
李航19831 小时前
DeepDraw 网页版
人工智能·python·计算机视觉·ai·ai编程
企业数字化笔记1 小时前
一段视频有两次跳投,动作分析为什么要先做事件分段?
人工智能·python·音视频
Aurorar0rua2 小时前
CS50 x 2024 Notes Python - 02
开发语言·python·学习方法
燕落南枝2 小时前
CSS 字体样式:颜色、字体族、倾斜、加粗与装饰
前端·css
鬼手点金2 小时前
消息的四种角色
java·前端·人工智能·git·深度学习
樱桃木2 小时前
回测可信度审查:样本外测试的五种常见自欺
python
陈随易2 小时前
副业搞钱之代办永不过时
前端·后端·程序员