feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(obs): prometheus counters for UsageEvent emission (#408) - #422

Merged
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter
May 27, 2026
Merged

feat(obs): prometheus counters for UsageEvent emission (#408)#422
moonming merged 2 commits into
mainfrom
feat/issue-408-usage-event-prometheus-counter

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes#408. PR #402 audit L1.

Pre-fix, the DP emitted `UsageEvent`s to the sink + OTLP fan-out but had no DP-side prometheus counter. Result: the e2e harness (no cp-api / no OTLP receiver in the loop) couldn't observe emission, so a regression that dropped emission was invisible to tests. This PR closes that gap.

Wiring

Two counters on the gateway's own `/metrics` scrape:

```
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
```

LabelValuesCardinality
`handler``chat` / `embeddings` / `messages` (today); `completions` / `responses` / `rerank` / `audio` / `images` after #403-#407Fixed set
`status_code``2xx` / `3xx` / `4xx` / `5xx` / `other`Bucketed (raw u16 would blow up at ~1000 values × handler × protocol)
`inbound_protocol``openai` / `anthropic`Mirrors wire-level UsageEvent field
`reason``sink_full` / `sink_closed`Distinguishes worker overload from clean shutdown

Invariant: `emitted = delivered + dropped`. Operators can compute delivery rate as `1 - drops/emitted`.

What changed structurally

`UsageSink` now optionally carries a `Metrics` handle via `with_metrics(metrics)`. The server bootstrap calls it after the shared `Metrics` is built (`main.rs:389`). `try_emit` signature gains a `handler: &'static str` arg — all 3 production callers updated to pass their fixed label.

The `M_USAGE_EVENT_DROPS_TOTAL` counter has existed since #302 but was never wired into `try_emit`; this PR finishes that wiring so both halves of the invariant are visible.

Test plan

  • Unit — 2 new tests in `aisix-obs::usage::tests`:
    • `emits_counter_increments_per_call` — pins emit counter labels (handler, bucketed status_code, inbound_protocol)
    • `dropped_event_records_reason_and_keeps_emit_count` — pins drop counter reason on a full-channel scenario + the `emit always bumps` invariant
  • E2E — `prometheus-metrics-e2e.test.ts` adds a Add prometheus counter for UsageEvent emissions (DP observability) #408 case: drives a real `/v1/chat/completions` through the harness, scrapes `/metrics`, asserts counter delta + label shape. Pins that `status_code` MUST be bucketed (`2xx`, not raw `200`).
  • All 13 pre-existing `aisix-obs::usage::tests` still pass.
  • All 317 `aisix-proxy::lib` tests still pass (sink signature change touched chat/embeddings/messages callsites).
  • `cargo clippy -p aisix-obs -p aisix-proxy -p aisix-server -- -D warnings` clean.
  • Local e2e run: all 4 tests in `prometheus-metrics-e2e.test.ts` pass (6 s).

References

Summary by CodeRabbit

New Features

  • Added telemetry metrics to track successful usage event emissions, providing visibility into event flow with details on handler type, HTTP status code ranges, and inbound protocol.

Tests

  • Added end-to-end test to validate the accuracy and correctness of usage event emission metrics.

Review Change Stack

PR #402 audit (L1) flagged that the e2e harness has no observable
for UsageEvent emission — no cp-api in the loop, no OTLP receiver,
no admin endpoint. A regression that dropped emission would be
caught only by line-by-line code review, not by tests.
This PR wires two DP-side prometheus counters so emission becomes
externally observable on the gateway's own `/metrics` scrape:
aisix_usage_events_emitted_total{handler, status_code, inbound_protocol}
aisix_usage_event_drops_total{reason}
Labels:
- handler: "chat", "embeddings", "messages" (today). Fixed-set
enumeration extended by the #226 follow-up endpoints
(#403-#407: completions / responses / rerank / audio / images).
- status_code: bucketed as `2xx` / `3xx` / `4xx` / `5xx` / `other`
rather than raw u16 — avoids the ~1000-value cardinality blowup.
- inbound_protocol: mirrors the wire-level UsageEvent field
("openai" / "anthropic").
- reason: "sink_full" / "sink_closed" — distinguishes worker
overload from a clean worker shutdown.
The drops counter has existed as `M_USAGE_EVENT_DROPS_TOTAL` since
#302 but was never wired into `UsageSink::try_emit`; this PR
finishes that wiring so operators see both halves of the
invariant `emitted = delivered + dropped`.
Architecture: `UsageSink` now optionally carries a `Metrics`
handle via the `with_metrics(metrics)` builder method. The server
bootstrap calls it after the shared `Metrics` is constructed. The
sink's `try_emit` signature gains a `handler: &'static str` label
arg — all three production callers (chat / embeddings / messages)
updated to pass their fixed label.
Tests:
- Unit: 2 new tests in `aisix-obs::usage::tests`:
- `emits_counter_increments_per_call` — pins the emit counter
labels (handler, bucketed status_code, inbound_protocol).
- `dropped_event_records_reason_and_keeps_emit_count` — pins
the drop counter reason on a full-channel scenario, and the
invariant that emit bumps even on drop.
- E2E: 1 new test in `prometheus-metrics-e2e.test.ts` — drives a
real /v1/chat/completions through the harness, scrapes
/metrics, asserts the counter increment delta and label shape.
Pins that status_code MUST be bucketed (`2xx`, not raw `200`).
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 9214d0f0-3bf9-40f6-8afa-29034b900f92

📥 Commits

Reviewing files that changed from the base of the PR and between 98e9835 and 090c590.

📒 Files selected for processing (7)
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

📝 Walkthrough

Walkthrough

This PR adds Prometheus telemetry to track usage event emission and drops. It introduces a new aisix_usage_events_emitted_total counter with bounded labels (handler, bucketed status_code, inbound_protocol), integrates metrics recording into UsageSink, wires handler labels across proxy endpoints, and validates the flow end-to-end.

Changes

Usage Event Telemetry

Layer / File(s)Summary
Metric contract and status bucketing
crates/aisix-obs/src/metrics.rs
Defines M_USAGE_EVENT_EMITS_TOTAL constant, adds record_usage_event_emit method to record the counter with bounded labels (handler, bucketed status_code, inbound_protocol), and provides status_bucket helper that normalizes status codes into 2xx/3xx/4xx/5xx/other buckets with unit test coverage.
UsageSink metrics integration and emit logic
crates/aisix-obs/src/usage.rs
Extends UsageSink with optional metrics field and with_metrics builder. Replaces try_emit(event) signature with try_emit(handler, event) that records emit-intent counters when metrics are present and drop counters with distinct reasons (sink_disabled, sink_full, sink_closed). Normalizes inbound_protocol values into bounded set (openai, anthropic, other). Tests validate emission counting, drop-reason tracking, emitted == delivered + dropped invariant, and protocol bucketing.
Handler label wiring in proxy modules
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs
Updates usage event emission across handlers to pass handler labels: "chat", "embeddings", and "messages" respectively, enabling metric aggregation by endpoint type.
Server-side sink wiring
crates/aisix-server/src/main.rs
Connects metrics to the usage sink during server initialization via usage_sink.with_metrics((*metrics).clone()), enabling metrics recording for emitted events.
End-to-end validation
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Adds e2e test verifying aisix_usage_events_emitted_total increments on successful OpenAI chat request with correct label presence and bucketing (handler="chat", status_code="2xx", inbound_protocol="openai"). Includes parseUsageEmittedCount helper to extract counter values from Prometheus scrape output by label set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

PR #422 audit raised 2 HIGH + 3 MEDIUM findings on the #408
UsageEvent counter wiring. All addressed:
HIGH-1 — `emitted == delivered + dropped` invariant broke when a
`disabled()` sink had a `Metrics` handle attached. Emit bumped but
the disabled-channel return path didn't bump drops. Fixed: the
`tx=None` branch now records a drop with `reason="sink_disabled"`
so the invariant holds strictly. Operators see "DP intended to
emit N but no sink wired" via `drops_total{reason=sink_disabled}`
instead of silent zeros. Doc comment + test pin this contract.
HIGH-2 — The drop-event test asserted metric names only ("counter
appears in scrape"), not values. A regression that double-bumped
emit on drop would pass that test. Fixed: added a numeric counter
parser; the test now asserts `emit_total == 2` and `drops_total ==
1` after one delivered + one dropped call. Helper function
`parse_counter_value` is reused by the new `disabled_sink` test.
MEDIUM-1 — `sink_closed` reason path had no coverage. Only
`sink_full` was exercised. Added test that drops the receiver
before any send, asserts `drops_total{reason=sink_closed} == 1`.
MEDIUM-2 — `status_bucket` boundary cases untested; a typo like
`200..299` (excluding 299) wouldn't be caught. Added boundary
test covering 199 / 200 / 299 / 300 / 399 / 400 / 499 / 500 / 599 /
600 / 0 — all five buckets including the dead-code `3xx` and
`other` arms with no live caller today.
MEDIUM-3 — `inbound_protocol` was accepted as `&str` and copied
via `.to_string()` on the request hot path. Two costs: heap alloc
per call, and the type system permitted user-controlled label
values. Normalised to `&'static str` at the `UsageSink::try_emit`
boundary: any wire value other than "openai"/"anthropic" buckets
to "other". Removes the alloc AND pins prometheus cardinality at
the type-system level. Test pins the boundary defence.
Also tightened the doc-comment invariant in usage.rs to match the
PR body (`emitted == delivered + dropped`, exact equality, not the
softer `≈`).
@moonming
moonming merged commit 1b69c35 into mainMay 27, 2026
8 checks passed
@moonming
moonming deleted the feat/issue-408-usage-event-prometheus-counter branch May 27, 2026 01:23
Sign up for freeto 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.

Add prometheus counter for UsageEvent emissions (DP observability)

1 participant

@moonming