tracing

Version, currently 0.5.04 versions

github.com/dsisnero/tracing.cr

structured, event based diagnostics for Crystal programs

0 stars
2 dependents
License: MIT

Nothing has been indexed for 0.5.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.

Installation

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

Then run:

shards install

shard.yml

No shard.yml has been indexed for 0.5.0. You can read it on the repository.

Dependencies

Unknown: the shard.yml for this version has not been read yet.

README

This README is the one indexed from the repository at its latest ref, not from the tag for this version.

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