Skip to content

Add OpenTelemetry metrics monitor (faust[opentelemetry]) - #746

Open
wbarnha wants to merge 4 commits into
masterfrom
claude/new-session-abcs0g
Open

Add OpenTelemetry metrics monitor (faust[opentelemetry])#746
wbarnha wants to merge 4 commits into
masterfrom
claude/new-session-abcs0g

Conversation

@wbarnha

@wbarnha wbarnha commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

Adds OpenTelemetryMonitor, a sensor that reports the same metric set as the built-in Statsd and Datadog monitors, but through the OpenTelemetry metrics API — so Faust metrics can be exported to any OpenTelemetry-compatible backend (OTLP, Prometheus, the console, ...).

This is the metrics counterpart to the OpenTelemetry tracing discussions in #688 / #689, and is intentionally standalone: it depends on neither of those PRs.

How it works

  • Faust depends only on opentelemetry-api, via the new optional faust[opentelemetry] extra. Every instrument is a cheap no-op until your application configures a global MeterProvider with the exporter of its choice — the library never picks an SDK or exporter for you.
  • Following OpenTelemetry conventions, each instrument is dimensioned by attributes (topic, partition, stream, table, status_code, ...) rather than baking those into the metric name the way Statsd does (read_offset.{topic}.{partition}).
  • Modeled on the existing DatadogMonitor (same Monitor hooks, same metric coverage), so it stays consistent with the other backends and calls super() first to preserve the in-memory Monitor stats.

Usage:

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import (
    ConsoleMetricExporter, PeriodicExportingMetricReader,
)
from faust.sensors.otel import OpenTelemetryMonitor

reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))

app = faust.App('example', broker='kafka://localhost:9092')
app.monitor = OpenTelemetryMonitor()

Instruments

  • Counters: faust.messages.received, faust.events.total, faust.messages.sent, faust.messages.send_errors, faust.table.operations, faust.assignments, faust.http.requests, faust.custom.count.
  • UpDownCounters (in-flight work): faust.messages.active, faust.events.active, faust.rebalances.active, faust.rebalances.recovering.
  • Histograms (unit ms): faust.events.runtime, faust.commit.latency, faust.send.latency, faust.send.error_latency, faust.assignment.latency, faust.rebalance.return_latency, faust.rebalance.end_latency, faust.http.latency.
  • Gauges (last-known value per topic/partition): faust.offset.read, faust.offset.committed, faust.offset.end, faust.producer.buffer.

Changes

  • faust/sensors/otel.pyOTelMetrics instrument container + OpenTelemetryMonitor.
  • requirements/extras/opentelemetry.txt (opentelemetry-api/-sdk >=1.23.0, for the synchronous Gauge), the opentelemetry bundle in setup.py, and the extra added to requirements/test.txt.
  • tests/unit/sensors/test_otel.py — 17 tests driving the Monitor hooks against an in-memory SDK metric reader (counter/up-down/histogram/gauge points + attributes), plus the no-OpenTelemetry ImproperlyConfigured path.
  • Docs — user-guide section in docs/userguide/sensors.rst, a bundle entry in docs/includes/installation.txt, and a reference autodoc page.

Verification

  • pytest tests/unit/sensors/ — 156 passed (17 new), the full sensors suite green.
  • black / isort / flake8 (repo pins) clean on the new files.

🤖 Generated with Claude Code


Generated by Claude Code

Add OpenTelemetryMonitor, a sensor that reports the same metric set as the
Statsd and Datadog monitors through the OpenTelemetry metrics API, so Faust
metrics can be exported to any OpenTelemetry-compatible backend (OTLP,
Prometheus, console, ...).

Faust depends only on opentelemetry-api via the new optional
faust[opentelemetry] extra; every instrument is a cheap no-op until the
application configures a global MeterProvider. Following OpenTelemetry
conventions, each instrument is dimensioned by attributes (topic, partition,
stream, table, ...) rather than baking those into the metric name as Statsd
does.

- faust/sensors/otel.py: OTelMetrics instrument container + OpenTelemetryMonitor.
- requirements/extras/opentelemetry.txt + setup.py bundle + test requirements.
- tests/unit/sensors/test_otel.py: 17 tests driving the hooks against an
  in-memory SDK metric reader.
