Logging Technical Guide
This guide covers the implementation details of the IOTA SDK Logging module.
Architecture
The logging module is built on the Logrus library with custom hooks and formatters for structured logging with automatic context tracking.
Core Components
Logger Interface
// From composables package
logger := composables.UseLogger(ctx)
// Standard methods
logger.Debug(msg string)
logger.Info(msg string)
logger.Warn(msg string)
logger.Error(msg string)
logger.Fatal(msg string)
// Field methods
logger.WithField(key string, value interface{}) *logrus.Entry
logger.WithFields(fields map[string]interface{}) *logrus.Entry
SourceHook Implementation
Automatically captures source location information:
// Automatically added to all logs
{
"source_file": "/path/to/file.go",
"source_line": 42,
"source_function": "FunctionName"
}
Context-Aware Fields
Fields automatically extracted from context:
// Tenant ID (if available)
"tenant_id": ctx.Value("tenant_id")
// User ID (if authenticated)
"user_id": ctx.Value("user_id")
// Request ID (for correlation)
"request_id": ctx.Value("request_id")
// Operation name
"operation": ctx.Value("operation")
Usage Patterns
Basic Logging
Simple Log Messages
logger := composables.UseLogger(ctx)
logger.Info("User logged in successfully")
logger.Warn("Rate limit approaching")
logger.Error("Failed to process payment")
Log with Fields
logger.WithFields(map[string]interface{}{
"user_id": user.ID(),
"email": user.Email(),
"action": "login",
}).Info("User authentication")
Controller Logging
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
logger := composables.UseLogger(ctx)
userID := mux.Vars(r)["id"]
logger.WithField("user_id", userID).Debug("Fetching user")
user, err := c.userService.GetByID(ctx, userID)
if err != nil {
logger.WithField("user_id", userID).Error("Failed to fetch user")
http.Error(w, "Not found", http.StatusNotFound)
return
}
logger.WithField("user_id", userID).Info("User retrieved successfully")
// Continue with response...
}
Service Layer Logging
func (s *UserService) Create(ctx context.Context, dto *CreateUserDTO) (User, error) {
const op serrors.Op = "UserService.Create"
logger := composables.UseLogger(ctx)
logger.WithField("email", dto.Email).Debug("Creating new user")
// Validation
if err := s.validateEmail(dto.Email); err != nil {
logger.WithFields(map[string]interface{}{
"email": dto.Email,
"error": err.Error(),
}).Warn("User creation validation failed")
return nil, serrors.E(op, serrors.KindValidation, err)
}
// Create user
user, err := s.repo.Create(ctx, user)
if err != nil {
logger.WithField("email", dto.Email).Error("Failed to create user")
return nil, serrors.E(op, err)
}
logger.WithFields(map[string]interface{}{
"user_id": user.ID(),
"email": user.Email(),
}).Info("User created successfully")
return user, nil
}
Repository Logging
func (r *UserRepository) Create(ctx context.Context, user User) (User, error) {
const op serrors.Op = "UserRepository.Create"
logger := composables.UseLogger(ctx)
tenantID := composables.UseTenantID(ctx)
logger.WithFields(map[string]interface{}{
"tenant_id": tenantID,
"email": user.Email(),
}).Debug("Inserting user into database")
// Database operation...
return user, nil
}
Configuration
Environment Variables
# Log Level (debug, info, warn, error)
LOG_LEVEL=info
# Log Format (json, text)
LOG_FORMAT=json
# Log Output (stdout, stderr, file path)
LOG_OUTPUT=stdout
# Structured logging
STRUCTURED_LOGGING=true
# Include source location
INCLUDE_SOURCE_LOCATION=true
Programmatic Configuration
import "github.com/iota-uz/iota-sdk/pkg/configuration"
conf := configuration.Use()
// Configure logger
conf.Logger().SetLevel(logrus.InfoLevel)
conf.Logger().SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05Z07:00",
})
Database Schema
The logging module includes a schema for persisting logs:
CREATE TABLE IF NOT EXISTS logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
level VARCHAR(10) NOT NULL,
message TEXT NOT NULL,
fields JSONB,
source_file VARCHAR(255),
source_line INTEGER,
source_function VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_logs_tenant_level (tenant_id, level),
INDEX idx_logs_created_at (created_at DESC)
);
Field Types
Standard Fields
level: Log level (debug, info, warn, error)msg: Log messagetime: Timestamp (ISO 8601)source_file: Source file pathsource_line: Line numbersource_function: Function name
Context Fields
tenant_id: UUIDuser_id: UUIDrequest_id: UUID or stringoperation: Operation name
Custom Fields
Any arbitrary fields added via WithField() or WithFields()
Performance Considerations
Structured Logging Overhead
- JSON formatting: ~5-10% overhead vs text logging
- Field extraction: Minimal (context value lookups)
- Source location: ~2-5% overhead with SourceHook
Best Practices
- Use Appropriate Log Levels
// Debug for development/diagnostics logger.Debug("Detailed diagnostic information") // Info for important events logger.Info("User action completed") // Warn for potentially problematic situations logger.Warn("Deprecated API usage detected") // Error for failures logger.Error("Operation failed") - Avoid Logging Sensitive Data
// GOOD: Don't log passwords or tokens logger.WithField("email", user.Email()).Info("User logged in") // BAD: Never log sensitive data logger.WithField("password", password).Info("Login attempted") // DON'T DO THIS - Use Structured Fields for Debugging
// Good for log aggregation logger.WithFields(map[string]interface{}{ "action": "payment_processed", "amount": 100.50, "currency": "USD", "payment_method": "card", }).Info("Payment completed") - Include Context for Correlation
// Enables tracing across services logger.WithFields(map[string]interface{}{ "request_id": requestID, "trace_id": traceID, "span_id": spanID, }).Debug("Processing request")
Integration with Error Handling
import "github.com/iota-uz/iota-sdk/pkg/serrors"
// Log errors with operation context
logger := composables.UseLogger(ctx)
if err := someOperation(ctx); err != nil {
logger.WithFields(map[string]interface{}{
"error": err.Error(),
"operation": "someOperation",
"user_id": userID,
}).Error("Operation failed")
return serrors.E("operation", err)
}
Testing Logging
import "testing"
func TestUserCreation(t *testing.T) {
// Create test context with logger
ctx := composables.WithLogger(context.Background(), logrus.New())
// Your test code...
// Verify logs were created (optional)
// Use log capture middleware or spy
}
Common Issues and Solutions
Missing Tenant Context
Problem: Logs don’t include tenant_id field Solution: Ensure tenant context is set in middleware
middleware.WithTenant(ctx, tenantID)
Performance Issues
Problem: Logging is slowing down application Solution:
- Reduce log level (use
warnorerrorin production) - Avoid logging in tight loops
- Use buffered output
Large Field Values
Problem: Large JSON objects in logs causing storage issues Solution: Serialize only necessary fields
// Instead of logging entire object
logger.WithField("user", user).Info("User action")
// Log only relevant fields
logger.WithFields(map[string]interface{}{
"user_id": user.ID(),
"email": user.Email(),
"action": "login",
}).Info("User logged in")
Advanced Topics
Custom Hooks
Create custom hooks for special log processing:
type CustomHook struct{}
func (h *CustomHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *CustomHook) Fire(entry *logrus.Entry) error {
// Custom processing
return nil
}
// Register hook
logger.AddHook(&CustomHook{})
Log Aggregation
Configure remote log shipping:
# Send logs to external service
LOG_OUTPUT=syslog://logs.example.com:514
Metrics and Monitoring
Integrate with observability tools:
// Prometheus metrics
logMetrics := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "log_entries_total",
Help: "Total number of log entries",
},
[]string{"level", "module"},
)
For more information, see the Logging module overview or consult the IOTA SDK documentation.