featherbit/plugins/native/
logging.rs1use async_trait::async_trait;
5use std::collections::HashMap;
6use tracing::info;
7
8use crate::context::Context;
9use crate::plugins::{Plugin, PluginOutput, PluginResult};
10
11pub struct LoggingPlugin {
15 include_headers: bool,
16 #[allow(dead_code)] include_body: bool,
18}
19
20impl LoggingPlugin {
21 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
36 let include_headers = config
37 .get("include_headers")
38 .and_then(|v| v.as_bool())
39 .unwrap_or(false);
40
41 let include_body = config
42 .get("include_body")
43 .and_then(|v| v.as_bool())
44 .unwrap_or(false);
45
46 Ok(Self {
47 include_headers,
48 include_body,
49 })
50 }
51}
52
53#[async_trait]
54impl Plugin for LoggingPlugin {
55 fn plugin_type(&self) -> &str {
56 "logging"
57 }
58
59 async fn execute(
60 &self,
61 ctx: Context,
62 _named_inputs: &HashMap<String, serde_json::Value>,
63 ) -> PluginResult {
64 let mut fields = serde_json::json!({
65 "method": ctx.request.method,
66 "path": ctx.request.path,
67 "host": ctx.request.host,
68 "remote_addr": ctx.request.remote_addr,
69 "status": ctx.response.status_code,
70 "response_body_bytes": ctx.response.body.len(),
71 });
72
73 if self.include_headers {
74 fields["request_headers"] =
75 serde_json::to_value(&ctx.request.headers).unwrap_or_default();
76 fields["response_headers"] =
77 serde_json::to_value(&ctx.response.headers).unwrap_or_default();
78 }
79
80 if !ctx.errors.is_empty() {
81 fields["errors"] = serde_json::to_value(&ctx.errors).unwrap_or_default();
82 }
83
84 info!(target: "access_log", "{}", fields);
85
86 Ok(PluginOutput {
87 context: ctx,
88 named_outputs: HashMap::new(),
89 })
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
101 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
102 use bytes::Bytes;
103
104 fn ctx() -> Context {
105 Context {
106 request: GatewayRequest {
107 method: "GET".to_string(),
108 path: "/hello".to_string(),
109 host: "h".to_string(),
110 scheme: "http".to_string(),
111 headers: HashMap::new(),
112 query_params: HashMap::new(),
113 body: Bytes::from_static(b"req"),
114 remote_addr: "1.2.3.4:5".to_string(),
115 protocol: Protocol::Http1,
116 },
117 response: GatewayResponse {
118 status_code: 200,
119 headers: HashMap::new(),
120 body: Bytes::from_static(b"resp"),
121 },
122 message: HashMap::new(),
123 errors: Vec::new(),
124 }
125 }
126
127 #[tokio::test]
128 async fn test_passes_context_through_unchanged() {
129 let mut map = HashMap::new();
130 map.insert("include_headers".to_string(), serde_json::json!(true));
131 let plugin = LoggingPlugin::from_config(&map).unwrap();
132
133 let out = plugin.execute(ctx(), &HashMap::new()).await.unwrap();
134 assert_eq!(out.context.response.status_code, 200);
136 assert_eq!(out.context.response.body, Bytes::from_static(b"resp"));
137 assert_eq!(out.context.request.path, "/hello");
138 }
139
140 #[test]
141 fn test_config_defaults_off() {
142 let plugin = LoggingPlugin::from_config(&HashMap::new()).unwrap();
143 assert!(!plugin.include_headers);
144 assert!(!plugin.include_body);
145 }
146}