Skip to content

Commit

Permalink
feat(core): Add metric summaries to spans (#10432)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Feb 5, 2024
1 parent 300bba4 commit 94cdd8b
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
_experiments: {
metricsAggregator: true,
},
});

// Stop the process from exiting before the transaction is sent
setInterval(() => {}, 1000);

Sentry.startSpan(
{
name: 'Test Transaction',
op: 'transaction',
},
() => {
Sentry.metrics.increment('root-counter');
Sentry.metrics.increment('root-counter');

Sentry.startSpan(
{
name: 'Some other span',
op: 'transaction',
},
() => {
Sentry.metrics.increment('root-counter');
Sentry.metrics.increment('root-counter');
Sentry.metrics.increment('root-counter', 2);

Sentry.metrics.set('root-set', 'some-value');
Sentry.metrics.set('root-set', 'another-value');
Sentry.metrics.set('root-set', 'another-value');

Sentry.metrics.gauge('root-gauge', 42);
Sentry.metrics.gauge('root-gauge', 20);

Sentry.metrics.distribution('root-distribution', 42);
Sentry.metrics.distribution('root-distribution', 20);
},
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createRunner } from '../../../utils/runner';

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
_metrics_summary: {
'c:root-counter@none': {
min: 1,
max: 1,
count: 2,
sum: 2,
tags: {
release: '1.0',
transaction: 'Test Transaction',
},
},
},
spans: expect.arrayContaining([
expect.objectContaining({
description: 'Some other span',
op: 'transaction',
_metrics_summary: {
'c:root-counter@none': {
min: 1,
max: 2,
count: 3,
sum: 4,
tags: {
release: '1.0',
transaction: 'Test Transaction',
},
},
's:root-set@none': {
min: 0,
max: 1,
count: 3,
sum: 2,
tags: {
release: '1.0',
transaction: 'Test Transaction',
},
},
'g:root-gauge@none': {
min: 20,
max: 42,
count: 2,
sum: 62,
tags: {
release: '1.0',
transaction: 'Test Transaction',
},
},
'd:root-distribution@none': {
min: 20,
max: 42,
count: 2,
sum: 62,
tags: {
release: '1.0',
transaction: 'Test Transaction',
},
},
},
}),
]),
};

test('Should add metric summaries to spans', done => {
createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
});
11 changes: 10 additions & 1 deletion packages/core/src/metrics/aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type {
Primitive,
} from '@sentry/types';
import { timestampInSeconds } from '@sentry/utils';
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants';
import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
import { METRIC_MAP } from './instance';
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
import type { MetricBucket, MetricType } from './types';
import { getBucketKey, sanitizeTags } from './utils';

Expand Down Expand Up @@ -62,7 +63,11 @@ export class MetricsAggregator implements MetricsAggregatorBase {
const tags = sanitizeTags(unsanitizedTags);

const bucketKey = getBucketKey(metricType, name, unit, tags);

let bucketItem = this._buckets.get(bucketKey);
// If this is a set metric, we need to calculate the delta from the previous weight.
const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0;

if (bucketItem) {
bucketItem.metric.add(value);
// TODO(abhi): Do we need this check?
Expand All @@ -82,6 +87,10 @@ export class MetricsAggregator implements MetricsAggregatorBase {
this._buckets.set(bucketKey, bucketItem);
}

// If value is a string, it's a set metric so calculate the delta from the previous weight.
const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value;
updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey);

// We need to keep track of the total weight of the buckets so that we can
// flush them when we exceed the max weight.
this._bucketsTotalWeight += bucketItem.metric.weight;
Expand Down
27 changes: 15 additions & 12 deletions packages/core/src/metrics/browser-aggregator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import type {
Client,
ClientOptions,
MeasurementUnit,
MetricBucketItem,
MetricsAggregator,
Primitive,
} from '@sentry/types';
import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';
import { timestampInSeconds } from '@sentry/utils';
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants';
import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants';
import { METRIC_MAP } from './instance';
import { updateMetricSummaryOnActiveSpan } from './metric-summary';
import type { MetricBucket, MetricType } from './types';
import { getBucketKey, sanitizeTags } from './utils';

Expand Down Expand Up @@ -46,24 +40,33 @@ export class BrowserMetricsAggregator implements MetricsAggregator {
const tags = sanitizeTags(unsanitizedTags);

const bucketKey = getBucketKey(metricType, name, unit, tags);
const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey);

let bucketItem = this._buckets.get(bucketKey);
// If this is a set metric, we need to calculate the delta from the previous weight.
const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0;

if (bucketItem) {
bucketItem.metric.add(value);
// TODO(abhi): Do we need this check?
if (bucketItem.timestamp < timestamp) {
bucketItem.timestamp = timestamp;
}
} else {
this._buckets.set(bucketKey, {
bucketItem = {
// @ts-expect-error we don't need to narrow down the type of value here, saves bundle size.
metric: new METRIC_MAP[metricType](value),
timestamp,
metricType,
name,
unit,
tags,
});
};
this._buckets.set(bucketKey, bucketItem);
}

// If value is a string, it's a set metric so calculate the delta from the previous weight.
const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value;
updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey);
}

