use metrics::Unit;
pub const HTTP_REQUESTS_TOTAL: &str = "schwab_http_requests_total";
pub const HTTP_REQUEST_DURATION: &str = "schwab_http_request_duration_seconds";
pub const SUBMISSION_UNKNOWN_TOTAL: &str = "schwab_submission_unknown_total";
pub const RETRY_ATTEMPTS_TOTAL: &str = "schwab_retry_attempts_total";
pub const CIRCUIT_STATE: &str = "schwab_circuit_state";
pub const CIRCUIT_TRANSITIONS_TOTAL: &str = "schwab_circuit_transitions_total";
pub const TOKEN_REFRESH_TOTAL: &str = "schwab_token_refresh_total";
pub const RECONCILIATION_TOTAL: &str = "schwab_reconciliation_total";
pub const UNTAGGED_MATCHES_TOTAL: &str = "schwab_reconciliation_untagged_matches_total";
pub const PENDING_SUBMISSIONS: &str = "schwab_pending_submissions";
pub const UNRECONCILED_ACCOUNTS: &str = "schwab_unreconciled_accounts";
pub const BASELINE_AGE_SECONDS: &str = "schwab_baseline_age_seconds";
pub const TRADING_READY: &str = "schwab_trading_ready";
pub const RECOVERY_READY: &str = "schwab_recovery_ready";
pub const RECOVERY_FAILURES_TOTAL: &str = "schwab_recovery_failures_total";
pub const STREAM_CONNECTS_TOTAL: &str = "schwab_stream_connects_total";
pub const STREAM_RECONNECTS_TOTAL: &str = "schwab_stream_reconnects_total";
pub const STREAM_CONNECTED: &str = "schwab_stream_connected";
pub const STREAM_MESSAGES_TOTAL: &str = "schwab_stream_messages_total";
pub const STREAM_DROPPED_MESSAGES_TOTAL: &str = "schwab_stream_dropped_messages_total";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricKind {
Counter,
Gauge,
Histogram,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Metric {
pub name: &'static str,
pub kind: MetricKind,
pub unit: Option<Unit>,
pub help: &'static str,
}
impl Metric {
pub const fn counter(name: &'static str, help: &'static str) -> Self {
Self {
name,
kind: MetricKind::Counter,
unit: Some(Unit::Count),
help,
}
}
pub const fn gauge(name: &'static str, unit: Option<Unit>, help: &'static str) -> Self {
Self {
name,
kind: MetricKind::Gauge,
unit,
help,
}
}
pub const fn histogram(name: &'static str, unit: Unit, help: &'static str) -> Self {
Self {
name,
kind: MetricKind::Histogram,
unit: Some(unit),
help,
}
}
}
pub const CATALOG: &[Metric] = &[
Metric::counter(
HTTP_REQUESTS_TOTAL,
"Logical Schwab API request completions, regardless of retries",
),
Metric::histogram(
HTTP_REQUEST_DURATION,
Unit::Seconds,
"Schwab API request wall-clock time including retries and token refresh",
),
Metric::counter(
SUBMISSION_UNKNOWN_TOTAL,
"Order submissions sent with an undetermined outcome; each needs reconciliation",
),
Metric::counter(
RETRY_ATTEMPTS_TOTAL,
"Retries fired by the retry policy, by final outcome",
),
Metric::gauge(
CIRCUIT_STATE,
None,
"Circuit breaker state: 0 closed, 1 half-open, 2 open",
),
Metric::counter(CIRCUIT_TRANSITIONS_TOTAL, "Circuit breaker state changes"),
Metric::counter(
TOKEN_REFRESH_TOTAL,
"OAuth token endpoint requests, by grant and outcome",
),
Metric::counter(
RECONCILIATION_TOTAL,
"Account reconciliation runs, by outcome",
),
Metric::counter(
UNTAGGED_MATCHES_TOTAL,
"Unresolved submissions with a matching broker order that lacks their tag",
),
Metric::gauge(
PENDING_SUBMISSIONS,
Some(Unit::Count),
"Order submissions awaiting reconciliation at the last readiness probe",
),
Metric::gauge(
UNRECONCILED_ACCOUNTS,
Some(Unit::Count),
"Required accounts without a fresh baseline at the last readiness probe",
),
Metric::gauge(
BASELINE_AGE_SECONDS,
Some(Unit::Seconds),
"Age of the oldest required-account baseline at the last readiness probe",
),
Metric::gauge(
TRADING_READY,
None,
"1 when the last readiness probe found trading admissible",
),
Metric::gauge(
RECOVERY_READY,
None,
"1 while the recovery worker has no accounts left to reconcile",
),
Metric::counter(
RECOVERY_FAILURES_TOTAL,
"Reconciliation passes the recovery worker could not complete",
),
Metric::counter(
STREAM_CONNECTS_TOTAL,
"Streaming connection attempts, by outcome",
),
Metric::counter(STREAM_RECONNECTS_TOTAL, "Streaming reconnects triggered"),
Metric::gauge(
STREAM_CONNECTED,
None,
"1 while the streaming session is authenticated",
),
Metric::counter(
STREAM_MESSAGES_TOTAL,
"Streaming frames delivered to the receiver, by service and kind",
),
Metric::counter(
STREAM_DROPPED_MESSAGES_TOTAL,
"Streaming frames dropped because the consumer queue was full or closed",
),
];
pub fn describe() {
describe_all(CATALOG);
}
pub fn describe_all(catalog: &[Metric]) {
metrics::with_recorder(|recorder| {
for metric in catalog {
let (name, help) = (metric.name.into(), metric.help.into());
match metric.kind {
MetricKind::Counter => recorder.describe_counter(name, metric.unit, help),
MetricKind::Gauge => recorder.describe_gauge(name, metric.unit, help),
MetricKind::Histogram => recorder.describe_histogram(name, metric.unit, help),
}
}
});
}
pub fn status_class(status: u16) -> &'static str {
match status {
100..=199 => "1xx",
200..=299 => "2xx",
300..=399 => "3xx",
400..=499 => "4xx",
500..=599 => "5xx",
_ => "unknown",
}
}
pub fn circuit_state_value(state: crate::circuit_breaker::CircuitState) -> f64 {
use crate::circuit_breaker::CircuitState;
match state {
CircuitState::Closed => 0.0,
CircuitState::HalfOpen => 1.0,
CircuitState::Open => 2.0,
}
}
pub fn circuit_state_name(state: crate::circuit_breaker::CircuitState) -> &'static str {
use crate::circuit_breaker::CircuitState;
match state {
CircuitState::Closed => "closed",
CircuitState::HalfOpen => "half_open",
CircuitState::Open => "open",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_class_buckets() {
assert_eq!(status_class(200), "2xx");
assert_eq!(status_class(204), "2xx");
assert_eq!(status_class(401), "4xx");
assert_eq!(status_class(429), "4xx");
assert_eq!(status_class(503), "5xx");
assert_eq!(status_class(999), "unknown");
}
}