relux_runtime/observe/structured/
mod.rs1pub mod artifact;
14pub mod buffer;
15pub mod builder;
16pub mod event;
17pub mod failure;
18pub mod log_sink;
19pub mod match_context;
20pub mod shell;
21pub mod skip;
22pub mod span;
23pub mod utf8_stream;
24
25use std::collections::HashMap;
26
27use serde::Deserialize;
28use serde::Serialize;
29use ts_rs::TS;
30
31pub use artifact::ArtifactEntry;
32pub use buffer::BufferEvent;
33pub use buffer::BufferEventKind;
34pub use builder::StructuredLogBuilder;
35pub use event::CancelReasonRecord;
36pub use event::Event;
37pub use event::EventKind;
38pub use event::EventSeq;
39pub use event::MultiMatchPattern;
40pub use failure::CancellationRecord;
41pub use failure::FailureRecord;
42pub use failure::StackFrame;
43pub use match_context::MatchContext;
44pub use shell::ShellRecord;
45pub use skip::SkipRecord;
46pub use span::FnCallKind;
47pub use span::MarkerEvalDecision;
48pub use span::MarkerEvalDetail;
49pub use span::MarkerEvalKind;
50pub use span::MarkerEvalModifier;
51pub use span::Span;
52pub use span::SpanId;
53pub use span::SpanKind;
54pub use utf8_stream::Utf8Stream;
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
59#[cfg_attr(
60 feature = "ts-export",
61 ts(export, export_to = "../../../viewer/src/types/")
62)]
63pub struct SourceLocation {
64 pub file: String,
65 pub line: usize,
66 pub start: usize,
67 pub end: usize,
68}
69
70impl std::fmt::Display for SourceLocation {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(f, "{}:{}", self.file, self.line)
73 }
74}
75
76pub const SCHEMA_VERSION: u32 = 3;
81
82#[derive(Debug, Clone, Serialize, Deserialize, TS)]
85#[cfg_attr(
86 feature = "ts-export",
87 ts(export, export_to = "../../../viewer/src/types/")
88)]
89pub struct StructuredLog {
90 pub schema_version: u32,
92 pub info: TestInfo,
93 pub outcome: TestOutcome,
94 pub env: EnvInfo,
95 pub shells: HashMap<String, ShellRecord>,
96 #[ts(as = "HashMap<String, Span>")]
99 pub spans: HashMap<SpanId, Span>,
100 pub events: Vec<Event>,
101 pub buffer_events: Vec<BufferEvent>,
102 pub sources: HashMap<String, String>,
105 pub artifacts: Vec<ArtifactEntry>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, TS)]
111#[cfg_attr(
112 feature = "ts-export",
113 ts(export, export_to = "../../../viewer/src/types/")
114)]
115pub struct TestInfo {
116 pub name: String,
117 pub path: String,
118 pub duration_ms: u64,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, TS)]
125#[cfg_attr(
126 feature = "ts-export",
127 ts(export, export_to = "../../../viewer/src/types/")
128)]
129#[serde(tag = "kind", rename_all = "kebab-case")]
133pub enum TestOutcome {
134 Pass,
135 Fail(FailureRecord),
136 Cancelled(CancellationRecord),
137 Skip(SkipRecord),
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
145#[cfg_attr(
146 feature = "ts-export",
147 ts(export, export_to = "../../../viewer/src/types/")
148)]
149#[serde(tag = "kind", rename_all = "kebab-case")]
150pub enum EnvSourceRecord {
151 Base,
152 DotEnv { path: String },
153 ReluxInternal,
154 EffectOverlay { mnemonic: String },
155}
156
157impl From<&relux_core::pure::LayeredEnvSource> for EnvSourceRecord {
158 fn from(s: &relux_core::pure::LayeredEnvSource) -> Self {
159 use relux_core::pure::LayeredEnvSource as S;
160 match s {
161 S::Base => Self::Base,
162 S::DotEnv(p) => Self::DotEnv {
163 path: p.to_string_lossy().into_owned(),
164 },
165 S::ReluxInternal => Self::ReluxInternal,
166 S::EffectOverlay(m) => Self::EffectOverlay {
167 mnemonic: m.clone(),
168 },
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
176#[cfg_attr(
177 feature = "ts-export",
178 ts(export, export_to = "../../../viewer/src/types/")
179)]
180pub struct EnvValue {
181 pub key: String,
182 pub value: String,
183 pub source: EnvSourceRecord,
184}
185
186#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
187#[cfg_attr(
188 feature = "ts-export",
189 ts(export, export_to = "../../../viewer/src/types/")
190)]
191pub struct EnvInfo {
192 pub bootstrap: Vec<EnvValue>,
193}
194
195pub(crate) mod ts_duration_ms {
198 use std::time::Duration;
199
200 use serde::Deserialize;
201 use serde::Deserializer;
202 use serde::Serializer;
203
204 pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
205 s.serialize_f64(d.as_secs_f64() * 1000.0)
206 }
207
208 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
209 let ms = f64::deserialize(d)?;
210 Ok(Duration::from_secs_f64(ms / 1000.0))
211 }
212}
213
214pub(crate) mod ts_duration_ms_opt {
216 use std::time::Duration;
217
218 use serde::Deserialize;
219 use serde::Deserializer;
220 use serde::Serializer;
221
222 pub fn serialize<S: Serializer>(d: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
223 match d {
224 Some(d) => s.serialize_some(&(d.as_secs_f64() * 1000.0)),
225 None => s.serialize_none(),
226 }
227 }
228
229 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
230 let opt = Option::<f64>::deserialize(d)?;
231 Ok(opt.map(|ms| Duration::from_secs_f64(ms / 1000.0)))
232 }
233}
234
235#[cfg(test)]
236mod env_provenance_tests {
237 use super::*;
238
239 #[test]
240 fn env_source_record_serialises_dot_env() {
241 let r = EnvSourceRecord::DotEnv {
242 path: "/p/.env".into(),
243 };
244 let v = serde_json::to_value(&r).unwrap();
245 assert_eq!(v, serde_json::json!({"kind": "dot-env", "path": "/p/.env"}));
246 }
247
248 #[test]
249 fn env_source_record_variants_serialise() {
250 assert_eq!(
251 serde_json::to_value(EnvSourceRecord::Base).unwrap(),
252 serde_json::json!({"kind": "base"})
253 );
254 assert_eq!(
255 serde_json::to_value(EnvSourceRecord::ReluxInternal).unwrap(),
256 serde_json::json!({"kind": "relux-internal"})
257 );
258 assert_eq!(
259 serde_json::to_value(EnvSourceRecord::EffectOverlay {
260 mnemonic: "brave-yak-0001".into()
261 })
262 .unwrap(),
263 serde_json::json!({"kind": "effect-overlay", "mnemonic": "brave-yak-0001"})
264 );
265 }
266
267 #[test]
268 fn env_source_record_from_core() {
269 use relux_core::pure::LayeredEnvSource;
270 let r: EnvSourceRecord = (&LayeredEnvSource::ReluxInternal).into();
271 assert_eq!(r, EnvSourceRecord::ReluxInternal);
272 let r: EnvSourceRecord = (&LayeredEnvSource::DotEnv("/a/.env".into())).into();
273 assert_eq!(
274 r,
275 EnvSourceRecord::DotEnv {
276 path: "/a/.env".into()
277 }
278 );
279 let r: EnvSourceRecord = (&LayeredEnvSource::EffectOverlay("m".into())).into();
280 assert_eq!(
281 r,
282 EnvSourceRecord::EffectOverlay {
283 mnemonic: "m".into()
284 }
285 );
286 }
287
288 #[test]
289 fn env_value_serialises() {
290 let ev = EnvValue {
291 key: "PORT".into(),
292 value: "5432".into(),
293 source: EnvSourceRecord::Base,
294 };
295 let v = serde_json::to_value(&ev).unwrap();
296 assert_eq!(v["key"], "PORT");
297 assert_eq!(v["value"], "5432");
298 assert_eq!(v["source"]["kind"], "base");
299 }
300
301 #[test]
302 fn schema_version_is_three() {
303 assert_eq!(SCHEMA_VERSION, 3);
304 }
305}