Skip to main content

talos_api_rs/resources/
logs.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed wrappers for the Logs API.
4//!
5//! Provides streaming access to service and container logs.
6
7use crate::api::generated::machine::LogsRequest as ProtoLogsRequest;
8
9/// Container driver type.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum ContainerDriver {
12    /// Default containerd driver.
13    #[default]
14    Containerd,
15    /// CRI driver.
16    Cri,
17}
18
19impl From<ContainerDriver> for i32 {
20    fn from(driver: ContainerDriver) -> Self {
21        match driver {
22            ContainerDriver::Containerd => 0,
23            ContainerDriver::Cri => 1,
24        }
25    }
26}
27
28impl std::fmt::Display for ContainerDriver {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            ContainerDriver::Containerd => write!(f, "containerd"),
32            ContainerDriver::Cri => write!(f, "cri"),
33        }
34    }
35}
36
37/// Request for service/container logs.
38#[derive(Debug, Clone, Default)]
39pub struct LogsRequest {
40    /// Namespace (e.g., "system" for system services).
41    pub namespace: String,
42    /// Service or container ID.
43    pub id: String,
44    /// Container driver.
45    pub driver: ContainerDriver,
46    /// Follow the log output.
47    pub follow: bool,
48    /// Number of lines to tail (0 = all).
49    pub tail_lines: i32,
50}
51
52impl LogsRequest {
53    /// Create a new logs request for a service.
54    #[must_use]
55    pub fn new(id: impl Into<String>) -> Self {
56        Self {
57            namespace: String::new(),
58            id: id.into(),
59            driver: ContainerDriver::Containerd,
60            follow: false,
61            tail_lines: 0,
62        }
63    }
64
65    /// Create a new logs request with namespace.
66    #[must_use]
67    pub fn with_namespace(namespace: impl Into<String>, id: impl Into<String>) -> Self {
68        Self {
69            namespace: namespace.into(),
70            id: id.into(),
71            driver: ContainerDriver::Containerd,
72            follow: false,
73            tail_lines: 0,
74        }
75    }
76
77    /// Create a builder.
78    #[must_use]
79    pub fn builder(id: impl Into<String>) -> LogsRequestBuilder {
80        LogsRequestBuilder::new(id)
81    }
82}
83
84impl From<LogsRequest> for ProtoLogsRequest {
85    fn from(req: LogsRequest) -> Self {
86        Self {
87            namespace: req.namespace,
88            id: req.id,
89            driver: req.driver.into(),
90            follow: req.follow,
91            tail_lines: req.tail_lines,
92        }
93    }
94}
95
96/// Builder for LogsRequest.
97#[derive(Debug, Clone)]
98pub struct LogsRequestBuilder {
99    namespace: String,
100    id: String,
101    driver: ContainerDriver,
102    follow: bool,
103    tail_lines: i32,
104}
105
106impl LogsRequestBuilder {
107    /// Create a new builder.
108    #[must_use]
109    pub fn new(id: impl Into<String>) -> Self {
110        Self {
111            namespace: String::new(),
112            id: id.into(),
113            driver: ContainerDriver::Containerd,
114            follow: false,
115            tail_lines: 0,
116        }
117    }
118
119    /// Set the namespace.
120    #[must_use]
121    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
122        self.namespace = namespace.into();
123        self
124    }
125
126    /// Set the container driver.
127    #[must_use]
128    pub fn driver(mut self, driver: ContainerDriver) -> Self {
129        self.driver = driver;
130        self
131    }
132
133    /// Follow log output.
134    #[must_use]
135    pub fn follow(mut self, follow: bool) -> Self {
136        self.follow = follow;
137        self
138    }
139
140    /// Tail the last N lines.
141    #[must_use]
142    pub fn tail(mut self, lines: i32) -> Self {
143        self.tail_lines = lines;
144        self
145    }
146
147    /// Build the request.
148    #[must_use]
149    pub fn build(self) -> LogsRequest {
150        LogsRequest {
151            namespace: self.namespace,
152            id: self.id,
153            driver: self.driver,
154            follow: self.follow,
155            tail_lines: self.tail_lines,
156        }
157    }
158}
159
160/// Response containing log data.
161#[derive(Debug, Clone)]
162pub struct LogsResponse {
163    /// Raw log data.
164    data: Vec<u8>,
165    /// Node that returned the logs.
166    pub node: Option<String>,
167}
168
169impl LogsResponse {
170    /// Create a new response.
171    #[must_use]
172    pub fn new(data: Vec<u8>, node: Option<String>) -> Self {
173        Self { data, node }
174    }
175
176    /// Get raw bytes.
177    #[must_use]
178    pub fn as_bytes(&self) -> &[u8] {
179        &self.data
180    }
181
182    /// Try to convert to UTF-8 string.
183    pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
184        std::str::from_utf8(&self.data)
185    }
186
187    /// Convert to string, replacing invalid UTF-8.
188    #[must_use]
189    pub fn as_string_lossy(&self) -> std::borrow::Cow<'_, str> {
190        String::from_utf8_lossy(&self.data)
191    }
192
193    /// Get the length in bytes.
194    #[must_use]
195    pub fn len(&self) -> usize {
196        self.data.len()
197    }
198
199    /// Check if empty.
200    #[must_use]
201    pub fn is_empty(&self) -> bool {
202        self.data.is_empty()
203    }
204
205    /// Get individual lines.
206    #[must_use]
207    pub fn lines(&self) -> Vec<&str> {
208        self.as_str()
209            .map(|s| s.lines().collect())
210            .unwrap_or_default()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_logs_request_new() {
220        let req = LogsRequest::new("kubelet");
221        assert_eq!(req.id, "kubelet");
222        assert!(req.namespace.is_empty());
223        assert!(!req.follow);
224    }
225
226    #[test]
227    fn test_logs_request_with_namespace() {
228        let req = LogsRequest::with_namespace("system", "kubelet");
229        assert_eq!(req.namespace, "system");
230        assert_eq!(req.id, "kubelet");
231    }
232
233    #[test]
234    fn test_logs_request_builder() {
235        let req = LogsRequest::builder("etcd")
236            .namespace("system")
237            .driver(ContainerDriver::Cri)
238            .follow(true)
239            .tail(100)
240            .build();
241
242        assert_eq!(req.id, "etcd");
243        assert_eq!(req.namespace, "system");
244        assert_eq!(req.driver, ContainerDriver::Cri);
245        assert!(req.follow);
246        assert_eq!(req.tail_lines, 100);
247    }
248
249    #[test]
250    fn test_container_driver() {
251        assert_eq!(i32::from(ContainerDriver::Containerd), 0);
252        assert_eq!(i32::from(ContainerDriver::Cri), 1);
253        assert_eq!(ContainerDriver::Cri.to_string(), "cri");
254    }
255
256    #[test]
257    fn test_proto_conversion() {
258        let req = LogsRequest::builder("kubelet")
259            .follow(true)
260            .tail(50)
261            .build();
262
263        let proto: ProtoLogsRequest = req.into();
264        assert_eq!(proto.id, "kubelet");
265        assert!(proto.follow);
266        assert_eq!(proto.tail_lines, 50);
267    }
268}