Perbaikan Middelware dan tool generete logger

This commit is contained in:
2025-08-22 04:38:22 +07:00
parent 80dafd2a69
commit ce7d12f20c
9 changed files with 1299 additions and 8 deletions

View File

@@ -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
}

View File

@@ -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

356
pkg/logger/README.md Normal file
View File

@@ -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.

137
pkg/logger/config.go Normal file
View File

@@ -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
}

142
pkg/logger/context.go Normal file
View File

@@ -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...)
}

View File

@@ -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")
}

View File

378
pkg/logger/logger.go Normal file
View File

@@ -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...)
}

191
pkg/logger/middleware.go Normal file
View File

@@ -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")
}