forked from aegis/pyserveX
- Introduced IgnoreRequestPath option in proxy configuration to allow exact match routing. - Implemented proxy_pass directive in routing extension to handle backend requests. - Enhanced error handling for backend unavailability and timeouts. - Added integration tests for reverse proxy, including basic requests, exact match routes, regex routes, header forwarding, and query string preservation. - Created helper functions for setting up test servers and backends, along with assertion utilities for response validation. - Updated server initialization to support extension management and middleware chaining. - Improved logging for debugging purposes during request handling.
339 lines
8.4 KiB
Go
339 lines
8.4 KiB
Go
// Package logging provides structured logging with zap
|
|
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
|
|
"github.com/konduktor/konduktor/internal/config"
|
|
)
|
|
|
|
// Config is a simple configuration for basic logger setup
|
|
type Config struct {
|
|
Level string
|
|
TimestampFormat string
|
|
}
|
|
|
|
// Logger wraps zap.SugaredLogger with additional functionality
|
|
type Logger struct {
|
|
*zap.SugaredLogger
|
|
zap *zap.Logger
|
|
config *config.LoggingConfig
|
|
name string
|
|
}
|
|
|
|
// New creates a new Logger with basic configuration
|
|
func New(cfg Config) (*Logger, error) {
|
|
level := parseLevel(cfg.Level)
|
|
|
|
timestampFormat := cfg.TimestampFormat
|
|
if timestampFormat == "" {
|
|
timestampFormat = "2006-01-02 15:04:05"
|
|
}
|
|
|
|
encoderConfig := zap.NewProductionEncoderConfig()
|
|
encoderConfig.TimeKey = "timestamp"
|
|
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(timestampFormat)
|
|
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
|
|
core := zapcore.NewCore(
|
|
zapcore.NewConsoleEncoder(encoderConfig),
|
|
zapcore.AddSync(os.Stdout),
|
|
level,
|
|
)
|
|
|
|
zapLogger := zap.New(core)
|
|
return &Logger{
|
|
SugaredLogger: zapLogger.Sugar(),
|
|
zap: zapLogger,
|
|
name: "konduktor",
|
|
}, nil
|
|
}
|
|
|
|
// NewFromConfig creates a Logger from full LoggingConfig
|
|
func NewFromConfig(cfg config.LoggingConfig) (*Logger, error) {
|
|
var cores []zapcore.Core
|
|
|
|
// Parse main level
|
|
mainLevel := parseLevel(cfg.Level)
|
|
|
|
// Add console core if enabled
|
|
if cfg.ConsoleOutput {
|
|
consoleLevel := mainLevel
|
|
if cfg.Console != nil && cfg.Console.Level != "" {
|
|
consoleLevel = parseLevel(cfg.Console.Level)
|
|
}
|
|
|
|
var consoleEncoder zapcore.Encoder
|
|
formatConfig := cfg.Format
|
|
if cfg.Console != nil {
|
|
formatConfig = mergeFormatConfig(cfg.Format, cfg.Console.Format)
|
|
}
|
|
|
|
encoderCfg := createEncoderConfig(formatConfig)
|
|
if formatConfig.Type == "json" {
|
|
consoleEncoder = zapcore.NewJSONEncoder(encoderCfg)
|
|
} else {
|
|
if formatConfig.UseColors {
|
|
encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
}
|
|
consoleEncoder = zapcore.NewConsoleEncoder(encoderCfg)
|
|
}
|
|
|
|
consoleSyncer := zapcore.AddSync(os.Stdout)
|
|
cores = append(cores, zapcore.NewCore(consoleEncoder, consoleSyncer, consoleLevel))
|
|
}
|
|
|
|
// Add file cores
|
|
for _, fileConfig := range cfg.Files {
|
|
fileCore, err := createFileCore(fileConfig, cfg.Format, mainLevel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create file logger for %s: %w", fileConfig.Path, err)
|
|
}
|
|
|
|
// If specific loggers are configured, wrap with filter
|
|
if len(fileConfig.Loggers) > 0 {
|
|
fileCore = &filteredCore{
|
|
Core: fileCore,
|
|
loggers: fileConfig.Loggers,
|
|
}
|
|
}
|
|
|
|
cores = append(cores, fileCore)
|
|
}
|
|
|
|
// If no cores configured, add default console
|
|
if len(cores) == 0 {
|
|
encoderCfg := zap.NewProductionEncoderConfig()
|
|
encoderCfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
|
|
encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
cores = append(cores, zapcore.NewCore(
|
|
zapcore.NewConsoleEncoder(encoderCfg),
|
|
zapcore.AddSync(os.Stdout),
|
|
mainLevel,
|
|
))
|
|
}
|
|
|
|
// Combine all cores
|
|
core := zapcore.NewTee(cores...)
|
|
zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
|
|
|
return &Logger{
|
|
SugaredLogger: zapLogger.Sugar(),
|
|
zap: zapLogger,
|
|
config: &cfg,
|
|
name: "konduktor",
|
|
}, nil
|
|
}
|
|
|
|
// Named returns a logger with a specific name (for filtering)
|
|
func (l *Logger) Named(name string) *Logger {
|
|
return &Logger{
|
|
SugaredLogger: l.SugaredLogger.Named(name),
|
|
zap: l.zap.Named(name),
|
|
config: l.config,
|
|
name: name,
|
|
}
|
|
}
|
|
|
|
// With returns a logger with additional fields
|
|
func (l *Logger) With(args ...interface{}) *Logger {
|
|
return &Logger{
|
|
SugaredLogger: l.SugaredLogger.With(args...),
|
|
zap: l.zap.Sugar().With(args...).Desugar(),
|
|
config: l.config,
|
|
name: l.name,
|
|
}
|
|
}
|
|
|
|
// Sync flushes any buffered log entries
|
|
func (l *Logger) Sync() error {
|
|
return l.zap.Sync()
|
|
}
|
|
|
|
// GetZap returns the underlying zap.Logger
|
|
func (l *Logger) GetZap() *zap.Logger {
|
|
return l.zap
|
|
}
|
|
|
|
// Debug logs a debug message
|
|
func (l *Logger) Debug(msg string, keysAndValues ...interface{}) {
|
|
l.SugaredLogger.Debugw(msg, keysAndValues...)
|
|
}
|
|
|
|
// Info logs an info message
|
|
func (l *Logger) Info(msg string, keysAndValues ...interface{}) {
|
|
l.SugaredLogger.Infow(msg, keysAndValues...)
|
|
}
|
|
|
|
// Warn logs a warning message
|
|
func (l *Logger) Warn(msg string, keysAndValues ...interface{}) {
|
|
l.SugaredLogger.Warnw(msg, keysAndValues...)
|
|
}
|
|
|
|
// Error logs an error message
|
|
func (l *Logger) Error(msg string, keysAndValues ...interface{}) {
|
|
l.SugaredLogger.Errorw(msg, keysAndValues...)
|
|
}
|
|
|
|
// Fatal logs a fatal message and exits
|
|
func (l *Logger) Fatal(msg string, keysAndValues ...interface{}) {
|
|
l.SugaredLogger.Fatalw(msg, keysAndValues...)
|
|
}
|
|
|
|
// --- Helper functions ---
|
|
|
|
func parseLevel(level string) zapcore.Level {
|
|
switch strings.ToUpper(level) {
|
|
case "DEBUG":
|
|
return zapcore.DebugLevel
|
|
case "INFO":
|
|
return zapcore.InfoLevel
|
|
case "WARN", "WARNING":
|
|
return zapcore.WarnLevel
|
|
case "ERROR":
|
|
return zapcore.ErrorLevel
|
|
case "CRITICAL", "FATAL":
|
|
return zapcore.FatalLevel
|
|
default:
|
|
return zapcore.InfoLevel
|
|
}
|
|
}
|
|
|
|
func createEncoderConfig(format config.LogFormatConfig) zapcore.EncoderConfig {
|
|
timestampFormat := format.TimestampFormat
|
|
if timestampFormat == "" {
|
|
timestampFormat = "2006-01-02 15:04:05"
|
|
}
|
|
|
|
cfg := zapcore.EncoderConfig{
|
|
TimeKey: "timestamp",
|
|
LevelKey: "level",
|
|
NameKey: "logger",
|
|
CallerKey: "caller",
|
|
FunctionKey: zapcore.OmitKey,
|
|
MessageKey: "msg",
|
|
StacktraceKey: "stacktrace",
|
|
LineEnding: zapcore.DefaultLineEnding,
|
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
|
EncodeTime: zapcore.TimeEncoderOfLayout(timestampFormat),
|
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
|
}
|
|
|
|
if !format.ShowModule {
|
|
cfg.NameKey = zapcore.OmitKey
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func mergeFormatConfig(base, override config.LogFormatConfig) config.LogFormatConfig {
|
|
result := base
|
|
if override.Type != "" {
|
|
result.Type = override.Type
|
|
}
|
|
if override.TimestampFormat != "" {
|
|
result.TimestampFormat = override.TimestampFormat
|
|
}
|
|
// UseColors and ShowModule are bool - check if override has non-default
|
|
result.UseColors = override.UseColors
|
|
result.ShowModule = override.ShowModule
|
|
return result
|
|
}
|
|
|
|
func createFileCore(fileConfig config.FileLogConfig, defaultFormat config.LogFormatConfig, defaultLevel zapcore.Level) (zapcore.Core, error) {
|
|
// Ensure directory exists
|
|
dir := filepath.Dir(fileConfig.Path)
|
|
if dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create log directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
// Configure log rotation with lumberjack
|
|
maxSize := 10 // MB
|
|
if fileConfig.MaxBytes > 0 {
|
|
maxSize = int(fileConfig.MaxBytes / (1024 * 1024))
|
|
if maxSize < 1 {
|
|
maxSize = 1
|
|
}
|
|
}
|
|
|
|
backupCount := 5
|
|
if fileConfig.BackupCount > 0 {
|
|
backupCount = fileConfig.BackupCount
|
|
}
|
|
|
|
rotator := &lumberjack.Logger{
|
|
Filename: fileConfig.Path,
|
|
MaxSize: maxSize,
|
|
MaxBackups: backupCount,
|
|
MaxAge: 30, // days
|
|
Compress: true,
|
|
}
|
|
|
|
// Determine level
|
|
level := defaultLevel
|
|
if fileConfig.Level != "" {
|
|
level = parseLevel(fileConfig.Level)
|
|
}
|
|
|
|
// Create encoder
|
|
format := defaultFormat
|
|
if fileConfig.Format.Type != "" {
|
|
format = mergeFormatConfig(defaultFormat, fileConfig.Format)
|
|
}
|
|
// Files should not use colors
|
|
format.UseColors = false
|
|
|
|
encoderConfig := createEncoderConfig(format)
|
|
var encoder zapcore.Encoder
|
|
if format.Type == "json" {
|
|
encoder = zapcore.NewJSONEncoder(encoderConfig)
|
|
} else {
|
|
encoder = zapcore.NewConsoleEncoder(encoderConfig)
|
|
}
|
|
|
|
return zapcore.NewCore(encoder, zapcore.AddSync(rotator), level), nil
|
|
}
|
|
|
|
// filteredCore wraps a Core to filter by logger name
|
|
type filteredCore struct {
|
|
zapcore.Core
|
|
loggers []string
|
|
}
|
|
|
|
func (c *filteredCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
|
if !c.shouldLog(entry.LoggerName) {
|
|
return ce
|
|
}
|
|
return c.Core.Check(entry, ce)
|
|
}
|
|
|
|
func (c *filteredCore) shouldLog(loggerName string) bool {
|
|
if len(c.loggers) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, allowed := range c.loggers {
|
|
if loggerName == allowed || strings.HasPrefix(loggerName, allowed+".") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *filteredCore) With(fields []zapcore.Field) zapcore.Core {
|
|
return &filteredCore{
|
|
Core: c.Core.With(fields),
|
|
loggers: c.loggers,
|
|
}
|
|
}
|