Skip to content
3 changes: 3 additions & 0 deletions changelog.d/24593_graph_edge_attributes.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`graph.edge_attributes` can now be added to transforms and sinks to add attributes to edges in graphs generated using `vector graph`. Memory enrichment tables are also considered for graphs, because they can have inputs and outputs.

authors: esensar Quad9DNS
41 changes: 39 additions & 2 deletions src/config/dot_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,52 @@ pub struct GraphConfig {
/// 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()"
docs::examples = "example_node_options()"
))]
#[serde(default)]
pub node_attributes: HashMap<String, String>,

/// Edge attributes to add to the edges linked to this component's node in resulting graph
///
/// They are added to the edge as provided
#[configurable(metadata(
docs::additional_props_description = "A collection of graph edge attributes in graphviz DOT language, related to a single input component.",
docs::examples = "example_edges_options()"
))]
#[serde(default)]
pub edge_attributes: HashMap<String, EdgeAttributes>,
}

fn example_graph_options() -> HashMap<String, String> {
#[configurable_component]
#[configurable(metadata(docs::advanced))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
/// A collection of graph edge attributes in graphviz DOT language, related to a single input
/// component.
pub struct EdgeAttributes(
#[configurable(metadata(
docs::additional_props_description = "A single graph edge attribute in graphviz DOT language.",
docs::examples = "example_edge_options()"
))]
pub HashMap<String, String>,
);

fn example_node_options() -> HashMap<String, String> {
HashMap::<_, _>::from_iter([
("name".to_string(), "Example Node".to_string()),
("color".to_string(), "red".to_string()),
("width".to_string(), "5.0".to_string()),
])
}

fn example_edges_options() -> HashMap<String, HashMap<String, String>> {
HashMap::<_, _>::from_iter([("example_input".to_string(), example_edge_options())])
}

fn example_edge_options() -> HashMap<String, String> {
HashMap::<_, _>::from_iter([
("label".to_string(), "Example Edge".to_string()),
("color".to_string(), "red".to_string()),
("width".to_string(), "5.0".to_string()),
])
}
139 changes: 117 additions & 22 deletions src/graph.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
use std::{collections::HashMap, fmt::Write as _, path::PathBuf};
use std::{
collections::{HashMap, HashSet},
fmt::Write as _,
path::PathBuf,
};

use clap::Parser;
use itertools::Itertools;
use vector_lib::{config::OutputId, id::ComponentKey};

use crate::config;
use crate::config::{
self,
dot_graph::{EdgeAttributes, GraphConfig},
};

#[derive(Parser, Debug)]
#[command(rename_all = "kebab-case")]
Expand Down Expand Up @@ -94,6 +102,16 @@ fn node_attributes_to_string(attributes: &HashMap<String, String>, default_shape
attrs.iter().map(|(k, v)| format!("{k}=\"{v}\"")).join(" ")
}

