v0.34.2
This commit is contained in:
1 parent
34fb534e02
commit
b2b5b45d8a
22 files changed
+2146
-107
No files matched your search
@@ -20,6 +20,8 @@ fn general_api_token_allowed(method: &Method, path: &str, permission: &str) -> b
|
||||
| "/api/file-logs"
|
||||
| "/api/file-logs/query"
|
||||
| "/api/metrics"
|
||||
| "/api/metrics/json"
|
||||
| "/api/metrics/summary"
|
||||
| "/api/cache/bans"
|
||||
);
|
||||
let write = permission == "rw"
|
||||
|
||||
@@ -298,6 +298,12 @@ async fn audit(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||
async fn metrics(State(state): State<Arc<App>>) -> Response {
|
||||
crate::prometheus::response(&state)
|
||||
}
|
||||
async fn metrics_json(State(state): State<Arc<App>>) -> Response {
|
||||
crate::prometheus::json_response(&state)
|
||||
}
|
||||
async fn metrics_summary(State(state): State<Arc<App>>) -> Response {
|
||||
crate::prometheus::summary_response(&state)
|
||||
}
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct EventsQuery {
|
||||
@@ -311,7 +317,10 @@ async fn events(
|
||||
) -> Result<Response, ApiError> {
|
||||
let interval = query.interval.unwrap_or(3);
|
||||
if ![1, 2, 3, 5, 10].contains(&interval) {
|
||||
return Err(ApiError(StatusCode::UNPROCESSABLE_ENTITY, "invalid SSE interval".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid SSE interval".into(),
|
||||
));
|
||||
}
|
||||
let secret = auth::cookie_secret(&headers)
|
||||
.ok_or_else(|| ApiError(StatusCode::UNAUTHORIZED, "auth.required".into()))?;
|
||||
@@ -349,7 +358,8 @@ async fn events(
|
||||
_ => return None,
|
||||
}
|
||||
let payload =
|
||||
json!({"status":live_status_payload(&state), "logs":logs_payload(&state)}).to_string();
|
||||
json!({"status":live_status_payload(&state), "logs":logs_payload(&state)})
|
||||
.to_string();
|
||||
let event = Event::default().event("snapshot").data(payload);
|
||||
Some((
|
||||
Ok::<Event, Infallible>(event),
|
||||
|
||||
+692
-1
@@ -6801,6 +6801,557 @@
|
||||
"database_path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MonitoringMetricSample": {
|
||||
"type": "object",
|
||||
"description": "Single metric sample. Values are JSON numbers; non-finite Prometheus values are null. Unlabelled samples use an empty labels object.",
|
||||
"properties": {
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"labels",
|
||||
"value"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MonitoringMetricFamilies": {
|
||||
"type": "object",
|
||||
"description": "All numeric samples returned by the same collectors as the Prometheus exporter, including histogram buckets, counts and sums.",
|
||||
"properties": {
|
||||
"types": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Metric-family name to Prometheus TYPE."
|
||||
},
|
||||
"samples": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MonitoringMetricSample"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"types",
|
||||
"samples"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MonitoringSnapshot": {
|
||||
"type": "object",
|
||||
"description": "Pull-only operational snapshot with a precomputed summary and all Prometheus samples. Summary per-second rates are computed over bounded polling intervals; the first sample has null rates. OS gauges may be null until the background collector samples.",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"collected_at_ms": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"vhosts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"enabled",
|
||||
"disabled",
|
||||
"items"
|
||||
]
|
||||
},
|
||||
"redirect_hosts": {
|
||||
"type": "object"
|
||||
},
|
||||
"not_found_hosts": {
|
||||
"type": "object"
|
||||
},
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"active": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"errors_total": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"status": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"active",
|
||||
"errors_total",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"system": {
|
||||
"type": "object"
|
||||
},
|
||||
"cache": {
|
||||
"type": "object"
|
||||
},
|
||||
"backends": {
|
||||
"type": "object"
|
||||
},
|
||||
"web_server": {
|
||||
"type": "object"
|
||||
},
|
||||
"streams": {
|
||||
"type": "object"
|
||||
},
|
||||
"docker": {
|
||||
"type": "object"
|
||||
},
|
||||
"traffic": {
|
||||
"type": "object"
|
||||
},
|
||||
"transform": {
|
||||
"type": "object"
|
||||
},
|
||||
"logging": {
|
||||
"type": "object"
|
||||
},
|
||||
"capacity": {
|
||||
"type": "object"
|
||||
},
|
||||
"prometheus": {
|
||||
"$ref": "#/components/schemas/MonitoringMetricFamilies"
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/components/schemas/MonitoringSummary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"summary",
|
||||
"version",
|
||||
"collected_at_ms",
|
||||
"uptime_seconds",
|
||||
"revision",
|
||||
"vhosts",
|
||||
"requests",
|
||||
"system",
|
||||
"cache",
|
||||
"backends",
|
||||
"web_server",
|
||||
"streams",
|
||||
"docker",
|
||||
"traffic",
|
||||
"transform",
|
||||
"logging",
|
||||
"capacity",
|
||||
"prometheus"
|
||||
]
|
||||
},
|
||||
"MonitoringSummary": {
|
||||
"type": "object",
|
||||
"description": "Compact read-only pull snapshot with computed counts, health states, bytes, percentages and rates. No Prometheus samples, paths, client addresses or secrets. Counters reset on process restart.",
|
||||
"properties": {
|
||||
"collected_at_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"vhosts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"serving_estimate": {
|
||||
"type": "integer",
|
||||
"description": "Enabled primary Web Server or ready backend pool; not an HTTP end-to-end health check."
|
||||
},
|
||||
"backend_ready": {
|
||||
"type": "integer"
|
||||
},
|
||||
"backend_not_ready": {
|
||||
"type": "integer"
|
||||
},
|
||||
"docker_enabled": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"enabled",
|
||||
"disabled",
|
||||
"serving_estimate",
|
||||
"backend_ready",
|
||||
"backend_not_ready",
|
||||
"docker_enabled"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"backends": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"description": "All currently compiled backend pool members, including Docker-generated pools."
|
||||
},
|
||||
"configured_static": {
|
||||
"type": "integer"
|
||||
},
|
||||
"up": {
|
||||
"type": "integer"
|
||||
},
|
||||
"down": {
|
||||
"type": "integer"
|
||||
},
|
||||
"checking": {
|
||||
"type": "integer"
|
||||
},
|
||||
"draining": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pools_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pools_ready": {
|
||||
"type": "integer"
|
||||
},
|
||||
"active_exchanges": {
|
||||
"type": "integer"
|
||||
},
|
||||
"traffic_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Sum of backend moving rates; excludes static/Web Server and local responses."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"configured_static",
|
||||
"up",
|
||||
"down",
|
||||
"checking",
|
||||
"draining",
|
||||
"disabled",
|
||||
"unknown",
|
||||
"pools_total",
|
||||
"pools_ready",
|
||||
"active_exchanges",
|
||||
"traffic_bytes_per_second"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"traffic": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requests_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_completed_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_active": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Rate between server-side polling snapshots; null until at least 1 second has elapsed or after a gap/reset."
|
||||
},
|
||||
"requests_per_second_average": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"errors_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"errors_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"error_percent": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "errors_total / completed_total * 100; lifetime ratio."
|
||||
},
|
||||
"status_2xx_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status_4xx_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status_5xx_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bytes_in_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bytes_out_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bytes_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"megabytes_total": {
|
||||
"type": "number",
|
||||
"description": "Decimal MB = (bytes_in + bytes_out)/1,000,000."
|
||||
},
|
||||
"bytes_in_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"bytes_out_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"sample_interval_seconds": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"network_rx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Host network rate, not RustProxy-only."
|
||||
},
|
||||
"network_tx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Host network rate, not RustProxy-only."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requests_total",
|
||||
"requests_completed_total",
|
||||
"requests_active",
|
||||
"requests_per_second",
|
||||
"requests_per_second_average",
|
||||
"errors_total",
|
||||
"errors_per_second",
|
||||
"error_percent",
|
||||
"status_2xx_total",
|
||||
"status_4xx_total",
|
||||
"status_5xx_total",
|
||||
"bytes_in_total",
|
||||
"bytes_out_total",
|
||||
"bytes_total",
|
||||
"megabytes_total",
|
||||
"bytes_in_per_second",
|
||||
"bytes_out_per_second",
|
||||
"bytes_per_second",
|
||||
"sample_interval_seconds",
|
||||
"network_rx_bytes_per_second",
|
||||
"network_tx_bytes_per_second"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"system": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"process_cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"memory_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"memory_used_bytes": {
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"memory_total_bytes": {
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"process_rss_bytes": {
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"sampled_at_ms": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cpu_percent",
|
||||
"process_cpu_percent",
|
||||
"memory_percent",
|
||||
"memory_used_bytes",
|
||||
"memory_total_bytes",
|
||||
"process_rss_bytes",
|
||||
"sampled_at_ms"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hits_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"misses_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hit_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"hot_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cold_entries": {
|
||||
"type": "integer"
|
||||
},
|
||||
"hot_bytes": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cold_bytes": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hits_total",
|
||||
"misses_total",
|
||||
"hit_percent",
|
||||
"hot_entries",
|
||||
"cold_entries",
|
||||
"hot_bytes",
|
||||
"cold_bytes"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"web_server": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services_total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_total": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"services_total",
|
||||
"requests_total"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"streams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"active_sessions": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"enabled",
|
||||
"active_sessions"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capacity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requests_limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_used": {
|
||||
"type": "integer"
|
||||
},
|
||||
"requests_used_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requests_limit",
|
||||
"requests_used",
|
||||
"requests_used_percent"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"collected_at_ms",
|
||||
"uptime_seconds",
|
||||
"vhosts",
|
||||
"backends",
|
||||
"traffic",
|
||||
"system",
|
||||
"cache",
|
||||
"web_server",
|
||||
"streams",
|
||||
"capacity"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9492,6 +10043,76 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/metrics/json": {
|
||||
"get": {
|
||||
"summary": "Read-only JSON metrics snapshot for dashboards and integrations",
|
||||
"tags": [
|
||||
"Monitoring"
|
||||
],
|
||||
"operationId": "get_metrics_json",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Bounded operational JSON snapshot, not a streaming feed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MonitoringSnapshot"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authentication required or revoked",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Host, origin or CSRF validation failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Bounded worker/session/rate limit",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Control-plane storage/worker unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Single GET JSON snapshot with summary (aggregated counts, percentages and bounded rates), Overview counters/gauges and every sample from the Prometheus exporter (under prometheus.types/samples). Same admin session or CIDR-scoped general RO/RW API token as /api/metrics. No SSE, request paths, IPs or raw User-Agent values. Prometheus exporter sibling: {configured metrics path}.json; the same exporter credentials/CIDRs apply.",
|
||||
"security": [
|
||||
{
|
||||
"AdminSession": []
|
||||
},
|
||||
{
|
||||
"GeneralApiBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/docs/openapi.json": {
|
||||
"get": {
|
||||
"summary": "This protected OpenAPI document",
|
||||
@@ -12841,7 +13462,77 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/metrics/summary": {
|
||||
"get": {
|
||||
"summary": "Compact computed live metrics summary for external dashboards",
|
||||
"tags": [
|
||||
"Monitoring"
|
||||
],
|
||||
"operationId": "get_metrics_summary",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Small JSON-only computed metrics summary",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MonitoringSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authentication required or revoked",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Host, origin or CSRF validation failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Bounded worker/session/rate limit",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Control-plane storage/worker unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Read-only compact JSON summary of vhosts, backend pools, total HTTP traffic, rolling per-second request and byte rates, CPU, RAM, cache, Web Server and streams. A first read or gap/reset produces null rolling rates; repeat GET at 2-5 second intervals. No SSE, no detailed Prometheus samples. Auth: admin session or CIDR-scoped General API Bearer (ro/rw). The separate enabled exporter offers {configured metrics path}.summary.json, protected by its own bearer/CIDR policy.",
|
||||
"security": [
|
||||
{
|
||||
"AdminSession": []
|
||||
},
|
||||
{
|
||||
"GeneralApiBearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -200,9 +200,17 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
"/api/certificates/{certificate_id}/reload",
|
||||
post(certificate_reload_one).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/api-tokens", get(list_api_tokens).post(create_api_token))
|
||||
.route(
|
||||
"/api/api-tokens",
|
||||
get(list_api_tokens).post(create_api_token),
|
||||
)
|
||||
.route("/api/api-tokens/{id}", delete(revoke_api_token))
|
||||
.route("/api/prometheus-token", get(prometheus_token_status).post(rotate_prometheus_token).delete(revoke_prometheus_token))
|
||||
.route(
|
||||
"/api/prometheus-token",
|
||||
get(prometheus_token_status)
|
||||
.post(rotate_prometheus_token)
|
||||
.delete(revoke_prometheus_token),
|
||||
)
|
||||
.route(
|
||||
"/api/certificate-api",
|
||||
get(certificate_api_status)
|
||||
@@ -267,6 +275,8 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/api/events", get(events))
|
||||
.route("/metrics", get(metrics))
|
||||
.route("/api/metrics", get(metrics))
|
||||
.route("/api/metrics/json", get(metrics_json))
|
||||
.route("/api/metrics/summary", get(metrics_summary))
|
||||
.fallback(|| async { reject(StatusCode::NOT_FOUND, "not_found") })
|
||||
.layer(DefaultBodyLimit::max(
|
||||
crate::config::MAX_CONFIG_BYTES + 1024,
|
||||
|
||||
@@ -2250,6 +2250,7 @@ mod tests {
|
||||
"/api/acme",
|
||||
"/api/auth/session",
|
||||
"/api/metrics",
|
||||
"/api/metrics/json",
|
||||
"/metrics",
|
||||
"/docs/openapi.json",
|
||||
"/docs/content.json",
|
||||
@@ -3664,6 +3665,84 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn json_metrics_api_is_authenticated_and_matches_prometheus_families() {
|
||||
let (router, store, _dir, secret, _) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let snapshot = router.clone().oneshot(
|
||||
request("/api/metrics/json").header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(snapshot.status(), StatusCode::OK);
|
||||
assert!(snapshot.headers()["content-type"].to_str().unwrap().starts_with("application/json"));
|
||||
let snapshot = json_body(snapshot).await;
|
||||
for field in ["summary", "vhosts", "requests", "system", "cache", "backends", "web_server",
|
||||
"streams", "docker", "traffic", "transform", "logging", "capacity", "prometheus"] {
|
||||
assert!(snapshot.get(field).is_some(), "missing: {field}");
|
||||
}
|
||||
assert_eq!(snapshot["vhosts"]["total"], 0);
|
||||
assert_eq!(snapshot["vhosts"]["enabled"], 0);
|
||||
assert_eq!(snapshot["vhosts"]["disabled"], 0);
|
||||
assert_eq!(snapshot["summary"]["vhosts"]["enabled"], 0);
|
||||
assert_eq!(snapshot["summary"]["backends"]["total"], 0);
|
||||
assert!(snapshot["summary"]["traffic"]["bytes_total"].is_number());
|
||||
assert!(snapshot["prometheus"]["samples"]["rust_proxy_requests_total"].is_array());
|
||||
assert_eq!(snapshot["prometheus"]["types"]["rust_proxy_requests_total"], "counter");
|
||||
assert!(general_api_token_allowed(&Method::GET, "/api/metrics/json", "ro"));
|
||||
assert!(general_api_token_allowed(&Method::GET, "/api/metrics/summary", "ro"));
|
||||
assert!(!general_api_token_allowed(&Method::POST, "/api/metrics/json", "rw"));
|
||||
assert!(!general_api_token_allowed(&Method::POST, "/api/metrics/summary", "rw"));
|
||||
let summary = router.clone().oneshot(
|
||||
request("/api/metrics/summary").header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(summary.status(), StatusCode::OK);
|
||||
assert_eq!(summary.headers()["cache-control"], "no-store");
|
||||
let summary = json_body(summary).await;
|
||||
assert!(summary.get("prometheus").is_none());
|
||||
assert_eq!(summary["vhosts"]["enabled"], snapshot["summary"]["vhosts"]["enabled"]);
|
||||
assert_eq!(summary["backends"]["total"], snapshot["summary"]["backends"]["total"]);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn json_metrics_accepts_ro_bearer_only_from_allowed_peer() {
|
||||
let (router, store, _dir, _, _) = fixture().await;
|
||||
let token = "T".repeat(64);
|
||||
let cidrs = vec!["127.0.0.1/32".to_string()];
|
||||
assert!(store.create_api_token(
|
||||
"api-monitor", &auth::secret_hash(&token), "ro", "monitor test", &cidrs, "owner"
|
||||
).await.unwrap());
|
||||
let build = |peer: Option<&str>| {
|
||||
let mut request = request("/api/metrics/json")
|
||||
.header("authorization", format!("Bearer {token}"));
|
||||
if let Some(peer) = peer {
|
||||
request = request.extension(PeerAddr(peer.parse().unwrap()));
|
||||
}
|
||||
request.body(Body::empty()).unwrap()
|
||||
};
|
||||
let response = router.clone().oneshot(build(None)).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
let response = router.clone().oneshot(build(Some("127.0.0.1:12345"))).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response = json_body(response).await;
|
||||
assert!(response["prometheus"]["samples"].is_object());
|
||||
assert!(response.get("config").is_none());
|
||||
let summarize = |peer: Option<&str>| {
|
||||
let mut req = request("/api/metrics/summary")
|
||||
.header("authorization", format!("Bearer {token}"));
|
||||
if let Some(peer) = peer { req = req.extension(PeerAddr(peer.parse().unwrap())); }
|
||||
req.body(Body::empty()).unwrap()
|
||||
};
|
||||
assert_eq!(router.clone().oneshot(summarize(None)).await.unwrap().status(), StatusCode::FORBIDDEN);
|
||||
let summary = router.oneshot(summarize(Some("127.0.0.1:12345"))).await.unwrap();
|
||||
assert_eq!(summary.status(), StatusCode::OK);
|
||||
let summary = json_body(summary).await;
|
||||
assert!(summary["traffic"]["requests_total"].is_number());
|
||||
assert!(summary.get("prometheus").is_none());
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_metadata_is_exposed_only_through_configuration_get_to_api_tokens() {
|
||||
assert!(general_api_token_allowed(&Method::GET, "/api/config", "ro"));
|
||||
|
||||
Reference in new issue
Block a user