Skip to content

Commit fdd257b

Browse files
authored
chore(metrics): add configurable sampling interval (#654)
Add the ability to configure the metrics sample interval in ms from the config or override in the sdk command to create a sandbox. This is so that users can configure how frequently metric samples are collected. It could be faster or slower but the default remains 1 second or 1000 ms. If a user wants to disable sampling completely they set it to 0 and the entire metric sampling task is skipped. If they disable metric sampling by setting the interval to 0 and then they try to read the metrics from cli/sdk, they will receive and error saying metrics are disabled for that sandbox. Example config: ```json { "database": { "max_connections": 1 }, "sandbox_defaults": { "metrics_sample_interval_ms": 0 } } ```
1 parent 46ad462 commit fdd257b

23 files changed

Lines changed: 443 additions & 13 deletions

File tree

crates/cli/lib/sandbox_cmd.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ pub struct SandboxArgs {
7272
#[arg(long, default_value_t = 512)]
7373
pub memory_mib: u32,
7474

75+
/// Metrics sampling interval in milliseconds; `0` disables sampling.
76+
#[arg(long = "metrics-sample-interval-ms", default_value_t = 1000)]
77+
pub metrics_sample_interval_ms: u64,
78+
79+
/// Disable metrics sampling; overrides `--metrics-sample-interval-ms`.
80+
#[arg(long = "disable-metrics-sample")]
81+
pub disable_metrics_sample: bool,
82+
7583
/// Root filesystem path for direct passthrough mounts.
7684
#[arg(long)]
7785
pub rootfs_path: Option<PathBuf>,
@@ -190,6 +198,11 @@ pub fn run(args: SandboxArgs, log_level: Option<LogLevel>) -> ! {
190198
forward_output: args.forward_output,
191199
idle_timeout_secs: args.idle_timeout,
192200
max_duration_secs: args.max_duration,
201+
metrics_sample_interval_ms: if args.disable_metrics_sample {
202+
None
203+
} else {
204+
std::num::NonZero::new(args.metrics_sample_interval_ms)
205+
},
193206
vm: vm_config,
194207
};
195208

crates/microsandbox/lib/config/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use std::{
77
collections::HashMap,
8+
num::NonZero,
89
path::{Path, PathBuf},
910
sync::OnceLock,
1011
};
@@ -32,6 +33,28 @@ pub(crate) const DEFAULT_MAX_CONNECTIONS: u32 = 5;
3233
/// Default database connection acquisition timeout in seconds.
3334
pub(crate) const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 30;
3435

36+
/// Default sandbox metrics sampling interval in milliseconds.
37+
pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
38+
39+
/// Default value for `metrics_sample_interval_ms` fields.
40+
pub fn default_metrics_sample_interval() -> Option<NonZero<u64>> {
41+
NonZero::new(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
42+
}
43+
44+
/// Serde adapter mapping the `metrics_sample_interval_ms` wire format `u64` to `Option<NonZero<u64>>` (`0` ↔ `None`).
45+
pub(crate) mod metrics_interval_serde {
46+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
47+
use std::num::NonZero;
48+
49+
pub fn serialize<S: Serializer>(v: &Option<NonZero<u64>>, s: S) -> Result<S::Ok, S::Error> {
50+
v.map(|n| n.get()).unwrap_or(0).serialize(s)
51+
}
52+
53+
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<NonZero<u64>>, D::Error> {
54+
Ok(NonZero::new(u64::deserialize(d)?))
55+
}
56+
}
57+
3558
/// Service name for microsandbox-managed registry credentials in the OS keyring.
3659
#[cfg(all(
3760
feature = "keyring",
@@ -131,6 +154,17 @@ pub struct SandboxDefaults {
131154

132155
/// Default working directory inside the sandbox.
133156
pub workdir: Option<String>,
157+
158+
/// Default metrics sampling interval in milliseconds; `0` disables sampling globally.
159+
#[serde(
160+
default = "default_metrics_sample_interval",
161+
with = "metrics_interval_serde"
162+
)]
163+
pub metrics_sample_interval_ms: Option<NonZero<u64>>,
164+
165+
/// Force-disable metrics sampling regardless of `metrics_sample_interval_ms`.
166+
#[serde(default)]
167+
pub disable_metrics_sample: bool,
134168
}
135169

136170
/// Registry configuration.
@@ -423,6 +457,8 @@ impl Default for SandboxDefaults {
423457
memory_mib: DEFAULT_MEMORY_MIB,
424458
shell: "/bin/sh".into(),
425459
workdir: None,
460+
metrics_sample_interval_ms: default_metrics_sample_interval(),
461+
disable_metrics_sample: false,
426462
}
427463
}
428464
}
@@ -895,6 +931,10 @@ mod tests {
895931
assert_eq!(cfg.sandbox_defaults.cpus, 1);
896932
assert_eq!(cfg.sandbox_defaults.memory_mib, 512);
897933
assert_eq!(cfg.sandbox_defaults.shell, "/bin/sh");
934+
assert_eq!(
935+
cfg.sandbox_defaults.metrics_sample_interval_ms,
936+
NonZero::new(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
937+
);
898938
assert_eq!(cfg.log_level, None);
899939
assert_eq!(cfg.database.max_connections, 5);
900940
assert_eq!(cfg.database.connect_timeout_secs, 30);
@@ -915,6 +955,59 @@ mod tests {
915955
assert_eq!(cfg.sandbox_defaults.memory_mib, 512);
916956
}
917957

958+
#[test]
959+
fn test_deserialize_metrics_interval_missing_uses_default() {
960+
let json = r#"{"sandbox_defaults": {}}"#;
961+
let cfg: GlobalConfig = serde_json::from_str(json).unwrap();
962+
assert_eq!(
963+
cfg.sandbox_defaults.metrics_sample_interval_ms,
964+
NonZero::new(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
965+
);
966+
}
967+
968+
#[test]
969+
fn test_deserialize_metrics_interval_zero_disables() {
970+
let json = r#"{"sandbox_defaults": {"metrics_sample_interval_ms": 0}}"#;
971+
let cfg: GlobalConfig = serde_json::from_str(json).unwrap();
972+
assert!(cfg.sandbox_defaults.metrics_sample_interval_ms.is_none());
973+
}
974+
975+
#[test]
976+
fn test_deserialize_metrics_interval_positive() {
977+
let json = r#"{"sandbox_defaults": {"metrics_sample_interval_ms": 2500}}"#;
978+
let cfg: GlobalConfig = serde_json::from_str(json).unwrap();
979+
assert_eq!(
980+
cfg.sandbox_defaults.metrics_sample_interval_ms,
981+
NonZero::new(2500)
982+
);
983+
}
984+
985+
#[test]
986+
fn test_serialize_metrics_interval_disabled_round_trips() {
987+
let mut cfg = GlobalConfig::default();
988+
cfg.sandbox_defaults.metrics_sample_interval_ms = None;
989+
let json = serde_json::to_string(&cfg).unwrap();
990+
assert!(
991+
json.contains("\"metrics_sample_interval_ms\":0"),
992+
"expected `0` serialization, got: {json}"
993+
);
994+
let round: GlobalConfig = serde_json::from_str(&json).unwrap();
995+
assert!(round.sandbox_defaults.metrics_sample_interval_ms.is_none());
996+
}
997+
998+
#[test]
999+
fn test_deserialize_disable_metrics_sample_default_false() {
1000+
let cfg: GlobalConfig = serde_json::from_str("{}").unwrap();
1001+
assert!(!cfg.sandbox_defaults.disable_metrics_sample);
1002+
}
1003+
1004+
#[test]
1005+
fn test_deserialize_disable_metrics_sample_true() {
1006+
let json = r#"{"sandbox_defaults": {"disable_metrics_sample": true}}"#;
1007+
let cfg: GlobalConfig = serde_json::from_str(json).unwrap();
1008+
assert!(cfg.sandbox_defaults.disable_metrics_sample);
1009+
}
1010+
9181011
#[test]
9191012
fn test_deserialize_log_level() {
9201013
let json = r#"{"log_level":"debug"}"#;

crates/microsandbox/lib/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ pub enum MicrosandboxError {
138138
#[error("snapshot integrity check failed: {0}")]
139139
SnapshotIntegrity(String),
140140

141+
/// Metrics sampling is disabled for this sandbox.
142+
#[error("metrics disabled for sandbox: {0}")]
143+
MetricsDisabled(String),
144+
141145
/// A custom error message.
142146
#[error("{0}")]
143147
Custom(String),

crates/microsandbox/lib/runtime/spawn.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,13 @@ fn sandbox_cli_args(
535535
args.push(OsString::from(config.cpus.to_string()));
536536
args.push(OsString::from("--memory-mib"));
537537
args.push(OsString::from(config.memory_mib.to_string()));
538+
match config.effective_metrics_interval() {
539+
Some(ms) => {
540+
args.push(OsString::from("--metrics-sample-interval-ms"));
541+
args.push(OsString::from(ms.get().to_string()));
542+
}
543+
None => args.push(OsString::from("--disable-metrics-sample")),
544+
}
538545

539546
match &config.image {
540547
RootfsSource::Bind(path) => {
@@ -931,6 +938,87 @@ mod tests {
931938
assert_eq!(parsed[1].hard, 1024);
932939
}
933940

941+
#[test]
942+
fn test_sandbox_cli_args_emit_metrics_interval_flag() {
943+
let config = SandboxBuilder::new("test")
944+
.image("/tmp/rootfs")
945+
.metrics_sample_interval(std::time::Duration::from_millis(1000))
946+
.build()
947+
.unwrap();
948+
949+
let rendered = render_args(&config);
950+
951+
assert!(
952+
rendered
953+
.windows(2)
954+
.any(|pair| pair == ["--metrics-sample-interval-ms", "1000"]),
955+
"expected metrics interval flag in {rendered:?}"
956+
);
957+
}
958+
959+
#[test]
960+
fn test_sandbox_cli_args_include_custom_metrics_sample_interval() {
961+
let config = SandboxBuilder::new("test")
962+
.image("/tmp/rootfs")
963+
.metrics_sample_interval(std::time::Duration::from_millis(2500))
964+
.build()
965+
.unwrap();
966+
967+
let rendered = render_args(&config);
968+
969+
assert!(
970+
rendered
971+
.windows(2)
972+
.any(|pair| pair == ["--metrics-sample-interval-ms", "2500"]),
973+
"expected custom metrics interval flag in {rendered:?}"
974+
);
975+
}
976+
977+
#[test]
978+
fn test_sandbox_cli_args_disabled_metrics_emit_disable_flag() {
979+
let config = SandboxBuilder::new("test")
980+
.image("/tmp/rootfs")
981+
.metrics_sample_interval(std::time::Duration::ZERO)
982+
.build()
983+
.unwrap();
984+
985+
let rendered = render_args(&config);
986+
987+
assert!(
988+
rendered.iter().any(|arg| arg == "--disable-metrics-sample"),
989+
"expected `--disable-metrics-sample` flag; got {rendered:?}"
990+
);
991+
assert!(
992+
!rendered
993+
.iter()
994+
.any(|arg| arg == "--metrics-sample-interval-ms"),
995+
"should not also emit interval flag; got {rendered:?}"
996+
);
997+
}
998+
999+
#[test]
1000+
fn test_sandbox_cli_args_disable_overrides_positive_interval() {
1001+
let config = SandboxBuilder::new("test")
1002+
.image("/tmp/rootfs")
1003+
.metrics_sample_interval(std::time::Duration::from_millis(2500))
1004+
.disable_metrics_sample()
1005+
.build()
1006+
.unwrap();
1007+
1008+
let rendered = render_args(&config);
1009+
1010+
assert!(
1011+
rendered.iter().any(|arg| arg == "--disable-metrics-sample"),
1012+
"expected disable flag to win over positive interval; got {rendered:?}"
1013+
);
1014+
assert!(
1015+
!rendered
1016+
.iter()
1017+
.any(|arg| arg == "--metrics-sample-interval-ms"),
1018+
"should not emit interval flag when disable is set; got {rendered:?}"
1019+
);
1020+
}
1021+
9341022
#[test]
9351023
fn test_sandbox_cli_args_include_db_connect_timeout() {
9361024
let config = SandboxBuilder::new("test")

crates/microsandbox/lib/sandbox/builder.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use super::{
1818
ImageBuilder, IntoImage, MountBuilder, Patch, PatchBuilder, RootfsSource, VolumeMount,
1919
},
2020
};
21-
use crate::{LogLevel, MicrosandboxResult, size::Mebibytes};
21+
use crate::{LogLevel, MicrosandboxError, MicrosandboxResult, size::Mebibytes};
22+
use std::time::Duration;
2223

2324
//--------------------------------------------------------------------------------------------------
2425
// Types
@@ -155,6 +156,27 @@ impl SandboxBuilder {
155156
self
156157
}
157158

159+
/// Force-disable metrics sampling regardless of `metrics_sample_interval`.
160+
pub fn disable_metrics_sample(mut self) -> Self {
161+
self.config.disable_metrics_sample = true;
162+
self
163+
}
164+
165+
/// Override the metrics sampling interval; pass `Duration::ZERO` to disable.
166+
pub fn metrics_sample_interval(mut self, interval: Duration) -> Self {
167+
let ms = interval.as_millis();
168+
if ms > u128::from(u64::MAX) {
169+
if self.build_error.is_none() {
170+
self.build_error = Some(MicrosandboxError::InvalidConfig(format!(
171+
"metrics sample interval {interval:?} overflows u64 milliseconds"
172+
)));
173+
}
174+
return self;
175+
}
176+
self.config.metrics_sample_interval_ms = std::num::NonZero::new(ms as u64);
177+
self
178+
}
179+
158180
/// Default working directory for commands executed in this sandbox
159181
/// (e.g., `/app`). Used by [`exec`](super::Sandbox::exec),
160182
/// [`shell`](super::Sandbox::shell), and [`attach`](super::Sandbox::attach)
@@ -845,6 +867,49 @@ mod tests {
845867
assert_eq!(config.log_level, None);
846868
}
847869

870+
#[test]
871+
fn test_builder_metrics_sample_interval_sets_ms() {
872+
let config = SandboxBuilder::new("test")
873+
.image("alpine")
874+
.metrics_sample_interval(std::time::Duration::from_millis(750))
875+
.build()
876+
.unwrap();
877+
878+
assert_eq!(
879+
config.metrics_sample_interval_ms,
880+
std::num::NonZero::new(750)
881+
);
882+
}
883+
884+
#[test]
885+
fn test_builder_metrics_sample_interval_zero_is_disabled() {
886+
let config = SandboxBuilder::new("test")
887+
.image("alpine")
888+
.metrics_sample_interval(std::time::Duration::ZERO)
889+
.build()
890+
.unwrap();
891+
892+
assert!(config.metrics_sample_interval_ms.is_none());
893+
assert!(config.effective_metrics_interval().is_none());
894+
}
895+
896+
#[test]
897+
fn test_builder_disable_metrics_sample_overrides_interval() {
898+
let config = SandboxBuilder::new("test")
899+
.image("alpine")
900+
.metrics_sample_interval(std::time::Duration::from_millis(5000))
901+
.disable_metrics_sample()
902+
.build()
903+
.unwrap();
904+
905+
assert!(config.disable_metrics_sample);
906+
assert_eq!(
907+
config.metrics_sample_interval_ms,
908+
std::num::NonZero::new(5000)
909+
);
910+
assert!(config.effective_metrics_interval().is_none());
911+
}
912+
848913
#[test]
849914
fn test_builder_replace_sets_replace_existing() {
850915
let config = SandboxBuilder::new("test")

0 commit comments

Comments
 (0)