наблюдаемость: завершить базовый контур Community
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
@@ -1,24 +1,27 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crank_core::{CacheScope, CoordinationStateStore, CoordinationStateValue};
|
||||
use crank_registry::{PostgresRegistry, PublishedAgentCatalog, PublishedAgentTool, RegistryError};
|
||||
use crank_trace::{DbOperation, ErrorCategory, Stage, StageOutcome};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{info, warn};
|
||||
use tracing::{Instrument, info, warn};
|
||||
|
||||
use crate::manifest::analyze_published_tool_catalog;
|
||||
|
||||
const MAX_LOCAL_CATALOGS: usize = 1_024;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PublishedToolCatalog {
|
||||
registry: PostgresRegistry,
|
||||
refresh_interval: Duration,
|
||||
coordination_store: Arc<dyn CoordinationStateStore>,
|
||||
cached: Arc<RwLock<HashMap<CatalogKey, CachedCatalog>>>,
|
||||
refresh_locks: Arc<Mutex<HashMap<CatalogKey, Arc<Mutex<()>>>>>,
|
||||
refresh_locks: Arc<Mutex<HashMap<CatalogKey, Weak<Mutex<()>>>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -30,6 +33,14 @@ struct CatalogKey {
|
||||
struct CachedCatalog {
|
||||
loaded_at: Option<Instant>,
|
||||
catalog: PublishedAgentCatalog,
|
||||
metrics: CatalogMetrics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct CatalogMetrics {
|
||||
tool_count: usize,
|
||||
estimated_context_tokens: usize,
|
||||
warning_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -66,15 +77,29 @@ impl PublishedToolCatalog {
|
||||
workspace_slug: &str,
|
||||
agent_slug: &str,
|
||||
) -> Result<PublishedAgentCatalog, RegistryError> {
|
||||
self.refresh_if_stale(workspace_slug, agent_slug).await?;
|
||||
let guard = self.cached.read().await;
|
||||
guard
|
||||
.get(&CatalogKey::new(workspace_slug, agent_slug))
|
||||
.map(|entry| entry.catalog.clone())
|
||||
.ok_or_else(|| RegistryError::PublishedAgentNotFound {
|
||||
workspace_slug: workspace_slug.to_owned(),
|
||||
agent_slug: agent_slug.to_owned(),
|
||||
})
|
||||
let span = Stage::McpCatalogLoad.span();
|
||||
let result = async {
|
||||
self.refresh_if_stale(workspace_slug, agent_slug).await?;
|
||||
let guard = self.cached.read().await;
|
||||
guard
|
||||
.get(&CatalogKey::new(workspace_slug, agent_slug))
|
||||
.map(|entry| entry.catalog.clone())
|
||||
.ok_or_else(|| RegistryError::PublishedAgentNotFound {
|
||||
workspace_slug: workspace_slug.to_owned(),
|
||||
agent_slug: agent_slug.to_owned(),
|
||||
})
|
||||
}
|
||||
.instrument(span.clone())
|
||||
.await;
|
||||
match &result {
|
||||
Ok(_) => StageOutcome::Success.record(&span),
|
||||
Err(_) => {
|
||||
StageOutcome::Error.record(&span);
|
||||
ErrorCategory::Catalog.record(&span);
|
||||
}
|
||||
}
|
||||
drop(span);
|
||||
result
|
||||
}
|
||||
|
||||
async fn refresh_if_stale(
|
||||
@@ -98,11 +123,14 @@ impl PublishedToolCatalog {
|
||||
|
||||
let refresh_lock = {
|
||||
let mut locks = self.refresh_locks.lock().await;
|
||||
Arc::clone(
|
||||
locks
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| Arc::new(Mutex::new(()))),
|
||||
)
|
||||
locks.retain(|_, lock| lock.strong_count() > 0);
|
||||
if let Some(lock) = locks.get(&key).and_then(Weak::upgrade) {
|
||||
lock
|
||||
} else {
|
||||
let lock = Arc::new(Mutex::new(()));
|
||||
locks.insert(key.clone(), Arc::downgrade(&lock));
|
||||
lock
|
||||
}
|
||||
};
|
||||
let _refresh_guard = refresh_lock.lock().await;
|
||||
let still_stale = {
|
||||
@@ -117,50 +145,59 @@ impl PublishedToolCatalog {
|
||||
}
|
||||
|
||||
if let Some((catalog, age)) = self.load_shared_snapshot(workspace_slug, agent_slug).await {
|
||||
log_catalog_analysis(workspace_slug, agent_slug, "shared_cache", &catalog.tools);
|
||||
let mut guard = self.cached.write().await;
|
||||
guard.insert(
|
||||
let metrics =
|
||||
log_catalog_analysis(workspace_slug, agent_slug, "shared_cache", &catalog.tools);
|
||||
self.store_local_catalog(
|
||||
key,
|
||||
CachedCatalog {
|
||||
loaded_at: Instant::now().checked_sub(age),
|
||||
catalog,
|
||||
metrics,
|
||||
},
|
||||
);
|
||||
)
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let catalog = match self
|
||||
let db_span = Stage::DbQuery
|
||||
.db_span(DbOperation::CatalogLoad)
|
||||
.expect("database stage");
|
||||
let catalog_result = self
|
||||
.registry
|
||||
.get_published_agent_catalog_by_slug(workspace_slug, agent_slug)
|
||||
.await
|
||||
{
|
||||
.instrument(db_span.clone())
|
||||
.await;
|
||||
let catalog = match catalog_result {
|
||||
Ok(catalog) => catalog,
|
||||
Err(error) => return Err(error),
|
||||
Err(error) => {
|
||||
StageOutcome::Error.record(&db_span);
|
||||
ErrorCategory::Database.record(&db_span);
|
||||
drop(db_span);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
log_catalog_analysis(workspace_slug, agent_slug, "postgres", &catalog.tools);
|
||||
StageOutcome::Success.record(&db_span);
|
||||
drop(db_span);
|
||||
let metrics = log_catalog_analysis(workspace_slug, agent_slug, "postgres", &catalog.tools);
|
||||
self.store_shared_snapshot(workspace_slug, agent_slug, &catalog)
|
||||
.await;
|
||||
let mut guard = self.cached.write().await;
|
||||
let previous_count = guard
|
||||
.get(&key)
|
||||
.map(|entry| entry.catalog.tools.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
guard.insert(
|
||||
key,
|
||||
CachedCatalog {
|
||||
loaded_at: Some(Instant::now()),
|
||||
catalog,
|
||||
},
|
||||
);
|
||||
let published_tool_count = catalog.tools.len();
|
||||
let previous_count = self
|
||||
.store_local_catalog(
|
||||
key,
|
||||
CachedCatalog {
|
||||
loaded_at: Some(Instant::now()),
|
||||
catalog,
|
||||
metrics,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
info!(
|
||||
name: "mcp.catalog.refreshed",
|
||||
workspace_slug,
|
||||
agent_slug,
|
||||
published_tool_count = guard
|
||||
.get(&CatalogKey::new(workspace_slug, agent_slug))
|
||||
.map(|entry| entry.catalog.tools.len())
|
||||
.unwrap_or_default(),
|
||||
published_tool_count,
|
||||
previous_published_tool_count = previous_count,
|
||||
"published agent catalog refreshed"
|
||||
);
|
||||
@@ -168,6 +205,27 @@ impl PublishedToolCatalog {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn store_local_catalog(&self, key: CatalogKey, entry: CachedCatalog) -> usize {
|
||||
let mut guard = self.cached.write().await;
|
||||
let previous_count = guard
|
||||
.get(&key)
|
||||
.map(|current| current.catalog.tools.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
if guard.len() >= MAX_LOCAL_CATALOGS && !guard.contains_key(&key) {
|
||||
let oldest = guard
|
||||
.iter()
|
||||
.min_by_key(|(_, current)| current.loaded_at)
|
||||
.map(|(candidate, _)| candidate.clone());
|
||||
if let Some(oldest) = oldest {
|
||||
guard.remove(&oldest);
|
||||
}
|
||||
}
|
||||
guard.insert(key, entry);
|
||||
record_catalog_metrics(guard.values().map(|entry| entry.metrics));
|
||||
previous_count
|
||||
}
|
||||
|
||||
async fn load_shared_snapshot(
|
||||
&self,
|
||||
workspace_slug: &str,
|
||||
@@ -226,12 +284,19 @@ fn log_catalog_analysis(
|
||||
agent_slug: &str,
|
||||
source: &str,
|
||||
tools: &[PublishedAgentTool],
|
||||
) {
|
||||
) -> CatalogMetrics {
|
||||
let analysis = match analyze_published_tool_catalog(tools) {
|
||||
Ok(analysis) => analysis,
|
||||
Err(error) => {
|
||||
warn!(workspace_slug, agent_slug, source, %error, "published catalog analysis failed");
|
||||
return;
|
||||
Err(_) => {
|
||||
warn!(
|
||||
name: "mcp.catalog.analysis_failed",
|
||||
workspace_slug,
|
||||
agent_slug,
|
||||
source,
|
||||
error_category = "catalog_validation",
|
||||
"published catalog analysis failed"
|
||||
);
|
||||
return CatalogMetrics::default();
|
||||
}
|
||||
};
|
||||
let warning_count = analysis
|
||||
@@ -242,6 +307,7 @@ fn log_catalog_analysis(
|
||||
.count();
|
||||
|
||||
info!(
|
||||
name: "mcp.catalog.analyzed",
|
||||
workspace_slug,
|
||||
agent_slug,
|
||||
source,
|
||||
@@ -255,6 +321,30 @@ fn log_catalog_analysis(
|
||||
catalog_quality_warning_count = warning_count,
|
||||
"published agent catalog analyzed"
|
||||
);
|
||||
|
||||
CatalogMetrics {
|
||||
tool_count: analysis.budget.tool_count,
|
||||
estimated_context_tokens: analysis.budget.estimated_context_tokens,
|
||||
warning_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_catalog_metrics(metrics: impl Iterator<Item = CatalogMetrics>) {
|
||||
let aggregate = metrics.fold(CatalogMetrics::default(), |mut aggregate, current| {
|
||||
aggregate.tool_count = aggregate.tool_count.saturating_add(current.tool_count);
|
||||
aggregate.estimated_context_tokens = aggregate
|
||||
.estimated_context_tokens
|
||||
.saturating_add(current.estimated_context_tokens);
|
||||
aggregate.warning_count = aggregate
|
||||
.warning_count
|
||||
.saturating_add(current.warning_count);
|
||||
aggregate
|
||||
});
|
||||
|
||||
metrics::gauge!("crank_catalog_tools").set(aggregate.tool_count as f64);
|
||||
metrics::gauge!("crank_catalog_estimated_context_tokens")
|
||||
.set(aggregate.estimated_context_tokens as f64);
|
||||
metrics::gauge!("crank_catalog_warnings").set(aggregate.warning_count as f64);
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> u64 {
|
||||
|
||||
Reference in New Issue
Block a user