diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6deea6ac38ce..3599448abf4c 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -670,6 +670,13 @@ export abstract class Client { */ public on(hook: 'afterCaptureLog', callback: (log: Log) => void): () => void; + /** + * A hook that is called when the client is flushing logs + * + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'flushLogs', callback: () => void): () => void; + /** * Register a hook on this client. */ @@ -827,6 +834,11 @@ export abstract class Client { */ public emit(hook: 'afterCaptureLog', log: Log): void; + /** + * Emit a hook event for client flush logs + */ + public emit(hook: 'flushLogs'): void; + /** * Emit a hook that was previously registered via `on()`. */ diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 5e12f5739729..8d9081fcc2bb 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -163,6 +163,8 @@ export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array // Clear the log buffer after envelopes have been constructed. logBuffer.length = 0; + client.emit('flushLogs'); + // sendEnvelope should not throw // eslint-disable-next-line @typescript-eslint/no-floating-promises client.sendEnvelope(envelope); diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index dfada209fcfd..b7910ed23d0a 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -25,6 +25,9 @@ import { resolvedSyncPromise } from './utils-hoist/syncpromise'; import { _INTERNAL_flushLogsBuffer } from './logs/exports'; import { isPrimitive } from './utils-hoist'; +// TODO: Make this configurable +const DEFAULT_LOG_FLUSH_INTERVAL = 5000; + export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; runtime?: { name: string; version?: string }; @@ -37,6 +40,7 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends Client { + private _logFlushIdleTimeout: ReturnType | undefined; private _logWeight: number; /** @@ -54,9 +58,9 @@ export class ServerRuntimeClient< if (this._options._experiments?.enableLogs) { // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; - client.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); + client.on('flushLogs', () => { client._logWeight = 0; + clearTimeout(client._logFlushIdleTimeout); }); client.on('afterCaptureLog', log => { @@ -67,7 +71,11 @@ export class ServerRuntimeClient< // the payload gets too big. if (client._logWeight >= 800_000) { _INTERNAL_flushLogsBuffer(client); - client._logWeight = 0; + } else { + // start an idle timeout to flush the logs buffer if no logs are captured for a while + client._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(client); + }, DEFAULT_LOG_FLUSH_INTERVAL); } }); } diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index 87c62b1567d4..4aaf77a06fb2 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, test, vi } from 'vitest'; import { Scope, createTransport } from '../../src'; import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; import { ServerRuntimeClient } from '../../src/server-runtime-client'; -import { _INTERNAL_captureLog } from '../../src/logs/exports'; +import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '../../src/logs/exports'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -256,8 +256,8 @@ describe('ServerRuntimeClient', () => { _INTERNAL_captureLog({ message: 'test1', level: 'info' }, client); _INTERNAL_captureLog({ message: 'test2', level: 'info' }, client); - // Trigger flush event - client.emit('flush'); + // Trigger flush directly + _INTERNAL_flushLogsBuffer(client); expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); expect(client['_logWeight']).toBe(0); // Weight should be reset after flush diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index 74f509ac42e7..f7d59add8056 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -4,7 +4,7 @@ import { trace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; -import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core'; +import { _INTERNAL_flushLogsBuffer, SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core'; import { getTraceContextForScope } from '@sentry/opentelemetry'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; @@ -18,6 +18,7 @@ export class NodeClient extends ServerRuntimeClient { private _tracer: Tracer | undefined; private _clientReportInterval: NodeJS.Timeout | undefined; private _clientReportOnExitFlushListener: (() => void) | undefined; + private _logOnExitFlushListener: (() => void) | undefined; public constructor(options: NodeClientOptions) { const clientOptions: ServerRuntimeClientOptions = { @@ -40,6 +41,14 @@ export class NodeClient extends ServerRuntimeClient { ); super(clientOptions); + + if (this.getOptions()._experiments?.enableLogs) { + this._logOnExitFlushListener = () => { + _INTERNAL_flushLogsBuffer(this); + }; + + process.on('beforeExit', this._logOnExitFlushListener); + } } /** Get the OTEL tracer. */ @@ -84,6 +93,10 @@ export class NodeClient extends ServerRuntimeClient { process.off('beforeExit', this._clientReportOnExitFlushListener); } + if (this._logOnExitFlushListener) { + process.off('beforeExit', this._logOnExitFlushListener); + } + return super.close(timeout); }