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, ¶ms).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}