platform_core/
telemetry.rs1use std::collections::HashMap;
38
39use async_trait::async_trait;
40
41use crate::envelope::EventEnvelope;
42use crate::function::{AppError, ComposableFunction};
43use crate::platform::Platform;
44
45pub const DISTRIBUTED_TRACING: &str = "distributed.tracing";
46pub const DISTRIBUTED_TRACE_FORWARDER: &str = "distributed.trace.forwarder";
47pub const TRANSACTION_JOURNAL_RECORDER: &str = "transaction.journal.recorder";
48
49pub const ZERO_TRACING_FILTER: [&str; 4] = [
53 DISTRIBUTED_TRACING,
54 DISTRIBUTED_TRACE_FORWARDER,
55 TRANSACTION_JOURNAL_RECORDER,
56 crate::inbox::TEMPORARY_INBOX,
57];
58
59pub struct Telemetry {
62 platform: Platform,
63}
64
65impl Telemetry {
66 pub fn new(platform: &Platform) -> Self {
67 Telemetry {
68 platform: platform.clone(),
69 }
70 }
71}
72
73#[async_trait]
74impl ComposableFunction for Telemetry {
75 async fn handle_event(
76 &self,
77 _headers: HashMap<String, String>,
78 input: EventEnvelope,
79 _instance: usize,
80 ) -> Result<EventEnvelope, AppError> {
81 let Ok(payload) = input.body_as::<serde_json::Value>() else {
82 return Ok(EventEnvelope::new());
83 };
84 let Some(payload) = payload.as_object() else {
85 return Ok(EventEnvelope::new());
86 };
87 let mut metrics = match payload.get("trace").and_then(|t| t.as_object()) {
88 Some(m) if !m.is_empty() => m.clone(),
89 _ => return Ok(EventEnvelope::new()),
90 };
91 let Some(service) = permitted_route(metrics.get("service")) else {
93 return Ok(EventEnvelope::new());
94 };
95 metrics.insert("service".to_string(), serde_json::Value::String(service));
96 if let Some(from) = metrics.get("from").and_then(|f| f.as_str()) {
97 let trimmed = trim_origin(from).to_string();
98 metrics.insert("from".to_string(), serde_json::Value::String(trimmed));
99 }
100 let annotations = payload
101 .get("annotations")
102 .and_then(|a| a.as_object())
103 .cloned()
104 .unwrap_or_default();
105 let mut dataset = serde_json::Map::new();
106 dataset.insert("trace".to_string(), serde_json::Value::Object(metrics));
107 if !annotations.is_empty() {
108 dataset.insert(
109 "annotations".to_string(),
110 serde_json::Value::Object(annotations),
111 );
112 }
113 let dataset = serde_json::Value::Object(dataset);
114 log::info!("{dataset}");
116 if self.platform.has_route(DISTRIBUTED_TRACE_FORWARDER) {
118 let event = EventEnvelope::new()
119 .set_to(DISTRIBUTED_TRACE_FORWARDER)
120 .set_body(&dataset)?;
121 let _ = self
122 .platform
123 .deliver(DISTRIBUTED_TRACE_FORWARDER, event)
124 .await;
125 }
126 if payload.contains_key("journal") && self.platform.has_route(TRANSACTION_JOURNAL_RECORDER)
127 {
128 let mut forward = dataset;
129 if let (Some(map), Some(journal)) = (forward.as_object_mut(), payload.get("journal")) {
130 map.insert("journal".to_string(), journal.clone());
131 }
132 let event = EventEnvelope::new()
133 .set_to(TRANSACTION_JOURNAL_RECORDER)
134 .set_body(&forward)?;
135 let _ = self
136 .platform
137 .deliver(TRANSACTION_JOURNAL_RECORDER, event)
138 .await;
139 }
140 Ok(EventEnvelope::new())
141 }
142}
143
144fn permitted_route(service: Option<&serde_json::Value>) -> Option<String> {
147 let route = service?.as_str()?;
148 let name = trim_origin(route);
149 if ZERO_TRACING_FILTER.contains(&name) {
150 None
151 } else {
152 Some(name.to_string())
153 }
154}
155
156fn trim_origin(route: &str) -> &str {
157 match route.find('@') {
158 Some(at) => &route[..at],
159 None => route,
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn plumbing_routes_are_filtered() {
169 let v = |s: &str| serde_json::Value::String(s.to_string());
170 assert_eq!(
171 permitted_route(Some(&v("v1.hello"))),
172 Some("v1.hello".into())
173 );
174 assert_eq!(
175 permitted_route(Some(&v("v1.hello@abc123"))),
176 Some("v1.hello".into())
177 );
178 assert_eq!(permitted_route(Some(&v("distributed.tracing"))), None);
179 assert_eq!(
180 permitted_route(Some(&v("distributed.trace.forwarder@x"))),
181 None
182 );
183 assert_eq!(permitted_route(None), None);
184 }
185}