feat: add FastAPI middleware for per-request emissions tracking - #1203
davidberenstein1957 wants to merge 3 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1203 +/- ##
==========================================
+ Coverage 91.70% 91.74% +0.04%
==========================================
Files 49 52 +3
Lines 5157 5357 +200
==========================================
+ Hits 4729 4915 +186
- Misses 428 442 +14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Hello, thanks for this. There is a problem with your branch : there are many changes that are already merged. Can you do a rebase ? |
f44740f to
15c0ccf
Compare
|
@benoit-cty I have reached out to some people at FastAPI, if they would be interested in a quick review :) For visibility, we could also consider deploying it as a standalone integration, but let's see if people like it. |
eabd526 to
bb6995c
Compare
SaboniAmine
left a comment
There was a problem hiding this comment.
Thanks David this a great PR! Left a few questions, and saw that you have already prepared a benchmark script, do you have any numbers / graph already computed to share ?
deba2ce to
86cc355
Compare
|
@inimaz This is ready for review. Rebased on v3.2.9 and benchmarked on a real HF embedder workload — middleware adds about 3 ms per request (~24 ms → ~27 ms). Details in the updated PR description and docs/how-to/fastapi.md. |
|
@SaboniAmine Thanks again for the thorough review — I've addressed the inline threads in the latest pushes:
Local |
|
Ponytail cleanup on this PR:
See |
|
Added a sync
Reproduce: |
|
@inimaz @SaboniAmine — ready for another look when you have time. This commit finishes the HTTP perf work:
Deferred middleware stays in the same ballpark as baseline on the live benchmark (~30–32 ms vs ~42 ms baseline mean on Darwin arm64). |
830edb6 to
d1e7a95
Compare
Adds an ASGI middleware that gives each HTTP request its share of a long-running tracker's energy, plus the attribution model behind it. One tracker runs for the app's lifetime. Each completed sampling window (t_prev, t_now, dE) is split across the requests in flight during it, weighted by their overlap with the window and normalised by the sum of the weights. Windows with nothing in flight are recorded as unattributed. The invariant attributed + unattributed == settled holds exactly after every window, and is what the concurrency test pins down. Why not per-request start/stop energy snapshots: with N requests in flight each request observes the whole machine's delta, so the sum overcounts by roughly N - measured up to 88x at 100 concurrent requests. Fair-share weighting is the only split that conserves the run total. A request's share is only known one or more sampling windows after its response was sent, so results are reported then, via a callback. A request that never covered a completed window reports energy_kwh=None rather than zero: there is no honest number for it. Tracker side: add_energy_window_observer / remove_energy_window_observer expose the sampling windows, and http_request_emissions() scales the run's EmissionsData down to one attributed share using the run's accumulated component ratios and carbon intensity. Depends on #1374 (duration int -> float in the emissions schemas, and dropping the duration < 1 send guard) and #1375 (scheduler pause handling around tasks). Both are carried by their own PRs rather than duplicated here, so this should merge after them. Deliberately left out, to keep the diff reviewable: hardware-tier gating of which backends can resolve a sampling window, include/exclude path filtering (endpoint labelling is two lines inline), idle-baseline subtraction, per-endpoint aggregation, routing per-request rows into the tracker's own CSV/API output handlers, a lifespan helper, and a dedicated docs page. Each is additive on top of this and can follow if there is demand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d1e7a95 to
e293e0c
Compare
Verdict: 🔧 Request changesSplitting each sampling window's energy across the requests in flight, weighted by overlap, is a nice design, and the attribution unit tests (sum invariant, concurrency) are good. But the PR has drifted: the branch was rewritten into a single commit (e293e0c), and the description and earlier review threads describe code that no longer exists: Must fix:
Tests: Housekeeping: Nits:
|
# Conflicts: # codecarbon/emissions_tracker.py
- Middleware is added at module level; the tracker is passed in or read from app.state.codecarbon_tracker, so the documented lifespan pattern no longer raises "Cannot add middleware after an application has started". Drop add_codecarbon_middleware and its per-call subclass. - Only record requests while the tracker runs; settle pending requests when it stops/changes and on lifespan shutdown (no unbounded growth). - Compute carbon intensity once per window and report emissions_kg as energy x intensity; remove http_request_emissions from the core tracker, so the scheduler thread no longer mutates run totals. - Iterate over a copy of the window observers. - Add a codecarbon[fastapi] extra with an install hint on ImportError. - Default log_request logs at DEBUG. - Tests: offline/fake trackers, no sleeps; cover the documented lifespan pattern, never-started tracker, 500 on raise, close on shutdown. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Made the changes in 6819b50: module-level middleware with lifespan tracker, no leak, per-window intensity, fastapi extra, offline tests. Merged master (not rebased, to avoid a force-push) and rewrote the description. |
Description
Adds per-request energy and emissions attribution for FastAPI/Starlette apps. One tracker runs for the app lifetime;
CodeCarbonMiddlewaresplits each of the tracker's sampling windows across the requests that were in flight during it, weighted by overlap and normalised by the sum of weights (codecarbon/integrations/fastapi/attribution.py). A request's number is only known after the next window closes, soon_request(energy, emissions_kg, status_code)fires then, on the scheduler thread.Related Issue
N/A
Motivation and Context
Per-request start/stop snapshots cannot do this attribution correctly: with N requests in flight, each one sees the whole machine's delta, so the sum overcounts by roughly N. This middleware maintains the invariant
attributed_kwh + unattributed_kwh == settled_kwhafter every window, and computes carbon intensity once per window (emissions_kg = energy_kwh x intensity) without touching tracker totals from the scheduler thread.How Has This Been Tested?
tests/integrations/test_fastapi.pycovers attribution invariants and concurrency, the middleware with a fake tracker (window resolution, 500 on raise, close on shutdown, never-started tracker), and the documented lifespan pattern on anOfflineEmissionsTracker.Screenshots (if appropriate):
N/A
Usage
Requests are only recorded while the tracker runs. Pending requests are settled when the tracker stops or changes, and on lifespan shutdown.
Changes
codecarbon/integrations/fastapi/— attributor + middleware.emissions_tracker.py—add/remove_energy_window_observer, notified after each sample, and_carbon_intensity_kg_per_kwh().pyproject.toml—codecarbon[fastapi]extra; fastapi/httpx indev.docs/how-to/examples.md— usage section.Types of changes
AI Usage Disclosure
Checklist: