logger

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: GPL-3.0 Imports: 10 Imported by: 0

README

Logger Package

Production-grade structured logging system built on zap.

Features

  • Structured Logging: Type-safe field logging with zap
  • Multiple Formats: JSON (default for k8s) and human-readable text
  • Progressive Disclosure: Verbosity-aware field injection based on log level
  • Caller Information: Automatic injection of file, function, and line for DEBUG/ERROR levels
  • Child Loggers: Create contextual loggers with With() and Named()
  • ISO8601 Timestamps: Standard timestamp format for log aggregation
  • Fake Logger: No-op implementation for testing

Quick Start

Basic Usage
package main

import (
    "github.com/grhili/cd-operator/pkg/logger"
    "go.uber.org/zap"
)

func main() {
    // Create logger (JSON format by default for k8s)
    log, err := logger.New(logger.Config{
        Level:  "info",
        Format: "json",
    })
    if err != nil {
        panic(err)
    }
    defer log.Sync() // CRITICAL: flush buffered logs

    // Structured logging
    log.Info("application started",
        zap.String("service", "cd-operator"),
        zap.String("version", "v1.0.0"),
    )
}
Development Mode (Text Format)
log, err := logger.New(logger.Config{
    Level:  "debug",
    Format: "text", // Human-readable format with colors
})
if err != nil {
    panic(err)
}
defer log.Sync()

log.Debug("processing request", zap.String("path", "/api/v1/health"))
// Output: 2026-02-14 09:52:30 [DEBUG] processing request path=/api/v1/health
Child Loggers
// Create base logger
baseLogger, _ := logger.New(logger.Config{Level: "info"})
defer baseLogger.Sync()

// Add stable metadata
serviceLog := baseLogger.With(
    zap.String("service", "cd-operator"),
    zap.String("version", "v1.0.0"),
)

// Create subsystem loggers
ghLogger := serviceLog.Named("github")
argoLogger := serviceLog.Named("argocd")

ghLogger.Info("fetching repository") // {"logger":"github", "service":"cd-operator", ...}
argoLogger.Info("syncing application") // {"logger":"argocd", "service":"cd-operator", ...}

Configuration

Config Struct
type Config struct {
    // Level controls minimum log level: "debug", "info", "warn", "error"
    Level string

    // Format controls output format: "text" (human-readable), "json" (structured)
    // Default is "json" for k8s log collection compatibility
    Format string

    // Writer is the output destination (default: os.Stdout)
    Writer io.Writer
}
Defaults
  • Level: "info" - Production default
  • Format: "json" - Kubernetes-friendly structured logs
  • Writer: os.Stdout - Standard output

Log Levels

Level Usage Verbosity
debug Development, troubleshooting Shows all fields + caller info
info Normal operation (default) Minimal fields, clean output
warn Recoverable errors, unexpected behavior Minimal fields
error Non-fatal errors Includes caller info automatically
fatal Unrecoverable errors (exits with code 1) Full context + stack trace

Progressive Disclosure

The logger automatically adjusts field visibility based on log level:

DEBUG Level
  • All logs include: caller, function, line, trace_id
  • Use for development and troubleshooting
INFO/WARN Level
  • Clean output: Only per-log data fields
  • Metadata fields (service, version, logger) are hidden in text format
  • JSON format includes all fields (filtered by log aggregation tools)
ERROR Level
  • Automatically includes caller for debugging context
  • Shows file path relative to project root

API Reference

Logger Methods
// Logging methods
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field)

// Context methods
With(fields ...zap.Field) *Logger        // Add stable metadata
Named(name string) *Logger                // Create subsystem logger
Sugar() *zap.SugaredLogger                // For key-value APIs

// Lifecycle
Sync() error                              // CRITICAL: flush buffered logs
Common Fields

Use zap's typed fields for structured logging:

zap.String("key", "value")
zap.Int("count", 42)
zap.Duration("latency", 100*time.Millisecond)
zap.Error(err)
zap.Bool("success", true)
zap.Time("timestamp", time.Now())
zap.Any("complex", struct{}{}) // Use sparingly

Testing

FakeLogger

Use FakeLogger in unit tests to avoid log noise:

func TestMyFunction(t *testing.T) {
    log := logger.NewFakeLogger()

    // All methods are no-ops
    myFunction(log) // No output
}
Testing with Real Logger

For integration tests, capture output to verify log content:

func TestLogging(t *testing.T) {
    buf := &bytes.Buffer{}
    log, _ := logger.New(logger.Config{
        Level:  "info",
        Format: "json",
        Writer: buf,
    })
    defer log.Sync()

    log.Info("test message")
    log.Sync()

    output := buf.String()
    if !strings.Contains(output, `"msg":"test message"`) {
        t.Error("Expected message not found")
    }
}

