weave_log
WeaveLog
A Crystal logging library that wraps the stdlib Log with rich terminal output via si_el_hay and structured JSON for production.
Drop-in compatible with Crystal's Log -- same Log.for, same Log::Entry, same backends. WeaveLog just makes it prettier and more powerful.
Features
- Pretty terminal output with severity icons, colors, source padding
- JSON (NDJSON) formatter for production log aggregation
- Pattern-based formatting (
{timestamp} {severity} {message}) - File backend with size & time-based rotation + gzip compression
- Async backend with bounded queue and backpressure strategies
- Enrichers that auto-attach metadata (pid, hostname, fiber, environment)
- Backtrace buffer -- stores suppressed messages, dumps context on error
- Sampling and rate limiting per source
- Redaction of sensitive fields and regex patterns
- Dynamic runtime level control via signals
- Exception deduplication within time windows
- Syslog backend (RFC 5424, UDP/TCP)
- Log groups / sections with nested indentation
- Spec helpers with
capture_logsand Spectator matchers - Thread-safe (mutex-protected backends, atomic counters)
Installation
Add to your shard.yml:
dependencies:
weave_log:
github: alexanderadam/weave_log
Then run shards install.
Quick Start
require "weave_log"
# Development: pretty console output, debug level
WeaveLog.setup(:development)
log = Log.for("myapp")
log.info { "server started" }
log.debug &.emit("request", method: "GET", path: "/users", status: 200)
log.warn { "disk space low" }
log.error(exception: ex) { "request failed" }
# Production: JSON to stdout, info level
WeaveLog.setup(:production)
# From environment variables
# LOG_LEVEL=debug LOG_FORMAT=json LOG_SOURCES=myapp.*
WeaveLog.setup_from_env
Formatters
PrettyFormatter
Rich terminal output with icons, colors, and auto-padded source names.
fmt = WeaveLog::PrettyFormatter.new(
severity_display: :icon, # :icon, :label, or :both
show_timestamp: true,
timestamp_format: "%H:%M:%S.%3N",
colorize: :auto, # true, false, or :auto (detects $NO_COLOR)
max_data_inline: 5, # wrap data fields after N
exception_style: :full, # :full, :compact, or :message_only
box_fatal: true, # draw a box around fatal messages
terminal_wrap: true, # wrap long lines to terminal width
)
JSONFormatter
Single-line NDJSON for log aggregation (Datadog, Loki, ELK, etc.).
fmt = WeaveLog::JSONFormatter.new(
timestamp_format: :iso8601, # :iso8601, :epoch_ms, :epoch_s
data_style: :flat, # :flat merges into root, :nested under "data" key
include_context: true,
)
PatternFormatter
Template-based formatting using {token} syntax.
fmt = WeaveLog::PatternFormatter.new(
"{timestamp:%Y-%m-%d %H:%M:%S} [{severity:short}] {source} - {message} {data:json}"
)
Tokens: timestamp, severity (+ :icon, :short), message, source, data (+ :json), context, exception, pid, newline, fiber_id, fiber_name, hostname, env, caller, caller_file, caller_line.
RedactingFormatter
Wraps any formatter, scrubs sensitive data from output.
redactor = WeaveLog::Redactor.new(
patterns: [WeaveLog::RedactionPatterns::CREDIT_CARD, WeaveLog::RedactionPatterns::SSN]
)
fmt = WeaveLog::RedactingFormatter.new(
WeaveLog::PrettyFormatter.new,
redactor
)
Default redacted fields: password, secret, token, api_key, authorization, credential, etc.
Backends
File Backend
backend = WeaveLog::FileBackend.new(
path: "log/app.log",
formatter: WeaveLog::JSONFormatter.new,
flush_mode: :immediate, # :immediate, :buffered, :periodic
)
Rotating File Backend
backend = WeaveLog::RotatingFileBackend.new(
path: "log/app.log",
formatter: WeaveLog::JSONFormatter.new,
policy: :size, # :size, :daily, :hourly
max_size: 10_000_000, # 10MB
max_files: 5,
compress: true, # gzip old files in background
)
Async Backend
Non-blocking writes with bounded queue.
inner = WeaveLog::FileBackend.new(path: "log/app.log")
backend = WeaveLog::AsyncBackend.new(
inner,
queue_size: 1024,
strategy: :drop_newest, # :block, :drop_oldest, :drop_newest
)
Syslog Backend
RFC 5424 format via UDP or TCP.
backend = WeaveLog::SyslogBackend.new(
host: "127.0.0.1",
port: 514,
transport: :udp, # :udp or :tcp
facility: :local0,
app_name: "myapp",
)
Enrichers
Auto-attach metadata to every log entry.
WeaveLog.setup(:development, enrichers: [
WeaveLog::ProcessInfo.new, # pid, hostname
WeaveLog::FiberInfo.new, # fiber_id, fiber_name
WeaveLog::EnvironmentInfo.new, # crystal_env
WeaveLog::OpenTelemetry.new, # trace_id, span_id from context
WeaveLog::CallerInfo.new, # caller file:line (via wlog macro)
])
Caller Info (via macro)
log = Log.for("myapp")
WeaveLog.wlog(log, :info, "hello", user_id: 42) # auto-injects __FILE__, __LINE__
OpenTelemetry Context
Reads trace_id/span_id from Log.context (set by your middleware):
Log.with_context(trace_id: "abc", span_id: "def") do
log.info { "traced request" } # enricher adds trace_id, span_id to data
end
Advanced Features
Backtrace Buffer
Stores suppressed (below-level) messages in a ring buffer. Auto-dumps context when an error occurs.
buffer = WeaveLog::BacktraceBuffer.new(
inner: backend,
buffer_size: 64,
trigger_level: Log::Severity::Error,
)
Sampling
Log 1 in N messages per source.
sampled = WeaveLog::SamplingBackend.new(backend, ratio: 10) # 1 in 10
Rate Limiting
Cap messages per source per time window.
limited = WeaveLog::RateLimitedBackend.new(
backend,
max_per_window: 100,
window: 60.seconds,
)
Dynamic Level Control
Change log levels at runtime without restart.
WeaveLog::LevelControl.set_level("noisy.source", Log::Severity::Warn)
WeaveLog::LevelControl.reset_all
Signal handlers: SIGUSR1 cycles levels, SIGUSR2 dumps current config.
Exception Deduplication
Groups identical exceptions within a time window.
dedup = WeaveLog::DeduplicationBackend.new(
backend,
window: 30.seconds,
group_by: :class_and_message, # or :class_only
)
Stopwatch
Timed logging with automatic elapsed_ms.
sw = WeaveLog::Stopwatch.new("http.request")
# ... do work ...
sw.info &.emit("done", path: "/users") # includes elapsed_ms in data
# Block form
WeaveLog.timed("db.query", "SELECT users") { query.execute }
Span / Trace
Structured tracing a la Rust's tracing crate. Spans nest, share a trace_id, and auto-set Log.context so all messages within a span are correlated.
WeaveLog.span("http.request", "GET /users") do |span|
span.set(method: "GET", path: "/users", user_id: 42)
WeaveLog.span("db.query", "SELECT users") do |inner|
# inner span shares trace_id, gets its own span_id
inner.event("rows fetched", count: 150)
end
# produces: start event, nested span events, finish event with elapsed_ms
end
Exception handling is automatic -- if the block raises, the span records the error and re-raises:
WeaveLog.span("risky.op") do |span|
raise "oops" # span finishes with status=error, logs the exception
end
WeaveLog.current_span returns the active span (or nil). Works with the existing {trace_id} / {span_id} pattern tokens and the OpenTelemetry enricher.
Conditional Logging
Filter entries at the backend level -- no if/else at the call site.
# only log DB entries with slow queries
backend = WeaveLog::ConditionalBackend.new(inner) do |entry|
entry.data[:duration_ms]?.try(&.to_s.to_f64) { 0.0 } > 100.0
end
Pre-built conditions compose with AND/OR/NOT:
cond = WeaveLog::Conditions.all(
WeaveLog::Conditions.source("api.*"),
WeaveLog::Conditions.min_severity(Log::Severity::Warn),
)
backend = WeaveLog::ConditionalBackend.new(inner, &cond)
Available conditions: source, has_data, has_exception, min_severity, message_matches, all, any, not.
Log Groups / Sections
Nested, indented log sections with depth tracking.
WeaveLog.group("http.request") do
log.info { "processing request" } # indented 1 level in pretty output
WeaveLog.group("db.query") do
log.debug { "SELECT * FROM users" } # indented 2 levels
end
end
Groups automatically:
- Track fiber-local nesting depth
- Set
groupandgroup_depthinLog.contextfor structured output - Indent lines in
PrettyFormatterbased on depth - Log start/end events with group metadata
- Clean up on exceptions (ensure block)
WeaveLog.group_depth # => current nesting depth (0 at top level)
WeaveLog.current_group # => innermost group name, or nil
WeaveLog.group_indent # => indentation string for current depth
Testing
Spec Helpers
require "weave_log/spec_helpers"
# Capture log entries during a block
logs = WeaveLog.capture_logs do
Log.for("test").info { "hello" }
end
logs.has_entry?(severity: Log::Severity::Info, message: "hello") # => true
logs.entries_matching(source: "test") # => [...]
Memory Backend
backend = WeaveLog::MemoryBackend.new
# ... use in tests ...
backend.size # => 3
backend.last.message # => "last message"
backend.clear!
Configuration
Block Form
WeaveLog.setup do |c|
c.bind("myapp.*", Log::Severity::Debug, WeaveLog::PrettyBackend.new)
c.bind("db.*", Log::Severity::Info, WeaveLog::JSONBackend.new)
end
Environment Variables
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
Minimum severity level |
LOG_FORMAT |
pretty |
pretty, json, or pattern |
LOG_SOURCES |
* |
Comma-separated source patterns |
Migrating from stdlib Log
WeaveLog is a drop-in enhancement. Replace your backend setup:
# Before
Log.setup(:info, Log::IOBackend.new)
# After
require "weave_log"
WeaveLog.setup(:development)
# or
WeaveLog.setup(:production)
Your existing Log.for("source"), log.info { }, log.error(exception:) calls all work unchanged.
Performance Tuning
Choose the Right Backend Dispatch Mode
- Development: Use
Log::DispatchMode::Sync(orWeaveLog.setup(:development)) so output appears immediately. Slower, but you see logs the instant they happen. - Production: Use
Log::DispatchMode::Async(default forWeaveLog.setup(:production)) so logging doesn't block your request path.
Async Backend for File I/O
Wrap file backends in AsyncBackend to move disk writes off the hot path:
file = WeaveLog::FileBackend.new("app.log")
async = WeaveLog::AsyncBackend.new(file, queue_size: 4096, strategy: :drop_newest)
Choose a backpressure strategy:
:block-- safest, no log loss, but pauses caller when queue is full:drop_newest-- never blocks, drops new messages under pressure:drop_oldest-- never blocks, drops oldest queued messages
Buffered Writes
Use flush_mode: :buffered or :periodic for FileBackend to batch disk writes. Errors always flush immediately.
Sampling & Rate Limiting
For high-throughput sources, sample or rate-limit to reduce volume:
WeaveLog.setup(:production, sampling_ratio: 10) # log 1 in 10
WeaveLog.setup(:production, rate_limit: {max: 100, window: 60.seconds})
Formatter Choice
JSONFormatter is faster than PrettyFormatter since it skips ANSI escape codes and alignment tracking. In production, always use JSON.
Suppressed Messages
Messages below the configured level are cheap -- they don't allocate an IO or call the formatter. Crystal's Log blocks aren't even evaluated:
log.debug { expensive_computation } # block never called if level > debug
Benchmarks
Run the built-in benchmarks to measure throughput on your hardware:
crystal run src/weave_log/benchmark.cr --release
Development
shards install
crystal spec # run all 386 specs
crystal tool format src/ # check formatting
./bin/ameba # lint
Contributing
- Fork it (https://github.com/alexanderadam/weave_log/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT
Contributors
- Alexander Adam - creator and maintainer
weave_log
- 0
- 0
- 0
- 0
- 3
- about 3 hours ago
- April 1, 2026
MIT License
Wed, 01 Apr 2026 08:33:33 GMT