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(&self, ctx: Context) -> PluginResult {
66 let mut fields = serde_json::json!({
67 "method": ctx.request.method,
68 "path": ctx.request.path,
69 "host": ctx.request.host,
70 "remote_addr": ctx.request.remote_addr,
71 "status": ctx.response.status_code,
72 "response_body_bytes": ctx.response.body.len(),
73 });
74
75 if self.include_headers {
76 fields["request_headers"] =
77 serde_json::to_value(&ctx.request.headers).unwrap_or_default();
78 fields["response_headers"] =
79 serde_json::to_value(&ctx.response.headers).unwrap_or_default();
80 }
81
82 if !ctx.errors.is_empty() {
83 fields["errors"] = serde_json::to_value(&ctx.errors).unwrap_or_default();
84 }
85
86 info!(target: "access_log", "{}", fields);
87
88 Ok(PluginOutput::success(ctx))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
100 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
101 use bytes::Bytes;
102
103 fn ctx() -> Context {
104 Context {
105 request: GatewayRequest {
106 method: "GET".to_string(),
107 path: "/hello".to_string(),
108 host: "h".to_string(),
109 scheme: "http".to_string(),
110 headers: HashMap::new(),
111 query_params: HashMap::new(),
112 body: Bytes::from_static(b"req"),
113 remote_addr: "1.2.3.4:5".to_string(),
114 protocol: Protocol::Http1,
115 },
116 response: GatewayResponse {
117 status_code: 200,
118 headers: HashMap::new(),
119 body: Bytes::from_static(b"resp"),
120 stream: None,
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()).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
147 #[test]
152 fn test_reads_response_body_defaults_true() {
153 let plugin = LoggingPlugin::from_config(&HashMap::new()).unwrap();
154 assert!(plugin.reads_response_body());
155 }
156}