Best Practices

1. Always Call Sync()
log, err := logger.New(cfg)
if err != nil {
    return err
}
defer log.Sync() // CRITICAL: flush buffered logs
2. Use Child Loggers for Context
// Create child logger once
reqLogger := log.With(
    zap.String("request_id", reqID),
    zap.String("user", userID),
)

// Use throughout request lifecycle
reqLogger.Info("processing request")
reqLogger.Info("validation successful")
3. Structured Fields Only
// Good: Structured fields
log.Info("user logged in", zap.String("user_id", "123"))

// Bad: String interpolation
log.Info(fmt.Sprintf("user %s logged in", userID)) // Don't do this
4. Use Named Loggers for Subsystems
ghClient := log.Named("github")
argoClient := log.Named("argocd")

ghClient.Info("api call") // {"logger":"github", ...}
5. Log Errors with Context
if err != nil {
    log.Error("failed to sync application",
        zap.String("app", appName),
        zap.String("namespace", ns),
        zap.Error(err),
    )
    return err
}

Examples

See example_test.go for more usage examples:

go test -v -run Example ./pkg/logger

Performance

  • Zero Allocation: Uses zap's zero-allocation design
  • Buffered Writes: Logs are buffered for performance (remember to Sync())
  • Level Filtering: Debug logs are skipped at higher levels (no performance cost)
  • Field Pooling: Reuses buffer pools for encoding

JSON Output Format

{
  "level": "info",
  "ts": "2026-02-14T09:52:30.123Z",
  "msg": "application started",
  "service": "cd-operator",
  "version": "v1.0.0"
}

Text Output Format

2026-02-14 09:52:30 [INFO] application started service=cd-operator version=v1.0.0

License

Part of cd-operator project.

Documentation

Overview

Package logger provides a production-grade structured logging system built on zap. It supports custom formatting, distributed tracing, and observability integration.

Package logger provides common types and constants for structured logging.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsMetadataField

func IsMetadataField(key string) bool

IsMetadataField checks if a field key is a metadata field.

func NewCallerHook

func NewCallerHook(core zapcore.Core, level zapcore.Level) zapcore.Core

NewCallerHook creates a Core wrapper that injects caller info based on verbosity. The level parameter should be the logger's configured level (not individual entry level).

func NewJSONEncoder

func NewJSONEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder

NewJSONEncoder creates an encoder for structured JSON output. Each log entry is a single-line JSON object.

func NewTextEncoder

func NewTextEncoder(cfg zapcore.EncoderConfig, enableColor bool, level zapcore.Level) zapcore.Encoder

NewTextEncoder creates an encoder for human-readable text output. enableColor determines whether ANSI color codes are added to level brackets.

Types

type CallerHook

type CallerHook struct {
	zapcore.Core
	// contains filtered or unexported fields
}

CallerHook wraps zapcore.Core to inject caller information based on log level.

Verbosity rules (progressive disclosure):

  • DEBUG level: ALL logs include trace_id + caller + function + line
  • INFO level: clean output, no extra fields
  • WARN level: clean output, no extra fields (same as info)
  • ERROR level: adds caller only

This follows the best practice of wrapping Core instead of Encoder.

func (*CallerHook) With

func (h *CallerHook) With(fields []zapcore.Field) zapcore.Core

With creates a child Core with additional fields.

func (*CallerHook) Write

func (h *CallerHook) Write(ent zapcore.Entry, fields []zapcore.Field) error

Write intercepts log entries and adds caller fields based on verbosity rules.

type ColorCode

type ColorCode string

ColorCode represents ANSI color escape sequences for terminal output.

const (
	ColorCyan   ColorCode = "\033[36m" // DEBUG
	ColorGreen  ColorCode = "\033[32m" // INFO
	ColorYellow ColorCode = "\033[33m" // WARN
	ColorRed    ColorCode = "\033[31m" // ERROR
	ColorBold   ColorCode = "\033[1m"  // FATAL/PANIC (bold)
	ColorReset  ColorCode = "\033[0m"  // Reset to default
)

func ColorForLevel

func ColorForLevel(level zapcore.Level) ColorCode

ColorForLevel returns the appropriate ANSI color code for a log level.

func (ColorCode) String

func (c ColorCode) String() string

String returns the ANSI escape sequence as a string.

type Config

type Config struct {
	// Level controls minimum log level: "debug", "info", "warn", "error"
	Level string

	// Format controls output format: "text" (human-readable), "json" (structured)
	// Default is "json" for k8s log collection compatibility
	Format string

	// Writer is the output destination (default: os.Stdout)
	// Can be wrapped with async/metrics writers for observability
	Writer io.Writer
}

