use std::sync::Arc;
use axum::response::IntoResponse;
use http::StatusCode;
use http::header::CONTENT_TYPE;
use opentelemetry_prometheus_text_exporter::PrometheusExporter;
use super::metrics::MetricsObserver;
use super::public::is_public_metric;
use super::session::OperatorAuth;
const PROMETHEUS_CONTENT_TYPE: &str = "text/plain; version=0.0.4";
#[derive(Clone)]
pub struct MetricsState {
pub exporter: PrometheusExporter,
pub observer: Arc<MetricsObserver>,
}
#[instrument(level = "debug", name = "observe::metrics", skip_all)]
pub async fn metrics(
axum::Extension(state): axum::Extension<MetricsState>,
OperatorAuth(is_operator): OperatorAuth,
) -> axum::response::Response {
match render_snapshot(&state.exporter, is_operator) {
Ok(buf) => (StatusCode::OK, [(CONTENT_TYPE, PROMETHEUS_CONTENT_TYPE)], buf).into_response(),
Err(err) => {
error!("failed to encode metrics: {err}");
(StatusCode::INTERNAL_SERVER_ERROR, "failed to encode metrics").into_response()
}
}
}
pub(crate) fn render_snapshot(
exporter: &PrometheusExporter,
is_operator: bool,
) -> Result<Vec<u8>, anyhow::Error> {
let mut buf = Vec::with_capacity(4096);
exporter.export(&mut buf).map_err(|e| anyhow::anyhow!("{e}"))?;
if is_operator {
return Ok(buf);
}
let text = std::str::from_utf8(&buf)
.map_err(|e| anyhow::anyhow!("non-utf8 prometheus output: {e}"))?;
Ok(filter_to_public_metrics(text).into_bytes())
}
fn filter_to_public_metrics(text: &str) -> String {
let mut out = String::with_capacity(text.len() / 4);
let mut keep_block = false;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("# HELP ") {
let name = rest.split_whitespace().next().unwrap_or("");
keep_block = is_public_metric(name);
} else if let Some(rest) = line.strip_prefix("# TYPE ") {
let name = rest.split_whitespace().next().unwrap_or("");
keep_block = is_public_metric(name);
}
if keep_block {
out.push_str(line);
out.push('\n');
}
}
out
}
#[cfg(test)]
mod tests {
use opentelemetry_prometheus_text_exporter::PrometheusExporter;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use surrealdb_core::observe::{
AuthAction, AuthEvent, AuthEventCtx, AuthEventSafe, AuthScope, ExecutionObserver,
HttpMethod, HttpRequestEvent, HttpRequestEventCtx, HttpRequestEventSafe,
HttpRequestStartEvent, HttpRequestStartEventSafe, HttpVersion, NetworkBytesEvent,
NetworkBytesEventCtx, NetworkBytesEventSafe, NetworkDirection, Outcome, QueryCounters,
QueryEvent, QueryEventCtx, QueryEventSafe, RpcEvent, RpcEventCtx, RpcEventSafe,
SessionAction, SessionEvent, SessionEventCtx, SessionEventSafe, SessionProtocol,
StatementEvent, StatementEventCtx, StatementEventSafe, StatementType, TransactionEvent,
TransactionEventCtx, TransactionEventSafe, TransactionMetricsSnapshot,
};
use super::*;
use crate::observe::ObservabilityRuntime;
#[test]
fn filter_drops_non_public_families() {
let raw = "\
# HELP surrealdb_build_info Build info
# TYPE surrealdb_build_info gauge
surrealdb_build_info{build_version=\"3.0\"} 1
# HELP surrealdb_statement_total Statements
# TYPE surrealdb_statement_total counter
surrealdb_statement_total{outcome=\"success\"} 42
# HELP surrealdb_process_uptime_seconds Uptime
# TYPE surrealdb_process_uptime_seconds gauge
surrealdb_process_uptime_seconds 99
";
let filtered = filter_to_public_metrics(raw);
assert!(filtered.contains("surrealdb_build_info"));
assert!(filtered.contains("surrealdb_process_uptime_seconds"));
assert!(!filtered.contains("surrealdb_statement_total"));
}
const CANARY: &str = "xCANARYx-tenant-secret-xyz";
fn fresh_runtime() -> (SdkMeterProvider, PrometheusExporter, ObservabilityRuntime) {
let exporter = PrometheusExporter::builder().build();
let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
let runtime = ObservabilityRuntime::builder(provider.clone())
.with_prometheus_exporter(exporter.clone())
.build();
(provider, exporter, runtime)
}
fn dump(exporter: &PrometheusExporter) -> String {
let mut buf = Vec::with_capacity(4096);
exporter.export(&mut buf).expect("export");
String::from_utf8(buf).expect("utf8")
}
#[tokio::test]
async fn no_tenant_canary_in_unauthenticated_prometheus_output() {
let (provider, exporter, runtime) = fresh_runtime();
let metrics_obs = MetricsObserver::new(&runtime).expect("metrics observer");
let stmt = StatementEvent {
safe: StatementEventSafe {
kind: StatementType::Select,
outcome: Outcome::Success,
duration: std::time::Duration::from_millis(2),
read_only: true,
result_rows: 0,
error_class: None,
},
ctx: StatementEventCtx {
sql: Some(format!("SELECT * FROM {CANARY}")),
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
};
metrics_obs.on_statement_complete(&stmt);
metrics_obs.on_query_complete(&QueryEvent {
safe: QueryEventSafe {
outcome: Outcome::Success,
duration: std::time::Duration::from_millis(1),
counters: QueryCounters::default(),
error_class: None,
},
ctx: QueryEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
});
let txn_event = TransactionEvent {
safe: TransactionEventSafe {
outcome: Outcome::Success,
write: true,
duration: std::time::Duration::from_millis(1),
metrics: TransactionMetricsSnapshot::default(),
error_class: None,
},
ctx: TransactionEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
};
metrics_obs.on_transaction_complete(&txn_event);
let rpc_event = RpcEvent {
safe: RpcEventSafe {
method: surrealdb_core::rpc::Method::Select,
outcome: Outcome::Success,
duration: std::time::Duration::from_millis(1),
error_class: None,
},
ctx: RpcEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
};
metrics_obs.on_rpc_complete(&rpc_event);
metrics_obs.on_auth_event(&AuthEvent {
safe: AuthEventSafe {
action: AuthAction::Signin,
scope: AuthScope::Root,
outcome: Outcome::Success,
error_class: None,
},
ctx: AuthEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
});
let session_event = SessionEvent {
safe: SessionEventSafe {
action: SessionAction::Connect,
protocol: SessionProtocol::WebSocket,
duration: None,
},
ctx: SessionEventCtx {
session_id: None,
service_name: Some(CANARY.into()),
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
client_ip: None,
},
};
metrics_obs.on_session_event(&session_event);
let bytes_event = NetworkBytesEvent {
safe: NetworkBytesEventSafe {
direction: NetworkDirection::Received,
protocol: SessionProtocol::WebSocket,
bytes: 64,
},
ctx: NetworkBytesEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
},
};
metrics_obs.on_network_bytes(&bytes_event);
let http_start = HttpRequestStartEvent {
safe: HttpRequestStartEventSafe {
method: HttpMethod::Get,
route: Some("/sql"),
version: HttpVersion::Http11,
},
ctx: HttpRequestEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
};
metrics_obs.on_http_request_started(&http_start);
let http_complete = HttpRequestEvent {
safe: HttpRequestEventSafe {
method: HttpMethod::Get,
route: Some("/sql"),
status_code: Some(200),
version: HttpVersion::Http11,
outcome: Outcome::Success,
duration: std::time::Duration::from_millis(2),
request_size: Some(64),
response_size: Some(128),
error_class: None,
},
ctx: HttpRequestEventCtx {
namespace: Some(CANARY.into()),
database: Some(CANARY.into()),
user: Some(CANARY.into()),
session_id: None,
client_ip: None,
},
};
metrics_obs.on_http_request_complete(&http_complete);
let body = dump(&exporter);
assert!(
body.contains(CANARY),
"events did not record: canary should appear on unfiltered output\n{body}",
);
let filtered = filter_to_public_metrics(&body);
assert!(
!filtered.contains(CANARY),
"tenant canary leaked into UNAUTHENTICATED Prometheus output:\n{filtered}",
);
provider.shutdown().expect("shutdown");
}
}