Master GORM Plugins: Build an Asynchronous Audit Log Extension in Go
Learn how to extend GORM with custom plugins by implementing the Plugin interface, registering lifecycle callbacks, and creating an asynchronous audit‑log module that records Create, Update, Delete, and Query actions to a dedicated table, with configurable batch size, flush interval, and buffer settings.
What Is a GORM Plugin
GORM plugins are wrappers around GORM lifecycle callbacks (Create, Update, Delete, Query). By implementing a plugin you can inject custom logic before or after these operations, modularize extensions for reuse across projects, and keep a development experience consistent with community plugins such as the Prometheus monitor.
Plugin Interface
type Plugin interface {
Name() string
Initialize(*gorm.DB) error
}Name() returns a unique identifier for the plugin. Initialize(db) registers callbacks, mounts configuration, or performs any setup required by the plugin.
Basic Steps to Write a Plugin
Define the plugin struct : hold configuration or dependencies.
Implement the Plugin interface : write Name() and Initialize() methods.
Register callbacks : use db.Callback().Create().Before("gorm:create").Register(...) (or After) to insert your logic.
Use the plugin : load it into a GORM instance with db.Use(plugin).
Case Study: Asynchronous Audit Log Plugin
This plugin records every Create, Update, Delete, and Query operation to an operation_logs table. Logs are written asynchronously in batches to minimise impact on the main transaction flow.
1. Plugin Definition
package audit
func (ap *AuditPlugin) Name() string { return "audit_plugin" }
func (ap *AuditPlugin) Initialize(db *gorm.DB) error {
// Auto‑migrate the log table
db.AutoMigrate(&OperationLog{})
// Initialise asynchronous channel
ap.logChan = make(chan OperationLog, ap.config.BufferSize)
ticker := time.NewTicker(ap.config.FlushInterval)
go func() {
buffer := make([]OperationLog, 0, ap.config.BatchSize)
for {
select {
case log := <-ap.logChan:
buffer = append(buffer, log)
if len(buffer) >= ap.config.BatchSize {
db.Session(&gorm.Session{NewDB: true}).Create(&buffer)
buffer = buffer[:0]
}
case <-ticker.C:
if len(buffer) > 0 {
db.Session(&gorm.Session{NewDB: true}).Create(&buffer)
buffer = buffer[:0]
}
}
}
}()
// Register callbacks for each operation type
db.Callback().Create().After("gorm:create").Register("audit:create", ap.afterCreate)
db.Callback().Update().After("gorm:update").Register("audit:update", ap.afterUpdate)
db.Callback().Delete().After("gorm:delete").Register("audit:delete", ap.afterDelete)
db.Callback().Query().After("gorm:query").Register("audit:query", ap.afterQuery)
return nil
}
func (ap *AuditPlugin) afterCreate(db *gorm.DB) { ap.enqueueLog(db, "create") }
func (ap *AuditPlugin) afterUpdate(db *gorm.DB) { ap.enqueueLog(db, "update") }
func (ap *AuditPlugin) afterDelete(db *gorm.DB) { ap.enqueueLog(db, "delete") }
func (ap *AuditPlugin) afterQuery(db *gorm.DB) { ap.enqueueLog(db, "query") }
func (ap *AuditPlugin) enqueueLog(db *gorm.DB, action string) {
if db.Statement.Table == "operation_logs" { return }
log := OperationLog{
TableName: db.Statement.Table,
Action: action,
RecordID: getPrimaryKey(db),
SQL: db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...),
CreatedAt: time.Now(),
}
select {
case ap.logChan <- log:
// enqueued successfully
default:
fmt.Println("[AUDIT] log channel is full, dropping log")
}
}
func getPrimaryKey(db *gorm.DB) string {
if len(db.Statement.Schema.PrimaryFields) > 0 {
if pk, ok := db.Statement.Schema.PrimaryFields[0].ValueOf(db.Statement.Context, db.Statement.ReflectValue); ok {
return fmt.Sprintf("%v", pk)
}
}
return ""
}2. Using the Plugin
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"your_project/audit"
)
type User struct {
ID uint
Name string
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// Register plugin with custom configuration
plugin := audit.NewAuditPlugin(audit.Config{BatchSize: 50, FlushInterval: 3 * time.Second, BufferSize: 500})
db.Use(plugin)
db.AutoMigrate(&User{})
// Test operations
user := User{Name: "Tom"}
db.Create(&user)
db.Model(&user).Update("Name", "Jerry")
db.First(&user, user.ID)
db.Delete(&user)
// Wait for asynchronous batch write
time.Sleep(4 * time.Second)
var logs []audit.OperationLog
db.Find(&logs)
fmt.Println("Operation logs:", logs)
}After running, the operation_logs table contains entries for all CRUD actions. Logs are written in asynchronous batches; batch size, flush interval, and buffer capacity are configurable.
Best Practices & Performance Tuning
BatchSize : Recommended 50‑500 depending on DB write throughput. Larger batches improve write efficiency but increase log latency.
FlushInterval : Recommended 1‑5 seconds to ensure timely persistence during traffic spikes. Too short adds DB pressure; too long delays logs.
BufferSize : Set to 10‑20× the BatchSize for high‑concurrency scenarios. Too small may drop logs; too large consumes memory.
Database Optimisation : Add indexes on operation_logs (e.g., TableName, CreatedAt). For heavy write loads consider dedicated log stores like ClickHouse or Elasticsearch.
Alternative Async Consumers : If log reliability is critical, replace direct DB writes with a message queue (Kafka, RabbitMQ) feeding a specialised log system.
Further Extensions
The plugin mechanism can be reused for many scenarios, such as:
Automatic field population (e.g., CreatedAt, UpdatedAt).
Multi‑tenant support by appending a TenantID condition.
Enhanced soft‑delete tracking (who deleted, reason).
Monitoring hooks (Prometheus, OpenTelemetry) for observability.
By leveraging GORM's plugin architecture together with asynchronous batch processing and configurable parameters, developers can achieve high code reuse, maintainability, and production‑grade performance for audit‑logging and other cross‑cutting concerns.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Wrench
Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