Config holds logger configuration parameters. This is a value type (small, immutable struct passed by value).

type ContextKey

type ContextKey string

ContextKey is a typed key for context values to prevent collisions.

const (
	// TraceIDKey is the context key for storing trace IDs.
	TraceIDKey ContextKey = "trace_id"

	// LoggerKey is the context key for storing logger instances.
	LoggerKey ContextKey = "logger"
)

func (ContextKey) String

func (k ContextKey) String() string

String returns the context key as a string.

type FakeLogger

type FakeLogger struct{}

FakeLogger is a no-op logger implementation for testing. All methods are no-ops and do not produce output. This is useful in unit tests to avoid log noise.

func NewFakeLogger

func NewFakeLogger() *FakeLogger

NewFakeLogger creates a new FakeLogger instance.

Example

ExampleNewFakeLogger demonstrates using FakeLogger for tests.

package main

import (
	"github.com/grhili/cd-operator/pkg/logger"
)

func main() {
	// Use FakeLogger in tests to avoid log noise
	fake := logger.NewFakeLogger()

	// All methods are no-ops
	fake.Info("this will not produce output")
	fake.Error("this will not produce output")
	fake.Sync() // Safe to call
}

func (*FakeLogger) Debug

func (f *FakeLogger) Debug(msg string, fields ...zap.Field)

Debug is a no-op implementation for testing that discards all debug log output.

func (*FakeLogger) Error

func (f *FakeLogger) Error(msg string, fields ...zap.Field)

Error is a no-op implementation for testing that discards all error log output.

func (*FakeLogger) Fatal

func (f *FakeLogger) Fatal(msg string, fields ...zap.Field)

Fatal is a no-op implementation for testing that discards the message and does not exit the process.

func (*FakeLogger) Info

func (f *FakeLogger) Info(msg string, fields ...zap.Field)

Info is a no-op implementation for testing that discards all info log output.

func (*FakeLogger) Named

func (f *FakeLogger) Named(name string) *FakeLogger

Named returns the same FakeLogger (no-op).

func (*FakeLogger) Sync

func (f *FakeLogger) Sync() error

Sync is a no-op.

func (*FakeLogger) Warn

func (f *FakeLogger) Warn(msg string, fields ...zap.Field)

Warn is a no-op implementation for testing that discards all warning log output.

func (*FakeLogger) With

func (f *FakeLogger) With(fields ...zap.Field) *FakeLogger

With returns the same FakeLogger (no-op).

type LogFormat

type LogFormat string

LogFormat represents output format options.

const (
	FormatText LogFormat = "text"
	FormatJSON LogFormat = "json"
)

type LogLevel

type LogLevel string

LogLevel represents valid log level strings.

const (
	LevelDebug LogLevel = "debug"
	LevelInfo  LogLevel = "info"
	LevelWarn  LogLevel = "warn"
	LevelError LogLevel = "error"
	LevelFatal LogLevel = "fatal"
)

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger wraps zap.Logger with custom configuration and observability hooks. This is a pointer type (holds state, manages zap instance lifecycle).

func New

func New(cfg Config) (*Logger, error)

New creates a new Logger from the given configuration. Uses zap.Config + Build() pattern for proper initialization. Returns error if configuration is invalid.

Example:

log, err := logger.New(logger.Config{
    Level:  "debug",
    Format: "json",
    Writer: os.Stdout,
})
if err != nil {
    return err
}
defer log.Sync()  // CRITICAL: flush buffered logs
Example

ExampleNew demonstrates basic logger creation and usage.

package main

import (
	"os"

	"github.com/grhili/cd-operator/pkg/logger"
	"go.uber.org/zap"
)

func main() {
	// Create logger with JSON format (default for k8s)
	log, err := logger.New(logger.Config{
		Level:  "info",
		Format: "json",
		Writer: os.Stdout,
	})
	if err != nil {
		panic(err)
	}
	defer log.Sync()

	// Basic logging
	log.Info("application started", zap.String("service", "cd-operator"))
}
Example (TextFormat)

ExampleNew_textFormat demonstrates logger with human-readable text format.

package main

import (
	"os"

	"github.com/grhili/cd-operator/pkg/logger"
	"go.uber.org/zap"
)

func main() {
	// Create logger with text format for local development
	log, err := logger.New(logger.Config{
		Level:  "debug",
		Format: "text",
		Writer: os.Stdout,
	})
	if err != nil {
		panic(err)
	}
	defer log.Sync()

	// Debug logging
	log.Debug("processing request", zap.String("path", "/api/v1/health"))
}

func (*Logger) Debug

func (l *Logger) Debug(msg string, fields ...zap.Field)

