package logger import ( "encoding/json" "fmt" "log" "os" "path/filepath" "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 logDir string } // 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, logDir ...string) *Logger { // Tentukan direktori log berdasarkan prioritas: // 1. Parameter logDir (jika disediakan) // 2. Environment variable LOG_DIR (jika ada) // 3. Default ke pkg/logger/data relatif terhadap root proyek var finalLogDir string // Cek apakah logDir disediakan sebagai parameter if len(logDir) > 0 && logDir[0] != "" { finalLogDir = logDir[0] } else { // Cek environment variable if envLogDir := os.Getenv("LOG_DIR"); envLogDir != "" { finalLogDir = envLogDir } else { // Default: dapatkan path relatif terhadap root proyek // Dapatkan path executable exePath, err := os.Executable() if err != nil { // Fallback ke current working directory jika gagal finalLogDir = filepath.Join(".", "pkg", "logger", "data") } else { // Dapatkan direktori executable exeDir := filepath.Dir(exePath) // Jika berjalan dengan go run, executable ada di temp directory // Coba dapatkan path source code if strings.Contains(exeDir, "go-build") || strings.Contains(exeDir, "tmp") { // Gunakan runtime.Caller untuk mendapatkan path source _, file, _, ok := runtime.Caller(0) if ok { // Dapatkan direktori source (2 level up dari pkg/logger) sourceDir := filepath.Dir(file) for i := 0; i < 3; i++ { // Naik 3 level ke root proyek sourceDir = filepath.Dir(sourceDir) } finalLogDir = filepath.Join(sourceDir, "pkg", "logger", "data") } else { // Fallback finalLogDir = filepath.Join(".", "pkg", "logger", "data") } } else { // Untuk binary yang sudah dikompilasi, asumsikan struktur proyek finalLogDir = filepath.Join(exeDir, "pkg", "logger", "data") } } } } // Konversi ke path absolut absPath, err := filepath.Abs(finalLogDir) if err == nil { finalLogDir = absPath } // Buat direktori jika belum ada if err := os.MkdirAll(finalLogDir, 0755); err != nil { // Fallback ke stdout jika gagal membuat direktori fmt.Printf("Warning: Failed to create log directory %s: %v\n", finalLogDir, err) return &Logger{ serviceName: serviceName, level: level, output: log.New(os.Stdout, "", 0), jsonFormat: jsonFormat, logDir: "", // Kosongkan karena gagal } } return &Logger{ serviceName: serviceName, level: level, output: log.New(os.Stdout, "", 0), jsonFormat: jsonFormat, logDir: finalLogDir, } } // 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, logDir: l.logDir, } } // 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, logDir: l.logDir, } } // 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, logDir: l.logDir, } } // 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 if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } // Tulis ke file dengan mutex lock untuk concurrency safety l.mu.Lock() defer l.mu.Unlock() filePath := filepath.Join(l.logDir, "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 if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } // Tulis ke file dengan mutex lock for concurrency safety l.mu.Lock() defer l.mu.Unlock() filePath := filepath.Join(l.logDir, "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) if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } // Tulis ke file dengan mutex lock for concurrency safety l.mu.Lock() defer l.mu.Unlock() filePath := filepath.Join(l.logDir, "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) }