vdev/
features.rs

1use std::{
2    collections::BTreeSet, collections::HashMap, ffi::OsStr, fs, path::Path, sync::LazyLock,
3};
4
5use anyhow::{bail, Context, Result};
6use serde::Deserialize;
7use serde_json::Value;
8
9type ComponentMap = HashMap<String, Component>;
10
11// Use a BTree to keep the results in sorted order
12type FeatureSet = BTreeSet<String>;
13
14macro_rules! mapping {
15    ( $( $key:ident => $value:ident, )* ) => {
16        HashMap::from([
17            $( (stringify!($key), stringify!($value)), )*
18        ])
19    };
20}
21
22// Mapping of component names to feature name exceptions.
23
24static SOURCE_FEATURE_MAP: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
25    mapping!(
26        prometheus_pushgateway => prometheus,
27        prometheus_scrape => prometheus,
28        prometheus_remote_write => prometheus,
29    )
30});
31
32static TRANSFORM_FEATURE_MAP: LazyLock<HashMap<&'static str, &'static str>> =
33    LazyLock::new(|| mapping!());
34
35static SINK_FEATURE_MAP: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
36    mapping!(
37        gcp_pubsub => gcp,
38        gcp_cloud_storage => gcp,
39        gcp_stackdriver_logs => gcp,
40        gcp_stackdriver_metrics => gcp,
41        prometheus_exporter => prometheus,
42        prometheus_remote_write => prometheus,
43        splunk_hec_logs => splunk_hec,
44    )
45});
46
47/// This is a ersatz copy of the Vector config, containing just the elements we are interested in
48/// examining. Everything else is thrown away.
49#[derive(Deserialize)]
50pub struct VectorConfig {
51    api: Option<Value>,
52
53    #[serde(default)]
54    sources: ComponentMap,
55    #[serde(default)]
56    transforms: ComponentMap,
57    #[serde(default)]
58    sinks: ComponentMap,
59}
60
61#[derive(Deserialize)]
62struct Component {
63    r#type: String,
64}
65
66pub fn load_and_extract(filename: &Path) -> Result<Vec<String>> {
67    let config = fs::read_to_string(filename)
68        .with_context(|| format!("failed to read {}", filename.display()))?;
69
70    let config: VectorConfig = match filename
71        .extension()
72        .and_then(OsStr::to_str)
73        .map(str::to_lowercase)
74        .as_deref()
75    {
76        None => bail!("Invalid filename {filename:?}, no extension"),
77        Some("json") => serde_json::from_str(&config)?,
78        Some("toml") => toml::from_str(&config)?,
79        Some("yaml" | "yml") => serde_yaml::from_str(&config)?,
80        Some(_) => bail!("Invalid filename {filename:?}, unknown extension"),
81    };
82
83    Ok(from_config(config))
84}
85
86pub fn from_config(config: VectorConfig) -> Vec<String> {
87    let mut features = FeatureSet::default();
88    add_option(&mut features, "api", config.api.as_ref());
89
90    get_features(
91        &mut features,
92        "sources",
93        config.sources,
94        &SOURCE_FEATURE_MAP,
95    );
96    get_features(
97        &mut features,
98        "transforms",
99        config.transforms,
100        &TRANSFORM_FEATURE_MAP,
101    );
102    get_features(&mut features, "sinks", config.sinks, &SINK_FEATURE_MAP);
103
104    // Set of always-compiled components, in terms of their computed feature flag, that should
105    // not be emitted as they don't actually have a feature flag because we always compile them.
106    features.remove("transforms-log_to_metric");
107
108    features.into_iter().collect()
109}
110
111fn add_option<T>(features: &mut FeatureSet, name: &str, field: Option<&T>) {
112    if field.is_some() {
113        features.insert(name.into());
114    }
115}
116
117// Extract the set of features for a particular key from the config, using the exception mapping to
118// rewrite component names to their feature names where needed.
119fn get_features(
120    features: &mut FeatureSet,
121    key: &str,
122    section: ComponentMap,
123    exceptions: &HashMap<&str, &str>,
124) {
125    features.extend(
126        section
127            .into_values()
128            .map(|component| component.r#type)
129            .map(|name| {
130                exceptions
131                    .get(name.as_str())
132                    .map_or(name, ToString::to_string)
133            })
134            .map(|name| format!("{key}-{name}")),
135    );
136}