Skip to main content

vector/
app.rs

1#![allow(missing_docs)]
2#[cfg(unix)]
3use std::os::unix::process::ExitStatusExt;
4#[cfg(windows)]
5use std::os::windows::process::ExitStatusExt;
6use std::{
7    num::{NonZeroU64, NonZeroUsize},
8    path::PathBuf,
9    process::ExitStatus,
10    sync::atomic::{AtomicUsize, Ordering},
11    time::Duration,
12};
13
14use exitcode::ExitCode;
15use futures::StreamExt;
16use tokio::{
17    runtime::{self, Handle, Runtime},
18    sync::{MutexGuard, broadcast::error::RecvError},
19};
20use tokio_stream::wrappers::UnboundedReceiverStream;
21
22#[cfg(feature = "api")]
23use crate::api;
24#[cfg(feature = "api")]
25use crate::internal_events::ApiStarted;
26use crate::{
27    cli::{LogFormat, Opts, RootOpts, WatchConfigMethod, handle_config_errors},
28    config::{self, ComponentConfig, ComponentType, Config, ConfigPath},
29    extra_context::ExtraContext,
30    heartbeat,
31    internal_events::{
32        VectorConfigLoadError, VectorQuit, VectorStarted, VectorStopped, VectorStopping,
33    },
34    signal::{SignalHandler, SignalPair, SignalRx, SignalTo},
35    topology::{
36        ReloadOutcome, RunningTopology, SharedTopologyController, ShutdownErrorReceiver,
37        TopologyController,
38    },
39    trace,
40};
41
42static WORKER_THREADS: AtomicUsize = AtomicUsize::new(0);
43
44pub fn worker_threads() -> Option<NonZeroUsize> {
45    NonZeroUsize::new(WORKER_THREADS.load(Ordering::Relaxed))
46}
47
48pub struct ApplicationConfig {
49    pub config_paths: Vec<config::ConfigPath>,
50    pub topology: RunningTopology,
51    pub graceful_crash_receiver: ShutdownErrorReceiver,
52    pub internal_topologies: Vec<RunningTopology>,
53    #[cfg(feature = "api")]
54    pub api: config::api::Options,
55    pub extra_context: ExtraContext,
56}
57
58pub struct Application {
59    pub root_opts: RootOpts,
60    pub config: ApplicationConfig,
61    pub signals: SignalPair,
62}
63
64impl ApplicationConfig {
65    pub async fn from_opts(
66        opts: &RootOpts,
67        signal_handler: &mut SignalHandler,
68        extra_context: ExtraContext,
69    ) -> Result<Self, ExitCode> {
70        let config_paths = opts.config_paths_with_formats();
71
72        let graceful_shutdown_duration = (!opts.no_graceful_shutdown_limit)
73            .then(|| Duration::from_secs(u64::from(opts.graceful_shutdown_limit_secs)));
74
75        let watcher_conf = if opts.watch_config {
76            Some(watcher_config(
77                opts.watch_config_method,
78                opts.watch_config_poll_interval_seconds,
79            ))
80        } else {
81            None
82        };
83
84        let config = load_configs(
85            &config_paths,
86            watcher_conf,
87            opts.require_healthy,
88            opts.allow_empty_config,
89            graceful_shutdown_duration,
90            signal_handler,
91        )
92        .await?;
93
94        Self::from_config(config_paths, config, extra_context).await
95    }
96
97    pub async fn from_config(
98        config_paths: Vec<ConfigPath>,
99        config: Config,
100        extra_context: ExtraContext,
101    ) -> Result<Self, ExitCode> {
102        #[cfg(feature = "api")]
103        let api = config.api;
104
105        let (topology, graceful_crash_receiver) =
106            RunningTopology::start_init_validated(config, extra_context.clone())
107                .await
108                .ok_or(exitcode::CONFIG)?;
109
110        Ok(Self {
111            config_paths,
112            topology,
113            graceful_crash_receiver,
114            internal_topologies: Vec::new(),
115            #[cfg(feature = "api")]
116            api,
117            extra_context,
118        })
119    }
120
121    pub async fn add_internal_config(
122        &mut self,
123        config: Config,
124        extra_context: ExtraContext,
125    ) -> Result<(), ExitCode> {
126        let Some((topology, _)) =
127            RunningTopology::start_init_validated(config, extra_context).await
128        else {
129            return Err(exitcode::CONFIG);
130        };
131        self.internal_topologies.push(topology);
132        Ok(())
133    }
134
135    /// Configure the gRPC API server, if applicable
136    #[cfg(feature = "api")]
137    pub fn setup_api(&self, handle: &Handle) -> Option<api::GrpcServer> {
138        if self.api.enabled {
139            // Start gRPC server
140            let api_server = handle.block_on(api::GrpcServer::start(
141                self.topology.config(),
142                self.topology.watch(),
143            ));
144            match api_server {
145                Ok(server) => {
146                    emit!(ApiStarted {
147                        addr: server.addr()
148                    });
149                    Some(server)
150                }
151                Err(error) => {
152                    let error = error.to_string();
153                    error!(
154                        message = "An error occurred that Vector couldn't handle.",
155                        %error,
156                        internal_log_rate_limit = false
157                    );
158                    // Trigger shutdown because the API was explicitly enabled but failed to start
159                    // This ensures users don't run Vector thinking top/tap will work when they won't
160                    _ = self
161                        .topology
162                        .abort_tx
163                        .send(crate::signal::ShutdownError::ApiFailed { error });
164                    None
165                }
166            }
167        } else {
168            info!(
169                message = "API is disabled, enable by setting `api.enabled` to `true` and use commands like `vector top`."
170            );
171            None
172        }
173    }
174}
175
176impl Application {
177    pub fn run(extra_context: ExtraContext) -> ExitStatus {
178        let (runtime, app) =
179            Self::prepare_start(extra_context).unwrap_or_else(|code| std::process::exit(code));
180
181        runtime.block_on(app.run())
182    }
183
184    pub fn prepare_start(
185        extra_context: ExtraContext,
186    ) -> Result<(Runtime, StartedApplication), ExitCode> {
187        Self::prepare(extra_context)
188            .and_then(|(runtime, app)| app.start(runtime.handle()).map(|app| (runtime, app)))
189    }
190
191    pub fn prepare(extra_context: ExtraContext) -> Result<(Runtime, Self), ExitCode> {
192        let opts = Opts::get_matches().map_err(|error| {
193            // Printing to stdout/err can itself fail; ignore it.
194            _ = error.print();
195            exitcode::USAGE
196        })?;
197
198        Self::prepare_from_opts(opts, extra_context)
199    }
200
201    pub fn prepare_from_opts(
202        opts: Opts,
203        extra_context: ExtraContext,
204    ) -> Result<(Runtime, Self), ExitCode> {
205        opts.root.init_global();
206
207        crate::sources::util::set_max_decompressed_size_bytes(
208            opts.root.max_decompressed_size_bytes,
209        );
210
211        let color = opts.root.color.use_color();
212
213        init_logging(
214            color,
215            opts.root.log_format,
216            opts.log_level(),
217            opts.root.internal_log_rate_limit,
218            opts.root.internal_logs_source_rate_limit,
219        );
220
221        #[cfg(unix)]
222        if opts.root.raise_fd_limit {
223            crate::cli::raise_file_descriptor_limit();
224        }
225
226        // Set global color preference for downstream modules
227        crate::set_global_color(color);
228
229        // Can only log this after initializing the logging subsystem
230        if opts.root.openssl_no_probe {
231            debug!(
232                message = "Disabled probing and configuration of root certificate locations on the system for OpenSSL."
233            );
234        }
235
236        let runtime = build_runtime(
237            opts.root.threads,
238            opts.root.chunk_size_events,
239            "vector-worker",
240        )?;
241
242        // Signal handler for OS and provider messages.
243        let mut signals = SignalPair::new(&runtime);
244
245        if let Some(sub_command) = &opts.sub_command {
246            // Combine root and subcommand flags before setting the global once.
247            config::set_env_var_interpolation(
248                opts.root.dangerously_allow_env_var_interpolation
249                    || sub_command.dangerously_allow_env_var_interpolation(),
250            );
251            return Err(runtime.block_on(sub_command.execute(signals, color)));
252        }
253
254        config::set_env_var_interpolation(opts.root.dangerously_allow_env_var_interpolation);
255
256        let config = runtime.block_on(ApplicationConfig::from_opts(
257            &opts.root,
258            &mut signals.handler,
259            extra_context,
260        ))?;
261
262        Ok((
263            runtime,
264            Self {
265                root_opts: opts.root,
266                config,
267                signals,
268            },
269        ))
270    }
271
272    pub fn start(self, handle: &Handle) -> Result<StartedApplication, ExitCode> {
273        // Any internal_logs sources will have grabbed a copy of the
274        // early buffer by this point and set up a subscriber.
275        crate::trace::stop_early_buffering();
276
277        emit!(VectorStarted);
278        handle.spawn(heartbeat::heartbeat());
279
280        let Self {
281            root_opts,
282            config,
283            signals,
284        } = self;
285
286        #[cfg(feature = "api")]
287        let api_server = config.setup_api(handle);
288
289        let topology_controller = SharedTopologyController::new(TopologyController {
290            #[cfg(feature = "api")]
291            api_server,
292            topology: config.topology,
293            config_paths: config.config_paths.clone(),
294            require_healthy: root_opts.require_healthy,
295            extra_context: config.extra_context,
296        });
297
298        Ok(StartedApplication {
299            config_paths: config.config_paths,
300            internal_topologies: config.internal_topologies,
301            graceful_crash_receiver: config.graceful_crash_receiver,
302            signals,
303            topology_controller,
304            allow_empty_config: root_opts.allow_empty_config,
305        })
306    }
307}
308
309pub struct StartedApplication {
310    pub config_paths: Vec<ConfigPath>,
311    pub internal_topologies: Vec<RunningTopology>,
312    pub graceful_crash_receiver: ShutdownErrorReceiver,
313    pub signals: SignalPair,
314    pub topology_controller: SharedTopologyController,
315    pub allow_empty_config: bool,
316}
317
318impl StartedApplication {
319    pub async fn run(self) -> ExitStatus {
320        self.main().await.shutdown().await
321    }
322
323    pub async fn main(self) -> FinishedApplication {
324        let Self {
325            config_paths,
326            graceful_crash_receiver,
327            signals,
328            topology_controller,
329            internal_topologies,
330            allow_empty_config,
331        } = self;
332
333        let mut graceful_crash = UnboundedReceiverStream::new(graceful_crash_receiver);
334
335        let mut signal_handler = signals.handler;
336        let mut signal_rx = signals.receiver;
337
338        let signal = loop {
339            let has_sources = !topology_controller.lock().await.topology.config.is_empty();
340            tokio::select! {
341                signal = signal_rx.recv() => if let Some(signal) = handle_signal(
342                    signal,
343                    &topology_controller,
344                    &config_paths,
345                    &mut signal_handler,
346                    allow_empty_config,
347                ).await {
348                    break signal;
349                },
350                // Trigger graceful shutdown if a component crashed, or all sources have ended.
351                error = graceful_crash.next() => break SignalTo::Shutdown(error),
352                _ = TopologyController::sources_finished(topology_controller.clone()), if has_sources => {
353                    info!("All sources have finished.");
354                    break SignalTo::Shutdown(None)
355                } ,
356                else => unreachable!("Signal streams never end"),
357            }
358        };
359
360        FinishedApplication {
361            signal,
362            signal_rx,
363            topology_controller,
364            internal_topologies,
365        }
366    }
367}
368
369async fn handle_signal(
370    signal: Result<SignalTo, RecvError>,
371    topology_controller: &SharedTopologyController,
372    config_paths: &[ConfigPath],
373    signal_handler: &mut SignalHandler,
374    allow_empty_config: bool,
375) -> Option<SignalTo> {
376    match signal {
377        Ok(SignalTo::ReloadComponents(components_to_reload)) => {
378            let mut topology_controller = topology_controller.lock().await;
379            topology_controller
380                .topology
381                .extend_reload_set(components_to_reload);
382
383            // Reload paths
384            if let Some(paths) = config::process_paths(config_paths) {
385                topology_controller.config_paths = paths;
386            }
387
388            // Reload config
389            let new_config = config::load_from_paths_with_provider_and_secrets(
390                &topology_controller.config_paths,
391                signal_handler,
392                allow_empty_config,
393            )
394            .await;
395
396            reload_config_from_result(topology_controller, new_config).await
397        }
398        Ok(SignalTo::ReloadFromConfigBuilder(config_builder)) => {
399            let topology_controller = topology_controller.lock().await;
400            reload_config_from_result(topology_controller, config_builder.build()).await
401        }
402        Ok(SignalTo::ReloadFromDisk) => {
403            let mut topology_controller = topology_controller.lock().await;
404
405            // Reload paths
406            if let Some(paths) = config::process_paths(config_paths) {
407                topology_controller.config_paths = paths;
408            }
409
410            // Reload config
411            let new_config = config::load_from_paths_with_provider_and_secrets(
412                &topology_controller.config_paths,
413                signal_handler,
414                allow_empty_config,
415            )
416            .await;
417
418            if let Ok(ref config) = new_config {
419                // Find all transforms that have external files to watch
420                let transform_keys_to_reload = config.transform_keys_with_external_files();
421
422                // Add these transforms to reload set
423                if !transform_keys_to_reload.is_empty() {
424                    info!(
425                        message = "Reloading transforms with external files.",
426                        count = transform_keys_to_reload.len()
427                    );
428                    topology_controller
429                        .topology
430                        .extend_reload_set(transform_keys_to_reload);
431                }
432            }
433
434            reload_config_from_result(topology_controller, new_config).await
435        }
436        Ok(SignalTo::ReloadEnrichmentTables) => {
437            let topology_controller = topology_controller.lock().await;
438
439            topology_controller
440                .topology
441                .reload_enrichment_tables()
442                .await;
443            None
444        }
445        Err(RecvError::Lagged(amt)) => {
446            warn!("Overflow, dropped {} signals.", amt);
447            None
448        }
449        Err(RecvError::Closed) => Some(SignalTo::Shutdown(None)),
450        Ok(signal) => Some(signal),
451    }
452}
453
454async fn reload_config_from_result(
455    mut topology_controller: MutexGuard<'_, TopologyController>,
456    config: Result<Config, Vec<String>>,
457) -> Option<SignalTo> {
458    match config {
459        Ok(new_config) => match topology_controller.reload(new_config).await {
460            ReloadOutcome::FatalError(error) => Some(SignalTo::Shutdown(Some(error))),
461            _ => None,
462        },
463        Err(errors) => {
464            handle_config_errors(errors);
465            emit!(VectorConfigLoadError);
466            None
467        }
468    }
469}
470
471pub struct FinishedApplication {
472    pub signal: SignalTo,
473    pub signal_rx: SignalRx,
474    pub topology_controller: SharedTopologyController,
475    pub internal_topologies: Vec<RunningTopology>,
476}
477
478impl FinishedApplication {
479    pub async fn shutdown(self) -> ExitStatus {
480        let FinishedApplication {
481            signal,
482            signal_rx,
483            topology_controller,
484            internal_topologies,
485        } = self;
486
487        // At this point, we'll have the only reference to the shared topology controller and can
488        // safely remove it from the wrapper to shut down the topology.
489        let topology_controller = topology_controller
490            .try_into_inner()
491            .expect("fail to unwrap topology controller")
492            .into_inner();
493
494        let status = match signal {
495            SignalTo::Shutdown(_) => Self::stop(topology_controller, signal_rx).await,
496            SignalTo::Quit => Self::quit(),
497            _ => unreachable!(),
498        };
499
500        for topology in internal_topologies {
501            topology.stop().await;
502        }
503
504        status
505    }
506
507    async fn stop(topology_controller: TopologyController, mut signal_rx: SignalRx) -> ExitStatus {
508        emit!(VectorStopping);
509        tokio::select! {
510            _ = topology_controller.stop() => {
511                emit!(VectorStopped);
512                ExitStatus::from_raw({
513                    #[cfg(windows)]
514                    {
515                        exitcode::OK as u32
516                    }
517                    #[cfg(unix)]
518                    exitcode::OK
519                })
520            }, // Graceful shutdown finished
521            _ = signal_rx.recv() => Self::quit(),
522        }
523    }
524
525    fn quit() -> ExitStatus {
526        // It is highly unlikely that this event will exit from topology.
527        emit!(VectorQuit);
528        ExitStatus::from_raw({
529            #[cfg(windows)]
530            {
531                exitcode::UNAVAILABLE as u32
532            }
533            #[cfg(unix)]
534            exitcode::OK
535        })
536    }
537}
538
539fn get_log_levels(default: &str) -> String {
540    std::env::var("VECTOR_LOG")
541        .or_else(|_| {
542            std::env::var("LOG").inspect(|_log| {
543                warn!(
544                    message =
545                        "DEPRECATED: Use of $LOG is deprecated. Please use $VECTOR_LOG instead."
546                );
547            })
548        })
549        .unwrap_or_else(|_| default.into())
550}
551
552pub fn build_runtime(
553    threads: Option<usize>,
554    chunk_size_events: Option<NonZeroUsize>,
555    thread_name: &str,
556) -> Result<Runtime, ExitCode> {
557    let mut rt_builder = runtime::Builder::new_multi_thread();
558    rt_builder.max_blocking_threads(20_000);
559    rt_builder.enable_all().thread_name(thread_name);
560
561    let threads = threads.unwrap_or_else(crate::num_threads);
562    if threads == 0 {
563        error!("The `threads` argument must be greater or equal to 1.");
564        return Err(exitcode::CONFIG);
565    }
566    WORKER_THREADS
567        .compare_exchange(0, threads, Ordering::Acquire, Ordering::Relaxed)
568        .unwrap_or_else(|_| panic!("double thread initialization"));
569    rt_builder.worker_threads(threads);
570
571    let chunk_size_events = chunk_size_events
572        .map(NonZeroUsize::get)
573        .unwrap_or(vector_lib::source_sender::DEFAULT_CHUNK_SIZE_EVENTS);
574
575    let Some(source_sender_buffer_size) = threads.checked_mul(chunk_size_events) else {
576        error!(
577            "The `chunk_size_events` argument is too large for the configured number of threads."
578        );
579        return Err(exitcode::CONFIG);
580    };
581    let Some(ready_array_capacity) =
582        chunk_size_events.checked_mul(crate::topology::builder::READY_ARRAY_CAPACITY_CHUNKS)
583    else {
584        error!("The `chunk_size_events` argument is too large.");
585        return Err(exitcode::CONFIG);
586    };
587
588    vector_lib::source_sender::set_chunk_size_events(chunk_size_events);
589    crate::topology::builder::set_source_sender_buffer_size(source_sender_buffer_size);
590    crate::topology::builder::set_ready_array_capacity(ready_array_capacity);
591
592    debug!(
593        message = "Building runtime.",
594        worker_threads = threads,
595        chunk_size_events
596    );
597    Ok(rt_builder.build().expect("Unable to create async runtime"))
598}
599
600pub async fn load_configs(
601    config_paths: &[ConfigPath],
602    watcher_conf: Option<config::watcher::WatcherConfig>,
603    require_healthy: Option<bool>,
604    allow_empty_config: bool,
605    graceful_shutdown_duration: Option<Duration>,
606    signal_handler: &mut SignalHandler,
607) -> Result<Config, ExitCode> {
608    let config_paths = config::process_paths(config_paths).ok_or(exitcode::CONFIG)?;
609
610    let watched_paths = config_paths
611        .iter()
612        .map(<&PathBuf>::from)
613        .collect::<Vec<_>>();
614
615    info!(
616        message = "Loading configs.",
617        paths = ?watched_paths
618    );
619
620    let mut config = config::load_from_paths_with_provider_and_secrets(
621        &config_paths,
622        signal_handler,
623        allow_empty_config,
624    )
625    .await
626    .map_err(handle_config_errors)?;
627
628    let mut watched_component_paths = Vec::new();
629
630    if let Some(watcher_conf) = watcher_conf {
631        for (name, transform) in config.transforms() {
632            let files = transform.inner.files_to_watch();
633            let component_config = ComponentConfig::new(
634                files.into_iter().cloned().collect(),
635                name.clone(),
636                ComponentType::Transform,
637            );
638            watched_component_paths.push(component_config);
639        }
640
641        for (name, sink) in config.sinks() {
642            let files = sink.inner.files_to_watch();
643            let component_config = ComponentConfig::new(
644                files.into_iter().cloned().collect(),
645                name.clone(),
646                ComponentType::Sink,
647            );
648            watched_component_paths.push(component_config);
649        }
650
651        for (name, table) in config.enrichment_tables() {
652            let files = table.inner.files_to_watch();
653            let component_config = ComponentConfig::new(
654                files.clone().into_iter().cloned().collect(),
655                name.clone(),
656                ComponentType::EnrichmentTable,
657            );
658            watched_component_paths.push(component_config);
659            if table.as_sink(name).is_some() {
660                let sink_component_config = ComponentConfig::new(
661                    files.into_iter().cloned().collect(),
662                    name.clone(),
663                    ComponentType::Sink,
664                );
665                watched_component_paths.push(sink_component_config);
666            }
667        }
668
669        info!(
670            message = "Starting watcher.",
671            paths = ?watched_paths
672        );
673        info!(
674            message = "Components to watch.",
675            paths = ?watched_component_paths
676        );
677
678        // Start listening for config changes.
679        config::watcher::spawn_thread(
680            watcher_conf,
681            signal_handler.clone_tx(),
682            watched_paths,
683            watched_component_paths,
684            None,
685        )
686        .map_err(|error| {
687            error!(message = "Unable to start config watcher.", %error);
688            exitcode::CONFIG
689        })?;
690    }
691
692    config::init_log_schema(config.global.log_schema.clone(), true);
693    config::init_telemetry(config.global.telemetry.clone(), true);
694
695    if !config.healthchecks.enabled {
696        info!("Health checks are disabled.");
697    }
698    config.healthchecks.set_require_healthy(require_healthy);
699    config.graceful_shutdown_duration = graceful_shutdown_duration;
700
701    Ok(config)
702}
703
704pub fn init_logging(
705    color: bool,
706    format: LogFormat,
707    log_level: &str,
708    internal_log_rate_limit_secs: u64,
709    internal_logs_source_rate_limit_secs: Option<NonZeroU64>,
710) {
711    let level = get_log_levels(log_level);
712    let json = match format {
713        LogFormat::Text => false,
714        LogFormat::Json => true,
715    };
716
717    trace::init(
718        color,
719        json,
720        &level,
721        internal_log_rate_limit_secs,
722        internal_logs_source_rate_limit_secs,
723    );
724    debug!(
725        message = "Internal log rate limit configured.",
726        internal_log_rate_limit_secs,
727        internal_logs_source_rate_limit_secs =
728            internal_logs_source_rate_limit_secs.map(NonZeroU64::get),
729    );
730    info!(message = "Log level is enabled.", ?level);
731}
732
733pub fn watcher_config(
734    method: WatchConfigMethod,
735    interval: NonZeroU64,
736) -> config::watcher::WatcherConfig {
737    match method {
738        WatchConfigMethod::Recommended => config::watcher::WatcherConfig::RecommendedWatcher,
739        WatchConfigMethod::Poll => config::watcher::WatcherConfig::PollWatcher(interval.into()),
740    }
741}