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 fn reads_response_body(&self) -> bool {
105 false
106 }
107
108 async fn execute(&self, ctx: Context) -> 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::success(ctx))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
130 use bytes::Bytes;
131
132 fn test_ctx(consumer: Option<&str>) -> Context {
133 let mut message = HashMap::new();
134 if let Some(name) = consumer {
135 message.insert("consumer.name".to_string(), serde_json::json!(name));
136 }
137 Context {
138 request: GatewayRequest {
139 method: "GET".to_string(),
140 path: "/api/users".to_string(),
141 host: "api.example.com".to_string(),
142 scheme: "http".to_string(),
143 headers: HashMap::new(),
144 query_params: HashMap::new(),
145 body: Bytes::new(),
146 remote_addr: "10.0.0.1:1234".to_string(),
147 protocol: Protocol::Http1,
148 },
149 response: GatewayResponse {
150 status_code: 200,
151 headers: HashMap::new(),
152 body: Bytes::new(),
153 stream: None,
154 },
155 message,
156 errors: Vec::new(),
157 }
158 }
159
160 fn plugin_with_metrics(metrics: Arc<GatewayMetrics>) -> PrometheusPlugin {
161 let resources = PluginResources::new(Some(metrics));
162 PrometheusPlugin::from_config(&HashMap::new(), &resources).unwrap()
163 }
164
165 #[test]
166 fn from_config_reads_prefer_name() {
167 let cfg: HashMap<String, serde_json::Value> =
168 serde_json::from_value(serde_json::json!({ "prefer_name": true })).unwrap();
169 let p = PrometheusPlugin::from_config(&cfg, &PluginResources::empty()).unwrap();
170 assert!(p.prefer_name);
171 let p = PrometheusPlugin::from_config(&HashMap::new(), &PluginResources::empty()).unwrap();
173 assert!(!p.prefer_name);
174 }
175
176 #[tokio::test]
177 async fn execute_bumps_consumer_counter() {
178 let metrics = Arc::new(GatewayMetrics::new());
179 let p = plugin_with_metrics(metrics.clone());
180
181 let out = p.execute(test_ctx(Some("alice"))).await.unwrap();
182 assert_eq!(out.context.request.path, "/api/users");
184
185 assert_eq!(
186 metrics
187 .consumer_requests
188 .with_label_values(&["alice", "api.example.com"])
189 .get(),
190 1
191 );
192
193 p.execute(test_ctx(Some("alice"))).await.unwrap();
195 assert_eq!(
196 metrics
197 .consumer_requests
198 .with_label_values(&["alice", "api.example.com"])
199 .get(),
200 2
201 );
202 }
203
204 #[tokio::test]
205 async fn execute_defaults_to_anonymous() {
206 let metrics = Arc::new(GatewayMetrics::new());
207 let p = plugin_with_metrics(metrics.clone());
208
209 p.execute(test_ctx(None)).await.unwrap();
210 assert_eq!(
211 metrics
212 .consumer_requests
213 .with_label_values(&["anonymous", "api.example.com"])
214 .get(),
215 1
216 );
217 }
218
219 #[tokio::test]
220 async fn execute_is_noop_without_metrics() {
221 let p = PrometheusPlugin::from_config(&HashMap::new(), &PluginResources::empty()).unwrap();
223 let out = p.execute(test_ctx(Some("bob"))).await.unwrap();
224 assert_eq!(out.context.response.status_code, 200);
225 }
226}