mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
c2c75d69c0
Performance / resource usage: - circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking) - server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo - graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off) - graphql.go: collapse two-pass AST scan into single pass; lower-case once - sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars - proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc - metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled - logging/logger.go: add IsLevelEnabled helper - lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%) - cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock - rps_tracker.go: RWMutex+uint64 -> atomic.Uint64 - retry_budget.go: drop unused mutex - api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers) - tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes - admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection Build / runtime: - Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes) - Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB - main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS) - main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts - README.md: env-var docs + metric-label docs updated; cardinality note Test coverage push (per package): - main 51.2% -> 74.7% - cache 66.3% -> 93.7% - cache/redis 45.5% -> 98.2% - tracing 66.7% -> 72.9% - (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged) New test files: coverage_micro_test, coverage_extras_test, server_handlers_test, api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test, cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test. Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
235 lines
5.3 KiB
Go
235 lines
5.3 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
|
|
}
|
|
|
|
// IsLevelEnabled reports whether the given level would be emitted by this logger.
|
|
// Useful to gate expensive log-field construction (map/slice allocations) behind a
|
|
// cheap level check when the log call would otherwise be dropped.
|
|
func (l *Logger) IsLevelEnabled(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)
|
|
}
|