diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bbc4993..f28b40bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- log fetch now repects the log API request limit (1 per second) +- log fetch now passes transaction id and filter expression to backend log API + ## [3.3.0] - 2025-06-17 ## [3.2.0] - 2025-06-17 diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 80365cbd3..874e19203 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -410,6 +410,43 @@ export function generateLogApi({ const request = createAxiosInstance(state, requestConfig); + // add a response interceptor for HTTP 429 errors from log API + request.interceptors.response.use( + (response) => { + // If the response is successful, simply return it + return response; + }, + async (error) => { + const originalRequest = error.config; + const status = error.response ? error.response.status : null; + + // Check if the error is a 429 Too Many Requests + // and if the Retry-After header is present + if ( + status === 429 && + error.response.headers['retry-after'] && + !originalRequest._retry + ) { + originalRequest._retry = true; // Mark the request as retried to prevent infinite loops + + const retryAfterSeconds = parseInt( + error.response.headers['retry-after'], + 10 + ); + const delayMs = (retryAfterSeconds + 1) * 1000; + + // Wait for the specified duration + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + // Retry the original request + return request(originalRequest); + } + + // For other errors, or if no Retry-After header is found, + // or if the request has already been retried, reject the promise + return Promise.reject(error); + } + ); // enable curlirizer output in debug mode if (state.getCurlirize()) { curlirize(request, state); diff --git a/src/api/cloud/LogApi.ts b/src/api/cloud/LogApi.ts index 8a671ba85..8ed6aa9c7 100644 --- a/src/api/cloud/LogApi.ts +++ b/src/api/cloud/LogApi.ts @@ -9,8 +9,7 @@ import { import { generateLogApi, generateLogKeysApi } from '../BaseApi'; const logsTailURLTemplate = '%s/monitoring/logs/tail?source=%s'; -const logsFetchURLTemplate = - '%s/monitoring/logs?source=%s&beginTime=%s&endTime=%s'; +const logsFetchURLTemplate = '%s/monitoring/logs?source=%s'; const logsSourcesURLTemplate = '%s/monitoring/logs/sources'; const logsCreateAPIKeyAndSecretURLTemplate = '%s/keys?_action=create'; const logsGetAPIKeysURLTemplate = '%s/keys'; @@ -208,6 +207,8 @@ export async function tail({ * @param {string} startTs start timestamp * @param {string} endTs end timestamp * @param {string} cookie paged results cookie + * @param {string} transactionId transaction id + * @param {string} queryFilter query filter * @returns {Promise>} promise resolving to paged log event result */ export async function fetch({ @@ -215,21 +216,33 @@ export async function fetch({ startTs, endTs, cookie, + txid, + filter, state, }: { source: string; startTs: string; endTs: string; cookie: string; + txid: string; + filter: string; state: State; }): Promise> { let urlString = util.format( logsFetchURLTemplate, getHostOnlyUrl(state.getHost()), - encodeURIComponent(source), - startTs, - endTs + encodeURIComponent(source) ); + if (startTs && endTs) { + urlString += `&beginTime=${startTs}&endTime=${endTs}`; + } + if (txid) { + urlString += `&transactionId=${txid}`; + } + if (filter) { + const filterParam = `_queryFilter=${filter}`; + urlString += `&${encodeURIComponent(filterParam)}`; + } if (cookie) { urlString += `&_pagedResultsCookie=${encodeURIComponent(cookie)}`; } diff --git a/src/ops/cloud/LogOps.ts b/src/ops/cloud/LogOps.ts index 05f4f57b8..757cc66c4 100644 --- a/src/ops/cloud/LogOps.ts +++ b/src/ops/cloud/LogOps.ts @@ -86,13 +86,17 @@ export type Log = { * @param {string} startTs start timestamp * @param {string} endTs end timestamp * @param {string} cookie paged results cookie + * @param {string} txid transaction id + * @param {string} filter query filter * @returns {Promise>} promise resolving to paged log event result */ fetch( source: string, startTs: string, endTs: string, - cookie: string + cookie: string, + txid: string, + filter: string ): Promise>; }; @@ -138,9 +142,11 @@ export default (state: State): Log => { source: string, startTs: string, endTs: string, - cookie: string + cookie: string, + txid: string, + filter: string ): Promise> { - return fetch({ source, startTs, endTs, cookie, state }); + return fetch({ source, startTs, endTs, cookie, txid, filter, state }); }, }; }; @@ -575,6 +581,8 @@ export async function tail({ * @param {string} startTs start timestamp * @param {string} endTs end timestamp * @param {string} cookie paged results cookie + * @param {string} txid transaction id + * @param {string} filter query filter * @returns {Promise>} promise resolving to paged log event result */ export async function fetch({ @@ -582,16 +590,20 @@ export async function fetch({ startTs, endTs, cookie, + txid, + filter, state, }: { source: string; startTs: string; endTs: string; cookie: string; + txid: string; + filter: string; state: State; }): Promise> { try { - return _fetch({ source, startTs, endTs, cookie, state }); + return _fetch({ source, startTs, endTs, cookie, txid, filter, state }); } catch (error) { throw new FrodoError(`Error fetching logs`, error); } diff --git a/src/shared/State.ts b/src/shared/State.ts index 121acc4d9..e420f0642 100644 --- a/src/shared/State.ts +++ b/src/shared/State.ts @@ -45,7 +45,9 @@ export type State = { getPassword(): string; setRealm(realm: string): void; getRealm(): string; - setUseRealmPrefixOnManagedObjects(useRealmPrefixOnManagedObjects: boolean): void; + setUseRealmPrefixOnManagedObjects( + useRealmPrefixOnManagedObjects: boolean + ): void; getUseRealmPrefixOnManagedObjects(): boolean; setDeploymentType(type: string): void; getDeploymentType(): string;