/**
Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/metrics/metric-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { MeasurementUnit, Span } from '@sentry/types';
import type { MetricSummary } from '@sentry/types';
import type { Primitive } from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';
import { getActiveSpan } from '../tracing';
import type { MetricType } from './types';

/**
* key: bucketKey
* value: [exportKey, MetricSummary]
*/
type MetricSummaryStorage = Map<string, [string, MetricSummary]>;

let SPAN_METRIC_SUMMARY: WeakMap<Span, MetricSummaryStorage> | undefined;

function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined {
return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined;
}

/**
* Fetches the metric summary if it exists for the passed span
*/
export function getMetricSummaryJsonForSpan(span: Span): Record<string, MetricSummary> | undefined {
const storage = getMetricStorageForSpan(span);

if (!storage) {
return undefined;
}
const output: Record<string, MetricSummary> = {};

for (const [, [exportKey, summary]] of storage) {
output[exportKey] = dropUndefinedKeys(summary);
}

return output;
}

/**
* 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) {
const storage = getMetricStorageForSpan(span) || new Map<string, [string, MetricSummary]>();

const exportKey = `${metricType}:${sanitizedName}@${unit}`;
const bucketItem = storage.get(bucketKey);

if (bucketItem) {
const [, summary] = bucketItem;
storage.set(bucketKey, [
exportKey,
{
min: Math.min(summary.min, value),
max: Math.max(summary.max, value),
count: (summary.count += 1),
sum: (summary.sum += value),
tags: summary.tags,
},
]);
} else {
storage.set(bucketKey, [
exportKey,
{
min: value,
max: value,
count: 1,
sum: value,
tags,
},
]);
}

if (!SPAN_METRIC_SUMMARY) {
SPAN_METRIC_SUMMARY = new WeakMap();
}

SPAN_METRIC_SUMMARY.set(span, storage);
}
}
2 changes: 2 additions & 0 deletions packages/core/src/tracing/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';

import { DEBUG_BUILD } from '../debug-build';
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import { getRootSpan } from '../utils/getRootSpan';
import {
Expand Down Expand Up @@ -624,6 +625,7 @@ export class Span implements SpanInterface {
timestamp: this._endTime,
trace_id: this._traceId,
origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
_metrics_summary: getMetricSummaryJsonForSpan(this),
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/tracing/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { dropUndefinedKeys, logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
import type { Hub } from '../hub';
import { getCurrentHub } from '../hub';
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
Expand Down Expand Up @@ -331,6 +332,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
capturedSpanIsolationScope,
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
},
_metrics_summary: getMetricSummaryJsonForSpan(this),
...(source && {
transaction_info: {
source,
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Request } from './request';
import type { CaptureContext } from './scope';
import type { SdkInfo } from './sdkinfo';
import type { Severity, SeverityLevel } from './severity';
import type { Span, SpanJSON } from './span';
import type { MetricSummary, Span, SpanJSON } from './span';
import type { Thread } from './thread';
import type { TransactionSource } from './transaction';
import type { User } from './user';
Expand Down Expand Up @@ -73,6 +73,7 @@ export interface ErrorEvent extends Event {
}
export interface TransactionEvent extends Event {
type: 'transaction';
_metrics_summary?: Record<string, MetricSummary>;
}

/** JSDoc */
Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export type {
SpanJSON,
SpanContextData,
TraceFlag,
MetricSummary,
} from './span';
export type { StackFrame } from './stackframe';
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
Expand Down Expand Up @@ -150,5 +151,9 @@ export type {

export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions';
export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin';
export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics';
export type {
MetricsAggregator,
MetricBucketItem,
MetricInstance,
} from './metrics';
export type { ParameterizedString } from './parameterize';
9 changes: 9 additions & 0 deletions packages/types/src/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export type SpanAttributes = Partial<{
}> &
Record<string, SpanAttributeValue | undefined>;

export type MetricSummary = {
min: number;
max: number;
count: number;
sum: number;
tags?: Record<string, Primitive> | undefined;
};

/** This type is aligned with the OpenTelemetry TimeInput type. */
export type SpanTimeInput = HrTime | number | Date;

Expand All @@ -47,6 +55,7 @@ export interface SpanJSON {
timestamp?: number;
trace_id: string;
origin?: SpanOrigin;
_metrics_summary?: Record<string, MetricSummary>;
}

// These are aligned with OpenTelemetry trace flags
Expand Down

0 comments on commit 94cdd8b

Please sign in to comment.