xet_runtime/config/groups/telemetry.rs
1use std::time::Duration;
2
3crate::config_group!({
4 /// Whether the client reports transfer performance telemetry to the CAS server.
5 ///
6 /// When enabled, each upload or download transfer sends a single summary document to
7 /// `POST /v1/telemetry` when it finishes, plus periodic heartbeat documents for transfers
8 /// that run longer than `heartbeat_after`. Sends are best-effort: they are never retried,
9 /// never block data movement, and failures are only logged at DEBUG.
10 ///
11 /// The payload carries no file names, paths, hashes, repository ids, or user ids.
12 ///
13 /// Telemetry is force-disabled regardless of this value when any of huggingface_hub's opt-out
14 /// variables (`HF_HUB_DISABLE_TELEMETRY`, `DISABLE_TELEMETRY`, `DO_NOT_TRACK`, `HF_HUB_OFFLINE`,
15 /// `TRANSFORMERS_OFFLINE`) is set to a truthy value.
16 ///
17 /// The default value is true.
18 ///
19 /// Use the environment variable `HF_XET_TELEMETRY_ENABLED` to set this value.
20 ref enabled: bool = true;
21
22 /// How long a transfer must run before it starts emitting heartbeat documents.
23 ///
24 /// Short transfers - the common case - only ever emit their terminal summary. Heartbeats
25 /// exist so a long transfer that hangs or is killed still reports something.
26 ///
27 /// Set to zero to disable heartbeats entirely.
28 ///
29 /// The default value is 5 minutes.
30 ///
31 /// Use the environment variable `HF_XET_TELEMETRY_HEARTBEAT_AFTER` to set this value.
32 ref heartbeat_after: Duration = Duration::from_secs(300);
33
34 /// The interval between heartbeat documents once `heartbeat_after` has elapsed.
35 ///
36 /// The default value is 5 minutes.
37 ///
38 /// Use the environment variable `HF_XET_TELEMETRY_HEARTBEAT_INTERVAL` to set this value.
39 ref heartbeat_interval: Duration = Duration::from_secs(300);
40
41 /// Whole-request budget for a single telemetry POST, including connection setup.
42 ///
43 /// The default value is 5 seconds.
44 ///
45 /// Use the environment variable `HF_XET_TELEMETRY_REQUEST_TIMEOUT` to set this value.
46 ref request_timeout: Duration = Duration::from_secs(5);
47
48 /// How long a transfer waits for its terminal telemetry document to be delivered.
49 ///
50 /// This runs after all transfer work has completed, so it delays no data movement, but it
51 /// does delay the return of `finalize()`. A short bounded wait is used because a fully
52 /// detached final send is usually lost: host processes frequently exit within milliseconds
53 /// of the transfer returning.
54 ///
55 /// Set to zero to make the terminal send fully detached.
56 ///
57 /// The default value is 2 seconds.
58 ///
59 /// Use the environment variable `HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT` to set this value.
60 ref final_flush_timeout: Duration = Duration::from_secs(2);
61
62 /// Maximum number of telemetry requests allowed in flight at once, across the whole process.
63 ///
64 /// Documents submitted beyond this cap are dropped rather than queued; this is the
65 /// backpressure mechanism that keeps a degraded telemetry endpoint from accumulating tasks.
66 ///
67 /// The cap is process-wide rather than per-transfer, so it bounds a wide fan-out - a snapshot
68 /// download becomes many concurrent per-file transfers, and a per-transfer cap would multiply
69 /// by that count instead of limiting it.
70 ///
71 /// The default value is 32. It is sized as a process-wide number: a transfer emits one terminal
72 /// document, and heartbeats only start after `heartbeat_after`, so the realistic burst is a set
73 /// of concurrent transfers finalizing together. A much smaller ceiling would shed most of a wide
74 /// snapshot's terminal documents, which are the ones worth keeping.
75 ///
76 /// Use the environment variable `HF_XET_TELEMETRY_MAX_IN_FLIGHT` to set this value.
77 ref max_in_flight: usize = 32;
78});
79
80#[cfg(all(test, not(target_family = "wasm")))]
81mod tests {
82 use serial_test::serial;
83
84 use crate::config::XetConfig;
85 use crate::utils::EnvVarGuard;
86 use crate::utils::configuration_utils::TELEMETRY_OPT_OUT_VARS;
87
88 const XET_ENABLED: &str = "HF_XET_TELEMETRY_ENABLED";
89
90 /// Clears every variable that participates in the gating decision, so a value exported in the
91 /// developer's shell cannot make these tests pass or fail spuriously.
92 fn clear_all() -> Vec<EnvVarGuard> {
93 std::iter::once(XET_ENABLED)
94 .chain(TELEMETRY_OPT_OUT_VARS.iter().copied())
95 .map(EnvVarGuard::unset)
96 .collect()
97 }
98
99 fn telemetry_enabled() -> bool {
100 XetConfig::default().with_env_overrides().telemetry.enabled
101 }
102
103 /// Also covers opt-out variables whose value does not parse as truthy: presence alone must not
104 /// disable, so `HF_HUB_OFFLINE=0` leaves telemetry on.
105 #[test]
106 #[serial(env)]
107 fn test_enabled_by_default() {
108 let _guards = clear_all();
109 assert!(telemetry_enabled());
110
111 for &var in TELEMETRY_OPT_OUT_VARS {
112 for value in ["0", "false", "", "unparseable"] {
113 let _g = EnvVarGuard::set(var, value);
114 assert!(telemetry_enabled(), "{var}={value:?} must not disable telemetry");
115 }
116 }
117 }
118
119 /// Disabled by our own variable, and by any huggingface_hub opt-out set to a truthy value. A user
120 /// asking for privacy wins over an explicit `HF_XET_TELEMETRY_ENABLED=1`.
121 #[test]
122 #[serial(env)]
123 fn test_disabled_by_env_vars() {
124 let _guards = clear_all();
125 {
126 let _g = EnvVarGuard::set(XET_ENABLED, "0");
127 assert!(!telemetry_enabled());
128 }
129
130 let _enabled = EnvVarGuard::set(XET_ENABLED, "1");
131 assert!(telemetry_enabled());
132 for &var in TELEMETRY_OPT_OUT_VARS {
133 // huggingface_hub's truthy set is 1/ON/YES/TRUE, case-insensitive.
134 for value in ["1", "ON", "yes", "True"] {
135 let _g = EnvVarGuard::set(var, value);
136 assert!(!telemetry_enabled(), "{var}={value:?} must disable telemetry");
137 }
138 }
139 }
140
141 #[test]
142 #[serial(env)]
143 fn test_durations_and_cap_have_expected_defaults() {
144 let _guards = clear_all();
145 let t = XetConfig::default().with_env_overrides().telemetry;
146 assert_eq!(t.heartbeat_after.as_secs(), 300);
147 assert_eq!(t.heartbeat_interval.as_secs(), 300);
148 assert_eq!(t.request_timeout.as_secs(), 5);
149 assert_eq!(t.final_flush_timeout.as_secs(), 2);
150 // Process-wide, not per-transfer - see `max_in_flight`'s docs for why it is sized this way.
151 assert_eq!(t.max_in_flight, 32);
152 }
153
154 #[test]
155 #[serial(env)]
156 fn test_durations_parse_from_env() {
157 let _guards = clear_all();
158 let _after = EnvVarGuard::set("HF_XET_TELEMETRY_HEARTBEAT_AFTER", "90s");
159 let _flush = EnvVarGuard::set("HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT", "0s");
160 let t = XetConfig::default().with_env_overrides().telemetry;
161 assert_eq!(t.heartbeat_after.as_secs(), 90);
162 assert_eq!(t.final_flush_timeout.as_secs(), 0);
163 }
164}