Skip to main content

vector/
graph.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::Write as _,
4    path::PathBuf,
5};
6
7use clap::Parser;
8use itertools::Itertools;
9use vector_lib::{config::OutputId, id::ComponentKey};
10
11use crate::config::{
12    self,
13    dot_graph::{EdgeAttributes, GraphConfig},
14};
15
16#[derive(Parser, Debug)]
17#[command(rename_all = "kebab-case")]
18pub struct Opts {
19    /// Read configuration from one or more files. Wildcard paths are supported.
20    /// File format is detected from the file name.
21    /// If zero files are specified the default config path
22    /// `/etc/vector/vector.yaml` will be targeted.
23    #[arg(
24        id = "config",
25        short,
26        long,
27        env = "VECTOR_CONFIG",
28        value_delimiter(',')
29    )]
30    paths: Vec<PathBuf>,
31
32    /// Vector config files in TOML format.
33    #[arg(id = "config-toml", long, value_delimiter(','))]
34    paths_toml: Vec<PathBuf>,
35
36    /// Vector config files in JSON format.
37    #[arg(id = "config-json", long, value_delimiter(','))]
38    paths_json: Vec<PathBuf>,
39
40    /// Vector config files in YAML format.
41    #[arg(id = "config-yaml", long, value_delimiter(','))]
42    paths_yaml: Vec<PathBuf>,
43
44    /// Read configuration from files in one or more directories.
45    /// File format is detected from the file name.
46    ///
47    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
48    #[arg(
49        id = "config-dir",
50        short = 'C',
51        long,
52        env = "VECTOR_CONFIG_DIR",
53        value_delimiter(',')
54    )]
55    pub config_dirs: Vec<PathBuf>,
56
57    /// Set the output format
58    ///
59    /// See https://mermaid.js.org/syntax/flowchart.html#styling-and-classes for
60    /// information on the `mermaid` format.
61    #[arg(id = "format", long, default_value = "dot")]
62    pub format: OutputFormat,
63
64    /// Allow interpolation of environment variables in configuration files. Enabling this may
65    /// expose environment secrets into your Vector configuration.
66    #[arg(
67        long,
68        env = "VECTOR_DANGEROUSLY_ALLOW_ENV_VAR_INTERPOLATION",
69        default_value = "false"
70    )]
71    pub dangerously_allow_env_var_interpolation: bool,
72}
73
74#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
75pub enum OutputFormat {
76    Dot,
77    Mermaid,
78}
79
80impl Opts {
81    fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
82        config::merge_path_lists(vec![
83            (&self.paths, None),
84            (&self.paths_toml, Some(config::Format::Toml)),
85            (&self.paths_json, Some(config::Format::Json)),
86            (&self.paths_yaml, Some(config::Format::Yaml)),
87        ])
88        .map(|(path, hint)| config::ConfigPath::File(path, hint))
89        .chain(
90            self.config_dirs
91                .iter()
92                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
93        )
94        .collect()
95    }
96}
97
98fn node_attributes_to_string(attributes: &HashMap<String, String>, default_shape: &str) -> String {
99    let mut attrs = attributes.clone();
100    if !attrs.contains_key("shape") {
101        attrs.insert("shape".to_string(), default_shape.to_string());
102    }
103    attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
104}
105
106fn edge_attributes_to_string(attributes: &EdgeAttributes, default_label: Option<&str>) -> String {
107    let mut attrs = attributes.0.clone();
108    if let Some(default_label) = default_label
109        && !attrs.contains_key("label")
110    {
111        attrs.insert("label".to_string(), default_label.to_string());
112    }
113    attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
114}
115
116pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
117    let paths = opts.paths_with_formats();
118    let paths = match config::process_paths(&paths) {
119        Some(paths) => paths,
120        None => return exitcode::CONFIG,
121    };
122
123    let config = match config::load_from_paths(&paths) {
124        Ok(config) => config,
125        Err(errs) => {
126            #[allow(clippy::print_stderr)]
127            for err in errs {
128                eprintln!("{err}");
129            }
130            return exitcode::CONFIG;
131        }
132    };
133
134    let format = opts.format;
135    match format {
136        OutputFormat::Dot => render_dot(config),
137        OutputFormat::Mermaid => render_mermaid(config),
138    }
139}
140
141fn render_dot(config: config::Config) -> exitcode::ExitCode {
142    let mut dot = String::from("digraph {\n");
143
144    let mut written_tables = HashSet::<ComponentKey>::new();
145
146    for (id, table) in config
147        .enrichment_tables
148        .iter()
149        .filter_map(|(key, table)| table.as_source(key))
150    {
151        writeln!(
152            dot,
153            "  \"{}\" [{}]",
154            id,
155            node_attributes_to_string(&table.graph.node_attributes, "cylinder")
156        )
157        .expect("write to String never fails");
158        written_tables.insert(id);
159    }
160
161    for (id, table) in config
162        .enrichment_tables
163        .iter()
164        .filter_map(|(key, table)| table.as_sink(key))
165    {
166        if !written_tables.contains(&id) {
167            writeln!(
168                dot,
169                "  \"{}\" [{}]",
170                id,
171                node_attributes_to_string(&table.graph.node_attributes, "cylinder")
172            )
173            .expect("write to String never fails");
174        }
175
176        for input in table.inputs.iter() {
177            render_dot_edge(&mut dot, &id, input, &table.graph);
178        }
179    }
180
181    for (id, source) in config.sources() {
182        writeln!(
183            dot,
184            "  \"{}\" [{}]",
185            id,
186            node_attributes_to_string(&source.graph.node_attributes, "trapezium")
187        )
188        .expect("write to String never fails");
189    }
190
191    for (id, transform) in config.transforms() {
192        writeln!(
193            dot,
194            "  \"{}\" [{}]",
195            id,
196            node_attributes_to_string(&transform.graph.node_attributes, "diamond")
197        )
198        .expect("write to String never fails");
199
200        for input in transform.inputs.iter() {
201            render_dot_edge(&mut dot, id, input, &transform.graph);
202        }
203    }
204
205    for (id, sink) in config.sinks() {
206        writeln!(
207            dot,
208            "  \"{}\" [{}]",
209            id,
210            node_attributes_to_string(&sink.graph.node_attributes, "invtrapezium")
211        )
212        .expect("write to String never fails");
213
214        for input in &sink.inputs {
215            render_dot_edge(&mut dot, id, input, &sink.graph);
216        }
217    }
218
219    dot += "}";
220
221    #[allow(clippy::print_stdout)]
222    {
223        println!("{dot}");
224    }
225
226    exitcode::OK
227}
228
229fn render_dot_edge(into: &mut String, id: &ComponentKey, input: &OutputId, graph: &GraphConfig) {
230    let edge_attributes = graph
231        .edge_attributes
232        .get(&input.to_string())
233        .or_else(|| graph.edge_attributes.get(&input.component.to_string()));
234    if let Some(port) = &input.port {
235        writeln!(
236            into,
237            "  \"{}\" -> \"{id}\" [{}]",
238            input.component,
239            edge_attributes_to_string(
240                edge_attributes.unwrap_or(&EdgeAttributes::default()),
241                Some(port)
242            )
243        )
244        .expect("write to String never fails");
245    } else if let Some(edge_attributes) = edge_attributes {
246        writeln!(
247            into,
248            "  \"{input}\" -> \"{id}\" [{}]",
249            edge_attributes_to_string(edge_attributes, None)
250        )
251        .expect("write to String never fails");
252    } else {
253        writeln!(into, "  \"{input}\" -> \"{id}\"").expect("write to String never fails");
254    }
255}
256
257fn render_mermaid(config: config::Config) -> exitcode::ExitCode {
258    let mut mermaid = String::from("flowchart TD;\n");
259
260    writeln!(mermaid, "\n  %% Enrichment tables").unwrap();
261    let mut written_tables = HashSet::<ComponentKey>::new();
262
263    for (id, _) in config
264        .enrichment_tables
265        .iter()
266        .filter_map(|(key, table)| table.as_source(key))
267    {
268        writeln!(mermaid, "  {id}[({id})]").unwrap();
269        written_tables.insert(id);
270    }
271
272    for (id, table) in config
273        .enrichment_tables
274        .iter()
275        .filter_map(|(key, table)| table.as_sink(key))
276    {
277        if !written_tables.contains(&id) {
278            writeln!(mermaid, "  {id}[({id})]").unwrap();
279        }
280
281        for input in table.inputs.iter() {
282            if let Some(port) = &input.port {
283                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
284            } else {
285                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
286            }
287        }
288    }
289
290    writeln!(mermaid, "\n  %% Sources").unwrap();
291    for (id, _) in config.sources() {
292        writeln!(mermaid, "  {id}[/{id}/]").unwrap();
293    }
294
295    writeln!(mermaid, "\n  %% Transforms").unwrap();
296    for (id, transform) in config.transforms() {
297        writeln!(mermaid, "  {id}{{{id}}}").unwrap();
298
299        for input in transform.inputs.iter() {
300            if let Some(port) = &input.port {
301                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
302            } else {
303                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
304            }
305        }
306    }
307
308    writeln!(mermaid, "\n  %% Sinks").unwrap();
309    for (id, sink) in config.sinks() {
310        writeln!(mermaid, "  {id}[\\{id}\\]").unwrap();
311
312        for input in &sink.inputs {
313            if let Some(port) = &input.port {
314                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
315            } else {
316                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
317            }
318        }
319    }
320
321    #[allow(clippy::print_stdout)]
322    {
323        println!("{mermaid}");
324    }
325
326    exitcode::OK
327}