pkg/log/logger.go
Source
- Package:
log - File:
pkg/log/logger.go - GitHub: https://github.com/theroutercompany/api_router/blob/main/pkg/log/logger.go
Overview
What:
Defines the gateway's minimal logging interface and a process-wide shared logger implementation.
The package intentionally exposes a small Logger interface (Debugw/Infow/Warnw/Errorw),
and provides a Zap-backed default logger plus hooks to replace it in tests or embeddings.
Why:
Most of the gateway code uses structured logging, but does not want to:
- import Zap directly everywhere
- carry a concrete logger through every constructor
- leak a rich logging API surface into packages that only need "log with fields"
This package provides a narrow abstraction and a safe default so the rest of the codebase can log consistently.
How:
Shared()returns the global logger, initializing the default implementation once (ensureDefault).Configure(...)replaces the global logger and its sync function.Sync()flushes buffered logs via the configured sync function and suppresses a few known-benign errors.
The default logger is Zap production config, optionally writing to the path in APIGW_LOG_PATH.
Notes: Security: log fields can accidentally include secrets. Prefer logging identifiers and metadata, not payloads or credentials.
Contents
Imports
import block 1
import (
"os"
"strings"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
Variables
var block 1
var (
once sync.Once
mu sync.RWMutex
global Logger
syncLogger = func() error { return nil }
)
once
What: Ensures default logger initialization happens exactly once.
Why: Avoids races and double-initialization while keeping Shared() cheap.
How: The ensureDefault function calls once.Do(...) with the initialization closure.
mu
What: Synchronizes access to global and syncLogger.
Why: Multiple goroutines may call Shared(), Configure(), and Sync() concurrently.
How: The Shared function uses RLock; Configure and ensureDefault use Lock.
global
What: Holds the current process-wide logger implementation.
Why: Enables low-friction logging for packages that do not carry a logger dependency.
How: Set during ensureDefault or Configure and accessed under mu.
syncLogger
What: Function used by Sync() to flush the active logger backend.
Why: Some loggers require explicit flush on shutdown (e.g., Zap), but not all implementations do.
How: Set by ensureDefault or Configure; called by Sync().
Notes: Defaults to a no-op until configured.
Types
type block 1
type Logger interface {
Debugw(msg string, keysAndValues ...any)
Infow(msg string, keysAndValues ...any)
Warnw(msg string, keysAndValues ...any)
Errorw(msg string, keysAndValues ...any)
}
Logger
What: Minimal structured logging interface used across the gateway.
Why: Keeps package dependencies small and makes it easy to swap logging backends.
How: Implemented by zapLogger (Zap-backed) and noopLogger; consumed by most gateway packages.
Notes: Methods follow the *w convention (message plus key/value pairs) for structured fields.
type block 2
type noopLogger struct{}
noopLogger
What: Implementation of Logger that discards all logs.
Why: Provides a safe fallback when no logger is configured.
How: Methods are defined with empty bodies.
type block 3
type zapLogger struct {
s *zap.SugaredLogger
}
zapLogger
What: Adapter that wraps a *zap.SugaredLogger to implement Logger.
Why: Allows most of the codebase to depend only on this package's interface while still using Zap.
How: Each method delegates to the corresponding sugared logger method.
Functions and Methods
(noopLogger).Debugw
What: Discards a debug-level log call.
Why: Implements the Logger interface for the no-op logger.
How: Empty method body.
func (noopLogger) Debugw(string, ...any) {}
(noopLogger).Infow
What: Discards an info-level log call.
Why: Implements the Logger interface for the no-op logger.
How: Empty method body.
func (noopLogger) Infow(string, ...any) {}
(noopLogger).Warnw
What: Discards a warning-level log call.
Why: Implements the Logger interface for the no-op logger.
How: Empty method body.
func (noopLogger) Warnw(string, ...any) {}
(noopLogger).Errorw
What: Discards an error-level log call.
Why: Implements the Logger interface for the no-op logger.
How: Empty method body.
func (noopLogger) Errorw(string, ...any) {}
(*zapLogger).Debugw
What: Emits a debug-level structured log entry.
Why: Allows call sites to use a consistent structured logging API independent of the backing implementation.
How: Delegates to the underlying zap.SugaredLogger.Debugw.
func (z *zapLogger) Debugw(msg string, keysAndValues ...any) {
z.s.Debugw(msg, keysAndValues...)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L32:
z.s.Debugw(msg, keysAndValues...)- What: Calls z.s.Debugw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
(*zapLogger).Infow
What: Emits an info-level structured log entry.
Why: Info logs capture normal operational events without being as noisy as debug.
How: Delegates to the underlying zap.SugaredLogger.Infow.
func (z *zapLogger) Infow(msg string, keysAndValues ...any) {
z.s.Infow(msg, keysAndValues...)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L36:
z.s.Infow(msg, keysAndValues...)- What: Calls z.s.Infow.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
(*zapLogger).Warnw
What: Emits a warning-level structured log entry.
Why: Warnings represent unexpected or degraded situations that may not be fatal.
How: Delegates to the underlying zap.SugaredLogger.Warnw.
func (z *zapLogger) Warnw(msg string, keysAndValues ...any) {
z.s.Warnw(msg, keysAndValues...)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L40:
z.s.Warnw(msg, keysAndValues...)- What: Calls z.s.Warnw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
(*zapLogger).Errorw
What: Emits an error-level structured log entry.
Why: Error logs are used for failures that operators should notice and potentially alert on.
How: Delegates to the underlying zap.SugaredLogger.Errorw.
func (z *zapLogger) Errorw(msg string, keysAndValues ...any) {
z.s.Errorw(msg, keysAndValues...)
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L44:
z.s.Errorw(msg, keysAndValues...)- What: Calls z.s.Errorw.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
Shared
What: Returns the process-wide shared logger.
Why: Provides a low-friction way for packages to log without threading a logger through every function signature.
How: Ensures the default logger is initialized once (ensureDefault), then returns global under an RW lock.
Notes: For highly testable code paths, prefer dependency injection over calling Shared() directly.
func Shared() Logger {
ensureDefault()
mu.RLock()
defer mu.RUnlock()
return global
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L56:
ensureDefault()- What: Calls ensureDefault.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L58:
mu.RLock()- What: Calls mu.RLock.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L59:
defer mu.RUnlock()- What: Defers a call for cleanup.
- Why: Ensures the deferred action runs even on early returns.
- How: Schedules the call to run when the surrounding function returns.
- L60:
return global- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
Configure
What: Replaces the global shared logger and its sync function.
Why: Enables tests and downstream consumers to inject a logger consistent with their environment.
How: Panics if logger is nil, marks the default initializer as "used", then swaps global and syncLogger under a mutex; when syncFn is nil, Sync() becomes a no-op.
Notes: Call this early during startup to avoid mixed loggers across packages.
func Configure(logger Logger, syncFn func() error) {
if logger == nil {
panic("log: Configure called with nil logger")
}
once.Do(func() {})
mu.Lock()
global = logger
if syncFn == nil {
syncLogger = func() error { return nil }
} else {
syncLogger = syncFn
}
mu.Unlock()
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L65:
if logger == nil { panic("log: Configure called with nil logger") }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L66:
panic("log: Configure called with nil logger")- What: Calls panic.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L66:
- L69:
once.Do(func() {})- What: Calls once.Do.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- Nested steps:
- L69:
func() {}- What: Defines an inline function (closure).
- Why: Encapsulates callback logic and may capture variables from the surrounding scope.
- How: Declares a
funcliteral and uses it as a value (for example, as an HTTP handler or callback).
- L69:
- L71:
mu.Lock()- What: Calls mu.Lock.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L72:
global = logger- What: Assigns global.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L73:
if syncFn == nil { syncLogger = func() error { return nil } } else { syncLogger = syncFn }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L74:
syncLogger = func() error { return nil }- What: Assigns syncLogger.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- Nested steps:
- L74:
func() error { return nil }- What: Defines an inline function (closure).
- Why: Encapsulates callback logic and may capture variables from the surrounding scope.
- How: Declares a
funcliteral and uses it as a value (for example, as an HTTP handler or callback). - Nested steps:
- L74:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L74:
- L74:
- L76:
syncLogger = syncFn- What: Assigns syncLogger.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L74:
- L78:
mu.Unlock()- What: Calls mu.Unlock.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
NewZapLogger
What: Adapts a *zap.Logger to the package's Logger interface.
Why: Keeps Zap out of most call sites while still using Zap for production-quality logging.
How: If the base logger is nil, returns NewNoop and a no-op sync function; otherwise wraps base.Sugar() in a zapLogger and returns base.Sync.
Notes: The returned sync function should typically be wired to log.Configure(..., syncFn) or called during shutdown.
func NewZapLogger(base *zap.Logger) (Logger, func() error) {
if base == nil {
return NewNoop(), func() error { return nil }
}
return &zapLogger{s: base.Sugar()}, base.Sync
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L83:
if base == nil { return NewNoop(), func() error { return nil } }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L84:
return NewNoop(), func() error { return nil }- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values). - Nested steps:
- L84:
func() error { return nil }- What: Defines an inline function (closure).
- Why: Encapsulates callback logic and may capture variables from the surrounding scope.
- How: Declares a
funcliteral and uses it as a value (for example, as an HTTP handler or callback). - Nested steps:
- L84:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L84:
- L84:
- L84:
- L86:
return &zapLogger{s: base.Sugar()}, base.Sync- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
NewZapSugaredLogger
What: Adapts a *zap.SugaredLogger to the package's Logger interface.
Why: Some callers already work with sugared loggers; this avoids re-wrapping.
How: If the sugared logger is nil, returns NewNoop and a no-op sync function; otherwise wraps it in a zapLogger and returns s.Sync.
func NewZapSugaredLogger(s *zap.SugaredLogger) (Logger, func() error) {
if s == nil {
return NewNoop(), func() error { return nil }
}
return &zapLogger{s: s}, s.Sync
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L91:
if s == nil { return NewNoop(), func() error { return nil } }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L92:
return NewNoop(), func() error { return nil }- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values). - Nested steps:
- L92:
func() error { return nil }- What: Defines an inline function (closure).
- Why: Encapsulates callback logic and may capture variables from the surrounding scope.
- How: Declares a
funcliteral and uses it as a value (for example, as an HTTP handler or callback). - Nested steps:
- L92:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L92:
- L92:
- L92:
- L94:
return &zapLogger{s: s}, s.Sync- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
NewNoop
What: Constructs a logger that discards all output.
Why: Useful for tests and for environments that explicitly want silence.
How: Returns a noopLogger value that implements Logger with empty method bodies.
func NewNoop() Logger {
return noopLogger{}
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L99:
return noopLogger{}- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
Sync
What: Flushes any buffered log entries for the current global logger.
Why: Ensures logs are not lost on process exit, especially when writing to files.
How: Calls the configured syncLogger function; suppresses a couple of known benign OS errors; otherwise returns the sync error.
Notes: Zap can return platform-specific errors when syncing stdout/stderr; this helper makes shutdown logic more robust.
func Sync() error {
if err := syncLogger(); err != nil {
msg := err.Error()
if strings.Contains(msg, "bad file descriptor") || strings.Contains(msg, "invalid argument") {
return nil
}
return err
}
return nil
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L104:
if err := syncLogger(); err != nil { msg := err.Error() if strings.Contains(msg, "bad file descriptor") || strings.Contains(msg, "invalid a…- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L104:
err := syncLogger()- What: Defines err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L105:
msg := err.Error()- What: Defines msg.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L106:
if strings.Contains(msg, "bad file descriptor") || strings.Contains(msg, "invalid argument") { return nil }- What: Branches conditionally.
- Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L107:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L107:
- L109:
return err- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
- L104:
- L111:
return nil- What: Returns from the current function.
- Why: Ends the current execution path and hands control back to the caller.
- How: Executes a
returnstatement (possibly returning values).
ensureDefault
What: Initializes the default Zap-backed global logger exactly once.
Why: Avoids requiring explicit setup for common paths while keeping initialization thread-safe.
How: Uses sync.Once to build a Zap production config, optionally updates output paths from APIGW_LOG_PATH, sets encoder keys and ISO8601 timestamps, builds the logger, then stores the adapter and sync function under a mutex.
Notes: Panics if the Zap logger cannot be built; treat this as a startup-time configuration error.
func ensureDefault() {
once.Do(func() {
cfg := zap.NewProductionConfig()
if path := strings.TrimSpace(os.Getenv("APIGW_LOG_PATH")); path != "" {
cfg.OutputPaths = []string{path}
cfg.ErrorOutputPaths = []string{path}
}
cfg.EncoderConfig.TimeKey = "time"
cfg.EncoderConfig.MessageKey = "msg"
cfg.EncoderConfig.LevelKey = "level"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
base, err := cfg.Build()
if err != nil {
panic(err)
}
logger, syncFn := NewZapLogger(base)
mu.Lock()
global = logger
syncLogger = syncFn
mu.Unlock()
})
}
Walkthrough
The list below documents the statements inside the function body, including nested blocks and inline closures.
- L115:
once.Do(func() { cfg := zap.NewProductionConfig() if path := strings.TrimSpace(os.Getenv("APIGW_LOG_PATH")); path != "" { cfg.OutputPaths =…- What: Calls once.Do.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- Nested steps:
- L115:
func() { cfg := zap.NewProductionConfig() if path := strings.TrimSpace(os.Getenv("APIGW_LOG_PATH")); path != "" { cfg.OutputPaths = []strin…- What: Defines an inline function (closure).
- Why: Encapsulates callback logic and may capture variables from the surrounding scope.
- How: Declares a
funcliteral and uses it as a value (for example, as an HTTP handler or callback). - Nested steps:
- L116:
cfg := zap.NewProductionConfig()- What: Defines cfg.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L117:
if path := strings.TrimSpace(os.Getenv("APIGW_LOG_PATH")); path != "" { cfg.OutputPaths = []string{path} cfg.ErrorOutputPaths = []string{pa…- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L117:
path := strings.TrimSpace(os.Getenv("APIGW_LOG_PATH"))- What: Defines path.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L118:
cfg.OutputPaths = []string{path}- What: Assigns cfg.OutputPaths.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L119:
cfg.ErrorOutputPaths = []string{path}- What: Assigns cfg.ErrorOutputPaths.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L117:
- L121:
cfg.EncoderConfig.TimeKey = "time"- What: Assigns cfg.EncoderConfig.TimeKey.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L122:
cfg.EncoderConfig.MessageKey = "msg"- What: Assigns cfg.EncoderConfig.MessageKey.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L123:
cfg.EncoderConfig.LevelKey = "level"- What: Assigns cfg.EncoderConfig.LevelKey.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L124:
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder- What: Assigns cfg.EncoderConfig.EncodeTime.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L126:
base, err := cfg.Build()- What: Defines base, err.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L127:
if err != nil { panic(err) }- What: Branches conditionally.
- Why: Handles different execution paths based on runtime state.
- How: Evaluates the condition and executes the matching branch.
- Nested steps:
- L128:
panic(err)- What: Calls panic.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L128:
- L131:
logger, syncFn := NewZapLogger(base)- What: Defines logger, syncFn.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L133:
mu.Lock()- What: Calls mu.Lock.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L134:
global = logger- What: Assigns global.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L135:
syncLogger = syncFn- What: Assigns syncLogger.
- Why: Keeps intermediate state available for later steps in the function.
- How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
- L136:
mu.Unlock()- What: Calls mu.Unlock.
- Why: Performs side effects or delegates work to a helper.
- How: Executes the expression statement.
- L116:
- L115: