Skip to content

Commit 3aba30c

Browse files
authored
feat(telemetry): add build-time option to compile out telemetry (#1845)
* feat(telemetry): add build-time option to compile out telemetry Gate anonymous telemetry emission behind a default-on `telemetry` Cargo feature in openshell-core. The data model (enums, validation, emit_*/enabled* signatures) stays always-compiled, while the endpoint, HTTP client, queue, and emission code are feature-gated. With the feature off, enabled() returns false and emit_* are no-ops, so dependent crates compile unchanged and no telemetry endpoint, HTTP client, or emission code is included in the binary. chrono and reqwest become optional dependencies of openshell-core, dropped from its dependency graph when telemetry is disabled. Thread the switch through the workspace: every crate depends on openshell-core with default-features = false, and the default-on `telemetry` passthrough lives on the binary crates that emit or collect telemetry (openshell-server, openshell-sandbox, openshell-driver-vm). In-process drivers inherit it via resolver v2 feature unification. Build a telemetry-free binary with, e.g.: cargo build --release -p openshell-server --no-default-features The runtime OPENSHELL_TELEMETRY_ENABLED switch is unchanged for default builds. Signed-off-by: Russell Bryant <russell.bryant@gmail.com> * ci(telemetry): guard that telemetry can be compiled out Add tasks/scripts/verify-telemetry-compiled-out.sh, which inspects a built binary for telemetry markers (the telemetry endpoint host and client ID) that exist only when emission code is compiled in. The rust:verify:telemetry-off mise task builds the gateway with default features (positive control: markers must be present, so the absent checks can never be silently vacuous) and with --no-default-features (markers must be absent), and checks the --no-default-features sandbox binary as well. Wire the task into the Rust branch-checks job so a regression that reintroduces telemetry code into a --no-default-features build fails CI. Signed-off-by: Russell Bryant <russell.bryant@gmail.com> --------- Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
1 parent 3a4463e commit 3aba30c

20 files changed

Lines changed: 241 additions & 18 deletions

File tree

.github/workflows/branch-checks.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ jobs:
122122
- name: Test
123123
run: mise run test:rust
124124

125+
- name: Verify telemetry can be compiled out
126+
run: mise run rust:verify:telemetry-off
127+
125128
- name: sccache stats
126129
if: always()
127130
run: |

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ OpenShell is built agent-first — your agent is your first collaborator. Before
249249

250250
OpenShell collects anonymous telemetry to help improve the project for developers. This data is not used to track individual user behavior. It helps us understand aggregate usage of sandbox, provider, and policy workflows so we can prioritize product improvements and share usage trends with the community.
251251

252-
Disable telemetry by setting `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway deployment. OpenShell propagates this deployment setting into sandbox supervisor environments so sandbox-side telemetry collection is disabled as well.
252+
Disable telemetry at runtime by setting `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway deployment. OpenShell propagates this deployment setting into sandbox supervisor environments so sandbox-side telemetry collection is disabled as well.
253+
254+
You can also compile telemetry out entirely. Telemetry support is a default-on `telemetry` Cargo feature; building with `--no-default-features` produces binaries that contain no telemetry endpoint, no telemetry HTTP client, and no emission code. Build telemetry-free artifacts with, for example, `cargo build --release -p openshell-server --no-default-features` (gateway) and the equivalent for `openshell-sandbox` and `openshell-driver-vm`. With telemetry compiled out, the gateway emits nothing and reports telemetry disabled to the sandboxes it launches.
253255

254256
Telemetry events are limited to anonymous operational categories and counts, such as sandbox lifecycle outcomes, provider profile buckets, policy decision counts, and aggregate network activity denial categories. OpenShell telemetry does not collect sandbox names or IDs, hostnames, file paths, binary paths, prompts, credentials, provider names, model names, or user content.
255257

architecture/build.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ OpenShell builds these main artifacts:
2020

2121
Sandbox community images are built outside this repository.
2222

23+
## Build Features
24+
25+
Anonymous telemetry emission is gated behind a default-on `telemetry` Cargo
26+
feature. It is defined in `openshell-core` (where the emission code, HTTP
27+
client, and endpoint live) and forwarded by the binary crates that emit or
28+
collect telemetry: `openshell-server` (gateway), `openshell-sandbox`
29+
(supervisor), and `openshell-driver-vm`. Every crate depends on
30+
`openshell-core` with `default-features = false`, so the binary crate's feature
31+
is the single switch that enables `openshell-core/telemetry` for its build
32+
graph. In-process drivers (`docker`, `kubernetes`, `podman`) inherit the
33+
gateway's setting through feature unification and carry no passthrough.
34+
35+
Building a binary with `--no-default-features` compiles out telemetry entirely:
36+
no endpoint, no telemetry HTTP client, and no emission code. With telemetry
37+
compiled out, `telemetry::enabled()` is always `false` and the `emit_*` helpers
38+
are no-ops, so the data-model types stay available and dependent crates compile
39+
unchanged. The runtime `OPENSHELL_TELEMETRY_ENABLED` switch remains the way to
40+
disable telemetry in a default (telemetry-enabled) build.
41+
2342
## Linux Runtime Environments
2443

2544
OpenShell uses different Linux libc environments for different host artifacts.

crates/openshell-bootstrap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repository.workspace = true
1010
rust-version.workspace = true
1111

1212
[dependencies]
13-
openshell-core = { path = "../openshell-core" }
13+
openshell-core = { path = "../openshell-core", default-features = false }
1414
bollard = "0.20"
1515
bytes = { workspace = true }
1616
futures = { workspace = true }

crates/openshell-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ path = "src/main.rs"
1616

1717
[dependencies]
1818
openshell-bootstrap = { path = "../openshell-bootstrap" }
19-
openshell-core = { path = "../openshell-core" }
19+
openshell-core = { path = "../openshell-core", default-features = false }
2020
openshell-policy = { path = "../openshell-policy" }
2121
openshell-providers = { path = "../openshell-providers" }
2222
openshell-prover = { path = "../openshell-prover" }

crates/openshell-core/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ serde = { workspace = true }
2020
serde_json = { workspace = true }
2121
url = { workspace = true }
2222
ipnet = "2"
23-
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
24-
reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"] }
23+
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true }
24+
reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"], optional = true }
2525

2626
[features]
27+
default = ["telemetry"]
28+
## Compile in anonymous telemetry emission support. On by default; disable with
29+
## `--no-default-features` (plus any other features you need) for a build that
30+
## contains no telemetry endpoint, no HTTP client, and no emission code at all.
31+
telemetry = ["dep:reqwest", "dep:chrono"]
2732
## Include test-only settings (dummy_bool, dummy_int) in the registry.
2833
## Off by default so production builds have an empty registry.
2934
## Enabled by e2e tests and during development.

crates/openshell-core/src/telemetry.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,40 @@
33

44
//! Best-effort anonymous telemetry emission helpers.
55
6+
#[cfg(feature = "telemetry")]
67
use chrono::{SecondsFormat, Utc};
8+
#[cfg(feature = "telemetry")]
79
use reqwest::blocking::Client;
810
use serde_json::{Value, json};
911
use std::collections::BTreeMap;
12+
#[cfg(feature = "telemetry")]
1013
use std::sync::{OnceLock, mpsc};
14+
#[cfg(feature = "telemetry")]
1115
use std::thread;
16+
#[cfg(feature = "telemetry")]
1217
use std::time::Duration;
1318

14-
const TELEMETRY_EVENT_QUEUE_CAPACITY: usize = 1024;
1519
const MAX_TELEMETRY_INTEGER: u64 = 9_223_372_036_854_775_807;
20+
const SOURCE: TelemetrySource = TelemetrySource::OpenShell;
21+
22+
#[cfg(feature = "telemetry")]
23+
const TELEMETRY_EVENT_QUEUE_CAPACITY: usize = 1024;
24+
#[cfg(feature = "telemetry")]
1625
const CLIENT_ID: &str = "415437562476676";
26+
#[cfg(feature = "telemetry")]
1727
const DEFAULT_ENDPOINT: &str = "https://events.telemetry.data.nvidia.com/v1.1/events/json";
28+
#[cfg(feature = "telemetry")]
1829
const EVENT_SCHEMA_VERSION: &str = "4.0";
30+
#[cfg(feature = "telemetry")]
1931
const EVENT_PROTOCOL_VERSION: &str = "1.6";
32+
#[cfg(feature = "telemetry")]
2033
const EVENT_SYSTEM_VERSION: &str = "openshell-telemetry/1.0";
34+
#[cfg(feature = "telemetry")]
2135
const HTTP_TIMEOUT: Duration = Duration::from_secs(5);
22-
const SOURCE: TelemetrySource = TelemetrySource::OpenShell;
36+
#[cfg(feature = "telemetry")]
2337
static TELEMETRY_SENDER: OnceLock<Option<mpsc::SyncSender<TelemetryEvent>>> = OnceLock::new();
2438

