Skip to content

Commit 8881a73

Browse files
committed
feat(internal_metrics source): expose topology connections as metrics
Adds the ability to expose Vector's component topology graph as metrics through the internal_metrics source. Each connection between components is represented as a component_connections gauge metric with labels for source and target components.
1 parent 0613d20 commit 8881a73

File tree

8 files changed

+339
-24
lines changed

8 files changed

+339
-24
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The `internal_metrics` source now exposes Vector's topology graph as Prometheus metrics via the `component_connections` gauge. Each connection between components is represented as a metric with labels indicating source and target component IDs, types, and kinds, enabling topology visualization and monitoring.
2+
3+
authors: elohmeier

src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub mod schema;
4242
mod secret;
4343
mod sink;
4444
mod source;
45+
mod topology_metadata;
4546
mod transform;
4647
pub mod unit_test;
4748
mod validation;
@@ -62,6 +63,7 @@ pub use provider::ProviderConfig;
6263
pub use secret::SecretBackend;
6364
pub use sink::{BoxedSink, SinkConfig, SinkContext, SinkHealthcheckOptions, SinkOuter};
6465
pub use source::{BoxedSource, SourceConfig, SourceContext, SourceOuter};
66+
pub use topology_metadata::{SharedTopologyMetadata, TopologyMetadata};
6567
pub use transform::{
6668
BoxedTransform, TransformConfig, TransformContext, TransformOuter, get_transform_output_ids,
6769
};

src/config/source.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ use vector_lib::{
1616
source::Source,
1717
};
1818

19-
use super::{ComponentKey, ProxyConfig, Resource, dot_graph::GraphConfig, schema};
19+
use super::{
20+
ComponentKey, ProxyConfig, Resource, SharedTopologyMetadata, dot_graph::GraphConfig, schema,
21+
};
2022
use crate::{SourceSender, extra_context::ExtraContext, shutdown::ShutdownSignal};
2123

2224
pub type BoxedSource = Box<dyn SourceConfig>;
@@ -143,6 +145,9 @@ pub struct SourceContext {
143145
/// Extra context data provided by the running app and shared across all components. This can be
144146
/// used to pass shared settings or other data from outside the components.
145147
pub extra_context: ExtraContext,
148+
149+
/// Optional topology metadata for internal sources to expose topology as metrics
150+
pub topology_metadata: Option<SharedTopologyMetadata>,
146151
}
147152

148153
impl SourceContext {
@@ -165,6 +170,7 @@ impl SourceContext {
165170
schema_definitions: HashMap::default(),
166171
schema: Default::default(),
167172
extra_context: Default::default(),
173+
topology_metadata: None,
168174
},
169175
shutdown,
170176
)
@@ -186,6 +192,7 @@ impl SourceContext {
186192
schema_definitions: schema_definitions.unwrap_or_default(),
187193
schema: Default::default(),
188194
extra_context: Default::default(),
195+
topology_metadata: None,
189196
}
190197
}
191198

src/config/topology_metadata.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use std::{
2+
collections::HashMap,
3+
sync::{Arc, RwLock},
4+
};
5+
6+
use super::{ComponentKey, OutputId};
7+
8+
/// Metadata about the topology connections and component types
9+
/// Used by internal_metrics source to expose topology as metrics
10+
#[derive(Clone, Debug, Default)]
11+
pub struct TopologyMetadata {
12+
/// Map of component to its inputs
13+
pub inputs: HashMap<ComponentKey, Vec<OutputId>>,
14+
/// Map of component to its (type, kind) tuple
15+
pub component_types: HashMap<ComponentKey, (String, String)>,
16+
}
17+
18+
impl TopologyMetadata {
19+
/// Create a new TopologyMetadata instance
20+
pub fn new() -> Self {
21+
Self::default()
22+
}
23+
24+
/// Clear all metadata
25+
pub fn clear(&mut self) {
26+
self.inputs.clear();
27+
self.component_types.clear();
28+
}
29+
}
30+
31+
/// Thread-safe reference to topology metadata
32+
pub type SharedTopologyMetadata = Arc<RwLock<TopologyMetadata>>;

src/sources/internal_metrics.rs

Lines changed: 154 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::time::Duration;
22

