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_from swallows (via process_action.action_controller), and broadcasts Rails.logger lines to foam as log.rails records — 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_id verbatim 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_from handlers) 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