apify-client
Version:
Apify API client for JavaScript
267 lines • 9.73 kB
JavaScript
;
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