Skip to content

Wire metrics_reporter into the OTLP exporter and flush it on shutdown - #117

Open
Alan-Marx wants to merge 4 commits into
simplepractice:mainfrom
Alan-Marx:fix/otlp-exporter-metrics-reporter
Open

Wire metrics_reporter into the OTLP exporter and flush it on shutdown#117
Alan-Marx wants to merge 4 commits into
simplepractice:mainfrom
Alan-Marx:fix/otlp-exporter-metrics-reporter

Conversation

@Alan-Marx

@Alan-Marx Alan-Marx commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

OtelSetup.build_otlp_exporter constructs OpenTelemetry::Exporter::OTLP::Exporter without passing metrics_reporter::

def build_otlp_exporter(config)
  OpenTelemetry::Exporter::OTLP::Exporter.new(
    endpoint: "#{config.base_url}/api/public/otel/v1/traces",
    headers: build_headers(config.public_key, config.secret_key),
    compression: "gzip"
  )
end

opentelemetry-exporter-otlp's exporter defaults metrics_reporter: nil to its own no-op MetricsReporter when not given one. So a user who configures config.metrics_reporter and exports over OTLP never receives otel.otlp_exporter.failure or otel.otlp_exporter.message.compressed_size - despite SpanProcessor already threading config.metrics_reporter through correctly for the otel.bsp.* metrics. It's an easy gap to miss because everything appears wired: the config option exists, otel.bsp.* metrics work, and there's no error - the two OTLP-exporter-level metrics just silently go nowhere.

Related: Langfuse.shutdown never gives the configured metrics_reporter a chance to flush before process exit, even though it already has explicit shutdown boundaries for the client and tracer provider. For an async reporter (e.g. a DogStatsd client with a background sender thread), the last few metrics from a shutdown-time export can be lost.

Fix

  1. build_otlp_exporter now passes metrics_reporter: ResilientMetricsReporter.wrap(config.metrics_reporter, logger: config.logger), matching the existing pattern in SpanProcessor.
  2. ResilientMetricsReporter gains an optional #shutdown that forwards to the wrapped reporter only if it responds to :shutdown, rescued the same way as its other three methods.
  3. Langfuse.shutdown calls it after OtelSetup.shutdown.

Testing

  • spec/langfuse/otel_setup_spec.rb: a mock-based test on the exporter constructor call, plus a contract test that builds the real tracer provider + real OTLP exporter, stubs the HTTP endpoint to fail with WebMock, and asserts both otel.otlp_exporter.failure and otel.otlp_exporter.message.compressed_size reach a configured reporter through the actual export path (not just a mocked constructor call).
  • spec/langfuse/resilient_metrics_reporter_spec.rb / spec/langfuse_spec.rb: cover the new #shutdown passthrough, including the no-shutdown-method and raising-reporter cases.

Both changes are backward compatible - nil metrics_reporter still resolves to the SDK's own no-op reporter, and Langfuse.shutdown is a no-op addition for anyone not using metrics_reporter.

Full suite (bundle exec rspec, bundle exec rubocop) passes locally.


Note

Low Risk
Observability wiring and documentation only; tracing export behavior is unchanged aside from forwarding OTLP exporter metrics to an optional reporter the app already owns.

Overview
config.metrics_reporter now feeds both the batch span processor and the default OTLP exporter, so OTLP operational metrics (e.g. otel.otlp_exporter.failure, otel.otlp_exporter.message.compressed_size) reach the same reporter as existing otel.bsp.* metrics instead of silently using OpenTelemetry’s no-op reporter.

OtelSetup wraps the configured reporter once via ResilientMetricsReporter and passes that instance into SpanProcessor and OpenTelemetry::Exporter::OTLP::Exporter. SpanProcessor no longer wraps the reporter itself—it requires an injected metrics_reporter:.

Docs and comments broaden the metrics_reporter contract to cover BSP + OTLP, state that Langfuse does not flush, close, or shut down an application-owned reporter, and document shutdown order: Langfuse.shutdown first, then the app flushes/closes backends (e.g. DogStatsD).

Tests cover shared wiring, real OTLP metric emission on failed export, final BSP metrics on shutdown, concurrent shutdown, a UDP integration proving post-shutdown app flush delivers BSP + OTLP metrics, and assertions that Langfuse.shutdown / Langfuse.reset! never call shutdown on the reporter.

Reviewed by Cursor Bugbot for commit 854307f. Bugbot is set up for automated code reviews on this repo. Configure here.

Alan Marx added 3 commits August 24, 2026 15:30
build_otlp_exporter never passed metrics_reporter to
OpenTelemetry::Exporter::OTLP::Exporter, so it silently fell back to
the SDK's no-op reporter - otel.otlp_exporter.failure and
otel.otlp_exporter.message.compressed_size never reached a
configured reporter, unlike the otel.bsp.* metrics that SpanProcessor
already threads through.
OtelSetup.shutdown can emit final metrics (e.g. otel.bsp.dropped_spans
on a forced drain) through the configured metrics_reporter, but
nothing gave that reporter a chance to flush before process exit.
Langfuse.shutdown now calls an optional #shutdown on the reporter,
resilient to a reporter that doesn't implement it or that raises.
The metrics_reporter section only described BatchSpanProcessor
metrics and said the application fully owns the reporter lifecycle -
both now incomplete after wiring the reporter into build_otlp_exporter
and calling an optional #shutdown from Langfuse.shutdown.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 36df803. Configure here.

Comment thread lib/langfuse.rb Outdated
@kxzk

kxzk commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@Alan-Marx I updated the branch in commit 854307f.

The refactor passes one resilient metrics_reporter instance to both the batch processor and the default OTLP exporter. Langfuse.shutdown flushes the tracing pipeline but does not flush, close, or shut down the application-owned reporter.

Please verify this sequence with your real reporter integration before we merge:

  1. Create and finish a Langfuse observation.
  2. Call Langfuse.shutdown.
  3. Flush and close the metrics reporter.
  4. Confirm that the final otel.bsp.* and otel.otlp_exporter.* metrics reach your metrics backend.
  5. Confirm that Langfuse never calls the reporter's shutdown method.

Local verification passed with 1,646 examples, 96.94% coverage, RuboCop, a real UDP lifecycle test, and live Langfuse ingestion plus API readback. Please reply when you have verified the reporter behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants