Perbaikan Middelware dan tool generete logger
This commit is contained in:
@@ -7,10 +7,10 @@ import (
|
|||||||
modelsretribusi "api-service/internal/models/retribusi"
|
modelsretribusi "api-service/internal/models/retribusi"
|
||||||
utils "api-service/internal/utils/filters"
|
utils "api-service/internal/utils/filters"
|
||||||
"api-service/internal/utils/validation"
|
"api-service/internal/utils/validation"
|
||||||
|
"api-service/pkg/logger"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,7 +38,7 @@ func init() {
|
|||||||
validate.RegisterValidation("retribusi_status", validateRetribusiStatus)
|
validate.RegisterValidation("retribusi_status", validateRetribusiStatus)
|
||||||
|
|
||||||
if db == nil {
|
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
|
// Enhanced error handling
|
||||||
func (h *RetribusiHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
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)
|
h.respondError(c, message, err, statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +881,10 @@ func (h *RetribusiHandler) parsePaginationParams(c *gin.Context) (int, int, erro
|
|||||||
offset = parsedOffset
|
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
|
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)
|
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
|
return retribusis, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||||
"api-service/internal/middleware"
|
"api-service/internal/middleware"
|
||||||
services "api-service/internal/services/auth"
|
services "api-service/internal/services/auth"
|
||||||
"log"
|
"api-service/pkg/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
@@ -23,13 +23,13 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
// Add global middleware
|
// Add global middleware
|
||||||
router.Use(middleware.CORSConfig())
|
router.Use(middleware.CORSConfig())
|
||||||
router.Use(middleware.ErrorHandler())
|
router.Use(middleware.ErrorHandler())
|
||||||
router.Use(gin.Logger())
|
router.Use(logger.RequestLoggerMiddleware(logger.Default()))
|
||||||
router.Use(gin.Recovery())
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
// Initialize services with error handling
|
// Initialize services with error handling
|
||||||
authService := services.NewAuthService(cfg)
|
authService := services.NewAuthService(cfg)
|
||||||
if authService == nil {
|
if authService == nil {
|
||||||
log.Fatal("Failed to initialize auth service")
|
logger.Fatal("Failed to initialize auth service")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swagger UI route
|
// Swagger UI route
|
||||||
|
|||||||
356
pkg/logger/README.md
Normal file
356
pkg/logger/README.md
Normal 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
137
pkg/logger/config.go
Normal 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
142
pkg/logger/context.go
Normal 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...)
|
||||||
|
}
|
||||||
77
pkg/logger/example_test.go
Normal file
77
pkg/logger/example_test.go
Normal 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")
|
||||||
|
}
|
||||||
378
pkg/logger/logger.go
Normal file
378
pkg/logger/logger.go
Normal 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
191
pkg/logger/middleware.go
Normal 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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user