Skip to content

Commit b2ce992

Browse files
committed
feat(output): add basic opentelemetry output
This output allows exporting file activity events as opentelemetry logs via otlp to be collected by compatible systems. The output is gated behind the "otel" feature, which allows regular fact builds to keep using just the gRPC and stodout outputs, no additional dependencies. WIP
1 parent ed338ea commit b2ce992

12 files changed

Lines changed: 1203 additions & 161 deletions

File tree

Cargo.lock

Lines changed: 442 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fact/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ hyper-util = { workspace = true }
2020
libc = { workspace = true }
2121
log = { workspace = true }
2222
native-tls = { workspace = true }
23+
opentelemetry = { version = "0.32.0", optional = true }
24+
opentelemetry-otlp = { version = "0.32.0", optional = true }
25+
opentelemetry_sdk = { version = "0.32.1", features = [ "logs"], optional = true }
2326
openssl = { workspace = true }
2427
tonic = { workspace = true }
2528
tokio = { workspace = true }
@@ -51,3 +54,4 @@ path = "src/main.rs"
5154

5255
[features]
5356
bpf-test = []
57+
otel = ["dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-otlp"]

fact/src/config/mod.rs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fn yaml_to_duration_secs(v: &Yaml) -> Option<Duration> {
3434
pub struct FactConfig {
3535
paths: Option<Vec<PathBuf>>,
3636
pub grpc: GrpcConfig,
37+
pub otel: OTelConfig,
3738
pub endpoint: EndpointConfig,
3839
pub bpf: BpfConfig,
3940
skip_pre_flight: Option<bool>,
@@ -72,7 +73,7 @@ impl FactConfig {
7273
)?;
7374

7475
// Once file configuration is handled, apply CLI arguments
75-
static CLI_ARGS: LazyLock<FactConfig> = LazyLock::new(|| FactCli::parse().to_config());
76+
static CLI_ARGS: LazyLock<FactConfig> = LazyLock::new(|| FactCli::parse().into_config());
7677
config.update(&CLI_ARGS);
7778

7879
Ok(config)
@@ -84,6 +85,7 @@ impl FactConfig {
8485
}
8586

8687
self.grpc.update(&from.grpc);
88+
self.otel.update(&from.otel);
8789
self.endpoint.update(&from.endpoint);
8890
self.bpf.update(&from.bpf);
8991

@@ -196,6 +198,10 @@ impl TryFrom<Vec<Yaml>> for FactConfig {
196198
let grpc = v.as_hash().unwrap();
197199
config.grpc = GrpcConfig::try_from(grpc)?;
198200
}
201+
"otel" if v.is_hash() => {
202+
let otel = v.as_hash().unwrap();
203+
config.otel = OTelConfig::try_from(otel)?;
204+
}
199205
"endpoint" if v.is_hash() => {
200206
let endpoint = v.as_hash().unwrap();
201207
config.endpoint = EndpointConfig::try_from(endpoint)?;
@@ -492,6 +498,48 @@ impl TryFrom<&yaml::Hash> for GrpcConfig {
492498
}
493499
}
494500

501+
#[derive(Debug, Default, PartialEq, Eq, Clone)]
502+
pub struct OTelConfig {
503+
endpoint: Option<String>,
504+
}
505+
506+
impl OTelConfig {
507+
fn update(&mut self, from: &OTelConfig) {
508+
if let Some(endpoint) = from.endpoint.as_deref() {
509+
self.endpoint = Some(endpoint.to_owned());
510+
}
511+
}
512+
513+
pub fn endpoint(&self) -> Option<&str> {
514+
self.endpoint.as_deref()
515+
}
516+
}
517+
518+
impl TryFrom<&yaml::Hash> for OTelConfig {
519+
type Error = anyhow::Error;
520+
521+
fn try_from(value: &yaml::Hash) -> Result<Self, Self::Error> {
522+
let mut otel = OTelConfig::default();
523+
for (k, v) in value.iter() {
524+
let Some(k) = k.as_str() else {
525+
bail!("key is not string: {k:?}");
526+
};
527+
528+
match k {
529+
"endpoint" => {
530+
let Some(endpoint) = v.as_str() else {
531+
bail!("otel.endpoint field has incorrect type: {v:?}")
532+
};
533+
otel.endpoint = Some(endpoint.to_owned());
534+
}
535+
name => bail!("Invalid field 'otel.{name}' with value: {v:?}"),
536+
}
537+
}
538+
539+
Ok(otel)
540+
}
541+
}
542+
495543
#[derive(Debug, Default, PartialEq, Eq, Clone)]
496544
pub struct BpfConfig {
497545
ringbuf_size: Option<u32>,
@@ -627,6 +675,10 @@ pub struct FactCli {
627675
#[arg(long, env = "FACT_GRPC_BACKOFF_RETRIES_MAX")]
628676
backoff_retries_max: Option<u64>,
629677

678+
/// OpenTelemetry endpoint to push logs into
679+
#[arg(long, env = "FACT_OTEL_ENDPOINT")]
680+
otel_endpoint: Option<String>,
681+
630682
/// The port to bind for all exposed endpoints
631683
#[arg(long, short, env = "FACT_ENDPOINT_ADDRESS")]
632684
address: Option<SocketAddr>,
@@ -711,12 +763,12 @@ pub struct FactCli {
711763
}
712764

713765
impl FactCli {
714-
fn to_config(&self) -> FactConfig {
766+
fn into_config(self) -> FactConfig {
715767
FactConfig {
716-
paths: self.paths.clone(),
768+
paths: self.paths,
717769
grpc: GrpcConfig {
718-
url: self.url.clone(),
719-
certs: self.certs.clone(),
770+
url: self.url,
771+
certs: self.certs,
720772
backoff: BackoffConfig {
721773
initial: self.backoff_initial,
722774
max: self.backoff_max,
@@ -725,6 +777,9 @@ impl FactCli {
725777
retries_max: self.backoff_retries_max,
726778
},
727779
},
780+
otel: OTelConfig {
781+
endpoint: self.otel_endpoint,
782+
},
728783
endpoint: EndpointConfig {
729784
address: self.address,
730785
expose_metrics: resolve_bool_arg(self.expose_metrics, self.no_expose_metrics),

fact/src/config/reloader.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ use tokio::{
99
time::interval,
1010
};
1111

12+
use crate::config::OTelConfig;
13+
1214
use super::{CONFIG_FILES, EndpointConfig, FactConfig, GrpcConfig};
1315

1416
pub struct Reloader {
1517
config: FactConfig,
1618
endpoint: watch::Sender<EndpointConfig>,
1719
grpc: watch::Sender<GrpcConfig>,
20+
otel: watch::Sender<OTelConfig>,
1821
paths: watch::Sender<Vec<PathBuf>>,
1922
files: HashMap<&'static str, i64>,
2023
scan_interval: watch::Sender<Duration>,
@@ -71,6 +74,12 @@ impl Reloader {
7174
self.grpc.subscribe()
7275
}
7376

77+
/// Subscribe to get notifications when otel configuration is
78+
/// changed.
79+
pub fn otel(&self) -> watch::Receiver<OTelConfig> {
80+
self.otel.subscribe()
81+
}
82+
7483
/// Subscribe to get notifications when paths configuration is
7584
/// changed.
7685
pub fn paths(&self) -> watch::Receiver<Vec<PathBuf>> {
@@ -174,6 +183,16 @@ impl Reloader {
174183
}
175184
});
176185

186+
self.otel.send_if_modified(|old| {
187+
if *old != new.otel {
188+
debug!("Sending new OTel configuration...");
189+
*old = new.otel.clone();
190+
true
191+
} else {
192+
false
193+
}
194+
});
195+
177196
self.paths.send_if_modified(|old| {
178197
let new = new.paths();
179198
if *old != new {
@@ -238,6 +257,7 @@ impl From<FactConfig> for Reloader {
238257
.collect();
239258
let (endpoint, _) = watch::channel(config.endpoint.clone());
240259
let (grpc, _) = watch::channel(config.grpc.clone());
260+
let (otel, _) = watch::channel(config.otel.clone());
241261
let (paths, _) = watch::channel(config.paths().to_vec());
242262
let (scan_interval, _) = watch::channel(config.scan_interval());
243263
let (rate_limit, _) = watch::channel(config.rate_limit());
@@ -247,6 +267,7 @@ impl From<FactConfig> for Reloader {
247267
config,
248268
endpoint,
249269
grpc,
270+
otel,
250271
paths,
251272
scan_interval,
252273
rate_limit,

0 commit comments

Comments
 (0)