Skip to content

feat: add AISIX-native Prometheus metrics - #331

Merged
nic-6443 merged 4 commits into
mainfrom
feat-prometheus-metrics-284
May 18, 2026
Merged

feat: add AISIX-native Prometheus metrics#331
nic-6443 merged 4 commits into
mainfrom
feat-prometheus-metrics-284

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Adds AISIX-native Prometheus metrics for the LiteLLM-style categories AISIX can support from DP data today, while keeping the existing compatibility metrics.

What changed:

  • adds aisix_proxy_*, aisix_llm_*, deployment, routing, quota, budget, Redis, usage-event, and OTLP fan-out metric families
  • records request, latency, token, micro-USD spend, TTFT, in-flight, rate-limit remaining, and optional budget metrics from the request paths that currently have the data
  • keeps existing aisix_requests_total, aisix_request_duration_seconds, aisix_ratelimit_rejections_total, and aisix_tokens_consumed_total
  • makes observability.metrics.prometheus.enabled/path control the admin scrape endpoint instead of always mounting /metrics; path values are normalized so internal/prom mounts as /internal/prom
  • documents the supported metric names and the DP-stable labels
  • adds an E2E scrape test that sends real traffic through aisix and checks representative aisix_* metrics

Budget gauges are emitted only when the CP budget-check response includes optional budget detail fields. Team/member labels use team_id and owner_id when those fields are present in DP config; otherwise they render as unknown rather than inventing aliases or emails.

No database changes.

Tested:

  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo build -p aisix-server --bin aisix
  • cargo test -p aisix-obs --lib
  • cargo test -p aisix-admin metrics_endpoint --lib
  • cd tests/e2e && pnpm exec vitest run src/cases/prometheus-metrics-e2e.test.ts
  • earlier full sweep before the review-fix commit: cargo test --workspace, cd tests/e2e && pnpm exec vitest run

Summary by CodeRabbit

  • New Features

    • Configurable Prometheus scrape endpoint (custom path or disable) and richer observability: AISIX-native metrics for LLM usage (tokens, spend, TTFT), proxy in-flight/request gauges, deployment/routing events, rate-limit and budget gauges, and exporter/cache health.
  • Bug Fixes / Tests

    • Expanded end-to-end tests covering default/custom/disabled metrics paths and metric emission/normalization.
  • Documentation

    • Updated bootstrap and operations docs for metrics configuration and AISIX-native naming.

Review Change Stack

Copilot AI review requested due to automatic review settings May 18, 2026 06:58
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cd6df708-0eb3-4e1f-ab33-cf1ebac40764

📥 Commits

Reviewing files that changed from the base of the PR and between 361eedd and d621cc4.

📒 Files selected for processing (6)
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/messages.rs
  • docs/operations/metrics-and-logs.md
  • tests/e2e/src/harness/app.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/aisix-admin/src/lib.rs
  • docs/operations/metrics-and-logs.md
  • tests/e2e/src/harness/app.ts
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-obs/src/metrics.rs

📝 Walkthrough

Walkthrough

This PR expands AISIX AI Gateway observability by introducing Prometheus metrics configuration, new AISIX-native metric families, and request-level instrumentation across chat, Anthropic, and quota handlers. The /metrics endpoint is now conditionally mounted with configurable path and enable/disable controls.

Changes

Prometheus Metrics System Expansion

