Skip to content

Commit f2015c1

Browse files
authored
feat: add core telemetry and trace tooling (#14)
* feat: add core telemetry and trace tooling Signed-off-by: Luca Muscariello <[email protected]> * test: cover trace helpers Signed-off-by: Luca Muscariello <[email protected]> * test: cover telemetry helpers Signed-off-by: Luca Muscariello <[email protected]> * test: cover trace list and summary Signed-off-by: Luca Muscariello <[email protected]> * test: expand telemetry and trace coverage Signed-off-by: Luca Muscariello <[email protected]> * test: serialize telemetry env tests Signed-off-by: Luca Muscariello <[email protected]> --------- Signed-off-by: Luca Muscariello <[email protected]>
1 parent 7a21d63 commit f2015c1

16 files changed

Lines changed: 1511 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 556 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"crates/shadi_memory",
77
"crates/shadi_py",
88
"crates/shadi_sandbox",
9+
"crates/shadi_telemetry",
910
"crates/shadictl"
1011
]
1112
resolver = "2"

agents/secops/telemetry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
_resource = Resource(
2828
attributes={
2929
RES_SERVICE_NAME: SERVICE_NAME,
30+
"service.namespace": "shadi",
3031
"telemetry.sdk.language": "python",
3132
}
3233
)

crates/shadi_py/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ doctest = false
1313
agent_secrets = { path = "../agent_secrets", features = ["onepassword"] }
1414
shadi_memory = { path = "../shadi_memory" }
1515
shadi_sandbox = { path = "../shadi_sandbox" }
16+
shadi_telemetry = { path = "../shadi_telemetry" }
17+
tracing = "0.1"
1618

1719
[dependencies.pyo3]
1820
version = "0.21"

crates/shadi_py/src/lib.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use pyo3::prelude::*;
1414
use pyo3::types::{PyBytes, PyModule};
1515
use shadi_memory::{MemoryEntry as ShadiMemoryEntry, SqlCipherStore};
1616
use shadi_sandbox::{spawn_sandboxed, SandboxError, SandboxPolicy};
17+
use tracing::{field, info_span};
1718

1819
struct SessionFlagVerifier;
1920

@@ -127,6 +128,8 @@ impl ShadiStore {
127128
}
128129

