- 后端
- 网络
- 运维
- 数据可视化
【免费下载链接】NetAlertX
Centralized network visibility and continuous asset discovery. Monitor devices, detect change, and stay aware across distributed networks.
NetAlertX 是一个集中式网络可见性与持续资产发现系统,所有设备数据最终都落在 SQLite 的Devices表中。本文以仓库内 .gemini/skills/database-patterns/SKILL.md 为骨架,结合server/db/authoritative_handler.py、server/db/db_history.py、server/models/device_instance.py与server/scan/device_handling.py等源码实现,系统讲解在设计任何"写 Devices 表"或"审计/历史日志"类功能前必须掌握的三大模式:完整的写入路径清单、基于*Source列的字段溯源体系,以及"SQLite 触发器优先于 Python 钩子"的横切关注点取舍。读完本文,你将能正确判断一个功能应该用触发器还是 Python 钩子实现,能写出带changedBy归属的事件溯源审计日志,并理解 NetAlertX 中DevicesHistory表与DEV_HIST_DAYS/DEV_HIST_TRACKED设置的真实运行机制。
Devices 表写入路径清单:动手前的强制审计
Devices表是 NetAlertX 的核心资产表,被扫描、用户操作、工作流、通知、清理任务等众多位置修改。该技能文档给出的首要规则是:在实现任何读写Devices表的功能之前,必须审计所有写入路径——漏掉一条路径就是正确性缺陷(例如审计日志会漏记某类变更,字段锁会被某条路径绕过)。
仓库中确认的生产写入路径如下表所示:
| 文件 | 函数 | 写入的字段 |
|---|---|---|
server/models/device_instance.py | setDeviceData() | 所有用户可编辑字段 |
server/models/device_instance.py | updateField() | 任意单个字段(工作流使用) |
server/models/device_instance.py | updateDeviceColumn() | 任意单个列 |
server/models/device_instance.py | deleteDevices()等 | DELETE 操作 |
server/scan/device_handling.py | update_devices_data_from_scan() | 扫描派生字段 |
server/scan/device_handling.py | update_vendors_from_mac() | devVendor、devVendorSource |
server/scan/device_handling.py | 名称解析代码块 | devName、devFQDN、*Source |
server/scan/device_handling.py | update_ipv4_ipv6() | devPrimaryIPv4、devPrimaryIPv6 |
server/scan/device_handling.py | update_icons_and_types() | devIcon、devType |
server/scan/device_handling.py | update_presence_from_CurrentScan() | devPresentLastScan |
server/scan/device_handling.py | update_devLastConnection_from_CurrentScan() | devLastConnection |
server/scan/device_handling.py | update_devPresentLastScan_based_on_*() | devPresentLastScan |
server/db/authoritative_handler.py | enforce_source_on_user_update() | *Source列 |
server/db/authoritative_handler.py | lock_field()/unlock_field() | *Source列 |
server/models/notification_instance.py | clearPendingEmailFlag() | devLastNotification |
server/plugins/db_cleanup/script.py | cleanup_database() | DELETE 操作 |
为什么"逐行 Python 钩子"行不通
清单中一个关键洞察值得重点强调:大部分扫描函数使用sql.executemany()批量更新,因此不存在"每行一个 Python 状态"可供钩子读取。以 server/scan/device_handling.py 中的update_ipv4_ipv6()为例,它对整批设备一次性执行UPDATE Devices SET devPrimaryIPv4 = COALESCE(NULLIF(?, ''), devPrimaryIPv4), ...;update_icons_and_types()、update_vendors_from_mac()同样通过executemany()批量写库。若要在 Python 层实现"写前/写后钩子",就必须先预取整表、逐行 diff 再回写,这种 pre-fetch+diff 模式既昂贵又容易出错,还会引入竞态窗口——这正是该技能文档推荐触发器方案的底层原因。
*Source 字段溯源体系:每一笔写入都有归属
Devices表中存在一组配对字段:主字段(如devName)加上对应的*Source列(如devNameSource),用于记录"这个值是谁写的"。
FIELD_SOURCE_MAP:10 个受溯源保护的字段
server/db/authoritative_handler.py 顶部定义了FIELD_SOURCE_MAP,将 10 个字段映射到各自的溯源列:
FIELD_SOURCE_MAP = { "devMac": "devMacSource", "devName": "devNameSource", "devFQDN": "devFQDNSource", "devLastIP": "devLastIPSource", "devVendor": "devVendorSource", "devSSID": "devSSIDSource", "devParentMAC": "devParentMACSource", "devParentPort": "devParentPortSource", "devParentRelType": "devParentRelTypeSource", "devVlan": "devVlanSource", }*Source列的取值是枚举性的:'USER'(用户手动修改)、'LOCKED'(用户显式锁定,插件不得覆盖)、'NEWDEV'(新设备创建时的占位溯源),或某个插件前缀(如'ARPSCAN'、'NSLOOKUP'、'UNIFIAPI'、'VNDRPDT')。
溯源写入发生在同一事务内
关键设计点是:溯源字段与主字段在同一条 SQL 语句中、同一事务内一起更新。例如update_devices_data_from_scan()在允许覆盖时构造UPDATE Devices SET devName = ?, devNameSource = ? WHERE devMac = ?,源值取插件前缀;server/scan/device_handling.py 中的create_new_devices()在插入新设备时逐字段调用get_source_for_field_update_with_value(),对空值/未知占位值(NULL_EQUIVALENTS,如(unknown)、0.0.0.0)返回NEWDEV,否则返回插件前缀。正因为主字段与溯源字段同事务落库,SQLiteAFTER UPDATE触发器才能直接读取NEW.devNameSource拿到正确归属,无需任何额外的上下文传递。
归属(changedBy)判定规则
对于需要changedBy的功能,按字段类别区分归属:
| 字段类别 | 归属 |
|---|---|
在FIELD_SOURCE_MAP中的字段 | COALESCE(NULLIF(NEW.<field>Source, ''), 'system') |
仅用户可写字段(devGroup、devComments、devFavorite、devOwner、devLocation等) | 'user:api'——只有setDeviceData()写这些字段 |
自动计算字段(devIcon、devType、devPrimaryIPv4、devPrimaryIPv6) | 'system' |
*Source字段本身 | 'system' |
这条规则表在 server/db/db_history.py 的_HIST_FIELDS配置中被逐字落地:10 个溯源字段使用COALESCE(NULLIF(TRIM(NEW.devNameSource), ''), 'system')形式的表达式,devOwner/devGroup/devComments等 15 个用户字段硬编码'user:api',devPrimaryIPv4/devIcon/devSyncHubNode等自动计算字段硬编码'system'。
权威覆盖规则:谁可以覆盖谁
authoritative_handler.py中的can_overwrite_field()定义了插件覆盖字段的完整裁决链,其规则被 test/scan/test_authoritative_handler.py 逐条验证:
- USER/LOCKED 保护:当前源为
USER或LOCKED时一律拒绝覆盖; - 非空校验:新值为空、空白字符串或
NULL_EQUIVALENTS时拒绝; - 同值刷新:新旧值相同时允许(用于刷新溯源字段);
- SET_ALWAYS:字段在插件的
SET_ALWAYS列表中时允许覆盖(只要非空); - SET_EMPTY:字段在
SET_EMPTY列表中时仅当当前值为空才允许; - 默认策略:仅当当前值为空时才允许覆盖;
- 特殊开关:
allow_override_if_changed(如devLastIP的FIELD_SPECS配置)允许在值发生变化时覆盖。
对应的 SQL 片段由get_overwrite_sql_clause()生成,用于把裁决下推到批量更新语句中。
横切关注点:优先选择 SQLite 触发器而非 Python 钩子
当功能需要拦截每一次对Devices表的写入(审计日志、计算列、级联逻辑)时,技能文档明确建议:优先使用SQLiteAFTER UPDATE/AFTER INSERT触发器,而不是 Python 层钩子。
为什么优先触发器:
- 自动覆盖全部 14+ 条写入路径,包括
executemany()批量更新——不用在每条写入函数里埋点; - 对既有写入函数零修改(DRY);
- 自愈性:未来新增的写入路径自动纳入覆盖;
- 归属信息可直接通过
NEW.*Source字段获取。
什么时候仍然适合 Python 钩子:
- 逻辑需要访问 SQL 中不可用的 Python 对象、设置或服务;
- 功能只从一两条已知写入路径触发;
- 逻辑复杂到难以用 SQL 表达(多表 join 叠加应用层业务规则)。
触发器性能模式模板
技能文档给出的触发器模板(可完整复制使用):
CREATE TRIGGER trg_example AFTER UPDATE ON Devices FOR EACH ROW -- Guard: short-circuit entire body when feature is disabled (zero cost) WHEN (SELECT CAST(setValue AS INTEGER) FROM Settings WHERE setKey = 'FEATURE_ENABLED') > 0 BEGIN -- Per-field conditional insert INSERT INTO SomeTable (devGUID, column, oldVal, newVal, changedBy, ts) SELECT NEW.devGUID, 'devName', OLD.devName, NEW.devName, COALESCE(NULLIF(NEW.devNameSource, ''), 'system'), datetime('now', 'utc') WHERE OLD.devName IS NOT NEW.devName AND instr(',' || (SELECT setValue FROM Settings WHERE setKey = 'TRACKED_FIELDS') || ',', ',devName,') > 0; -- Repeat for each tracked field... END;该模板包含三个关键技巧:WHEN 守卫(功能关闭时整个触发器体零开销短路)、逐字段条件插入(WHERE OLD.x IS NOT NEW.x只记录真实变化)、设置驱动的字段跟踪(用instr()判断字段是否在跟踪列表中)。性能方面,Settings表很小(约 100 行),常驻 SQLite 页缓存,触发器内逐行读取设置实质上是内存查找。
快照式 vs 事件溯源式审计日志
实现变更历史时,技能文档要求始终使用事件溯源式(按字段逐行记录),而不是快照式(整行复制):
| 对比维度 | 事件溯源式 | 快照式 |
|---|---|---|
| 存储 | 小——只记录变化的字段 | 大——每次变更复制全部 40+ 列 |
| 按字段过滤 | O(log n),走索引 | O(n)——必须逐对 diff |
| 按来源过滤 | O(log n),走索引 | 无法做到(除非 diff) |
changedBy归属 | 写入时即嵌入 | 无额外上下文则不可得 |
| 保留期计算 | 简单按时间戳 DELETE | 相同,但存储量高得多 |
技能文档给出了一组量级估算:1000 台设备、5 分钟扫描间隔、14 天保留期下,快照式存储约280 MB/天;而事件溯源式同样负载通常<1 MB/天(因为大多数扫描周期不会产生受跟踪字段的变化)。这个数量级差异正是 NetAlertX 选择事件溯源方案的核心动因。
DevicesHistory 表:参考 Schema 与真实实现
参考 Schema
技能文档给出的DevicesHistory参考建表语句:
CREATE TABLE IF NOT EXISTS DevicesHistory ( id INTEGER PRIMARY KEY AUTOINCREMENT, devGUID TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, changedBy TEXT NOT NULL, changedColumn TEXT NOT NULL, oldValue TEXT, newValue TEXT, FOREIGN KEY (devGUID) REFERENCES Devices(devGUID) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_devhist_guid_column ON DevicesHistory(devGUID, changedColumn); CREATE INDEX IF NOT EXISTS idx_devhist_timestamp ON DevicesHistory(timestamp);仓库中的真实落地:db_history.py
该 schema 在 server/db/db_history.py 中原样落地(含两索引),并由两个设置驱动:
DEV_HIST_DAYS:保留天数,0时整体禁用审计引擎(作为两个触发器的WHEN守卫);DEV_HIST_TRACKED:逗号分隔的字段名列表,决定审计哪些列。
back/app.conf 中的默认配置为DEV_HIST_DAYS=1、DEV_HIST_TRACKED=['devMac','devName','devOwner','devType','devVendor','devFavorite','devGroup','devComments','devLastIP','devFQDN','devPrimaryIPv4','devPrimaryIPv6','devVlan','devForceStatus','devStaticIP','devScan','devAlertDown','devCanSleep','devSkipRepeated','devLocation','devIsArchived','devParentMAC','devParentPort','devParentRelType','devReqNicsOnline','devIcon','devSite','devSSID','devSyncHubNode'],用户可在设置页(设置键DEV_HIST_DAYS/DEV_HIST_TRACKED)调整,将DEV_HIST_DAYS设为0即完全禁用。
实现上,ensure_deviceshistory_table()幂等建表(IF NOT EXISTS),并回填devGUID为空的 Devices 行(用 SQL 生成 UUIDv4 风格的 GUID),保证触发器能为其写入历史;ensure_deviceshistory_triggers()每次启动/升级时先DROP TRIGGER IF EXISTS再重建,确保触发器逻辑随版本刷新。
_build_update_trigger_sql()为每个跟踪字段生成一段INSERT ... SELECT,其核心为:
INSERT INTO DevicesHistory (devGUID, changedColumn, oldValue, newValue, changedBy, timestamp) SELECT NEW.devGUID, 'devName', CAST(OLD.devName AS TEXT), CAST(NEW.devName AS TEXT), COALESCE(NULLIF(TRIM(NEW.devNameSource), ''), 'system'), datetime('now') WHERE (OLD.devName IS NOT NEW.devName) AND COALESCE(NEW.devGUID, '') != '' AND instr((SELECT COALESCE(setValue, '') FROM Settings WHERE setKey = 'DEV_HIST_TRACKED'), 'devName') > 0;可见这正是技能文档"触发器性能模式模板"的完整生产级版本:WHEN 守卫取DEV_HIST_DAYS,changedBy取NEW.devNameSource的COALESCE(NULLIF(...), 'system')归并,WHERE中IS NOT判异并配合DEV_HIST_TRACKED的instr()字段过滤。_build_insert_trigger_sql()生成对应的AFTER INSERT触发器,oldValue记NULL、newValue取NEW.<field>。这一实现与技能文档的描述完全吻合,也印证了"同一事务内写溯源字段"这一前提的可操作性。
数据如何被消费
DevicesHistory的数据通过 GraphQL API 暴露(docs/API_GRAPHQL.md中说明历史跟踪由DEV_HIST_DAYS与DEV_HIST_TRACKED控制、DEV_HIST_DAYS = 0完全禁用),并在前端设备详情页的变更日志(Change log)中展示。设置项说明可参考 docs/DEVICE_CHANGE_LOG.md 与 docs/PERFORMANCE.md——后者特别提醒:为改善性能,可通过收窄DEV_HIST_TRACKED与调小DEV_HIST_DAYS来控制历史表增长。
实践清单:为 Devices 表设计新功能时的决策路径
综合上述模式,为一个"写 Devices 表或做审计"的新功能给出可操作的决策路径:
- 先盘写入路径:对照上文写入路径清单,确认你的功能是否引入了新的写入入口,审计日志/锁机制是否会漏掉它;
- 选写入方式:如果是横切关注点(每次写入都要生效),用
AFTER UPDATE/AFTER INSERT触发器;如果只服务于单一路径且需要 Python 侧对象/服务,才用 Python 钩子; - 用溯源做归属:需要
changedBy时,按字段类别套用归属规则表;在触发器中通过NEW.*Source读取,避免任何上下文传递; - 选审计模型:一律使用事件溯源式(按字段行),参考
DevicesHistoryschema 与db_history.py的字段过滤、WHEN 守卫写法; - 用设置控制成本:参考
DEV_HIST_DAYS/DEV_HIST_TRACKED的双设置模式,为你的功能提供"总开关 + 字段级开关",并利用WHEN守卫让关闭态零开销。
结语
NetAlertX 的 Devices 表写入设计展示了三条可复用的工程经验:写入路径必须显式清单化以避免"漏埋点";字段级溯源列(*Source)让归属信息与主字段同事务落库,为下游触发器提供零成本的changedBy;跨切面的审计需求应下沉到 SQLite 触发器,用WHEN守卫与instr()字段过滤实现"关闭零成本、开启仅记变化"。无论你是要扩展 NetAlertX 的插件生态(server/plugins 下的各插件以插件前缀写入*Source),还是在自己项目中设计类似的设备清单数据库,这份模式清单都能直接迁移使用。相关源码入口:server/db/authoritative_handler.py、server/db/db_history.py、server/models/device_instance.py、server/scan/device_handling.py、test/scan/test_authoritative_handler.py。
- 后端
- 网络
- 运维
- 数据可视化
【免费下载链接】NetAlertX
Centralized network visibility and continuous asset discovery. Monitor devices, detect change, and stay aware across distributed networks.
相关推荐
NetAlertX 数据库写入模式全解析:Devices 表写入路径、*Source 归属系统与 SQLite 触发器审计实践
NetAlertX 数据库写入模式全解析:Devices 表写入路径、 Source 归属系统与 SQLite 触发器审计实践 本文是 NetAlertX 仓库
后端网络运维数据可视化Electric 写入路径实战指南:四种本地写入与写路径同步模式
Electric 写入路径实战指南:四种本地写入与写路径同步模式 本文围绕 Electric 同步平台的写路径(write path)展开:Electric 负
后端数据同步数据库人工智能AI AgentMCP 服务treg 数据模型全解析:注册表表结构、异步数据库池与审计写入器
treg 数据模型全解析:注册表表结构、异步数据库池与审计写入器 导读 本文是 treg(OpenRouter for agent tools,一个面向 Age
后端API网关MCP 服务dsh-plugin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考