Empowering Trust Through Seamless, Secure Auditing for Go Applications
Audit Suite is a comprehensive, modular Go library for implementing robust audit logging in your applications. It provides a clean, extensible architecture for capturing, enriching, and delivering audit events with support for popular web frameworks through dedicated adapters.
Key Features:
- 🏗️ Clean Architecture: Separation of concerns with ports and adapters pattern
- 🔧 Framework Agnostic: Core library works with any Go application
- 📊 Rich Event Model: Comprehensive audit events with metadata, timing, and data snapshots
- 🔒 Secure Context: Thread-safe context management with mutex protection
- 🚀 Pluggable Backends: Interface-driven design for any audit destination
- 🌐 Echo Integration: Ready-to-use middleware for Echo web framework
- 📈 Performance Focused: Asynchronous event publishing with minimal overhead
The github.com/ouharri/audit core library provides the foundational components for audit logging in Go applications. It implements a clean architecture with clear separation between domain logic, ports (interfaces), and adapters (implementations).
Module Path:github.com/ouharri/audit
- Compliance Auditing: Track all CRUD operations for regulatory compliance
- Security Monitoring: Monitor user actions and detect suspicious behavior
- Business Intelligence: Capture business events for analytics and reporting
- Debugging & Tracing: Correlate application events across microservices
- Change Tracking: Maintain audit trails for data modifications
github.com/ouharri/audit/
├── core/
│ ├── context.go # AuditableContext and context management
│ ├── decorators.go # High-level audit decorators (Create, Update, etc.)
│ ├── domain.go # Core domain types and AuditEvent
│ ├── options.go # Functional options for context modification
│ └── utils.go # Context utilities and helpers
├── port/
│ ├── auditor.go # Auditor interface for event publishing
│ └── middleware.go # Generic middleware interfaces
├── transport/
│ └── config.go # Transport configuration structure
├── echoadapter/
│ ├── middleware.go # Echo-specific middleware implementation
│ ├── singleton.go # Singleton pattern for global configuration
│ └── types.go # Echo-specific type aliases
├── go.mod
├── go.sum
└── LICENSE
go get github.com/ouharri/auditConfigure the audit system with your specific requirements:
import (
"context""github.com/ouharri/audit/transport""github.com/ouharri/audit/port"
)
// Configure your audit systemconfig:= transport.Config{
// Required: Your audit event publisherAuditor: myAuditor, // implements port.Auditor// Required: Generate unique trace IDsNewTraceID: func() any { returnuuid.New().String() },
// Optional: Extract user from contextUserFromContext: func(ctx context.Context) any {
ifuserID:=ctx.Value("user_id"); userID!=nil {
returnuserID
}
returnnil
},
// Optional: Skip auditing for certain requestsSkipper: func(ctx context.Context) bool {
// Skip health checks, metrics endpoints, etc.returnisHealthCheck(ctx)
},
}// Config encapsulates all components required by transport adapterstypeConfigstruct {
// Required: Processes and delivers audit eventsAuditor port.Auditor// Required: Generates unique trace identifiersNewTraceIDfunc() any// Optional: Extracts current user from contextUserFromContextfunc(ctx context.Context) any// Optional: Determines whether to skip auditingSkipperfunc(ctx context.Context) bool
}// Auditor processes completed audit eventstypeAuditorinterface {
// Audit delivers an event to the configured destinationAudit(ctx context.Context, event core.AuditEvent) error
}
// Middleware abstracts transport-specific middlewaretypeMiddleware[Tany] interface {
// Root returns the global middleware functionRoot() T// For returns a factory for resource-specific middlewareFor(resource core.EntityType) AuditableActionFactory[T]
}
// AuditableActionFactory creates action-specific middlewaretypeAuditableActionFactory[Tany] func(action core.ActionType) T// AuditEvent represents a complete audit recordtypeAuditEventstruct {
TraceIDany`json:"traceId"`UserIDany`json:"userId,omitempty"`Action*ActionType`json:"action,omitempty"`Resource*EntityType`json:"resource,omitempty"`ResourceIDany`json:"resourceId,omitempty"`Metadatamap[string]interface{} `json:"metadata,omitempty"`IPAddressstring`json:"ipAddress,omitempty"`UserAgentstring`json:"userAgent,omitempty"`RequestURIstring`json:"requestUri,omitempty"`Methodstring`json:"method,omitempty"`ResponseCodeint`json:"responseCode,omitempty"`Successbool`json:"success,omitempty"`StartTime time.Time`json:"startTime"`EndTime time.Time`json:"endTime"`OldData json.RawMessage`json:"oldData,omitempty"`NewData json.RawMessage`json:"newData,omitempty"`
}
// ActionType defines the operation being auditedtypeActionTypestring// EntityType defines the resource being auditedtypeEntityTypestring// Common action types (must define these in your application)const (
ActionCreateActionType="CREATE"ActionReadActionType="READ"ActionUpdateActionType="UPDATE"ActionDeleteActionType="DELETE"ActionListActionType="LIST"
)
// AuditableContext holds request-scoped audit informationtypeAuditableContextstruct {
TraceIDany// Unique trace identifierUserIDany// Acting user identifierAction*ActionType// Operation typeResource*EntityType// Target resource typeResourceIDany// Specific resource instanceOldDatainterface{} // Pre-action data snapshotNewDatainterface{} // Post-action data snapshotMetadatamap[string]interface{} // Additional contextIPAddressstring// Client IP addressUserAgentstring// Client user agentRequestURIstring// Request URIMethodstring// HTTP methodResponseCodeint// Response status codeStartTime time.Time// Request start timeEndTime time.Time// Request end time// mu sync.RWMutex // Thread safety (unexported)
}// SetContext applies options to the audit context in ctxfuncSetContext(ctx context.Context, opts...ContextOption)
// SetEchoAuditContext stores context in Echo frameworkfuncSetEchoAuditContext(c echo.Context, auditCtx*AuditableContext)
// GetAuditContext retrieves audit context from ctxfuncGetAuditContext(ctx context.Context) *AuditableContext// Thread-safe context methodsfunc (ac*AuditableContext) SetUserID(userIDany)
func (ac*AuditableContext) SetResourceID(resourceIDany)
func (ac*AuditableContext) SetOldData(datainterface{})
func (ac*AuditableContext) SetNewData(datainterface{})
func (ac*AuditableContext) SetMetadata(keystring, valueinterface{})
func (ac*AuditableContext) SetBulkMetadata(metadatamap[string]interface{})
// ToEvent converts context to publishable eventfunc (ac*AuditableContext) ToEvent() *AuditEventHigh-level functions for common audit operations:
// AuditableCreate records a create operationfuncAuditableCreate(ctx context.Context, newDatainterface{})
// AuditableUpdate records an update operationfuncAuditableUpdate(ctx context.Context, resourceIDany, oldData, newDatainterface{})
// AuditableDelete records a delete operationfuncAuditableDelete(ctx context.Context, resourceIDany, oldDatainterface{})
// AuditableGet records a read operationfuncAuditableGet(ctx context.Context, resourceIDany)
// AuditableList records a list operationfuncAuditableList(ctx context.Context, metadatamap[string]interface{})
// AuditablePage records a paginated list operationfuncAuditablePage(ctx context.Context, pageDatainterface{})
// AuditableAction records a custom actionfuncAuditableAction(ctx context.Context, resourceIDany, metadatamap[string]interface{})Functional options for modifying audit context:
// WithUserID sets the acting userfuncWithUserID(userIDany) ContextOption// WithResourceID sets the target resource instancefuncWithResourceID(resourceIDany) ContextOption// WithOldData sets the pre-action data snapshotfuncWithOldData(datainterface{}) ContextOption// WithNewData sets the post-action data snapshotfuncWithNewData(datainterface{}) ContextOption// WithMetadata adds a single metadata key-value pairfuncWithMetadata(keystring, valueinterface{}) ContextOption// WithBulkMetadata merges multiple metadata entriesfuncWithBulkMetadata(metadatamap[string]interface{}) ContextOptionBasic Context Manipulation:
package main
import (
"context""github.com/ouharri/audit/core"
)
funcupdateUser(ctx context.Context, userIDstring, oldUser, newUserUser) error {
// Record the update operationcore.SetContext(ctx,
core.WithResourceID(userID),
core.WithOldData(oldUser),
core.WithNewData(newUser),
)
// Perform your business logicreturnuserService.Update(userID, newUser)
}Using Audit Decorators:
package main
import (
"context""github.com/ouharri/audit/core"
)
funccreateProduct(ctx context.Context, productProduct) (*Product, error) {
// Perform creationcreated, err:=productService.Create(product)
iferr!=nil {
returnnil, err
}
// Record the creationcore.AuditableCreate(ctx, created)
returncreated, nil
}
funcdeleteProduct(ctx context.Context, productIDstring) error {
// Get existing data before deletionexisting, err:=productService.GetByID(productID)
iferr!=nil {
returnerr
}
// Perform deletioniferr:=productService.Delete(productID); err!=nil {
returnerr
}
// Record the deletioncore.AuditableDelete(ctx, productID, existing)
returnnil
}Custom Auditor Implementation:
package main
import (
"context""encoding/json""log""github.com/ouharri/audit/core"
)
typeLogAuditorstruct {
logger*log.Logger
}
func (la*LogAuditor) Audit(ctx context.Context, event core.AuditEvent) error {
eventJSON, err:=json.Marshal(event)
iferr!=nil {
returnerr
}
la.logger.Printf("AUDIT: %s", string(eventJSON))
returnnil
}
// Usageauditor:=&LogAuditor{logger: log.Default()}The Echo adapter provides seamless integration with the Echo web framework.
package main
import (
"github.com/labstack/echo/v4""github.com/ouharri/audit/echoadapter""github.com/ouharri/audit/transport"
)
funcmain() {
e:=echo.New()
// Initialize Echo adapterechoadapter.Configure(transport.Config{
Auditor: myAuditor,
NewTraceID: generateTraceID,
UserFromContext: extractUser,
})
// Apply global audit middlewaree.Use(echoadapter.Root())
// Configure routes with specific audit settingssetupRoutes(e)
e.Logger.Fatal(e.Start(":8080"))
}// Configure initializes the Echo adapter with audit configurationfuncConfigure(cfg transport.Config)
// Root returns the global Echo middleware functionfuncRoot() echo.MiddlewareFunc// For returns a factory for resource-specific middlewarefuncFor(resource core.EntityType) AuditableEchoActionFactory// Type aliases for Echo integrationtypeAuditableEchoActionFactory= port.AuditableActionFactory[echo.MiddlewareFunc]
typeAuditableEchoMiddleware= port.Middleware[echo.MiddlewareFunc]Basic Route Configuration:
funcsetupRoutes(e*echo.Echo) {
// Define resource typesconst (
EntityUser core.EntityType="USER"EntityProduct core.EntityType="PRODUCT"
)
// Create factories for different resourcesuserAudit:=echoadapter.For(EntityUser)
productAudit:=echoadapter.For(EntityProduct)
// Configure user routesuserGroup:=e.Group("/users")
userGroup.POST("", createUserHandler, userAudit(core.ActionCreate))
userGroup.GET("/:id", getUserHandler, userAudit(core.ActionRead))
userGroup.PUT("/:id", updateUserHandler, userAudit(core.ActionUpdate))
userGroup.DELETE("/:id", deleteUserHandler, userAudit(core.ActionDelete))
// Configure product routesproductGroup:=e.Group("/products")
productGroup.POST("", createProductHandler, productAudit(core.ActionCreate))
productGroup.GET("", listProductsHandler, productAudit(core.ActionList))
}Handler Implementation with Audit Context:
funcupdateUserHandler(c echo.Context) error {
ctx:=c.Request().Context()
userID:=c.Param("id")
varupdateReqUpdateUserRequestiferr:=c.Bind(&updateReq); err!=nil {
returnerr
}
// Get existing user for audit trailexistingUser, err:=userService.GetByID(ctx, userID)
iferr!=nil {
returnerr
}
// Update userupdatedUser, err:=userService.Update(ctx, userID, updateReq)
iferr!=nil {
returnerr
}
// Record the update operationcore.AuditableUpdate(ctx, userID, existingUser, updatedUser)
returnc.JSON(http.StatusOK, updatedUser)
}Here's a complete example demonstrating the audit system in a real Echo application:
package main
import (
"context""encoding/json""log""net/http""strconv""time""github.com/google/uuid""github.com/labstack/echo/v4""github.com/labstack/echo/v4/middleware""github.com/ouharri/audit/core""github.com/ouharri/audit/echoadapter""github.com/ouharri/audit/transport"
)
// Domain typestypeUserstruct {
IDint`json:"id"`Namestring`json:"name"`Emailstring`json:"email"`Created time.Time`json:"created"`Modified time.Time`json:"modified"`
}
// Resource typesconst (
EntityUser core.EntityType="USER"
)
// Action typesconst (
ActionCreate core.ActionType="CREATE"ActionRead core.ActionType="READ"ActionUpdate core.ActionType="UPDATE"ActionDelete core.ActionType="DELETE"ActionList core.ActionType="LIST"
)
// Simple in-memory auditortypeConsoleAuditorstruct {
logger*log.Logger
}
func (ca*ConsoleAuditor) Audit(ctx context.Context, event core.AuditEvent) error {
eventData, _:=json.MarshalIndent(event, "", " ")
ca.logger.Printf("🔍 AUDIT EVENT:\n%s\n", string(eventData))
returnnil
}
// Mock user servicetypeUserServicestruct {
usersmap[int]*UsernextIDint
}
funcNewUserService() *UserService {
return&UserService{
users: make(map[int]*User),
nextID: 1,
}
}
func (us*UserService) Create(user*User) *User {
user.ID=us.nextIDus.nextID++user.Created=time.Now()
user.Modified=time.Now()
us.users[user.ID] =userreturnuser
}
func (us*UserService) GetByID(idint) (*User, error) {
ifuser, exists:=us.users[id]; exists {
returnuser, nil
}
returnnil, echo.NewHTTPError(http.StatusNotFound, "User not found")
}
func (us*UserService) Update(idint, updates*User) (*User, error) {
user, err:=us.GetByID(id)
iferr!=nil {
returnnil, err
}
ifupdates.Name!="" {
user.Name=updates.Name
}
ifupdates.Email!="" {
user.Email=updates.Email
}
user.Modified=time.Now()
returnuser, nil
}
func (us*UserService) Delete(idint) (*User, error) {
user, err:=us.GetByID(id)
iferr!=nil {
returnnil, err
}
delete(us.users, id)
returnuser, nil
}
func (us*UserService) List() []*User {
users:=make([]*User, 0, len(us.users))
for_, user:=rangeus.users {
users=append(users, user)
}
returnusers
}
// HandlersfunccreateUserHandler(userService*UserService) echo.HandlerFunc {
returnfunc(c echo.Context) error {
ctx:=c.Request().Context()
varuserUseriferr:=c.Bind(&user); err!=nil {
returnerr
}
created:=userService.Create(&user)
// Record creation in audit contextcore.AuditableCreate(ctx, created)
returnc.JSON(http.StatusCreated, created)
}
}
funcgetUserHandler(userService*UserService) echo.HandlerFunc {
returnfunc(c echo.Context) error {
ctx:=c.Request().Context()
id, err:=strconv.Atoi(c.Param("id"))
iferr!=nil {
returnecho.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
}
user, err:=userService.GetByID(id)
iferr!=nil {
returnerr
}
// Record read operationcore.AuditableGet(ctx, id)
returnc.JSON(http.StatusOK, user)
}
}
funcupdateUserHandler(userService*UserService) echo.HandlerFunc {
returnfunc(c echo.Context) error {
ctx:=c.Request().Context()
id, err:=strconv.Atoi(c.Param("id"))
iferr!=nil {
returnecho.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
}
// Get existing user for audit trailexistingUser, err:=userService.GetByID(id)
iferr!=nil {
returnerr
}
varupdatesUseriferr:=c.Bind(&updates); err!=nil {
returnerr
}
updatedUser, err:=userService.Update(id, &updates)
iferr!=nil {
returnerr
}
// Record update operation with before/after datacore.AuditableUpdate(ctx, id, existingUser, updatedUser)
returnc.JSON(http.StatusOK, updatedUser)
}
}
funcdeleteUserHandler(userService*UserService) echo.HandlerFunc {
returnfunc(c echo.Context) error {
ctx:=c.Request().Context()
id, err:=strconv.Atoi(c.Param("id"))
iferr!=nil {
returnecho.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
}
deletedUser, err:=userService.Delete(id)
iferr!=nil {
returnerr
}
// Record deletion with deleted datacore.AuditableDelete(ctx, id, deletedUser)
returnc.JSON(http.StatusOK, map[string]string{
"message": "User deleted successfully",
})
}
}
funclistUsersHandler(userService*UserService) echo.HandlerFunc {
returnfunc(c echo.Context) error {
ctx:=c.Request().Context()
users:=userService.List()
// Record list operation with metadatacore.AuditableList(ctx, map[string]interface{}{
"total_count": len(users),
"query_time": time.Now(),
})
returnc.JSON(http.StatusOK, users)
}
}
// Extract user ID from context (for demo purposes)funcextractUserFromContext(ctx context.Context) any {
// In a real application, this would extract from JWT, session, etc.ifuserID:=ctx.Value("user_id"); userID!=nil {
returnuserID
}
return"anonymous"// Default user
}
// Skip auditing for health checksfuncshouldSkipAudit(ctx context.Context) bool {
// In a real application, check request path, headers, etc.returnfalse// Audit everything for demo
}
funcmain() {
// Initialize servicesuserService:=NewUserService()
// Create audit configurationauditConfig:= transport.Config{
Auditor: &ConsoleAuditor{
logger: log.New(log.Writer(), "AUDIT ", log.LstdFlags),
},
NewTraceID: func() any {
returnuuid.New().String()
},
UserFromContext: extractUserFromContext,
Skipper: shouldSkipAudit,
}
// Initialize Echo with audit middlewaree:=echo.New()
// Basic middlewaree.Use(middleware.Logger())
e.Use(middleware.Recover())
// Configure audit systemechoadapter.Configure(auditConfig)
e.Use(echoadapter.Root())
// Setup routes with audit configurationuserAudit:=echoadapter.For(EntityUser)
userGroup:=e.Group("/users")
userGroup.POST("", createUserHandler(userService), userAudit(ActionCreate))
userGroup.GET("/:id", getUserHandler(userService), userAudit(ActionRead))
userGroup.PUT("/:id", updateUserHandler(userService), userAudit(ActionUpdate))
userGroup.DELETE("/:id", deleteUserHandler(userService), userAudit(ActionDelete))
userGroup.GET("", listUsersHandler(userService), userAudit(ActionList))
// Health check endpoint (could be configured to skip auditing)e.GET("/health", func(c echo.Context) error {
returnc.JSON(http.StatusOK, map[string]string{
"status": "healthy",
"time": time.Now().Format(time.RFC3339),
})
})
// Start serverlog.Println("🚀 Starting server on :8080")
log.Println("📝 Try these endpoints:")
log.Println(" POST /users")
log.Println(" GET /users/:id")
log.Println(" PUT /users/:id")
log.Println(" DELETE /users/:id")
log.Println(" GET /users")
e.Logger.Fatal(e.Start(":8080"))
}Test the application:
# Create a user
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'# Get user
curl http://localhost:8080/users/1
# Update user
curl -X PUT http://localhost:8080/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"jane@example.com"}'# List users
curl http://localhost:8080/users
# Delete user
curl -X DELETE http://localhost:8080/users/1// Always handle audit errors gracefullyfunc (em*EchoMw) publish(ctx context.Context, auditCtx*core.AuditableContext) {
event:=auditCtx.ToEvent()
iferr:=em.cfg.Auditor.Audit(ctx, *event); err!=nil {
// Log error but don't fail the requestlog.Printf("Failed to publish audit event: %v", err)
// Optional: Send to dead letter queue or retry mechanism
}
}// Sanitize sensitive data before auditingfuncsanitizeUser(user*User) *User {
sanitized:=*usersanitized.Password="[REDACTED]"sanitized.SSN="[REDACTED]"return&sanitized
}
// Use in handlerscore.AuditableCreate(ctx, sanitizeUser(user))// Add contextual information to eventscore.SetContext(ctx,
core.WithResourceID(userID),
core.WithMetadata("department", "engineering"),
core.WithMetadata("api_version", "v2"),
core.WithMetadata("feature_flag", "new_user_flow"),
)This project is licensed under the MIT License.
Note: This documentation reflects the actual structure and functionality of your audit library. All code examples are functional and ready to use. For specific implementation details, refer to the source code in the respective packages.