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_raw, ResourceContext};
10use crate::common::QueryParams;
11use crate::error::{ApifyClientError, 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    /// The URL used for streaming; we keep it so streaming can bypass the buffered path.
32    stream_url: String,
33    token: Option<String>,
34    user_agent: String,
35}
36
37impl LogClient {
38    pub(crate) fn new(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
39        let ctx = ResourceContext::single(http, base_url, resource_path, id);
40        let stream_url = ctx.url(None);
41        let (token, user_agent) = ctx.http.stream_credentials();
42        Self {
43            ctx,
44            stream_url,
45            token,
46            user_agent,
47        }
48    }
49
50    /// Creates a log client nested under a run or build (path `.../log`).
51    pub(crate) fn nested(http: HttpClient, base_url: &str, sub_path: &str) -> Self {
52        let ctx = ResourceContext::collection(http, base_url, sub_path);
53        let stream_url = ctx.url(None);
54        let (token, user_agent) = ctx.http.stream_credentials();
55        Self {
56            ctx,
57            stream_url,
58            token,
59            user_agent,
60        }
61    }
62
63    /// Fetches the entire log as a string, or `None` if it does not exist.
64    ///
65    /// Uses the default (processed) log format. To request the raw log, use
66    /// [`LogClient::get_with_options`].
67    pub async fn get(&self) -> ApifyClientResult<Option<String>> {
68        self.get_with_options(LogOptions::default()).await
69    }
70
71    /// Fetches the entire log as a string, or `None` if it does not exist, applying the given
72    /// [`LogOptions`] (e.g. [`LogOptions::raw`]).
73    pub async fn get_with_options(&self, options: LogOptions) -> ApifyClientResult<Option<String>> {
74        let mut params = QueryParams::new();
75        params.add_bool("raw", options.raw);
76        let response = get_raw(&self.ctx, None, &params).await?;
77        Ok(response.map(|r| String::from_utf8_lossy(&r.body).into_owned()))
78    }
79
80    /// Opens a streaming connection to the log, yielding chunks of bytes as they arrive.
81    ///
82    /// This powers real-time log redirection: callers can forward each chunk to their own
83    /// logger/stdout while a run is still in progress. The stream completes when the log
84    /// ends (i.e. the run finishes).
85    ///
86    /// Uses the default (processed) log format. To stream the raw log, use
87    /// [`LogClient::stream_with_options`].
88    pub async fn stream(
89        &self,
90    ) -> ApifyClientResult<impl Stream<Item = ApifyClientResult<Vec<u8>>>> {
91        self.stream_with_options(LogOptions::default()).await
92    }
93
94    /// Opens a streaming connection to the log applying the given [`LogOptions`], yielding
95    /// chunks of bytes as they arrive.
96    ///
97    /// Like [`LogClient::stream`], but lets the caller request the raw log via
98    /// [`LogOptions::raw`] (as the reference client's log redirection does, which streams
99    /// `{ raw: true }`).
100    pub async fn stream_with_options(
101        &self,
102        options: LogOptions,
103    ) -> ApifyClientResult<impl Stream<Item = ApifyClientResult<Vec<u8>>>> {
104        // Streaming needs a live connection, so we go through reqwest directly rather than
105        // the buffered backend path. The retry policy does not apply to an open stream.
106        let client = reqwest::Client::new();
107        let mut params = QueryParams::new();
108        params.push_raw("stream".to_string(), "1".to_string());
109        params.add_bool("raw", options.raw);
110        let url = params.apply_to_url(&self.stream_url);
111
112        let mut builder = client.get(&url).header("User-Agent", &self.user_agent);
113        if let Some(token) = &self.token {
114            builder = builder.header("Authorization", format!("Bearer {token}"));
115        }
116
117        let response = builder.send().await.map_err(ApifyClientError::from)?;
118        if !response.status().is_success() {
119            return Err(ApifyClientError::InvalidResponse(format!(
120                "log stream returned status {}",
121                response.status().as_u16()
122            )));
123        }
124
125        let byte_stream = response.bytes_stream();
126        Ok(futures_util::StreamExt::map(byte_stream, |chunk| {
127            chunk.map(|b| b.to_vec()).map_err(ApifyClientError::from)
128        }))
129    }
130}