featherbit/plugins/native/
prometheus.rs1use async_trait::async_trait;
29use std::collections::HashMap;
30use std::sync::Arc;
31
32use crate::context::Context;
33use crate::metrics::GatewayMetrics;
34use crate::plugins::resources::PluginResources;
35use crate::plugins::{Plugin, PluginOutput, PluginResult};
36
37pub struct PrometheusPlugin {
40 prefer_name: bool,
42 metrics: Option<Arc<GatewayMetrics>>,
44}
45
46impl PrometheusPlugin {
47 pub fn from_config(
60 config: &HashMap<String, serde_json::Value>,
61 resources: &Arc<PluginResources>,
62 ) -> Result<Self, String> {
63 let prefer_name = config
64 .get("prefer_name")
65 .and_then(|v| v.as_bool())
66 .unwrap_or(false);
67
68 Ok(Self {
69 prefer_name,
70 metrics: resources.metrics.clone(),
71 })
72 }
73
74 fn consumer_label(ctx: &Context) -> String {
77 ctx.message
78 .get("consumer.name")
79 .and_then(|v| v.as_str())
80 .filter(|s| !s.is_empty())
81 .unwrap_or("anonymous")
82 .to_string()
83 }
84
85 fn route_label(ctx: &Context) -> String {
89 let host = ctx.request.host.trim();
90 if host.is_empty() {
91 "unknown".to_string()
92 } else {
93 host.to_string()
94 }
95 }
96}
97
98#[async_trait]
99impl Plugin for PrometheusPlugin {
100 fn plugin_type(&self) -> &str {
101 "prometheus"
102 }
103
104 async fn execute(
105 &self,
106 ctx: Context,
107 _named_inputs: &HashMap<String, serde_json::Value>,
108 ) -> PluginResult {
109 if let Some(ref metrics) = self.metrics {
110 let consumer = Self::consumer_label(&ctx);
111 let route = Self::route_label(&ctx);
112 metrics
113 .consumer_requests
114 .with_label_values(&[&consumer, &route])
115 .inc();
116 }
117
118 let _ = self.prefer_name;
121
122 Ok(PluginOutput {
123 context: ctx,
124 named_outputs: HashMap::new(),
125 })
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
133 use bytes::Bytes;
134
135 fn test_ctx(consumer: Option<&str>) -> Context {
136 let mut message = HashMap::new();
137 if let Some(name) = consumer {
138 message.insert("consumer.name".to_string(), serde_json::json!(name));
139 }
140 Context {
141 request: GatewayRequest {
142 method: "GET".to_string(),
143 path: "/api/users".to_string(),
144 host: "api.example.com".to_string(),
145 scheme: "http".to_string(),
146 headers: HashMap::new(),
147 query_params: HashMap::new(),
148 body: Bytes::new(),
149 remote_addr: "10.0.0.1:1234".to_string(),
150 protocol: Protocol::Http1,
151 },
152 response: GatewayResponse {
153 status_code: 200,
154 headers: HashMap::new(),
155 body: Bytes::new(),
156 },
157 message,
158 errors: Vec::new(),
159 }
160 }
161
162 fn plugin_with_metrics(metrics: Arc<GatewayMetrics>) -> PrometheusPlugin {
163 let resources = PluginResources::new(Some(metrics));
164 PrometheusPlugin::from_config(&HashMap::new(), &resources).unwrap()
165 }
166
167 #[test]
168 fn from_config_reads_prefer_name() {
169 let cfg: HashMap<String, serde_json::Value> =
170 serde_json::from_value(serde_json::json!({ "prefer_name": true })).unwrap();
171 let p = PrometheusPlugin::from_config(&cfg, &PluginResources::empty()).unwrap();
172 assert!(p.prefer_name);
173 let p = PrometheusPlugin::from_config(&HashMap::new(), &PluginResources::empty()).unwrap();
175 assert!(!p.prefer_name);
176 }
177
178 #[tokio::test]
179 async fn execute_bumps_consumer_counter() {
180 let metrics = Arc::new(GatewayMetrics::new());
181 let p = plugin_with_metrics(metrics.clone());
182
183 let out = p
184 .execute(test_ctx(Some("alice")), &HashMap::new())
185 .await
186 .unwrap();
187 assert_eq!(out.context.request.path, "/api/users");
189
190 assert_eq!(
191 metrics
192 .consumer_requests
193 .with_label_values(&["alice", "api.example.com"])
194 .get(),
195 1
196 );
197
198 p.execute(test_ctx(Some("alice")), &HashMap::new())
200 .await
201 .unwrap();
202 assert_eq!(
203 metrics
204 .consumer_requests
205 .with_label_values(&["alice", "api.example.com"])
206 .get(),
207 2
208 );
209 }
210
211 #[tokio::test]
212 async fn execute_defaults_to_anonymous() {
213 let metrics = Arc::new(GatewayMetrics::new());
214 let p = plugin_with_metrics(metrics.clone());
215
216 p.execute(test_ctx(None), &HashMap::new()).await.unwrap();
217 assert_eq!(
218 metrics
219 .consumer_requests
220 .with_label_values(&["anonymous", "api.example.com"])
221 .get(),
222 1
223 );
224 }
225
226 #[tokio::test]
227 async fn execute_is_noop_without_metrics() {
228 let p = PrometheusPlugin::from_config(&HashMap::new(), &PluginResources::empty()).unwrap();
230 let out = p
231 .execute(test_ctx(Some("bob")), &HashMap::new())
232 .await
233 .unwrap();
234 assert_eq!(out.context.response.status_code, 200);
235 }
236}