我们想要跟踪数据库(PostgreSQL)中各个表的所有条目的操作(审核)历史记录。编程语言是 golang,GORM ( https://gorm.io/ ) 作为 ORM。
基本上在单独的(例如audit_logs)表中捕获表上的CREATE、UPDATE 和DELETE 操作。
audit_logs 表所需的架构是:
Hibernate 的 enver 中提供了此类功能 ( https://www.baeldung.com/database-auditing-jpa#hibernate )。
但是,对于golang的GORM来说,没有任何这样的功能。
有人在 golang 中遇到过这样的要求吗?如果是,找到/构建的方法或解决方案是什么?任何线索都会有帮助。
我们尝试在 gorm.DB 对象上注册钩子。
说audit_log的对象是
import (
"gorm.io/gorm"
)
type AuditLog struct {
EntityClass string `gorm:"column:entity_class"`
EntityID string `gorm:"column:entity_id"`
Action string `gorm:"column:action"`
ActionTaker string `gorm:"column:action_taker"`
PrevData interface{} `gorm:"column:prev_data"`
CurData interface{} `gorm:"column:cur_data"`
At time.Time `gorm:"column:at"`
}
现在,在数据库的初始化函数中,注册钩子
_ = db.AutoMigrate(&AuditLog{}) // creating the table if not exists
_ = db.Callback().Create().After("gorm:create").Register("audit_log:create", auditLogCreateCallback)
_ = db.Callback().Update().After("gorm:update").Register("audit_log:update", auditLogUpdateCallback)
_ = db.Callback().Delete().After("gorm:delete").Register("audit_log:delete", auditLogDeleteCallback)
并定义了各自的功能,例如,
func auditLogCreateCallback(db *gorm.DB) {
if db.Error != nil {
return
}
entityName := db.Statement.Table
// here we need to extract the information from db.Statement object (OR other location if required)
// basically we need EntityID, PrevData and CurData here
// but db.Statement.Dest or db.Statement.Model both contains data indifferent forms in different situation (eg, sometimes updated via map, sometimes via model itself). Also, incase of bulk inserts / conditional edits (eg, update product's discount where product category is 'xyz') the data is stored different in the db.Statement model.
usernameInterface := db.Statement.Context.Value(constant.UserId)
username, _ := usernameInterface.(string)
auditLog := AuditLog{
EntityClass: entityName,
EntityID: "<id here>",
Action: "CREATE",
ActionTaker: username,
PrevData: "<json dump of prev data>",
CurData: "<json dump of cur data>",
At: time.Now(),
}
auditDB.Create(&auditLog)
}
我已经使用该回调函数创建了审核日志。 我在同一个数据库中创建了一个audit_log表,但它不起作用,然后我在单独的数据库中创建了一个audit_log表,它工作得很好
这里是代码示例
架构
type AuditLog struct {
gorm.Modal
UserID uuid.UUID `json:"user_id"`
UserEmail string `json:"user_email"`
UserName string `json:"user_name"`
PhcID uuid.UUID `json:"phc_id"`
Role string `json:"role"`
EventName string `json:"event_name"`
Action string `json:"action"`
EntityID string `json:"entity_id"`
EntityModel string `json:"entity_model"`
OldData string `json:"old_data"`
NewData string `json:"new_data"`
Diff string `json:"diff"`
Domain string `json:"domain"`
}
此函数用于注册回调
func RegisterAuditLogCallbacks(db *gorm.DB) {
db.Callback().Create().After("gorm:create").Register("audit_log_create", logCreate)
db.Callback().Update().Before("gorm:update").Register("audit_log_update", logUpdate)
}
记录功能
创建
// logCreate callback
func logCreate(db *gorm.DB) {
// Check if the audit log flag is set
if db.Statement.Context.Value(auditLogFlagKey) != nil {
return // Skip logging if already inside audit log creation
}
if db.Statement.Schema != nil {
// Get the primary key value
var entityID interface{}
if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil {
entityID, _ = db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.Context, db.Statement.ReflectValue)
}
if db.Statement.Table == "audit_logs" {
return
}
userID := user_id
role := user_role
newData := make(map[string]interface{})
newJSON, _ := json.Marshal(db.Statement.Model)
json.Unmarshal(newJSON, &newData)
//remove password from audit log ,I do this because encripted passwords can be expose :)
delete(newData, "password")
delete(newData, "Password")
newJSON, err := json.Marshal(newData)
if err != nil {
logrus.Error("Error marshalling new data: ", err)
}
createAuditLogEntry(db, userID, role, "Create Event", "CREATE", db.Statement.Table, entityID, nil, newJSON)
}
}
更新
func logUpdate(db *gorm.DB) {
// Check if the audit log flag is set
if db.Statement.Context.Value(auditLogFlagKey) != nil {
return // Skip logging if already inside audit log creation
}
if db.Statement.Schema != nil {
var entityID interface{}
if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil {
entityID, _ = db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.Context, db.Statement.ReflectValue)
}
if db.Statement.Table == "audit_logs" {
return
}
userID := user_id
role := user_role
var oldData map[string]interface{}
db.Session(&gorm.Session{NewDB: true}).Table(db.Statement.Table).Where("id = ?", entityID).Find(&oldData)
newData := make(map[string]interface{})
newJSON, _ := json.Marshal(db.Statement.Model)
json.Unmarshal(newJSON, &newData)
// Fetch the old data from the database
//remove password from audit log
delete(newData, "password")
delete(newData, "Password")
delete(oldData, "password")
oldJSON, err := json.Marshal(oldData)
if err != nil {
logrus.Error("Error marshalling old data: ", err)
}
newJSON, err = json.Marshal(newData)
if err != nil {
logrus.Error("Error marshalling new data: ", err)
}
createAuditLogEntry(db, userID, role, "Update Event", "UPDATE", db.Statement.Table, entityID, oldJSON, newJSON)
}
}
保存审核日志功能
const auditLogFlagKey = "audit_log"
// createAuditLogEntry creates a log entry in the AuditLog table.
func createAuditLogEntry(db *gorm.DB, userID uuid.UUID, role, eventName, action, entityModel string, entityID interface{}, oldData,
newData []byte) {
// Check if the flag is already set in the context, to avoid recursive logging
if db.Statement.Context.Value(auditLogFlagKey) != nil {
// If the flag is set, skip creating the audit log entry
return
}
var diffJSON []byte
if newData != nil {
// Optionally, you can calculate the diff here
diffJSON = calculateDiff(oldData, newData)
}
auditLog := AuditLog{
UserID: userID,
Role: role,
EventName: eventName,
Action: action,
EntityID: fmt.Sprintf("%v", entityID),
EntityModel: entityModel,
OldData: string(oldData),
NewData: string(newData),
Diff: string(diffJSON),
UserEmail: user_email,
UserName: user_name,
PhcID: phc_id,
Domain: domain,
}
// Set the flag in the context to avoid infinite loop
newCtx := context.WithValue(db.Statement.Context, auditLogFlagKey, true)
db = db.WithContext(newCtx)
// time.Sleep(5 * time.Second)
// Create the audit log entry
auditLogDB.Create(&auditLog)
}
我创建了这个额外的函数来标准化键并获取差异
func convertToSnakeCase(str string) string {
// Compile a regex to find capital letters and replace with _<lowercase>
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
// Insert underscore before the capital letters
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
// Convert to lowercase
return strings.ToLower(snake)
}
// NormalizeKeys converts all keys in a map to snake_case
func normalizeKeys(data map[string]interface{}) map[string]interface{} {
normalized := make(map[string]interface{})
for key, value := range data {
// Convert each key to snake_case
snakeKey := convertToSnakeCase(key)
normalized[snakeKey] = value
}
return normalized
}
func calculateDiff(oldData, newData []byte) []byte {
diffMap := make(map[string]interface{})
oldMap := make(map[string]interface{})
newMap := make(map[string]interface{})
json.Unmarshal(oldData, &oldMap)
json.Unmarshal(newData, &newMap)
oldMap = normalizeKeys(oldMap)
newMap = normalizeKeys(newMap)
// fmt.Println("👉️ Old Map: ", oldMap)
// fmt.Println("👉️ New Map: ", newMap)
for key, value := range oldMap {
if key == "password" {
continue
}
if newMap[key] != value {
diffMap[key] = map[string]interface{}{
"old": value,
"new": newMap[key],
}
}
}
diffJSON, _ := json.Marshal(diffMap)
return diffJSON
}