Layer / File(s) Summary
Admin metrics configuration and endpoint mounting
crates/aisix-admin/src/state.rs, crates/aisix-admin/src/lib.rs, crates/aisix-server/src/main.rs
AdminState now holds PrometheusConfig and includes a builder method; the admin router conditionally mounts the metrics handler when enabled at the configured path; startup wires the config; tests verify custom path and disabled behavior.
Metrics API and AISIX-native constants
crates/aisix-obs/src/lib.rs, crates/aisix-obs/src/metrics.rs
Introduces AISIX-native metric name constants and label/value types; extends Metrics with in-flight proxy gauge, proxy/LLM request recording, LLM usage/TTFT, deployment/routing metrics, rate-limit and budget gauge APIs, and unit tests.
Budget data model and CP API parsing
crates/aisix-proxy/src/budget.rs
Decision gains optional BudgetDetails; CP wire types parse top-level and reason-embedded budget fields; helpers convert JSON string/number to numeric types; budget preserved across decision transforms; tests added.
Proxy in-flight request tracking middleware
crates/aisix-proxy/src/lib.rs
Adds middleware that increments/decrements an in-flight proxy gauge keyed by endpoint and inbound protocol (anthropic vs openai) across request handling via an RAII guard.
Chat completions request metrics recording
crates/aisix-proxy/src/chat.rs
Chat handler records richer RequestLabels/UsageLabels, emits proxy and LLM request metrics, records LLM usage and TTFT, sets rate-limit remaining gauges, and sets/clears budget gauges when present; streaming path records usage and TTFT on completion.
Anthropic messages request metrics recording
crates/aisix-proxy/src/messages.rs
Messages handler threads team_id/owner_id into success/error paths and records LLM usage via UsageLabels and LlmUsage.
Quota enforcement budget gauge recording
crates/aisix-proxy/src/quota.rs
Quota enforcement now sets or clears budget gauges based on decision.budget, labeling by api_key_id, team_id, owner_id (fallback \"unknown\").
Configuration documentation and E2E tests
docs/configuration/bootstrap-config.md, docs/operations/metrics-and-logs.md, tests/e2e/src/cases/prometheus-metrics-e2e.test.ts, tests/e2e/src/harness/app.ts
Bootstrap docs include observability.metrics.prometheus with enabled and path; operations doc describes AISIX-native naming and label set; E2E tests validate default/custom/disabled Prometheus mounting and metric emission; test harness supports prometheusPath override.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • api7/aisix#39: Complementary changes around Anthropic /v1/messages observability and usage/TTFT recording.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add AISIX-native Prometheus metrics' directly and clearly summarizes the main change: adding new AISIX-native Prometheus metrics while maintaining backward compatibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-obs/src/metrics.rs`:
- Around line 194-212: The function record_proxy_request is recording proxy
latency into the LLM histogram constant M_LLM_REQUEST_DURATION; change it to use
the proxy-specific histogram constant M_PROXY_REQUEST_DURATION instead (in the
metrics::histogram! call and where .record(...) is applied) so proxy latencies
are emitted to the aisix_proxy_* metric family; update any references inside
record_proxy_request (labels and .record(duration.as_secs_f64())) to point to
M_PROXY_REQUEST_DURATION and ensure the histogram label set remains identical.
- Around line 165-191: The shared atomic inner.proxy_in_flight used in
increment_proxy_in_flight and decrement_proxy_in_flight causes cross-label
interference and can underflow the stored counter; replace it with a per-label
counter map (e.g., a Mutex<HashMap<(String,String), AtomicIsize>> or a
concurrent map like DashMap) keyed by (endpoint, inbound_protocol) and update
that specific counter in increment_proxy_in_flight/decrement_proxy_in_flight,
publishing its value with metrics::gauge!(M_PROXY_IN_FLIGHT, "endpoint"=>...,
"inbound_protocol"=>...). In decrement_proxy_in_flight ensure atomic decrement
is done safely (use fetch_update or a CAS loop on AtomicIsize to never drop
below zero, or remove the map entry when it reaches zero) so the stored state
cannot go negative and future increments/metrics remain correct.

In `@crates/aisix-proxy/src/lib.rs`:
- Around line 117-121: The in-flight gauge is decremented immediately after
calling next.run(request).await, but for streaming responses you must decrement
only when the response body finishes or is dropped; wrap the response body with
an RAII/body-wrapper that calls
state.metrics.decrement_proxy_in_flight(&endpoint, &inbound_protocol) from its
Drop (or when the body stream completes) so the gauge remains incremented for
the full lifetime of the SSE/stream; locate where next.run(request).await
returns the Response and replace its body with the wrapper (use the same
response variable name) so the decrement is tied to the wrapped body lifecycle
instead of executing immediately after next.run.

In `@tests/e2e/src/cases/prometheus-metrics-e2e.test.ts`:
- Around line 70-104: Add two additional e2e cases around the existing "scrape
contains AISIX-native request and token metrics" test: one that sets
observability.metrics.prometheus.enabled = false and asserts the /metrics
endpoint returns 404 (or is absent) and that scrape text does NOT contain the
aisix_* metrics, and another that sets observability.metrics.prometheus.path to
a custom path (e.g., /custom-metrics), uses waitConfigPropagation and
ProxyClient/chat probe exactly as the original test to ensure config propagated,
then fetches app.adminUrl + custom path and asserts status 200 and the same
metric names and labels are present; use the same helpers (ProxyClient,
waitConfigPropagation, app.adminUrl) and the existing expectation patterns so
these new tests cover the disabled and custom-path boundary/combination
scenarios.
- Around line 26-27: The test currently short-circuits when etcd/app/upstream
are unreachable by returning early (using etcdReachable from new
EtcdClient().ping()), which hides failures; remove the conditional skips and
instead make the availability checks fail fast: replace the early return that
uses etcdReachable with an assertion or thrown Error (e.g.,
assert(etcdReachable) or throw new Error(...)) that includes diagnostic info
from EtcdClient().ping(), and apply the same treatment to the other environment
checks referenced around the suite (the app/upstream reachability checks) so the
suite fails deterministically rather than silently skipping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e19aafb-1f98-49b3-bb4a-0b6c5b86b8ca

📥 Commits

Reviewing files that changed from the base of the PR and between 30f70a2 and 63e2dcf.

📒 Files selected for processing (13)
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/state.rs
  • crates/aisix-obs/src/lib.rs
  • crates/aisix-obs/src/metrics.rs
  • crates/aisix-proxy/src/budget.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/quota.rs
  • crates/aisix-server/src/main.rs
  • docs/configuration/bootstrap-config.md
  • docs/operations/metrics-and-logs.md
  • tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

Comment thread crates/aisix-obs/src/metrics.rs
Comment thread crates/aisix-obs/src/metrics.rs
Comment thread crates/aisix-proxy/src/lib.rs Outdated
Comment thread tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Comment thread tests/e2e/src/cases/prometheus-metrics-e2e.test.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new set of AISIX-native Prometheus metric families (aisix_proxy_*, aisix_llm_*, etc.) while keeping the existing compatibility series, and makes the admin scrape endpoint configurable/disable-able via bootstrap config. The PR also wires additional request-path instrumentation (usage, TTFT, in-flight, rate-limit remaining, and optional budget gauges) and adds both unit and E2E coverage plus operator docs for the new metrics.

Changes:

  • Introduces AISIX-native metric families in aisix-obs, plus additional proxy instrumentation for usage, TTFT, in-flight requests, rate-limit remaining, and optional budget gauges.
  • Makes the admin Prometheus scrape endpoint configurable (observability.metrics.prometheus.enabled/path) and adds admin router tests for path/disable behavior.
  • Adds operator documentation for metric names/labels and an E2E scrape test that drives real traffic and checks representative aisix_* output.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/e2e/src/cases/prometheus-metrics-e2e.test.ts New E2E test that drives traffic and validates representative Prometheus output.
docs/operations/metrics-and-logs.md Documents Prometheus endpoint configurability and the new AISIX-native metric names/labels.
docs/configuration/bootstrap-config.md Documents bootstrap config keys for Prometheus scrape endpoint enable/path.
crates/aisix-server/src/main.rs Wires Prometheus config into AdminState.
crates/aisix-proxy/src/quota.rs Emits optional budget gauges during quota enforcement when CP returns budget details.
crates/aisix-proxy/src/messages.rs Records AISIX-native LLM usage metrics for the Anthropic /v1/messages path.
crates/aisix-proxy/src/lib.rs Adds middleware to track proxy in-flight requests by endpoint/protocol.
crates/aisix-proxy/src/chat.rs Records AISIX-native usage + TTFT for streaming and non-streaming chat; emits remaining RL gauges; emits optional budget gauges.
crates/aisix-proxy/src/budget.rs Extends CP budget-check parsing to include optional budget detail fields (with tests).
crates/aisix-obs/src/metrics.rs Implements new metric families, label structs, and recording helpers; adds unit tests for rendering.
crates/aisix-obs/src/lib.rs Re-exports new metrics label/value structs.
crates/aisix-admin/src/state.rs Adds Prometheus config to admin state and builder method.
crates/aisix-admin/src/lib.rs Makes metrics endpoint mount conditional/configurable; adds router tests.
Comments suppressed due to low confidence (6)

crates/aisix-obs/src/metrics.rs:138

  • record_request forwards the same end-to-end duration into record_llm_request, which records aisix_llm_api_latency_seconds. That makes aisix_llm_api_latency_seconds identical to gateway request latency rather than upstream API latency. Consider recording aisix_llm_api_latency_seconds only when you have the upstream timing available (or rename the metric to reflect what’s actually measured).
        self.record_proxy_request(labels, duration);
        self.record_llm_request(labels, duration);

crates/aisix-obs/src/metrics.rs:173

  • proxy_in_flight is tracked with a single global AtomicI64 but exported with endpoint/inbound_protocol labels. As soon as multiple endpoints/protocols are in flight concurrently, each labeled gauge will be set to the global count (and will be wrong for all but the last-updated label set). Track in-flight counts per label (e.g., a DashMap keyed by (endpoint,inbound_protocol)), or drop the labels if you only want a process-wide gauge.
    pub fn increment_proxy_in_flight(&self, endpoint: &str, inbound_protocol: &str) {
        let value = self.inner.proxy_in_flight.fetch_add(1, Ordering::Relaxed) + 1;
        metrics::with_local_recorder(&self.inner.recorder, || {
            metrics::gauge!(
                M_PROXY_IN_FLIGHT,
                "endpoint" => endpoint.to_string(),
                "inbound_protocol" => inbound_protocol.to_string(),
            )
            .set(value as f64);

crates/aisix-obs/src/metrics.rs:183

  • decrement_proxy_in_flight always does an AtomicI64::fetch_sub(1), which can drive the stored counter negative if it’s ever decremented when already at 0 (e.g., cancellation/early-drop paths). Even though the exported value is clamped, the atomic itself remains negative and future increments will be incorrect. Consider using an unsigned atomic plus a saturating fetch_update/CAS loop (or a guard that guarantees 1:1 inc/dec).
    pub fn decrement_proxy_in_flight(&self, endpoint: &str, inbound_protocol: &str) {
        let value = self
            .inner
            .proxy_in_flight
            .fetch_sub(1, Ordering::Relaxed)
            .saturating_sub(1)
            .max(0);

crates/aisix-obs/src/metrics.rs:510

  • aisix_llm_spend_total is emitted via metrics::gauge!(...).increment(...) (see record_counter_f64), so it will be a Prometheus Gauge even though the name ends with _total and is described as a cumulative series. If you need a true counter type, consider exporting spend in an integer unit (e.g., micro-USD) via counter!, or rename the metric to avoid Prometheus counter semantics.
    fn record_counter_f64(&self, metric: &'static str, value: f64) {
        metrics::gauge!(
            metric,
            "endpoint" => self.endpoint.to_string(),
            "inbound_protocol" => self.inbound_protocol.to_string(),
            "provider" => self.provider.to_string(),
            "model" => self.model.to_string(),
            "upstream_model" => self.upstream_model.to_string(),
            "provider_key_id" => self.provider_key_id.to_string(),
            "api_key_id" => self.api_key_id.to_string(),
            "team_id" => self.team_id.to_string(),
            "owner_id" => self.owner_id.to_string(),
        )
        .increment(value);
    }

crates/aisix-obs/src/metrics.rs:344

  • set_rate_limit_remaining only sets gauges when requests/tokens are Some. If a key/model ever transitions from having a limit to unlimited (or the limiter can’t compute a value), the previous gauge sample will remain exported and become stale. Consider explicitly setting a sentinel value (e.g., -1) when the limit is not applicable, or adding a separate configured/present gauge so scrapes can distinguish “unlimited/unknown” from “stale”.
    pub fn set_rate_limit_remaining(
        &self,
        api_key_id: &str,
        model: &str,
        requests: Option<u64>,
        tokens: Option<u64>,
    ) {
        metrics::with_local_recorder(&self.inner.recorder, || {
            if let Some(value) = requests {
                metrics::gauge!(
                    M_RATELIMIT_REMAINING_REQUESTS,
                    "api_key_id" => api_key_id.to_string(),
                    "model" => model.to_string(),
                )
                .set(value as f64);
            }
            if let Some(value) = tokens {
                metrics::gauge!(
                    M_RATELIMIT_REMAINING_TOKENS,
                    "api_key_id" => api_key_id.to_string(),
                    "model" => model.to_string(),
                )
                .set(value as f64);
            }
        });

crates/aisix-obs/src/metrics.rs:360

  • set_budget_gauges only updates the gauges for fields that are Some. If the control plane intermittently omits some optional budget fields (or they become absent after being present), previously-exported gauge values for those fields will remain and can become stale. Consider defining a clear behavior for “missing” (e.g., set to NaN/-1 or emit a budget_detail_present gauge) so dashboards don’t silently show outdated values.
    pub fn set_budget_gauges(&self, labels: BudgetLabels<'_>, budget: BudgetGauges) {
        metrics::with_local_recorder(&self.inner.recorder, || {
            if let Some(value) = budget.limit_usd {
                labels.record_gauge(M_BUDGET_LIMIT_USD, value);
            }
            if let Some(value) = budget.spent_usd {
                labels.record_gauge(M_BUDGET_SPENT_USD, value);
            }
            if let Some(value) = budget.remaining_usd {
                labels.record_gauge(M_BUDGET_REMAINING_USD, value);
            }
            if let Some(value) = budget.reset_seconds {
                labels.record_gauge(M_BUDGET_RESET_SECONDS, value as f64);
            }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/aisix-obs/src/metrics.rs Outdated
Comment thread crates/aisix-proxy/src/lib.rs Outdated
Comment thread tests/e2e/src/cases/prometheus-metrics-e2e.test.ts
Comment thread crates/aisix-proxy/src/chat.rs Outdated
Copilot AI review requested due to automatic review settings May 18, 2026 07:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

crates/aisix-proxy/src/messages.rs:677

  • record_llm_usage for /v1/messages uses ..UsageLabels::default(), so upstream_model and provider_key_id will always be "unknown" in usage/token/spend metrics even though UsageEvent already carries provider_model_version (and the handler resolves the provider key). Consider passing those through into UsageLabels so the AISIX-native usage series can be grouped the same way as the rest of the request metrics.
    state.metrics.record_llm_usage(
        UsageLabels {
            endpoint: "/v1/messages",
            inbound_protocol: "anthropic",
            provider,
            model,
            api_key_id,
            team_id: team_id.unwrap_or("unknown"),
            owner_id: owner_id.unwrap_or("unknown"),
            ..UsageLabels::default()
        },

Comment thread crates/aisix-obs/src/metrics.rs
Comment thread crates/aisix-obs/src/metrics.rs Outdated
Comment thread crates/aisix-proxy/src/chat.rs
Comment thread crates/aisix-proxy/src/messages.rs
Comment thread crates/aisix-admin/src/lib.rs
Comment thread tests/e2e/src/harness/app.ts Outdated
@nic-6443
nic-6443 merged commit e6125b6 into main May 18, 2026
8 checks passed
@nic-6443
nic-6443 deleted the feat-prometheus-metrics-284 branch May 18, 2026 07:58
@nic-6443 nic-6443 linked an issue May 18, 2026 that may be closed by this pull request
janiussyafiq added a commit that referenced this pull request May 18, 2026
…s fields

PR #331 (commit e6125b6) wired the `metrics.prometheus.enabled` and
`metrics.prometheus.path` consumers in `crates/aisix-admin/src/lib.rs:154-159`
plus the `normalized_prometheus_path` helper at `:164-174`, and exercised
both paths end-to-end in three new unit tests
(`metrics_endpoint_uses_configured_path`,
`metrics_endpoint_normalizes_configured_path`,
`metrics_endpoint_can_be_disabled`).

The two rows in this PR's observability field table that called those
fields "reserved (not yet consulted)" are now factually stale. Update
both rows to `wired` and align the row descriptions with #331's
runtime behaviour. Also adjust the introductory paragraph above the
table so it no longer asserts that only `service_name` and `log_level`
are consulted at runtime.

This is the consumer-trace discipline locked in during round 2 of this
PR, applied prospectively to a moving target: PR #331 changed the
consumer site after round 2's verification window, so the doc text
needs to update.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: supports integrating Prometheus for metrics statistics

3 participants