39+
#[cfg(feature = "telemetry")]
2540
#[derive(Debug)]
2641
struct TelemetryEvent {
2742
endpoint: String,
@@ -277,14 +292,30 @@ impl DenyGroup {
277292
}
278293
}
279294

295+
#[cfg(feature = "telemetry")]
280296
pub fn enabled() -> bool {
281297
telemetry_enabled_from(std::env::var("OPENSHELL_TELEMETRY_ENABLED").ok().as_deref())
282298
}
283299

300+
/// Telemetry support is compiled out: always disabled.
301+
#[cfg(not(feature = "telemetry"))]
302+
pub fn enabled() -> bool {
303+
false
304+
}
305+
306+
#[cfg(feature = "telemetry")]
284307
pub fn enabled_env_value() -> &'static str {
285308
enabled_env_value_from(std::env::var("OPENSHELL_TELEMETRY_ENABLED").ok().as_deref())
286309
}
287310

311+
/// Telemetry support is compiled out: report disabled so sandbox supervisors
312+
/// inherit the disabled state and skip activity collection.
313+
#[cfg(not(feature = "telemetry"))]
314+
pub fn enabled_env_value() -> &'static str {
315+
"false"
316+
}
317+
318+
#[cfg(feature = "telemetry")]
288319
fn enabled_env_value_from(value: Option<&str>) -> &'static str {
289320
if telemetry_enabled_from(value) {
290321
"true"
@@ -293,6 +324,7 @@ fn enabled_env_value_from(value: Option<&str>) -> &'static str {
293324
}
294325
}
295326

