vdev/
features.rs

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