mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
3aa83d4480
* chore(security,refactor): extract sanitization and improve code quality
- [x] Extract sanitization functions to dedicated sanitization.go module
- [x] Add comprehensive golangci-lint v2 configuration with security rules
- [x] Replace interface{} with any type throughout codebase
- [x] Add admin API authentication security warning
- [x] Extract WebSocket and stats streaming constants
- [x] Add best-effort error handling comments for resource cleanup
- [x] Expand sensitive field patterns for improved PII redaction
- [x] Simplify safety checks and remove redundant nil validations
- [x] Improve test coverage for password field redaction patterns
* refactor: replace interface{} with any type alias
- [x] Replace all `map[string]interface{}` with `map[string]any`
- [x] Replace all `interface{}` with `any` in function signatures and type definitions
- [x] Update sync.Pool New function returns from `interface{}` to `any`
- [x] Add package documentation comments to 8 package files
- [x] Update type assertions and casts to work with `any` type
228 lines
4.9 KiB
Go
228 lines
4.9 KiB
Go
// Package libpack_logger provides structured JSON logging with configurable
|
|
// log levels, caller information, and automatic sensitive data redaction.
|
|
// Supports debug, info, warning, and error log levels.
|
|
package libpack_logger
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/goccy/go-json"
|
|
)
|
|
|
|
const (
|
|
LEVEL_DEBUG = iota
|
|
LEVEL_INFO
|
|
LEVEL_WARN
|
|
LEVEL_ERROR
|
|
LEVEL_FATAL
|
|
)
|
|
|
|
var levelNames = []string{
|
|
"debug",
|
|
"info",
|
|
"warn",
|
|
"error",
|
|
"fatal",
|
|
}
|
|
|
|
const (
|
|
defaultTimeFormat = time.RFC3339
|
|
defaultMinLevel = LEVEL_INFO
|
|
defaultShowCaller = false
|
|
)
|
|
|
|
// Logger represents the logging object with configurations.
|
|
type Logger struct {
|
|
output io.Writer
|
|
timeFormat string
|
|
minLogLevel int
|
|
showCaller bool
|
|
mu sync.Mutex // Mutex to protect concurrent access to output
|
|
}
|
|
|
|
// LogMessage represents a log message with optional pairs.
|
|
type LogMessage struct {
|
|
Pairs map[string]any
|
|
Message string
|
|
}
|
|
|
|
// bufferPool is used to reuse bytes.Buffer for efficiency.
|
|
var bufferPool = sync.Pool{
|
|
New: func() any {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
// fieldNames allows customization of output field names.
|
|
var fieldNames = map[string]string{
|
|
"timestamp": "timestamp",
|
|
"level": "level",
|
|
"message": "message",
|
|
}
|
|
|
|
// osExit is a variable to allow mocking os.Exit in tests
|
|
var osExit = os.Exit
|
|
|
|
// exitMutex ensures thread-safe access to osExit
|
|
var exitMutex sync.RWMutex
|
|
|
|
// New creates a new Logger with default settings.
|
|
func New() *Logger {
|
|
return &Logger{
|
|
timeFormat: defaultTimeFormat,
|
|
minLogLevel: defaultMinLevel,
|
|
output: os.Stdout,
|
|
showCaller: defaultShowCaller,
|
|
}
|
|
}
|
|
|
|
// SetOutput sets the output destination for the logger.
|
|
func (l *Logger) SetOutput(output io.Writer) *Logger {
|
|
l.mu.Lock()
|
|
l.output = output
|
|
l.mu.Unlock()
|
|
return l
|
|
}
|
|
|
|
// GetLogLevel returns the log level integer corresponding to the given level name.
|
|
func GetLogLevel(level string) int {
|
|
level = strings.ToLower(level)
|
|
for i, name := range levelNames {
|
|
if name == level {
|
|
return i
|
|
}
|
|
}
|
|
return defaultMinLevel
|
|
}
|
|
|
|
// SetTimeFormat sets the time format for the logger's timestamp field.
|
|
func (l *Logger) SetTimeFormat(format string) *Logger {
|
|
l.timeFormat = format
|
|
return l
|
|
}
|
|
|
|
// SetMinLogLevel sets the minimum log level for the logger.
|
|
func (l *Logger) SetMinLogLevel(level int) *Logger {
|
|
l.minLogLevel = level
|
|
return l
|
|
}
|
|
|
|
// SetFieldName allows customizing the field names in log output.
|
|
func (l *Logger) SetFieldName(field, name string) *Logger {
|
|
fieldNames[field] = name
|
|
return l
|
|
}
|
|
|
|
// SetShowCaller enables or disables including the caller information in log output.
|
|
func (l *Logger) SetShowCaller(show bool) *Logger {
|
|
l.showCaller = show
|
|
return l
|
|
}
|
|
|
|
// shouldLog determines if the message should be logged based on the logger's minimum log level.
|
|
func (l *Logger) shouldLog(level int) bool {
|
|
return level >= l.minLogLevel
|
|
}
|
|
|
|
// log writes the log message with the given level.
|
|
func (l *Logger) log(level int, m *LogMessage) {
|
|
if m.Pairs == nil {
|
|
m.Pairs = make(map[string]any)
|
|
}
|
|
|
|
m.Pairs[fieldNames["timestamp"]] = time.Now().Format(l.timeFormat)
|
|
m.Pairs[fieldNames["level"]] = levelNames[level]
|
|
m.Pairs[fieldNames["message"]] = m.Message
|
|
|
|
if l.showCaller {
|
|
m.Pairs["caller"] = getCaller()
|
|
}
|
|
|
|
buffer := bufferPool.Get().(*bytes.Buffer)
|
|
buffer.Reset()
|
|
defer bufferPool.Put(buffer)
|
|
|
|
encoder := json.NewEncoder(buffer)
|
|
err := encoder.Encode(m.Pairs)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "Error marshalling log message:", err)
|
|
return
|
|
}
|
|
// Lock the mutex before writing to the output to prevent race conditions
|
|
l.mu.Lock()
|
|
_, err = l.output.Write(buffer.Bytes())
|
|
l.mu.Unlock()
|
|
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "Error writing log message:", err)
|
|
}
|
|
}
|
|
|
|
// Debug logs a debug-level message.
|
|
func (l *Logger) Debug(m *LogMessage) {
|
|
if l.shouldLog(LEVEL_DEBUG) {
|
|
l.log(LEVEL_DEBUG, m)
|
|
}
|
|
}
|
|
|
|
// Info logs an info-level message.
|
|
func (l *Logger) Info(m *LogMessage) {
|
|
if l.shouldLog(LEVEL_INFO) {
|
|
l.log(LEVEL_INFO, m)
|
|
}
|
|
}
|
|
|
|
// Warn logs a warning-level message.
|
|
func (l *Logger) Warn(m *LogMessage) {
|
|
if l.shouldLog(LEVEL_WARN) {
|
|
l.log(LEVEL_WARN, m)
|
|
}
|
|
}
|
|
|
|
// Warning is an alias for Warn.
|
|
func (l *Logger) Warning(m *LogMessage) {
|
|
l.Warn(m)
|
|
}
|
|
|
|
// Error logs an error-level message.
|
|
func (l *Logger) Error(m *LogMessage) {
|
|
if l.shouldLog(LEVEL_ERROR) {
|
|
l.log(LEVEL_ERROR, m)
|
|
}
|
|
}
|
|
|
|
// Fatal logs a fatal-level message.
|
|
func (l *Logger) Fatal(m *LogMessage) {
|
|
if l.shouldLog(LEVEL_FATAL) {
|
|
l.log(LEVEL_FATAL, m)
|
|
}
|
|
}
|
|
|
|
// Critical logs a critical-level message and exits the application.
|
|
func (l *Logger) Critical(m *LogMessage) {
|
|
l.Fatal(m)
|
|
exitMutex.RLock()
|
|
defer exitMutex.RUnlock()
|
|
osExit(1)
|
|
}
|
|
|
|
// getCaller retrieves the file and line number of the caller.
|
|
func getCaller() string {
|
|
// Skip 3 stack frames: getCaller -> log -> [Debug|Info|...]
|
|
const depth = 3
|
|
_, file, line, ok := runtime.Caller(depth)
|
|
if !ok {
|
|
return "unknown:0"
|
|
}
|
|
file = filepath.Base(file)
|
|
return fmt.Sprintf("%s:%d", file, line)
|
|
}
|