617 lines
15 KiB
Go
617 lines
15 KiB
Go
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)
|
|
}
|