UNPKG

apify-client

Version:
267 lines 9.73 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamedLog = exports.LoggerActorRedirect = exports.LogClient = void 0; const tslib_1 = require("tslib"); const ansi_colors_1 = tslib_1.__importDefault(require("ansi-colors")); const log_1 = require("@apify/log"); const resource_client_1 = require("../base/resource_client"); const utils_1 = require("../utils"); /** * Client for accessing Actor run or build logs. * * Provides methods to retrieve logs as text or stream them in real-time. Logs can be accessed * for both running and finished Actor runs and builds. * * @example * ```javascript * const client = new ApifyClient({ token: 'my-token' }); * const runClient = client.run('my-run-id'); * * // Get the log content * const log = await runClient.log().get(); * console.log(log); * * // Stream the log in real-time * const stream = await runClient.log().stream(); * stream.on('line', (line) => console.log(line)); * ``` * * @see https://docs.apify.com/platform/actors/running/runs-and-builds#logging */ class LogClient extends resource_client_1.ResourceClient { /** * @hidden */ constructor(options) { super({ resourcePath: 'logs', ...options, }); } /** * Retrieves the log as a string. * * @param options - Log retrieval options. * @param options.raw - If `true`, returns raw log content without any processing. Default is `false`. * @returns The log content as a string, or `undefined` if it does not exist. * @see https://docs.apify.com/api/v2/log-get */ async get(options = {}) { const requestOpts = { url: this._url(), method: 'GET', params: this._params(options), }; try { const response = await this.httpClient.call(requestOpts); return (0, utils_1.cast)(response.data); } catch (err) { (0, utils_1.catchNotFoundOrThrow)(err); } return undefined; } /** * Retrieves the log as a Readable stream. Only works in Node.js. * * @param options - Log retrieval options. * @param options.raw - If `true`, returns raw log content without any processing. Default is `false`. * @returns The log content as a Readable stream, or `undefined` if it does not exist. * @see https://docs.apify.com/api/v2/log-get */ async stream(options = {}) { const params = { stream: true, raw: options.raw, }; const requestOpts = { url: this._url(), method: 'GET', params: this._params(params), responseType: 'stream', }; try { const response = await this.httpClient.call(requestOpts); return (0, utils_1.cast)(response.data); } catch (err) { (0, utils_1.catchNotFoundOrThrow)(err); } return undefined; } } exports.LogClient = LogClient; /** * Logger for redirected actor logs. */ class LoggerActorRedirect extends log_1.Logger { constructor(options = {}) { super({ skipTime: true, level: log_1.LogLevel.DEBUG, ...options }); } _log(level, message, data, exception, opts = {}) { if (level > this.options.level) { return; } if (data || exception) { throw new Error('Redirect logger does not use other arguments than level and message'); } let { prefix } = opts; prefix = prefix ? `${prefix}` : ''; let maybeDate = ''; if (!this.options.skipTime) { maybeDate = `${new Date().toISOString().replace('Z', '').replace('T', ' ')} `; } const line = `${ansi_colors_1.default.gray(maybeDate)}${ansi_colors_1.default.cyan(prefix)}${message || ''}`; // All redirected logs are logged at info level to avid any console specific formating for non-info levels, // which have already been applied once to the original log. (For example error stack traces etc.) this._outputWithConsole(log_1.LogLevel.INFO, line); return line; } } exports.LoggerActorRedirect = LoggerActorRedirect; /** * Helper class for redirecting streamed Actor logs to another log. */ class StreamedLog { constructor(options) { Object.defineProperty(this, "destinationLog", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "streamBuffer", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "splitMarker", { enumerable: true, configurable: true, writable: true, value: /(?:\n|^)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/g }); Object.defineProperty(this, "relevancyTimeLimit", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "logClient", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "streamingTask", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "stopLogging", { enumerable: true, configurable: true, writable: true, value: false }); const { toLog, logClient, fromStart = true } = options; this.destinationLog = toLog; this.logClient = logClient; this.relevancyTimeLimit = fromStart ? null : new Date(); } /** * Start log redirection. */ start() { if (this.streamingTask) { throw new Error('Streaming task already active'); } this.stopLogging = false; this.streamingTask = this.streamLog(); } /** * Stop log redirection. */ async stop() { if (!this.streamingTask) { throw new Error('Streaming task is not active'); } this.stopLogging = true; try { await this.streamingTask; } catch (err) { if (!(err instanceof Error && err.name === 'AbortError')) { throw err; } } finally { this.streamingTask = null; } } /** * Get log stream from response and redirect it to another log. */ async streamLog() { const logStream = await this.logClient.stream({ raw: true }); if (!logStream) { return; } const lastChunkRemainder = await this.logStreamChunks(logStream); // Process whatever is left when exiting. Maybe it is incomplete, maybe it is last log without EOL. const lastMessage = Buffer.from(lastChunkRemainder).toString().trim(); if (lastMessage.length) { this.destinationLog.info(lastMessage); } } async logStreamChunks(logStream) { // Chunk may be incomplete. Keep remainder for next chunk. let previousChunkRemainder = new Uint8Array(); for await (const chunk of logStream) { // Handle possible leftover incomplete line from previous chunk. // Everything before last end of line is complete. const chunkWithPreviousRemainder = new Uint8Array(previousChunkRemainder.length + chunk.length); chunkWithPreviousRemainder.set(previousChunkRemainder, 0); chunkWithPreviousRemainder.set(chunk, previousChunkRemainder.length); const lastCompleteMessageIndex = chunkWithPreviousRemainder.lastIndexOf(0x0a); previousChunkRemainder = chunkWithPreviousRemainder.slice(lastCompleteMessageIndex); // Push complete part of the chunk to the buffer this.streamBuffer.push(Buffer.from(chunkWithPreviousRemainder.slice(0, lastCompleteMessageIndex))); this.logBufferContent(); // Keep processing the new data until stopped if (this.stopLogging) { break; } } return previousChunkRemainder; } /** * Parse the buffer and log complete messages. */ logBufferContent() { const allParts = Buffer.concat(this.streamBuffer).toString().split(this.splitMarker).slice(1); // Parse the buffer parts into complete messages const messageMarkers = allParts.filter((_, i) => i % 2 === 0); const messageContents = allParts.filter((_, i) => i % 2 !== 0); this.streamBuffer = []; messageMarkers.forEach((marker, index) => { const decodedMarker = marker; const decodedContent = messageContents[index]; if (this.relevancyTimeLimit) { // Log only relevant messages. Ignore too old log messages. const logTime = new Date(decodedMarker); if (logTime < this.relevancyTimeLimit) { return; } } const message = decodedMarker + decodedContent; // Original log level information is not available. Log all on info level. Log level could be guessed for // some logs, but for any multiline logs such guess would be probably correct only for the first line. this.destinationLog.info(message.trim()); }); } } exports.StreamedLog = StreamedLog; //# sourceMappingURL=log.js.map