diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index ca6b860..120c7be 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -4174,7 +4174,7 @@ impl AdminService { error_kind: request.error_kind, request_preview: request.request_preview, response_preview: request.response_preview, - created_at: now_string()?, + created_at: OffsetDateTime::now_utc(), }; self.registry diff --git a/apps/mcp-server/src/app.rs b/apps/mcp-server/src/app.rs index 988a100..7a39333 100644 --- a/apps/mcp-server/src/app.rs +++ b/apps/mcp-server/src/app.rs @@ -1821,9 +1821,7 @@ async fn persist_invocation( tool: &PublishedAgentTool, record: InvocationRecord<'_>, ) -> Result<(), crank_registry::RegistryError> { - let created_at = OffsetDateTime::now_utc() - .format(&Rfc3339) - .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_owned()); + let created_at = OffsetDateTime::now_utc(); let duration_ms = u64::try_from(record.duration.as_millis()).unwrap_or(u64::MAX); let log = InvocationLog { id: InvocationLogId::new(format!("log_{}", uuid::Uuid::now_v7().simple())), diff --git a/crates/crank-core/src/observability.rs b/crates/crank-core/src/observability.rs index 02a67e4..8347f61 100644 --- a/crates/crank-core/src/observability.rs +++ b/crates/crank-core/src/observability.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use time::OffsetDateTime; use crate::{AgentId, OperationId, WorkspaceId}; @@ -63,7 +64,8 @@ pub struct InvocationLog { pub error_kind: Option, pub request_preview: Value, pub response_preview: Value, - pub created_at: String, + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -79,3 +81,52 @@ pub struct UsageRollup { pub p95_ms: u64, pub p99_ms: u64, } + +#[cfg(test)] +mod tests { + use serde_json::json; + use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + + use super::{InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod}; + use crate::{AgentId, OperationId, WorkspaceId, ids::InvocationLogId}; + + fn timestamp(value: &str) -> OffsetDateTime { + OffsetDateTime::parse(value, &Rfc3339).unwrap() + } + + #[test] + fn invocation_log_serializes_timestamps_as_rfc3339() { + let log = InvocationLog { + id: InvocationLogId::new("log_01"), + workspace_id: WorkspaceId::new("ws_01"), + agent_id: Some(AgentId::new("agent_01")), + operation_id: OperationId::new("op_01"), + source: InvocationSource::AdminTestRun, + level: InvocationLevel::Info, + status: InvocationStatus::Ok, + tool_name: "create_lead".to_owned(), + message: "ok".to_owned(), + request_id: Some("req_01".to_owned()), + status_code: Some(200), + duration_ms: 123, + error_kind: None, + request_preview: json!({"input": "value"}), + response_preview: json!({"ok": true}), + created_at: timestamp("2026-04-19T12:34:56Z"), + }; + + let value = serde_json::to_value(&log).unwrap(); + + assert_eq!(value["created_at"], "2026-04-19T12:34:56Z"); + assert_eq!(value["source"], "admin_test_run"); + assert_eq!(value["level"], "info"); + assert_eq!(value["status"], "ok"); + } + + #[test] + fn usage_period_serializes_compact_labels() { + let value = serde_json::to_value(UsagePeriod::Last7Days).unwrap(); + + assert_eq!(value, "7d"); + } +} diff --git a/crates/crank-registry/src/model.rs b/crates/crank-registry/src/model.rs index 823c67e..cbfc3af 100644 --- a/crates/crank-registry/src/model.rs +++ b/crates/crank-registry/src/model.rs @@ -130,7 +130,8 @@ pub struct UsageRollupRecord { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct UsageTimelinePoint { - pub bucket_start: String, + #[serde(with = "time::serde::rfc3339")] + pub bucket_start: OffsetDateTime, pub calls_ok: u64, pub calls_error: u64, } diff --git a/crates/crank-registry/src/postgres/mod.rs b/crates/crank-registry/src/postgres/mod.rs index 90f2635..f1f3c83 100644 --- a/crates/crank-registry/src/postgres/mod.rs +++ b/crates/crank-registry/src/postgres/mod.rs @@ -2496,7 +2496,7 @@ mod tests { error_kind: None, request_preview: json!({"input":"value"}), response_preview: json!({"ok":true}), - created_at: created_at.to_owned(), + created_at: timestamp(created_at), } } diff --git a/crates/crank-registry/src/postgres/observability.rs b/crates/crank-registry/src/postgres/observability.rs index 7903070..7795b88 100644 --- a/crates/crank-registry/src/postgres/observability.rs +++ b/crates/crank-registry/src/postgres/observability.rs @@ -47,7 +47,7 @@ impl PostgresRegistry { .bind(&request.log.error_kind) .bind(Json(request.log.request_preview.clone())) .bind(Json(request.log.response_preview.clone())) - .bind(&request.log.created_at) + .bind(request.log.created_at) .execute(&self.pool) .await?; @@ -75,7 +75,7 @@ impl PostgresRegistry { 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, + l.created_at as created_at, o.name as operation_name, o.display_name as operation_display_name, a.slug as agent_slug, @@ -147,7 +147,7 @@ impl PostgresRegistry { 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, + l.created_at as created_at, o.name as operation_name, o.display_name as operation_display_name, a.slug as agent_slug, @@ -229,7 +229,7 @@ impl PostgresRegistry { 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, + (date_trunc('{bucket}', created_at at time zone 'UTC') at time zone 'UTC') as bucket_start, count(*) filter (where status = 'ok')::bigint as calls_ok, count(*) filter (where status = 'error')::bigint as calls_error from invocation_logs