mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-09 23:59:45 +00:00
206 lines
4.9 KiB
Go
206 lines
4.9 KiB
Go
// Package logger provides structured logging with support for text and JSON
|
|
// output formats. It intercepts Kubernetes client-go logs and routes them
|
|
// through the structured logger.
|
|
//
|
|
// The package provides both instance-based and global logging:
|
|
//
|
|
// // Instance-based logging
|
|
// log := logger.New(logger.LevelInfo, logger.FormatJSON, os.Stderr)
|
|
// log.Info("message", "key", "value")
|
|
//
|
|
// // Global logging (after Init)
|
|
// logger.Init(logger.LevelInfo, logger.FormatText, os.Stderr)
|
|
// logger.Info("message", "key", "value")
|
|
//
|
|
// Log levels: DEBUG < INFO < WARN < ERROR
|
|
// Output formats: FormatText (human-readable), FormatJSON (structured)
|
|
package logger
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Level represents the logging level.
|
|
// Higher levels include all lower levels (e.g., LevelInfo includes WARN and ERROR).
|
|
type Level int
|
|
|
|
const (
|
|
// LevelDebug is for detailed troubleshooting information.
|
|
LevelDebug Level = iota
|
|
// LevelInfo is for general operational information.
|
|
LevelInfo
|
|
// LevelWarn is for unexpected but handled situations.
|
|
LevelWarn
|
|
// LevelError is for failures that require attention.
|
|
LevelError
|
|
)
|
|
|
|
// Format represents the output format for log entries.
|
|
type Format int
|
|
|
|
const (
|
|
// FormatText outputs human-readable log lines.
|
|
FormatText Format = iota
|
|
// FormatJSON outputs structured JSON log entries.
|
|
FormatJSON
|
|
)
|
|
|
|
// Logger is a structured logger with configurable level and format.
|
|
// It is safe for concurrent use.
|
|
type Logger struct {
|
|
output io.Writer
|
|
level Level
|
|
format Format
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// logEntry represents a single log entry for JSON output.
|
|
type logEntry struct {
|
|
Fields map[string]any `json:"fields,omitempty"`
|
|
Time string `json:"time"`
|
|
Level string `json:"level"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// New creates a new Logger with the specified level, format, and output writer.
|
|
// If output is nil, os.Stderr is used.
|
|
func New(level Level, format Format, output io.Writer) *Logger {
|
|
if output == nil {
|
|
output = os.Stderr
|
|
}
|
|
return &Logger{
|
|
level: level,
|
|
format: format,
|
|
output: output,
|
|
}
|
|
}
|
|
|
|
func (l *Logger) log(level Level, msg string, fields map[string]interface{}) {
|
|
if level < l.level {
|
|
return
|
|
}
|
|
|
|
levelStr := levelToString(level)
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
if l.format == FormatJSON {
|
|
entry := logEntry{
|
|
Time: time.Now().Format(time.RFC3339),
|
|
Level: levelStr,
|
|
Message: msg,
|
|
Fields: fields,
|
|
}
|
|
data, err := json.Marshal(entry)
|
|
if err != nil {
|
|
// Fall back to simple text format on marshal error
|
|
// Error intentionally ignored - best effort fallback logging
|
|
_, _ = fmt.Fprintf(l.output, "[%s] %s (json marshal error: %v)\n", levelStr, msg, err)
|
|
return
|
|
}
|
|
if _, err := fmt.Fprintln(l.output, string(data)); err != nil {
|
|
// Write errors are typically unrecoverable (e.g., closed pipe, disk full)
|
|
// We silently ignore them to prevent cascading failures in logging
|
|
return
|
|
}
|
|
} else {
|
|
// Text format
|
|
// Write errors are silently ignored to prevent cascading failures
|
|
if len(fields) > 0 {
|
|
_, _ = fmt.Fprintf(l.output, "[%s] %s %v\n", levelStr, msg, fields)
|
|
} else {
|
|
_, _ = fmt.Fprintf(l.output, "[%s] %s\n", levelStr, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Debug(msg string, fields ...map[string]interface{}) {
|
|
f := make(map[string]interface{})
|
|
if len(fields) > 0 {
|
|
f = fields[0]
|
|
}
|
|
l.log(LevelDebug, msg, f)
|
|
}
|
|
|
|
func (l *Logger) Info(msg string, fields ...map[string]interface{}) {
|
|
f := make(map[string]interface{})
|
|
if len(fields) > 0 {
|
|
f = fields[0]
|
|
}
|
|
l.log(LevelInfo, msg, f)
|
|
}
|
|
|
|
func (l *Logger) Warn(msg string, fields ...map[string]interface{}) {
|
|
f := make(map[string]interface{})
|
|
if len(fields) > 0 {
|
|
f = fields[0]
|
|
}
|
|
l.log(LevelWarn, msg, f)
|
|
}
|
|
|
|
func (l *Logger) Error(msg string, fields ...map[string]interface{}) {
|
|
f := make(map[string]interface{})
|
|
if len(fields) > 0 {
|
|
f = fields[0]
|
|
}
|
|
l.log(LevelError, msg, f)
|
|
}
|
|
|
|
func levelToString(level Level) string {
|
|
switch level {
|
|
case LevelDebug:
|
|
return "DEBUG"
|
|
case LevelInfo:
|
|
return "INFO"
|
|
case LevelWarn:
|
|
return "WARN"
|
|
case LevelError:
|
|
return "ERROR"
|
|
default:
|
|
return "UNKNOWN"
|
|
}
|
|
}
|
|
|
|
// Global logger for backward compatibility
|
|
var globalLogger *Logger
|
|
|
|
func Init(level Level, format Format, output ...io.Writer) {
|
|
var out io.Writer
|
|
if len(output) > 0 && output[0] != nil {
|
|
out = output[0]
|
|
} else {
|
|
out = os.Stderr
|
|
}
|
|
globalLogger = New(level, format, out)
|
|
}
|
|
|
|
func Debug(msg string, fields ...map[string]interface{}) {
|
|
if globalLogger != nil {
|
|
globalLogger.Debug(msg, fields...)
|
|
}
|
|
}
|
|
|
|
func Info(msg string, fields ...map[string]interface{}) {
|
|
if globalLogger != nil {
|
|
globalLogger.Info(msg, fields...)
|
|
}
|
|
}
|
|
|
|
func Warn(msg string, fields ...map[string]interface{}) {
|
|
if globalLogger != nil {
|
|
globalLogger.Warn(msg, fields...)
|
|
}
|
|
}
|
|
|
|
func Error(msg string, fields ...map[string]interface{}) {
|
|
if globalLogger != nil {
|
|
globalLogger.Error(msg, fields...)
|
|
}
|
|
}
|