简介:Tracking Master 是一款面向 Bludit Flat File CMS 的开源网络分析跟踪插件,主要服务于个人站长、小型网站开发者以及重视数据自主可控的使用者。它针对无数据库轻量站点在访问统计与用户行为分析上的需求,可记录访问量、页面浏览、来源渠道等信息,并兼容多供应商分析服务,便于按场景选择统计方案。压缩包共 12 个文件,约 17KB,包含 JSON 配置与元数据、PHP 主程序、CSS 样式、语言资源、图片、说明文档及许可证,虽体量精简,却覆盖插件运行、界面呈现和配置说明所需的基本模块。目前已有 481 人学习下载,适合用作理解 Bludit 插件结构、分析服务接入和轻量统计实现的参考。读者可从中梳理插件目录组织、多语言与样式资源的配合方式,并依据开源许可进行二次定制,以较低部署成本增强站点分析能力。
1. 网络多供应商数据怎么统一:Tracking Master 解决的那点事
机房里三排机柜、四家厂商的设备,出故障时你得同时打开四个管理后台,日志格式各写各的,时间还差着十几秒,一条链路抖动的完整链路拼都拼不起来——这活干一次骂一次。Tracking Master 就是冲这个场景来的:一个开源的多供应商网络分析跟踪器,把异构设备的 syslog、SNMP 轮询、流量会话收进同一条流水线,归一化成统一事件与实体轨迹,再落到可视化看板上。它适合手上有异构网络、又不想被单一厂商网管绑死的运维和二开同学,也适合想学插件式采集架构怎么落地的人。
2. 技术栈与目录结构:四层拆分让采集和解析各干各的
2.1 为什么拆成采集、归一化、存储、查询四层
刚接触这类系统时最容易犯的错,是把收日志、解析、入库、查接口全塞进一个进程里。本地跑着挺爽,一上生产就原形毕露:某台核心交换机重启瞬间吐出几千条 syslog,主线程被正则吃满,HTTP 接口跟着超时,看板一片红——其实底层设备压根没事,是你自己的采集器先崩了。
Tracking Master 的分层思路很朴素,四层各自只认一种数据结构:
- 采集层(collector)只负责“拿到东西”,输出
RawRecord:原始文本、来源 IP、到达时间、设备 ID。它不知道什么叫链路状态,也不做任何业务判断。 - 归一化层(normalize)只负责“翻译”,输入
RawRecord,输出NormalizedEvent。这一层是纯函数式的,给定同样的输入必然得到同样的输出,所以单元测试特别好写。 - 存储层(store)只负责“落地”,认
NormalizedEvent,往时序表和关系表里写,顺带做去重和聚合。 - 查询层(api)只负责“取出来”,对外暴露 REST 和 WebSocket,不碰解析逻辑。
层与层之间靠两个 Pydantic 模型传递,这个约定非常重要。我见过不少二次开发把厂商私有字段直接塞进事件模型的主字段里,结果加第二个厂商时字段名打架,最后整张表重建。Tracking Master 的做法是把厂商特有的东西统统塞进attrs这个 JSONB 字段,主模型只保留ts / kind / severity / vendor / device_id / entity六个通用维度,加厂商不用改表结构。
另一个容易被忽略的点是时钟。采集层拿到的是“本机收到的时间”,归一化层要的是“设备认为发生的时间”,两者在网络抖动时能差出好几秒。四层拆分后,时钟对齐放在归一化层做,主流程不用感知,这是后面避坑章节要重点说的地方。
2.2 目录结构与模块职责
工程是标准的 Pythonsrc布局,适配器单独抽成一个可插拔目录,加厂商基本只动adapters/下一级目录:
| 路径 | 职责 | 二开时改动频率 |
|---|---|---|
src/tracking_master/adapters/base.py | 适配器抽象基类,定义五个必须实现的方法 | 极低,除非要加新能力类型 |
src/tracking_master/adapters/registry.py | 按vendor名发现并实例化适配器 | 低 |
src/tracking_master/adapters/vendor_a/ | A 厂设备的解析与轮询实现 | 高 |
src/tracking_master/adapters/generic_snmp/ | 通用 SNMP 采集,走标准 MIB-2 OID | 中,常改 OID 表 |
src/tracking_master/collector/ | 调度器、连接池、背压控制、syslog 监听 | 中 |
src/tracking_master/normalize/ | 归一化管道、时钟对齐、去重、单位换算 | 高 |
src/tracking_master/store/ | ORM 模型、时序写入、连续聚合视图 | 中 |
src/tracking_master/graph/ | 拓扑构建与 MAC/IP 轨迹拼装 | 中 |
src/tracking_master/api/ | REST 路由、WebSocket 推送 | 中 |
src/tracking_master/rules/ | 阈值与告警规则,纯配置驱动 | 高 |
web/ | Vue 3 + Vite 前端,ECharts 画拓扑和时序 | 中 |
deploy/ | compose 文件、Alembic 迁移、环境变量样例 | 低 |
tests/ | 适配器快照测试、管道单测 | 高 |
registry.py用的是“目录即插件”的约定式发现:扫描adapters/下每个包含adapter.py且定义了Adapter类的子目录,读取类属性vendor作为注册键。好处是加厂商不用改任何注册代码,坏处是拼错目录名会静默失败——所以启动日志里会打一行已注册的 vendor 列表,第一次接新厂商时先看这行。
2.3 依赖清单与关键配置项
技术栈是 Python 3.10+ 那一套,没有特别花哨的东西,选型基本围绕“异构数据 + 时间序列”这两个特征:
| 组件 | 用途 | 选型理由 |
|---|---|---|
| FastAPI | REST + WebSocket | 异步原生,和采集侧的 asyncio 同一套事件循环 |
| Pydantic v2 | 事件模型校验 | 校验快,序列化直接吐 JSONB |
| SQLAlchemy 2.x + Alembic | ORM 与迁移 | 迁移脚本进版本库,二开加字段不慌 |
| TimescaleDB | 事件与会话时序存储 | 就是 PostgreSQL 加超表,SQL 生态不用重学 |
| Redis | 去重指纹、采集任务锁、看板缓存 | 一个组件干三件事,少一个运维对象 |
| APScheduler | 轮询调度 | 支持抖动(jitter),避免全网设备同时被轮询 |
| Vue 3 + ECharts | 看板 | 拓扑图和大箭头图开箱可用 |
配置文件集中在一个config/adapters.yaml里,环境相关的密钥走环境变量插值,不明文进仓库:
| 配置项 | 默认值 | 说明 |
|---|---|---|
collectors[].mode | poll | poll主动轮询,syslog被动监听,flow收 NetFlow/sFlow |
collectors[].interval | 60 | 轮询周期(秒),高频指标建议不低于 30 |
collectors[].jitter | 0.2 | 调度抖动比例,全网设备按 ±20% 打散 |
normalize.dedup_ttl | 180 | 去重窗口(秒),小于设备重传间隔 |
normalize.clock_drift_max | 120 | 允许的最大时钟偏移(秒),超出直接标记异常 |
store.retention_days | 30 | 原始事件保留天数,聚合视图不受影响 |
jitter这个参数是血泪经验换来的。早期版本没做抖动,两百多台设备整点同时被轮询,管理面 CPU 直接冲顶,网络没坏但设备先受不了。加上抖动之后曲线立刻平了,这个改动比任何优化都划算。
3. 供应商适配层拆解:一条 syslog 如何变成统一事件
3.1 BaseAdapter 抽象:五个方法定死插件边界
适配器是整个项目最需要克制的部分。写得太自由,每个厂商各写一套,三个月后没人敢动;约束太死,遇到只上报 syslog 不响应 SNMP 的设备又没法接。Tracking Master 的折中是:五个方法里两个必须实现,其余给默认实现。
# src/tracking_master/adapters/base.py from __future__ import annotations from abc import ABC, abstractmethod from typing import AsyncIterator, ClassVar from tracking_master.models import NormalizedEvent, RawRecord class BaseAdapter(ABC): # 注册键,必须全局唯一,会原样写进事件的 vendor 字段 vendor: ClassVar[str] = "unknown" # 能力声明:syslog / snmp / flow / api,调度器据此决定走哪种采集模式 capabilities: ClassVar[set[str]] = set() # 未在配置里指定周期时的兜底轮询间隔(秒) default_interval: ClassVar[int] = 60 @abstractmethod async def poll(self) -> AsyncIterator[RawRecord]: """主动采集入口。被动接收型设备(只发 syslog)直接 return 空迭代器。""" @abstractmethod def parse(self, raw: RawRecord) -> list[NormalizedEvent]: """把一条原始记录拆成零到多条统一事件。必须是纯函数,方便快照测试。""" def normalize(self, event: NormalizedEvent) -> NormalizedEvent: """单位换算与字段清洗,默认原样返回,子类按需覆盖。""" return event async def healthcheck(self) -> bool: """采集前探活,默认认为健康;有管理面 API 的设备建议覆盖。""" return True def map_entity(self, raw: RawRecord, key: str, type_: str) -> dict: """把厂商侧的实体名映射成统一实体键,默认不做转换。""" return {"type": type_, "key": key}poll返回异步迭代器而不是列表,是为了让 SNMP 这种按表分批取的模式能流式吐数据,不必一次性把几千行塞进内存。parse强制纯函数是硬约束:一旦有人在parse里查数据库或发 HTTP 请求,快照测试就废了,后面所有回归都靠不住。
capabilities这个声明看似多余,实际在调度器里起决定作用。配置里写mode: snmp但适配器没声明snmp能力时,启动阶段就会报错退出,而不是等到运行时空转半小时才发现一条数据都没采到。
3.2 从厂商 A 的 syslog 到统一事件模型
先看统一事件模型长什么样,字段不多,但每个都有明确归属:
# src/tracking_master/models.py from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field class EntityRef(BaseModel): type: Literal["device", "port", "mac", "ip", "session"] key: str # 复合键,习惯写成 "<设备ID>:<端口名>",保证跨厂商可对齐 class NormalizedEvent(BaseModel): ts: datetime # 一律 UTC,采集侧不落本地时区 kind: str # link_state / flow / session / metric severity: Literal["info", "warning", "critical"] = "info" vendor: str device_id: str entity: EntityRef attrs: dict[str, Any] = Field(default_factory=dict) # 厂商私有字段全放这里 source: str = "syslog" def fingerprint(self) -> str: """去重指纹:设备 + 实体 + 类型 + 状态,刻意不含时间戳。""" state = self.attrs.get("state", "") return f"{self.device_id}|{self.entity.key}|{self.kind}|{state}"attrs用 JSONB 存是这套设计里最关键的一步。MAC 地址、端口索引、VLAN、会话五元组这些字段,在不同厂商之间语义对不齐,强行建列会导致表宽到无法维护。放进 JSONB 之后,TimescaleDB 照样能建 GIN 索引按attrs->>'mac'查,代价可控。
A 厂的解析实现,核心就是一个正则加一段映射:
# src/tracking_master/adapters/vendor_a/adapter.py import re from tracking_master.adapters.base import BaseAdapter from tracking_master.models import EntityRef, NormalizedEvent, RawRecord # A 厂设备 syslog 样例: # <134>Mar 12 09:41:07 SW-01 PORT: port 1/0/12 link down, mac 00:1a:2b:3c:4d:5e LINE = re.compile( r"port (?P<port>\S+) link (?P<state>up|down).*?mac (?P<mac>[0-9a-f:]{17})", re.IGNORECASE, ) class Adapter(BaseAdapter): vendor = "vendor_a" capabilities = {"syslog"} default_interval = 30 async def poll(self): # 该系列只上报 syslog,主动采集留空 return yield def parse(self, raw: RawRecord) -> list[NormalizedEvent]: m = LINE.search(raw.message) if not m: return [] # 解析失败不抛异常,交给上层统计 parse_miss 指标 state = m["state"].lower() return [ NormalizedEvent( ts=raw.received_at, kind="link_state", severity="warning" if state == "down" else "info", vendor=self.vendor, device_id=raw.device_id, entity=EntityRef(type="port", key=f"{raw.device_id}:{m['port']}"), attrs={"mac": m["mac"].lower(), "state": state}, source="syslog", ) ] def normalize(self, event: NormalizedEvent) -> NormalizedEvent: # A 厂会把 MAC 写成大写加连字符的变体,这里统一成小写冒号分隔 mac = event.attrs.get("mac", "") event.attrs["mac"] = mac.replace("-", ":").lower() return event三个细节值得掰开说。第一,parse解析失败返回空列表而不是抛异常,因为 syslog 里混着大量调试信息和厂商内部日志,一条不匹配就中断整批,是会真出事故的;上层按parse_miss_total计数并配告警阈值,比抛异常可控。第二,端口实体键用设备ID:端口名的复合形式,不同设备的1/0/12才不会被混成一个东西。第三,normalize里处理 MAC 格式差异,把厂商怪癖挡在存储层之外,看板上就不用做二次清洗。
3.3 去重、时钟对齐与轨迹拼装
单条事件归一化完还只是半成品,真正让数据“能用”的是管道里的三件套:时钟对齐、去重、轨迹拼装。
# src/tracking_master/normalize/pipeline.py async def run(raw_batch, adapter, store, dedup, clock): events = [] for raw in raw_batch: for ev in adapter.parse(raw): ev = adapter.normalize(ev) # 用滑动中位数估设备时钟偏移,超出阈值的事件打标不丢弃 offset = await clock.estimate(raw.device_id, raw.received_at) ev.ts = raw.received_at - offset if offset else ev.ts ev.attrs["clock_offset_s"] = offset or 0 # 指纹去重:TTL 内重复上报直接丢弃 if await dedup.seen(ev.fingerprint(), ttl=180): continue events.append(ev) if events: await store.write_events(events) return eventsclock.estimate用的是笨办法但够用:把同一设备最近若干条“设备时间 - 到达时间”的差值取中位数,作为当前偏移估计。之所以用中位数而不是平均值,是因为网络里偶尔会有一条卡了十几秒才到的日志,平均值会被这种离群点带偏。ttl=180是对着设备的重传间隔调的,常见设备 syslog 重传窗口在 60 到 120 秒之间,取 180 有冗余但不至于把正常的连续抖动吃掉。
轨迹拼装走的是另一条路,输入是 MAC,输出是它在时间窗内经过的端口序列:
# src/tracking_master/graph/trace.py async def build_path(store, mac: str, window: str = "10 minutes"): rows = await store.query_mac_sightings(mac, window) path, last_hop = [], None for r in rows: hop = (r["device_id"], r["port"]) if hop == last_hop: continue # 同一跳重复出现只保留最早一条 path.append({"device": hop[0], "port": hop[1], "ts": r["ts"]}) last_hop = hop return pathwindow参数是给“终端在楼里走动”这种场景留的,无线环境一般给 10 分钟内够用;有线接入层排查环路时我会调到 2 分钟,窗口太长会把上一次接入的位置混进来,路径看着像瞬移。last_hop去重是为了处理同一端口反复上报的情况,否则一条轨迹里全是同一个端口,看图的人会以为在抖。
4. 跑起来:本地起服务、接第一个数据源、看第一条轨迹
4.1 依赖起服务:compose 加迁移加两个进程
整套东西要跑起来只需要 PostgreSQL(带 TimescaleDB 扩展)和 Redis 两个外部依赖,compose 文件里都备好了:
# 1. 取代码进目录 git clone <仓库地址> tracking-master && cd tracking-master # 2. 准备环境变量,样例里有数据库、Redis、SNMP community 的占位 cp deploy/env.example .env # 3. 只起外部依赖,主服务留在本机跑,方便改代码热重载 docker compose -f deploy/docker-compose.yml up -d timescaledb redis # 4. 建虚拟环境并装依赖,[dev] 里带 pytest 和 ruff python -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" # 5. 建表并转成超表,迁移脚本会顺手建好连续聚合视图 alembic upgrade head注意第 3 步只起依赖,不把应用塞进容器。排查解析问题时要反复改adapters/下的代码,容器里改一行重建一次镜像,一天下来时间全耗在构建上。等适配器写完了再整体打包,节奏更顺。
服务是双进程:API 和采集器分开跑,一个挂了不影响另一个。
# 终端 A:API 与 WebSocket,默认 8000 端口 python -m tracking_master.api.main --host 0.0.0.0 --port 8000 # 终端 B:采集器,按 config/adapters.yaml 起所有 collector python -m tracking_master.collector.runner --config config/adapters.yaml --log-level INFO启动后先看采集器日志里那行registered adapters: [vendor_a, vendor_b, generic_snmp]。这行没出现你想要的 vendor,就是目录名或类名写错了,别急着往下走。
4.2 接第一个数据源:syslog 监听与 SNMP 轮询配置
最省事的接入方式是先用 syslog。配置文件里加一段,把监听端口指到 5514(避免和宿主机已有的 514 冲突):
# config/adapters.yaml collectors: - vendor: vendor_a mode: syslog listen: 0.0.0.0:5514 devices: - id: sw-access-01 host: 10.0.0.21 # 用来和来源 IP 做校验,防止串设备 - vendor: generic_snmp mode: poll interval: 30 jitter: 0.2 devices: - id: sw-core-01 host: 10.0.0.11 version: 2c community: ${SNMP_COMMUNITY} oids: if_in_octets: 1.3.6.1.2.1.31.1.1.1.6 if_out_octets: 1.3.6.1.2.1.31.1.1.1.10几个参数值得留意。devices[].id是全局唯一标识,事件的device_id直接取它,所以命名规则最好一次定死,比如“位置-角色-序号”,后面改起来涉及存量数据。host在 syslog 模式下不是用来连的,是用来校验来源 IP 的,防止别的设备把日志发到这个端口造成串数据。generic_snmp用的是标准 MIB-2 的ifXTableOID(64 位计数器),比老的ifTable好在不会因为流量大而在 32 位计数器上回绕,实测千兆口跑满时这个差别很明显。
手动灌一条测试日志验证链路,不用真去拔网线:
# 模拟 A 厂设备发一条 link down,注意用 UDP,syslog 默认无连接 echo '<134>Mar 12 09:41:07 SW-01 PORT: port 1/0/12 link down, mac 00:1A:2B:3C:4D:5E' \ | nc -u -w1 127.0.0.1 5514发完之后采集器日志里应该出现一条parsed=1 missed=0的统计行。如果missed=1,说明正则没匹配上,把这条原文复制到tests/fixtures/vendor_a/下当样本,先在单测里调通再回来看运行时,比在服务里打断点快得多。
4.3 验证链路:接口、看板与自检指标
数据进没进去,别看数据库,先看接口:
# 查最近 15 分钟 sw-access-01 的事件,jq 只取第一条看结构 curl -s "http://127.0.0.1:8000/api/v1/events?device=sw-access-01&from=-15m&limit=1" \ | jq '.[0] | {ts, kind, entity, attrs}' # 查一条 MAC 的移动轨迹,确认拼装逻辑生效 curl -s "http://127.0.0.1:8000/api/v1/trace?mac=00:1a:2b:3c:4d:5e&window=10m" | jq 'length'第一个接口返回里重点看两项:ts是不是 UTC 且和当前时间对得上,attrs.clock_offset_s是不是接近 0。偏移超过几十秒,说明设备那边 NTP 没配好,先去治设备,别在应用层硬扛。第二个接口返回的数组长度大于 1,说明轨迹拼装跑通了;返回空数组但事件接口有数据,八成是attrs.mac的格式和查询参数对不上,大小写或分隔符不一致。
采集器自身也暴露 Prometheus 指标,/metrics里三个数最关键:parse_miss_total(解析失败累计)、collector_poll_duration_seconds(单轮轮询耗时)、dedup_drop_total(去重丢弃数)。parse_miss_total持续上涨基本等于厂商改了日志格式;poll_duration逼近interval说明设备响应慢或 OID 取多了;dedup_drop_total突然暴涨通常意味着某台设备在疯狂重传,先去看设备侧。
前端是独立的 Vite 项目,开发和后端分开跑:
cd web && npm install VITE_API_BASE=http://127.0.0.1:8000 npm run dev进去之后先在拓扑页确认设备节点有没有出来,再去轨迹页按 MAC 搜。第一次跑建议就拿测试日志造三五条事件,人工核对看板显示和curl结果是否一致,比直接接生产设备容易定位问题。
5. 避坑与排查:接入第三方设备时最容易翻车的五处
5.1 数据侧:时钟漂移、重复上报与解析丢失
现象一:同一条链路的 up 和 down 在时间轴上倒挂,轨迹图上出现来回横跳。原因多半是设备没配 NTP,本机时间和设备时间差了几十秒,而早期版本直接用了设备上报的时间戳。解决办法是在归一化层做偏移估计,用“到达时间减设备时间”的滑动中位数修正,并把clock_offset_s写进attrs便于事后核对;同时把clock_drift_max设成 120 秒,超出的直接打clock_skew标记,让看板用不同颜色区分,而不是悄悄改成看起来正常的值。
现象二:同一条告警五分钟内出现十几次,看板上全是毛刺。syslog 走 UDP,设备侧有重传机制,加上双上行组网时同一条日志可能从两条路径各来一份。光靠时间去重不行,因为重传的时间戳可能一模一样。这里靠的是fingerprint():设备 + 实体 + 类型 + 状态拼成的字符串,不含时间戳,dedup_ttl设 180 秒。注意指纹里别放attrs全量内容,否则厂商多带一个序列号字段,指纹就变了,去重直接失效。
现象三:设备固件升级后事件量掉到零,但采集器一切正常。这是最典型的一类翻车:厂商在新固件里给日志加了前缀,或者在 MAC 后面追加了 VLAN 信息,正则匹配不上,parse返回空列表,而空列表不报错,所以日志里看着风平浪静。排查顺序是先看parse_miss_total曲线有没有抬升,再抓一条原始报文丢进tests/fixtures/跑单测。长期做法是把正则里的关键字段顺序放宽,用命名分组加可选段,别写死整行格式。
5.2 采集侧:轮询压力、超时与并发
现象四:接上三十台设备后,交换机管理面响应变慢,甚至 SSH 登不进去。根因往往不是单次请求太重,而是全网设备在同一秒被轮询。interval: 30加上默认jitter: 0就是这种效果。解法是把jitter开到 0.2 以上让调度打散,同时改用GetBulk批量取 OID 而不是逐个Get,并且只取ifXTable里真正需要的列,别为了省事遍历整张接口表。还有一个容易忽略的点:轮询超时要设得比interval小,否则上一轮没结束下一轮又开始,任务会越堆越多,最后 Redis 里的任务锁全被占满。
提示:接入生产设备前,先在非核心设备上把
interval调到 10 秒压一轮,观察collector_poll_duration_seconds和设备管理面 CPU,再决定正式周期。参数拍脑袋定,后面全是返工。
5.3 存储侧:标签基数与写入放大
现象五:跑了一周磁盘涨了几十 G,查询还越来越慢。打开表结构一看,mac、port、session_id全被当成了索引列或者标签。时间序列表最怕高基数维度,一张表几百万个不同 MAC,索引体积能超过数据本身。正确做法是标签只保留低基数字段:device_id、vendor、kind,把 MAC、端口、五元组这些放进 JSONB 的attrs,需要查的时候建 GIN 索引或者用表达式索引单独开一个。另外原始事件按retention_days定期清理,聚合视图单独设更长的保留期,看板查的是聚合结果,不查原始明细。
6. 二次开发:加一个自定义供应商适配器并做回归验证
真正落地时,几乎一定会遇到“官方没覆盖的那家设备”。加适配器的完整动作其实只有四步,但第四步最容易被跳过。
第一步,在adapters/下新建目录,比如vendor_c/,放一个adapter.py,类名统一叫Adapter,vendor填唯一键。第二步,把抓到的真实报文存成 fixture,一条 link down、一条 link up、一条无关的调试日志,后者用来验证“解析失败返回空列表”这条约定。第三步,写解析和normalize,把厂商乱七八糟的字段在normalize里洗干净,别留到存储层。第四步,跑回归。
回归这套我是被坑出来的。早先加完适配器只手动灌了两条日志看接口有数据就上线了,结果那家设备在端口震荡时会连发五条状态相同的日志,去重指纹因为带了个自增序列号字段而完全不同,看板上一条链路抖成了心电图。后来补了下面这组测试:
| 测试项 | 断言内容 | 覆盖的坑 |
|---|---|---|
| 快照解析 | parse(fixture)输出与expected.json完全一致 | 正则改动导致的字段漂移 |
| 空输入 | 无关日志返回[]且不抛异常 | 混入调试日志时整批中断 |
| 指纹稳定性 | 同状态不同序列号的两条事件指纹相同 | 去重失效导致毛刺 |
| 实体键唯一性 | 两台设备的同名端口key不相等 | 跨设备实体混淆 |
| 时钟偏移 | 手工把received_at推后 40 秒,clock_offset_s应接近 40 | 轨迹时间轴倒挂 |
测试本身不复杂,pytest加参数化就够,关键是把它挂进 CI,改了normalize里的任何一行都必须跑一遍。
有个偏工程化的技巧值得单独说:给新适配器加一个“影子模式”。配置里加shadow: true,采集器照常解析、照常打日志和指标,但写库时跳过。挂一天看parse_miss_total和解析出的事件量曲线,跟设备侧的实际告警数对得上,再切正式写库。这招比任何灰度方案都便宜,也基本杜绝了“上线才发现字段全错、历史数据全脏”这种需要清表重来的局面。
从那以后我每次接新厂商,都强制走一遍「fixture 三条 → 五项回归 → 影子跑一天」这个流程,哪怕只加一个字段的解析也不例外。慢是慢了半天,但省下的是整张表重刷的工夫。希望帮到你。
本文还有配套的精品资源,点击获取