蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #06 操作审计日志:谁在什么时候做了什么
蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— #06 操作审计日志:谁在什么时候做了什么
学习重点
- 理解 AuditLog 顶层结构:谁、在什么时候、对什么资源、做了什么操作
- 掌握 ActionType 三类:create/update/delete 与 assign_host/transfer_host_module 的区别
- 理解 BasicContent 快照模式:PreData(变更前)↔ CurData(变更后)↔ UpdateFields(本次变更字段)
- 理解 HostTransferOpDetail 专门为主机拓扑转移设计,记录 PreData/CurData 双重拓扑快照
- 理解 WithPrevious / WithCurrent 双时刻快照的短路设计(只执行一次)
- 理解 SaveAuditLog 写入链路:scene_server → CoreService API → MongoDB cc_AuditLog
- 理解 DetailFactory:根据 ResourceType 自动派发操作详情类型的 BSON 反序列化机制
目录
一、SRE 痛点:主机被误删了,查不到是谁干的
想象这样一个 SRE 场景:凌晨 2 点,告警系统发现某台主机 192.168.1.100 突然从监控列表中消失了。查了一圈发现,这台主机被某人在 10 分钟前删除了。但"某人"是谁?什么时候删的?删之前这台主机在哪个业务、哪个集群、哪个模块?
这类需求在大型运维团队中极为普遍——谁在什么时候做了什么变更,变更前后的数据是什么。蓝鲸 CMDB 的 auditlog 模块就是为解决这类问题而设计的。
SRE 痛点提炼
- 合规要求:等保/ISO27001 等合规标准要求记录所有配置变更,且不可篡改
- 故障定位:变更引发故障后,需要快速定位"这次故障和哪个变更相关"
- 责任追溯:出现配置错误时,需要知道是谁、在什么时间、做了什么操作
CMDB 的审计日志方案核心在于三点:快照双时刻(变更前 + 变更后)、操作详情分类(通用属性变更用 BasicContent,主机拓扑转移用 HostTransferOpDetail)、不可变存储(写入 cc_AuditLog 后仅追加,不修改不删除)。
2. 审计日志核心数据结构
2.1 AuditLog 顶层结构
在 src/common/metadata/audit.go 第 174-208 行,定义了审计日志的核心结构:
// AuditLog — 审计日志顶层结构(L174-208)
type AuditLog struct {
// AuditType 是资源的高层分类:business / host / model / business_resource / cloud_resource / kube 等
AuditType AuditType `json:"audit_type" bson:"audit_type"`
// SupplierAccount 多租户隔离字段
SupplierAccount string `json:"bk_supplier_account" bson:"bk_supplier_account"`
// User 操作用户名(如 "zuoyang")
User string `json:"user" bson:"user"`
// ResourceType 资源类型:business / host / set / module / process 等
ResourceType ResourceType `json:"resource_type" bson:"resource_type"`
// Action 操作类型:create / update / delete / assign_host / transfer_host_module 等
Action ActionType `json:"action" bson:"action"`
// OperateFrom 操作来源:user(用户操作)/ cc_system(系统升级)/ data_collection(数据采集)
OperateFrom OperateFromType `json:"operate_from" bson:"operate_from"`
// OperationDetail 变更详情(类型由 ResourceType 决定,见 DetailFactory 表)
OperationDetail DetailFactory `json:"operation_detail" bson:"operation_detail"`
// OperationTime 操作时间(自动填充)
OperationTime Time `json:"operation_time" bson:"operation_time"`
// BusinessID 所属业务 ID(仅业务资源填)
BusinessID int64 `json:"bk_biz_id,omitempty" bson:"bk_biz_id,omitempty"`
// ResourceID 被操作资源的 ID(主机 ID、业务 ID 等)
ResourceID interface{} `json:"resource_id" bson:"resource_id"`
// ResourceName 被操作资源的名称(主机 IP、业务名称等)
ResourceName string `json:"resource_name" bson:"resource_name"`
// RequestID 请求链路 ID(用于关联同一个请求内的所有日志)
RequestID string `json:"rid,omitempty" bson:"rid,omitempty"`
// ExtendResourceName IPv6 地址(主机名扩展)
ExtendResourceName string `json:"extend_resource_name" bson:"extend_resource_name"`
}
2.2 ActionType 操作类型常量
在 src/common/metadata/audit.go 第 789-822 行,定义了所有操作类型:
// ActionType(L789-822)
const (
AuditCreate ActionType = "create" // 创建资源
AuditUpdate ActionType = "update" // 更新资源
AuditDelete ActionType = "delete" // 删除资源
AuditAssignHost ActionType = "assign_host" // 主机从资源池分配到业务
AuditUnassignHost ActionType = "unassign_host" // 主机从业务退回资源池
AuditTransferHostModule ActionType = "transfer_host_module" // 主机在业务内转移模块
AuditArchive ActionType = "archive" // 归档
AuditRecover ActionType = "recover" // 恢复
AuditPause ActionType = "stop" // 停用
AuditResume ActionType = "resume" // 启用
)
2.3 DetailFactory:ResourceType 到操作详情的类型映射
在 src/common/metadata/audit.go 第 251-269 行,resTypeOpDetailTypeMap 根据资源类型自动选择操作详情结构体:
// DetailFactory 接口(L247-249)
type DetailFactory interface {
WithName() string
}
// resTypeOpDetailTypeMap(L251-269):资源类型 → 操作详情类型
var resTypeOpDetailTypeMap = map[ResourceType]DetailFactory{
BusinessRes: new(InstanceOpDetail), // 业务/集群/模块
BizSetRes: new(InstanceOpDetail),
ProjectRes: new(InstanceOpDetail),
SetRes: new(InstanceOpDetail),
ModuleRes: new(InstanceOpDetail),
ProcessRes: new(InstanceOpDetail),
HostRes: new(InstanceOpDetail), // 主机通用操作
CloudAreaRes: new(InstanceOpDetail),
ModelInstanceRes: new(InstanceOpDetail),
MainlineInstanceRes: new(InstanceOpDetail),
ServiceInstanceRes: new(ServiceInstanceOpDetail), // 服务实例
InstanceAssociationRes: new(InstanceAssociationOpDetail), // 关联关系
}
2.4 通用操作详情:BasicContent
最通用的操作详情结构体是 BasicContent,在 src/common/metadata/audit.go 第 570-577 行定义:
// BasicContent(L570-577):create / update / delete 三种操作共用
type BasicContent struct {
PreData map[string]interface{} `json:"pre_data" bson:"pre_data"` // 删除/更新前数据
CurData map[string]interface{} `json:"cur_data" bson:"cur_data"` // 创建后的数据
UpdateFields map[string]interface{} `json:"update_fields" bson:"update_fields"` // 本次更新的字段(仅 update)
}
关键设计:PreData 和 CurData 是互斥的——创建操作只填 CurData,删除操作只填 PreData,更新操作两者都填并额外记录 UpdateFields。这个逻辑在 src/common/auditlog/metadata.go 第 49-67 行的 NewBasicContent() 方法中实现。
2.5 cc_AuditLog 表名与索引
审计日志写入 MongoDB 的表名在 src/common/tablenames.go 第 64 行定义:
// src/common/tablenames.go:64
BKTableNameAuditLog = "cc_AuditLog"
索引定义在 src/common/index/collections/auditlog.go 第 25 行注册,索引包含:user(按用户查)、bk_biz_id(按业务查)、operation_time(按时间查)、resource_type(按资源类型查)、action(按操作类型查),支持高并发组合查询。
3. 快照模式:变更前后双时刻记录
3.1 generateAuditCommonParameter:审计参数构建器
在 src/common/auditlog/metadata.go 第 20-68 行,generateAuditCommonParameter 是 Builder 模式的参数构建器:
// generateAuditCommonParameter(L20-26)
type generateAuditCommonParameter struct {
kit *rest.Kit // 请求上下文(含用户身份、RequestID)
action metadata.ActionType // 操作类型(create/update/delete)
operateFrom metadata.OperateFromType // 来源(user / cc_system / data_collection)
updateFields map[string]interface{} // 本次更新的字段(仅 update)
}
// NewGenerateAuditCommonParameter(L28-34):构造器
func NewGenerateAuditCommonParameter(kit *rest.Kit, action metadata.ActionType) *generateAuditCommonParameter {
return &generateAuditCommonParameter{
kit: kit,
action: action,
}
}
// WithUpdateFields(L42-46):链式调用,追加本次更新的字段
func (a *generateAuditCommonParameter) WithUpdateFields(updateFields map[string]interface{}) *generateAuditCommonParameter {
a.updateFields = updateFields
return a
}
// NewBasicContent(L48-68):根据 action 自动生成 PreData / CurData / UpdateFields
func (a *generateAuditCommonParameter) NewBasicContent(data map[string]interface{}) *metadata.BasicContent {
var basicDetail *metadata.BasicContent
switch a.action {
case metadata.AuditCreate:
basicDetail = &metadata.BasicContent{CurData: data} // 只填 CurData
case metadata.AuditDelete:
basicDetail = &metadata.BasicContent{PreData: data} // 只填 PreData
case metadata.AuditUpdate:
basicDetail = &metadata.BasicContent{
PreData: data, // 更新前的完整数据
UpdateFields: a.updateFields, // 本次修改的字段
}
}
return basicDetail
}
3.2 hostAuditLog.generateAuditLog:主机审计生成
在 src/common/auditlog/host.go 第 29-97 行,hostAuditLog 实现主机级别的审计生成。导出的 GenerateAuditLog(L29-34)直接转发给内部方法 generateAuditLog(L47-97):
// GenerateAuditLog(L29-34):公开入口,内部转发给 generateAuditLog
func (h *hostAuditLog) GenerateAuditLog(parameter *generateAuditCommonParameter, bizID int64,
data []mapstr.MapStr) ([]metadata.AuditLog, error) {
return h.generateAuditLog(parameter, bizID, data)
}
// generateAuditLog(L47-97):实际生成逻辑,遍历主机列表为每台生成一条 AuditLog
func (h *hostAuditLog) generateAuditLog(parameter *generateAuditCommonParameter, bizID int64,
data []mapstr.MapStr) ([]metadata.AuditLog, error) {
kit := parameter.kit
if len(data) == 0 {
return nil, kit.CCError.CCErrorf(common.CCErrCommParamsNeedSet, "host audit log data")
}
auditLogs := make([]metadata.AuditLog, len(data))
hostIDs := make([]int64, len(data)) // 收集所有主机 ID
for index, host := range data {
hostID, err := util.GetInt64ByInterface(host[common.BKHostIDField])
if err != nil {
return nil, kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, common.BKHostIDField)
}
hostIDs[index] = hostID
// 根据 action 自动填 PreData / CurData / UpdateFields(NewBasicContent 实现)
auditLog := metadata.AuditLog{
AuditType: metadata.HostType,
ResourceType: metadata.HostRes,
Action: parameter.action,
BusinessID: bizID,
ResourceID: hostIDs[index],
ResourceName: util.GetStrByInterface(host[common.BKHostInnerIPField]),
ExtendResourceName: util.GetStrByInterface(host[common.BKHostInnerIPv6Field]),
OperateFrom: parameter.operateFrom,
OperationDetail: &metadata.InstanceOpDetail{
BasicOpDetail: metadata.BasicOpDetail{
Details: parameter.NewBasicContent(host),
},
ModelID: common.BKInnerObjIDHost,
},
}
auditLogs[index] = auditLog
}
// 若 bizID==0(主机不在任何业务中,如新建主机在资源池),反查业务 ID
if bizID == 0 {
hostBizMap, err := h.getBizIDByHostID(parameter.kit, hostIDs)
if err != nil {
return nil, err
}
for index := range auditLogs {
auditLogs[index].BusinessID = hostBizMap[hostIDs[index]]
}
}
return auditLogs, nil
}
3.3 审计写入链路:SaveAuditLog
所有审计日志最终通过 src/common/auditlog/audit.go 第 33-36 行的 SaveAuditLog 写入 MongoDB:
// SaveAuditLog(L33-36):经 CoreService API 写入 cc_AuditLog 表
func (a *audit) SaveAuditLog(kit *rest.Kit, logs ...metadata.AuditLog) errors.CCErrorCoder {
return a.clientSet.Audit().SaveAuditLog(kit.Ctx, kit.Header, logs...)
}
对应的 API 路由在 src/source_controller/coreservice/service/service_initfunc.go 第 346-349 行注册:
// service_initfunc.go:340-352
func (s *coreService) audit(web *restful.WebService) {
utility.AddHandler(rest.Action{Verb: http.MethodPost, Path: "/create/auditlog",
Handler: s.CreateAuditLog})
utility.AddHandler(rest.Action{Verb: http.MethodPost, Path: "/read/auditlog",
Handler: s.SearchAuditLog})
utility.AddToRestfulWebService(web)
}
写入链路总结:scene_server(业务逻辑层)→ auditlog 包(生成审计日志)→ CoreService API → MongoDB cc_AuditLog。整个链路通过 rest.Kit 携带用户身份(kit.SupplierAccount、kit.User)和请求链路 ID(kit.Rid),确保审计日志的归属清晰可溯。
4. 主机转移的专属审计:HostTransferOpDetail
4.1 为什么要单独设计?
主机在业务间转移(资源池 ↔ 业务、业务 A → 业务 B)和在业务内转移模块(MySQL 模块 → PaymentServer 模块),不仅仅是"属性变更",而是涉及 拓扑结构的改变。通用 BasicContent 只记录主机自身字段,无法描述"从哪个业务的哪个集群哪个模块,转移到了哪里"。
CMDB 为此专门设计了 HostTransferOpDetail,在 src/common/metadata/audit.go 第 464-476 行:
// HostTransferOpDetail(L464-469):主机转移专属操作详情
type HostTransferOpDetail struct {
// PreData 转移前的业务拓扑(biz + set[] + module[])
PreData HostBizTopo `json:"pre_data" bson:"pre_data"`
// CurData 转移后的业务拓扑
CurData HostBizTopo `json:"cur_data" bson:"cur_data"`
}
// HostBizTopo(L471-476):主机所在业务拓扑的快照
type HostBizTopo struct {
BizID int64 `json:"bk_biz_id" bson:"bk_biz_id"` // 业务 ID
BizName string `json:"bk_biz_name" bson:"bk_biz_name"` // 业务名称
Set []Topo `json:"set" bson:"set"` // 集群列表
}
// Topo(hostserver.go L678-682):单个集群及其模块信息
type Topo struct {
SetID int64 `json:"bk_set_id" bson:"bk_set_id"` // 集群 ID
SetName string `json:"bk_set_name" bson:"bk_set_name"` // 集群名称
Module []Module `json:"module" bson:"module"` // 该集群下的所有模块
}
// Module(hostserver.go L704-707):单个模块信息
type Module struct {
ModuleID int64 `json:"bk_module_id" bson:"bk_module_id"` // 模块 ID
ModuleName string `json:"bk_module_name" bson:"bk_module_name"` // 模块名称
}
4.2 WithPrevious / WithCurrent:拓扑快照捕获
在 src/common/auditlog/hostmodule.go 第 28-69 行,hostModuleLog 实现了双时刻拓扑快照:
type hostModuleLog struct {
audit audit // CoreService 客户端
hostIDArr []int64 // 待审计的主机 ID 列表
pre []metadata.ModuleHost // 转移前拓扑快照
cur []metadata.ModuleHost // 转移后拓扑快照
}
// NewHostModuleLog(L36-43):创建主机拓扑审计记录器
func NewHostModuleLog(clientSet coreservice.CoreServiceClientInterface, hostID []int64) *hostModuleLog {
return &hostModuleLog{
audit: audit{clientSet: clientSet},
hostIDArr: hostID,
}
}
// WithPrevious(L45-56):捕获转移前的拓扑快照(只执行一次)
func (h *hostModuleLog) WithPrevious(kit *rest.Kit) errors.CCError {
if h.pre != nil {
return nil // 已捕获则跳过
}
h.pre, err = h.getHostModuleConfig(kit) // 查询 cc_ModuleHostConfig 表
return err
}
// WithCurrent(L58-69):捕获转移后的拓扑快照(只执行一次)
func (h *hostModuleLog) WithCurrent(kit *rest.Kit) errors.CCError {
if h.cur != nil {
return nil // 已捕获则跳过
}
h.cur, err = h.getHostModuleConfig(kit)
return err
}
// getHostModuleConfig(L171-184):查询主机当前所属业务-集群-模块关系
func (h *hostModuleLog) getHostModuleConfig(kit *rest.Kit) ([]metadata.ModuleHost, errors.CCError) {
conds := &metadata.HostModuleRelationRequest{
HostIDArr: h.hostIDArr,
Fields: []string{common.BKAppIDField, common.BKSetIDField,
common.BKModuleIDField, common.BKHostIDField},
}
result, err := h.audit.clientSet.Host().GetHostModuleRelation(kit.Ctx, kit.Header, conds)
if err != nil {
return nil, kit.CCError.Error(common.CCErrCommHTTPDoRequestFailed)
}
return result.Info, nil
}
4.3 SaveAudit:主机转移审计的完整生成与写入
在 src/common/auditlog/hostmodule.go 第 72-169 行,SaveAudit 是主机转移审计的核心实现:
// SaveAudit(L72-169):生成主机转移审计日志并写入 cc_AuditLog
func (h *hostModuleLog) SaveAudit(kit *rest.Kit) errors.CCError {
// 1. 获取主机 IP/IPv6 信息
hostInfos, err := h.getInnerIPAndInnerIPv6(kit)
if err != nil { return err }
// 2. 确保捕获了当前拓扑快照(如果调用方没先调 WithCurrent,这里补上)
if err := h.WithCurrent(kit); err != nil { return err }
// 3. 获取默认业务 ID(资源池业务,用于判断主机是否在"未分配"状态)
defaultBizID, err := h.audit.getDefaultAppID(kit)
if err != nil { return err }
// 4. 收集所有涉及到的业务/集群/模块 ID,反查名称
var setIDs, moduleIDs, appIDs []int64
for _, val := range append(h.pre, h.cur...) {
setIDs = append(setIDs, val.SetID)
moduleIDs = append(moduleIDs, val.ModuleID)
appIDs = append(appIDs, val.AppID)
}
moduleMap := h.getInstIDNameMap(kit, common.BKInnerObjIDModule, moduleIDs)
setMap := h.getInstIDNameMap(kit, common.BKInnerObjIDSet, setIDs)
bizMap := h.getInstIDNameMap(kit, common.BKInnerObjIDApp, appIDs)
// 5. 组装 PreData 和 CurData(HostBizTopo 格式)
preDataMap := h.getHostTransferDataMap(h.pre, bizMap, setMap, moduleMap)
curDataMap := h.getHostTransferDataMap(h.cur, bizMap, setMap, moduleMap)
// 6. 为每台主机生成一条 AuditLog
var logs = make([]metadata.AuditLog, 0)
for _, host := range hostInfos {
hostID, _ := util.GetInt64ByInterface(host[common.BKHostIDField])
hostIP, _ := host.String(common.BKHostInnerIPField)
hostIPv6, _ := host.String(common.BKHostInnerIPv6Field)
preData, curData := preDataMap[hostID], curDataMap[hostID]
preBizID, curBizID := preData.BizID, curData.BizID
// 7. 根据业务 ID 变化判断 Action 类型
var action metadata.ActionType
var bizID int64
if preBizID != curBizID && preBizID == defaultBizID {
action = metadata.AuditAssignHost // 资源池 → 业务(分配)
bizID = curBizID
} else if preBizID != curBizID && curBizID == defaultBizID {
action = metadata.AuditUnassignHost // 业务 → 资源池(退回)
bizID = preBizID
} else {
action = metadata.AuditTransferHostModule // 业务内转移模块
bizID = curBizID
}
// 8. 构造 HostTransferOpDetail
logs = append(logs, metadata.AuditLog{
AuditType: metadata.HostType, // "host"
ResourceType: metadata.HostRes, // "host"
Action: action,
BusinessID: bizID,
ResourceID: hostID,
ResourceName: hostIP,
ExtendResourceName: hostIPv6,
OperationDetail: &metadata.HostTransferOpDetail{
PreData: preData, // 转移前:{ biz_id, biz_name, set[], module[] }
CurData: curData, // 转移后:{ biz_id, biz_name, set[], module[] }
},
})
}
// 9. 写入 cc_AuditLog
if err := h.audit.SaveAuditLog(kit, logs...); err != nil {
blog.Errorf("save audit log failed, err: %v, rid: %s", err, kit.Rid)
return err
}
return nil
}
♦ 关键设计细节:Action 的三元判断
第 135-144 行的三元判断是整个主机转移审计的核心:
- preBizID != curBizID && preBizID == defaultBizID → 分配(资源池 → 业务):主机从 ID=0(资源池)转移到业务,Action = assign_host
- preBizID != curBizID && curBizID == defaultBizID → 退回(业务 → 资源池):主机从业务退回资源池,Action = unassign_host
- 其余情况 → 模块转移(业务内):主机在同一业务内跨模块移动,Action = transfer_host_module
defaultBizID 通过 src/common/auditlog/audit.go 第 115-149 行的 getDefaultAppID() 查询 cc_ApplicationBase 表中 bk_default=1 的记录得到(即"资源池"业务的 ID)。
5. 审计日志写入链路
完整的审计写入链路如下(以主机删除为例):
SRE 操作:删除主机 192.168.1.1
↓
host_server/service/host.go: DeleteHostBatch()
↓
auditlog.NewHostAudit(clientSet).WithPrevious(kit) ← WithPrevious/WithCurrent 是 hostModuleLog 的方法(拓扑快照),主机删除用 hostAuditLog
↓
执行删除操作(MongoDB DELETE)
↓
auditlog.NewHostAudit(clientSet).WithCurrent(kit)
↓
auditlog.NewHostAudit(clientSet).GenerateAuditLog()
↓
audit.saveAuditLog() → CoreService API
↓
source_controller: CreateAuditLog()
↓
MongoDB cc_AuditLog 写入文档:
{
"audit_type": "host",
"resource_type": "host",
"action": "delete",
"bk_supplier_account": "0",
"user": "zuoyang",
"bk_biz_id": 2,
"resource_id": 12345,
"resource_name": "192.168.1.1",
"rid": "abc123", ← 请求链路 ID
"operation_time": "2026-07-04T10:00:00Z",
"operation_detail": {
"pre_data": { ← 变更前完整主机数据
"bk_host_innerip": "192.168.1.1",
"bk_os_name": "linux",
"bk_biz_id": 2,
"bk_set_ids": [100],
"bk_module_ids": [200]
},
"cur_data": {} ← 删除后为空
}
}
6. 主机转移全链路追踪实战
6.1 场景:主机从 MySQL 模块转移到 PaymentServer 模块
SRE 操作:主机 192.168.1.1 从 MySQL 模块 → PaymentServer 模块
↓
scene_server/host_server/service/transfer.go: TransferHostModule()
↓
auditlog.NewHostModuleLog(clientSet, []int64{hostID})
→ 初始化审计记录器(传入主机 ID 列表)
↓
.WithPrevious(kit)
→ 查询 cc_ModuleHostConfig:当前主机在 MySQL 模块(biz=2, set=100, module=201)
↓
执行主机模块关系变更(MongoDB UPDATE cc_ModuleHostConfig)
↓
.WithCurrent(kit)
→ 查询 cc_ModuleHostConfig:主机现在在 PaymentServer 模块(biz=2, set=100, module=202)
↓
.SaveAudit(kit)
→ 生成 HostTransferOpDetail + 写入 cc_AuditLog
↓
cc_AuditLog 写入文档(关键字段):
{
"audit_type": "host",
"resource_type": "host",
"action": "transfer_host_module",
"user": "zuoyang",
"bk_biz_id": 2,
"resource_id": 12345,
"resource_name": "192.168.1.1",
"operation_detail": {
"pre_data": { ← 转移前
"bk_biz_id": 2,
"bk_biz_name": "游戏业务",
"set": [{"SetID": "100", "SetName": "北京一区",
"ModuleID": "201", "ModuleName": "MySQL"}]
},
"cur_data": { ← 转移后
"bk_biz_id": 2,
"bk_biz_name": "游戏业务",
"set": [{"SetID": "100", "SetName": "北京一区",
"ModuleID": "202", "ModuleName": "PaymentServer"}]
}
}
}
↓
SRE 查询审计日志:GET /api/v3/audit/search
返回:zuoyang 于 10:00 将 192.168.1.1 从 MySQL 转移到 PaymentServer
6.2 审计日志查询接口
cc_AuditLog 支持按以下维度组合查询(在 src/common/metadata/audit.go 第 160-171 行的 InstAuditCondition 中定义):
| 查询字段 | 说明 | 示例 |
|---|---|---|
| user | 操作用户 | "zuoyang" → 查该用户所有操作 |
| bk_biz_id | 所属业务 | 2 → 查该业务下所有操作 |
| resource_type | 资源类型 | "host" → 主机相关操作 |
| action | 操作类型 | ["delete", "transfer_host_module"] |
| operation_time | 操作时间范围 | 精确到秒,支持区间查询 |
| resource_name | 资源名称 | "192.168.1.1" → 主机 IP 精确查 |
7. FAQ
FAQ 共 20 组,涵盖:①数据结构理解 ②方法调用规则 ③Action类型判断 ④查询接口 ⑤与拓扑缓存/Change Stream的关系 ⑥边界与坑点
- ① 数据结构(Q1-Q5):BasicContent vs HostTransferOpDetail / DetailFactory 派发规则 / AuditType vs ResourceType vs Action / HostBizTopo vs Topo vs Module 的嵌套关系 / PreData/CurData 在不同 Action 下的填充规则
- ② 方法调用规则(Q6-Q10):WithPrevious/WithCurrent 的短路设计 / hostAuditLog vs hostModuleLog 的分工 / GenerateAuditLog 自动反查 bizID / SaveAuditLog 异步不阻塞 / 谁负责调用这些方法
- ③ Action 类型判断(Q11-Q13):三元判断逻辑 / assign_host vs unassign_host 的边界 / transfer_host_module 何时触发
- ④ 查询接口(Q14-Q16):SearchAuditLog 的查询字段 / InstAuditCondition 结构 / 按用户/业务/时间组合查询
- ⑤ 边界与坑点(Q17-Q20):CurData 在删除时为空的原因 / 审计日志不可修改删除 / WithPrevious/WithCurrent 各自只调一次 / getDefaultAppID 查资源池业务
Q1:BasicContent 和 HostTransferOpDetail 用哪个?
一句话结论:通用资源(业务/集群/模块/主机属性)用 BasicContent,主机拓扑转移用 HostTransferOpDetail。在 src/common/metadata/audit.go 第 361-368 行,当 Action 为 transfer_host_module / assign_host / unassign_host 时,UnmarshalBSON 会强制使用 HostTransferOpDetail;其余 Action 则走 resTypeOpDetailTypeMap(L251-269)派发——主机走 InstanceOpDetail(内含 BasicContent)。
Q2:DetailFactory 是如何根据 ResourceType 自动派发操作详情类型的?
一句话结论:通过 resTypeOpDetailTypeMap 查表,根据 ResourceType 找到对应的 DetailFactory 实现类。在 src/common/metadata/audit.go 第 251-269 行定义了派发表:BusinessRes → InstanceOpDetail、ServiceInstanceRes → ServiceInstanceOpDetail、InstanceAssociationRes → InstanceAssociationOpDetail。UnmarshalBSON(L380-393)通过查表拿到具体类型,再用反射实例化并反序列化。
Q3:AuditType、ResourceType、Action 三者有什么区别?
一句话结论:AuditType 是大类(决定审计来源系统),ResourceType 是具体资源(决定 DetailFactory 类型),Action 是操作动词(决定填充 PreData 还是 CurData)。以主机转移为例:AuditType="host"(主机大类)、ResourceType="host"(具体资源)、Action="transfer_host_module"(操作动词)。AuditType 还在 src/common/metadata/audit.go 第 596-664 行定义了 business / business_resource / model / cloud_resource / kube 等大类,用于审计后台按大类聚合。
Q4:HostBizTopo 里的 Set 是 []Topo,Topo 里的 Module 是 []Module,这两层嵌套是怎么来的?
一句话结论:getHostTransferDataMap(hostmodule.go L239-274)根据主机模块关系实时构建这两层嵌套结构。在 src/common/auditlog/hostmodule.go 第 239-274 行,先用 ModuleHost 列表按 HostID + SetID 分组,同一 SetID 下的所有 ModuleID 聚合成一个 Topo,再把所有 Topo 归入对应的 HostBizTopo。所以 PreData 和 CurData 天然就是树状结构——一台主机可能属于多个集群(多 Set),每个集群下有多个模块(多 Module)。
Q5:PreData 和 CurData 在不同 Action 下分别填什么?
一句话结论:Create 填 CurData、Delete 填 PreData、Update 两者都填并额外记录 UpdateFields。在 src/common/auditlog/metadata.go 第 48-68 行的 NewBasicContent() 中用 switch 语句实现这一逻辑:AuditCreate → CurData=data(新建资源完整的当前状态);AuditDelete → PreData=data(被删资源删除前的完整状态);AuditUpdate → PreData=data + UpdateFields=updateFields(变更前的完整数据加上本次修改的字段列表)。
Q6:WithPrevious 和 WithCurrent 为什么各自最多只调用一次?
一句话结论:方法内部有 if h.pre != nil { return nil } 的短路设计,第二次调用直接返回,避免重复查询 MongoDB。在 src/common/auditlog/hostmodule.go 第 46-56 行(WithPrevious)和第 58-69 行(WithCurrent),方法入口都先判断字段是否已赋值——已赋值则跳过查询。这确保了业务层可以安全地多次调用,不会因为重复查询导致数据不一致或性能浪费。
Q7:hostAuditLog 和 hostModuleLog 有什么区别?分别在哪里用?
一句话结论:hostAuditLog 处理主机属性的 CRUD 审计(Create/Update/Delete),hostModuleLog 处理主机拓扑转移审计(assign/unassign/transfer)。在 src/common/auditlog/host.go 第 29-97 行,hostAuditLog 的 GenerateAuditLog() 遍历主机列表,用 BasicContent 记录属性变化。在 src/common/auditlog/hostmodule.go 第 28-169 行,hostModuleLog 的 SaveAudit() 处理主机在业务间的归属变化和模块间的拓扑转移,用 HostTransferOpDetail 记录完整的拓扑快照。
Q8:GenerateAuditLog 里 bizID==0 时反查业务 ID 的逻辑是怎么工作的?
一句话结论:当主机不在任何业务中(bizID=0,如刚录入资源池的主机),GenerateAuditLog 调用 getBizIDByHostID 从主机记录里反查所属业务。在 src/common/auditlog/host.go 第 84-94 行,当传入的 bizID == 0 时,遍历 hostIDs 列表调用 getBizIDByHostID() 反查,然后把结果回填到每条 AuditLog.BusinessID。这一步确保审计日志始终有正确的业务归属,否则按业务过滤审计记录会漏掉资源池主机的变更。
Q9:SaveAuditLog 写入失败会阻塞业务主操作吗?
一句话结论:不会。SaveAuditLog 是旁路调用,不在业务主事务中,失败只记录 error 日志。在 src/common/auditlog/hostmodule.go 第 954-958 行,SaveAudit 返回的错误只被 blog.Errorf 记录,业务层可以选择忽略。这是符合"审计是旁路能力"的设计原则——审计失败不影响业务数据一致性。
Q10:谁负责调用 auditlog 包的方法?业务层怎么接入?
一句话结论:业务层(scene_server)在执行变更操作前后,显式调用 auditlog 包的 WithPrevious/GenerateAuditLog/SaveAudit 方法。在 src/common/auditlog/hostmodule.go 第 946-953 行,auditlog.NewHostModuleLog(clientSet, hostIDArr) 构造审计记录器,业务层传入 CoreService clientSet(用于查询数据)和主机 ID 列表。这种接入方式要求业务逻辑和审计逻辑显式分离,通过 rest.Kit 隐式传递用户身份。
Q11:Action 的三元判断(assign_host / unassign_host / transfer_host_module)具体怎么判断?
一句话结论:比较转移前后的业务 ID 是否变化,以及变化方向是否是"资源池 ↔ 业务"。在 src/common/auditlog/hostmodule.go 第 133-144 行:若 preBizID != curBizID && preBizID == defaultBizID,说明主机从资源池进入业务,Action = assign_host;若 preBizID != curBizID && curBizID == defaultBizID,说明主机从业务退回资源池,Action = unassign_host;其余情况(同一业务内跨 Set/Module 转移),Action = transfer_host_module。
Q12:defaultBizID 是怎么得到的?为什么用它判断资源池?
一句话结论:通过查询 cc_ApplicationBase 表中 bk_default=1 的记录得到,CMDB 里这个默认业务就是资源池业务。在 src/common/auditlog/audit.go 第 115-149 行,getDefaultAppID() 查询条件为 mapstr.MapStr{common.BKDefaultField: common.DefaultAppFlag}。CMDB 设计中,资源池不是独立数据库,而是一个特殊标记的业务(bk_default=1),主机在该业务下意味着"未分配"。
Q13:transfer_host_module 和 assign_host 在 Action 判断上会不会冲突?
一句话结论:不会。两个条件互斥,因为 preBizID==curBizID(业务不变)是 transfer_host_module 的必要条件,而 assign_host 要求 preBizID==defaultBizID(资源池)。资源池业务 ID(defaultBizID)和普通业务 ID 完全不同,所以三元判断的三个分支(资源池→业务 / 业务→资源池 / 同业务内)逻辑上完全互斥,不会出现歧义。
Q14:SearchAuditLog 支持哪些查询条件?
一句话结论:支持按 user / bk_biz_id / resource_type / action / operation_time / resource_name / resource_id / obj_id 组合查询。在 src/common/metadata/audit.go 第 160-171 行的 InstAuditCondition 中定义了查询条件结构,对应 src/source_controller/coreservice/service/service_initfunc.go 第 348 行注册的 /read/auditlog 接口。这些字段全部有索引(auditlog.go L25),支持高并发组合查询。
Q15:能不能同时按用户和业务来过滤审计日志?
一句话结论:可以。InstAuditCondition 支持多字段组合,MongoDB 会用复合索引加速。在 src/common/metadata/audit.go 第 160-171 行定义的 InstAuditCondition 中,User 和 BizID 是独立字段,查询时同时设置即可。例如查询用户 zhangsan 在游戏业务下的所有操作:user="zhangsan" + bk_biz_id=2。
Q16:OperateFrom 有哪些枚举值?如何区分人工操作和系统同步?
一句话结论:有 5 种来源,其中 user 是人工操作,其余为系统自动同步。在 src/common/metadata/audit.go 第 772-786 行:user(Web 用户操作)、cc_system(系统升级)、data_collection(数据采集器)、synchronizer(跨 CMDB 同步)、cloud_sync(云平台同步)。SRE 可以通过 operate_from 字段过滤出纯人工操作,满足合规审计"只统计人工作为"的需求。
Q17:删除主机的审计日志为什么 CurData 是空的?
一句话结论:因为 CurData 是在操作执行后查询的,此时主机已从数据库中消失。在 src/common/auditlog/host.go 第 29-97 行,GenerateAuditLog() 使用操作前的数据(由调用方在删除前传入)作为 PreData,而 CurData 字段保持 Go 零值(nil / 空 map)。这是符合审计逻辑的——删除操作的本质就是"记录删除前有什么",不需要记录删除后是什么(自然为空)。
Q18:审计日志可以修改或删除吗?
一句话结论:不可以。auditlog 模块只提供写入和查询接口,没有任何更新或删除接口。在 src/source_controller/coreservice/service/service_initfunc.go 第 346-349 行,audit 模块只注册了 CreateAuditLog 和 SearchAuditLog 两个接口,UpdateAuditLog 和 DeleteAuditLog 完全不存在。这是合规审计的强制要求——审计日志是不可篡改的法律记录。
Q19:WithPrevious 和 WithCurrent 的调用顺序有没有严格要求?
一句话结论:WithPrevious 必须在业务操作前调用,WithCurrent 必须在业务操作后调用,顺序不能颠倒。以主机转移为例:调用方先调 WithPrevious(kit) 捕获转移前的拓扑(查询 cc_ModuleHostConfig),然后执行主机模块关系变更(MongoDB UPDATE),最后调 WithCurrent(kit) 捕获转移后的拓扑。如果顺序颠倒,WithPrevious 会拿到变更后的状态,WithCurrent 拿到变更前的状态,导致 PreData 和 CurData 颠倒。
Q20:getHostTransferDataMap 为什么要先按 SetID 分组再构建 Topo,而不是直接按 Module 构建?
一句话结论:因为一台主机在同一 Set 下可能有多个 Module(属于多个服务),必须保留 Set 层级才能完整还原拓扑结构。在 src/common/auditlog/hostmodule.go 第 239-274 行,先用 val.SetID 分组(hostRelationMap[hostID][setID] = []Module),再把所有 Module 聚合到对应的 Topo 中,最后组装成 HostBizTopo。这样 PreData 和 CurData 才能完整还原"主机在哪些 Set 的哪些 Module 中"的信息,满足 SRE 审计"从哪到哪"的核心诉求。
全篇总结:auditlog 模块设计思想
- 一个核心接口:DetailFactory,ResourceType 到操作详情的类型派发表(resTypeOpDetailTypeMap)
- 两条审计链路:hostAuditLog.GenerateAuditLog(属性 CRUD,BasicContent) + hostModuleLog.SaveAudit(拓扑转移,HostTransferOpDetail)
- 三个快照时刻:WithPrevious(操作前拓扑) → 执行业务变更 → WithCurrent(操作后拓扑)
- 三个 Action 判断分支:preBizID==defaultBizID(assign) / curBizID==defaultBizID(unassign) / 其余(transfer_module)
- 一个不可变原则:cc_AuditLog 只有 Create/Search 接口,无 Update/Delete
8. Roadmap
审计日志模块的底层依赖是 MongoDB 的 Change Stream 能力——第 5 篇《拓扑缓存加速》涉及的 source_controller 缓存服务,正是通过 Change Stream 监听了 cc_AuditLog 的变更事件。后续将依次介绍:
- #07 批量操作与异步任务:host_server 如何用 RabbitMQ 队列实现主机批量转移的异步任务,auditlog 如何与任务系统联动
- #08 主机属性自动应用:模块绑定属性模板后,新进入模块的主机如何自动继承属性,auditlog 记录的是哪一步的变更
- #09 服务实例管理:进程配置模板(ServiceTemplate)如何生成服务实例,auditlog 中 ServiceInstanceRes 类型日志的结构
- #10 k8s 资源纳管:KubeCluster/KubeNode/KubePod 等 Kube 类型审计日志(AuditType="kube")与通用 ResourceType 的区别
蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】· #06 操作审计日志:谁在什么时候做了什么 · 版本:v3.14.6 LTS

浙公网安备 33010602011771号