Skip to content

Commit 4f553f5

Browse files
committed
Step 2
1 parent 39ed6dd commit 4f553f5

File tree

5 files changed

+146
-124
lines changed

5 files changed

+146
-124
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
build
77
*~
88
.idea/
9-
.cargo/
9+
.cargo/
10+
11+
**/*.egg-info/

codetracer-python-recorder/src/lib.rs

Lines changed: 6 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -4,143 +4,26 @@
44
//! signal when CPython should disable further monitoring for a location by propagating
55
//! the `sys.monitoring.DISABLE` sentinel.
66
7-
use std::fs;
8-
use std::path::Path;
9-
use std::sync::atomic::{AtomicBool, Ordering};
10-
use std::sync::Once;
11-
12-
use pyo3::exceptions::PyRuntimeError;
13-
use pyo3::prelude::*;
147
pub mod code_object;
8+
mod logging;
159
mod runtime_tracer;
10+
mod session;
1611
pub mod tracer;
12+
1713
pub use crate::code_object::{CodeObjectRegistry, CodeObjectWrapper};
14+
pub use crate::session::{flush_tracing, is_tracing, start_tracing, stop_tracing};
1815
pub use crate::tracer::{
1916
install_tracer, uninstall_tracer, CallbackOutcome, CallbackResult, EventSet, Tracer,
2017
};
2118

22-
/// Global flag tracking whether tracing is active.
23-
static ACTIVE: AtomicBool = AtomicBool::new(false);
24-
25-
// Initialize Rust logging once per process. Defaults to debug for this crate
26-
// unless overridden by RUST_LOG. This helps surface debug! output during dev.
27-
static INIT_LOGGER: Once = Once::new();
28-
29-
fn init_rust_logging_with_default(default_filter: &str) {
30-
INIT_LOGGER.call_once(|| {
31-
let env = env_logger::Env::default().default_filter_or(default_filter);
32-
// Use a compact format with timestamps and targets to aid debugging.
33-
let mut builder = env_logger::Builder::from_env(env);
34-
builder.format_timestamp_micros().format_target(true);
35-
let _ = builder.try_init();
36-
});
37-
}
38-
39-
/// Start tracing using sys.monitoring and runtime_tracing writer.
40-
#[pyfunction]
41-
fn start_tracing(path: &str, format: &str, activation_path: Option<&str>) -> PyResult<()> {
42-
// Ensure logging is ready before any tracer logs might be emitted.
43-
// Default only our crate to debug to avoid excessive verbosity from deps.
44-
init_rust_logging_with_default("codetracer_python_recorder=debug");
45-
if ACTIVE.load(Ordering::SeqCst) {
46-
return Err(PyRuntimeError::new_err("tracing already active"));
47-
}
48-
49-
// Interpret `path` as a directory where trace files will be written.
50-
let out_dir = Path::new(path);
51-
if out_dir.exists() && !out_dir.is_dir() {
52-
return Err(PyRuntimeError::new_err(
53-
"trace path exists and is not a directory",
54-
));
55-
}
56-
if !out_dir.exists() {
57-
// Best-effort create the directory tree
58-
fs::create_dir_all(&out_dir).map_err(|e| {
59-
PyRuntimeError::new_err(format!("failed to create trace directory: {}", e))
60-
})?;
61-
}
62-
63-
// Map format string to enum
64-
let fmt = match format.to_lowercase().as_str() {
65-
"json" => runtime_tracing::TraceEventsFileFormat::Json,
66-
// Use BinaryV0 for "binary" to avoid streaming writer here.
67-
"binary" | "binaryv0" | "binary_v0" | "b0" => {
68-
runtime_tracing::TraceEventsFileFormat::BinaryV0
69-
}
70-
//TODO AI! We need to assert! that the format is among the known values.
71-
other => {
72-
eprintln!("Unknown format '{}', defaulting to binary (v0)", other);
73-
runtime_tracing::TraceEventsFileFormat::BinaryV0
74-
}
75-
};
76-
77-
// Build output file paths inside the directory.
78-
let (events_path, meta_path, paths_path) = match fmt {
79-
runtime_tracing::TraceEventsFileFormat::Json => (
80-
out_dir.join("trace.json"),
81-
out_dir.join("trace_metadata.json"),
82-
out_dir.join("trace_paths.json"),
83-
),
84-
_ => (
85-
out_dir.join("trace.bin"),
86-
out_dir.join("trace_metadata.json"),
87-
out_dir.join("trace_paths.json"),
88-
),
89-
};
90-
91-
// Activation path: when set, tracing starts only after entering it.
92-
let activation_path = activation_path.map(|s| Path::new(s));
93-
94-
Python::with_gil(|py| {
95-
// Program and args: keep minimal; Python-side API stores full session info if needed
96-
let sys = py.import("sys")?;
97-
let argv = sys.getattr("argv")?;
98-
let program: String = argv.get_item(0)?.extract::<String>()?;
99-
//TODO: Error-handling. What to do if argv is empty? Does this ever happen?
100-
101-
let mut tracer = runtime_tracer::RuntimeTracer::new(&program, &[], fmt, activation_path);
102-
103-
// Start location: prefer activation path, otherwise best-effort argv[0]
104-
let start_path: &Path = activation_path.unwrap_or(Path::new(&program));
105-
log::debug!("{}", start_path.display());
106-
tracer.begin(&meta_path, &paths_path, &events_path, start_path, 1)?;
107-
108-
// Install callbacks
109-
install_tracer(py, Box::new(tracer))?;
110-
ACTIVE.store(true, Ordering::SeqCst);
111-
Ok(())
112-
})
113-
}
114-
115-
/// Stop tracing by resetting the global flag.
116-
#[pyfunction]
117-
fn stop_tracing() -> PyResult<()> {
118-
Python::with_gil(|py| {
119-
// Uninstall triggers finish() on tracer implementation.
120-
uninstall_tracer(py)?;
121-
ACTIVE.store(false, Ordering::SeqCst);
122-
Ok(())
123-
})
124-
}
125-
126-
/// Query whether tracing is currently active.
127-
#[pyfunction]
128-
fn is_tracing() -> PyResult<bool> {
129-
Ok(ACTIVE.load(Ordering::SeqCst))
130-
}
131-
132-
/// Flush buffered trace data (best-effort, non-streaming formats only).
133-
#[pyfunction]
134-
fn flush_tracing() -> PyResult<()> {
135-
Python::with_gil(|py| crate::tracer::flush_installed_tracer(py))
136-
}
19+
use pyo3::prelude::*;
13720

13821
/// Python module definition.
13922
#[pymodule]
14023
fn codetracer_python_recorder(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
14124
// Initialize logging on import so users see logs without extra setup.
14225
// Respect RUST_LOG if present; otherwise default to debug for this crate.
143-
init_rust_logging_with_default("codetracer_python_recorder=debug");
26+
logging::init_rust_logging_with_default("codetracer_python_recorder=debug");
14427
m.add_function(wrap_pyfunction!(start_tracing, m)?)?;
14528
m.add_function(wrap_pyfunction!(stop_tracing, m)?)?;
14629
m.add_function(wrap_pyfunction!(is_tracing, m)?)?;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use std::sync::Once;
2+
3+
/// Initialise the process-wide Rust logger with a default filter.
4+
///
5+
/// The logger is only set up once per process. Callers can override the filter
6+
/// by setting the `RUST_LOG` environment variable before the first invocation.
7+
pub fn init_rust_logging_with_default(default_filter: &str) {
8+
static INIT_LOGGER: Once = Once::new();
9+
10+
INIT_LOGGER.call_once(|| {
11+
let env = env_logger::Env::default().default_filter_or(default_filter);
12+
// Use a compact format with timestamps and targets to aid debugging.
13+
let mut builder = env_logger::Builder::from_env(env);
14+
builder.format_timestamp_micros().format_target(true);
15+
let _ = builder.try_init();
16+
});
17+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use std::fs;
2+
use std::path::Path;
3+
use std::sync::atomic::{AtomicBool, Ordering};
4+
5+
use pyo3::exceptions::PyRuntimeError;
6+
use pyo3::prelude::*;
7+
8+
use crate::logging::init_rust_logging_with_default;
9+
use crate::runtime_tracer;
10+
use crate::tracer::{flush_installed_tracer, install_tracer, uninstall_tracer};
11+
12+
/// Global flag tracking whether tracing is active.
13+
static ACTIVE: AtomicBool = AtomicBool::new(false);
14+
15+
/// Start tracing using sys.monitoring and runtime_tracing writer.
16+
#[pyfunction]
17+
pub fn start_tracing(path: &str, format: &str, activation_path: Option<&str>) -> PyResult<()> {
18+
// Ensure logging is ready before any tracer logs might be emitted.
19+
// Default only our crate to debug to avoid excessive verbosity from deps.
20+
init_rust_logging_with_default("codetracer_python_recorder=debug");
21+
if ACTIVE.load(Ordering::SeqCst) {
22+
return Err(PyRuntimeError::new_err("tracing already active"));
23+
}
24+
25+
// Interpret `path` as a directory where trace files will be written.
26+
let out_dir = Path::new(path);
27+
if out_dir.exists() && !out_dir.is_dir() {
28+
return Err(PyRuntimeError::new_err(
29+
"trace path exists and is not a directory",
30+
));
31+
}
32+
if !out_dir.exists() {
33+
// Best-effort create the directory tree
34+
fs::create_dir_all(&out_dir).map_err(|e| {
35+
PyRuntimeError::new_err(format!("failed to create trace directory: {}", e))
36+
})?;
37+
}
38+
39+
// Map format string to enum
40+
let fmt = match format.to_lowercase().as_str() {
41+
"json" => runtime_tracing::TraceEventsFileFormat::Json,
42+
// Use BinaryV0 for "binary" to avoid streaming writer here.
43+
"binary" | "binaryv0" | "binary_v0" | "b0" => {
44+
runtime_tracing::TraceEventsFileFormat::BinaryV0
45+
}
46+
//TODO AI! We need to assert! that the format is among the known values.
47+
other => {
48+
eprintln!("Unknown format '{}', defaulting to binary (v0)", other);
49+
runtime_tracing::TraceEventsFileFormat::BinaryV0
50+
}
51+
};
52+
53+
// Build output file paths inside the directory.
54+
let (events_path, meta_path, paths_path) = match fmt {
55+
runtime_tracing::TraceEventsFileFormat::Json => (
56+
out_dir.join("trace.json"),
57+
out_dir.join("trace_metadata.json"),
58+
out_dir.join("trace_paths.json"),
59+
),
60+
_ => (
61+
out_dir.join("trace.bin"),
62+
out_dir.join("trace_metadata.json"),
63+
out_dir.join("trace_paths.json"),
64+
),
65+
};
66+
67+
// Activation path: when set, tracing starts only after entering it.
68+
let activation_path = activation_path.map(|s| Path::new(s));
69+
70+
Python::with_gil(|py| {
71+
// Program and args: keep minimal; Python-side API stores full session info if needed
72+
let sys = py.import("sys")?;
73+
let argv = sys.getattr("argv")?;
74+
let program: String = argv.get_item(0)?.extract::<String>()?;
75+
//TODO: Error-handling. What to do if argv is empty? Does this ever happen?
76+
77+
let mut tracer = runtime_tracer::RuntimeTracer::new(&program, &[], fmt, activation_path);
78+
79+
// Start location: prefer activation path, otherwise best-effort argv[0]
80+
let start_path: &Path = activation_path.unwrap_or(Path::new(&program));
81+
log::debug!("{}", start_path.display());
82+
tracer.begin(&meta_path, &paths_path, &events_path, start_path, 1)?;
83+
84+
// Install callbacks
85+
install_tracer(py, Box::new(tracer))?;
86+
ACTIVE.store(true, Ordering::SeqCst);
87+
Ok(())
88+
})
89+
}
90+
91+
/// Stop tracing by resetting the global flag.
92+
#[pyfunction]
93+
pub fn stop_tracing() -> PyResult<()> {
94+
Python::with_gil(|py| {
95+
// Uninstall triggers finish() on tracer implementation.
96+
uninstall_tracer(py)?;
97+
ACTIVE.store(false, Ordering::SeqCst);
98+
Ok(())
99+
})
100+
}
101+
102+
/// Query whether tracing is currently active.
103+
#[pyfunction]
104+
pub fn is_tracing() -> PyResult<bool> {
105+
Ok(ACTIVE.load(Ordering::SeqCst))
106+
}
107+
108+
/// Flush buffered trace data (best-effort, non-streaming formats only).
109+
#[pyfunction]
110+
pub fn flush_tracing() -> PyResult<()> {
111+
Python::with_gil(|py| flush_installed_tracer(py))
112+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# File-Level SRP Refactor Status
2+
3+
## Current Status
4+
- ✅ Step 2 complete: introduced `src/logging.rs` for one-time logger initialisation and migrated tracing session lifecycle (`start_tracing`, `stop_tracing`, `is_tracing`, `flush_tracing`, `ACTIVE` flag) into `src/session.rs`, with `src/lib.rs` now limited to PyO3 wiring and re-exports.
5+
- ⚠️ Test baseline still pending: `cargo check` succeeds; `cargo test` currently fails to link in the sandbox because CPython development symbols are unavailable, matching the pre-refactor limitation.
6+
7+
## Next Task
8+
- Step 3: Restructure runtime tracer internals by creating `src/runtime/mod.rs` and extracting activation control, value encoding, and writer/output-path handling into focused submodules before reconnecting them through the new façade.

0 commit comments

Comments
 (0)