-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathspanUtils.ts
312 lines (273 loc) · 10 KB
/
spanUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
import { getAsyncContextStrategy } from '../asyncContext';
import { getMainCarrier } from '../carrier';
import { getCurrentScope } from '../currentScopes';
import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary';
import type { MetricType } from '../metrics/types';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import type { SentrySpan } from '../tracing/sentrySpan';
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
import type {
MeasurementUnit,
Primitive,
Span,
SpanAttributes,
SpanJSON,
SpanOrigin,
SpanStatus,
SpanTimeInput,
TraceContext,
} from '../types-hoist';
import { consoleSandbox } from '../utils-hoist/logger';
import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object';
import { generateSpanId } from '../utils-hoist/propagationContext';
import { timestampInSeconds } from '../utils-hoist/time';
import { generateSentryTraceHeader } from '../utils-hoist/tracing';
import { _getSpanForScope } from './spanOnScope';
// These are aligned with OpenTelemetry trace flags
export const TRACE_FLAG_NONE = 0x0;
export const TRACE_FLAG_SAMPLED = 0x1;
// todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan`
let hasShownSpanDropWarning = false;
/**
* Convert a span to a trace context, which can be sent as the `trace` context in an event.
* By default, this will only include trace_id, span_id & parent_span_id.
* If `includeAllData` is true, it will also include data, op, status & origin.
*/
export function spanToTransactionTraceContext(span: Span): TraceContext {
const { spanId: span_id, traceId: trace_id } = span.spanContext();
const { data, op, parent_span_id, status, origin } = spanToJSON(span);
return dropUndefinedKeys({
parent_span_id,
span_id,
trace_id,
data,
op,
status,
origin,
});
}
/**
* Convert a span to a trace context, which can be sent as the `trace` context in a non-transaction event.
*/
export function spanToTraceContext(span: Span): TraceContext {
const { spanId, traceId: trace_id, isRemote } = span.spanContext();
// If the span is remote, we use a random/virtual span as span_id to the trace context,
// and the remote span as parent_span_id
const parent_span_id = isRemote ? spanId : spanToJSON(span).parent_span_id;
const span_id = isRemote ? generateSpanId() : spanId;
return dropUndefinedKeys({
parent_span_id,
span_id,
trace_id,
});
}
/**
* Convert a Span to a Sentry trace header.
*/
export function spanToTraceHeader(span: Span): string {
const { traceId, spanId } = span.spanContext();
const sampled = spanIsSampled(span);
return generateSentryTraceHeader(traceId, spanId, sampled);
}
/**
* Convert a span time input into a timestamp in seconds.
*/
export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number {
if (typeof input === 'number') {
return ensureTimestampInSeconds(input);
}
if (Array.isArray(input)) {
// See {@link HrTime} for the array-based time format
return input[0] + input[1] / 1e9;
}
if (input instanceof Date) {
return ensureTimestampInSeconds(input.getTime());
}
return timestampInSeconds();
}
/**
* Converts a timestamp to second, if it was in milliseconds, or keeps it as second.
*/
function ensureTimestampInSeconds(timestamp: number): number {
const isMs = timestamp > 9999999999;
return isMs ? timestamp / 1000 : timestamp;
}
/**
* Convert a span to a JSON representation.
*/
// Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json).
// This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility.
// And `spanToJSON` needs the Span class from `span.ts` to check here.
export function spanToJSON(span: Span): Partial<SpanJSON> {
if (spanIsSentrySpan(span)) {
return span.getSpanJSON();
}
try {
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, parentSpanId, status } = span;
return dropUndefinedKeys({
span_id,
trace_id,
data: attributes,
description: name,
parent_span_id: parentSpanId,
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,
status: getStatusMessage(status),
op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP],
origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
_metrics_summary: getMetricSummaryJsonForSpan(span),
});
}
// Finally, at least we have `spanContext()`....
return {
span_id,
trace_id,
};
} catch {
return {};
}
}
function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan {
const castSpan = span as OpenTelemetrySdkTraceBaseSpan;
return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status;
}
/** Exported only for tests. */
export interface OpenTelemetrySdkTraceBaseSpan extends Span {
attributes: SpanAttributes;
startTime: SpanTimeInput;
name: string;
status: SpanStatus;
endTime: SpanTimeInput;
parentSpanId?: string;
}
/**
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}
/**
* Returns true if a span is sampled.
* In most cases, you should just use `span.isRecording()` instead.
* However, this has a slightly different semantic, as it also returns false if the span is finished.
* So in the case where this distinction is important, use this method.
*/
export function spanIsSampled(span: Span): boolean {
// We align our trace flags with the ones OpenTelemetry use
// So we also check for sampled the same way they do.
const { traceFlags } = span.spanContext();
return traceFlags === TRACE_FLAG_SAMPLED;
}
/** Get the status message to use for a JSON representation of a span. */
export function getStatusMessage(status: SpanStatus | undefined): string | undefined {
if (!status || status.code === SPAN_STATUS_UNSET) {
return undefined;
}
if (status.code === SPAN_STATUS_OK) {
return 'ok';
}
return status.message || 'unknown_error';
}
const CHILD_SPANS_FIELD = '_sentryChildSpans';
const ROOT_SPAN_FIELD = '_sentryRootSpan';
type SpanWithPotentialChildren = Span & {
[CHILD_SPANS_FIELD]?: Set<Span>;
[ROOT_SPAN_FIELD]?: Span;
};
/**
* Adds an opaque child span reference to a span.
*/
export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: Span): void {
// We store the root span reference on the child span
// We need this for `getRootSpan()` to work
const rootSpan = span[ROOT_SPAN_FIELD] || span;
addNonEnumerableProperty(childSpan as SpanWithPotentialChildren, ROOT_SPAN_FIELD, rootSpan);
// We store a list of child spans on the parent span
// We need this for `getSpanDescendants()` to work
if (span[CHILD_SPANS_FIELD]) {
span[CHILD_SPANS_FIELD].add(childSpan);
} else {
addNonEnumerableProperty(span, CHILD_SPANS_FIELD, new Set([childSpan]));
}
}
/** This is only used internally by Idle Spans. */
export function removeChildSpanFromSpan(span: SpanWithPotentialChildren, childSpan: Span): void {
if (span[CHILD_SPANS_FIELD]) {
span[CHILD_SPANS_FIELD].delete(childSpan);
}
}
/**
* Returns an array of the given span and all of its descendants.
*/
export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] {
const resultSet = new Set<Span>();
function addSpanChildren(span: SpanWithPotentialChildren): void {
// This exit condition is required to not infinitely loop in case of a circular dependency.
if (resultSet.has(span)) {
return;
// We want to ignore unsampled spans (e.g. non recording spans)
} else if (spanIsSampled(span)) {
resultSet.add(span);
const childSpans = span[CHILD_SPANS_FIELD] ? Array.from(span[CHILD_SPANS_FIELD]) : [];
for (const childSpan of childSpans) {
addSpanChildren(childSpan);
}
}
}
addSpanChildren(span);
return Array.from(resultSet);
}
/**
* Returns the root span of a given span.
*/
export function getRootSpan(span: SpanWithPotentialChildren): Span {
return span[ROOT_SPAN_FIELD] || span;
}
/**
* Returns the currently active span.
*/
export function getActiveSpan(): Span | undefined {
const carrier = getMainCarrier();
const acs = getAsyncContextStrategy(carrier);
if (acs.getActiveSpan) {
return acs.getActiveSpan();
}
return _getSpanForScope(getCurrentScope());
}
/**
* Updates the metric summary on the currently active span
*/
export function updateMetricSummaryOnActiveSpan(
metricType: MetricType,
sanitizedName: string,
value: number,
unit: MeasurementUnit,
tags: Record<string, Primitive>,
bucketKey: string,
): void {
const span = getActiveSpan();
if (span) {
updateMetricSummaryOnSpan(span, metricType, sanitizedName, value, unit, tags, bucketKey);
}
}
/**
* Logs a warning once if `beforeSendSpan` is used to drop spans.
*
* todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan`.
*/
export function showSpanDropWarning(): void {
if (!hasShownSpanDropWarning) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
'[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.',
);
});
hasShownSpanDropWarning = true;
}
}