tracing

Version, currently 0.5.24 versions

github.com/dsisnero/tracing.cr

structured, event based diagnostics for Crystal programs

0 stars
2 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  tracing:
    github: dsisnero/tracing.cr
    version: ~> 0.5.2

Then run:

shards install

shard.yml

Crystal
>= 1.20.2
License
MIT
Author
Dominic Sisneros

Dependencies

Runtime Dependencies

Development Dependencies

  • ameba~> 1.6github: crystal-ameba/amebadev
  • perf_tools*github: crystal-lang/perf-toolsdev

README

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 shard tracing-opentelemetry.

Documentation

DocumentPurpose
ArchitectureRuntime structure, data flow, public subsystems
DevelopmentSetup, source tree, local workflow
Coding GuidelinesPorting and Crystal style rules
TestingQuality gates and test organization
PR WorkflowReview checklist and branch/commit conventions
ChangelogRelease history
Parity StatusShipped 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 dispatch
  • src/tracing/ facade files for Tracing.span, Tracing.event, span!, info!, and Tracing::Span
  • src/tracing/subscriber/ for registry, layers, filters, formatting, reload, appenders, flame, log bridge, mock subscriber, and SpanTrace
  • src/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.fmt for the builder API
  • Tracing::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 object
  • with_current_span(false) omits the current span name
  • with_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:

  • NonBlocking worker fiber + WorkerGuard
  • RollingFileAppender
  • 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, and otel.status_description are 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.spawn
  • Tracing::Concurrency.spawn_with_span
  • Tracing::Concurrency.with_subscriber
  • Fiber.spawn_traced
  • Channel#traced
  • Tracing::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