nika_engine/runtime/builtin/
log.rs1use super::BuiltinTool;
23use crate::error::NikaError;
24use serde::{Deserialize, Serialize};
25use std::future::Future;
26use std::pin::Pin;
27use std::str::FromStr;
28use tracing::{debug, error, info, trace, warn};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
32#[serde(rename_all = "lowercase")]
33pub enum LogLevel {
34 Trace,
35 Debug,
36 Info,
37 Warn,
38 Error,
39}
40
41impl LogLevel {
42 pub fn as_str(&self) -> &'static str {
44 match self {
45 Self::Trace => "trace",
46 Self::Debug => "debug",
47 Self::Info => "info",
48 Self::Warn => "warn",
49 Self::Error => "error",
50 }
51 }
52}
53
54impl std::str::FromStr for LogLevel {
55 type Err = ();
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 match s.to_lowercase().as_str() {
60 "trace" => Ok(Self::Trace),
61 "debug" => Ok(Self::Debug),
62 "info" => Ok(Self::Info),
63 "warn" | "warning" => Ok(Self::Warn),
64 "error" => Ok(Self::Error),
65 _ => Err(()),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Deserialize)]
72struct LogParams {
73 level: String,
75 message: String,
77}
78
79#[derive(Debug, Clone, Serialize)]
81struct LogResponse {
82 logged: bool,
84 level: String,
86 message: String,
88}
89
90pub struct LogTool;
94
95impl BuiltinTool for LogTool {
96 fn name(&self) -> &'static str {
97 "log"
98 }
99
100 fn description(&self) -> &'static str {
101 "Emit log event at specified level (trace, debug, info, warn, error)"
102 }
103
104 fn parameters_schema(&self) -> serde_json::Value {
105 serde_json::json!({
107 "type": "object",
108 "properties": {
109 "level": {
110 "type": "string",
111 "description": "Log level: trace, debug, info, warn, error",
112 "enum": ["trace", "debug", "info", "warn", "error"]
113 },
114 "message": {
115 "type": "string",
116 "description": "Log message to emit"
117 }
118 },
119 "required": ["level", "message"],
120 "additionalProperties": false
121 })
122 }
123
124 fn call<'a>(
125 &'a self,
126 args: String,
127 ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
128 Box::pin(async move {
129 let params: LogParams =
131 serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
132 tool: "nika:log".into(),
133 reason: format!("Invalid JSON parameters: {}", e),
134 })?;
135
136 let level =
138 LogLevel::from_str(¶ms.level).map_err(|_| NikaError::BuiltinInvalidParams {
139 tool: "nika:log".into(),
140 reason: format!(
141 "Invalid log level '{}'. Valid levels: trace, debug, info, warn, error",
142 params.level
143 ),
144 })?;
145
146 match level {
148 LogLevel::Trace => trace!(target: "nika:log", "{}", params.message),
149 LogLevel::Debug => debug!(target: "nika:log", "{}", params.message),
150 LogLevel::Info => info!(target: "nika:log", "{}", params.message),
151 LogLevel::Warn => warn!(target: "nika:log", "{}", params.message),
152 LogLevel::Error => error!(target: "nika:log", "{}", params.message),
153 }
154
155 let response = LogResponse {
157 logged: true,
158 level: level.as_str().to_string(),
159 message: params.message,
160 };
161
162 serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
163 tool: "nika:log".into(),
164 reason: format!("Failed to serialize response: {}", e),
165 })
166 })
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_log_tool_name() {
176 let tool = LogTool;
177 assert_eq!(tool.name(), "log");
178 }
179
180 #[test]
181 fn test_log_tool_description() {
182 let tool = LogTool;
183 assert!(tool.description().contains("log"));
184 }
185
186 #[test]
187 fn test_log_tool_schema() {
188 let tool = LogTool;
189 let schema = tool.parameters_schema();
190 assert_eq!(schema["type"], "object");
191 assert!(schema["properties"]["level"].is_object());
192 assert!(schema["properties"]["message"].is_object());
193 assert!(schema["required"]
194 .as_array()
195 .unwrap()
196 .contains(&serde_json::json!("level")));
197 assert!(schema["required"]
198 .as_array()
199 .unwrap()
200 .contains(&serde_json::json!("message")));
201 }
202
203 #[test]
204 fn test_log_level_from_str() {
205 assert_eq!(LogLevel::from_str("trace"), Ok(LogLevel::Trace));
206 assert_eq!(LogLevel::from_str("DEBUG"), Ok(LogLevel::Debug));
207 assert_eq!(LogLevel::from_str("Info"), Ok(LogLevel::Info));
208 assert_eq!(LogLevel::from_str("warn"), Ok(LogLevel::Warn));
209 assert_eq!(LogLevel::from_str("WARNING"), Ok(LogLevel::Warn));
210 assert_eq!(LogLevel::from_str("error"), Ok(LogLevel::Error));
211 assert_eq!(LogLevel::from_str("invalid"), Err(()));
212 }
213
214 #[test]
215 fn test_log_level_as_str() {
216 assert_eq!(LogLevel::Trace.as_str(), "trace");
217 assert_eq!(LogLevel::Debug.as_str(), "debug");
218 assert_eq!(LogLevel::Info.as_str(), "info");
219 assert_eq!(LogLevel::Warn.as_str(), "warn");
220 assert_eq!(LogLevel::Error.as_str(), "error");
221 }
222
223 #[tokio::test]
224 async fn test_log_info() {
225 let tool = LogTool;
226 let result = tool
227 .call(r#"{"level": "info", "message": "Test info message"}"#.to_string())
228 .await;
229
230 assert!(result.is_ok());
231 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
232 assert_eq!(response["logged"], true);
233 assert_eq!(response["level"], "info");
234 assert_eq!(response["message"], "Test info message");
235 }
236
237 #[tokio::test]
238 async fn test_log_error() {
239 let tool = LogTool;
240 let result = tool
241 .call(r#"{"level": "error", "message": "Test error message"}"#.to_string())
242 .await;
243
244 assert!(result.is_ok());
245 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
246 assert_eq!(response["logged"], true);
247 assert_eq!(response["level"], "error");
248 }
249
250 #[tokio::test]
251 async fn test_log_all_levels() {
252 let tool = LogTool;
253
254 for level in ["trace", "debug", "info", "warn", "error"] {
255 let result = tool
256 .call(format!(r#"{{"level": "{}", "message": "Test"}}"#, level))
257 .await;
258 assert!(result.is_ok(), "Failed for level: {}", level);
259 }
260 }
261
262 #[tokio::test]
263 async fn test_log_invalid_level() {
264 let tool = LogTool;
265 let result = tool
266 .call(r#"{"level": "critical", "message": "Test"}"#.to_string())
267 .await;
268
269 assert!(result.is_err());
270 let err = result.unwrap_err();
271 assert!(err.to_string().contains("Invalid log level"));
272 }
273
274 #[tokio::test]
275 async fn test_log_invalid_json() {
276 let tool = LogTool;
277 let result = tool.call("not json".to_string()).await;
278
279 assert!(result.is_err());
280 let err = result.unwrap_err();
281 assert!(err.to_string().contains("Invalid JSON parameters"));
282 }
283
284 #[tokio::test]
285 async fn test_log_missing_level() {
286 let tool = LogTool;
287 let result = tool.call(r#"{"message": "Test"}"#.to_string()).await;
288
289 assert!(result.is_err());
290 let err = result.unwrap_err();
291 assert!(err.to_string().contains("Invalid JSON parameters"));
292 }
293
294 #[tokio::test]
295 async fn test_log_missing_message() {
296 let tool = LogTool;
297 let result = tool.call(r#"{"level": "info"}"#.to_string()).await;
298
299 assert!(result.is_err());
300 let err = result.unwrap_err();
301 assert!(err.to_string().contains("Invalid JSON parameters"));
302 }
303
304 #[tokio::test]
305 async fn test_log_case_insensitive_level() {
306 let tool = LogTool;
307
308 let result = tool
310 .call(r#"{"level": "INFO", "message": "Test"}"#.to_string())
311 .await;
312 assert!(result.is_ok());
313
314 let result = tool
316 .call(r#"{"level": "WaRn", "message": "Test"}"#.to_string())
317 .await;
318 assert!(result.is_ok());
319 }
320
321 #[tokio::test]
322 async fn test_log_warning_alias() {
323 let tool = LogTool;
324 let result = tool
325 .call(r#"{"level": "warning", "message": "Test warning"}"#.to_string())
326 .await;
327
328 assert!(result.is_ok());
329 let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
330 assert_eq!(response["level"], "warn"); }
332}