Documentation
¶
Overview ¶
Package logging provides slog handlers for structured logging with multi-destination support, NATS publishing, and graceful fallback behavior.
Overview ¶
The logging package implements slog.Handler interfaces that enable logs to be written to multiple destinations simultaneously (stdout, NATS JetStream, etc.). This supports the out-of-band logging pattern where logs are always available via NATS for real-time streaming, even when WebSocket connections are not established.
Quick Start ¶
Basic multi-handler setup:
// Create stdout handler
stdoutHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
// Create NATS handler for publishing to logs.> subjects
natsHandler := logging.NewNATSLogHandler(natsClient, logging.NATSLogHandlerConfig{
MinLevel: slog.LevelInfo,
ExcludeSources: []string{"flow-service.websocket"},
})
// Compose handlers
multiHandler := logging.NewMultiHandler(stdoutHandler, natsHandler)
logger := slog.New(multiHandler)
slog.SetDefault(logger)
Handlers ¶
MultiHandler:
Composes multiple slog.Handler instances, dispatching log records to all of them. If one handler fails, others continue processing (graceful degradation).
multi := logging.NewMultiHandler(handler1, handler2, handler3)
NATSLogHandler:
Publishes log records to NATS subjects in the format logs.{source}.{level}. Publishing is asynchronous to avoid blocking the logging chain.
natsHandler := logging.NewNATSLogHandler(natsClient, logging.NATSLogHandlerConfig{
MinLevel: slog.LevelDebug,
ExcludeSources: []string{"noisy-component"},
})
Source Extraction ¶
NATSLogHandler extracts the source identifier from log attributes with the following priority: source > component > service > "system".
// Log with explicit source
logger.With("source", "my-component").Info("Processing started")
// Published to: logs.my-component.INFO
// Log with component attribute
logger.With("component", "udp-input").Info("Packet received")
// Published to: logs.udp-input.INFO
// Log without source attributes
slog.Info("System message")
// Published to: logs.system.INFO
Source Filtering ¶
NATSLogHandler supports excluding sources from NATS publishing using prefix matching. This is useful for preventing log feedback loops (e.g., WebSocket worker logs being sent over WebSocket).
natsHandler := logging.NewNATSLogHandler(natsClient, logging.NATSLogHandlerConfig{
MinLevel: slog.LevelDebug,
ExcludeSources: []string{"flow-service.websocket"},
})
// This log goes to stdout only, not NATS:
logger.With("source", "flow-service.websocket.health").Info("Health check")
// This log goes to both stdout and NATS:
logger.With("source", "flow-service").Info("Flow started")
The prefix matching rule: excluding "flow-service.websocket" also excludes "flow-service.websocket.health", but NOT "flow-service" itself.
NATS Subject Pattern ¶
Logs are published to NATS subjects following this pattern:
logs.{source}.{level}
└── logs.udp-input.INFO
└── logs.graph-processor.ERROR
└── logs.system.WARN
A JetStream stream named "LOGS" should be configured with appropriate TTL and size limits to prevent storage issues:
Stream: LOGS Subjects: logs.> MaxAge: 1h (TTL) MaxBytes: 100MB Discard: DiscardOld
Log Entry Format ¶
Logs published to NATS are JSON-encoded:
{
"timestamp": "2024-01-15T10:30:00.123456789Z",
"level": "INFO",
"source": "udp-input",
"message": "Packet received",
"fields": {
"bytes": 1024,
"remote_addr": "192.168.1.1:5000"
}
}
Graceful Degradation ¶
The logging architecture is designed to never block or fail due to NATS issues:
- MultiHandler ignores errors from individual handlers
- NATSLogHandler publishes asynchronously (non-blocking)
- NATS publish errors are silently dropped
- Stdout logging always works regardless of NATS state
This ensures that logging continues to function even when NATS is temporarily unavailable or experiencing issues.
Thread Safety ¶
All handlers are safe for concurrent use:
- MultiHandler dispatches to handlers sequentially but is safe for concurrent calls
- NATSLogHandler uses atomic operations and goroutines for async publishing
- WithAttrs and WithGroup create new handler instances (immutable pattern)
Integration with WebSocket Status Stream ¶
This package is designed to work with the WebSocket status stream feature:
- Application logs are published to NATS via NATSLogHandler
- The LOGS JetStream stream stores logs with TTL
- WebSocket clients subscribe to logs.> subjects
- Real-time log streaming without slog interception timing issues
The exclude_sources configuration allows filtering out WebSocket worker logs to prevent feedback loops where log messages trigger more log messages.
Performance ¶
Benchmarks on M3 MacBook Pro:
- MultiHandler dispatch: ~50ns overhead per additional handler
- NATSLogHandler: ~100ns for async publish setup (publish itself is async)
- Combined (stdout + NATS): ~150ns per log call
At 10,000 logs/second, this adds ~1.5ms total overhead per second, which is negligible for most applications.
Memory:
- MultiHandler: O(n) where n is number of handlers
- NATSLogHandler: O(1) for handler, O(m) per log where m is attributes
- Async publish goroutines: Short-lived, minimal memory impact
Testing ¶
Both handlers have comprehensive test coverage:
go test -race ./pkg/logging
Tests verify:
- Handler composition and dispatch
- Source extraction priority
- Prefix-based source filtering
- Async publishing behavior
- WithAttrs and WithGroup immutability
Example: Complete Setup ¶
package main
import (
"log/slog"
"os"
"github.com/c360studio/semstreams/natsclient"
"github.com/c360studio/semstreams/pkg/logging"
)
func setupLogger(natsClient *natsclient.Client, level slog.Level) *slog.Logger {
// Create stdout handler
stdoutHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})
// Create NATS handler with source filtering
natsHandler := logging.NewNATSLogHandler(natsClient, logging.NATSLogHandlerConfig{
MinLevel: level,
ExcludeSources: []string{"flow-service.websocket"},
})
// Compose handlers
multiHandler := logging.NewMultiHandler(stdoutHandler, natsHandler)
return slog.New(multiHandler)
}
func main() {
natsClient, _ := natsclient.NewClient("nats://localhost:4222")
defer natsClient.Close()
logger := setupLogger(natsClient, slog.LevelInfo)
slog.SetDefault(logger)
// Logs go to both stdout and NATS
slog.Info("Application started", "version", "1.0.0")
// Component-tagged logs
componentLogger := slog.With("component", "processor")
componentLogger.Info("Processing started")
}
See Also ¶
- service package: WebSocket status stream that consumes NATS logs
- config package: LOGS stream configuration with TTL and size limits
- natsclient package: NATS client used for publishing
Package logging provides slog handlers for structured logging across the application.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type MultiHandler ¶
type MultiHandler struct {
// contains filtered or unexported fields
}
MultiHandler composes multiple slog.Handler instances, dispatching log records to all of them. This allows logs to be written to multiple destinations (e.g., stdout and NATS) simultaneously.
func NewMultiHandler ¶
func NewMultiHandler(handlers ...slog.Handler) *MultiHandler
NewMultiHandler creates a new MultiHandler that dispatches to all provided handlers.
func (*MultiHandler) Enabled ¶
Enabled reports whether any handler handles records at the given level.
func (*MultiHandler) Handle ¶
Handle dispatches the record to all handlers. If a handler fails, we continue to the next handler (don't fail the logging chain).
type NATSLogHandler ¶
type NATSLogHandler struct {
// contains filtered or unexported fields
}
NATSLogHandler is an slog.Handler that publishes log records to NATS JetStream. Logs are published to subjects in the format: logs.{level}.{source} This enables NATS wildcard filtering:
- logs.WARN.> (all WARN and above)
- logs.*.graph-processor (one component, all levels)
The handler requires a non-nil publisher at construction time. Create the handler AFTER NATS is connected and streams are created.
func NewNATSLogHandler ¶
func NewNATSLogHandler(publisher NATSPublisher, cfg NATSLogHandlerConfig) *NATSLogHandler
NewNATSLogHandler creates a new NATSLogHandler. The publisher must be non-nil - create this handler AFTER NATS is connected.
func (*NATSLogHandler) Enabled ¶
Enabled reports whether the handler handles records at the given level.
type NATSLogHandlerConfig ¶
NATSLogHandlerConfig holds configuration for NATSLogHandler.
type NATSPublisher ¶
type NATSPublisher interface {
PublishToStream(ctx context.Context, subject string, data []byte) error
}
NATSPublisher defines the interface needed for publishing to NATS JetStream. Uses PublishToStream for durability - logs are persisted to JetStream LOGS stream. This allows for easier testing with mocks.