327+
#[cfg(feature = "telemetry")]
296328
fn telemetry_enabled_from(value: Option<&str>) -> bool {
297329
let value = value.unwrap_or("true");
298330
!matches!(
@@ -301,6 +333,7 @@ fn telemetry_enabled_from(value: Option<&str>) -> bool {
301333
)
302334
}
303335

336+
#[cfg(feature = "telemetry")]
304337
fn telemetry_endpoint() -> Option<String> {
305338
telemetry_endpoint_from(
306339
std::env::var("OPENSHELL_TELEMETRY_ENDPOINT")
@@ -309,6 +342,7 @@ fn telemetry_endpoint() -> Option<String> {
309342
)
310343
}
311344

345+
#[cfg(feature = "telemetry")]
312346
fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option<String> {
313347
let endpoint = endpoint.unwrap_or(DEFAULT_ENDPOINT);
314348
let endpoint = endpoint.trim();
@@ -319,14 +353,17 @@ fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option<String> {
319353
}
320354
}
321355

356+
#[cfg(feature = "telemetry")]
322357
fn timestamp() -> String {
323358
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
324359
}
325360

361+
#[cfg(feature = "telemetry")]
326362
fn client_version() -> &'static str {
327363
crate::VERSION
328364
}
329365

366+
#[cfg(feature = "telemetry")]
330367
fn build_payload(name: &str, event: Value, event_ts: &str, sent_ts: &str) -> Value {
331368
json!({
332369
"browserType": "undefined",
@@ -368,6 +405,7 @@ fn build_payload(name: &str, event: Value, event_ts: &str, sent_ts: &str) -> Val
368405
})
369406
}
370407

408+
#[cfg(feature = "telemetry")]
371409
fn telemetry_sender() -> Option<&'static mpsc::SyncSender<TelemetryEvent>> {
372410
TELEMETRY_SENDER
373411
.get_or_init(|| {
@@ -381,13 +419,15 @@ fn telemetry_sender() -> Option<&'static mpsc::SyncSender<TelemetryEvent>> {
381419
.as_ref()
382420
}
383421

422+
#[cfg(feature = "telemetry")]
384423
fn telemetry_worker(rx: mpsc::Receiver<TelemetryEvent>) {
385424
for event in rx {
386425
let payload = build_payload(event.name, event.event, &event.event_ts, &timestamp());
387426
let _ = publish_payload(&event.endpoint, payload);
388427
}
389428
}
390429

430+
#[cfg(feature = "telemetry")]
391431
fn publish_payload(endpoint: &str, payload: Value) -> Result<(), reqwest::Error> {
392432
Client::builder()
393433
.use_rustls_tls()
@@ -401,10 +441,12 @@ fn publish_payload(endpoint: &str, payload: Value) -> Result<(), reqwest::Error>
401441
Ok(())
402442
}
403443

444+
#[cfg(feature = "telemetry")]
404445
fn try_enqueue_event(sender: &mpsc::SyncSender<TelemetryEvent>, event: TelemetryEvent) -> bool {
405446
sender.try_send(event).is_ok()
406447
}
407448

449+
#[cfg(feature = "telemetry")]
408450
fn emit_event(name: &'static str, event: Value) {
409451
if !enabled() {
410452
return;
@@ -427,6 +469,10 @@ fn emit_event(name: &'static str, event: Value) {
427469
);
428470
}
429471

472+
/// Telemetry support is compiled out: emission is a no-op.
473+
#[cfg(not(feature = "telemetry"))]
474+
fn emit_event(_name: &'static str, _event: Value) {}
475+
430476
pub fn emit_lifecycle(
431477
resource: LifecycleResource,
432478
operation: LifecycleOperation,
@@ -563,7 +609,7 @@ where
563609
Some(sanitized)
564610
}
565611

566-
#[cfg(test)]
612+
#[cfg(all(test, feature = "telemetry"))]
567613
mod tests {
568614
use super::*;
569615

@@ -709,3 +755,44 @@ mod tests {
709755
assert_eq!(rows.get(&DenyGroup::Unknown), Some(&3));
710756
}
711757
}
758+
759+
#[cfg(all(test, not(feature = "telemetry")))]
760+
mod disabled_tests {
761+
use super::*;
762+
763+
#[test]
764+
fn telemetry_reports_disabled_when_compiled_out() {
765+
assert!(!enabled());
766+
assert_eq!(enabled_env_value(), "false");
767+
}
768+
769+
#[test]
770+
fn emit_functions_are_no_ops_when_compiled_out() {
771+
// These must remain callable so dependent crates compile unchanged;
772+
// with telemetry compiled out they do nothing and never panic.
773+
emit_lifecycle(
774+
LifecycleResource::Sandbox,
775+
LifecycleOperation::Create,
776+
TelemetryOutcome::Success,
777+
);
778+
emit_provider_lifecycle(
779+
LifecycleOperation::Create,
780+
TelemetryOutcome::Success,
781+
ProviderProfile::Custom,
782+
);
783+
emit_sandbox_create(
784+
TelemetryOutcome::Success,
785+
false,
786+
1,
787+
false,
788+
SandboxTemplateSource::Default,
789+
TelemetryComputeDriver::Docker,
790+
);
791+
emit_policy_decision(
792+
PolicyDecisionOperation::Approve,
793+
TelemetryOutcome::Success,
794+
1,
795+
);
796+
emit_sandbox_activity_summary(0, 0, 0.0, [(DenyGroup::ConnectPolicy, 0)]);
797+
}
798+
}

crates/openshell-driver-docker/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license.workspace = true
1111
repository.workspace = true
1212

1313
[dependencies]
14-
openshell-core = { path = "../openshell-core" }
14+
openshell-core = { path = "../openshell-core", default-features = false }
1515

1616
tokio = { workspace = true }
1717
tonic = { workspace = true }

crates/openshell-driver-kubernetes/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ name = "openshell-driver-kubernetes"
1515
path = "src/main.rs"
1616

1717
[dependencies]
18-
openshell-core = { path = "../openshell-core" }
18+
openshell-core = { path = "../openshell-core", default-features = false }
1919

2020
tokio = { workspace = true }
2121
tonic = { workspace = true, features = ["transport"] }

crates/openshell-driver-podman/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ name = "openshell-driver-podman"
1515
path = "src/main.rs"
1616

1717
[dependencies]
18-
openshell-core = { path = "../openshell-core" }
18+
openshell-core = { path = "../openshell-core", default-features = false }
1919

2020
tokio = { workspace = true }
2121
tonic = { workspace = true, features = ["transport"] }

0 commit comments

Comments
 (0)