-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(core): Add captureLog
method
#15717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2f69a75
d0b8803
0508266
9231910
77225ee
96be3dd
5a665c6
75f744c
1fc07a7
aef70a3
e2de16f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { LogSeverityLevel } from '../types-hoist'; | ||
|
||
/** | ||
* Maps a log severity level to a log severity number. | ||
* | ||
* @see LogSeverityLevel | ||
*/ | ||
export const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial<Record<LogSeverityLevel, number>> = { | ||
trace: 1, | ||
debug: 5, | ||
info: 9, | ||
warn: 13, | ||
error: 17, | ||
fatal: 21, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { createEnvelope } from '../utils-hoist'; | ||
|
||
import type { DsnComponents, SdkMetadata, SerializedOtelLog } from '../types-hoist'; | ||
import type { OtelLogEnvelope, OtelLogItem } from '../types-hoist/envelope'; | ||
import { dsnToString } from '../utils-hoist'; | ||
|
||
/** | ||
* Creates OTEL log envelope item for a serialized OTEL log. | ||
* | ||
* @param log - The serialized OTEL log to include in the envelope. | ||
* @returns The created OTEL log envelope item. | ||
*/ | ||
export function createOtelLogEnvelopeItem(log: SerializedOtelLog): OtelLogItem { | ||
return [ | ||
{ | ||
type: 'otel_log', | ||
}, | ||
log, | ||
]; | ||
} | ||
|
||
/** | ||
* Creates an envelope for a list of logs. | ||
* | ||
* @param logs - The logs to include in the envelope. | ||
* @param metadata - The metadata to include in the envelope. | ||
* @param tunnel - The tunnel to include in the envelope. | ||
* @param dsn - The DSN to include in the envelope. | ||
* @returns The created envelope. | ||
*/ | ||
export function createOtelLogEnvelope( | ||
logs: Array<SerializedOtelLog>, | ||
metadata?: SdkMetadata, | ||
tunnel?: string, | ||
dsn?: DsnComponents, | ||
): OtelLogEnvelope { | ||
const headers: OtelLogEnvelope[0] = {}; | ||
|
||
if (metadata?.sdk) { | ||
headers.sdk = { | ||
name: metadata.sdk.name, | ||
version: metadata.sdk.version, | ||
}; | ||
} | ||
|
||
if (!!tunnel && !!dsn) { | ||
headers.dsn = dsnToString(dsn); | ||
} | ||
|
||
return createEnvelope<OtelLogEnvelope>(headers, logs.map(createOtelLogEnvelopeItem)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import type { Client } from '../client'; | ||
import { _getTraceInfoFromScope } from '../client'; | ||
import { getClient, getCurrentScope } from '../currentScopes'; | ||
import { DEBUG_BUILD } from '../debug-build'; | ||
import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; | ||
import type { SerializedLogAttribute, SerializedOtelLog } from '../types-hoist'; | ||
import type { Log } from '../types-hoist/log'; | ||
import { logger } from '../utils-hoist'; | ||
import { _getSpanForScope } from '../utils/spanOnScope'; | ||
import { createOtelLogEnvelope } from './envelope'; | ||
|
||
const MAX_LOG_BUFFER_SIZE = 100; | ||
chargome marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const CLIENT_TO_LOG_BUFFER_MAP = new WeakMap<Client, Array<SerializedOtelLog>>(); | ||
|
||
/** | ||
* Converts a log attribute to a serialized log attribute. | ||
* | ||
* @param key - The key of the log attribute. | ||
* @param value - The value of the log attribute. | ||
* @returns The serialized log attribute. | ||
*/ | ||
export function logAttributeToSerializedLogAttribute(key: string, value: unknown): SerializedLogAttribute { | ||
switch (typeof value) { | ||
case 'number': | ||
return { | ||
key, | ||
value: { doubleValue: value }, | ||
}; | ||
case 'boolean': | ||
return { | ||
key, | ||
value: { boolValue: value }, | ||
}; | ||
case 'string': | ||
return { | ||
key, | ||
value: { stringValue: value }, | ||
}; | ||
default: { | ||
let stringValue = ''; | ||
try { | ||
stringValue = JSON.stringify(value) ?? ''; | ||
} catch (_) { | ||
// Do nothing | ||
} | ||
return { | ||
key, | ||
value: { stringValue }, | ||
}; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Captures a log event and sends it to Sentry. | ||
* | ||
* @param log - The log event to capture. | ||
* @param scope - A scope. Uses the current scope if not provided. | ||
* @param client - A client. Uses the current client if not provided. | ||
* | ||
* @experimental This method will experience breaking changes. This is not yet part of | ||
* the stable Sentry SDK API and can be changed or removed without warning. | ||
*/ | ||
export function captureLog(log: Log, scope = getCurrentScope(), client = getClient()): void { | ||
if (!client) { | ||
DEBUG_BUILD && logger.warn('No client available to capture log.'); | ||
return; | ||
} | ||
|
||
const { _experiments, release, environment } = client.getOptions(); | ||
if (!_experiments?.enableLogs) { | ||
DEBUG_BUILD && logger.warn('logging option not enabled, log will not be captured.'); | ||
return; | ||
} | ||
|
||
const [, traceContext] = _getTraceInfoFromScope(client, scope); | ||
|
||
const { level, message, attributes, severityNumber } = log; | ||
|
||
const logAttributes = { | ||
...attributes, | ||
}; | ||
|
||
if (release) { | ||
logAttributes.release = release; | ||
} | ||
|
||
if (environment) { | ||
logAttributes.environment = environment; | ||
} | ||
|
||
const span = _getSpanForScope(scope); | ||
if (span) { | ||
// Add the parent span ID to the log attributes for trace context | ||
logAttributes['sentry.trace.parent_span_id'] = span.spanContext().spanId; | ||
} | ||
|
||
const serializedLog: SerializedOtelLog = { | ||
severityText: level, | ||
body: { | ||
stringValue: message, | ||
}, | ||
attributes: Object.entries(logAttributes).map(([key, value]) => logAttributeToSerializedLogAttribute(key, value)), | ||
timeUnixNano: `${new Date().getTime().toString()}000000`, | ||
traceId: traceContext?.trace_id, | ||
severityNumber: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level], | ||
}; | ||
|
||
const logBuffer = CLIENT_TO_LOG_BUFFER_MAP.get(client); | ||
if (logBuffer === undefined) { | ||
CLIENT_TO_LOG_BUFFER_MAP.set(client, [serializedLog]); | ||
// Every time we initialize a new log buffer, we start a new interval to flush the buffer | ||
return; | ||
} | ||
|
||
logBuffer.push(serializedLog); | ||
if (logBuffer.length > MAX_LOG_BUFFER_SIZE) { | ||
_INTERNAL_flushLogsBuffer(client, logBuffer); | ||
} | ||
} | ||
|
||
/** | ||
* Flushes the logs buffer to Sentry. | ||
* | ||
* @param client - A client. | ||
* @param maybeLogBuffer - A log buffer. Uses the log buffer for the given client if not provided. | ||
*/ | ||
export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array<SerializedOtelLog>): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only thing that is not ideal about this approach is that we have to expose this method to allow us to flush. This is as we need a way for the browser and server-runtime-client to get access to the logs buffer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another approach would be to register a callback on the client maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That still has bundle size hit because we need to add that stuff to the client. I'll think about this though, it's a good point. |
||
const logBuffer = maybeLogBuffer ?? CLIENT_TO_LOG_BUFFER_MAP.get(client) ?? []; | ||
if (logBuffer.length === 0) { | ||
return; | ||
} | ||
|
||
const clientOptions = client.getOptions(); | ||
const envelope = createOtelLogEnvelope(logBuffer, clientOptions._metadata, clientOptions.tunnel, client.getDsn()); | ||
|
||
// Clear the log buffer after envelopes have been constructed. | ||
logBuffer.length = 0; | ||
|
||
// sendEnvelope should not throw | ||
// eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
client.sendEnvelope(envelope); | ||
} | ||
|
||
/** | ||
* Returns the log buffer for a given client. | ||
* | ||
* Exported for testing purposes. | ||
* | ||
* @param client - The client to get the log buffer for. | ||
* @returns The log buffer for the given client. | ||
*/ | ||
export function _INTERNAL_getLogBuffer(client: Client): Array<SerializedOtelLog> | undefined { | ||
return CLIENT_TO_LOG_BUFFER_MAP.get(client); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.