vector/
graph.rs

1use std::{collections::HashMap, fmt::Write as _, path::PathBuf};
2
3use clap::Parser;
4use itertools::Itertools;
5
6use crate::config;
7
8#[derive(Parser, Debug)]
9#[command(rename_all = "kebab-case")]
10pub struct Opts {
11    /// Read configuration from one or more files. Wildcard paths are supported.
12    /// File format is detected from the file name.
13    /// If zero files are specified the default config path
14    /// `/etc/vector/vector.yaml` will be targeted.
15    #[arg(
16        id = "config",
17        short,
18        long,
19        env = "VECTOR_CONFIG",
20        value_delimiter(',')
21    )]
22    paths: Vec<PathBuf>,
23
24    /// Vector config files in TOML format.
25    #[arg(id = "config-toml", long, value_delimiter(','))]
26    paths_toml: Vec<PathBuf>,
27
28    /// Vector config files in JSON format.
29    #[arg(id = "config-json", long, value_delimiter(','))]
30    paths_json: Vec<PathBuf>,
31
32    /// Vector config files in YAML format.
33    #[arg(id = "config-yaml", long, value_delimiter(','))]
34    paths_yaml: Vec<PathBuf>,
35
36    /// Read configuration from files in one or more directories.
37    /// File format is detected from the file name.
38    ///
39    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
40    #[arg(
41        id = "config-dir",
42        short = 'C',
43        long,
44        env = "VECTOR_CONFIG_DIR",
45        value_delimiter(',')
46    )]
47    pub config_dirs: Vec<PathBuf>,
48
49    /// Set the output format
50    ///
51    /// See https://mermaid.js.org/syntax/flowchart.html#styling-and-classes for
52    /// information on the `mermaid` format.
53    #[arg(id = "format", long, default_value = "dot")]
54    pub format: OutputFormat,
55
56    /// Disable interpolation of environment variables in configuration files.
57    #[arg(
58        long,
59        env = "VECTOR_DISABLE_ENV_VAR_INTERPOLATION",
60        default_value = "false"
61    )]
62    pub disable_env_var_interpolation: bool,
63}
64
65#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
66pub enum OutputFormat {
67    Dot,
68    Mermaid,
69}
70
71impl Opts {
72    fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
73        config::merge_path_lists(vec![
74            (&self.paths, None),
75            (&self.paths_toml, Some(config::Format::Toml)),
76            (&self.paths_json, Some(config::Format::Json)),
77            (&self.paths_yaml, Some(config::Format::Yaml)),
78        ])
79        .map(|(path, hint)| config::ConfigPath::File(path, hint))
80        .chain(
81            self.config_dirs
82                .iter()
83                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
84        )
85        .collect()
86    }
87}
88
89fn node_attributes_to_string(attributes: &HashMap<String, String>, default_shape: &str) -> String {
90    let mut attrs = attributes.clone();
91    if !attrs.contains_key("shape") {
92        attrs.insert("shape".to_string(), default_shape.to_string());
93    }
94    attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
95}
96
97pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
98    let paths = opts.paths_with_formats();
99    let paths = match config::process_paths(&paths) {
100        Some(paths) => paths,
101        None => return exitcode::CONFIG,
102    };
103
104    let config = match config::load_from_paths(&paths, !opts.disable_env_var_interpolation) {
105        Ok(config) => config,
106        Err(errs) => {
107            #[allow(clippy::print_stderr)]
108            for err in errs {
109                eprintln!("{err}");
110            }
111            return exitcode::CONFIG;
112        }
113    };
114
115    let format = opts.format;
116    match format {
117        OutputFormat::Dot => render_dot(config),
118        OutputFormat::Mermaid => render_mermaid(config),
119    }
120}
121
122fn render_dot(config: config::Config) -> exitcode::ExitCode {
123    let mut dot = String::from("digraph {\n");
124
125    for (id, source) in config.sources() {
126        writeln!(
127            dot,
128            "  \"{}\" [{}]",
129            id,
130            node_attributes_to_string(&source.graph.node_attributes, "trapezium")
131        )
132        .expect("write to String never fails");
133    }
134
135    for (id, transform) in config.transforms() {
136        writeln!(
137            dot,
138            "  \"{}\" [{}]",
139            id,
140            node_attributes_to_string(&transform.graph.node_attributes, "diamond")
141        )
142        .expect("write to String never fails");
143
144        for input in transform.inputs.iter() {
145            if let Some(port) = &input.port {
146                writeln!(
147                    dot,
148                    "  \"{}\" -> \"{}\" [label=\"{}\"]",
149                    input.component, id, port
150                )
151                .expect("write to String never fails");
152            } else {
153                writeln!(dot, "  \"{input}\" -> \"{id}\"").expect("write to String never fails");
154            }
155        }
156    }
157
158    for (id, sink) in config.sinks() {
159        writeln!(
160            dot,
161            "  \"{}\" [{}]",
162            id,
163            node_attributes_to_string(&sink.graph.node_attributes, "invtrapezium")
164        )
165        .expect("write to String never fails");
166
167        for input in &sink.inputs {
168            if let Some(port) = &input.port {
169                writeln!(
170                    dot,
171                    "  \"{}\" -> \"{}\" [label=\"{}\"]",
172                    input.component, id, port
173                )
174                .expect("write to String never fails");
175            } else {
176                writeln!(dot, "  \"{input}\" -> \"{id}\"").expect("write to String never fails");
177            }
178        }
179    }
180
181    dot += "}";
182
183    #[allow(clippy::print_stdout)]
184    {
185        println!("{dot}");
186    }
187
188    exitcode::OK
189}
190
191fn render_mermaid(config: config::Config) -> exitcode::ExitCode {
192    let mut mermaid = String::from("flowchart TD;\n");
193
194    writeln!(mermaid, "\n  %% Sources").unwrap();
195    for (id, _) in config.sources() {
196        writeln!(mermaid, "  {id}[/{id}/]").unwrap();
197    }
198
199    writeln!(mermaid, "\n  %% Transforms").unwrap();
200    for (id, transform) in config.transforms() {
201        writeln!(mermaid, "  {id}{{{id}}}").unwrap();
202
203        for input in transform.inputs.iter() {
204            if let Some(port) = &input.port {
205                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
206            } else {
207                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
208            }
209        }
210    }
211
212    writeln!(mermaid, "\n  %% Sinks").unwrap();
213    for (id, sink) in config.sinks() {
214        writeln!(mermaid, "  {id}[\\{id}\\]").unwrap();
215
216        for input in &sink.inputs {
217            if let Some(port) = &input.port {
218                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
219            } else {
220                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
221            }
222        }
223    }
224
225    #[allow(clippy::print_stdout)]
226    {
227        println!("{mermaid}");
228    }
229
230    exitcode::OK
231}