129130
fn put(&self, session: &PySessionContext, key: &str, secret: &[u8]) -> PyResult<()> {
131+
let span = info_span!("shadi.secret.put", secret.key = %key);
132+
let _guard = span.enter();
130133
let ctx = session.to_context();
131134
let guard = self.store.lock().map_err(|_| PyRuntimeError::new_err("lock poisoned"))?;
132135
let access = AgentSecretAccess::new(guard.as_ref(), &self.verifier);
@@ -141,6 +144,8 @@ impl ShadiStore {
141144
session: &PySessionContext,
142145
key: &str,
143146
) -> PyResult<Bound<'py, PyBytes>> {
147+
let span = info_span!("shadi.secret.get", secret.key = %key);
148+
let _guard = span.enter();
144149
let ctx = session.to_context();
145150
let guard = self.store.lock().map_err(|_| PyRuntimeError::new_err("lock poisoned"))?;
146151
let access = AgentSecretAccess::new(guard.as_ref(), &self.verifier);
@@ -150,6 +155,8 @@ impl ShadiStore {
150155
}
151156

152157
fn delete(&self, session: &PySessionContext, key: &str) -> PyResult<()> {
158+
let span = info_span!("shadi.secret.delete", secret.key = %key);
159+
let _guard = span.enter();
153160
let ctx = session.to_context();
154161
let guard = self.store.lock().map_err(|_| PyRuntimeError::new_err("lock poisoned"))?;
155162
let access = AgentSecretAccess::new(guard.as_ref(), &self.verifier);
@@ -159,6 +166,8 @@ impl ShadiStore {
159166
}
160167

161168
fn list_keys(&self, session: &PySessionContext) -> PyResult<Vec<String>> {
169+
let span = info_span!("shadi.secret.list_keys");
170+
let _guard = span.enter();
162171
let ctx = session.to_context();
163172
AgentSecretAccess::require_verified(&ctx).map_err(map_secret_error)?;
164173
let guard = self.store.lock().map_err(|_| PyRuntimeError::new_err("lock poisoned"))?;
@@ -178,12 +187,16 @@ impl SqlCipherMemoryStore {
178187
}
179188

180189
fn put(&self, scope: &str, entry_key: &str, payload: &str) -> PyResult<i64> {
190+
let span = info_span!("shadi.memory.put", memory.scope = %scope, memory.entry_key = %entry_key);
191+
let _guard = span.enter();
181192
self.store
182193
.put(scope, entry_key, payload)
183194
.map_err(|err| PyRuntimeError::new_err(err.to_string()))
184195
}
185196

186197
fn get_latest(&self, scope: &str, entry_key: &str) -> PyResult<Option<MemoryEntry>> {
198+
let span = info_span!("shadi.memory.get_latest", memory.scope = %scope, memory.entry_key = %entry_key);
199+
let _guard = span.enter();
187200
let entry = self
188201
.store
189202
.get_latest(scope, entry_key)
@@ -193,6 +206,13 @@ impl SqlCipherMemoryStore {
193206

194207
#[pyo3(signature = (query, scope=None, limit=10))]
195208
fn search(&self, query: &str, scope: Option<String>, limit: usize) -> PyResult<Vec<MemoryEntry>> {
209+
let span = info_span!(
210+
"shadi.memory.search",
211+
memory.query = %query,
212+
memory.scope = %scope.as_deref().unwrap_or(""),
213+
memory.limit = limit as i64,
214+
);
215+
let _guard = span.enter();
196216
let entries = self
197217
.store
198218
.search(scope.as_deref(), query, limit)
@@ -205,6 +225,12 @@ impl SqlCipherMemoryStore {
205225

206226
#[pyo3(signature = (scope=None, limit=50))]
207227
fn list(&self, scope: Option<String>, limit: usize) -> PyResult<Vec<MemoryEntry>> {
228+
let span = info_span!(
229+
"shadi.memory.list",
230+
memory.scope = %scope.as_deref().unwrap_or(""),
231+
memory.limit = limit as i64,
232+
);
233+
let _guard = span.enter();
208234
let entries = self
209235
.store
210236
.list(scope.as_deref(), limit)
@@ -216,6 +242,8 @@ impl SqlCipherMemoryStore {
216242
}
217243

218244
fn delete(&self, scope: &str, entry_key: &str) -> PyResult<usize> {
245+
let span = info_span!("shadi.memory.delete", memory.scope = %scope, memory.entry_key = %entry_key);
246+
let _guard = span.enter();
219247
self.store
220248
.delete(scope, entry_key)
221249
.map_err(|err| PyRuntimeError::new_err(err.to_string()))
@@ -286,6 +314,7 @@ impl PySessionContext {
286314

287315
#[pymodule]
288316
fn shadi(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
317+
shadi_telemetry::init("shadi-runtime");
289318
m.add_class::<ShadiStore>()?;
290319
m.add_class::<PySessionContext>()?;
291320
m.add_class::<SqlCipherMemoryStore>()?;
@@ -325,6 +354,8 @@ fn inject_keychain_with_store(
325354
command: &mut Command,
326355
mappings: &[String],
327356
) -> Result<(), String> {
357+
let span = info_span!("shadi.secrets.inject", secret.count = mappings.len() as i64);
358+
let _guard = span.enter();
328359
for mapping in mappings {
329360
let (key, env) = parse_key_env(mapping)?;
330361
let secret = store
@@ -361,6 +392,15 @@ fn run_sandboxed(
361392
return Err(PyRuntimeError::new_err("command must not be empty"));
362393
}
363394

395+
let cwd_value = cwd.as_deref().unwrap_or("");
396+
let span = info_span!(
397+
"shadi.sandbox.run",
398+
command = %command[0],
399+
cwd = %cwd_value,
400+
exit.code = field::Empty,
401+
);
402+
let _guard = span.enter();
403+
364404
let mut cmd = Command::new(&command[0]);
365405
if command.len() > 1 {
366406
cmd.args(&command[1..]);
@@ -381,6 +421,7 @@ fn run_sandboxed(
381421
let status = child
382422
.wait()
383423
.map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
424+
span.record("exit.code", &status.code().unwrap_or(-1));
384425
Ok(status.code().unwrap_or(1))
385426
}
386427

crates/shadi_sandbox/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ coverage = []
99

1010
[dependencies]
1111
thiserror = "1.0"
12+
tracing = "0.1"
1213

1314
[target.'cfg(target_os = "macos")'.dependencies]
1415
libc = "0.2"

crates/shadi_sandbox/src/lib.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,32 @@ mod platform;
77
pub use policy::SandboxPolicy;
88
use std::process::{Command, ExitStatus};
99
use std::io;
10+
use tracing::{field, info_span};
1011

1112
pub fn spawn_sandboxed(command: &mut Command, policy: &SandboxPolicy) -> Result<SandboxedChild, SandboxError> {
13+
let program = command.get_program().to_string_lossy().to_string();
14+
let args = command
15+
.get_args()
16+
.map(|arg| arg.to_string_lossy())
17+
.collect::<Vec<_>>()
18+
.join(" ");
19+
let cwd = command
20+
.get_current_dir()
21+
.map(|path| path.display().to_string())
22+
.unwrap_or_else(|| "".to_string());
23+
let allowed_paths = policy.allow_read().len() + policy.allow_write().len();
24+
let network_mode = if policy.net_blocked() { "blocked" } else { "allowed" };
25+
26+
let span = info_span!(
27+
"shadi.sandbox.spawn",
28+
command = %program,
29+
args = %args,
30+
cwd = %cwd,
31+
policy.allowed_paths = allowed_paths as i64,
32+
network.mode = %network_mode,
33+
);
34+
let _guard = span.enter();
35+
1236
platform::spawn_sandboxed(command, policy)
1337
}
1438

@@ -37,14 +61,26 @@ impl SandboxedChild {
3761
}
3862

3963
pub fn wait(&mut self) -> io::Result<ExitStatus> {
40-
match &mut self.inner {
64+
let span = info_span!("shadi.sandbox.wait", pid = self.id(), exit.code = field::Empty);
65+
let _guard = span.enter();
66+
67+
let status = match &mut self.inner {
4168
SandboxedChildInner::Std(child) => child.wait(),
4269
#[cfg(target_os = "windows")]
4370
SandboxedChildInner::Windows(child) => child.wait(),
71+
};
72+
73+
if let Ok(ref status) = status {
74+
span.record("exit.code", &status.code().unwrap_or(-1));
4475
}
76+
77+
status
4578
}
4679

4780
pub fn kill(&mut self) -> io::Result<()> {
81+
let span = info_span!("shadi.sandbox.kill", pid = self.id());
82+
let _guard = span.enter();
83+
4884
match &mut self.inner {
4985
SandboxedChildInner::Std(child) => child.kill(),
5086
#[cfg(target_os = "windows")]

crates/shadi_telemetry/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "shadi_telemetry"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
opentelemetry = "0.24"
8+
opentelemetry_sdk = "0.24"
9+
opentelemetry-otlp = { version = "0.17", features = ["http-proto", "reqwest-client"] }
10+
tracing = "0.1"
11+
tracing-opentelemetry = "0.25"
12+
tracing-subscriber = { version = "0.3", features = ["json"] }
13+
tracing-appender = "0.2"

0 commit comments

Comments
 (0)