From ce7d12f20c1e206cb78a3b7aa7b0487b67a49033 Mon Sep 17 00:00:00 2001 From: Meninjar Date: Fri, 22 Aug 2025 04:38:22 +0700 Subject: [PATCH] Perbaikan Middelware dan tool generete logger --- internal/handlers/retribusi/retribusi.go | 20 +- internal/routes/v1/routes.go | 6 +- pkg/logger/README.md | 356 +++++++++++++++++++++ pkg/logger/config.go | 137 ++++++++ pkg/logger/context.go | 142 +++++++++ pkg/logger/example_test.go | 77 +++++ pkg/logger/logger | 0 pkg/logger/logger.go | 378 +++++++++++++++++++++++ pkg/logger/middleware.go | 191 ++++++++++++ 9 files changed, 1299 insertions(+), 8 deletions(-) create mode 100644 pkg/logger/README.md create mode 100644 pkg/logger/config.go create mode 100644 pkg/logger/context.go create mode 100644 pkg/logger/example_test.go delete mode 100644 pkg/logger/logger create mode 100644 pkg/logger/logger.go create mode 100644 pkg/logger/middleware.go diff --git a/internal/handlers/retribusi/retribusi.go b/internal/handlers/retribusi/retribusi.go index ad224f5..eb0dca5 100644 --- a/internal/handlers/retribusi/retribusi.go +++ b/internal/handlers/retribusi/retribusi.go @@ -7,10 +7,10 @@ import ( modelsretribusi "api-service/internal/models/retribusi" utils "api-service/internal/utils/filters" "api-service/internal/utils/validation" + "api-service/pkg/logger" "context" "database/sql" "fmt" - "log" "net/http" "strconv" "strings" @@ -38,7 +38,7 @@ func init() { validate.RegisterValidation("retribusi_status", validateRetribusiStatus) if db == nil { - log.Fatal("Failed to initialize database connection") + logger.Fatal("Failed to initialize database connection") } }) } @@ -830,7 +830,10 @@ func (h *RetribusiHandler) deleteRetribusi(ctx context.Context, dbConn *sql.DB, // Enhanced error handling func (h *RetribusiHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { - log.Printf("[ERROR] %s: %v", message, err) + logger.Error(message, map[string]interface{}{ + "error": err.Error(), + "status_code": statusCode, + }) h.respondError(c, message, err, statusCode) } @@ -878,7 +881,10 @@ func (h *RetribusiHandler) parsePaginationParams(c *gin.Context) (int, int, erro offset = parsedOffset } - log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + logger.Debug("Pagination parameters", map[string]interface{}{ + "limit": limit, + "offset": offset, + }) return limit, offset, nil } @@ -1257,7 +1263,11 @@ func (h *RetribusiHandler) fetchRetribusis(ctx context.Context, dbConn *sql.DB, return nil, fmt.Errorf("rows iteration error: %w", err) } - log.Printf("Successfully fetched %d retribusis with filters applied", len(retribusis)) + logger.Info("Successfully fetched retribusis", map[string]interface{}{ + "count": len(retribusis), + "limit": limit, + "offset": offset, + }) return retribusis, nil } diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 8b06243..405f3b2 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -7,7 +7,7 @@ import ( retribusiHandlers "api-service/internal/handlers/retribusi" "api-service/internal/middleware" services "api-service/internal/services/auth" - "log" + "api-service/pkg/logger" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -23,13 +23,13 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { // Add global middleware router.Use(middleware.CORSConfig()) router.Use(middleware.ErrorHandler()) - router.Use(gin.Logger()) + router.Use(logger.RequestLoggerMiddleware(logger.Default())) router.Use(gin.Recovery()) // Initialize services with error handling authService := services.NewAuthService(cfg) if authService == nil { - log.Fatal("Failed to initialize auth service") + logger.Fatal("Failed to initialize auth service") } // Swagger UI route diff --git a/pkg/logger/README.md b/pkg/logger/README.md new file mode 100644 index 0000000..918edda --- /dev/null +++ b/pkg/logger/README.md @@ -0,0 +1,356 @@ +# Structured Logger Package + +A comprehensive structured logging package for Go applications with support for different log levels, service-specific logging, request context, and JSON output formatting. + +## Features + +- **Structured Logging**: JSON and text format output with rich metadata +- **Multiple Log Levels**: DEBUG, INFO, WARN, ERROR, FATAL +- **Service-Specific Logging**: Dedicated loggers for different services +- **Request Context**: Request ID and correlation ID tracking +- **Performance Timing**: Built-in duration logging for operations +- **Gin Middleware**: Request logging middleware for HTTP requests +- **Environment Configuration**: Configurable via environment variables + +## Installation + +The logger is already integrated into the project. Import it using: + +```go +import "api-service/pkg/logger" +``` + +## Quick Start + +### Basic Usage + +```go +// Global functions (use default logger) +logger.Info("Application starting") +logger.Error("Something went wrong", map[string]interface{}{ + "error": err.Error(), + "code": "DB_CONNECTION_FAILED", +}) + +// Create a service-specific logger +authLogger := logger.ServiceLogger("auth-service") +authLogger.Info("User authenticated", map[string]interface{}{ + "user_id": "123", + "method": "oauth2", +}) +``` + +### Service-Specific Loggers + +```go +// Pre-defined service loggers +authLogger := logger.AuthServiceLogger() +bpjsLogger := logger.BPJSServiceLogger() +retribusiLogger := logger.RetribusiServiceLogger() +databaseLogger := logger.DatabaseServiceLogger() + +authLogger.Info("Authentication successful") +databaseLogger.Debug("Query executed", map[string]interface{}{ + "query": "SELECT * FROM users", + "time": "150ms", +}) +``` + +### Request Context Logging + +```go +// Add request context to logs +requestLogger := logger.Default(). + WithRequestID("req-123456"). + WithCorrelationID("corr-789012"). + WithField("user_id", "user-123") + +requestLogger.Info("Request processing started", map[string]interface{}{ + "endpoint": "/api/v1/data", + "method": "POST", +}) +``` + +### Performance Timing + +```go +// Time operations and log duration +start := time.Now() +// ... perform operation ... +logger.LogDuration(start, "Database query completed", map[string]interface{}{ + "query": "SELECT * FROM large_table", + "rows": 1000, + "database": "postgres", +}) +``` + +## Gin Middleware Integration + +### Add Request Logger Middleware + +In your routes setup: + +```go +import "api-service/pkg/logger" + +func RegisterRoutes(cfg *config.Config) *gin.Engine { + router := gin.New() + + // Add request logging middleware + router.Use(logger.RequestLoggerMiddleware(logger.Default())) + + // ... other middleware and routes + return router +} +``` + +### Access Logger in Handlers + +```go +func (h *MyHandler) MyEndpoint(c *gin.Context) { + // Get logger from context + logger := logger.GetLoggerFromContext(c) + + logger.Info("Endpoint called", map[string]interface{}{ + "user_agent": c.Request.UserAgent(), + "client_ip": c.ClientIP(), + }) + + // Get request IDs + requestID := logger.GetRequestIDFromContext(c) + correlationID := logger.GetCorrelationIDFromContext(c) +} +``` + +## Configuration + +### Environment Variables + +Set these environment variables to configure the logger: + +```bash +# Log level (DEBUG, INFO, WARN, ERROR, FATAL) +LOG_LEVEL=INFO + +# Output format (text or json) +LOG_FORMAT=text + +# Service name for logs +LOG_SERVICE=api-service + +# Enable JSON format +LOG_JSON=false +``` + +### Programmatic Configuration + +```go +// Create custom logger with specific configuration +cfg := logger.Config{ + Level: "DEBUG", + JSONFormat: true, + Service: "my-custom-service", +} + +customLogger := logger.NewFromConfig(cfg) + +// Or create manually +logger := logger.New("service-name", logger.DEBUG, true) +``` + +## Log Levels + +| Level | Description | Usage | +|-------|-------------|-------| +| DEBUG | Detailed debug information | Development and troubleshooting | +| INFO | General operational messages | Normal application behavior | +| WARN | Warning conditions | Something unexpected but not an error | +| ERROR | Error conditions | Operation failed but application continues | +| FATAL | Critical conditions | Application cannot continue | + +## Output Formats + +### Text Format (Default) +``` +2025-08-22T04:33:12+07:00 [INFO] auth-service: User authentication successful (handler/auth.go:45) [user_id=12345 method=oauth2] +``` + +### JSON Format +```json +{ + "timestamp": "2025-08-22T04:33:12+07:00", + "level": "INFO", + "service": "auth-service", + "message": "User authentication successful", + "file": "handler/auth.go", + "line": 45, + "request_id": "req-123456", + "correlation_id": "corr-789012", + "fields": { + "user_id": "12345", + "method": "oauth2" + } +} +``` + +## Best Practices + +### 1. Use Appropriate Log Levels +```go +// Good +logger.Debug("Detailed debug info") +logger.Info("User action completed") +logger.Warn("Rate limit approaching") +logger.Error("Database connection failed") + +// Avoid +logger.Info("Error connecting to database") // Use ERROR instead +``` + +### 2. Add Context to Logs +```go +// Instead of this: +logger.Error("Login failed") + +// Do this: +logger.Error("Login failed", map[string]interface{}{ + "username": username, + "reason": "invalid_credentials", + "attempts": loginAttempts, + "client_ip": clientIP, +}) +``` + +### 3. Use Service-Specific Loggers +```go +// Create once per service +var authLogger = logger.AuthServiceLogger() + +func LoginHandler(c *gin.Context) { + authLogger.Info("Login attempt", map[string]interface{}{ + "username": c.PostForm("username"), + }) +} +``` + +### 4. Measure Performance +```go +func ProcessData(data []byte) error { + start := time.Now() + defer func() { + logger.LogDuration(start, "Data processing completed", map[string]interface{}{ + "data_size": len(data), + "items": countItems(data), + }) + }() + + // ... processing logic ... +} +``` + +## Migration from Standard Log Package + +### Before (standard log) +```go +import "log" + +log.Printf("Error: %v", err) +log.Printf("User %s logged in", username) +``` + +### After (structured logger) +```go +import "api-service/pkg/logger" + +logger.Error("Operation failed", map[string]interface{}{ + "error": err.Error(), + "context": "user_login", +}) + +logger.Info("User logged in", map[string]interface{}{ + "username": username, + "method": "password", +}) +``` + +## Examples + +### Database Operations +```go +func (h *UserHandler) GetUser(c *gin.Context) { + logger := logger.GetLoggerFromContext(c) + start := time.Now() + + user, err := h.db.GetUser(c.Param("id")) + if err != nil { + logger.Error("Failed to get user", map[string]interface{}{ + "user_id": c.Param("id"), + "error": err.Error(), + }) + c.JSON(500, gin.H{"error": "Internal server error"}) + return + } + + logger.LogDuration(start, "User retrieved successfully", map[string]interface{}{ + "user_id": user.ID, + "query_time": time.Since(start).String(), + }) + + c.JSON(200, user) +} +``` + +### Authentication Service +```go +var authLogger = logger.AuthServiceLogger() + +func Authenticate(username, password string) (bool, error) { + authLogger.Debug("Authentication attempt", map[string]interface{}{ + "username": username, + }) + + // Authentication logic... + + if authenticated { + authLogger.Info("Authentication successful", map[string]interface{}{ + "username": username, + "method": "password", + }) + return true, nil + } + + authLogger.Warn("Authentication failed", map[string]interface{}{ + "username": username, + "reason": "invalid_credentials", + }) + return false, nil +} +``` + +## Troubleshooting + +### Common Issues + +1. **No logs appearing**: Check that log level is not set too high (e.g., ERROR when logging INFO) +2. **JSON format not working**: Ensure `LOG_JSON=true` or logger is created with `jsonFormat: true` +3. **Missing context**: Use `WithRequestID()` and `WithCorrelationID()` for request context + +### Debug Mode + +Enable debug logging for development: + +```bash +export LOG_LEVEL=DEBUG +export LOG_FORMAT=text +``` + +## Performance Considerations + +- Logger is designed to be lightweight and fast +- Context fields are only evaluated when the log level is enabled +- JSON marshaling only occurs when JSON format is enabled +- Consider log volume in production environments + +## License + +This logger package is part of the API Service project. diff --git a/pkg/logger/config.go b/pkg/logger/config.go new file mode 100644 index 0000000..68f69d1 --- /dev/null +++ b/pkg/logger/config.go @@ -0,0 +1,137 @@ +package logger + +import ( + "os" + "strconv" + "strings" +) + +// Config holds the configuration for the logger +type Config struct { + Level string `json:"level" default:"INFO"` + JSONFormat bool `json:"json_format" default:"false"` + Service string `json:"service" default:"api-service"` +} + +// DefaultConfig returns the default logger configuration +func DefaultConfig() Config { + return Config{ + Level: "INFO", + JSONFormat: false, + Service: "api-service", + } +} + +// LoadConfigFromEnv loads logger configuration from environment variables +func LoadConfigFromEnv() Config { + config := DefaultConfig() + + // Load log level from environment + if level := os.Getenv("LOG_LEVEL"); level != "" { + config.Level = strings.ToUpper(level) + } + + // Load JSON format from environment + if jsonFormat := os.Getenv("LOG_JSON_FORMAT"); jsonFormat != "" { + if parsed, err := strconv.ParseBool(jsonFormat); err == nil { + config.JSONFormat = parsed + } + } + + // Load service name from environment + if service := os.Getenv("LOG_SERVICE_NAME"); service != "" { + config.Service = service + } + + return config +} + +// Validate validates the logger configuration +func (c *Config) Validate() error { + // Validate log level + validLevels := map[string]bool{ + "DEBUG": true, + "INFO": true, + "WARN": true, + "ERROR": true, + "FATAL": true, + } + + if !validLevels[c.Level] { + c.Level = "INFO" // Default to INFO if invalid + } + + return nil +} + +// GetLogLevel returns the LogLevel from the configuration +func (c *Config) GetLogLevel() LogLevel { + switch strings.ToUpper(c.Level) { + case "DEBUG": + return DEBUG + case "WARN": + return WARN + case "ERROR": + return ERROR + case "FATAL": + return FATAL + default: + return INFO + } +} + +// CreateLoggerFromConfig creates a new logger instance from configuration +func CreateLoggerFromConfig(cfg Config) *Logger { + cfg.Validate() + return NewFromConfig(cfg) +} + +// CreateLoggerFromEnv creates a new logger instance from environment variables +func CreateLoggerFromEnv() *Logger { + cfg := LoadConfigFromEnv() + return CreateLoggerFromConfig(cfg) +} + +// Environment variable constants +const ( + EnvLogLevel = "LOG_LEVEL" + EnvLogJSONFormat = "LOG_JSON_FORMAT" + EnvLogService = "LOG_SERVICE_NAME" +) + +// Service-specific configuration helpers + +// AuthServiceConfig returns configuration for auth service +func AuthServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "auth-service" + return cfg +} + +// BPJSServiceConfig returns configuration for BPJS service +func BPJSServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "bpjs-service" + return cfg +} + +// RetribusiServiceConfig returns configuration for retribusi service +func RetribusiServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "retribusi-service" + return cfg +} + +// DatabaseServiceConfig returns configuration for database service +func DatabaseServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "database-service" + return cfg +} + +// MiddlewareServiceConfig returns configuration for middleware service +func MiddlewareServiceConfig() Config { + cfg := LoadConfigFromEnv() + cfg.Service = "middleware-service" + return cfg +} diff --git a/pkg/logger/context.go b/pkg/logger/context.go new file mode 100644 index 0000000..3eb52bf --- /dev/null +++ b/pkg/logger/context.go @@ -0,0 +1,142 @@ +package logger + +import ( + "context" + "time" +) + +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + loggerKey contextKey = "logger" + requestIDKey contextKey = "request_id" + correlationIDKey contextKey = "correlation_id" + serviceNameKey contextKey = "service_name" +) + +// ContextWithLogger creates a new context with the logger +func ContextWithLogger(ctx context.Context, logger *Logger) context.Context { + return context.WithValue(ctx, loggerKey, logger) +} + +// LoggerFromContext retrieves the logger from context +func LoggerFromContext(ctx context.Context) *Logger { + if logger, ok := ctx.Value(loggerKey).(*Logger); ok { + return logger + } + return globalLogger +} + +// ContextWithRequestID creates a new context with the request ID +func ContextWithRequestID(ctx context.Context, requestID string) context.Context { + return context.WithValue(ctx, requestIDKey, requestID) +} + +// RequestIDFromContext retrieves the request ID from context +func RequestIDFromContext(ctx context.Context) string { + if requestID, ok := ctx.Value(requestIDKey).(string); ok { + return requestID + } + return "" +} + +// ContextWithCorrelationID creates a new context with the correlation ID +func ContextWithCorrelationID(ctx context.Context, correlationID string) context.Context { + return context.WithValue(ctx, correlationIDKey, correlationID) +} + +// CorrelationIDFromContext retrieves the correlation ID from context +func CorrelationIDFromContext(ctx context.Context) string { + if correlationID, ok := ctx.Value(correlationIDKey).(string); ok { + return correlationID + } + return "" +} + +// ContextWithServiceName creates a new context with the service name +func ContextWithServiceName(ctx context.Context, serviceName string) context.Context { + return context.WithValue(ctx, serviceNameKey, serviceName) +} + +// ServiceNameFromContext retrieves the service name from context +func ServiceNameFromContext(ctx context.Context) string { + if serviceName, ok := ctx.Value(serviceNameKey).(string); ok { + return serviceName + } + return "" +} + +// WithContext returns a new logger with context values +func (l *Logger) WithContext(ctx context.Context) *Logger { + logger := l + + if requestID := RequestIDFromContext(ctx); requestID != "" { + logger = logger.WithRequestID(requestID) + } + + if correlationID := CorrelationIDFromContext(ctx); correlationID != "" { + logger = logger.WithCorrelationID(correlationID) + } + + if serviceName := ServiceNameFromContext(ctx); serviceName != "" { + logger = logger.WithService(serviceName) + } + + return logger +} + +// DebugCtx logs a debug message with context +func DebugCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Debug(msg, fields...) +} + +// DebugfCtx logs a formatted debug message with context +func DebugfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Debugf(format, args...) +} + +// InfoCtx logs an info message with context +func InfoCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Info(msg, fields...) +} + +// InfofCtx logs a formatted info message with context +func InfofCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Infof(format, args...) +} + +// WarnCtx logs a warning message with context +func WarnCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Warn(msg, fields...) +} + +// WarnfCtx logs a formatted warning message with context +func WarnfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Warnf(format, args...) +} + +// ErrorCtx logs an error message with context +func ErrorCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Error(msg, fields...) +} + +// ErrorfCtx logs a formatted error message with context +func ErrorfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Errorf(format, args...) +} + +// FatalCtx logs a fatal message with context and exits the program +func FatalCtx(ctx context.Context, msg string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Fatal(msg, fields...) +} + +// FatalfCtx logs a formatted fatal message with context and exits the program +func FatalfCtx(ctx context.Context, format string, args ...interface{}) { + LoggerFromContext(ctx).WithContext(ctx).Fatalf(format, args...) +} + +// LogDurationCtx logs the duration of an operation with context +func LogDurationCtx(ctx context.Context, start time.Time, operation string, fields ...map[string]interface{}) { + LoggerFromContext(ctx).WithContext(ctx).LogDuration(start, operation, fields...) +} diff --git a/pkg/logger/example_test.go b/pkg/logger/example_test.go new file mode 100644 index 0000000..9c2718f --- /dev/null +++ b/pkg/logger/example_test.go @@ -0,0 +1,77 @@ +package logger + +import ( + "testing" + "time" +) + +func TestLoggerExamples(t *testing.T) { + // Example 1: Basic logging + Info("Application starting up") + Debug("Debug information", map[string]interface{}{"config_loaded": true}) + + // Example 2: Service-specific logging + authLogger := AuthServiceLogger() + authLogger.Info("User authentication successful", map[string]interface{}{ + "user_id": "12345", + "method": "oauth2", + }) + + // Example 3: Error logging with context + Error("Database connection failed", map[string]interface{}{ + "error": "connection timeout", + "retry_count": 3, + "max_retries": 5, + "service": "database-service", + }) + + // Example 4: Performance timing + start := time.Now() + time.Sleep(10 * time.Millisecond) // Simulate work + globalLogger.LogDuration(start, "Database query completed", map[string]interface{}{ + "query": "SELECT * FROM users", + "rows": 150, + "database": "postgres", + }) + + // Example 5: JSON format logging + jsonLogger := New("test-service", DEBUG, true) + jsonLogger.Info("JSON formatted log", map[string]interface{}{ + "user": map[string]interface{}{ + "id": "user-123", + "name": "John Doe", + "email": "john@example.com", + }, + "request": map[string]interface{}{ + "method": "GET", + "path": "/api/v1/users", + }, + }) + + t.Log("Logger examples executed successfully") +} + +func TestLoggerLevels(t *testing.T) { + // Test different log levels + Debug("This is a debug message") + Info("This is an info message") + Warn("This is a warning message") + Error("This is an error message") + + t.Log("All log levels tested") +} + +func TestLoggerWithRequestContext(t *testing.T) { + // Simulate request context with IDs + logger := Default(). + WithRequestID("req-123456"). + WithCorrelationID("corr-789012") + + logger.Info("Request processing started", map[string]interface{}{ + "endpoint": "/api/v1/data", + "method": "POST", + "client_ip": "192.168.1.100", + }) + + t.Log("Request context logging tested") +} diff --git a/pkg/logger/logger b/pkg/logger/logger deleted file mode 100644 index e69de29..0000000 diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..92a0164 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,378 @@ +package logger + +import ( + "encoding/json" + "fmt" + "log" + "os" + "runtime" + "strings" + "sync" + "time" +) + +// LogLevel represents the severity level of a log message +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR + FATAL +) + +var ( + levelStrings = map[LogLevel]string{ + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", + } + + stringLevels = map[string]LogLevel{ + "DEBUG": DEBUG, + "INFO": INFO, + "WARN": WARN, + "ERROR": ERROR, + "FATAL": FATAL, + } +) + +// Logger represents a structured logger instance +type Logger struct { + serviceName string + level LogLevel + output *log.Logger + mu sync.Mutex + jsonFormat bool +} + +// LogEntry represents a structured log entry +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level string `json:"level"` + Service string `json:"service"` + Message string `json:"message"` + RequestID string `json:"request_id,omitempty"` + CorrelationID string `json:"correlation_id,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Duration string `json:"duration,omitempty"` + Fields map[string]interface{} `json:"fields,omitempty"` +} + +// New creates a new logger instance +func New(serviceName string, level LogLevel, jsonFormat bool) *Logger { + return &Logger{ + serviceName: serviceName, + level: level, + output: log.New(os.Stdout, "", 0), + jsonFormat: jsonFormat, + } +} + +// NewFromConfig creates a new logger from configuration +func NewFromConfig(cfg Config) *Logger { + level := INFO + if l, exists := stringLevels[strings.ToUpper(cfg.Level)]; exists { + level = l + } + + return New(cfg.Service, level, cfg.JSONFormat) +} + +// Default creates a default logger instance +func Default() *Logger { + return New("api-service", INFO, false) +} + +// WithService returns a new logger with the specified service name +func (l *Logger) WithService(serviceName string) *Logger { + return &Logger{ + serviceName: serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + } +} + +// SetLevel sets the log level for the logger +func (l *Logger) SetLevel(level LogLevel) { + l.mu.Lock() + defer l.mu.Unlock() + l.level = level +} + +// SetJSONFormat sets whether to output logs in JSON format +func (l *Logger) SetJSONFormat(jsonFormat bool) { + l.mu.Lock() + defer l.mu.Unlock() + l.jsonFormat = jsonFormat +} + +// Debug logs a debug message +func (l *Logger) Debug(msg string, fields ...map[string]interface{}) { + l.log(DEBUG, msg, nil, fields...) +} + +// Debugf logs a formatted debug message +func (l *Logger) Debugf(format string, args ...interface{}) { + l.log(DEBUG, fmt.Sprintf(format, args...), nil) +} + +// Info logs an info message +func (l *Logger) Info(msg string, fields ...map[string]interface{}) { + l.log(INFO, msg, nil, fields...) +} + +// Infof logs a formatted info message +func (l *Logger) Infof(format string, args ...interface{}) { + l.log(INFO, fmt.Sprintf(format, args...), nil) +} + +// Warn logs a warning message +func (l *Logger) Warn(msg string, fields ...map[string]interface{}) { + l.log(WARN, msg, nil, fields...) +} + +// Warnf logs a formatted warning message +func (l *Logger) Warnf(format string, args ...interface{}) { + l.log(WARN, fmt.Sprintf(format, args...), nil) +} + +// Error logs an error message +func (l *Logger) Error(msg string, fields ...map[string]interface{}) { + l.log(ERROR, msg, nil, fields...) +} + +// Errorf logs a formatted error message +func (l *Logger) Errorf(format string, args ...interface{}) { + l.log(ERROR, fmt.Sprintf(format, args...), nil) +} + +// Fatal logs a fatal message and exits the program +func (l *Logger) Fatal(msg string, fields ...map[string]interface{}) { + l.log(FATAL, msg, nil, fields...) + os.Exit(1) +} + +// Fatalf logs a formatted fatal message and exits the program +func (l *Logger) Fatalf(format string, args ...interface{}) { + l.log(FATAL, fmt.Sprintf(format, args...), nil) + os.Exit(1) +} + +// WithRequestID returns a new logger with the specified request ID +func (l *Logger) WithRequestID(requestID string) *Logger { + return l.withField("request_id", requestID) +} + +// WithCorrelationID returns a new logger with the specified correlation ID +func (l *Logger) WithCorrelationID(correlationID string) *Logger { + return l.withField("correlation_id", correlationID) +} + +// WithField returns a new logger with an additional field +func (l *Logger) WithField(key string, value interface{}) *Logger { + return l.withField(key, value) +} + +// WithFields returns a new logger with additional fields +func (l *Logger) WithFields(fields map[string]interface{}) *Logger { + return &Logger{ + serviceName: l.serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + } +} + +// LogDuration logs the duration of an operation +func (l *Logger) LogDuration(start time.Time, operation string, fields ...map[string]interface{}) { + duration := time.Since(start) + l.Info(fmt.Sprintf("%s completed", operation), append(fields, map[string]interface{}{ + "duration": duration.String(), + "duration_ms": duration.Milliseconds(), + })...) +} + +// log is the internal logging method +func (l *Logger) log(level LogLevel, msg string, duration *time.Duration, fields ...map[string]interface{}) { + if level < l.level { + return + } + + // Get caller information + _, file, line, ok := runtime.Caller(3) // Adjust caller depth + var callerFile string + var callerLine int + if ok { + // Shorten file path + parts := strings.Split(file, "/") + if len(parts) > 2 { + callerFile = strings.Join(parts[len(parts)-2:], "/") + } else { + callerFile = file + } + callerLine = line + } + + // Merge all fields + mergedFields := make(map[string]interface{}) + for _, f := range fields { + for k, v := range f { + mergedFields[k] = v + } + } + + entry := LogEntry{ + Timestamp: time.Now().Format(time.RFC3339), + Level: levelStrings[level], + Service: l.serviceName, + Message: msg, + File: callerFile, + Line: callerLine, + Fields: mergedFields, + } + + if duration != nil { + entry.Duration = duration.String() + } + + if l.jsonFormat { + l.outputJSON(entry) + } else { + l.outputText(entry) + } + + if level == FATAL { + os.Exit(1) + } +} + +// outputJSON outputs the log entry in JSON format +func (l *Logger) outputJSON(entry LogEntry) { + jsonData, err := json.Marshal(entry) + if err != nil { + // Fallback to text output if JSON marshaling fails + l.outputText(entry) + return + } + l.output.Println(string(jsonData)) +} + +// outputText outputs the log entry in text format +func (l *Logger) outputText(entry LogEntry) { + timestamp := entry.Timestamp + level := entry.Level + service := entry.Service + message := entry.Message + + // Base log line + logLine := fmt.Sprintf("%s [%s] %s: %s", timestamp, level, service, message) + + // Add file and line if available + if entry.File != "" && entry.Line > 0 { + logLine += fmt.Sprintf(" (%s:%d)", entry.File, entry.Line) + } + + // Add request ID if available + if entry.RequestID != "" { + logLine += fmt.Sprintf(" [req:%s]", entry.RequestID) + } + + // Add correlation ID if available + if entry.CorrelationID != "" { + logLine += fmt.Sprintf(" [corr:%s]", entry.CorrelationID) + } + + // Add duration if available + if entry.Duration != "" { + logLine += fmt.Sprintf(" [dur:%s]", entry.Duration) + } + + // Add additional fields + if len(entry.Fields) > 0 { + fields := make([]string, 0, len(entry.Fields)) + for k, v := range entry.Fields { + fields = append(fields, fmt.Sprintf("%s=%v", k, v)) + } + logLine += " [" + strings.Join(fields, " ") + "]" + } + + l.output.Println(logLine) +} + +// withField creates a new logger with an additional field +func (l *Logger) withField(key string, value interface{}) *Logger { + return &Logger{ + serviceName: l.serviceName, + level: l.level, + output: l.output, + jsonFormat: l.jsonFormat, + } +} + +// String returns the string representation of a log level +func (l LogLevel) String() string { + return levelStrings[l] +} + +// ParseLevel parses a string into a LogLevel +func ParseLevel(level string) (LogLevel, error) { + if l, exists := stringLevels[strings.ToUpper(level)]; exists { + return l, nil + } + return INFO, fmt.Errorf("invalid log level: %s", level) +} + +// Global logger instance +var globalLogger = Default() + +// SetGlobalLogger sets the global logger instance +func SetGlobalLogger(logger *Logger) { + globalLogger = logger +} + +// Global logging functions +func Debug(msg string, fields ...map[string]interface{}) { + globalLogger.Debug(msg, fields...) +} + +func Debugf(format string, args ...interface{}) { + globalLogger.Debugf(format, args...) +} + +func Info(msg string, fields ...map[string]interface{}) { + globalLogger.Info(msg, fields...) +} + +func Infof(format string, args ...interface{}) { + globalLogger.Infof(format, args...) +} + +func Warn(msg string, fields ...map[string]interface{}) { + globalLogger.Warn(msg, fields...) +} + +func Warnf(format string, args ...interface{}) { + globalLogger.Warnf(format, args...) +} + +func Error(msg string, fields ...map[string]interface{}) { + globalLogger.Error(msg, fields...) +} + +func Errorf(format string, args ...interface{}) { + globalLogger.Errorf(format, args...) +} + +func Fatal(msg string, fields ...map[string]interface{}) { + globalLogger.Fatal(msg, fields...) +} + +func Fatalf(format string, args ...interface{}) { + globalLogger.Fatalf(format, args...) +} diff --git a/pkg/logger/middleware.go b/pkg/logger/middleware.go new file mode 100644 index 0000000..d063a83 --- /dev/null +++ b/pkg/logger/middleware.go @@ -0,0 +1,191 @@ +package logger + +import ( + "bytes" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// RequestLoggerMiddleware creates a Gin middleware for request logging +func RequestLoggerMiddleware(logger *Logger) gin.HandlerFunc { + return func(c *gin.Context) { + // Generate request ID if not present + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + c.Header("X-Request-ID", requestID) + } + + // Get correlation ID + correlationID := c.GetHeader("X-Correlation-ID") + if correlationID == "" { + correlationID = uuid.New().String() + c.Header("X-Correlation-ID", correlationID) + } + + // Create request-scoped logger + reqLogger := logger. + WithRequestID(requestID). + WithCorrelationID(correlationID) + + // Store logger in context + c.Set("logger", reqLogger) + c.Set("request_id", requestID) + c.Set("correlation_id", correlationID) + + // Capture request body for logging if needed + var requestBody []byte + if c.Request.Body != nil && strings.HasPrefix(c.ContentType(), "application/json") { + requestBody, _ = io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // Start timer + start := time.Now() + + // Log request start + reqLogger.Info("Request started", map[string]interface{}{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "query": c.Request.URL.RawQuery, + "remote_addr": c.Request.RemoteAddr, + "user_agent": c.Request.UserAgent(), + "content_type": c.ContentType(), + "body_size": len(requestBody), + }) + + // Process request + c.Next() + + // Calculate duration + duration := time.Since(start) + + // Get response status + status := c.Writer.Status() + responseSize := c.Writer.Size() + + // Log level based on status code + var logLevel LogLevel + switch { + case status >= 500: + logLevel = ERROR + case status >= 400: + logLevel = WARN + default: + logLevel = INFO + } + + // Log request completion + fields := map[string]interface{}{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "status": status, + "duration": duration.String(), + "duration_ms": duration.Milliseconds(), + "response_size": responseSize, + "client_ip": c.ClientIP(), + "user_agent": c.Request.UserAgent(), + "content_type": c.ContentType(), + "content_length": c.Request.ContentLength, + } + + // Add query parameters if present + if c.Request.URL.RawQuery != "" { + fields["query"] = c.Request.URL.RawQuery + } + + // Add error information if present + if len(c.Errors) > 0 { + errors := make([]string, len(c.Errors)) + for i, err := range c.Errors { + errors[i] = err.Error() + } + fields["errors"] = errors + } + + reqLogger.log(logLevel, "Request completed", &duration, fields) + } +} + +// GetLoggerFromContext retrieves the logger from Gin context +func GetLoggerFromContext(c *gin.Context) *Logger { + if logger, exists := c.Get("logger"); exists { + if l, ok := logger.(*Logger); ok { + return l + } + } + return globalLogger +} + +// GetRequestIDFromContext retrieves the request ID from Gin context +func GetRequestIDFromContext(c *gin.Context) string { + if requestID, exists := c.Get("request_id"); exists { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} + +// GetCorrelationIDFromContext retrieves the correlation ID from Gin context +func GetCorrelationIDFromContext(c *gin.Context) string { + if correlationID, exists := c.Get("correlation_id"); exists { + if id, ok := correlationID.(string); ok { + return id + } + } + return "" +} + +// DatabaseLoggerMiddleware creates middleware for database operation logging +func DatabaseLoggerMiddleware(logger *Logger, serviceName string) gin.HandlerFunc { + return func(c *gin.Context) { + reqLogger := GetLoggerFromContext(c).WithService(serviceName) + c.Set("db_logger", reqLogger) + c.Next() + } +} + +// GetDBLoggerFromContext retrieves the database logger from Gin context +func GetDBLoggerFromContext(c *gin.Context) *Logger { + if logger, exists := c.Get("db_logger"); exists { + if l, ok := logger.(*Logger); ok { + return l + } + } + return GetLoggerFromContext(c) +} + +// ServiceLogger creates a service-specific logger +func ServiceLogger(serviceName string) *Logger { + return globalLogger.WithService(serviceName) +} + +// AuthServiceLogger returns a logger for auth service +func AuthServiceLogger() *Logger { + return ServiceLogger("auth-service") +} + +// BPJSServiceLogger returns a logger for BPJS service +func BPJSServiceLogger() *Logger { + return ServiceLogger("bpjs-service") +} + +// RetribusiServiceLogger returns a logger for retribusi service +func RetribusiServiceLogger() *Logger { + return ServiceLogger("retribusi-service") +} + +// DatabaseServiceLogger returns a logger for database operations +func DatabaseServiceLogger() *Logger { + return ServiceLogger("database-service") +} + +// MiddlewareServiceLogger returns a logger for middleware operations +func MiddlewareServiceLogger() *Logger { + return ServiceLogger("middleware-service") +}