Logging
> Log messages with Velocity's driver-based logging system supporting console, file, daily rotation, stack fan-out, and secret redaction.
Velocity provides a driver-based logging system that the framework wires up for you
from environment configuration when you boot an app with velocity.New().
Quick Start
velocity.New() reads the LOG_* environment variables, builds a
logger via log.NewLogger, and exposes it as app.Log. Inside a request handler the
same logger is available as c.Log(). No manual setup is required for the common case.import "github.com/velocitykode/velocity"
func main() {
app, err := velocity.New()
if err != nil {
panic(err)
}
// app.Log is a contract.Logger wired from your LOG_* env vars.
app.Log.Info("Application started")
app.Log.Debug("Debugging information")
app.Log.Warn("Warning message")
app.Log.Error("An error occurred")
}// Structured logging with key-value pairs. Every Logger method takes a
// message followed by alternating key/value arguments.
app.Log.Info("User logged in",
"user_id", 123,
"email", "user@example.com",
"ip_address", "192.168.1.1",
)
app.Log.Info("Order processed",
"order_id", "ORD-123",
"amount", 99.99,
"currency", "USD",
)func processOrder(logger contract.Logger, orderID string) error {
logger.Info("Processing order", "order_id", orderID)
if err := validateOrder(orderID); err != nil {
logger.Error("Order validation failed",
"order_id", orderID,
"error", err,
)
return err
}
logger.Info("Order processed successfully", "order_id", orderID)
return nil
}The logger satisfies the contract.Logger interface, so any code that accepts a
contract.Logger can log without importing the log package directly:
import "github.com/velocitykode/velocity/contract"
type Logger interface {
Debug(msg string, kvs ...any)
Info(msg string, kvs ...any)
Warn(msg string, kvs ...any)
Error(msg string, kvs ...any)
Fatal(msg string, kvs ...any)
}Configuration
velocity.New() builds the logger from these environment variables:
# Driver selection
LOG_DRIVER=console # Options: console, file, daily, stack, null (default: console)
# File / daily driver settings
LOG_PATH=./storage/logs # Directory for log files
LOG_DAYS=14 # Retention in days for rotated files (default: 14)
# Stack driver (comma-separated channel names)
LOG_STACK=console,file
# General settings
LOG_LEVEL=debug # Minimum log level (default: debug)
# Optional: redact secrets/PII before they hit any sink
LOG_REDACT=trueThese map onto a log.LogConfig, which you can also construct by hand:
import "github.com/velocitykode/velocity/log"
cfg := log.LogConfig{
Driver: "file",
Config: map[string]any{
"path": "./storage/logs",
"days": 14,
"level": "debug",
},
}
logger, err := log.NewLogger(cfg)log.NewLogger resolves the driver through Velocity’s driver registry. An empty
Driver defaults to "console", so a zero-value LogConfig still produces a working
logger.
Driver imports: the console and null drivers register themselves whenever the
log package is imported. The file, daily, and stack drivers live in leaf
packages and must be enabled with a blank import:
import (
_ "github.com/velocitykode/velocity/log/file" // file + daily
_ "github.com/velocitykode/velocity/log/stack" // stack
)Or pull in all built-in drivers at once with the aggregator:
import _ "github.com/velocitykode/velocity/log/standard"Drivers
Console Driver
The console driver outputs formatted logs to stdout:
[09:42:06] INFO: Application started
[09:42:07] DEBUG: Processing request | method=GET path=/api/users
[09:42:08] ERROR: Database connection failed | error=connection timeoutYou can construct one directly when you need a logger outside the framework:
import "github.com/velocitykode/velocity/log/drivers"
// level is the minimum severity: 0=debug, 1=info, 2=warn, 3=error, 4=fatal.
logger := drivers.NewConsoleLogger(0)File and Daily Drivers
The file and daily drivers both write to daily-rotating files (they share a
factory, so daily is an alias):
- Location: Configured via
LOG_PATH/ thepathoption. - Format:
velocity-YYYY-MM-DD.log - Rotation: Automatic daily rotation; retention is controlled by
LOG_DAYS/ thedaysoption (0keeps files forever). - Permissions: Files are created
0o600inside a0o700directory by default. - Thread-safe: Concurrent writes are serialized; opt into cross-process advisory locking when multiple instances share a log directory.
Example file output:
[2024-01-15 09:42:06.000] INFO: Application started
[2024-01-15 09:42:07.001] DEBUG: Processing request | method=GET path=/api/users
[2024-01-15 09:42:08.002] ERROR: Database connection failed | error=connection timeoutConstructing a file logger directly exposes the rotation and permission options:
import "github.com/velocitykode/velocity/log/file"
// path, days (retention), level, then optional functional options.
logger := file.NewFileLogger("./storage/logs", 14, 0,
file.WithFileMode(0o640), // override the default 0o600
file.WithFileLock(), // advisory flock for shared log dirs
)Stack Driver
The stack driver fans every message out to several child channels at once. Configure
it through the stack option (a list of channel names) or the LOG_STACK env var:
cfg := log.LogConfig{
Driver: "stack",
Config: map[string]any{
"stack": []string{"console", "file"},
"path": "./storage/logs",
},
}You can also build one directly from existing loggers:
stack := log.NewStackLogger(consoleLogger, fileLogger)Null Driver
For testing or to disable a channel, use the null driver, which discards everything:
logger, _ := log.NewLogger(log.LogConfig{Driver: "null"})
// or directly:
logger := log.NewNullLogger()Advanced Usage
Manager for Multiple Channels
For applications that need several independently-configured channels, use the
Manager. Channel configuration lives in log.LoggingConfig / log.ChannelConfig:
import "github.com/velocitykode/velocity/log"
func setupLogging() {
cfg := log.LoggingConfig{
Default: "file",
Channels: map[string]log.ChannelConfig{
"file": {
Driver: "file",
Path: "./storage/logs",
MaxAge: 14, // retention in days
},
"audit": {
Driver: "file",
Path: "./storage/audit",
},
"errors": {
Driver: "file",
Path: "./storage/errors",
Level: "error",
},
"console": {
Driver: "console",
},
},
}
manager := log.NewManager(cfg)
// Get specific channels (created lazily, thread-safe).
auditLog, _ := manager.Channel("audit")
errorLog, _ := manager.Channel("errors")
// The default channel is available via Default().
defaultLog, _ := manager.Default()
_ = defaultLog
// Use different loggers for different purposes.
auditLog.Info("User action", "action", "delete", "user_id", 123)
errorLog.Error("Critical error", "component", "payment")
}A manager channel can itself be a stack that references other configured channels:
cfg := log.LoggingConfig{
Default: "stack",
Channels: map[string]log.ChannelConfig{
"console": {Driver: "console"},
"file": {Driver: "file", Path: "./storage/logs"},
"stack": {
Driver: "stack",
Options: map[string]any{
"channels": []string{"console", "file"},
},
},
},
}Redaction
Velocity can strip secrets and PII from every log line before it reaches a sink. The redaction layer runs against the message and each key/value, across all drivers.
Turn it on per channel with the redact option, supply your own chain with the
redactors option, or flip it on process-wide with LOG_REDACT=true:
cfg := log.LogConfig{
Driver: "console",
Config: map[string]any{"redact": true},
}The default chain (log.BuildDefaultRedactors()) covers Authorization-style
headers, JWTs, and card numbers (PAN). Email-address redaction is opt-in: set
LOG_REDACT_EMAILS=true and BuildDefaultRedactors() adds the email redactor.
You can wrap any logger with a custom chain:
import "github.com/velocitykode/velocity/log"
redacted := log.WithRedactors(inner,
log.HeaderRedactor(),
log.JWTRedactor(),
log.RedactorFunc(func(s string) string {
// custom rule
return s
}),
)Testing
When testing, use the null driver to silence output:
func TestMyFunction(t *testing.T) {
logger, _ := log.NewLogger(log.LogConfig{Driver: "null"})
// Pass the logger into the code under test; it produces no output.
MyFunction(logger)
}Best Practices
- Use structured logging: Pass key-value pairs for better searchability.
- Choose appropriate levels: Debug for development, Info for production.
- Configure per environment: Use different drivers for dev/staging/production.
- Rotate logs: The file/daily drivers handle this automatically via
LOG_DAYS. - Redact secrets: Enable redaction in compliance-sensitive environments.
Examples
HTTP Middleware
Inside the router, the request-scoped logger is available via c.Log():
import (
"time"
"github.com/velocitykode/velocity/router"
)
func LoggingMiddleware(next router.HandlerFunc) router.HandlerFunc {
return func(c *router.Context) error {
start := time.Now()
c.Log().Info("Request started",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"ip", c.Request.RemoteAddr,
)
// Call the next handler.
err := next(c)
c.Log().Info("Request completed",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"duration", time.Since(start),
)
return err
}
}Error Handling
func ProcessPayment(logger contract.Logger, amount float64) error {
logger.Debug("Processing payment", "amount", amount)
err := chargeCard(amount)
if err != nil {
logger.Error("Payment failed",
"amount", amount,
"error", err,
"timestamp", time.Now(),
)
return err
}
logger.Info("Payment successful", "amount", amount)
return nil
}