| name | audit-log-reporting |
| description | 指导实现审计日志上报功能。当开发人员需要在新模块中添加审计日志上报、创建审计服务层、或集成审计服务RPC客户端时调用此SKILL。 |
审计日志上报功能实现指南
概述
本SKILL提供在其他模块/仓库中实现审计日志上报功能的完整指导。审计日志用于记录用户对数据库实例、集群、保护对象等资源的操作行为。
前置条件
- 项目使用Go语言开发
- 已集成
framework_go-thrifts 库(包含 ncaudittype 和 ncauditsvc)
- 已集成
service_resource-center_hrc-core 库
- 审计服务运行在端口 9125(Thrift RPC)
相关依赖仓库
在实现审计日志功能时,以下仓库可能提供参考和解决方案:
| 仓库 | 用途 | 参考内容 |
|---|
hrcm-app-dbalone | 参考实现 | service/audit/ 目录下的审计服务层实现 |
hrcm-app-dbcluster | 参考实现 | 集群相关的审计日志上报实现 |
backup-mgm-database | 参考实现 | 保护对象相关的审计日志上报实现 |
i18nlanguageresources | 国际化资源 | event/Audit_event.json 和 resource/Audit_resource.json |
framework_go-thrifts | Thrift类型定义 | ncaudittype 和 ncauditsvc 包 |
service_resource-center_hrc-core | 基础工具 | NewThriftClient 等工具函数 |
遇到问题时的排查建议:
- 如果审计日志上报失败,检查
framework_go-thrifts 中的类型定义是否有更新
- 如果国际化不生效,检查
i18nlanguageresources 仓库中的资源码定义
- 如果Thrift连接失败,参考
service_resource-center_hrc-core 中的客户端创建方式
实现步骤
步骤1:创建审计服务层
创建 service/audit/ 目录,包含以下文件:
1.1 audit_constants.go
注意:以下常量定义仅供参考格式,请根据实际业务需求定义符合自身业务场景的审计操作类型和资源码。
package audit
type AuditAction string
const (
AuditActionCreated AuditAction = "created"
AuditActionDeleted AuditAction = "deleted"
AuditActionAuthorized AuditAction = "authorized"
AuditActionAuthCancelled AuditAction = "auth_cancelled"
AuditActionFrozen AuditAction = "frozen"
AuditActionThawed AuditAction = "thawed"
AuditActionModified AuditAction = "modified"
)
type AuditResCode string
const (
AuditResDBInstanceCreated AuditResCode = "Audit.DBInstanceCreated"
AuditResDBInstanceDeleted AuditResCode = "Audit.DBInstanceDeleted"
AuditResDBInstanceAuthorized AuditResCode = "Audit.DBInstanceAuthorized"
AuditResDBInstanceAuthCancelled AuditResCode = "Audit.DBInstanceAuthCancelled"
AuditResDBInstanceFrozen AuditResCode = "Audit.DBInstanceFrozen"
AuditResDBInstanceThawed AuditResCode = "Audit.DBInstanceThawed"
AuditResDBInstanceModified AuditResCode = "Audit.DBInstanceModified"
)
var AuditActionToResCode = map[AuditAction]AuditResCode{
AuditActionCreated: AuditResDBInstanceCreated,
AuditActionDeleted: AuditResDBInstanceDeleted,
AuditActionAuthorized: AuditResDBInstanceAuthorized,
AuditActionAuthCancelled: AuditResDBInstanceAuthCancelled,
AuditActionFrozen: AuditResDBInstanceFrozen,
AuditActionThawed: AuditResDBInstanceThawed,
}
1.2 audit_service.go
package audit
import (
"context"
"time"
"github.com/anybackup-ai/Zeus/_git/framework_go-thrifts/ncaudittype"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
type AuditUserInfo struct {
UserId string
UserName string
LoginIp string
TenantId string
}
type AuditService interface {
ReportInstanceAudit(c *gin.Context, action AuditAction, instanceName, resourceId string, propertySet []*ncaudittype.NcAuditProperty) error
}
type auditService struct {
auditSvcRpc IAuditSvcRpc
}
type IAuditSvcRpc interface {
SaveAuditObject(ctx context.Context, auditObject *ncaudittype.NcAuditObject) error
SaveAuditLog(ctx context.Context, auditLog *ncaudittype.NcAuditLogContent) error
}
func NewAuditService(rpc IAuditSvcRpc) AuditService {
return &auditService{
auditSvcRpc: rpc,
}
}
func (s *auditService) getAuditUserInfo(c *gin.Context) (*AuditUserInfo, error) {
return nil, nil
}
func buildObjectProperty(nameResCode string, value string) *ncaudittype.NcAuditObjectProperty {
return &ncaudittype.NcAuditObjectProperty{
Name: &ncaudittype.NcLanguageText{
Text: nameResCode,
},
Value: []string{value},
}
}
func buildInstanceProperties(instanceName string) []*ncaudittype.NcAuditObjectProperty {
return []*ncaudittype.NcAuditObjectProperty{
buildObjectProperty("Audit.ObjectName", instanceName),
}
}
func getCurrentTimeMillis() int64 {
return time.Now().UnixNano() / 1e6
}
func buildAuditProperty(nameResCode string, value string) *ncaudittype.NcAuditProperty {
return &ncaudittype.NcAuditProperty{
Name: &ncaudittype.NcLanguageText{
Text: nameResCode,
},
Value: []string{value},
}
}
func (s *auditService) ReportInstanceAudit(c *gin.Context, action AuditAction, instanceName, resourceId string, propertySet []*ncaudittype.NcAuditProperty) error {
userInfo, err := s.getAuditUserInfo(c)
if err != nil {
logrus.Errorf("get user info failed: %v", err)
return err
}
objectProperties := buildInstanceProperties(instanceName)
auditObject := &ncaudittype.NcAuditObject{
ObjectId: resourceId,
UserId: userInfo.UserId,
TenantId: userInfo.TenantId,
ObjectName: instanceName,
ObjectType: ncaudittype.NcAuditObjectType_ProtectObject,
ObjectProperty: objectProperties,
}
ctx := c.Request.Context()
err = s.auditSvcRpc.SaveAuditObject(ctx, auditObject)
if err != nil {
logrus.Errorf("save audit object failed: %v", err)
}
resCode, ok := AuditActionToResCode[action]
if !ok {
logrus.Warnf("unknown audit action: %s", action)
return nil
}
auditLog := &ncaudittype.NcAuditLogContent{
UserId: userInfo.UserId,
TriggerTime: getCurrentTimeMillis(),
IP: userInfo.LoginIp,
Description: &ncaudittype.NcLanguageText{
Text: string(resCode),
Params: map[string]string{
"userName": userInfo.UserName,
"time": time.Now().Format("2006-01-02 15:04:05"),
"instanceName": instanceName,
},
},
AuditObjectId: resourceId,
PropertySet: propertySet,
}
err = s.auditSvcRpc.SaveAuditLog(ctx, auditLog)
if err != nil {
logrus.Errorf("save audit log failed: %v", err)
return err
}
return nil
}
步骤2:创建RPC客户端
创建 ext-rpc/audit_svc.go:
package ext_rpc
import (
"context"
"time"
"github.com/anybackup-ai/Flora/_git/greed/rpc"
"github.com/anybackup-ai/Zeus/_git/framework_go-thrifts/ncauditsvc"
"github.com/anybackup-ai/Zeus/_git/framework_go-thrifts/ncaudittype"
hrc_core "github.com/anybackup-ai/Zeus/_git/service_resource-center_hrc-core"
hrc_util "github.com/anybackup-ai/Zeus/_git/service_resource-center_hrc-core/util"
"github.com/apache/thrift/lib/go/thrift"
"github.com/sirupsen/logrus"
)
type IAuditSvcService interface {
SaveAuditLog(ctx context.Context, auditLog *ncaudittype.NcAuditLogContent) error
SaveAuditObject(ctx context.Context, auditObject *ncaudittype.NcAuditObject) error
GetAuditObject(ctx context.Context, auditObjectId string) (*ncaudittype.NcAuditObject, error)
}
type AuditSvcService struct {
ThriftClient rpc.ThriftClient
TimeOut int
SvcName string
}
func NewAuditSvcService(ip string, port int) IAuditSvcService {
auditService := &AuditSvcService{
TimeOut: 30,
SvcName: "AuditService",
}
client, err := hrc_util.NewThriftClient(ip, port)
if err != nil {
hrc_core.Logger.Errorf("new audit service thrift client failed")
return nil
}
auditService.ThriftClient = client
return auditService
}
type clientHolder struct {
transport thrift.TTransport
client *ncauditsvc.NcAuditSvcClient
}
func (a *AuditSvcService) createClient() (*clientHolder, error) {
transport, protocol, err := a.ThriftClient.CreateExternalRpcParam()
if err != nil {
logrus.Errorf("create external rpc param error: %s", err)
return nil, err
}
if err = transport.Open(); err != nil {
logrus.Errorf("open transport error: %s", err)
return nil, err
}
client := ncauditsvc.NewNcAuditSvcClientProtocol(transport, protocol, protocol)
return &clientHolder{
transport: transport,
client: client,
}, nil
}
func (a *AuditSvcService) SaveAuditLog(ctx context.Context, auditLog *ncaudittype.NcAuditLogContent) error {
ctx, cancel := context.WithTimeout(ctx, time.Duration(a.TimeOut)*time.Second)
defer cancel()
holder, err := a.createClient()
if err != nil {
return err
}
defer func() {
_ = holder.transport.Close()
}()
err = holder.client.SaveAuditLog(ctx, auditLog)
if err != nil {
logrus.Error(err)
return err
}
return nil
}
func (a *AuditSvcService) SaveAuditObject(ctx context.Context, auditObject *ncaudittype.NcAuditObject) error {
ctx, cancel := context.WithTimeout(ctx, time.Duration(a.TimeOut)*time.Second)
defer cancel()
holder, err := a.createClient()
if err != nil {
return err
}
defer func() {
_ = holder.transport.Close()
}()
err = holder.client.SaveAuditObject(ctx, auditObject)
if err != nil {
logrus.Error(err)
return err
}
return nil
}
func (a *AuditSvcService) GetAuditObject(ctx context.Context, auditObjectId string) (*ncaudittype.NcAuditObject, error) {
ctx, cancel := context.WithTimeout(ctx, time.Duration(a.TimeOut)*time.Second)
defer cancel()
holder, err := a.createClient()
if err != nil {
return nil, err
}
defer func() {
_ = holder.transport.Close()
}()
result, err := holder.client.GetAuditObject(ctx, auditObjectId)
if err != nil {
logrus.Error(err)
return nil, err
}
return result, nil
}
步骤3:全局初始化
在 global/init.go 中添加:
var auditSvcRpc extrpc.IAuditSvcService
func GetAuditSvcRpc() extrpc.IAuditSvcService {
if auditSvcRpc != nil {
return auditSvcRpc
}
envConfig, err := hrc_core.GetEnvConfig()
if err != nil {
GetLog().Errorf("GetEnvConfig failed")
return nil
}
auditSvcRpc = extrpc.NewAuditSvcService(
hrc_core.GetSelfIp(),
envConfig.ServerIni.DefaultInt("AuditService::thrift", 9125),
)
return auditSvcRpc
}
步骤4:在API层使用(异步方式)
基础用法(无操作参数)
func CreateInstance(c *gin.Context) {
go func() {
auditSvc := audit.NewAuditService(global.GetAuditSvcRpc())
err := auditSvc.ReportInstanceAudit(c, audit.AuditActionCreated, instanceName, resourceId, nil)
if err != nil {
logrus.Errorf("report audit log failed: %v", err)
}
}()
}
高级用法(带操作参数)
func CreateInstance(c *gin.Context) {
go func() {
propertySet := []*ncaudittype.NcAuditProperty{
audit.BuildAuditProperty("Audit.DBDBType", fmt.Sprintf("%d", req.Type)),
audit.BuildAuditProperty("Audit.DBDBName", req.DbName),
}
auditSvc := audit.NewAuditService(global.GetAuditSvcRpc())
err := auditSvc.ReportInstanceAudit(c, audit.AuditActionCreated, instanceName, resourceId, propertySet)
if err != nil {
logrus.Errorf("report audit log failed: %v", err)
}
}()
}
重要原则:
- 直接上报原始编码值(如数据库类型上报"201"而不是"Oracle")
- 避免在代码中维护转换函数,减少维护负担
- 国际化和可读性展示由前端或审计服务端处理
国际化资源配置
注意:以下JSON格式仅供参考,请根据实际业务需求定义资源码和描述内容。
添加事件描述
在 i18nlanguageresources 仓库的以下文件中添加:
zh_CN/event/Audit_event.json:
{
"Audit.DBInstanceCreated": {
"resId": "Audit.DBInstanceCreated",
"description": "用户[userName]于[time]创建了数据库实例[instanceName]"
}
}
en_US/event/Audit_event.json:
{
"Audit.DBInstanceCreated": {
"resId": "Audit.DBInstanceCreated",
"description": "User[userName]created database instance[instanceName]at[time]"
}
}
资源属性定义(如需要)
zh_CN/resource/Audit_resource.json:
{
"Audit.ObjectName": {
"resId": "Audit.ObjectName",
"description": "对象名称"
},
"Audit.ObjectType": {
"resId": "Audit.ObjectType",
"description": "对象类型"
},
"Audit.DBDBType": {
"resId": "Audit.DBDBType",
"description": "数据库类型"
},
"Audit.DBDBName": {
"resId": "Audit.DBDBName",
"description": "数据库名"
},
"Audit.DBAuthType": {
"resId": "Audit.DBAuthType",
"description": "授权类型"
},
"Audit.DBAuthMode": {
"resId": "Audit.DBAuthMode",
"description": "认证权限"
},
"Audit.DBClusterType": {
"resId": "Audit.DBClusterType",
"description": "集群类型"
},
"Audit.DBInstanceCount": {
"resId": "Audit.DBInstanceCount",
"description": "实例数量"
}
}
en_US/resource/Audit_resource.json:
{
"Audit.DBDBType": {
"resId": "Audit.DBDBType",
"description": "Database Type"
},
"Audit.DBDBName": {
"resId": "Audit.DBDBName",
"description": "Database Name"
},
"Audit.DBAuthType": {
"resId": "Audit.DBAuthType",
"description": "Authorization Type"
},
"Audit.DBAuthMode": {
"resId": "Audit.DBAuthMode",
"description": "Authentication Mode"
},
"Audit.DBClusterType": {
"resId": "Audit.DBClusterType",
"description": "Cluster Type"
},
"Audit.DBInstanceCount": {
"resId": "Audit.DBInstanceCount",
"description": "Instance Count"
}
}
最佳实践
- 异步上报:所有审计日志上报都应使用异步方式(goroutine),避免影响主业务流程
- 错误处理:审计日志上报失败不应影响主业务流程,只需记录错误日志
- 对象属性:始终填充
ObjectProperty 字段,包含对象名称和类型
- 资源码命名:
- 数据库模块的操作参数使用
Audit.DBxxx 格式(如 Audit.DBDBType、Audit.DBClusterType)
- 对于特定数据库,如DB2,使用
Audit.DB2xxx 前缀(如 Audit.DB2InstanceCreated)
- 时间戳:使用毫秒级时间戳(13位)
- 测试:使用
NewAuditServiceWithRpc 注入mock RPC实现进行单元测试
- 操作参数(PropertySet):
- 对于关键操作(创建、授权、修改),建议添加操作参数以增强审计可追溯性
- 操作参数应包含业务关键信息(如数据库类型、授权方式等)
- 直接上报原始编码值,不做转换(如数据库类型上报"201"而不是"Oracle")
- 严禁在操作参数中记录敏感信息(密码、授权码、私钥等)
- 保持操作参数简洁,避免记录冗余信息
- 国际化和可读性展示由前端或审计服务端处理,不在代码层转换
常见问题
Q: 审计日志上报失败会影响主业务吗?
A: 不应该。审计日志上报应该使用异步方式,且失败只记录日志,不返回错误给调用方。
Q: 如何支持新的操作类型?
A: 1) 在 audit_constants.go 中添加新的Action常量;2) 添加对应的资源码映射;3) 在i18n资源文件中添加描述。
Q: 对象属性中的资源码在哪里定义?
A: 对象属性名称(如 Audit.ObjectName、Audit.ObjectType)在 i18nlanguageresources 仓库的 resource/Audit_resource.json 文件中定义;审计日志描述(如 Audit.DBInstanceCreated)在 event/Audit_event.json 文件中定义。
Q: 审计日志描述没有成功国际化怎么办?
A: 请检查以下几点:
- 确认资源码在
i18nlanguageresources 仓库的 event/Audit_event.json 中已定义
- 确认
NcLanguageText 的 Params 字段正确填充了参数
- 检查审计服务端是否正确处理了
Params 字段
- 查看审计服务端日志是否有相关错误
Q: 如何为审计日志添加操作参数(PropertySet)?
A: 参考以下步骤:
- 在
i18nlanguageresources 仓库的 resource/Audit_resource.json 中定义操作参数名称的资源码(如 Audit.DBDBType)
- 使用
buildAuditProperty 辅助函数构建 NcAuditProperty 对象
- 在调用
ReportInstanceAudit 时传入 propertySet 参数
- 示例代码见上文"高级用法(带操作参数)"部分
Q: 操作参数中应该包含哪些信息?
A: 建议包含:
- 业务关键配置信息(如数据库类型、集群类型)- 直接上报原始编码值
- 操作的具体方式(如授权类型、认证模式)- 直接上报原始编码值
DB2 备份配置参数上报示例
需求场景
当 DB2 备份配置创建或更新时,需要全量上报所有配置参数(约 70+ 个参数)到审计日志的 PropertySet 中,以便审计服务能够展示配置变更前后的参数对比。
实现方案
1. 修改审计服务接口,添加 PropertySet 支持
向后兼容性设计:
- 添加新函数
ReportProtectObjectAuditWithGinAndPropertySet,保持 ReportProtectObjectAuditWithGin 不变
- 修改
ReportProtectObjectAudit 方法签名,添加 propertySet 参数
- 旧代码无需修改,继续使用
ReportProtectObjectAuditWithGin
2. 构建 DB2 备份配置 PropertySet
分模块构建:
- 为每个参数结构体创建独立的构建函数,提高可维护性和可测试性
- 主函数
BuildDB2BackupConfigPropertySet 调用所有子构建函数
参数结构体:
CommonBackupParam(24 个参数)
DB2MultiSessionParam(2 个参数)
DB2BufferParam(4 个参数)
DB2PriorityParam(2 个参数)
DB2BackupParam(35 个参数)
SANGatewayInfo(7 个参数)
DB2BackupScriptsParam(8 个参数)
3. 参数值转换规则
直接转换为字符串:
bool → fmt.Sprintf("%t", value)
int32/int64 → fmt.Sprintf("%d", value)
float64 → fmt.Sprintf("%f", value)
string → 直接使用
[]string → strings.Join(value, ",")
敏感信息过滤:
- 只过滤
AdgPrimaryPwd 字段(密码)
- 其他参数全量上报
4. 国际化资源码
资源码命名:
- 使用
Audit.DB2xxx 前缀(如 Audit.DB2StoragePoolType、Audit.DB2BackupGran)
- 共约 70+ 个资源码
资源文件位置:
i18nlanguageresources/zh_CN/resource/Audit_resource.json
i18nlanguageresources/en_US/resource/Audit_resource.json
5. API 层使用
修改 UpdateBackupConfig 方法:
propertySet := audit.BuildDB2BackupConfigPropertySet(&req.DB2BackupConfig)
err = utils.ReportProtectObjectAuditWithGinAndPropertySet(c, req.ObjectId, objectName,
"Audit.DB2ProtectConfigUpdated", map[string]string{
"userName": userInfo.UserName,
}, propertySet)
6. 关键设计决策
向后兼容性:通过添加新函数保持向后兼容,不影响现有代码
全量上报:所有参数(除密码外)都上报到 PropertySet
异步上报:使用 goroutine 异步上报,不影响主业务流程
敏感信息过滤:只过滤密码字段,不上报到审计日志
Q: 为什么操作参数要上报原始编码值而不是可读名称?
A: 原因如下:
- 减少维护负担:避免在代码中维护转换函数,当新增类型时无需修改代码
- 国际化统一:原始编码值可以在前端或审计服务端统一转换和国际化
- 数据一致性:避免不同模块转换不一致的问题
- 示例:数据库类型上报"201"(Oracle的编码),而不是"Oracle"或"甲骨文"
严禁包含:
- 密码、密钥、证书等敏感凭证
- 授权码、激活码等敏感信息
- 连接字符串中的敏感部分