foam-otel
The shared foam OpenTelemetry core for Ruby services — traces, logs, and
metrics over OTLP HTTP, implementing the fleet wire contract
(contract/SPEC.md). Verified end-to-end by the
test-apps/ruby-rails conformance gate on every PR.
# Gemfile
gem "foam-otel"
# config/initializers/foam.rb (or before Rails loads)
Foam::Otel.init(service_name: "my-service")
That's the whole integration. Everything else self-wires by presence detection (zero customer effort):
- Rails: railtie inserts the Rack middleware at position 0, captures
controller exceptions that
rescue_fromswallows (viaprocess_action.action_controller), and broadcastsRails.loggerlines to foam aslog.railsrecords — the app's own logger chain is never touched. - Sidekiq: server middleware auto-registers; one
sidekiq.consume <queue>consumer span per job, failures re-raised identically (retries untouched). - Datadog (SPEC §7.3): foam spans and logs carry
dd.trace_id/dd.span_idverbatim from the DD correlation API when a DD trace is active. Foam never patches, parents from, or reconfigures the DD tracer. - Rollbar (SPEC §12): exceptions the app reports to Rollbar (e.g. inside
rescue_fromhandlers) are also recorded on the active foam span — Rollbar itself is untouched, and each exception records at most once per span.
| option | default | |
|---|---|---|
service_name: |
(required) | resource service.name |
token: |
FOAM_OTEL_TOKEN env |
missing → warn + inert |
endpoint: |
OTEL_EXPORTER_OTLP_ENDPOINT env → https://otel.api.foam.ai |
|
force_export: |
false |
export outside production |
redact_contact_info: |
false |
append email/phone/telefono/celular to redaction |
redact_keys: |
[] |
extra redacted keys |
health_routes: |
[] |
routes exempted from request-context capture (none by default — health checks are captured) |
sidekiq: / rollbar: / rails: |
true |
opt out of auto-wiring that adapter |
Export is production-gated (RAILS_ENV/RACK_ENV/ENVIRONMENT/ENV/
NODE_ENV ∈ prod) unless force_export. Batch cadence comes
from the standard OTel env vars (OTEL_BSP_SCHEDULE_DELAY,
OTEL_BLRP_SCHEDULE_DELAY, OTEL_METRIC_EXPORT_INTERVAL).
Route names: span names and http.route use Rails' route template
(action_dispatch.route_uri_pattern, e.g. /users/:id) when available. On
older Rails or bare Rack where it isn't populated, the raw path is used —
which raises span-name cardinality for routes with dynamic segments; prefer
Rails 7.1+ for automatic low-cardinality route names.
Forked servers (Puma workers, Sidekiq): span/log pipelines self-heal
after fork; the metrics reader is re-armed automatically by the first
request/job in the child via a pid check. Belt-and-braces:
on_worker_boot { Foam::Otel.after_fork! }.
Custom telemetry passthrough (stamped app.custom automatically). Create
each instrument ONCE and reuse it — creating an instrument per call
re-registers it under a mutex and logs a duplicate-registration warning:
Foam::Otel.tracer.in_span("work") { |span| ... }
JOBS_DONE = Foam::Otel.meter.create_counter("jobs_done") # once, at load
JOBS_DONE.add(1) # per event
Foam::Otel.logger.on_emit(severity_number: 9, body: "hi", context: OpenTelemetry::Context.current)
Foam::Otel.record_exception(error) # foam span only; never notifies a tracker
bundle exec rspec # unit suite
bash ../../test-apps/ruby-rails/run-verify.sh # full conformance gate