наблюдаемость: ввести безопасный контракт метрик
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s

This commit is contained in:
2026-07-31 05:04:01 +03:00
parent ec2453c00f
commit 9b1a739e39
50 changed files with 3066 additions and 433 deletions
+2 -1
View File
@@ -24,7 +24,6 @@ crank-registry = { path = "../../crates/crank-registry" }
crank-runtime = { path = "../../crates/crank-runtime" }
crank-schema = { path = "../../crates/crank-schema" }
crank-trace = { path = "../../crates/crank-trace" }
metrics.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -41,6 +40,8 @@ uuid.workspace = true
[dev-dependencies]
async-trait = "0.1"
crank-test-support = { path = "../../crates/crank-test-support" }
metrics.workspace = true
metrics-util = "0.20.4"
opentelemetry.workspace = true
opentelemetry_sdk.workspace = true
reqwest.workspace = true
+1
View File
@@ -3,6 +3,7 @@ pub mod auth;
pub mod dto;
pub mod error;
pub mod import_guidance;
pub mod pool_metrics;
pub mod rate_limit;
pub mod request_context;
pub mod routes;
+2 -11
View File
@@ -3,6 +3,7 @@ use std::{env, net::SocketAddr, path::PathBuf, time::Duration};
use admin_api::{
app::build_app,
auth::{AuthSettings, BootstrapAdminConfig},
pool_metrics::spawn_postgres_pool_metrics,
service::AdminServiceBuilder,
state::AppState,
};
@@ -16,7 +17,7 @@ use crank_runtime::{
RequestRateLimitConfig, RequestRateLimiter, RuntimeCacheConfig, RuntimeCacheStores,
RuntimeLimits, SecretCrypto,
};
use sqlx::{PgPool, postgres::PgConnectOptions};
use sqlx::postgres::PgConnectOptions;
use tokio::net::TcpListener;
use tracing::{info, warn};
@@ -196,16 +197,6 @@ fn spawn_invocation_log_cleanup(service: admin_api::service::AdminService, reten
});
}
fn spawn_postgres_pool_metrics(pool: PgPool) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
crank_observability::record_db_pool_connections(pool.size(), pool.num_idle());
}
});
}
fn env_flag(name: &str) -> bool {
matches!(
env::var(name)
+13
View File
@@ -0,0 +1,13 @@
use std::time::Duration;
use sqlx::PgPool;
pub fn spawn_postgres_pool_metrics(pool: PgPool) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
crank_observability::record_db_pool_connections(pool.size(), pool.num_idle());
}
})
}
+89
View File
@@ -0,0 +1,89 @@
#[path = "integration/common.rs"]
mod common;
use std::{collections::BTreeSet, time::Duration};
use common::*;
use metrics_util::debugging::DebuggingRecorder;
#[tokio::test(flavor = "multi_thread")]
async fn real_admin_routes_and_postgres_sampler_emit_bounded_metrics() {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
recorder
.install()
.expect("isolated integration test recorder");
let registry = test_registry().await;
let sampler = admin_api::pool_metrics::spawn_postgres_pool_metrics(registry.pool().clone());
let server = spawn_admin_api(build_test_app(
registry,
test_storage_root("product_metrics"),
))
.await;
let client = authorized_client(&server).await;
client
.get(format!("{server}/operations"))
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let snapshot = wait_for_metrics(&snapshotter).await;
assert!(snapshot.iter().any(|(key, _, _, _)| {
key.key().name() == "crank_http_requests_total"
&& has_label(
key.key(),
"route",
"/api/admin/workspaces/{workspace_id}/operations",
)
&& has_label(key.key(), "method", "GET")
&& has_label(key.key(), "status_class", "2xx")
}));
let pool_states = snapshot
.iter()
.filter(|(key, _, _, _)| key.key().name() == "crank_db_pool_connections")
.filter_map(|(key, _, _, _)| {
key.key()
.labels()
.find(|label| label.key() == "state")
.map(|label| label.value().to_owned())
})
.collect::<BTreeSet<_>>();
assert_eq!(
pool_states,
BTreeSet::from(["idle".to_owned(), "used".to_owned()])
);
sampler.abort();
}
async fn wait_for_metrics(
snapshotter: &metrics_util::debugging::Snapshotter,
) -> Vec<(
metrics_util::CompositeKey,
Option<metrics::Unit>,
Option<metrics::SharedString>,
metrics_util::debugging::DebugValue,
)> {
for _ in 0..50 {
let snapshot = snapshotter.snapshot().into_vec();
let has_http = snapshot
.iter()
.any(|(key, _, _, _)| key.key().name() == "crank_http_requests_total");
let has_pool = snapshot
.iter()
.any(|(key, _, _, _)| key.key().name() == "crank_db_pool_connections");
if has_http && has_pool {
return snapshot;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("product metrics did not become visible");
}
fn has_label(key: &metrics::Key, name: &str, value: &str) -> bool {
key.labels()
.any(|label| label.key() == name && label.value() == value)
}