Skip to content

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

Merged
wbarnha merged 5 commits into
masterfrom
claude/new-session-abcs0g
Aug 10, 2026
Merged

Add OpenTelemetry metrics monitor (faust[opentelemetry])#746
wbarnha merged 5 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.13%. Comparing base (e9f102f) to head (cae6966).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #746      +/-   ##
==========================================
+ Coverage   96.05%   96.13%   +0.07%     
==========================================
  Files         104      105       +1     
  Lines       11087    11267     +180     
  Branches     1189     1200      +11     
==========================================
+ Hits        10650    10831     +181     
+ Misses        345      344       -1     
  Partials       92       92              

☔ 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 and others added 4 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
@wbarnha
wbarnha merged commit 46109b5 into master Aug 10, 2026
31 of 32 checks passed
wbarnha pushed a commit that referenced this pull request Aug 13, 2026
Brings the OTel tracing foundation up to date with master, which has since
landed the sensor-level OpenTelemetry path (#748), the OTel metrics monitor
(#746), and moved `opentracing` out of core requirements into an extra (#686).

Conflict resolutions:

- `requirements/extras/opentelemetry.txt` (add/add): keep master's
  API floor and the fastapi/aiokafka instrumentation packages, and add
  `opentelemetry-sdk` on top so apps can configure an exporter out of the
  box. The tracing core itself still needs only the API.
- `setup.py`: take master's bundle list, which already contains the
  `opentelemetry` entry this branch added.
- `requirements/test.txt`: drop this branch's `-r extras/opentelemetry.txt`
  line; master already pulls the extra in, plus explicit api/sdk pins.

The ADR is refreshed against the new tree: `opentracing` is described as an
extra rather than a hard core dependency, a section explains how this work
relates to `faust.contrib.opentelemetry` (sensor covers the message
boundary, this covers the built-in span tree), and the phase list records
what has already shipped.

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