use super::*; impl PostgresRegistry { pub async fn create_invocation_log( &self, request: CreateInvocationLogRequest<'_>, ) -> Result<(), RegistryError> { sqlx::query( "insert into invocation_logs ( id, workspace_id, agent_id, operation_id, source, level, status, tool_name, message, request_id, status_code, duration_ms, error_kind, request_preview_json, response_preview_json, created_at ) values ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16::timestamptz )", ) .bind(request.log.id.as_str()) .bind(request.log.workspace_id.as_str()) .bind(request.log.agent_id.as_ref().map(|value| value.as_str())) .bind(request.log.operation_id.as_str()) .bind(serialize_enum_text(&request.log.source, "source")?) .bind(serialize_enum_text(&request.log.level, "level")?) .bind(serialize_enum_text(&request.log.status, "status")?) .bind(&request.log.tool_name) .bind(&request.log.message) .bind(&request.log.request_id) .bind(request.log.status_code.map(i32::from)) .bind(i64::try_from(request.log.duration_ms).map_err(|_| { RegistryError::InvalidNumericValue { field: "duration_ms", value: i64::MAX, } })?) .bind(&request.log.error_kind) .bind(Json(request.log.request_preview.clone())) .bind(Json(request.log.response_preview.clone())) .bind(&request.log.created_at) .execute(&self.pool) .await?; Ok(()) } pub async fn list_invocation_logs( &self, query: ListInvocationLogsQuery<'_>, ) -> Result, RegistryError> { let rows = sqlx::query( "select l.id, l.workspace_id, l.agent_id, l.operation_id, l.source, l.level, l.status, l.tool_name, l.message, l.request_id, l.status_code, l.duration_ms, l.error_kind, l.request_preview_json, l.response_preview_json, to_char(l.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"') as created_at, o.name as operation_name, o.display_name as operation_display_name, a.slug as agent_slug, a.display_name as agent_display_name from invocation_logs l join operations o on o.id = l.operation_id left join agents a on a.id = l.agent_id where l.workspace_id = $1 and ($2::text is null or l.level = $2) and ($3::text is null or l.source = $3) and ($4::text is null or l.operation_id = $4) and ($5::text is null or l.agent_id = $5) and ($6::timestamptz is null or l.created_at >= $6::timestamptz) and ( $7::text is null or l.tool_name ilike '%' || $7 || '%' or l.message ilike '%' || $7 || '%' or o.name ilike '%' || $7 || '%' or o.display_name ilike '%' || $7 || '%' ) order by l.created_at desc limit $8", ) .bind(query.workspace_id.as_str()) .bind( query .level .as_ref() .map(|value| serialize_enum_text(value, "level")) .transpose()?, ) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .bind(query.operation_id.map(|value| value.as_str())) .bind(query.agent_id.map(|value| value.as_str())) .bind(query.created_after) .bind(query.search_text) .bind(i64::from(query.limit)) .fetch_all(&self.pool) .await?; rows.iter().map(map_invocation_log_record).collect() } pub async fn get_invocation_log( &self, workspace_id: &WorkspaceId, log_id: &InvocationLogId, ) -> Result, RegistryError> { let row = sqlx::query( "select l.id, l.workspace_id, l.agent_id, l.operation_id, l.source, l.level, l.status, l.tool_name, l.message, l.request_id, l.status_code, l.duration_ms, l.error_kind, l.request_preview_json, l.response_preview_json, to_char(l.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"') as created_at, o.name as operation_name, o.display_name as operation_display_name, a.slug as agent_slug, a.display_name as agent_display_name from invocation_logs l join operations o on o.id = l.operation_id left join agents a on a.id = l.agent_id where l.workspace_id = $1 and l.id = $2", ) .bind(workspace_id.as_str()) .bind(log_id.as_str()) .fetch_optional(&self.pool) .await?; row.as_ref().map(map_invocation_log_record).transpose() } pub async fn summarize_usage( &self, query: UsageQuery<'_>, ) -> Result { let row = sqlx::query( "select count(*)::bigint as calls_total, count(*) filter (where status = 'ok')::bigint as calls_ok, count(*) filter (where status = 'error')::bigint as calls_error, coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms, coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms, coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms from invocation_logs where workspace_id = $1 and created_at >= $2::timestamptz and ($3::text is null or source = $3)", ) .bind(query.workspace_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_one(&self.pool) .await?; let calls_total = row.try_get::("calls_total")?; let calls_ok = row.try_get::("calls_ok")?; let calls_error = row.try_get::("calls_error")?; let rollup = UsageRollup { workspace_id: query.workspace_id.clone(), agent_id: None, operation_id: None, period: query.period, calls_total: to_u64(calls_total, "calls_total")?, calls_ok: to_u64(calls_ok, "calls_ok")?, calls_error: to_u64(calls_error, "calls_error")?, p50_ms: to_u64(row.try_get::("p50_ms")?, "p50_ms")?, p95_ms: to_u64(row.try_get::("p95_ms")?, "p95_ms")?, p99_ms: to_u64(row.try_get::("p99_ms")?, "p99_ms")?, }; let success_rate = if rollup.calls_total == 0 { 0.0 } else { (rollup.calls_ok as f64 / rollup.calls_total as f64) * 100.0 }; Ok(UsageSummary { rollup, success_rate, }) } pub async fn list_usage_timeline( &self, query: UsageQuery<'_>, ) -> Result, RegistryError> { let bucket = usage_bucket_sql(query.bucket); let sql = format!( "select to_char(date_trunc('{bucket}', created_at at time zone 'UTC'), 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as bucket_start, count(*) filter (where status = 'ok')::bigint as calls_ok, count(*) filter (where status = 'error')::bigint as calls_error from invocation_logs where workspace_id = $1 and created_at >= $2::timestamptz and ($3::text is null or source = $3) group by 1 order by 1 asc" ); let rows = sqlx::query(&sql) .bind(query.workspace_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_all(&self.pool) .await?; rows.iter() .map(|row| { Ok(UsageTimelinePoint { bucket_start: row.try_get("bucket_start")?, calls_ok: to_u64(row.try_get::("calls_ok")?, "calls_ok")?, calls_error: to_u64(row.try_get::("calls_error")?, "calls_error")?, }) }) .collect() } pub async fn list_usage_by_operation( &self, query: UsageQuery<'_>, ) -> Result, RegistryError> { let rows = sqlx::query( "select o.id as operation_id, o.name as operation_name, o.display_name as operation_display_name, o.protocol, count(*)::bigint as calls_total, count(*) filter (where l.status = 'error')::bigint as calls_error, coalesce(percentile_cont(0.5) within group (order by l.duration_ms), 0)::bigint as p50_ms, coalesce(percentile_cont(0.95) within group (order by l.duration_ms), 0)::bigint as p95_ms, coalesce(percentile_cont(0.99) within group (order by l.duration_ms), 0)::bigint as p99_ms from invocation_logs l join operations o on o.id = l.operation_id where l.workspace_id = $1 and l.created_at >= $2::timestamptz and ($3::text is null or l.source = $3) group by o.id, o.name, o.display_name, o.protocol order by calls_total desc, o.name asc", ) .bind(query.workspace_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_all(&self.pool) .await?; rows.iter().map(map_usage_operation_breakdown).collect() } pub async fn get_usage_for_operation( &self, query: UsageQuery<'_>, operation_id: &OperationId, ) -> Result, RegistryError> { let row = sqlx::query( "select count(*)::bigint as calls_total, count(*) filter (where status = 'ok')::bigint as calls_ok, count(*) filter (where status = 'error')::bigint as calls_error, coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms, coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms, coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms from invocation_logs where workspace_id = $1 and operation_id = $2 and created_at >= $3::timestamptz and ($4::text is null or source = $4)", ) .bind(query.workspace_id.as_str()) .bind(operation_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_one(&self.pool) .await?; let calls_total = to_u64(row.try_get::("calls_total")?, "calls_total")?; if calls_total == 0 { return Ok(None); } Ok(Some(UsageRollupRecord { rollup: UsageRollup { workspace_id: query.workspace_id.clone(), agent_id: None, operation_id: Some(operation_id.clone()), period: query.period, calls_total, calls_ok: to_u64(row.try_get::("calls_ok")?, "calls_ok")?, calls_error: to_u64(row.try_get::("calls_error")?, "calls_error")?, p50_ms: to_u64(row.try_get::("p50_ms")?, "p50_ms")?, p95_ms: to_u64(row.try_get::("p95_ms")?, "p95_ms")?, p99_ms: to_u64(row.try_get::("p99_ms")?, "p99_ms")?, }, })) } pub async fn list_usage_by_agent( &self, query: UsageQuery<'_>, ) -> Result, RegistryError> { let rows = sqlx::query( "select a.id as agent_id, a.slug as agent_slug, a.display_name as agent_display_name, count(*)::bigint as calls_total, count(*) filter (where l.status = 'error')::bigint as calls_error, coalesce(percentile_cont(0.5) within group (order by l.duration_ms), 0)::bigint as p50_ms, coalesce(percentile_cont(0.95) within group (order by l.duration_ms), 0)::bigint as p95_ms, coalesce(percentile_cont(0.99) within group (order by l.duration_ms), 0)::bigint as p99_ms from invocation_logs l join agents a on a.id = l.agent_id where l.workspace_id = $1 and l.created_at >= $2::timestamptz and ($3::text is null or l.source = $3) group by a.id, a.slug, a.display_name order by calls_total desc, a.slug asc", ) .bind(query.workspace_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_all(&self.pool) .await?; rows.iter().map(map_usage_agent_breakdown).collect() } pub async fn get_usage_for_agent( &self, query: UsageQuery<'_>, agent_id: &AgentId, ) -> Result, RegistryError> { let row = sqlx::query( "select count(*)::bigint as calls_total, count(*) filter (where status = 'ok')::bigint as calls_ok, count(*) filter (where status = 'error')::bigint as calls_error, coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms, coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms, coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms from invocation_logs where workspace_id = $1 and agent_id = $2 and created_at >= $3::timestamptz and ($4::text is null or source = $4)", ) .bind(query.workspace_id.as_str()) .bind(agent_id.as_str()) .bind(query.created_after) .bind( query .source .as_ref() .map(|value| serialize_enum_text(value, "source")) .transpose()?, ) .fetch_one(&self.pool) .await?; let calls_total = to_u64(row.try_get::("calls_total")?, "calls_total")?; if calls_total == 0 { return Ok(None); } Ok(Some(UsageRollupRecord { rollup: UsageRollup { workspace_id: query.workspace_id.clone(), agent_id: Some(agent_id.clone()), operation_id: None, period: query.period, calls_total, calls_ok: to_u64(row.try_get::("calls_ok")?, "calls_ok")?, calls_error: to_u64(row.try_get::("calls_error")?, "calls_error")?, p50_ms: to_u64(row.try_get::("p50_ms")?, "p50_ms")?, p95_ms: to_u64(row.try_get::("p95_ms")?, "p95_ms")?, p99_ms: to_u64(row.try_get::("p99_ms")?, "p99_ms")?, }, })) } }