Instruction file imported from hieumh/tracking-finance-stock (
.github/instructions/logging-backend-go.instructions.md). Copyright stays with the author.
DDD Backend Logging Instructions for Go - VS Code Copilot
Overview
This document provides comprehensive instructions for implementing logging best practices in Domain-Driven Design (DDD) backend systems using Go. Follow these guidelines when generating code suggestions for logging in DDD architectures.
Core Logging Principles
1. Layer-Specific Logging Rules
Domain Layer
- NEVER log infrastructure concerns (database connections, HTTP details)
- ALWAYS log business-critical events and domain state changes
- LOG aggregate creation, modification, and business rule violations
- INCLUDE aggregate ID, correlation ID, and business context
- AVOID logging sensitive domain data in plain text
// ✅ GOOD - Domain logging
log.Info("Order created",
slog.String("order_id", order.ID),
slog.String("customer_id", order.CustomerID),
slog.Float64("total_amount", order.TotalAmount),
slog.String("correlation_id", ctx.Value("correlation_id").(string)))
// ❌ BAD - Infrastructure concerns in domain
log.Debug("Database connection established for order creation")
Application Layer
- LOG use case/command execution start and completion
- LOG authorization and validation results
- LOG transaction boundaries and performance metrics
- INCLUDE execution time, user context, and command details
- AVOID logging sensitive data directly (use masking functions)
- AVOID logging duration executed commands unless necessary for performance monitoring
start := time.Now()
// ... command execution
duration := time.Since(start)
log.Debug("CreateOrderCommand duration",
slog.String("command_id", cmd.ID),
slog.Duration("duration", duration))
Infrastructure Layer
- LOG external service calls and their outcomes
- LOG database operations, connections, and performance
- LOG message queue operations and failures
- INCLUDE connection details, retry attempts, and error details
2. Structured Logging Format
Required Fields
Always include these fields in log entries:
timestamp- RFC3339 format (handled by slog automatically)level- Log level (DEBUG, INFO, WARN, ERROR)message- Human-readable messagecorrelation_id- Request/operation correlation IDservice_id- Microservice or bounded context identifieruser_id- Acting user identifier (when available)
Contextual Fields (when applicable)
aggregate_id- Domain aggregate identifieraggregate_type- Type of aggregate being operated ondomain_event- Domain event type being processeduse_case_type- Application use case or command typetransaction_id- Database transaction identifierduration- Operation execution time
3. Go Logging Setup
Recommended Logger Configuration
package logging
import (
"log/slog"
"os"
"context"
)
type Logger struct {
*slog.Logger
}
func NewLogger(serviceName string) *Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler).With(
slog.String("service", serviceName),
)
return &Logger{Logger: logger}
}
// WithContext adds correlation ID and other context info
func (l *Logger) WithContext(ctx context.Context) *slog.Logger {
attrs := []slog.Attr{}
if correlationID := ctx.Value("correlation_id"); correlationID != nil {
attrs = append(attrs, slog.String("correlation_id", correlationID.(string)))
}
if userID := ctx.Value("user_id"); userID != nil {
attrs = append(attrs, slog.String("user_id", userID.(string)))
}
return l.With(attrs...)
}
4. Log Level Guidelines
INFO Level
Use for business-significant events that operators should be aware of:
- Successful completion of use cases/commands
- Domain events being raised or processed
- Important business state transitions
- User authentication and authorization events
DEBUG Level
Use for detailed execution flow helpful during development:
- Method entry/exit in domain services
- Internal domain logic flow
- Query execution details
- Cache hit/miss information
WARN Level
Use for potentially problematic situations that don't stop execution:
- Business rule violations that are handled gracefully
- Retry attempts for external services
- Performance degradation warnings
- Non-critical validation failures
ERROR Level
Use for errors that prevent normal operation:
- Unhandled errors in use cases
- External service failures that can't be retried
- Data consistency violations
- Authentication/authorization failures
Implementation Patterns
1. Domain Aggregate Logging
package domain
import (
"context"
"fmt"
"log/slog"
)
type OrderAggregate struct {
ID string
CustomerID string
Status OrderStatus
logger *config.Logger
}
func NewOrderAggregate(id, customerID string, logger *config.Logger) *OrderAggregate {
return &OrderAggregate{
ID: id,
CustomerID: customerID,
Status: OrderStatusPending,
logger: logger,
}
}
func (o *OrderAggregate) ChangeStatus(ctx context.Context, newStatus OrderStatus) error {
oldStatus := o.Status
// Validate business rules
if !o.canChangeStatusTo(newStatus) {
o.logger.Warn("Invalid status transition attempted",
slog.String("order_id", o.ID),
slog.String("from_status", string(oldStatus)),
slog.String("to_status", string(newStatus)),
slog.String("reason", "Business rule violation"),
slog.String("correlation_id", getCorrelationID(ctx)))
return fmt.Errorf("cannot change status from %s to %s", oldStatus, newStatus)
}
o.Status = newStatus
o.logger.Info("Order status changed",
slog.String("order_id", o.ID),
slog.String("from_status", string(oldStatus)),
slog.String("to_status", string(newStatus)),
slog.String("user_id", getUserID(ctx)),
slog.String("correlation_id", getCorrelationID(ctx)))
// Raise domain event
event := NewOrderStatusChangedEvent(o.ID, oldStatus, newStatus)
o.raiseDomainEvent(event)
o.logger.Debug("Domain event raised",
slog.String("event_type", "OrderStatusChangedEvent"),
slog.String("order_id", o.ID),
slog.String("correlation_id", getCorrelationID(ctx)))
return nil
}
func (o *OrderAggregate) canChangeStatusTo(newStatus OrderStatus) bool {
// Business logic here
return true
}
2. Application Service Logging
package application
import (
"context"
"fmt"
"log/slog"
"time"
)
type CreateOrderCommandHandler struct {
orderService OrderService
validator Validator
logger *config.Logger // Always inject *config.Logger, never *slog.Logger
}
func NewCreateOrderCommandHandler(
orderService OrderService,
validator Validator,
logger *config.Logger,
) *CreateOrderCommandHandler {
return &CreateOrderCommandHandler{
orderService: orderService,
validator: validator,
logger: logger,
}
}
func (h *CreateOrderCommandHandler) Handle(ctx context.Context, cmd CreateOrderCommand) (string, error) {
// Always call WithContext at the top of every handler method.
// This injects correlation_id / user_id and gives back a plain *slog.Logger to use locally.
log := h.logger.WithContext(ctx)
log.Info("CreateOrderCommand started",
slog.String("command_id", cmd.ID),
slog.String("customer_id", cmd.CustomerID),
slog.Int("item_count", len(cmd.Items)),
slog.String("command_type", "CreateOrder"))
// Validation
if err := h.validator.Validate(cmd); err != nil {
log.Warn("CreateOrderCommand validation failed",
slog.String("error", err.Error()),
slog.String("command_type", "CreateOrder"))
return "", fmt.Errorf("validation failed: %w", err)
}
// Business logic
order, err := h.orderService.CreateOrder(ctx, cmd)
if err != nil {
log.Error("CreateOrderCommand failed",
slog.String("error", err.Error()),
slog.String("command_type", "CreateOrder"))
return "", fmt.Errorf("failed to create order: %w", err)
}
log.Info("CreateOrderCommand completed successfully",
slog.String("order_id", order.ID),
slog.String("command_type", "CreateOrder"))
return order.ID, nil
}
3. Infrastructure Repository Logging
package infrastructure
import (
"context"
"database/sql"
"log/slog"
)
type OrderRepository struct {
db *sql.DB
logger *config.Logger
}
func NewOrderRepository(db *sql.DB, logger *config.Logger) *OrderRepository {
return &OrderRepository{
db: db,
logger: logger,
}
}
func (r *OrderRepository) Save(ctx context.Context, order *domain.Order) error {
r.logger.Debug("Saving order to database",
slog.String("order_id", order.ID),
slog.String("correlation_id", getCorrelationID(ctx)))
query := `INSERT INTO orders (id, customer_id, status, total_amount)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status, total_amount = EXCLUDED.total_amount`
result, err := r.db.ExecContext(ctx, query,
order.ID, order.CustomerID, order.Status, order.TotalAmount)
if err != nil {
r.logger.Error("Failed to save order",
slog.String("order_id", order.ID),
slog.String("error", err.Error()),
slog.String("correlation_id", getCorrelationID(ctx)))
return fmt.Errorf("failed to save order: %w", err)
}
rowsAffected, _ := result.RowsAffected()
r.logger.Info("Order saved successfully",
slog.String("order_id", order.ID),
slog.Int64("rows_affected", rowsAffected),
slog.String("correlation_id", getCorrelationID(ctx)))
return nil
}
func (r *OrderRepository) FindByID(ctx context.Context, id string) (*domain.Order, error) {
r.logger.Debug("Finding order by ID",
slog.String("order_id", id),
slog.String("correlation_id", getCorrelationID(ctx)))
query := `SELECT id, customer_id, status, total_amount FROM orders WHERE id = $1`
var order domain.Order
err := r.db.QueryRowContext(ctx, query, id).Scan(
&order.ID, &order.CustomerID, &order.Status, &order.TotalAmount)
if err != nil {
if err == sql.ErrNoRows {
r.logger.Warn("Order not found",
slog.String("order_id", id),
slog.String("correlation_id", getCorrelationID(ctx)))
return nil, domain.ErrOrderNotFound
}
r.logger.Error("Failed to find order",
slog.String("order_id", id),
slog.String("error", err.Error()),
slog.String("correlation_id", getCorrelationID(ctx)))
return nil, fmt.Errorf("failed to find order: %w", err)
}
r.logger.Debug("Order found successfully",
slog.String("order_id", id),
slog.String("correlation_id", getCorrelationID(ctx)))
return &order, nil
}
4. Event Handler Logging
package application
import (
"context"
"log/slog"
)
type OrderStatusChangedEventHandler struct {
notificationService NotificationService
logger *config.Logger
}
func NewOrderStatusChangedEventHandler(
notificationService NotificationService,
logger *config.Logger,
) *OrderStatusChangedEventHandler {
return &OrderStatusChangedEventHandler{
notificationService: notificationService,
logger: logger,
}
}
func (h *OrderStatusChangedEventHandler) Handle(ctx context.Context, event domain.OrderStatusChangedEvent) error {
log := h.logger.WithContext(ctx)
log.Info("Processing domain event",
slog.String("event_type", "OrderStatusChangedEvent"),
slog.String("order_id", event.OrderID),
slog.String("from_status", string(event.OldStatus)),
slog.String("to_status", string(event.NewStatus)),
slog.String("correlation_id", event.CorrelationID))
if err := h.notificationService.SendOrderStatusUpdate(ctx, event); err != nil {
log.Error("Failed to process domain event",
slog.String("event_type", "OrderStatusChangedEvent"),
slog.String("order_id", event.OrderID),
slog.String("error", err.Error()),
slog.String("correlation_id", event.CorrelationID))
return fmt.Errorf("failed to send notification: %w", err)
}
log.Info("Domain event processed successfully",
slog.String("event_type", "OrderStatusChangedEvent"),
slog.String("order_id", event.OrderID),
slog.String("correlation_id", event.CorrelationID))
return nil
}
5. HTTP Middleware for Correlation ID
package middleware
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/google/uuid"
)
type LoggingMiddleware struct {
logger *config.Logger
}
func NewLoggingMiddleware(logger *config.Logger) *LoggingMiddleware {
return &LoggingMiddleware{logger: logger}
}
func (m *LoggingMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Get or generate correlation ID
correlationID := r.Header.Get("X-Correlation-ID")
if correlationID == "" {
correlationID = uuid.New().String()
}
// Add correlation ID to context
ctx := context.WithValue(r.Context(), "correlation_id", correlationID)
r = r.WithContext(ctx)
// Add correlation ID to response header
w.Header().Set("X-Correlation-ID", correlationID)
// Wrap response writer to capture status code
wrappedWriter := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
m.logger.Info("HTTP request started",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
slog.String("user_agent", r.UserAgent()),
slog.String("correlation_id", correlationID))
next.ServeHTTP(wrappedWriter, r)
duration := time.Since(start)
m.logger.Info("HTTP request completed",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status_code", wrappedWriter.statusCode),
slog.Duration("duration", duration),
slog.String("correlation_id", correlationID))
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
Security and Privacy Guidelines
1. Never Log Sensitive Data
- Passwords, tokens, or API keys
- Credit card numbers or payment details
- Personal identification numbers
- Full email addresses (use masked versions)
- Full phone numbers (use masked versions)
2. Data Masking Patterns
package logging
import (
"strings"
)
// MaskEmail masks email address for logging
func MaskEmail(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return "invalid_email"
}
username := parts[0]
domain := parts[1]
if len(username) <= 2 {
return "**@" + domain
}
return username[:2] + "***@" + domain
}
// MaskPhone masks phone number for logging
func MaskPhone(phone string) string {
if len(phone) < 4 {
return "****"
}
return "****" + phone[len(phone)-4:]
}
// Example usage
log.Info("User login attempt",
slog.String("email", MaskEmail(user.Email)),
slog.Bool("success", loginResult.IsSuccess),
slog.String("correlation_id", correlationID))
Performance Considerations
1. Conditional Logging for Expensive Operations
// ✅ GOOD - Check log level before expensive operations
if logger.Enabled(ctx, slog.LevelDebug) {
complexData := serializeComplexObject(calculationResult)
logger.Debug("Complex calculation result",
slog.String("order_id", orderID),
slog.String("details", complexData),
slog.String("correlation_id", correlationID))
}
2. Use Structured Logging Parameters
// ✅ GOOD - Structured parameters
logger.Info("Order processed",
slog.String("order_id", order.ID),
slog.Float64("amount", order.Amount))
// ❌ BAD - String formatting
logger.Info(fmt.Sprintf("Order processed - OrderId: %s, Amount: %.2f",
order.ID, order.Amount))
Error Handling and Logging
1. Error Wrapping and Logging Pattern
func (s *OrderService) ProcessOrder(ctx context.Context, orderID string) error {
logger := s.logger.With(
slog.String("order_id", orderID),
slog.String("correlation_id", getCorrelationID(ctx)))
order, err := s.repository.FindByID(ctx, orderID)
if err != nil {
if errors.Is(err, domain.ErrOrderNotFound) {
logger.Warn("Order not found for processing",
slog.String("error", err.Error()))
return fmt.Errorf("order not found: %w", err)
}
logger.Error("Failed to retrieve order for processing",
slog.String("error", err.Error()))
return fmt.Errorf("failed to retrieve order: %w", err)
}
if err := order.Process(); err != nil {
var domainErr *domain.DomainError
if errors.As(err, &domainErr) {
logger.Warn("Domain rule violation during order processing",
slog.String("rule", domainErr.Rule),
slog.String("details", domainErr.Details))
} else {
logger.Error("Unexpected error during order processing",
slog.String("error", err.Error()))
}
return fmt.Errorf("failed to process order: %w", err)
}
logger.Info("Order processed successfully")
return nil
}
2. Context Helper Functions
package logging
import "context"
func getCorrelationID(ctx context.Context) string {
if id, ok := ctx.Value("correlation_id").(string); ok {
return id
}
return "unknown"
}
func getUserID(ctx context.Context) string {
if id, ok := ctx.Value("user_id").(string); ok {
return id
}
return "anonymous"
}
func getTraceID(ctx context.Context) string {
if id, ok := ctx.Value("trace_id").(string); ok {
return id
}
return ""
}
Monitoring and Observability
1. Health Check Logging
func (h *HealthChecker) CheckDatabase(ctx context.Context) error {
start := time.Now()
err := h.db.PingContext(ctx)
duration := time.Since(start)
if err != nil {
h.logger.Error("Database health check failed",
slog.String("component", "database"),
slog.Duration("duration", duration),
slog.String("error", err.Error()))
return err
}
h.logger.Info("Database health check passed",
slog.String("component", "database"),
slog.Duration("duration", duration))
return nil
}
2. Performance Metrics
func (s *Service) measureOperation(ctx context.Context, operationName string, fn func() error) error {
start := time.Now()
err := fn()
duration := time.Since(start)
s.logger.Info("Operation completed",
slog.String("operation", operationName),
slog.Duration("duration", duration),
slog.Bool("success", err == nil),
slog.String("correlation_id", getCorrelationID(ctx)))
return err
}
Testing Considerations
1. Logger Mock for Testing
package testing
import (
"bytes"
"log/slog"
"testing"
)
func NewTestLogger(t *testing.T) (*config.Logger, *bytes.Buffer) {
var buf bytes.Buffer
handler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
return &config.Logger{Logger: slog.New(handler)}, &buf
}
// Example test
func TestOrderService_CreateOrder(t *testing.T) {
logger, logBuffer := NewTestLogger(t)
service := NewOrderService(mockRepo, logger)
// Act
err := service.CreateOrder(ctx, command)
// Assert
assert.NoError(t, err)
logOutput := logBuffer.String()
assert.Contains(t, logOutput, "Order created")
assert.Contains(t, logOutput, command.ID)
}
2. Integration Test Logging
func TestIntegration_OrderWorkflow(t *testing.T) {
// Setup test logger that outputs to test logs
logger := slog.New(slog.NewTextHandler(testWriter{t}, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
// Test implementation with detailed logging
}
type testWriter struct {
t *testing.T
}
func (tw testWriter) Write(p []byte) (n int, err error) {
tw.t.Log(string(p))
return len(p), nil
}
Configuration and Environment Setup
1. Environment-Based Logger Configuration
package config
import (
"log/slog"
"os"
"strings"
)
type LogConfig struct {
Level string `json:"level" env:"LOG_LEVEL" default:"info"`
Format string `json:"format" env:"LOG_FORMAT" default:"json"`
Service string `json:"service" env:"SERVICE_NAME" default:"unknown"`
}
func NewLogger(config LogConfig) *slog.Logger {
var level slog.Level
switch strings.ToLower(config.Level) {
case "debug":
level = slog.LevelDebug
case "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
opts := &slog.HandlerOptions{
Level: level,
AddSource: level == slog.LevelDebug,
}
var handler slog.Handler
if config.Format == "text" {
handler = slog.NewTextHandler(os.Stdout, opts)
} else {
handler = slog.NewJSONHandler(os.Stdout, opts)
}
return slog.New(handler).With(
slog.String("service", config.Service),
)
}
Quick Reference Checklist
When implementing logging in Go DDD backend systems, ensure:
- Use
log/slogfor structured logging with JSON output - Include correlation ID in all log entries via context
- Log business events at INFO level in domain layer
- Log technical details at DEBUG level in infrastructure layer
- Never log sensitive data in plain text - use masking functions
- Use structured logging with
slog.String(),slog.Int(), etc. - Include execution duration for performance monitoring
- Log both success and failure scenarios with appropriate levels
- Maintain consistency in log attribute naming (snake_case)
- Use context to propagate correlation ID through all layers
- Implement proper error wrapping with
fmt.Errorf - Test logging behavior in unit tests using test loggers
- Configure log levels via environment variables
- Use conditional logging for expensive debug operations