-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(cloudflare): Split alarms into multiple traces and link them #19373
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
base: develop
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -1,20 +1,31 @@ | ||
| import type { DurableObjectStorage } from '@cloudflare/workers-types'; | ||
| import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; | ||
| import { isThenable, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; | ||
| import { storeSpanContext } from '../utils/traceLinks'; | ||
|
|
||
| const STORAGE_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list'] as const; | ||
| const STORAGE_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list', 'setAlarm', 'getAlarm', 'deleteAlarm'] as const; | ||
|
|
||
| type StorageMethod = (typeof STORAGE_METHODS_TO_INSTRUMENT)[number]; | ||
|
|
||
| type WaitUntil = (promise: Promise<unknown>) => void; | ||
|
|
||
| /** | ||
| * Instruments DurableObjectStorage methods with Sentry spans. | ||
| * | ||
| * Wraps the following async methods: | ||
| * - get, put, delete, list (KV API) | ||
| * - setAlarm, getAlarm, deleteAlarm (Alarm API) | ||
| * | ||
| * When setAlarm is called, it also stores the current span context so that when | ||
| * the alarm fires later, it can link back to the trace that called setAlarm. | ||
| * | ||
| * @param storage - The DurableObjectStorage instance to instrument | ||
| * @param waitUntil - Optional waitUntil function to defer span context storage | ||
| * @returns An instrumented DurableObjectStorage instance | ||
| */ | ||
| export function instrumentDurableObjectStorage(storage: DurableObjectStorage): DurableObjectStorage { | ||
| export function instrumentDurableObjectStorage( | ||
| storage: DurableObjectStorage, | ||
| waitUntil?: WaitUntil, | ||
| ): DurableObjectStorage { | ||
| return new Proxy(storage, { | ||
| get(target, prop, _receiver) { | ||
| // Use `target` as the receiver instead of the proxy (`_receiver`). | ||
|
|
@@ -46,7 +57,33 @@ export function instrumentDurableObjectStorage(storage: DurableObjectStorage): D | |
| }, | ||
| }, | ||
| () => { | ||
| return (original as (...args: unknown[]) => unknown).apply(target, args); | ||
| const teardown = async (): Promise<void> => { | ||
| // When setAlarm is called, store the current span context so that when the alarm | ||
| // fires later, it can link back to the trace that called setAlarm. | ||
| // We use the original (uninstrumented) storage (target) to avoid creating a span | ||
| // for this internal operation. The storage is deferred via waitUntil to not block. | ||
| if (methodName === 'setAlarm') { | ||
| await storeSpanContext(target, 'alarm'); | ||
| } | ||
| }; | ||
|
|
||
| const result = (original as (...args: unknown[]) => unknown).apply(target, args); | ||
|
|
||
| if (!isThenable(result)) { | ||
| waitUntil?.(teardown()); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| return result.then( | ||
| res => { | ||
| waitUntil?.(teardown()); | ||
| return res; | ||
| }, | ||
| e => { | ||
| throw e; | ||
| }, | ||
| ); | ||
|
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. Unnecessary teardown overhead for non-setAlarm storage methodsLow Severity The Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit 43b8f06. Configure here. |
||
| }, | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import type { DurableObjectStorage } from '@cloudflare/workers-types'; | ||
| import { TraceFlags } from '@opentelemetry/api'; | ||
| import type { SpanLink } from '@sentry/core'; | ||
| import { debug, getActiveSpan } from '@sentry/core'; | ||
| import { DEBUG_BUILD } from '../debug-build'; | ||
|
|
||
| /** Storage key prefix for the span context that links consecutive method invocations */ | ||
| const SENTRY_TRACE_LINK_KEY_PREFIX = '__SENTRY_TRACE_LINK__'; | ||
|
|
||
| /** Stored span context for creating span links */ | ||
| export interface StoredSpanContext { | ||
| traceId: string; | ||
| spanId: string; | ||
| sampled: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the storage key for a specific method's trace link. | ||
| */ | ||
| export function getTraceLinkKey(methodName: string): string { | ||
| return `${SENTRY_TRACE_LINK_KEY_PREFIX}${methodName}`; | ||
| } | ||
|
|
||
| /** | ||
| * Stores the current span context in Durable Object storage for trace linking. | ||
| * Uses the original uninstrumented storage to avoid creating spans for internal operations. | ||
| * Errors are silently ignored to prevent internal storage failures from propagating to user code. | ||
| */ | ||
| export async function storeSpanContext(originalStorage: DurableObjectStorage, methodName: string): Promise<void> { | ||
| try { | ||
| const activeSpan = getActiveSpan(); | ||
| if (activeSpan) { | ||
| const spanContext = activeSpan.spanContext(); | ||
| const storedContext: StoredSpanContext = { | ||
| traceId: spanContext.traceId, | ||
| spanId: spanContext.spanId, | ||
| sampled: spanContext.traceFlags === TraceFlags.SAMPLED, | ||
| }; | ||
| await originalStorage.put(getTraceLinkKey(methodName), storedContext); | ||
| } | ||
| } catch (error) { | ||
| // Silently ignore storage errors to prevent internal failures from affecting user code | ||
JPeer264 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| DEBUG_BUILD && debug.log(`[CloudflareClient] Error storing span context for method ${methodName}`, error); | ||
| } | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Retrieves a stored span context from Durable Object storage. | ||
| */ | ||
| export async function getStoredSpanContext( | ||
| originalStorage: DurableObjectStorage, | ||
| methodName: string, | ||
| ): Promise<StoredSpanContext | undefined> { | ||
| try { | ||
| return await originalStorage.get<StoredSpanContext>(getTraceLinkKey(methodName)); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Builds span links from a stored span context. | ||
| */ | ||
| export function buildSpanLinks(storedContext: StoredSpanContext): SpanLink[] { | ||
| return [ | ||
| { | ||
| context: { | ||
| traceId: storedContext.traceId, | ||
| spanId: storedContext.spanId, | ||
| traceFlags: storedContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, | ||
| }, | ||
| attributes: { | ||
| 'sentry.link.type': 'previous_trace', | ||
| }, | ||
| }, | ||
| ]; | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.