github.com/dsisnero/tracing.cr
0.5.2 / published Aug 26, 2026 / repository
structured, event based diagnostics for Crystal programs
tracing
A Crystal port of tokio-rs/tracing: structured, event-based diagnostics for Crystal programs.
- Current version:
0.5.2 - Upstream pin:
tracing-0.1.44(2d55f6f) - Current status: core facade, subscriber stack, appender, flame, log bridge,
mock, error/span trace, attributes, and concurrency helpers are shipped in
src/. The OpenTelemetry bridge now lives in the optional companion shardtracing-opentelemetry.
Documentation
| Document | Purpose |
|---|---|
| Architecture | Runtime structure, data flow, public subsystems |
| Development | Setup, source tree, local workflow |
| Coding Guidelines | Porting and Crystal style rules |
| Testing | Quality gates and test organization |
| PR Workflow | Review checklist and branch/commit conventions |
| Changelog | Release history |
| Parity Status | Shipped feature ledger vs upstream |
Installation
dependencies:
tracing:
github: dsisnero/tracing.cr
shards install
Optional OpenTelemetry support is provided by the sibling shard:
dependencies:
tracing:
github: dsisnero/tracing.cr
branch: codex-tracing-opentelemetry-split
tracing-opentelemetry:
github: dsisnero/tracing-opentelemetry
Quick Start
require "tracing"
Tracing.fmt
.compact
.with_target(true)
.with_max_level(Tracing::LevelFilter::INFO)
.init
span!(Tracing::Level::INFO, "request", method: "GET").in_scope do
info!("request.started", user_id: 42)
end
Public Surface
The current entrypoint is src/tracing.cr. It requires and re-exports:
src/tracing/core/for metadata, fields, spans, events, subscribers, and dispatchsrc/tracing/facade files forTracing.span,Tracing.event,span!,info!, andTracing::Spansrc/tracing/subscriber/for registry, layers, filters, formatting, reload, appenders, flame, log bridge, mock subscriber, andSpanTracesrc/tracing/concurrency*for fiber and channel helpers
Core Concepts
Spans
Spans represent work with duration and context.
span = span!(Tracing::Level::INFO, "request", method: "GET")
span.in_scope do
span.record(path: "/users")
info!("request.authenticated", user: "alice")
end
Events
Events are point-in-time records, optionally attached to the current span.
info!("boot", port: 8080)
span!(Tracing::Level::DEBUG, "db_query").in_scope do
debug!("query.executed", rows: 100, duration_ms: 12)
end
Subscribers and Layers
Tracing::Registry stores span state. Tracing::Layer implementations observe
that state and render or export it.
registry = Tracing::Registry.default
.with(Tracing::FmtLayer.new(STDOUT).compact)
.with(Tracing::EnvFilter.new("info,my_app=debug"))
registry.init
Formatting
The formatting layer lives at src/tracing/subscriber/fmt.cr. There are two common entrypoints:
Tracing.fmtfor the builder APITracing::FmtLayer.new(...)for direct layer composition
Builder API
Tracing.fmt
.pretty
.with_target(true)
.with_thread_ids(true)
.with_max_level(Tracing::LevelFilter::DEBUG)
.init
JSON Output
Tracing.fmt
.json
.flatten_event(true)
.with_current_span(true)
.with_span_list(true)
.init
Current JSON controls:
flatten_event(true)moves event fields to the root objectwith_current_span(false)omits the current span namewith_span_list(false)omits the root-to-leaf span list
Filtering
The current filter surface lives under src/tracing/subscriber/.
# Level threshold
fmt = Tracing::FmtLayer.new(STDOUT).with_filter(Tracing::LevelFilter::INFO)
# Environment grammar
env = Tracing::EnvFilter.new("info,my_app=debug,my_app[db]=trace")
# Field-value matching (target[span{field=val}]=level)
field_filter = Tracing::EnvFilter.new("my_app[db{query=auth}]=debug")
# Closure-based
warn_only = Tracing::FilterFn.new { |meta| meta.level <= Tracing::Level::WARN }
# Programmatic targets
targets = Tracing::Targets.new
.with_target("my_app", Tracing::Level::DEBUG)
.with_default(Tracing::Level::INFO)
Filter combinators are also shipped:
combined = targets.and(env.not)
Runtime Reloading
The reload layer is implemented in src/tracing/subscriber/reload.cr.
builder = Tracing.fmt
.with_max_level(Tracing::LevelFilter::INFO)
.with_filter_reloading
builder.init
handle = builder.reload_handle
handle.reload(Tracing::LevelFilterLayer.new(Tracing::LevelFilter::DEBUG))
Crystal Log Bridge
Forward Crystal Log entries into tracing with
src/tracing/subscriber/log_tracer.cr.
Tracing.fmt.compact.init
Log.setup(:trace, Tracing::LogTracer.new)
Log.info { "routed to tracing" }
Level conversion between tracing and ::Log is available in
src/tracing/log.cr:
# Level → ::Log::Severity
Tracing::Log.level_as_log(Tracing::Level::WARN) # => ::Log::Severity::Warn
# LevelFilter → ::Log::Severity
Tracing::Log.level_filter_as_log(Tracing::LevelFilter.off) # => ::Log::Severity::None
# ::Log::Severity → Level
Tracing::Log.severity_as_trace(::Log::Severity::Notice) # => Tracing::Level::INFO
Non-Blocking Output and Rotation
Appender support lives in src/tracing/subscriber/appender.cr.
appender = Tracing::RollingFileAppender
.builder
.rotation(Tracing::Rotation::DAILY)
.filename_prefix("app")
.filename_suffix("log")
.build("logs")
non_blocking, guard = Tracing::NonBlocking.new(appender)
Tracing::Registry.default
.with(Tracing::FmtLayer.make_writer { non_blocking.make_writer }.compact)
.init
Keep guard alive until shutdown so the worker can flush buffered writes.
Current appender features:
NonBlockingworker fiber +WorkerGuardRollingFileAppender- builder support for
rotation,filename_prefix,filename_suffix,max_log_files
Flame Output
Tracing::FlameLayer is ported in src/tracing/subscriber/flame.cr.
It writes folded stack output for external tools such as inferno-flamegraph.
flame, guard = Tracing::FlameLayer.with_file("trace.folded")
Tracing::Registry.default.with(flame).init
# ... run app ...
# cat trace.folded | inferno-flamegraph > flame.svg
This is flamegraph/flamechart data, not Chrome trace format.
Keep guard alive until shutdown so remaining span samples are flushed.
OpenTelemetry
The OpenTelemetry bridge moved into the optional companion shard
tracing-opentelemetry. Install it alongside tracing, then require it
explicitly:
require "tracing"
require "tracing-opentelemetry"
exporter = OpenTelemetry::Exporter.new(:io, io: STDOUT)
provider = OpenTelemetry::TraceProvider.new(
service_name: "my_app",
exporter: exporter
)
Tracing::Registry.default
.with(
Tracing::OpenTelemetryLayer.new(provider)
.with_level(Tracing::Level::INFO)
.with_context_activation(true)
.with_target(true)
)
.init
span!(Tracing::Level::INFO, "request").in_scope do
info!("request.started", user: "alice")
end
Current OTel behavior:
- root and child spans export on span close
- contextual events become OTel span events
otel.name,otel.kind,otel.status_code, andotel.status_descriptionare mapped from tracing fields- error events can update span status and emit exception-style attributes
- context activation tracks the current trace/span on the active fiber
Note: those OTel override fields are read as dotted keys such as otel.kind.
The current Crystal facade is ergonomic for identifier-style named fields; if you
need dotted override keys today, inject them through lower-level field/value
construction rather than plain named args.
Concurrency Helpers
Concurrency helpers are split across src/tracing/concurrency*.
require "tracing/concurrency"
require "tracing/concurrency/channel_ext"
done = Tracing::Concurrency.spawn(name: "worker", job: "reindex") do
info!("worker.started")
42
end
result = done.receive
channel = Channel(String).new
traced = channel.traced("jobs")
Shipped helpers:
Tracing::Concurrency.spawnTracing::Concurrency.spawn_with_spanTracing::Concurrency.with_subscriberFiber.spawn_tracedChannel#tracedTracing::Concurrency::TracedChannel
Instrumentation Helpers
@[Tracing::Instrument]
def process(id : Int32)
Tracing.instrument("process", id: id) do
info!("process.started")
end
end
Development
shards install
crystal tool format --check src spec
ameba src spec
crystal spec
The current suite contains 314 examples in spec/tracing_spec.cr.
License
MIT — see LICENSE
API
- Channel(T)
A
Channelenables concurrent communication between fibers. - Fiber
A
Fiberis a light-weight execution unit managed by the Crystal runtime. - FiberWithResult(T)
- Tracing