- docs: user-guide section, installation bundle entry, reference autodoc page.

Assisted-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ1voGxC8Nqs3AUPKnFVzZ
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.06%. Comparing base (706b502) to head (20e563e).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #746      +/-   ##
==========================================
+ Coverage   95.98%   96.06%   +0.07%     
==========================================
  Files         103      104       +1     
  Lines       11069    11249     +180     
  Branches     1191     1202      +11     
==========================================
+ Hits        10625    10806     +181     
+ Misses        350      349       -1     
  Partials       94       94              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

claude added 3 commits July 23, 2026 03:28
Adds a test that passes a pre-built OTelMetrics container via the
`metrics=` kwarg, covering the last uncovered line/branch in
OpenTelemetryMonitor.metrics (patch coverage 98.9% -> 100%).

Assisted-by: Claude Opus 4.8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ1voGxC8Nqs3AUPKnFVzZ
`mypy -p faust` now checks the whole package (#758 removed the ratchet), and
this branch fails it with 8 errors -- two implicit-Optional parameters, each
producing one `[assignment]` plus one `[override]` against every supertype
that declares it:

    faust/sensors/otel.py:320: error: Argument 5 of "on_stream_event_out" is
    incompatible with supertype "faust.sensors.monitor.Monitor"; supertype
    defines the argument type as "dict[Any, Any] | None"  [override]
    ... and the same against Sensor and SensorInterfaceT
    faust/sensors/otel.py:320: error: Incompatible default for parameter
    "state" (default has type "None", parameter has type "dict[Any, Any]")
    [assignment]
    ... and the same four for "view" at :441

PEP 484's implicit-Optional was removed in mypy 0.990, so `state: Dict = None`
no longer means `Optional[Dict]`; it means `Dict`, which both rejects the
`None` default and narrows the parameter relative to the base class.  The
bodies already handle `None` (`if state is not None:`) and the base
signatures already say `Optional`, so spelling it out is the correction, not
a behaviour change.

Verified: merged into master, `mypy -p faust` reports "Success: no issues
found in 165 source files"; tests/unit/sensors/test_otel.py passes (18
passed, with opentelemetry installed -- it importorskips otherwise).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
This PR adds `-r extras/opentelemetry.txt` to `requirements/test.txt`, so the
lint job -- which installs test.txt and runs `mypy -p faust` via
`scripts/check` -- will have opentelemetry present.  That matters: with
`ignore_missing_imports = true`, an absent package resolves to `Any` and
nothing in it can be wrong.  Once it is installed mypy checks against the real
API and reports:

    faust/sensors/otel.py:44: error: Incompatible types in assignment
    (expression has type "None", variable has type Module)  [assignment]
    faust/sensors/otel.py:47: error: Module "opentelemetry.metrics" has no
    attribute "Gauge"; maybe "_Gauge"?  [attr-defined]

The second one is a real bug, not a checker artifact.  `opentelemetry.metrics`
has never exported a public `Gauge`: the synchronous gauge landed in
opentelemetry-api 1.23.0 -- the floor `extras/opentelemetry.txt` pins -- as
`_Gauge`, and `__all__` still lists it that way in 1.44.0.  The class is
*named* `Gauge` (which is why `Meter.create_gauge` renders as returning
`opentelemetry.metrics.Gauge`), but the importable name is the underscore one.
So `from opentelemetry.metrics import Gauge` would fail outright were it not
sitting under `if typing.TYPE_CHECKING`.  Import `_Gauge as Gauge` instead, and
say why in a comment so it does not look like an accident.

The first is the module-as-sentinel pattern `sensors/prometheus.py` already
carries: `otel_metrics` is both the module and the "is the extra installed?"
flag that `OpenTelemetryMonitor.meter` checks, and mypy binds the name to the
module type at the import. Same ignore, same explanation, as prometheus.py.

Verified against master with opentelemetry-api 1.44.0 installed: `mypy -p
faust` reports "Success: no issues found in 165 source files".
tests/unit/sensors/test_otel.py passes (18 passed); it importorskips when the
extra is absent, which is why nothing caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N2yysiNbzzVcvYhtrpsM7
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