Skip to main content

apify_client/clients/
log.rs

1//! Client for accessing a run's or build's log.
2//!
3//! Logs are accessible at the top level (`/v2/logs/{buildOrRunId}`) and nested under a
4//! run or build (`.../log`). The [`LogClient`] supports fetching the whole log as text
5//! and streaming it for real-time redirection (the "log redirection" feature).
6
7use futures_util::Stream;
8
9use crate::clients::base::{get_byte_stream, get_raw, ResourceContext};
10use crate::common::QueryParams;
11use crate::error::ApifyClientResult;
12use crate::http_client::HttpClient;
13
14/// Options for retrieving or streaming a log ([`LogClient::get_with_options`] /
15/// [`LogClient::stream_with_options`]).
16///
17/// Covers the spec's optional `raw` query parameter on the log endpoints
18/// (`GET /v2/logs/{buildOrRunId}`, `GET /v2/actor-runs/{runId}/log`, and the last-run log
19/// variants), matching the reference client's `LogOptions`.
20#[derive(Debug, Default, Clone)]
21pub struct LogOptions {
22    /// If `true`, return the raw log content without any server-side processing (e.g. without
23    /// the per-line timestamps the API adds by default). Defaults to `false` when unset.
24    pub raw: Option<bool>,
25}
26
27/// Client for an Actor run or build log.
28#[derive(Debug, Clone)]
29pub struct LogClient {
30    ctx: ResourceContext,
31}
32
33impl LogClient {
34    pub(crate) fn new(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
35        Self {
36            ctx: ResourceContext::single(http, base_url, resource_path, id),
37        }
38    }
39
40    /// Creates a log client nested under a run or build (path `.../log`).
41    pub(crate) fn nested(http: HttpClient, base_url: &str, sub_path: &str) -> Self {
42        Self {
43            ctx: ResourceContext::collection(http, base_url, sub_path),
44        }
45    }
46
47    /// Fetches the entire log as a string, or `None` if it does not exist.
48    ///
49    /// Uses the default (processed) log format. To request the raw log, use
50    /// [`LogClient::get_with_options`]. To get the log as raw bytes instead of a `String`
51    /// (e.g. if it is not valid UTF-8), use [`LogClient::get_bytes`].
52    pub async fn get(&self) -> ApifyClientResult<Option<String>> {
53        self.get_with_options(LogOptions::default()).await
54    }
55
56    /// Fetches the entire log as a string, or `None` if it does not exist, applying the given
57    /// [`LogOptions`] (e.g. [`LogOptions::raw`]).
58    pub async fn get_with_options(&self, options: LogOptions) -> ApifyClientResult<Option<String>> {
59        Ok(self
60            .get_bytes_with_options(options)
61            .await?
62            .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()))
63    }
64
65    /// Fetches the entire log as raw bytes, or `None` if it does not exist.
66    ///
67    /// The counterpart of [`LogClient::get`] for a log that is not valid UTF-8, or when the
68    /// caller wants to avoid the lossy-UTF-8 conversion `get()` applies. Uses the default
69    /// (processed) log format; see [`LogClient::get_bytes_with_options`] for the raw format.
70    pub async fn get_bytes(&self) -> ApifyClientResult<Option<Vec<u8>>> {
71        self.get_bytes_with_options(LogOptions::default()).await
72    }
73
74    /// Fetches the entire log as raw bytes, or `None` if it does not exist, applying the given
75    /// [`LogOptions`] (e.g. [`LogOptions::raw`]). The byte-returning counterpart of
76    /// [`LogClient::get_with_options`].
77    pub async fn get_bytes_with_options(
78        &self,
79        options: LogOptions,
80    ) -> ApifyClientResult<Option<Vec<u8>>> {
81        let mut params = QueryParams::new();
82        params.add_bool("raw", options.raw);
83        let response = get_raw(&self.ctx, None, &params).await?;
84        Ok(response.map(|r| r.body))
85    }
86
87    /// Opens a streaming connection to the log, yielding chunks of bytes as they arrive.
88    ///
89    /// This powers real-time log redirection: callers can forward each chunk to their own
90    /// logger/stdout while a run is still in progress. The stream completes when the log ends
91    /// (i.e. the run finishes) — as long as a new chunk arrives at least every 6 minutes; a run
92    /// that legitimately logs nothing for that long errors with
93    /// [`ApifyClientError::Timeout`](crate::error::ApifyClientError::Timeout) instead.
94    ///
95    /// Uses the default (processed) log format. To stream the raw log, use
96    /// [`LogClient::stream_with_options`].
97    pub async fn stream(
98        &self,
99    ) -> ApifyClientResult<impl Stream<Item = ApifyClientResult<Vec<u8>>>> {
100        self.stream_with_options(LogOptions::default()).await
101    }
102
103    /// Opens a streaming connection to the log applying the given [`LogOptions`], yielding
104    /// chunks of bytes as they arrive.
105    ///
106    /// Like [`LogClient::stream`], but lets the caller request the raw log via
107    /// [`LogOptions::raw`] (as the reference client's log redirection does, which streams
108    /// `{ raw: true }`).
109    pub async fn stream_with_options(
110        &self,
111        options: LogOptions,
112    ) -> ApifyClientResult<impl Stream<Item = ApifyClientResult<Vec<u8>>>> {
113        // Goes through `get_byte_stream` (the configured `HttpBackend`'s streaming path) instead
114        // of `HttpClient::call`, whose buffered backend fully reads a response before returning
115        // it. The retry policy does not apply to an open stream.
116        let mut params = QueryParams::new();
117        params.push_raw("stream".to_string(), "1".to_string());
118        params.add_bool("raw", options.raw);
119        let url = params.apply_to_url(&self.ctx.url(None));
120        get_byte_stream(&self.ctx, url).await
121    }
122}