3+
use chrono::Utc;
34
use futures::StreamExt;
45
use serde_with::serde_as;
56
use tokio::time;
@@ -14,7 +15,8 @@ use vector_lib::{
1415

1516
use crate::{
1617
SourceSender,
17-
config::{SourceConfig, SourceContext, SourceOutput, log_schema},
18+
config::{SharedTopologyMetadata, SourceConfig, SourceContext, SourceOutput, log_schema},
19+
event::{Metric, MetricKind, MetricTags, MetricValue},
1820
internal_events::{EventsReceived, StreamClosedError},
1921
metrics::Controller,
2022
shutdown::ShutdownSignal,
@@ -122,6 +124,7 @@ impl SourceConfig for InternalMetricsConfig {
122124
interval,
123125
out: cx.out,
124126
shutdown: cx.shutdown,
127+
topology_metadata: cx.topology_metadata.clone(),
125128
}
126129
.run(),
127130
))
@@ -144,6 +147,7 @@ struct InternalMetrics<'a> {
144147
interval: time::Duration,
145148
out: SourceSender,
146149
shutdown: ShutdownSignal,
150+
topology_metadata: Option<SharedTopologyMetadata>,
147151
}
148152

149153
impl InternalMetrics<'_> {
@@ -164,25 +168,35 @@ impl InternalMetrics<'_> {
164168
bytes_received.emit(ByteSize(byte_size));
165169
events_received.emit(CountByteSize(count, json_size));
166170

167-
let batch = metrics.into_iter().map(|mut metric| {
168-
// A metric starts out with a default "vector" namespace, but will be overridden
169-
// if an explicit namespace is provided to this source.
170-
if self.namespace != "vector" {
171-
metric = metric.with_namespace(Some(self.namespace.clone()));
172-
}
173-
174-
if let Some(host_key) = &self.host_key.path
175-
&& let Ok(hostname) = &hostname
176-
{
177-
metric.replace_tag(host_key.to_string(), hostname.to_owned());
178-
}
179-
if let Some(pid_key) = &self.pid_key {
180-
metric.replace_tag(pid_key.to_owned(), pid.clone());
181-
}
182-
metric
183-
});
184-
185-
if (self.out.send_batch(batch).await).is_err() {
171+
let mut batch: Vec<Metric> = metrics
172+
.into_iter()
173+
.map(|mut metric| {
174+
// A metric starts out with a default "vector" namespace, but will be overridden
175+
// if an explicit namespace is provided to this source.
176+
if self.namespace != "vector" {
177+
metric = metric.with_namespace(Some(self.namespace.clone()));
178+
}
179+
180+
if let Some(host_key) = &self.host_key.path
181+
&& let Ok(hostname) = &hostname
182+
{
183+
metric.replace_tag(host_key.to_string(), hostname.to_owned());
184+
}
185+
if let Some(pid_key) = &self.pid_key {
186+
metric.replace_tag(pid_key.to_owned(), pid.clone());
187+
}
188+
metric
189+
})
190+
.collect();
191+
192+
// Add topology metrics if available
193+
if let Some(topology_metadata) = &self.topology_metadata {
194+
let topology = topology_metadata.read().unwrap();
195+
let topology_metrics = generate_topology_metrics(&topology, Utc::now());
196+
batch.extend(topology_metrics);
197+
}
198+
199+
if (self.out.send_batch(batch.into_iter()).await).is_err() {
186200
emit!(StreamClosedError { count });
187201
return Err(());
188202
}
@@ -192,6 +206,50 @@ impl InternalMetrics<'_> {
192206
}
193207
}
194208

