Skip to main content

archivist_lib/logs/servers/
log_stream.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use crate::identity::ComponentIdentity;
5use crate::logs::shared_buffer::FilterCursor;
6use diagnostics_log_encoding::encode::{Encoder, EncoderOpts, ResizableBuffer};
7use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, MONIKER, Record, URL};
8use fidl_fuchsia_diagnostics_types::Severity;
9use fuchsia_async as fasync;
10use futures::AsyncWriteExt;
11use std::collections::HashMap;
12use std::future::poll_fn;
13use std::io::Cursor;
14use std::pin::pin;
15use std::sync::Arc;
16use std::task::{Poll, ready};
17use zerocopy::{FromBytes, IntoBytes};
18
19#[derive(thiserror::Error, Debug)]
20pub enum StreamError {
21    #[error(transparent)]
22    Io(#[from] std::io::Error),
23}
24
25pub struct LogStreamServer;
26
27impl LogStreamServer {
28    pub async fn stream_logs_with_manifest(
29        mut socket: fasync::Socket,
30        logs: FilterCursor,
31    ) -> Result<(), StreamError> {
32        let mut logs = pin!(logs);
33        let mut sent_tags = HashMap::new();
34        let mut buffer = Vec::new();
35        while let Some((identity, tag)) = poll_fn(|cx| {
36            loop {
37                match ready!(logs.as_mut().poll_next(cx)) {
38                    Some(message) => {
39                        let (identity_ref, header, data) = match message.parse() {
40                            Ok(res) => res,
41                            Err(_) => continue,
42                        };
43                        let identity = Arc::clone(identity_ref);
44                        buffer.clear();
45                        buffer.reserve(data.len());
46                        buffer.extend_from_slice(header.as_bytes());
47                        buffer.extend_from_slice(&data[8..]);
48                        return Poll::Ready(Some((identity, header.tag() as u64)));
49                    }
50                    None => return Poll::Ready(None),
51                }
52            }
53        })
54        .await
55        {
56            let send = match sent_tags.entry(tag) {
57                std::collections::hash_map::Entry::Vacant(e) => {
58                    e.insert(Arc::clone(&identity));
59                    true
60                }
61                std::collections::hash_map::Entry::Occupied(mut e) => {
62                    if !Arc::ptr_eq(e.get(), &identity) && **e.get() != *identity {
63                        e.insert(Arc::clone(&identity));
64                        true
65                    } else {
66                        false
67                    }
68                }
69            };
70
71            if send {
72                Self::send_component_change(&mut socket, tag, &identity).await?;
73            }
74            socket.write_all(&buffer).await?;
75        }
76        Ok(())
77    }
78
79    async fn send_component_change(
80        socket: &mut fasync::Socket,
81        id: u64,
82        identity: &ComponentIdentity,
83    ) -> Result<(), std::io::Error> {
84        let mut encoder =
85            Encoder::new(Cursor::new(ResizableBuffer::from(Vec::new())), EncoderOpts::default());
86        let record = Record {
87            timestamp: zx::BootInstant::from_nanos(0),
88            severity: Severity::Info.into_primitive(),
89            arguments: vec![
90                Argument::other(MONIKER, identity.moniker.to_string()),
91                Argument::other(URL, identity.url.as_str()),
92            ],
93        };
94        encoder.write_record(record).map_err(std::io::Error::other)?;
95
96        let mut buffer = encoder.take().into_inner().into_inner();
97        if buffer.len() >= 8 {
98            let mut header = Header::read_from_bytes(&buffer[0..8]).unwrap();
99            header.set_tag((id as u32) | LOG_CONTROL_BIT);
100            buffer[0..8].copy_from_slice(header.as_bytes());
101        }
102        socket.write_all(&buffer).await?;
103        Ok(())
104    }
105}
106
107#[derive(Default)]
108pub struct ExtendRecordOpts {
109    pub moniker: bool,
110    pub component_url: bool,
111    pub rolled_out: bool,
112}
113
114impl ExtendRecordOpts {
115    fn should_extend(&self) -> bool {
116        let Self { moniker, component_url, rolled_out } = self;
117        *moniker || *component_url || *rolled_out
118    }
119}
120
121/// Returns zero padding for `len`.
122fn padding(len: usize) -> &'static [u8] {
123    &[0; 8][(len + 7) % 8 + 1..]
124}
125
126pub fn extend_fxt_record(
127    identity: &ComponentIdentity,
128    rolled_out: u64,
129    opts: &ExtendRecordOpts,
130    buffer: &mut Vec<u8>,
131) {
132    if !opts.should_extend() {
133        return;
134    }
135
136    let moniker = if opts.moniker { identity.moniker.as_ref() } else { "" };
137    let component_url = if opts.component_url { identity.url.as_ref() } else { "" };
138    let rolled_out_value = if opts.rolled_out { rolled_out } else { 0 };
139
140    let moniker_len = moniker.len() as u32;
141    let component_url_len = component_url.len() as u32;
142
143    buffer.extend_from_slice(&moniker_len.to_le_bytes());
144    buffer.extend_from_slice(&component_url_len.to_le_bytes());
145    buffer.extend_from_slice(&rolled_out_value.to_le_bytes());
146
147    buffer.extend_from_slice(moniker.as_bytes());
148    buffer.extend_from_slice(padding(moniker.len()));
149
150    buffer.extend_from_slice(component_url.as_bytes());
151    buffer.extend_from_slice(padding(component_url.len()));
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::logs::repository::LogsRepository;
158    use crate::logs::testing::make_message;
159    use diagnostics_log_encoding::Value;
160    use diagnostics_log_encoding::parse::parse_record;
161    use fidl_fuchsia_diagnostics::StreamMode;
162    use futures::AsyncReadExt;
163    use moniker::ExtendedMoniker;
164    use test_case::test_case;
165    use zx;
166
167    #[fuchsia::test]
168    async fn log_stream_with_manifest() {
169        let repo = LogsRepository::for_test(fasync::Scope::new());
170        let identity = Arc::new(ComponentIdentity::new(
171            ExtendedMoniker::parse_str("./foo").unwrap(),
172            "fuchsia-pkg://foo",
173        ));
174        let container = repo.get_log_container(Arc::clone(&identity));
175        let container_tag = container.buffer().iob_tag() as u32;
176
177        let scope = fasync::Scope::new();
178        let (client_socket, server_socket) = zx::Socket::create_stream();
179        let mut client_socket = fasync::Socket::from_socket(client_socket);
180        let logs = repo.logs_cursor_raw(StreamMode::SnapshotThenSubscribe, Vec::new());
181        scope.spawn(async move {
182            let _ = LogStreamServer::stream_logs_with_manifest(
183                fasync::Socket::from_socket(server_socket),
184                logs,
185            )
186            .await;
187        });
188
189        // Wait for connection to be established/handled?
190        // We can just ingest. The cursor should pick it up.
191        container.ingest_message(make_message("a", None, zx::BootInstant::from_nanos(1)));
192
193        let mut buf = vec![0u8; 4096];
194        let mut offset = 0;
195
196        // 1. Read Manifest
197        let (manifest_record, manifest_len) = loop {
198            if offset > 0
199                && let Ok((record, rest)) = parse_record(&buf[..offset])
200            {
201                let len = offset - rest.len();
202                break (record, len);
203            }
204            let n = client_socket.read(&mut buf[offset..]).await.expect("read");
205            assert!(n > 0, "socket closed before receiving manifest");
206            offset += n;
207        };
208
209        // Check manifest arguments
210        assert_eq!(manifest_record.arguments[0].name(), "moniker");
211        assert_eq!(manifest_record.arguments[0].value(), Value::Text("foo".into()));
212        assert_eq!(manifest_record.arguments[1].name(), "url");
213        assert_eq!(manifest_record.arguments[1].value(), Value::Text("fuchsia-pkg://foo".into()));
214
215        // 2. Read Log Record
216        let (log_record, _log_len) = loop {
217            if offset > manifest_len
218                && let Ok((record, rest)) = parse_record(&buf[manifest_len..offset])
219            {
220                let len = offset - manifest_len - rest.len();
221                break (record, len);
222            }
223            let n = client_socket.read(&mut buf[offset..]).await.expect("read");
224            assert!(n > 0, "socket closed before receiving log record");
225            offset += n;
226        };
227
228        // Verify header tag of first record (Manifest)
229        let header1 = Header::read_from_bytes(&buf[0..8]).unwrap();
230        let tag_id = header1.tag();
231        assert_ne!(tag_id & LOG_CONTROL_BIT, 0, "Manifest should have LOG_CONTROL_BIT set");
232
233        // Verify header tag of second record (Log)
234        let header2 = Header::read_from_bytes(&buf[manifest_len..manifest_len + 8]).unwrap();
235        assert_eq!(
236            header2.tag() & LOG_CONTROL_BIT,
237            0,
238            "Log record should NOT have LOG_CONTROL_BIT set"
239        );
240        assert_eq!(
241            header2.tag(),
242            tag_id & !LOG_CONTROL_BIT,
243            "Log record tag should match Manifest tag ID"
244        );
245        assert_eq!(header2.tag(), container_tag, "Log record tag ID should equal IOB tag ID");
246
247        assert_eq!(log_record.arguments[2].value(), Value::Text("a".into()));
248    }
249
250    #[fuchsia::test]
251    async fn log_stream_with_manifest_reused_tag() {
252        use crate::logs::shared_buffer::create_ring_buffer;
253        // Use a small buffer to facilitate rolling out logs.
254        let repo = LogsRepository::new(
255            create_ring_buffer(65536),
256            std::iter::empty(),
257            &Default::default(),
258            fasync::Scope::new(),
259        );
260
261        let scope = fasync::Scope::new();
262        let (client_socket, server_socket) = zx::Socket::create_stream();
263        let mut client_socket = fasync::Socket::from_socket(client_socket);
264        let logs = repo.logs_cursor_raw(StreamMode::SnapshotThenSubscribe, Vec::new());
265        scope.spawn(async move {
266            let _ = LogStreamServer::stream_logs_with_manifest(
267                fasync::Socket::from_socket(server_socket),
268                logs,
269            )
270            .await;
271        });
272
273        // 1. Setup Identity A
274        let identity_a = Arc::new(ComponentIdentity::new(
275            ExtendedMoniker::parse_str("./foo").unwrap(),
276            "fuchsia-pkg://foo",
277        ));
278        let container_a = repo.get_log_container(Arc::clone(&identity_a));
279        let tag_a = container_a.buffer().iob_tag();
280
281        // 2. Ingest A
282        container_a.ingest_message(make_message("msg_a", None, zx::BootInstant::from_nanos(1)));
283
284        let mut buf = vec![0u8; 65536];
285        let mut offset = 0;
286
287        // Helper to read one record from the socket
288        async fn read_one_record(
289            socket: &mut fasync::Socket,
290            buf: &mut [u8],
291            offset: &mut usize,
292        ) -> (diagnostics_log_encoding::Record<'static>, usize) {
293            loop {
294                if *offset > 0
295                    && let Ok((record, rest)) = parse_record(&buf[..*offset])
296                {
297                    let len = *offset - rest.len();
298                    let owned_record = record.into_owned();
299                    // Shift buffer
300                    buf.copy_within(len..*offset, 0);
301                    *offset -= len;
302                    // Return owned record to avoid lifetime issues
303                    return (owned_record, len);
304                }
305                let n = socket.read(&mut buf[*offset..]).await.expect("read");
306                assert!(n > 0, "socket closed unexpectedly");
307                *offset += n;
308            }
309        }
310
311        // 3. Read Manifest A and Log A
312        let (manifest_a, _) = read_one_record(&mut client_socket, &mut buf, &mut offset).await;
313        assert_eq!(manifest_a.arguments[0].value(), Value::Text("foo".into()));
314
315        let (log_a, _) = read_one_record(&mut client_socket, &mut buf, &mut offset).await;
316        assert_eq!(log_a.arguments[2].value(), Value::Text("msg_a".into()));
317
318        // 4. Mark A inactive and release
319        container_a.mark_stopped();
320        drop(container_a);
321
322        // 5. Force rollout A by ingesting filler logs
323        let identity_filler = Arc::new(ComponentIdentity::new(
324            ExtendedMoniker::parse_str("./filler").unwrap(),
325            "fuchsia-pkg://filler",
326        ));
327        let container_filler = repo.get_log_container(Arc::clone(&identity_filler));
328
329        let identity_b = Arc::new(ComponentIdentity::new(
330            ExtendedMoniker::parse_str("./bar").unwrap(),
331            "fuchsia-pkg://bar",
332        ));
333
334        let mut container_b;
335        loop {
336            // Ingest filler
337            container_filler.ingest_message(make_message(
338                "fill",
339                None,
340                zx::BootInstant::from_nanos(1),
341            ));
342
343            // Drain socket to ensure flow
344
345            // Read available data
346            loop {
347                let mut temp_buf = [0u8; 1024];
348                // Use poll_read to not block
349                let read_fut = client_socket.read(&mut temp_buf);
350                match futures::poll!(read_fut) {
351                    std::task::Poll::Ready(Ok(n)) if n > 0 => {
352                        // We just discard filler data for now, but we need to watch for B?
353                        // No, B hasn't been created/ingested yet.
354                        // We are just draining filler.
355                    }
356                    _ => break, // No more data or pending
357                }
358            }
359
360            // Try to allocate B
361            container_b = repo.get_log_container(Arc::clone(&identity_b));
362            if container_b.buffer().iob_tag() == tag_a {
363                break;
364            }
365
366            // Failed to reuse, clean up B
367            container_b.mark_stopped();
368            drop(container_b);
369
370            // Yield to let cleanup tasks run
371            fasync::Timer::new(std::time::Duration::from_millis(10)).await;
372        }
373
374        // 6. Ingest B
375        container_b.ingest_message(make_message("msg_b", None, zx::BootInstant::from_nanos(2)));
376
377        // 7. Read Manifest B + Log B
378        // Note: Our buffer `buf` might contain leftover filler data or partial records.
379        // But we discarded filler data in the loop above (into temp_buf).
380        // `buf` and `offset` state from `read_one_record` was preserved.
381        // Wait, `read_one_record` modifies `buf` and `offset` (shifts data).
382        // So `buf` should be clean or contain partial data.
383
384        // We need to read until we find Manifest B.
385        loop {
386            let (record, _) = read_one_record(&mut client_socket, &mut buf, &mut offset).await;
387
388            // Is it Manifest B?
389            if !record.arguments.is_empty()
390                && record.arguments[0].name() == "moniker"
391                && record.arguments[0].value() == Value::Text("bar".into())
392            {
393                // Found Manifest B!
394                break;
395            }
396            // Otherwise it's filler log or Manifest Filler
397        }
398
399        // Next should be Log B
400        let (log_b, _) = read_one_record(&mut client_socket, &mut buf, &mut offset).await;
401        assert_eq!(log_b.arguments[2].value(), Value::Text("msg_b".into()));
402    }
403
404    #[test_case(ExtendRecordOpts::default(), "", "", 0 ; "no_additional_metadata")]
405    #[test_case(
406        ExtendRecordOpts { moniker: true, ..Default::default() },
407        "UNKNOWN",
408        "",
409        0
410        ; "with_moniker")]
411    #[test_case(
412        ExtendRecordOpts { component_url: true, ..Default::default() },
413        "",
414        "fuchsia-pkg://UNKNOWN",
415        0
416        ; "with_url")]
417    #[test_case(
418        ExtendRecordOpts { rolled_out: true, ..Default::default() },
419        "",
420        "",
421        42
422        ; "with_rolled_out")]
423    #[test_case(
424        ExtendRecordOpts { moniker: true, component_url: true, rolled_out: true },
425        "UNKNOWN",
426        "fuchsia-pkg://UNKNOWN",
427        42
428        ; "with_all")]
429    #[fuchsia::test]
430    fn extend_record_with_metadata(
431        opts: ExtendRecordOpts,
432        expected_moniker: &str,
433        expected_url: &str,
434        expected_rolled_out: u64,
435    ) {
436        let mut buffer = Vec::new();
437        extend_fxt_record(&ComponentIdentity::unknown(), 42, &opts, &mut buffer);
438
439        if !opts.should_extend() {
440            assert!(buffer.is_empty());
441            return;
442        }
443
444        let moniker_len = u32::from_le_bytes(buffer[0..4].try_into().unwrap()) as usize;
445        let component_url_len = u32::from_le_bytes(buffer[4..8].try_into().unwrap()) as usize;
446
447        let rolled_out = u64::from_le_bytes(buffer[8..16].try_into().unwrap());
448        if opts.rolled_out {
449            assert_eq!(rolled_out, expected_rolled_out);
450        } else {
451            assert_eq!(rolled_out, 0);
452        }
453
454        let mut offset = 16;
455        let moniker = std::str::from_utf8(&buffer[offset..offset + moniker_len]).unwrap();
456        assert_eq!(moniker, expected_moniker);
457        let moniker_padded_len = (moniker_len + 7) & !7;
458        offset += moniker_padded_len;
459
460        let url = std::str::from_utf8(&buffer[offset..offset + component_url_len]).unwrap();
461        assert_eq!(url, expected_url);
462        let component_url_padded_len = (component_url_len + 7) & !7;
463        offset += component_url_padded_len;
464
465        assert_eq!(offset, buffer.len());
466    }
467}