use std::collections::BTreeMap; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{AppId, RequestId, ScriptId}; /// One row in the `execution_logs` table. Same shape flows through the /// `ExecutionLogSink` trait and the `GET /scripts/{id}/logs` response. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionLog { pub id: Uuid, /// Owning app at the time of execution. Materialized at write time /// so a future "move script to another app" doesn't retag history. pub app_id: AppId, pub script_id: ScriptId, pub request_id: RequestId, pub request_path: String, pub request_headers: BTreeMap, pub request_body: serde_json::Value, pub response_code: Option, pub response_body: Option, /// `log::*` entries captured during the execution, serialized as a /// JSON array of `{timestamp, level, message, data}` objects. pub script_logs: serde_json::Value, pub duration_ms: u64, pub status: ExecutionStatus, /// What dispatched this execution: `http` for direct data-plane /// ingress, or one of the trigger kinds (`kv`, `cron`, `queue`, /// `invoke`, …) for background runs. Materialized so `pic logs` /// can surface — and filter by — the origin of every run, not just /// the HTTP ones. Defaults to `http` for rows written before the /// column existed (migration 0043). #[serde(default)] pub source: ExecutionSource, pub created_at: DateTime, } /// Origin of an execution. Wire strings mirror /// `manager-core::OutboxSourceKind` (plus `Queue`, which the queue /// consumer dispatches outside the outbox) so a trigger's source kind /// maps straight through to its execution-log row. Keep the variants and /// the `source` CHECK constraint in migration 0043 in sync. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "snake_case")] pub enum ExecutionSource { #[default] Http, Kv, Docs, DeadLetter, Cron, Files, Pubsub, Email, Invoke, Queue, } impl ExecutionSource { #[must_use] pub fn as_str(self) -> &'static str { match self { Self::Http => "http", Self::Kv => "kv", Self::Docs => "docs", Self::DeadLetter => "dead_letter", Self::Cron => "cron", Self::Files => "files", Self::Pubsub => "pubsub", Self::Email => "email", Self::Invoke => "invoke", Self::Queue => "queue", } } /// Parse a wire string back into a variant. Returns `None` for an /// unknown value (callers treat that as "no filter" or a 422). #[must_use] pub fn from_wire(s: &str) -> Option { match s { "http" => Some(Self::Http), "kv" => Some(Self::Kv), "docs" => Some(Self::Docs), "dead_letter" => Some(Self::DeadLetter), "cron" => Some(Self::Cron), "files" => Some(Self::Files), "pubsub" => Some(Self::Pubsub), "email" => Some(Self::Email), "invoke" => Some(Self::Invoke), "queue" => Some(Self::Queue), _ => None, } } } /// Matches the CHECK constraint on `execution_logs.status`. Keep the /// serde rename in sync with the migration. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ExecutionStatus { Success, Error, Timeout, BudgetExceeded, } impl ExecutionStatus { #[must_use] pub fn as_str(self) -> &'static str { match self { Self::Success => "success", Self::Error => "error", Self::Timeout => "timeout", Self::BudgetExceeded => "budget_exceeded", } } }