209+
/// Generate metrics for topology connections
210+
fn generate_topology_metrics(
211+
topology: &crate::config::TopologyMetadata,
212+
timestamp: chrono::DateTime<Utc>,
213+
) -> Vec<Metric> {
214+
let mut metrics = Vec::new();
215+
216+
for (to_component, inputs) in &topology.inputs {
217+
for input in inputs {
218+
let mut tags = MetricTags::default();
219+
220+
// Source component labels
221+
tags.insert("from_component_id".to_string(), input.component.to_string());
222+
if let Some((type_name, kind)) = topology.component_types.get(&input.component) {
223+
tags.insert("from_component_type".to_string(), type_name.clone());
224+
tags.insert("from_component_kind".to_string(), kind.clone());
225+
}
226+
if let Some(port) = &input.port {
227+
tags.insert("from_output".to_string(), port.clone());
228+
}
229+
230+
// Target component labels
231+
tags.insert("to_component_id".to_string(), to_component.to_string());
232+
if let Some((type_name, kind)) = topology.component_types.get(to_component) {
233+
tags.insert("to_component_type".to_string(), type_name.clone());
234+
tags.insert("to_component_kind".to_string(), kind.clone());
235+
}
236+
237+
metrics.push(
238+
Metric::new(
239+
"component_connections",
240+
MetricKind::Absolute,
241+
MetricValue::Gauge { value: 1.0 },
242+
)
243+
.with_namespace(Some("vector".to_string()))
244+
.with_tags(Some(tags))
245+
.with_timestamp(Some(timestamp)),
246+
);
247+
}
248+
}
249+
250+
metrics
251+
}
252+
195253
#[cfg(test)]
196254
mod tests {
197255
use std::collections::BTreeMap;
@@ -201,6 +259,7 @@ mod tests {
201259

202260
use super::*;
203261
use crate::{
262+
config::{ComponentKey, OutputId},
204263
event::{
205264
Event,
206265
metric::{Metric, MetricValue},
@@ -344,4 +403,79 @@ mod tests {
344403

345404
assert_eq!(event.as_metric().namespace(), Some(namespace));
346405
}
406+
407+
#[test]
408+
fn test_topology_metrics_generation() {
409+
let mut topology = crate::config::TopologyMetadata::new();
410+
411+
// Add a source -> transform connection
412+
topology.inputs.insert(
413+
ComponentKey::from("my_transform"),
414+
vec![OutputId {
415+
component: ComponentKey::from("my_source"),
416+
port: None,
417+
}],
418+
);
419+
420+
// Add a transform -> sink connection
421+
topology.inputs.insert(
422+
ComponentKey::from("my_sink"),
423+
vec![OutputId {
424+
component: ComponentKey::from("my_transform"),
425+
port: Some("output1".to_string()),
426+
}],
427+
);
428+
429+
// Add component types
430+
topology.component_types.insert(
431+
ComponentKey::from("my_source"),
432+
("file".to_string(), "source".to_string()),
433+
);
434+
topology.component_types.insert(
435+
ComponentKey::from("my_transform"),
436+
("remap".to_string(), "transform".to_string()),
437+
);
438+
topology.component_types.insert(
439+
ComponentKey::from("my_sink"),
440+
("console".to_string(), "sink".to_string()),
441+
);
442+
443+
let timestamp = Utc::now();
444+
let metrics = generate_topology_metrics(&topology, timestamp);
445+
446+
// Should have 2 connection metrics
447+
assert_eq!(metrics.len(), 2);
448+
449+
// Find the source -> transform connection
450+
let source_to_transform = metrics
451+
.iter()
452+
.find(|m| m.tags().and_then(|t| t.get("from_component_id")) == Some("my_source"))
453+
.expect("Should find source -> transform metric");
454+
455+
assert_eq!(source_to_transform.name(), "component_connections");
456+
assert_eq!(source_to_transform.namespace(), Some("vector"));
457+
match source_to_transform.value() {
458+
MetricValue::Gauge { value } => assert_eq!(*value, 1.0),
459+
_ => panic!("Expected gauge metric"),
460+
}
461+
462+
let tags1 = source_to_transform.tags().expect("Should have tags");
463+
assert_eq!(tags1.get("from_component_id"), Some("my_source"));
464+
assert_eq!(tags1.get("from_component_type"), Some("file"));
465+
assert_eq!(tags1.get("from_component_kind"), Some("source"));
466+
assert_eq!(tags1.get("to_component_id"), Some("my_transform"));
467+
assert_eq!(tags1.get("to_component_type"), Some("remap"));
468+
assert_eq!(tags1.get("to_component_kind"), Some("transform"));
469+
470+
// Find the transform -> sink connection
471+
let transform_to_sink = metrics
472+
.iter()
473+
.find(|m| m.tags().and_then(|t| t.get("from_component_id")) == Some("my_transform"))
474+
.expect("Should find transform -> sink metric");
475+
476+
let tags2 = transform_to_sink.tags().expect("Should have tags");
477+
assert_eq!(tags2.get("from_component_id"), Some("my_transform"));
478+
assert_eq!(tags2.get("from_output"), Some("output1"));
479+
assert_eq!(tags2.get("to_component_id"), Some("my_sink"));
480+
}
347481
}

src/sources/socket/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ mod test {
10551055
schema: Default::default(),
10561056
schema_definitions: HashMap::default(),
10571057
extra_context: Default::default(),
1058+
topology_metadata: None,
10581059
})
10591060
.await
10601061
.unwrap();

0 commit comments

Comments
 (0)