diff --git a/changelog.d/21194_dot_graph_attributes.feature.md b/changelog.d/21194_dot_graph_attributes.feature.md new file mode 100644 index 0000000000000..872dfae13064a --- /dev/null +++ b/changelog.d/21194_dot_graph_attributes.feature.md @@ -0,0 +1,3 @@ +Adds support for additional graph configuration on each component so that users can add arbitrary graphviz node attributes when generating a graph via `vector graph`. + +authors: esensar diff --git a/lib/vector-core/src/config/proxy.rs b/lib/vector-core/src/config/proxy.rs index 0d5d66f20888e..3c31d2bb8791f 100644 --- a/lib/vector-core/src/config/proxy.rs +++ b/lib/vector-core/src/config/proxy.rs @@ -44,7 +44,7 @@ impl NoProxyInterceptor { /// Configure to proxy traffic through an HTTP(S) proxy when making external requests. /// /// Similar to common proxy configuration convention, you can set different proxies -/// to use based on the type of traffic being proxied, as well as set specific hosts that +/// to use based on the type of traffic being proxied. You can also set specific hosts that /// should not be proxied. #[configurable_component] #[configurable(metadata(docs::advanced))] diff --git a/src/config/dot_graph.rs b/src/config/dot_graph.rs new file mode 100644 index 0000000000000..0d3d7ca1e4900 --- /dev/null +++ b/src/config/dot_graph.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use vector_lib::configurable::configurable_component; + +/// Extra graph configuration +/// +/// Configure output for component when generated with graph command +#[configurable_component] +#[configurable(metadata(docs::advanced))] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct GraphConfig { + /// Node attributes to add to this component's node in resulting graph + /// + /// They are added to the node as provided + #[configurable(metadata( + docs::additional_props_description = "A single graph node attribute in graphviz DOT language.", + docs::examples = "example_graph_options()" + ))] + pub node_attributes: HashMap, +} + +fn example_graph_options() -> HashMap { + HashMap::<_, _>::from_iter([ + ("name".to_string(), "Example Node".to_string()), + ("color".to_string(), "red".to_string()), + ("width".to_string(), "5.0".to_string()), + ]) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index e6ea2bb3329db..89780fa42d2fb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -26,6 +26,7 @@ mod builder; mod cmd; mod compiler; mod diff; +mod dot_graph; mod enrichment_table; pub mod format; mod graph; diff --git a/src/config/sink.rs b/src/config/sink.rs index a52fddac41599..a322863f11926 100644 --- a/src/config/sink.rs +++ b/src/config/sink.rs @@ -15,7 +15,7 @@ use vector_lib::{ sink::VectorSink, }; -use super::{schema, ComponentKey, ProxyConfig, Resource}; +use super::{dot_graph::GraphConfig, schema, ComponentKey, ProxyConfig, Resource}; use crate::extra_context::ExtraContext; use crate::sinks::{util::UriSerde, Healthcheck}; @@ -53,6 +53,10 @@ pub struct SinkOuter where T: Configurable + Serialize + 'static, { + #[configurable(derived)] + #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")] + pub graph: GraphConfig, + #[configurable(derived)] pub inputs: Inputs, @@ -96,6 +100,7 @@ where healthcheck_uri: None, inner: inner.into(), proxy: Default::default(), + graph: Default::default(), } } @@ -152,6 +157,7 @@ where healthcheck: self.healthcheck, healthcheck_uri: self.healthcheck_uri, proxy: self.proxy, + graph: self.graph, } } } diff --git a/src/config/source.rs b/src/config/source.rs index 20b2f227e7198..3121ab8c80f29 100644 --- a/src/config/source.rs +++ b/src/config/source.rs @@ -16,7 +16,7 @@ use vector_lib::{ source::Source, }; -use super::{schema, ComponentKey, ProxyConfig, Resource}; +use super::{dot_graph::GraphConfig, schema, ComponentKey, ProxyConfig, Resource}; use crate::{extra_context::ExtraContext, shutdown::ShutdownSignal, SourceSender}; pub type BoxedSource = Box; @@ -54,6 +54,10 @@ pub struct SourceOuter { #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")] pub proxy: ProxyConfig, + #[configurable(derived)] + #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")] + pub graph: GraphConfig, + #[serde(default, skip)] pub sink_acknowledgements: bool, @@ -66,6 +70,7 @@ impl SourceOuter { pub(crate) fn new>(inner: I) -> Self { Self { proxy: Default::default(), + graph: Default::default(), sink_acknowledgements: false, inner: inner.into(), } diff --git a/src/config/transform.rs b/src/config/transform.rs index c72f4af1faa16..9261b63e2ace9 100644 --- a/src/config/transform.rs +++ b/src/config/transform.rs @@ -17,6 +17,7 @@ use vector_lib::{ transform::Transform, }; +use super::dot_graph::GraphConfig; use super::schema::Options as SchemaOptions; use super::ComponentKey; use super::OutputId; @@ -56,6 +57,10 @@ pub struct TransformOuter where T: Configurable + Serialize + 'static, { + #[configurable(derived)] + #[serde(default, skip_serializing_if = "vector_lib::serde::is_default")] + pub graph: GraphConfig, + #[configurable(derived)] pub inputs: Inputs, @@ -75,7 +80,11 @@ where { let inputs = Inputs::from_iter(inputs); let inner = inner.into(); - TransformOuter { inputs, inner } + TransformOuter { + inputs, + inner, + graph: Default::default(), + } } pub(super) fn map_inputs(self, f: impl Fn(&T) -> U) -> TransformOuter @@ -94,6 +103,7 @@ where TransformOuter { inputs: Inputs::from_iter(inputs), inner: self.inner, + graph: self.graph, } } } diff --git a/src/graph.rs b/src/graph.rs index cf5f009b3a8f2..ad54db1878763 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; use std::fmt::Write as _; use std::path::PathBuf; use clap::Parser; +use itertools::Itertools; use crate::config; @@ -65,6 +67,17 @@ impl Opts { } } +fn node_attributes_to_string(attributes: &HashMap, default_shape: &str) -> String { + let mut attrs = attributes.clone(); + if !attrs.contains_key("shape") { + attrs.insert("shape".to_string(), default_shape.to_string()); + } + return attrs + .iter() + .map(|(k, v)| format!("{}=\"{}\"", k, v)) + .join(" "); +} + pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode { let paths = opts.paths_with_formats(); let paths = match config::process_paths(&paths) { @@ -85,12 +98,24 @@ pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode { let mut dot = String::from("digraph {\n"); - for (id, _source) in config.sources() { - writeln!(dot, " \"{}\" [shape=trapezium]", id).expect("write to String never fails"); + for (id, source) in config.sources() { + writeln!( + dot, + " \"{}\" [{}]", + id, + node_attributes_to_string(&source.graph.node_attributes, "trapezium") + ) + .expect("write to String never fails"); } for (id, transform) in config.transforms() { - writeln!(dot, " \"{}\" [shape=diamond]", id).expect("write to String never fails"); + writeln!( + dot, + " \"{}\" [{}]", + id, + node_attributes_to_string(&transform.graph.node_attributes, "diamond") + ) + .expect("write to String never fails"); for input in transform.inputs.iter() { if let Some(port) = &input.port { @@ -108,7 +133,13 @@ pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode { } for (id, sink) in config.sinks() { - writeln!(dot, " \"{}\" [shape=invtrapezium]", id).expect("write to String never fails"); + writeln!( + dot, + " \"{}\" [{}]", + id, + node_attributes_to_string(&sink.graph.node_attributes, "invtrapezium") + ) + .expect("write to String never fails"); for input in &sink.inputs { if let Some(port) = &input.port { diff --git a/website/cue/reference/components/base/sinks.cue b/website/cue/reference/components/base/sinks.cue index 4c71d4549e778..1dbc0ff55c2fa 100644 --- a/website/cue/reference/components/base/sinks.cue +++ b/website/cue/reference/components/base/sinks.cue @@ -76,6 +76,34 @@ base: components: sinks: configuration: { } } } + graph: { + description: """ + Extra graph configuration + + Configure output for component when generated with graph command + """ + required: false + type: object: options: node_attributes: { + description: """ + Node attributes to add to this component's node in resulting graph + + They are added to the node as provided + """ + required: false + type: object: { + examples: [{ + color: "red" + name: "Example Node" + width: "5.0" + }] + options: "*": { + description: "A single graph node attribute in graphviz DOT language." + required: true + type: string: {} + } + } + } + } healthcheck: { description: "Healthcheck configuration." required: false @@ -119,7 +147,7 @@ base: components: sinks: configuration: { Configure to proxy traffic through an HTTP(S) proxy when making external requests. Similar to common proxy configuration convention, you can set different proxies - to use based on the type of traffic being proxied, as well as set specific hosts that + to use based on the type of traffic being proxied. You can also set specific hosts that should not be proxied. """ required: false diff --git a/website/cue/reference/components/base/sources.cue b/website/cue/reference/components/base/sources.cue index f7f48b24c9eb7..dd7c7431c9189 100644 --- a/website/cue/reference/components/base/sources.cue +++ b/website/cue/reference/components/base/sources.cue @@ -1,60 +1,90 @@ package metadata -base: components: sources: configuration: proxy: { - description: """ - Proxy configuration. - - Configure to proxy traffic through an HTTP(S) proxy when making external requests. - - Similar to common proxy configuration convention, you can set different proxies - to use based on the type of traffic being proxied, as well as set specific hosts that - should not be proxied. - """ - required: false - type: object: options: { - enabled: { - description: "Enables proxying support." - required: false - type: bool: default: true - } - http: { - description: """ - Proxy endpoint to use when proxying HTTP traffic. +base: components: sources: configuration: { + graph: { + description: """ + Extra graph configuration - Must be a valid URI string. - """ - required: false - type: string: examples: ["http://foo.bar:3128"] - } - https: { + Configure output for component when generated with graph command + """ + required: false + type: object: options: node_attributes: { description: """ - Proxy endpoint to use when proxying HTTPS traffic. + Node attributes to add to this component's node in resulting graph - Must be a valid URI string. + They are added to the node as provided """ required: false - type: string: examples: ["http://foo.bar:3128"] + type: object: { + examples: [{ + color: "red" + name: "Example Node" + width: "5.0" + }] + options: "*": { + description: "A single graph node attribute in graphviz DOT language." + required: true + type: string: {} + } + } } - no_proxy: { - description: """ - A list of hosts to avoid proxying. + } + proxy: { + description: """ + Proxy configuration. - Multiple patterns are allowed: + Configure to proxy traffic through an HTTP(S) proxy when making external requests. - | Pattern | Example match | - | ------------------- | --------------------------------------------------------------------------- | - | Domain names | `example.com` matches requests to `example.com` | - | Wildcard domains | `.example.com` matches requests to `example.com` and its subdomains | - | IP addresses | `127.0.0.1` matches requests to `127.0.0.1` | - | [CIDR][cidr] blocks | `192.168.0.0/16` matches requests to any IP addresses in this range | - | Splat | `*` matches all hosts | + Similar to common proxy configuration convention, you can set different proxies + to use based on the type of traffic being proxied. You can also set specific hosts that + should not be proxied. + """ + required: false + type: object: options: { + enabled: { + description: "Enables proxying support." + required: false + type: bool: default: true + } + http: { + description: """ + Proxy endpoint to use when proxying HTTP traffic. - [cidr]: https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing - """ - required: false - type: array: { - default: [] - items: type: string: examples: ["localhost", ".foo.bar", "*"] + Must be a valid URI string. + """ + required: false + type: string: examples: ["http://foo.bar:3128"] + } + https: { + description: """ + Proxy endpoint to use when proxying HTTPS traffic. + + Must be a valid URI string. + """ + required: false + type: string: examples: ["http://foo.bar:3128"] + } + no_proxy: { + description: """ + A list of hosts to avoid proxying. + + Multiple patterns are allowed: + + | Pattern | Example match | + | ------------------- | --------------------------------------------------------------------------- | + | Domain names | `example.com` matches requests to `example.com` | + | Wildcard domains | `.example.com` matches requests to `example.com` and its subdomains | + | IP addresses | `127.0.0.1` matches requests to `127.0.0.1` | + | [CIDR][cidr] blocks | `192.168.0.0/16` matches requests to any IP addresses in this range | + | Splat | `*` matches all hosts | + + [cidr]: https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing + """ + required: false + type: array: { + default: [] + items: type: string: examples: ["localhost", ".foo.bar", "*"] + } } } } diff --git a/website/cue/reference/components/base/transforms.cue b/website/cue/reference/components/base/transforms.cue index 07225e2b78671..d53acc4ae4f35 100644 --- a/website/cue/reference/components/base/transforms.cue +++ b/website/cue/reference/components/base/transforms.cue @@ -1,17 +1,47 @@ package metadata -base: components: transforms: configuration: inputs: { - description: """ - A list of upstream [source][sources] or [transform][transforms] IDs. +base: components: transforms: configuration: { + graph: { + description: """ + Extra graph configuration - Wildcards (`*`) are supported. + Configure output for component when generated with graph command + """ + required: false + type: object: options: node_attributes: { + description: """ + Node attributes to add to this component's node in resulting graph - See [configuration][configuration] for more info. + They are added to the node as provided + """ + required: false + type: object: { + examples: [{ + color: "red" + name: "Example Node" + width: "5.0" + }] + options: "*": { + description: "A single graph node attribute in graphviz DOT language." + required: true + type: string: {} + } + } + } + } + inputs: { + description: """ + A list of upstream [source][sources] or [transform][transforms] IDs. - [sources]: https://vector.dev/docs/reference/configuration/sources/ - [transforms]: https://vector.dev/docs/reference/configuration/transforms/ - [configuration]: https://vector.dev/docs/reference/configuration/ - """ - required: true - type: array: items: type: string: examples: ["my-source-or-transform-id", "prefix-*"] + Wildcards (`*`) are supported. + + See [configuration][configuration] for more info. + + [sources]: https://vector.dev/docs/reference/configuration/sources/ + [transforms]: https://vector.dev/docs/reference/configuration/transforms/ + [configuration]: https://vector.dev/docs/reference/configuration/ + """ + required: true + type: array: items: type: string: examples: ["my-source-or-transform-id", "prefix-*"] + } } diff --git a/website/cue/reference/components/transforms/base/aws_ec2_metadata.cue b/website/cue/reference/components/transforms/base/aws_ec2_metadata.cue index de6895f194a1d..252ed59f8b67d 100644 --- a/website/cue/reference/components/transforms/base/aws_ec2_metadata.cue +++ b/website/cue/reference/components/transforms/base/aws_ec2_metadata.cue @@ -26,7 +26,7 @@ base: components: transforms: aws_ec2_metadata: configuration: { Configure to proxy traffic through an HTTP(S) proxy when making external requests. Similar to common proxy configuration convention, you can set different proxies - to use based on the type of traffic being proxied, as well as set specific hosts that + to use based on the type of traffic being proxied. You can also set specific hosts that should not be proxied. """ required: false