Skip to main content

qubit_progress/reporter/
text_reporter.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Line-oriented human-readable event reporter.
9
10use std::fmt::Write as _;
11use std::io::Write;
12use std::sync::Mutex;
13use std::sync::PoisonError;
14
15use crate::Event;
16use crate::Reporter;
17use crate::ReporterError;
18
19/// Writes one complete human-readable record for each reported event.
20pub struct TextReporter<W> {
21    /// Writer serialized across concurrent reporter calls.
22    writer: Mutex<W>,
23}
24
25impl<W> TextReporter<W> {
26    /// Creates a text reporter that owns `writer`.
27    #[must_use]
28    pub const fn new(writer: W) -> Self {
29        Self {
30            writer: Mutex::new(writer),
31        }
32    }
33
34    /// Consumes the reporter and returns its writer.
35    ///
36    /// Returns the contained writer in the error when a reporting thread
37    /// panicked while holding the mutex.
38    pub fn into_inner(self) -> Result<W, PoisonError<W>> {
39        self.writer.into_inner()
40    }
41}
42
43impl<W> Reporter for TextReporter<W>
44where
45    W: Write + Send,
46{
47    /// Formats and writes one complete event line under the writer lock.
48    fn report(&self, event: &Event) -> Result<(), ReporterError> {
49        let mut line = format_event(event);
50        line.push('\n');
51        let mut writer = self.writer.lock().map_err(|_| {
52            ReporterError::message("text reporter mutex is poisoned")
53        })?;
54        writer
55            .write_all(line.as_bytes())
56            .map_err(ReporterError::new)
57    }
58}
59
60/// Produces one complete human-readable event record with escaped metadata.
61fn format_event(event: &Event) -> String {
62    let mut line = format!(
63        "operation={} sequence={} phase={} elapsed={:?}",
64        event.operation_id(),
65        event.sequence(),
66        event.phase().as_str(),
67        event.elapsed(),
68    );
69    if let Some(stage) = event.stage() {
70        let _ = write!(
71            line,
72            " stage={}({}) position={:?} total={:?}",
73            stage.id().escape_default(),
74            stage.name().escape_default(),
75            stage.position_value(),
76            stage.total(),
77        );
78    }
79    for (key, value) in event.attributes().iter() {
80        let _ = write!(
81            line,
82            " attribute={}({})",
83            key.escape_default(),
84            value.escape_default(),
85        );
86    }
87    for metric in event.metrics() {
88        let _ = write!(
89            line,
90            " metric={}({}) total={:?} completed={} active={} succeeded={} failed={} cancelled={}",
91            metric.id().escape_default(),
92            metric.name().escape_default(),
93            metric.total(),
94            metric.completed(),
95            metric.active(),
96            metric.succeeded(),
97            metric.failed(),
98            metric.cancelled(),
99        );
100    }
101    line
102}