syncable_cli/agent/tools/platform/
get_service_logs.rs1use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11use crate::platform::api::{PlatformApiClient, PlatformApiError};
12
13#[derive(Debug, Deserialize)]
15pub struct GetServiceLogsArgs {
16 pub service_id: String,
18 pub start: Option<String>,
20 pub end: Option<String>,
22 pub limit: Option<i32>,
24}
25
26#[derive(Debug, thiserror::Error)]
28#[error("Get service logs error: {0}")]
29pub struct GetServiceLogsError(String);
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct GetServiceLogsTool;
37
38impl GetServiceLogsTool {
39 pub fn new() -> Self {
41 Self
42 }
43}
44
45impl Tool for GetServiceLogsTool {
46 const NAME: &'static str = "get_service_logs";
47
48 type Error = GetServiceLogsError;
49 type Args = GetServiceLogsArgs;
50 type Output = String;
51
52 async fn definition(&self, _prompt: String) -> ToolDefinition {
53 ToolDefinition {
54 name: Self::NAME.to_string(),
55 description: r#"Get container logs for a deployed service.
56
57Returns recent log entries from the service's containers with timestamps
58and metadata. Useful for debugging and monitoring deployed services.
59
60**Parameters:**
61- service_id: The deployment/service ID (from list_deployments output)
62- start: Optional ISO timestamp to filter logs from (e.g., "2024-01-01T00:00:00Z")
63- end: Optional ISO timestamp to filter logs until
64- limit: Optional max number of log lines (default: 100)
65
66**Prerequisites:**
67- User must be authenticated via `sync-ctl auth login`
68- Service must be deployed (use list_deployments to find service IDs)
69
70**Use Cases:**
71- Debug application errors by viewing recent logs
72- Monitor service behavior after deployment
73- Investigate issues by filtering logs to a specific time range
74- View startup logs to verify configuration"#
75 .to_string(),
76 parameters: json!({
77 "type": "object",
78 "properties": {
79 "service_id": {
80 "type": "string",
81 "description": "The deployment/service ID (from list_deployments output)"
82 },
83 "start": {
84 "type": "string",
85 "description": "Optional: ISO timestamp to filter logs from (e.g., \"2024-01-01T00:00:00Z\")"
86 },
87 "end": {
88 "type": "string",
89 "description": "Optional: ISO timestamp to filter logs until"
90 },
91 "limit": {
92 "type": "integer",
93 "description": "Optional: max number of log lines to return (default 100)"
94 }
95 },
96 "required": ["service_id"]
97 }),
98 }
99 }
100
101 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
102 if args.service_id.trim().is_empty() {
104 return Ok(format_error_for_llm(
105 "get_service_logs",
106 ErrorCategory::ValidationFailed,
107 "service_id cannot be empty",
108 Some(vec![
109 "Use list_deployments to find valid service IDs",
110 "The service_id is the 'id' field from deployment entries",
111 ]),
112 ));
113 }
114
115 let client = match PlatformApiClient::new() {
117 Ok(c) => c,
118 Err(e) => {
119 return Ok(format_api_error("get_service_logs", e));
120 }
121 };
122
123 let start_ref = args.start.as_deref();
125 let end_ref = args.end.as_deref();
126
127 match client
128 .get_service_logs(&args.service_id, start_ref, end_ref, args.limit)
129 .await
130 {
131 Ok(response) => {
132 if response.data.is_empty() {
133 return Ok(json!({
134 "success": true,
135 "logs": [],
136 "count": 0,
137 "stats": {
138 "entries_returned": 0,
139 "query_time_ms": response.stats.query_time_ms
140 },
141 "message": "No logs found for this service. The service may not have produced any logs yet, or the time filter may be too restrictive."
142 })
143 .to_string());
144 }
145
146 let log_entries: Vec<serde_json::Value> = response
148 .data
149 .iter()
150 .map(|entry| {
151 json!({
152 "timestamp": entry.timestamp,
153 "message": entry.message,
154 "labels": entry.labels
155 })
156 })
157 .collect();
158
159 let result = json!({
160 "success": true,
161 "logs": log_entries,
162 "count": response.data.len(),
163 "stats": {
164 "entries_returned": response.stats.entries_returned,
165 "query_time_ms": response.stats.query_time_ms
166 },
167 "message": format!("Retrieved {} log entries", response.data.len())
168 });
169
170 serde_json::to_string_pretty(&result)
171 .map_err(|e| GetServiceLogsError(format!("Failed to serialize: {}", e)))
172 }
173 Err(e) => Ok(format_api_error("get_service_logs", e)),
174 }
175 }
176}
177
178fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
180 match error {
181 PlatformApiError::Unauthorized => format_error_for_llm(
182 tool_name,
183 ErrorCategory::PermissionDenied,
184 "Not authenticated - please run `sync-ctl auth login` first",
185 Some(vec![
186 "The user needs to authenticate with the Syncable platform",
187 "Run: sync-ctl auth login",
188 ]),
189 ),
190 PlatformApiError::NotFound(msg) => format_error_for_llm(
191 tool_name,
192 ErrorCategory::ResourceUnavailable,
193 &format!("Service not found: {}", msg),
194 Some(vec![
195 "The service_id may be incorrect or the service no longer exists",
196 "Use list_deployments to find valid service IDs",
197 ]),
198 ),
199 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
200 tool_name,
201 ErrorCategory::PermissionDenied,
202 &format!("Permission denied: {}", msg),
203 Some(vec![
204 "The user does not have access to view logs for this service",
205 "Contact the project admin for access",
206 ]),
207 ),
208 PlatformApiError::RateLimited => format_error_for_llm(
209 tool_name,
210 ErrorCategory::ResourceUnavailable,
211 "Rate limit exceeded - please try again later",
212 Some(vec!["Wait a moment before retrying"]),
213 ),
214 PlatformApiError::HttpError(e) => format_error_for_llm(
215 tool_name,
216 ErrorCategory::NetworkError,
217 &format!("Network error: {}", e),
218 Some(vec![
219 "Check network connectivity",
220 "The Syncable API may be temporarily unavailable",
221 ]),
222 ),
223 PlatformApiError::ParseError(msg) => format_error_for_llm(
224 tool_name,
225 ErrorCategory::InternalError,
226 &format!("Failed to parse API response: {}", msg),
227 Some(vec!["This may be a temporary API issue"]),
228 ),
229 PlatformApiError::ApiError { status, message } => format_error_for_llm(
230 tool_name,
231 ErrorCategory::ExternalCommandFailed,
232 &format!("API error ({}): {}", status, message),
233 Some(vec!["Check the error message for details"]),
234 ),
235 PlatformApiError::ServerError { status, message } => format_error_for_llm(
236 tool_name,
237 ErrorCategory::ExternalCommandFailed,
238 &format!("Server error ({}): {}", status, message),
239 Some(vec![
240 "The Syncable API is experiencing issues",
241 "Try again later",
242 ]),
243 ),
244 PlatformApiError::ConnectionFailed => format_error_for_llm(
245 tool_name,
246 ErrorCategory::NetworkError,
247 "Could not connect to Syncable API",
248 Some(vec![
249 "Check your internet connection",
250 "The Syncable API may be temporarily unavailable",
251 ]),
252 ),
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_tool_name() {
262 assert_eq!(GetServiceLogsTool::NAME, "get_service_logs");
263 }
264
265 #[test]
266 fn test_tool_creation() {
267 let tool = GetServiceLogsTool::new();
268 assert!(format!("{:?}", tool).contains("GetServiceLogsTool"));
269 }
270}