Skip to main content

vector/
cli.rs

1#![allow(missing_docs)]
2
3use std::{
4    num::{NonZeroU64, NonZeroUsize},
5    path::PathBuf,
6};
7
8use clap::{ArgAction, CommandFactory, FromArgMatches, Parser};
9
10#[cfg(windows)]
11use crate::service;
12#[cfg(feature = "api-client")]
13use crate::tap;
14#[cfg(feature = "top")]
15use crate::top;
16
17use crate::{
18    completion, config, convert_config, generate, generate_schema, get_version, graph, list,
19    signal, unit_test, validate,
20};
21
22#[derive(Parser, Debug)]
23#[command(rename_all = "kebab-case")]
24pub struct Opts {
25    #[command(flatten)]
26    pub root: RootOpts,
27
28    #[command(subcommand)]
29    pub sub_command: Option<SubCommand>,
30}
31
32impl Opts {
33    pub fn get_matches() -> Result<Self, clap::Error> {
34        let version = get_version();
35        let app = Opts::command().version(version);
36        Opts::from_arg_matches(&app.get_matches())
37    }
38
39    pub const fn log_level(&self) -> &'static str {
40        let (quiet_level, verbose_level) = match self.sub_command {
41            Some(SubCommand::Validate(_))
42            | Some(SubCommand::Graph(_))
43            | Some(SubCommand::Generate(_))
44            | Some(SubCommand::ConvertConfig(_))
45            | Some(SubCommand::List(_))
46            | Some(SubCommand::Test(_)) => {
47                if self.root.verbose == 0 {
48                    (self.root.quiet + 1, self.root.verbose)
49                } else {
50                    (self.root.quiet, self.root.verbose - 1)
51                }
52            }
53            _ => (self.root.quiet, self.root.verbose),
54        };
55        match quiet_level {
56            0 => match verbose_level {
57                0 => "info",
58                1 => "debug",
59                2..=255 => "trace",
60            },
61            1 => "warn",
62            2 => "error",
63            3..=255 => "off",
64        }
65    }
66}
67
68#[derive(Parser, Debug)]
69#[command(rename_all = "kebab-case")]
70pub struct RootOpts {
71    /// Read configuration from one or more files. Wildcard paths are supported.
72    /// File format is detected from the file name.
73    /// If zero files are specified, the deprecated default config path
74    /// `/etc/vector/vector.yaml` is targeted.
75    #[arg(
76        id = "config",
77        short,
78        long,
79        env = "VECTOR_CONFIG",
80        value_delimiter(',')
81    )]
82    pub config_paths: Vec<PathBuf>,
83
84    /// Read configuration from files in one or more directories.
85    /// File format is detected from the file name.
86    ///
87    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
88    #[arg(
89        id = "config-dir",
90        short = 'C',
91        long,
92        env = "VECTOR_CONFIG_DIR",
93        value_delimiter(',')
94    )]
95    pub config_dirs: Vec<PathBuf>,
96
97    /// Read configuration from one or more files. Wildcard paths are supported.
98    /// TOML file format is expected.
99    #[arg(
100        id = "config-toml",
101        long,
102        env = "VECTOR_CONFIG_TOML",
103        value_delimiter(',')
104    )]
105    pub config_paths_toml: Vec<PathBuf>,
106
107    /// Read configuration from one or more files. Wildcard paths are supported.
108    /// JSON file format is expected.
109    #[arg(
110        id = "config-json",
111        long,
112        env = "VECTOR_CONFIG_JSON",
113        value_delimiter(',')
114    )]
115    pub config_paths_json: Vec<PathBuf>,
116
117    /// Read configuration from one or more files. Wildcard paths are supported.
118    /// YAML file format is expected.
119    #[arg(
120        id = "config-yaml",
121        long,
122        env = "VECTOR_CONFIG_YAML",
123        value_delimiter(',')
124    )]
125    pub config_paths_yaml: Vec<PathBuf>,
126
127    /// Exit on startup if any sinks fail healthchecks
128    #[arg(short, long, env = "VECTOR_REQUIRE_HEALTHY")]
129    pub require_healthy: Option<bool>,
130
131    /// Number of threads to use for processing (default is number of available cores)
132    #[arg(short, long, env = "VECTOR_THREADS")]
133    pub threads: Option<usize>,
134
135    /// Number of events batched per source send and used as the base for source output buffer sizing
136    /// (source output buffer capacity is this value multiplied by the number of worker threads)
137    #[arg(long, env = "VECTOR_CHUNK_SIZE_EVENTS")]
138    pub chunk_size_events: Option<NonZeroUsize>,
139
140    /// Enable more detailed internal logging. Repeat to increase level. Overridden by `--quiet`.
141    #[arg(short, long, action = ArgAction::Count)]
142    pub verbose: u8,
143
144    /// Reduce detail of internal logging. Repeat to reduce further. Overrides `--verbose`.
145    #[arg(short, long, action = ArgAction::Count)]
146    pub quiet: u8,
147
148    /// Allow interpolation of environment variables in configuration files. Enabling this may
149    /// expose environment secrets into your Vector configuration.
150    #[arg(
151        long,
152        env = "VECTOR_DANGEROUSLY_ALLOW_ENV_VAR_INTERPOLATION",
153        default_value = "false"
154    )]
155    pub dangerously_allow_env_var_interpolation: bool,
156
157    /// Set the logging format
158    #[arg(long, default_value = "text", env = "VECTOR_LOG_FORMAT")]
159    pub log_format: LogFormat,
160
161    /// Control when ANSI terminal formatting is used.
162    ///
163    /// By default `vector` will try and detect if `stdout` is a terminal, if it is
164    /// ANSI will be enabled. Otherwise it will be disabled. By providing this flag with
165    /// the `--color always` option will always enable ANSI terminal formatting. `--color never`
166    /// will disable all ANSI terminal formatting. `--color auto` will attempt
167    /// to detect it automatically.
168    #[arg(long, default_value = "auto", env = "VECTOR_COLOR")]
169    pub color: Color,
170
171    /// Watch for changes in configuration file, and reload accordingly.
172    #[arg(short, long, env = "VECTOR_WATCH_CONFIG")]
173    pub watch_config: bool,
174
175    /// Method for configuration watching.
176    ///
177    /// By default, `vector` uses recommended watcher for host OS
178    /// - `inotify` for Linux-based systems.
179    /// - `kqueue` for unix/macos
180    /// - `ReadDirectoryChangesWatcher` for windows
181    ///
182    /// The `poll` watcher can be used in cases where `inotify` doesn't work, e.g., when attaching the configuration via NFS.
183    #[arg(
184        long,
185        default_value = "recommended",
186        env = "VECTOR_WATCH_CONFIG_METHOD"
187    )]
188    pub watch_config_method: WatchConfigMethod,
189
190    /// Poll for changes in the configuration file at the given interval.
191    ///
192    /// This setting is only applicable if `Poll` is set in `--watch-config-method`.
193    #[arg(
194        long,
195        env = "VECTOR_WATCH_CONFIG_POLL_INTERVAL_SECONDS",
196        default_value = "30"
197    )]
198    pub watch_config_poll_interval_seconds: NonZeroU64,
199
200    /// Set the internal log rate limit in seconds.
201    ///
202    /// This controls the time window for rate limiting Vector's own internal logs.
203    /// Within each time window, the first occurrence of a log is emitted, the second
204    /// shows a suppression warning, and subsequent occurrences are silent until the
205    /// window expires. When the window expires and the log fires again, a summary of
206    /// the suppressed count is emitted followed by the log itself.
207    ///
208    /// Logs are grouped by their location in the code and the `component_id` field, so logs
209    /// from different components are rate limited independently.
210    ///
211    /// Examples:
212    /// - 1: Very verbose, logs can repeat every second
213    /// - 10 (default): Logs can repeat every 10 seconds
214    /// - 60: Less verbose, logs can repeat every minute
215    #[arg(
216        short,
217        long,
218        env = "VECTOR_INTERNAL_LOG_RATE_LIMIT",
219        default_value = "10"
220    )]
221    pub internal_log_rate_limit: u64,
222
223    /// Apply a rate limit (in seconds) to the broadcast channel that feeds all `internal_logs`
224    /// sources. When set, the first occurrence of a repeated log is emitted, the second shows a
225    /// suppression warning, and subsequent occurrences are silent until the window expires. When
226    /// the window expires and the log fires again, a summary of the suppressed count is emitted
227    /// followed by the log itself. Unset by default so that `internal_logs` consumers receive
228    /// every log event. This limit is independent of `--internal-log-rate-limit`, which only
229    /// applies to stdout/stderr output.
230    #[arg(long, env = "VECTOR_INTERNAL_LOGS_SOURCE_RATE_LIMIT")]
231    pub internal_logs_source_rate_limit: Option<NonZeroU64>,
232
233    /// Set the duration in seconds to wait for graceful shutdown after SIGINT or SIGTERM are
234    /// received. After the duration has passed, Vector will force shutdown. To never force
235    /// shutdown, use `--no-graceful-shutdown-limit`.
236    #[arg(
237        long,
238        default_value = "60",
239        env = "VECTOR_GRACEFUL_SHUTDOWN_LIMIT_SECS",
240        group = "graceful-shutdown-limit"
241    )]
242    pub graceful_shutdown_limit_secs: NonZeroU64,
243
244    /// Never time out while waiting for graceful shutdown after SIGINT or SIGTERM received.
245    /// This is useful when you would like for Vector to attempt to send data until terminated
246    /// by a SIGKILL. Overrides/cannot be set with `--graceful-shutdown-limit-secs`.
247    #[arg(
248        long,
249        default_value = "false",
250        env = "VECTOR_NO_GRACEFUL_SHUTDOWN_LIMIT",
251        group = "graceful-shutdown-limit"
252    )]
253    pub no_graceful_shutdown_limit: bool,
254
255    /// Set runtime allocation tracing
256    #[cfg(all(unix, feature = "tikv-jemallocator"))]
257    #[arg(long, env = "ALLOCATION_TRACING", default_value = "false")]
258    pub allocation_tracing: bool,
259
260    /// Set allocation tracing reporting rate in milliseconds.
261    #[cfg(all(unix, feature = "tikv-jemallocator"))]
262    #[arg(
263        long,
264        env = "ALLOCATION_TRACING_REPORTING_INTERVAL_MS",
265        default_value = "5000"
266    )]
267    pub allocation_tracing_reporting_interval_ms: u64,
268
269    /// Disable probing and configuration of root certificate locations on the system for OpenSSL.
270    ///
271    /// The probe functionality manipulates the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables
272    /// in the Vector process. This behavior can be problematic for users of the `exec` source, which by
273    /// default inherits the environment of the Vector process.
274    #[arg(long, env = "VECTOR_OPENSSL_NO_PROBE", default_value = "false")]
275    pub openssl_no_probe: bool,
276
277    /// Allow the configuration to run without any components. This is useful for loading in an
278    /// empty stub config that will later be replaced with actual components. Note that this is
279    /// likely not useful without also watching for config file changes as described in
280    /// `--watch-config`.
281    #[arg(long, env = "VECTOR_ALLOW_EMPTY_CONFIG", default_value = "false")]
282    pub allow_empty_config: bool,
283
284    /// Maximum number of bytes allowed after decompressing a payload.
285    ///
286    /// Sources that decompress incoming payloads (gzip, deflate, zstd, snappy) enforce this cap to
287    /// prevent a compressed "bomb" from exhausting memory. Payloads whose decompressed size exceeds
288    /// the limit are rejected.
289    ///
290    /// Defaults to 104857600 (100 MiB). Raise this only when sources routinely receive
291    /// legitimately large compressed payloads.
292    #[arg(
293        long,
294        env = "VECTOR_MAX_DECOMPRESSED_SIZE_BYTES",
295        default_value = "104857600"
296    )]
297    pub max_decompressed_size_bytes: usize,
298
299    /// Raise the file descriptor soft limit (RLIMIT_NOFILE) to the hard limit at startup.
300    ///
301    /// Many systems default the soft limit to 1024 (Linux) or 256 (macOS), which is too low
302    /// when Vector monitors large numbers of log files. This flag raises the soft limit to
303    /// prevent "Too many open files" errors without requiring manual sysadmin intervention.
304    #[cfg(unix)]
305    #[arg(long, env = "VECTOR_RAISE_FD_LIMIT", default_value = "false")]
306    pub raise_fd_limit: bool,
307}
308
309impl RootOpts {
310    /// Return a list of config paths with the associated formats.
311    pub fn config_paths_with_formats(&self) -> Vec<config::ConfigPath> {
312        config::merge_path_lists(vec![
313            (&self.config_paths, None),
314            (&self.config_paths_toml, Some(config::Format::Toml)),
315            (&self.config_paths_json, Some(config::Format::Json)),
316            (&self.config_paths_yaml, Some(config::Format::Yaml)),
317        ])
318        .map(|(path, hint)| config::ConfigPath::File(path, hint))
319        .chain(
320            self.config_dirs
321                .iter()
322                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
323        )
324        .collect()
325    }
326
327    pub fn init_global(&self) {
328        if !self.openssl_no_probe {
329            unsafe {
330                openssl_probe::init_openssl_env_vars();
331            }
332        }
333
334        crate::metrics::init_global().expect("metrics initialization failed");
335    }
336}
337
338/// Raise the soft file descriptor limit (RLIMIT_NOFILE) as high as the OS allows.
339///
340/// Many systems default the soft limit to 1024 (Linux) or 256 (macOS), which is too low
341/// for Vector when it monitors large numbers of log files. Raising it prevents
342/// "Too many open files (os error 24)" errors without requiring manual sysadmin intervention.
343///
344/// On Linux, the soft limit is raised to the hard limit (typically 65536+).
345/// On macOS, the hard limit can be RLIM_INFINITY, so we first try the hard limit,
346/// then fall back to the kernel-enforced `kern.maxfilesperproc` (typically 10240).
347#[cfg(unix)]
348pub(crate) fn raise_file_descriptor_limit() {
349    use nix::sys::resource::{Resource, getrlimit, setrlimit};
350    use tracing::{info, warn};
351
352    let (soft, hard) = match getrlimit(Resource::RLIMIT_NOFILE) {
353        Ok(limits) => limits,
354        Err(err) => {
355            warn!(message = "Failed to get file descriptor limit.", %err);
356            return;
357        }
358    };
359
360    if soft >= hard {
361        return; // Already at maximum
362    }
363
364    // Try setting soft limit to hard limit (works on Linux, may fail on macOS)
365    if setrlimit(Resource::RLIMIT_NOFILE, hard, hard).is_ok() {
366        info!(
367            message = "Raised file descriptor limit.",
368            from = soft,
369            to = hard,
370        );
371        return;
372    }
373
374    // On macOS, the hard limit can be RLIM_INFINITY which setrlimit rejects.
375    // Fall back to the kernel-enforced kern.maxfilesperproc.
376    #[cfg(target_os = "macos")]
377    {
378        if let Some(maxfiles) = macos_maxfilesperproc()
379            && maxfiles > soft
380            && setrlimit(Resource::RLIMIT_NOFILE, maxfiles, hard).is_ok()
381        {
382            info!(
383                message = "Raised file descriptor limit.",
384                from = soft,
385                to = maxfiles,
386            );
387            return;
388        }
389    }
390
391    warn!(
392        message = "Failed to raise file descriptor limit.",
393        current = soft,
394        attempted = hard,
395    );
396}
397
398/// Query the macOS kernel limit on per-process open files.
399#[cfg(target_os = "macos")]
400fn macos_maxfilesperproc() -> Option<libc::rlim_t> {
401    let mut maxfiles: libc::c_int = 0;
402    let mut len = std::mem::size_of::<libc::c_int>() as libc::size_t;
403    // Safety: sysctlbyname with a valid null-terminated name and correctly sized output buffer.
404    // No safe wrapper exists for this macOS-specific call.
405    let ret = unsafe {
406        libc::sysctlbyname(
407            c"kern.maxfilesperproc".as_ptr(),
408            &mut maxfiles as *mut libc::c_int as *mut libc::c_void,
409            &mut len,
410            std::ptr::null_mut(),
411            0,
412        )
413    };
414    if ret == 0 && maxfiles > 0 {
415        Some(maxfiles as libc::rlim_t)
416    } else {
417        None
418    }
419}
420
421#[derive(Parser, Debug)]
422#[command(rename_all = "kebab-case")]
423pub enum SubCommand {
424    /// Validate the target config, then exit.
425    Validate(validate::Opts),
426
427    /// Convert a config file from one format to another.
428    /// This command can also walk directories recursively and convert all config files that are discovered.
429    /// Note that this is a best effort conversion due to the following reasons:
430    /// * The comments from the original config file are not preserved.
431    /// * Explicitly set default values in the original implementation might be omitted.
432    /// * Depending on how each source/sink config struct configures serde, there might be entries with null values.
433    ConvertConfig(convert_config::Opts),
434
435    /// Generate a Vector configuration containing a list of components.
436    Generate(generate::Opts),
437
438    /// Generate the configuration schema for this version of Vector. (experimental)
439    ///
440    /// A JSON Schema document will be generated that represents the valid schema for a
441    /// Vector configuration. This schema is based on the "full" configuration, such that for usages
442    /// where a configuration is split into multiple files, the schema would apply to those files
443    /// only when concatenated together.
444    ///
445    /// By default all output is written to stdout. The `output_path` option can be used to redirect to a file.
446    GenerateSchema(generate_schema::Opts),
447
448    /// Generate shell completion, then exit.
449    #[command(hide = true)]
450    Completion(completion::Opts),
451
452    /// Output a provided Vector configuration file/dir as a single JSON object, useful for checking in to version control.
453    #[command(hide = true)]
454    Config(config::Opts),
455
456    /// List available components, then exit.
457    List(list::Opts),
458
459    /// Run Vector config unit tests, then exit. This command is experimental and therefore subject to change.
460    /// For guidance on how to write unit tests check out <https://vector.dev/guides/level-up/unit-testing/>.
461    Test(unit_test::Opts),
462
463    /// Output the topology as visual representation using the DOT language which can be rendered by GraphViz
464    Graph(graph::Opts),
465
466    /// Display topology and metrics in the console, for a local or remote Vector instance
467    #[cfg(feature = "top")]
468    Top(top::Opts),
469
470    /// Observe output log events from source or transform components. Logs are sampled at a specified interval.
471    #[cfg(feature = "api-client")]
472    Tap(tap::Opts),
473
474    /// Manage the vector service.
475    #[cfg(windows)]
476    Service(service::Opts),
477
478    /// Vector Remap Language CLI
479    Vrl(vrl::cli::Opts),
480}
481
482impl SubCommand {
483    #[expect(
484        clippy::missing_const_for_fn,
485        reason = "the #[cfg(windows)] arm calls a non-const method"
486    )]
487    pub fn dangerously_allow_env_var_interpolation(&self) -> bool {
488        match self {
489            Self::Config(c) => c.dangerously_allow_env_var_interpolation,
490            Self::Graph(g) => g.dangerously_allow_env_var_interpolation,
491            Self::Test(t) => t.dangerously_allow_env_var_interpolation,
492            Self::Validate(v) => v.dangerously_allow_env_var_interpolation,
493            #[cfg(windows)]
494            Self::Service(s) => s.dangerously_allow_env_var_interpolation(),
495            _ => false,
496        }
497    }
498
499    pub async fn execute(
500        &self,
501        mut signals: signal::SignalPair,
502        color: bool,
503    ) -> exitcode::ExitCode {
504        match self {
505            Self::Completion(s) => completion::cmd(s),
506            Self::Config(c) => config::cmd(c),
507            Self::ConvertConfig(opts) => convert_config::cmd(opts),
508            Self::Generate(g) => generate::cmd(g),
509            Self::GenerateSchema(opts) => generate_schema::cmd(opts),
510            Self::Graph(g) => graph::cmd(g),
511            Self::List(l) => list::cmd(l),
512            #[cfg(windows)]
513            Self::Service(s) => service::cmd(s),
514            #[cfg(feature = "api-client")]
515            Self::Tap(t) => tap::cmd(t, signals.receiver).await,
516            Self::Test(t) => unit_test::cmd(t, &mut signals.handler).await,
517            #[cfg(feature = "top")]
518            Self::Top(t) => top::cmd(t).await,
519            Self::Validate(v) => validate::validate(v, color).await,
520            Self::Vrl(s) => vrl::cli::cmd::cmd(s, vector_vrl_functions::all()),
521        }
522    }
523}
524
525#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
526pub enum Color {
527    Auto,
528    Always,
529    Never,
530}
531
532impl Color {
533    pub fn use_color(&self) -> bool {
534        match self {
535            #[cfg(unix)]
536            Color::Auto => {
537                use std::io::IsTerminal;
538                std::io::stdout().is_terminal()
539            }
540            #[cfg(windows)]
541            Color::Auto => false, // ANSI colors are not supported by cmd.exe
542            Color::Always => true,
543            Color::Never => false,
544        }
545    }
546}
547
548#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
549pub enum LogFormat {
550    Text,
551    Json,
552}
553
554#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
555pub enum WatchConfigMethod {
556    /// Recommended watcher for the current OS, usually `inotify` for Linux-based systems.
557    Recommended,
558    /// Poll-based watcher, typically used for watching files on EFS/NFS-like network storage systems.
559    /// The interval is determined by  [`RootOpts::watch_config_poll_interval_seconds`].
560    Poll,
561}
562
563pub fn handle_config_errors(errors: Vec<String>) -> exitcode::ExitCode {
564    for error in errors {
565        error!(message = "Configuration error.", %error, internal_log_rate_limit = false);
566    }
567
568    exitcode::CONFIG
569}
570
571#[cfg(test)]
572mod tests {
573    #[cfg(unix)]
574    fn run_in_subprocess(test_name: &str) {
575        let exe = std::env::current_exe().unwrap();
576        let output = std::process::Command::new(exe)
577            .env("__VECTOR_SUBPROCESS_TEST", "1")
578            .args(["--exact", test_name, "--nocapture"])
579            .output()
580            .unwrap();
581        assert!(
582            output.status.success(),
583            "subprocess test failed:\nstdout: {}\nstderr: {}",
584            String::from_utf8_lossy(&output.stdout),
585            String::from_utf8_lossy(&output.stderr),
586        );
587    }
588
589    #[test]
590    #[cfg(unix)]
591    fn test_raise_file_descriptor_limit() {
592        if std::env::var("__VECTOR_SUBPROCESS_TEST").is_err() {
593            run_in_subprocess("cli::tests::test_raise_file_descriptor_limit");
594            return;
595        }
596
597        use nix::sys::resource::{Resource, getrlimit, setrlimit};
598
599        let (original_soft, hard) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
600        let lowered = std::cmp::min(original_soft, 256);
601        if lowered < hard {
602            setrlimit(Resource::RLIMIT_NOFILE, lowered, hard).unwrap();
603
604            let (soft_before, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
605            assert_eq!(soft_before, lowered);
606
607            super::raise_file_descriptor_limit();
608
609            let (soft_after, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
610            assert!(
611                soft_after > lowered,
612                "Expected soft limit to be raised above {lowered}, got {soft_after}"
613            );
614        }
615    }
616
617    #[test]
618    #[cfg(unix)]
619    fn test_raise_file_descriptor_limit_already_at_max() {
620        if std::env::var("__VECTOR_SUBPROCESS_TEST").is_err() {
621            run_in_subprocess("cli::tests::test_raise_file_descriptor_limit_already_at_max");
622            return;
623        }
624
625        use nix::sys::resource::{Resource, getrlimit, setrlimit};
626
627        let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
628
629        if setrlimit(Resource::RLIMIT_NOFILE, hard, hard).is_err() {
630            #[cfg(target_os = "macos")]
631            if let Some(maxfiles) = super::macos_maxfilesperproc() {
632                setrlimit(Resource::RLIMIT_NOFILE, maxfiles, hard).ok();
633            }
634        }
635
636        let (soft_before, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
637
638        super::raise_file_descriptor_limit();
639
640        let (soft_after, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
641        assert_eq!(soft_before, soft_after);
642    }
643
644    #[test]
645    #[cfg(target_os = "macos")]
646    fn test_macos_maxfilesperproc_returns_positive() {
647        let result = super::macos_maxfilesperproc();
648        assert!(
649            result.is_some(),
650            "macos_maxfilesperproc() should return Some on macOS"
651        );
652        assert!(
653            result.unwrap() > 0,
654            "kern.maxfilesperproc should be positive"
655        );
656    }
657}