syncable_cli/agent/tools/platform/
get_service_logs.rs

1//! Get service logs tool for the agent
2//!
3//! Allows the agent to fetch container logs for deployed services.
4
5use 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/// Arguments for the get service logs tool
14#[derive(Debug, Deserialize)]
15pub struct GetServiceLogsArgs {
16    /// Service ID (from list_deployments output)
17    pub service_id: String,
18    /// Start time filter (ISO timestamp, optional)
19    pub start: Option<String>,
20    /// End time filter (ISO timestamp, optional)
21    pub end: Option<String>,
22    /// Maximum number of log lines to return (default: 100)
23    pub limit: Option<i32>,
24}
25
26/// Error type for get service logs operations
27#[derive(Debug, thiserror::Error)]
28#[error("Get service logs error: {0}")]
29pub struct GetServiceLogsError(String);
30
31/// Tool to get container logs for a deployed service
32///
33/// Returns recent log entries with timestamps and container metadata.
34/// Supports time filtering and line limits for efficient log retrieval.
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct GetServiceLogsTool;
37
38impl GetServiceLogsTool {
39    /// Create a new GetServiceLogsTool
40    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        // Validate service_id
103        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        // Create the API client
116        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        // Fetch logs
124        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                // Format log entries for readability
147                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
178/// Format a PlatformApiError for LLM consumption
179fn 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}