Skip to content

Commit a8f6d23

Browse files
committed
Milestone 5 - Step 4
codetracer-python-recorder/src/logging.rs: codetracer-python-recorder/src/runtime/tracer/lifecycle.rs: codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs: design-docs/codetracer-architecture-refactor-implementation-plan.status.md: Signed-off-by: Tzanko Matev <[email protected]>
1 parent 0ce9bf5 commit a8f6d23

File tree

4 files changed

+343
-173
lines changed

4 files changed

+343
-173
lines changed

codetracer-python-recorder/src/logging.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod logger;
44
mod metrics;
55
mod trailer;
66

7+
#[cfg(test)]
8+
pub(crate) use logger::snapshot_run_and_trace;
79
pub use logger::{
810
init_rust_logging_with_default, log_recorder_error, set_active_trace_id, with_error_code,
911
with_error_code_opt,
Lines changed: 294 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,296 @@
11
//! Lifecycle orchestration for `RuntimeTracer`.
22
3-
// Placeholder module; implementations will arrive during Milestone 5.
3+
use crate::logging::set_active_trace_id;
4+
use crate::policy::RecorderPolicy;
5+
use crate::runtime::activation::ActivationController;
6+
use crate::runtime::io_capture::ScopedMuteIoCapture;
7+
use crate::runtime::output_paths::TraceOutputPaths;
8+
use crate::runtime::tracer::filtering::FilterCoordinator;
9+
use recorder_errors::{enverr, usage, ErrorCode, RecorderResult};
10+
use runtime_tracing::{NonStreamingTraceWriter, TraceWriter};
11+
use serde_json::{self, json};
12+
use std::fs;
13+
use std::path::{Path, PathBuf};
14+
use uuid::Uuid;
15+
16+
/// Coordinates writer setup, activation, and teardown flows.
17+
#[derive(Debug)]
18+
pub struct LifecycleController {
19+
activation: ActivationController,
20+
program_path: PathBuf,
21+
output_paths: Option<TraceOutputPaths>,
22+
events_recorded: bool,
23+
encountered_failure: bool,
24+
trace_id: String,
25+
}
26+
27+
impl LifecycleController {
28+
pub fn new(program: &str, activation_path: Option<&Path>) -> Self {
29+
Self {
30+
activation: ActivationController::new(activation_path),
31+
program_path: PathBuf::from(program),
32+
output_paths: None,
33+
events_recorded: false,
34+
encountered_failure: false,
35+
trace_id: Uuid::new_v4().to_string(),
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
pub fn activation(&self) -> &ActivationController {
41+
&self.activation
42+
}
43+
44+
pub fn activation_mut(&mut self) -> &mut ActivationController {
45+
&mut self.activation
46+
}
47+
48+
pub fn begin(
49+
&mut self,
50+
writer: &mut NonStreamingTraceWriter,
51+
outputs: &TraceOutputPaths,
52+
start_line: u32,
53+
) -> RecorderResult<()> {
54+
let start_path = self.activation.start_path(&self.program_path);
55+
{
56+
let _mute = ScopedMuteIoCapture::new();
57+
log::debug!("{}", start_path.display());
58+
}
59+
outputs.configure_writer(writer, start_path, start_line)?;
60+
self.output_paths = Some(outputs.clone());
61+
self.events_recorded = false;
62+
self.encountered_failure = false;
63+
self.set_trace_id_active();
64+
Ok(())
65+
}
66+
67+
pub fn mark_event(&mut self) {
68+
self.events_recorded = true;
69+
}
70+
71+
pub fn mark_failure(&mut self) {
72+
self.encountered_failure = true;
73+
}
74+
75+
pub fn encountered_failure(&self) -> bool {
76+
self.encountered_failure
77+
}
78+
79+
pub fn require_trace_or_fail(&self, policy: &RecorderPolicy) -> RecorderResult<()> {
80+
if policy.require_trace && !self.events_recorded {
81+
return Err(usage!(
82+
ErrorCode::TraceMissing,
83+
"recorder policy requires a trace but no events were recorded"
84+
));
85+
}
86+
Ok(())
87+
}
88+
89+
pub fn cleanup_partial_outputs(&self) -> RecorderResult<()> {
90+
if let Some(outputs) = &self.output_paths {
91+
for path in [outputs.events(), outputs.metadata(), outputs.paths()] {
92+
if path.exists() {
93+
fs::remove_file(path).map_err(|err| {
94+
enverr!(ErrorCode::Io, "failed to remove partial trace file")
95+
.with_context("path", path.display().to_string())
96+
.with_context("io", err.to_string())
97+
})?;
98+
}
99+
}
100+
}
101+
Ok(())
102+
}
103+
104+
pub fn finalise(
105+
&mut self,
106+
writer: &mut NonStreamingTraceWriter,
107+
filter: &FilterCoordinator,
108+
) -> RecorderResult<()> {
109+
TraceWriter::finish_writing_trace_metadata(writer).map_err(|err| {
110+
enverr!(ErrorCode::Io, "failed to finalise trace metadata")
111+
.with_context("source", err.to_string())
112+
})?;
113+
self.append_filter_metadata(filter)?;
114+
TraceWriter::finish_writing_trace_paths(writer).map_err(|err| {
115+
enverr!(ErrorCode::Io, "failed to finalise trace paths")
116+
.with_context("source", err.to_string())
117+
})?;
118+
TraceWriter::finish_writing_trace_events(writer).map_err(|err| {
119+
enverr!(ErrorCode::Io, "failed to finalise trace events")
120+
.with_context("source", err.to_string())
121+
})?;
122+
Ok(())
123+
}
124+
125+
pub fn output_paths(&self) -> Option<&TraceOutputPaths> {
126+
self.output_paths.as_ref()
127+
}
128+
129+
pub fn reset_event_state(&mut self) {
130+
self.output_paths = None;
131+
self.events_recorded = false;
132+
self.encountered_failure = false;
133+
}
134+
135+
fn append_filter_metadata(&self, filter: &FilterCoordinator) -> RecorderResult<()> {
136+
let Some(outputs) = &self.output_paths else {
137+
return Ok(());
138+
};
139+
let Some(engine) = filter.engine() else {
140+
return Ok(());
141+
};
142+
143+
let path = outputs.metadata();
144+
let original = fs::read_to_string(path).map_err(|err| {
145+
enverr!(ErrorCode::Io, "failed to read trace metadata")
146+
.with_context("path", path.display().to_string())
147+
.with_context("source", err.to_string())
148+
})?;
149+
150+
let mut metadata: serde_json::Value = serde_json::from_str(&original).map_err(|err| {
151+
enverr!(ErrorCode::Io, "failed to parse trace metadata JSON")
152+
.with_context("path", path.display().to_string())
153+
.with_context("source", err.to_string())
154+
})?;
155+
156+
let filters = engine.summary();
157+
let filters_json: Vec<serde_json::Value> = filters
158+
.entries
159+
.iter()
160+
.map(|entry| {
161+
json!({
162+
"path": entry.path.to_string_lossy(),
163+
"sha256": entry.sha256,
164+
"name": entry.name,
165+
"version": entry.version,
166+
})
167+
})
168+
.collect();
169+
170+
if let serde_json::Value::Object(ref mut obj) = metadata {
171+
obj.insert(
172+
"trace_filter".to_string(),
173+
json!({
174+
"filters": filters_json,
175+
"stats": filter.summary_json(),
176+
}),
177+
);
178+
let serialised = serde_json::to_string(&metadata).map_err(|err| {
179+
enverr!(ErrorCode::Io, "failed to serialise trace metadata")
180+
.with_context("path", path.display().to_string())
181+
.with_context("source", err.to_string())
182+
})?;
183+
fs::write(path, serialised).map_err(|err| {
184+
enverr!(ErrorCode::Io, "failed to write trace metadata")
185+
.with_context("path", path.display().to_string())
186+
.with_context("source", err.to_string())
187+
})?;
188+
Ok(())
189+
} else {
190+
Err(
191+
enverr!(ErrorCode::Io, "trace metadata must be a JSON object")
192+
.with_context("path", path.display().to_string()),
193+
)
194+
}
195+
}
196+
197+
fn set_trace_id_active(&self) {
198+
set_active_trace_id(Some(self.trace_id.clone()));
199+
}
200+
201+
pub fn trace_id_scope(&self) -> TraceIdScope {
202+
self.set_trace_id_active();
203+
TraceIdScope
204+
}
205+
}
206+
207+
/// Guard that clears the active trace id when dropped.
208+
pub(crate) struct TraceIdScope;
209+
210+
impl Drop for TraceIdScope {
211+
fn drop(&mut self) {
212+
set_active_trace_id(None);
213+
}
214+
}
215+
216+
#[cfg(test)]
217+
mod tests {
218+
use super::*;
219+
use crate::logging::{init_rust_logging_with_default, snapshot_run_and_trace};
220+
use crate::policy::RecorderPolicy;
221+
use crate::runtime::output_paths::TraceOutputPaths;
222+
use recorder_errors::ErrorCode;
223+
use runtime_tracing::{NonStreamingTraceWriter, TraceEventsFileFormat};
224+
225+
fn writer() -> NonStreamingTraceWriter {
226+
NonStreamingTraceWriter::new("program.py", &[])
227+
}
228+
229+
#[test]
230+
fn policy_requiring_trace_fails_without_events() {
231+
let controller = LifecycleController::new("program.py", None);
232+
let mut policy = RecorderPolicy::default();
233+
policy.require_trace = true;
234+
235+
let err = controller.require_trace_or_fail(&policy).unwrap_err();
236+
assert_eq!(err.code, ErrorCode::TraceMissing);
237+
}
238+
239+
#[test]
240+
fn policy_requiring_trace_passes_after_event() {
241+
let mut controller = LifecycleController::new("program.py", None);
242+
let mut policy = RecorderPolicy::default();
243+
policy.require_trace = true;
244+
controller.mark_event();
245+
246+
assert!(controller.require_trace_or_fail(&policy).is_ok());
247+
}
248+
249+
#[test]
250+
fn cleanup_removes_partial_outputs() {
251+
let tmp = tempfile::tempdir().expect("tempdir");
252+
let outputs = TraceOutputPaths::new(tmp.path(), TraceEventsFileFormat::Json);
253+
let mut controller = LifecycleController::new("program.py", None);
254+
let mut writer = writer();
255+
256+
controller
257+
.begin(&mut writer, &outputs, 1)
258+
.expect("begin lifecycle");
259+
260+
std::fs::write(outputs.events(), "events").expect("write events");
261+
std::fs::write(outputs.metadata(), "{}").expect("write metadata");
262+
std::fs::write(outputs.paths(), "[]").expect("write paths");
263+
264+
controller
265+
.cleanup_partial_outputs()
266+
.expect("cleanup outputs");
267+
268+
assert!(
269+
!outputs.events().exists(),
270+
"expected events file removed after cleanup"
271+
);
272+
assert!(
273+
!outputs.metadata().exists(),
274+
"expected metadata file removed after cleanup"
275+
);
276+
assert!(
277+
!outputs.paths().exists(),
278+
"expected paths file removed after cleanup"
279+
);
280+
}
281+
282+
#[test]
283+
fn trace_id_scope_sets_and_clears_active_id() {
284+
init_rust_logging_with_default("codetracer_python_recorder=error");
285+
let controller = LifecycleController::new("program.py", None);
286+
287+
{
288+
let _scope = controller.trace_id_scope();
289+
let (_, active) = snapshot_run_and_trace().expect("logger initialised");
290+
assert!(matches!(active.as_deref(), Some(id) if !id.is_empty()));
291+
}
292+
293+
let (_, cleared) = snapshot_run_and_trace().expect("logger initialised");
294+
assert!(cleared.is_none(), "expected trace id cleared after scope");
295+
}
296+
}

0 commit comments

Comments
 (0)