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 #[arg(
76 id = "config",
77 short,
78 long,
79 env = "VECTOR_CONFIG",
80 value_delimiter(',')
81 )]
82 pub config_paths: Vec<PathBuf>,
83
84 #[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 #[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 #[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 #[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 #[arg(short, long, env = "VECTOR_REQUIRE_HEALTHY")]
129 pub require_healthy: Option<bool>,
130
131 #[arg(short, long, env = "VECTOR_THREADS")]
133 pub threads: Option<usize>,
134
135 #[arg(long, env = "VECTOR_CHUNK_SIZE_EVENTS")]
138 pub chunk_size_events: Option<NonZeroUsize>,
139
140 #[arg(short, long, action = ArgAction::Count)]
142 pub verbose: u8,
143
144 #[arg(short, long, action = ArgAction::Count)]
146 pub quiet: u8,
147
148 #[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 #[arg(long, default_value = "text", env = "VECTOR_LOG_FORMAT")]
159 pub log_format: LogFormat,
160
161 #[arg(long, default_value = "auto", env = "VECTOR_COLOR")]
169 pub color: Color,
170
171 #[arg(short, long, env = "VECTOR_WATCH_CONFIG")]
173 pub watch_config: bool,
174
175 #[arg(
184 long,
185 default_value = "recommended",
186 env = "VECTOR_WATCH_CONFIG_METHOD"
187 )]
188 pub watch_config_method: WatchConfigMethod,
189
190 #[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 #[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 #[arg(long, env = "VECTOR_INTERNAL_LOGS_SOURCE_RATE_LIMIT")]
231 pub internal_logs_source_rate_limit: Option<NonZeroU64>,
232
233 #[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 #[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 #[cfg(all(unix, feature = "tikv-jemallocator"))]
257 #[arg(long, env = "ALLOCATION_TRACING", default_value = "false")]
258 pub allocation_tracing: bool,
259
260 #[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 #[arg(long, env = "VECTOR_OPENSSL_NO_PROBE", default_value = "false")]
275 pub openssl_no_probe: bool,
276
277 #[arg(long, env = "VECTOR_ALLOW_EMPTY_CONFIG", default_value = "false")]
282 pub allow_empty_config: bool,
283
284 #[arg(
293 long,
294 env = "VECTOR_MAX_DECOMPRESSED_SIZE_BYTES",
295 default_value = "104857600"
296 )]
297 pub max_decompressed_size_bytes: usize,
298
299 #[cfg(unix)]
305 #[arg(long, env = "VECTOR_RAISE_FD_LIMIT", default_value = "false")]
306 pub raise_fd_limit: bool,
307}
308
309impl RootOpts {
310 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#[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; }
363
364 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 #[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#[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 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(validate::Opts),
426
427 ConvertConfig(convert_config::Opts),
434
435 Generate(generate::Opts),
437
438 GenerateSchema(generate_schema::Opts),
447
448 #[command(hide = true)]
450 Completion(completion::Opts),
451
452 #[command(hide = true)]
454 Config(config::Opts),
455
456 List(list::Opts),
458
459 Test(unit_test::Opts),
462
463 Graph(graph::Opts),
465
466 #[cfg(feature = "top")]
468 Top(top::Opts),
469
470 #[cfg(feature = "api-client")]
472 Tap(tap::Opts),
473
474 #[cfg(windows)]
476 Service(service::Opts),
477
478 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, 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,
558 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}