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
57#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
58pub enum OutputFormat {
59    Dot,
60    Mermaid,
61}
62
63impl Opts {
64    fn paths_with_formats(&self) -> Vec<config::ConfigPath> {
65        config::merge_path_lists(vec![
66            (&self.paths, None),
67            (&self.paths_toml, Some(config::Format::Toml)),
68            (&self.paths_json, Some(config::Format::Json)),
69            (&self.paths_yaml, Some(config::Format::Yaml)),
70        ])
71        .map(|(path, hint)| config::ConfigPath::File(path, hint))
72        .chain(
73            self.config_dirs
74                .iter()
75                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
76        )
77        .collect()
78    }
79}
80
81fn node_attributes_to_string(attributes: &HashMap<String, String>, default_shape: &str) -> String {
82    let mut attrs = attributes.clone();
83    if !attrs.contains_key("shape") {
84        attrs.insert("shape".to_string(), default_shape.to_string());
85    }
86    attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
87}
88
89pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
90    let paths = opts.paths_with_formats();
91    let paths = match config::process_paths(&paths) {
92        Some(paths) => paths,
93        None => return exitcode::CONFIG,
94    };
95
96    let config = match config::load_from_paths(&paths) {
97        Ok(config) => config,
98        Err(errs) => {
99            #[allow(clippy::print_stderr)]
100            for err in errs {
101                eprintln!("{err}");
102            }
103            return exitcode::CONFIG;
104        }
105    };
106
107    let format = opts.format;
108    match format {
109        OutputFormat::Dot => render_dot(config),
110        OutputFormat::Mermaid => render_mermaid(config),
111    }
112}
113
114fn render_dot(config: config::Config) -> exitcode::ExitCode {
115    let mut dot = String::from("digraph {\n");
116
117    for (id, source) in config.sources() {
118        writeln!(
119            dot,
120            "  \"{}\" [{}]",
121            id,
122            node_attributes_to_string(&source.graph.node_attributes, "trapezium")
123        )
124        .expect("write to String never fails");
125    }
126
127    for (id, transform) in config.transforms() {
128        writeln!(
129            dot,
130            "  \"{}\" [{}]",
131            id,
132            node_attributes_to_string(&transform.graph.node_attributes, "diamond")
133        )
134        .expect("write to String never fails");
135
136        for input in transform.inputs.iter() {
137            if let Some(port) = &input.port {
138                writeln!(
139                    dot,
140                    "  \"{}\" -> \"{}\" [label=\"{}\"]",
141                    input.component, id, port
142                )
143                .expect("write to String never fails");
144            } else {
145                writeln!(dot, "  \"{input}\" -> \"{id}\"").expect("write to String never fails");
146            }
147        }
148    }
149
150    for (id, sink) in config.sinks() {
151        writeln!(
152            dot,
153            "  \"{}\" [{}]",
154            id,
155            node_attributes_to_string(&sink.graph.node_attributes, "invtrapezium")
156        )
157        .expect("write to String never fails");
158
159        for input in &sink.inputs {
160            if let Some(port) = &input.port {
161                writeln!(
162                    dot,
163                    "  \"{}\" -> \"{}\" [label=\"{}\"]",
164                    input.component, id, port
165                )
166                .expect("write to String never fails");
167            } else {
168                writeln!(dot, "  \"{input}\" -> \"{id}\"").expect("write to String never fails");
169            }
170        }
171    }
172
173    dot += "}";
174
175    #[allow(clippy::print_stdout)]
176    {
177        println!("{dot}");
178    }
179
180    exitcode::OK
181}
182
183fn render_mermaid(config: config::Config) -> exitcode::ExitCode {
184    let mut mermaid = String::from("flowchart TD;\n");
185
186    writeln!(mermaid, "\n  %% Sources").unwrap();
187    for (id, _) in config.sources() {
188        writeln!(mermaid, "  {id}[/{id}/]").unwrap();
189    }
190
191    writeln!(mermaid, "\n  %% Transforms").unwrap();
192    for (id, transform) in config.transforms() {
193        writeln!(mermaid, "  {id}{{{id}}}").unwrap();
194
195        for input in transform.inputs.iter() {
196            if let Some(port) = &input.port {
197                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
198            } else {
199                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
200            }
201        }
202    }
203
204    writeln!(mermaid, "\n  %% Sinks").unwrap();
205    for (id, sink) in config.sinks() {
206        writeln!(mermaid, "  {id}[\\{id}\\]").unwrap();
207
208        for input in &sink.inputs {
209            if let Some(port) = &input.port {
210                writeln!(mermaid, "  {0} -->|{port}| {id}", input.component).unwrap();
211            } else {
212                writeln!(mermaid, "  {0} --> {id}", input.component).unwrap();
213            }
214        }
215    }
216
217    #[allow(clippy::print_stdout)]
218    {
219        println!("{mermaid}");
220    }
221
222    exitcode::OK
223}