Trifle::Traces
Structured execution tracing for Ruby. Track timestamped outputs from background jobs, API integrations, and anything else that runs in a black box. Know exactly what happened, when, and in what order.
Part of the Trifle ecosystem.
Also available as Trifle.Traces for Elixir, with the same 2.0 persistence format.
Quick Start
gem 'trifle-traces'Trifle::Traces.tracer = Trifle::Traces::Tracer::Hash.new(key: 'jobs/sync')
Trifle::Traces.trace('Starting sync')
result = Trifle::Traces.trace('Fetching records from API') { api.fetch_all }
Trifle::Traces.trace('Sync complete')
Trifle::Traces.tracer.wrapupEvery message becomes a structured timeline entry on the tracer:
Trifle::Traces.tracer.data
#=> [
# {at: 1739700000, message: 'Starting sync', state: :success, type: :text, level: 0},
# {at: 1739700001, message: 'Fetching records from API', state: :success, type: :text, level: 0},
# {at: 1739700003, message: 'Sync complete', state: :success, type: :text, level: 0}
# ]Row at timestamps are Unix seconds with a fractional part, preserving subsecond
precision. Existing integer-second payloads remain readable.
Ideal for debugging those background-job-that-talks-to-API-and-works-every-time-when-you-run-it-manually-but-never-in-production type of jobs.
Persistence (v2.0)
Traces persist through two pluggable drivers: an index driver (searchable trace metadata) and a data driver (the payload). Configure both and the gem handles the whole lifecycle — no hand-written persistence callbacks.
Trifle::Traces.configure do |config|
config.index_driver = Trifle::Traces::Driver::Index::Mongo.new(
mongo_client, collection_name: 'trifle_traces'
)
config.data_driver = Trifle::Traces::Driver::Data::S3.new(
client: s3_client, buckets: %w[traces-a traces-b], gzip: true
)
config.context = ->(tracer) { { tenant_id: tracer.meta&.first } } # extra index fields
config.retention = ->(tracer) { 3 } # days, value or callable
end-
Index drivers:
Postgres(shared schema with the Elixir implementation),Mongo(production-proven at 100M+ traces/day),Memory,Null. ClickHouse/OpenSearch are planned — seelib/trifle/traces/driver/README.mdto write your own. -
Data drivers:
S3(any S3-compatible storage, multi-bucket sharding, gzip),File,Memory,Null. -
Retention: carried on every record (
retentiondays +expires_at). Mongo expires metadata via TTL index; Postgres providescleanup!for a scheduled job; S3 expires payloads via one lifecycle rule per retention class (Driver::Data::S3.setup!creates them). - Without configured drivers nothing persists — data stays on the tracer for your callbacks, as in 1.x.
S3 selects a bucket once per trace and persists its name as bucket_name in the
index. Reads, writes, and deletes use that name directly, so changing the bucket
list does not redirect existing traces. File, Memory, and Null store nil.
Automated persistence removes uploaded artifact source files after final payload
and index writes succeed at wrapup, before wrapup callbacks. Live uploads and
failed wrapups retain files until then. Pass cleanup: false to
Trifle::Traces.artifact(name, path, cleanup: false) to keep a reusable source.
Callback-only setups, Null data drivers, and direct driver writes retain sources.
Cleanup errors warn; termination before wrapup can leave files behind.
Postgres keeps the client injectable, so the gem still has no runtime
dependencies. Add pg to the host application, provision the table once, and
schedule cleanup for expired metadata:
require 'pg'
client = PG.connect(ENV.fetch('DATABASE_URL'))
Trifle::Traces::Driver::Index::Postgres.setup!(client)
Trifle::Traces.configure do |config|
config.index_driver = Trifle::Traces::Driver::Index::Postgres.new(client)
end
# Run periodically from your scheduler:
Trifle::Traces.default.index_driver.cleanup!Write modes
Every liftoff/bump/wrapup writes to the index in :live mode (default),
giving you live progress. For fast, line-heavy, high-volume jobs use
:deferred — zero I/O until wrapup, then exactly one index write and one
payload part per trace:
class Commodity::PollJob
include Sidekiq::Job
sidekiq_options tracer_key: 'commodity/poll', tracer_mode: :deferred
endDeferred mode skips liftoff and bump callbacks as well as persistence. Only
wrapup callbacks run. When a queued Sidekiq payload has no tracer_mode, the
middleware uses the worker class's current option, then config.default_mode.
An explicit mode in the payload takes priority.
Reading traces back
record = Trifle::Traces.find(reference) #=> TraceRecord with duration and counters
result = Trifle::Traces.search(
segment: 'jobs/sync',
tags: { any: ['tenant:42', 'tenant:43'], all: ['failed'] },
state: :error,
from: Time.utc(2026, 8, 30),
to: Time.utc(2026, 8, 31),
duration_min: 5_000,
limit: 50,
cursor: nil
)
entries = Trifle::Traces.payload(record) # all parts, in orderduration is elapsed monotonic time in integer milliseconds. counters
contains totals by entry state and type plus max_level. Search selects traces
that started in the half-open interval first_at >= from and first_at < to;
duration_min is inclusive. Tag groups mean (any[0] OR any[1] ...) AND all[0] AND all[1] ...; empty groups are ignored.
Search is intentionally narrow — materialized key-path segment, explicit tag groups, exact state, start-time range and minimum duration, newest-first with opaque cursor pagination. That is what stays fast at hundreds of millions of traces.
Activity Stats
Install and require trifle-stats, configure a Stats instance, and pass it to
Traces to enable native activity tracking:
require 'trifle/stats'
stats_config = Trifle::Stats::Configuration.new
stats_config.driver = Trifle::Stats::Driver::Mongo.new(
mongo_client, collection_name: 'trifle_traces_stats', system_tracking: true
)
stats_config.granularities = %w[10m 1h 1d]
stats_config.buffer_enabled = false
Trifle::Traces.configure do |config|
config.stats_config = stats_config
endstats_config defaults to nil; global Stats is not used implicitly. The
supplied configuration owns storage, granularities, timezone and buffering.
Provision the Stats driver's indexes once before starting workers.
Traces tracks the full trace key once after successful wrapup, before user
callbacks, for live and deferred traces. Values are count, states.<state>,
entries.count, and duration count/sum/square samples in milliseconds, including
per-state samples. This is the same activity format used by Elixir and Trifle
App. Final record metadata needs no index query. Ignored traces and failed
final writes emit no metrics; Stats errors warn without failing the trace.
User callbacks remain independent. Tracking also works without persistence,
preserving callback data. See the
configuration docs.
Callbacks
Live traces fire :liftoff, :bump and :wrapup callbacks. Deferred traces
only fire :wrapup. Use callbacks for side effects like emitting metrics
(persistence no longer belongs here):
Trifle::Traces.configure do |config|
config.on(:wrapup) do |tracer|
tracer.keys.each do |key|
Trifle::Stats.track(key: "traces::#{key}", at: Time.now, values: { count: 1 })
end
end
endWith persistence configured, tracer.trace_record exposes the dispatcher's
in-memory record. After final writes, wrapup callbacks can read its duration
(milliseconds), length, counters, tags, and bucket name without another index
query. Treat this record as read-only; payload entries still come from
Trifle::Traces.payload(record).
Upgrading from 1.x
- The return value of
:liftoffcallbacks no longer becomestracer.reference— the index driver generates references. Move persistence out of callbacks into drivers (or implement a custom driver; seelib/trifle/traces/driver/README.md). - Persistence errors are no longer silent: liftoff/wrapup failures raise, bump failures re-queue the data and retry on the next flush. Override
config.error_handlerto customize.
Features
- Simple tracing. Collect messages and return values from code execution.
- Driver-based persistence. Postgres or Mongo metadata + S3/File payloads out of the box; contracts for custom backends.
- Deferred mode. One write per trace for high-volume jobs (100M/day proven).
- State management. Automatic success/error state tracking.
- Callbacks. Hook into trace events for custom processing.
- Middleware integration. Built-in support for Rack, Rails, and Sidekiq.
- Thread-safe. Safe for concurrent execution.
- Zero runtime dependencies. Database/storage clients are injected.
Middleware
Trifle::Traces provides middleware for popular frameworks:
- Rack. HTTP request tracing.
- Rails. Controller and view tracing.
-
Sidekiq. Background job tracing (
sidekiq_options tracer_key:, tracer_mode:).
Documentation
Full guides and API reference at docs.trifle.io/trifle-traces
Trifle Ecosystem
| Component | What it does |
|---|---|
| Trifle App | Dashboards, alerts, scheduled reports, AI-powered chat. |
| Trifle::Stats | Time-series metrics for Ruby (Postgres, Redis, MongoDB, MySQL, SQLite). |
| Trifle.Traces (Elixir) | Elixir implementation with compatible Postgres, Mongo, and File/S3 persistence. |
| Trifle CLI | Terminal access to metrics. MCP server mode for AI agents. |
| Trifle::Logs | File-based log storage with ripgrep-powered search. |
| Trifle::Docs | Map a folder of Markdown files to documentation URLs. |
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/trifle-io/trifle-traces.
License
The gem is available as open source under the terms of the MIT License.