fn edge_attributes_to_string(attributes: &EdgeAttributes, default_label: Option<&str>) -> String {
let mut attrs = attributes.0.clone();
if let Some(default_label) = default_label
&& !attrs.contains_key("label")
{
attrs.insert("label".to_string(), default_label.to_string());
}
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) {
Expand Down Expand Up @@ -122,6 +140,43 @@ pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
fn render_dot(config: config::Config) -> exitcode::ExitCode {
let mut dot = String::from("digraph {\n");

let mut written_tables = HashSet::<ComponentKey>::new();

for (id, table) in config
.enrichment_tables
.iter()
.filter_map(|(key, table)| table.as_source(key))
{
writeln!(
dot,
" \"{}\" [{}]",
id,
node_attributes_to_string(&table.graph.node_attributes, "cylinder")
)
.expect("write to String never fails");
written_tables.insert(id);
}

for (id, table) in config
.enrichment_tables
.iter()
.filter_map(|(key, table)| table.as_sink(key))
{
if !written_tables.contains(&id) {
writeln!(
dot,
" \"{}\" [{}]",
id,
node_attributes_to_string(&table.graph.node_attributes, "cylinder")
)
.expect("write to String never fails");
}

for input in table.inputs.iter() {
render_dot_edge(&mut dot, &id, input, &table.graph);
}
}

for (id, source) in config.sources() {
writeln!(
dot,
Expand All @@ -142,16 +197,7 @@ fn render_dot(config: config::Config) -> exitcode::ExitCode {
.expect("write to String never fails");

for input in transform.inputs.iter() {
if let Some(port) = &input.port {
writeln!(
dot,
" \"{}\" -> \"{}\" [label=\"{}\"]",
input.component, id, port
)
.expect("write to String never fails");
} else {
writeln!(dot, " \"{input}\" -> \"{id}\"").expect("write to String never fails");
}
render_dot_edge(&mut dot, id, input, &transform.graph);
}
}

Expand All @@ -165,16 +211,7 @@ fn render_dot(config: config::Config) -> exitcode::ExitCode {
.expect("write to String never fails");

for input in &sink.inputs {
if let Some(port) = &input.port {
writeln!(
dot,
" \"{}\" -> \"{}\" [label=\"{}\"]",
input.component, id, port
)
.expect("write to String never fails");
} else {
writeln!(dot, " \"{input}\" -> \"{id}\"").expect("write to String never fails");
}
render_dot_edge(&mut dot, id, input, &sink.graph);
}
}

Expand All @@ -188,9 +225,67 @@ fn render_dot(config: config::Config) -> exitcode::ExitCode {
exitcode::OK
}

fn render_dot_edge(into: &mut String, id: &ComponentKey, input: &OutputId, graph: &GraphConfig) {
let edge_attributes = graph
.edge_attributes
.get(&input.to_string())
.or_else(|| graph.edge_attributes.get(&input.component.to_string()));
if let Some(port) = &input.port {
writeln!(
into,
" \"{}\" -> \"{id}\" [{}]",
input.component,
edge_attributes_to_string(
edge_attributes.unwrap_or(&EdgeAttributes::default()),
Some(port)
)
)
.expect("write to String never fails");
} else if let Some(edge_attributes) = edge_attributes {
writeln!(
into,
" \"{input}\" -> \"{id}\" [{}]",
edge_attributes_to_string(edge_attributes, None)
)
.expect("write to String never fails");
} else {
writeln!(into, " \"{input}\" -> \"{id}\"").expect("write to String never fails");
}
}

fn render_mermaid(config: config::Config) -> exitcode::ExitCode {
let mut mermaid = String::from("flowchart TD;\n");

writeln!(mermaid, "\n %% Enrichment tables").unwrap();
let mut written_tables = HashSet::<ComponentKey>::new();

for (id, _) in config
.enrichment_tables
.iter()
.filter_map(|(key, table)| table.as_source(key))
{
writeln!(mermaid, " {id}[({id})]").unwrap();
written_tables.insert(id);
}

for (id, table) in config
.enrichment_tables
.iter()
.filter_map(|(key, table)| table.as_sink(key))
{
if !written_tables.contains(&id) {
writeln!(mermaid, " {id}[({id})]").unwrap();
}

for input in table.inputs.iter() {
if let Some(port) = &input.port {
writeln!(mermaid, " {0} -->|{port}| {id}", input.component).unwrap();
} else {
writeln!(mermaid, " {0} --> {id}", input.component).unwrap();
}
}
}

writeln!(mermaid, "\n %% Sources").unwrap();
for (id, _) in config.sources() {
writeln!(mermaid, " {id}[/{id}/]").unwrap();
Expand Down
69 changes: 52 additions & 17 deletions website/cue/reference/components/generated/sinks.cue
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,58 @@ generated: components: sinks: 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: {}
type: object: options: {
edge_attributes: {
description: """
Edge attributes to add to the edges linked to this component's node in resulting graph

They are added to the edge as provided
"""
required: false
type: object: {
examples: [{
example_input: {
color: "red"
label: "Example Edge"
width: "5.0"
}
}]
options: "*": {
description: "A collection of graph edge attributes in graphviz DOT language, related to a single input component."
required: true
type: object: {
examples: [{
color: "red"
label: "Example Edge"
width: "5.0"
}]
options: "*": {
description: "A single graph edge attribute in graphviz DOT language."
required: true
type: string: {}
}
}
}
}
}
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: {}
}
}
}
}
Expand Down
67 changes: 51 additions & 16 deletions website/cue/reference/components/generated/sources.cue
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,58 @@ generated: components: sources: 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
type: object: options: {
edge_attributes: {
description: """
Edge attributes to add to the edges linked to this component's node in resulting graph

They are added to the edge as provided
"""
required: false
type: object: {
examples: [{
example_input: {
color: "red"
label: "Example Edge"
width: "5.0"
}
}]
options: "*": {
description: "A collection of graph edge attributes in graphviz DOT language, related to a single input component."
required: true
type: object: {
examples: [{
color: "red"
label: "Example Edge"
width: "5.0"
}]
options: "*": {
description: "A single graph edge attribute in graphviz DOT language."
required: true
type: string: {}
}
}
}
}
}
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: {}
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: {}
}
}
}
}
Expand Down
Loading
Loading