Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core): Add span serialization utilities#19140
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e54e1512774fed5a6d17bfc18e222c7a20ae6ac9fca4324e47ec8aa872ac30349b96701eaa995File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import { getAsyncContextStrategy } from '../asyncContext'; | ||
| import type { RawAttributes } from '../attributes'; | ||
| import { serializeAttributes } from '../attributes'; | ||
| import { getMainCarrier } from '../carrier'; | ||
| import { getCurrentScope } from '../currentScopes'; | ||
| import { | ||
| @@ -12,7 +14,15 @@ import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; | ||
| import { getCapturedScopesOnSpan } from '../tracing/utils'; | ||
| import type { TraceContext } from '../types-hoist/context'; | ||
| import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; | ||
| import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; | ||
| import type { | ||
| SerializedSpan, | ||
| Span, | ||
| SpanAttributes, | ||
| SpanJSON, | ||
| SpanOrigin, | ||
| SpanTimeInput, | ||
| StreamedSpanJSON, | ||
| } from '../types-hoist/span'; | ||
| import type { SpanStatus } from '../types-hoist/spanStatus'; | ||
| import { addNonEnumerableProperty } from '../utils/object'; | ||
| import { generateSpanId } from '../utils/propagationContext'; | ||
| @@ -105,6 +115,27 @@ export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] | ||
| } | ||
| } | ||
| /** | ||
| * Converts the span links array to a flattened version with serialized attributes for V2 spans. | ||
| * | ||
| * If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent. | ||
| */ | ||
| export function getStreamedSpanLinks( | ||
| links?: SpanLink[], | ||
| ): SpanLinkJSON<RawAttributes<Record<string, unknown>>>[] | undefined { | ||
| if (links?.length) { | ||
| return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ | ||
| span_id: spanId, | ||
| trace_id: traceId, | ||
| sampled: traceFlags === TRACE_FLAG_SAMPLED, | ||
| attributes, | ||
| ...restContext, | ||
| })); | ||
| } else { | ||
| return undefined; | ||
| } | ||
| } | ||
| /** | ||
| * Convert a span time input into a timestamp in seconds. | ||
| */ | ||
| @@ -150,23 +181,12 @@ export function spanToJSON(span: Span): SpanJSON { | ||
| if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { | ||
| const { attributes, startTime, name, endTime, status, links } = span; | ||
| // In preparation for the next major of OpenTelemetry, we want to support | ||
| // looking up the parent span id according to the new API | ||
| // In OTel v1, the parent span id is accessed as `parentSpanId` | ||
| // In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` | ||
| const parentSpanId = | ||
| 'parentSpanId' in span | ||
| ? span.parentSpanId | ||
| : 'parentSpanContext' in span | ||
| ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId | ||
| : undefined; | ||
| return { | ||
| span_id, | ||
| trace_id, | ||
| data: attributes, | ||
| description: name, | ||
| parent_span_id: parentSpanId, | ||
| parent_span_id: getOtelParentSpanId(span), | ||
| start_timestamp: spanTimeInputToSeconds(startTime), | ||
| // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time | ||
| timestamp: spanTimeInputToSeconds(endTime) || undefined, | ||
| @@ -187,6 +207,77 @@ export function spanToJSON(span: Span): SpanJSON { | ||
| }; | ||
| } | ||
| /** | ||
| * Convert a span to the intermediate {@link StreamedSpanJSON} representation. | ||
| */ | ||
| export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { | ||
| if (spanIsSentrySpan(span)) { | ||
| return span.getStreamedSpanJSON(); | ||
| } | ||
| const { spanId: span_id, traceId: trace_id } = span.spanContext(); | ||
| // Handle a span from @opentelemetry/sdk-base-trace's `Span` class | ||
| if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { | ||
| const { attributes, startTime, name, endTime, status, links } = span; | ||
| return { | ||
| name, | ||
| span_id, | ||
| trace_id, | ||
| parent_span_id: getOtelParentSpanId(span), | ||
| start_timestamp: spanTimeInputToSeconds(startTime), | ||
| end_timestamp: spanTimeInputToSeconds(endTime), | ||
sentry[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. Lms24 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| is_segment: span === INTERNAL_getSegmentSpan(span), | ||
| status: getSimpleStatusMessage(status), | ||
| attributes, | ||
| links: getStreamedSpanLinks(links), | ||
| }; | ||
| } | ||
| // Finally, as a fallback, at least we have `spanContext()`.... | ||
| // This should not actually happen in reality, but we need to handle it for type safety. | ||
| return { | ||
| span_id, | ||
| trace_id, | ||
| start_timestamp: 0, | ||
| name: '', | ||
| end_timestamp: 0, | ||
| status: 'ok', | ||
| is_segment: span === INTERNAL_getSegmentSpan(span), | ||
| }; | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * In preparation for the next major of OpenTelemetry, we want to support | ||
| * looking up the parent span id according to the new API | ||
| * In OTel v1, the parent span id is accessed as `parentSpanId` | ||
| * In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` | ||
| */ | ||
| function getOtelParentSpanId(span: OpenTelemetrySdkTraceBaseSpan): string | undefined { | ||
| return 'parentSpanId' in span | ||
| ? span.parentSpanId | ||
| : 'parentSpanContext' in span | ||
| ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId | ||
| : undefined; | ||
| } | ||
| /** | ||
| * Converts a {@link StreamedSpanJSON} to a {@link SerializedSpan}. | ||
| * This is the final serialized span format that is sent to Sentry. | ||
| * The returned serilaized spans must not be consumed by users or SDK integrations. | ||
| */ | ||
| export function streamedSpanJsonToSerializedSpan(spanJson: StreamedSpanJSON): SerializedSpan { | ||
| return { | ||
| ...spanJson, | ||
| attributes: serializeAttributes(spanJson.attributes), | ||
| links: spanJson.links?.map(link => ({ | ||
| ...link, | ||
| attributes: serializeAttributes(link.attributes), | ||
| })), | ||
| }; | ||
| } | ||
| function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan { | ||
| const castSpan = span as Partial<OpenTelemetrySdkTraceBaseSpan>; | ||
| return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status; | ||
| @@ -237,6 +328,13 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef | ||
| return status.message || 'internal_error'; | ||
| } | ||
| /** | ||
| * Convert the various statuses to the simple onces expected by Sentry for steamed spans ('ok' is default). | ||
| */ | ||
| export function getSimpleStatusMessage(status: SpanStatus | undefined): 'ok' | 'error' { | ||
| return !status || status.code === SPAN_STATUS_OK || status.code === SPAN_STATUS_UNSET ? 'ok' : 'error'; | ||
| } | ||
| const CHILD_SPANS_FIELD = '_sentryChildSpans'; | ||
| const ROOT_SPAN_FIELD = '_sentryRootSpan'; | ||
| @@ -298,7 +396,12 @@ export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] { | ||
| /** | ||
| * Returns the root span of a given span. | ||
| */ | ||
| export function getRootSpan(span: SpanWithPotentialChildren): Span { | ||
| export const getRootSpan = INTERNAL_getSegmentSpan; | ||
| /** | ||
| * Returns the segment span of a given span. | ||
| */ | ||
| export function INTERNAL_getSegmentSpan(span: SpanWithPotentialChildren): Span { | ||
| return span[ROOT_SPAN_FIELD] || span; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make this even more visible that this is an internal API: Should this maybe prefixed with
_INTERNAL_?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users should never interact with
SentrySpandirectly but leave it up tostart*Span*APIs which kind ofSpan(as in interface) they get back or work with.I oriented myself here primarily on the already existing
getSpanJSONmethod. In light of keeping the name short for bundle size, I'd tend to keep it that way, unless you think we should still do it?