Debug logs a debug-level message with typed fields. Only appears when log level is set to "debug".

func (*Logger) Error

func (l *Logger) Error(msg string, fields ...zap.Field)

Error logs an error-level message with typed fields. Always use zap.Error() field for actual error values.

Example

ExampleLogger_Error demonstrates error logging with context.

package main

import (
	"os"

	"github.com/grhili/cd-operator/pkg/logger"
	"go.uber.org/zap"
)

func main() {
	log, err := logger.New(logger.Config{
		Level:  "info",
		Format: "text",
		Writer: os.Stdout,
	})
	if err != nil {
		panic(err)
	}
	defer log.Sync()

	// Error logging includes caller information automatically
	log.Error("failed to sync application",
		zap.String("app", "my-app"),
		zap.String("namespace", "default"),
		zap.Error(err),
	)
}

func (*Logger) Fatal

func (l *Logger) Fatal(msg string, fields ...zap.Field)

Fatal logs a fatal-level message and exits with os.Exit(1). Use sparingly - only for unrecoverable errors.

func (*Logger) Info

func (l *Logger) Info(msg string, fields ...zap.Field)

Info logs an info-level message with typed fields. This is the default log level for production.

func (*Logger) Named

func (l *Logger) Named(name string) *Logger

Named creates a child logger with the given name. Use this to identify subsystems/components in logs. Returns a new Logger instance (doesn't modify the parent).

Example:

ghLogger := baseLogger.Named("github")
ghLogger.Info("api call")  // logs with {"logger":"github"}
Example

ExampleLogger_Named demonstrates creating named loggers for subsystems.

package main

import (
	"os"

	"github.com/grhili/cd-operator/pkg/logger"
)

func main() {
	log, err := logger.New(logger.Config{
		Level:  "info",
		Format: "json",
		Writer: os.Stdout,
	})
	if err != nil {
		panic(err)
	}
	defer log.Sync()

	// Create named loggers for different subsystems
	ghLogger := log.Named("github")
	argoLogger := log.Named("argocd")

	ghLogger.Info("fetching repository")
	argoLogger.Info("syncing application")
}

func (*Logger) Sugar

func (l *Logger) Sugar() *zap.SugaredLogger

Sugar returns a zap.SugaredLogger for use with APIs that need key-value logging. The sugared logger is slower but more convenient for variadic key-value pairs.

Example:

sugar := log.Sugar()
sugar.Infow("request processed", "method", "GET", "path", "/api/data")

func (*Logger) Sync

func (l *Logger) Sync() error

Sync flushes any buffered log entries. CRITICAL: Always defer this after creating a logger. Failing to call Sync() may result in lost logs.

func (*Logger) Warn

func (l *Logger) Warn(msg string, fields ...zap.Field)

Warn logs a warning-level message with typed fields. Use for recoverable errors or unexpected conditions.

func (*Logger) With

func (l *Logger) With(fields ...zap.Field) *Logger

With creates a child logger with the given fields attached. Use this for stable metadata that should appear in all subsequent logs. Returns a new Logger instance (doesn't modify the parent).

Example:

baseLogger := logger.New(cfg).With(
    zap.String("service", "cd-operator"),
    zap.String("version", "v1.0.0"),
)
Example

ExampleLogger_With demonstrates creating child loggers with context.

package main

import (
	"os"

	"github.com/grhili/cd-operator/pkg/logger"
	"go.uber.org/zap"
)

func main() {
	log, err := logger.New(logger.Config{
		Level:  "info",
		Format: "json",
		Writer: os.Stdout,
	})
	if err != nil {
		panic(err)
	}
	defer log.Sync()

	// Create child logger with service metadata
	serviceLog := log.With(
		zap.String("service", "cd-operator"),
		zap.String("version", "v1.0.0"),
	)

	// All logs from serviceLog will include service and version fields
	serviceLog.Info("service initialized")
	serviceLog.Info("processing webhook")
}

func (*Logger) Zap

func (l *Logger) Zap() *zap.Logger

Zap returns the underlying *zap.Logger for components that need it directly. Use this when passing the logger to components that expect *zap.Logger.

Example:

zapLogger := log.Zap()
component := NewComponent(zapLogger)

type MetadataField

type MetadataField string

MetadataField represents field keys that are considered metadata (not per-log data) and subject to progressive disclosure filtering.

const (
	FieldService  MetadataField = "service"
	FieldVersion  MetadataField = "version"
	FieldTraceID  MetadataField = "trace_id"
	FieldLogger   MetadataField = "logger"
	FieldCaller   MetadataField = "caller"
	FieldFunction MetadataField = "function"
	FieldLine     MetadataField = "line"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL