-
Notifications
You must be signed in to change notification settings - Fork 34
feat(analytics): add read-only analytics API endpoints (#238) #243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| pub mod analytics; | ||
| pub mod hybrid_routing; | ||
| // pub mod data; | ||
| pub mod decide_gateway; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,336 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::collections::HashMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::Arc; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use axum::{extract::Query, Json}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::tenant::GlobalAppState; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Shared types | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct TimeRangeParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Time range: 15m, 1h, 6h, 24h, 7d | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub range: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Bucket granularity: 10s, 1m, 5m, 1h | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub granularity: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct GatewayScoreParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub merchant: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub pmt: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub gateway: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(flatten)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub time: TimeRangeParams, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct DecisionParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub group_by: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[serde(flatten)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub time: TimeRangeParams, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+34
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct TimeRangeParams { | |
| /// Time range: 15m, 1h, 6h, 24h, 7d | |
| pub range: Option<String>, | |
| /// Bucket granularity: 10s, 1m, 5m, 1h | |
| pub granularity: Option<String>, | |
| } | |
| #[derive(Debug, Deserialize)] | |
| pub struct GatewayScoreParams { | |
| pub merchant: Option<String>, | |
| pub pmt: Option<String>, | |
| pub gateway: Option<String>, | |
| #[serde(flatten)] | |
| pub time: TimeRangeParams, | |
| } | |
| #[derive(Debug, Deserialize)] | |
| pub struct DecisionParams { | |
| pub group_by: Option<String>, | |
| #[serde(flatten)] | |
| pub time: TimeRangeParams, | |
| pub struct GatewayScoreParams { | |
| pub merchant: Option<String>, | |
| pub pmt: Option<String>, | |
| pub gateway: Option<String>, | |
| } | |
| #[derive(Debug, Deserialize)] | |
| pub struct DecisionParams { | |
| pub group_by: Option<String>, |
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collect_total_counts calls prometheus::gather(). Since handlers also call collect_status_counts (which gathers again), a single request typically gathers all metrics twice. Consider gathering once per request and passing the gathered MetricFamily list into both parsing functions (or a single function that extracts both totals and status counts).
| /// Collect per-endpoint totals from `API_REQUEST_TOTAL_COUNTER`. | |
| fn collect_total_counts() -> HashMap<String, u64> { | |
| let mut totals: HashMap<String, u64> = HashMap::new(); | |
| let metric_families = prometheus::gather(); | |
| for mf in &metric_families { | |
| /// Collect per-endpoint totals from an already-gathered Prometheus snapshot. | |
| fn collect_total_counts( | |
| metric_families: &[prometheus::proto::MetricFamily], | |
| ) -> HashMap<String, u64> { | |
| let mut totals: HashMap<String, u64> = HashMap::new(); | |
| for mf in metric_families { |
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as collect_total_counts: this function calls prometheus::gather() again, so most analytics handlers gather the entire Prometheus registry twice per request. Refactor to share a single gather result across both totals + status extraction to reduce overhead on the hot path.
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
group_by is described as a grouping control, but here it only switches between hard-coded endpoint lists and the response is still per-endpoint (no grouping by gateway/approach is actually performed). This also makes group_by=approach misleading since no "approach" label is available from these metrics. Consider rejecting unsupported group_by values with a 400, or implementing real grouping semantics that match the documented API.
Copilot
AI
Apr 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RoutingStatsParams includes a range query param but the handler ignores it entirely. Either apply the range to the returned stats (e.g., windowed rates) or remove the param to avoid silently ignoring client input.
| pub async fn routing_stats( | |
| Query(_params): Query<RoutingStatsParams>, | |
| ) -> Json<RoutingStatsResponse> { | |
| pub async fn routing_stats() -> Json<RoutingStatsResponse> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merchantandpmtquery params are accepted but never used for filtering, andgatewayfiltering is implemented as a substring match against the endpoint label. Consider either (a) implementing merchant/pmt filtering, and renaming the filter toendpoint(or making it an exact match) to avoid implying this filters by PSP/gateway name.