Files
antrean-anjungan/pkg/logger/logger.go
2025-08-25 05:22:46 +07:00

535 lines
13 KiB
Go

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...)
}
// SaveLogText menyimpan log dalam format teks dengan pemisah |
func (l *Logger) SaveLogText(entry LogEntry) error {
// Format log dengan pemisah |
logLine := fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s|%s:%d",
entry.Timestamp,
entry.Level,
entry.Service,
entry.Message,
entry.RequestID,
entry.CorrelationID,
entry.Duration,
entry.File,
entry.Line)
// Tambahkan fields jika ada
if len(entry.Fields) > 0 {
fieldsStr := ""
for k, v := range entry.Fields {
fieldsStr += fmt.Sprintf("|%s=%v", k, v)
}
logLine += fieldsStr
}
logLine += "\n"
// Buat direktori jika belum ada
dirPath := "pkg/logger/data"
if err := os.MkdirAll(dirPath, 0755); err != nil {
return err
}
// Tulis ke file
filePath := dirPath + "/logs.txt"
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString(logLine); err != nil {
return err
}
return nil
}
// SaveLogJSON menyimpan log dalam format JSON
func (l *Logger) SaveLogJSON(entry LogEntry) error {
jsonData, err := json.Marshal(entry)
if err != nil {
return err
}
// Buat direktori jika belum ada
dirPath := "pkg/logger/data"
if err := os.MkdirAll(dirPath, 0755); err != nil {
return err
}
// Tulis ke file
filePath := dirPath + "/logs.json"
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString(string(jsonData) + "\n"); err != nil {
return err
}
return nil
}
// SaveLogToDatabase menyimpan log ke database
func (l *Logger) SaveLogToDatabase(entry LogEntry) error {
// Implementasi penyimpanan ke database
// Ini adalah contoh implementasi, sesuaikan dengan struktur database Anda
// Untuk saat ini, kita akan simpan ke file sebagai placeholder
// Anda dapat mengganti ini dengan koneksi database yang sesuai
dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n",
entry.Timestamp, entry.Level, entry.Service, entry.Message)
dirPath := "pkg/logger/data"
if err := os.MkdirAll(dirPath, 0755); err != nil {
return err
}
filePath := dirPath + "/database_logs.txt"
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString(dbLogLine); err != nil {
return err
}
return nil
}
// LogAndSave melakukan logging dan menyimpan ke semua format
func (l *Logger) LogAndSave(level LogLevel, msg string, fields ...map[string]interface{}) {
// Panggil fungsi log biasa
l.log(level, msg, nil, fields...)
// Dapatkan entry log yang baru dibuat
_, file, line, ok := runtime.Caller(2)
var callerFile string
var callerLine int
if ok {
parts := strings.Split(file, "/")
if len(parts) > 2 {
callerFile = strings.Join(parts[len(parts)-2:], "/")
} else {
callerFile = file
}
callerLine = line
}
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,
}
// Simpan ke semua format
go func() {
l.SaveLogText(entry)
l.SaveLogJSON(entry)
l.SaveLogToDatabase(entry)
}()
}
// Global fungsi untuk menyimpan log
func SaveLogText(entry LogEntry) error {
return globalLogger.SaveLogText(entry)
}
func SaveLogJSON(entry LogEntry) error {
return globalLogger.SaveLogJSON(entry)
}
func SaveLogToDatabase(entry LogEntry) error {
return globalLogger.SaveLogToDatabase(entry)
}