Skip to content
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

feat(core): Add metric summaries to spans #10432

Merged
merged 12 commits into from
Feb 5, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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.startSpan(
{
name: 'Some other span',
op: 'transaction',
},
() => {
Sentry.metrics.increment('root-counter');
Sentry.metrics.increment('root-counter');
Sentry.metrics.increment('root-counter', 2);
timfish marked this conversation as resolved.
Show resolved Hide resolved
},
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createRunner } from '../../../utils/runner';

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'Some other span',
op: 'transaction',
_metrics_summary: {
'c:root-counter@undefined': {
min: 1,
max: 2,
count: 3,
sum: 4,
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);
});
56 changes: 56 additions & 0 deletions packages/core/src/metric-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { MeasurementUnit, Primitive, SpanMetricSummaryAggregator } from '@sentry/types';
import type { MetricSpanSummary } from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';

/** */
export class MetricSummaryAggregator implements SpanMetricSummaryAggregator {
private readonly _measurements: Map<string, MetricSpanSummary>;

public constructor() {
this._measurements = new Map<string, MetricSpanSummary>();
}

/** @inheritdoc */
public add(
metricType: 'c' | 'g' | 's' | 'd',
name: string,
value: number,
unit?: MeasurementUnit | undefined,
tags?: Record<string, Primitive> | undefined,
): void {
const exportKey = `${metricType}:${name}@${unit}`;
const bucketKey = `${exportKey}\n${JSON.stringify(tags)}`;

const summary = this._measurements.get(bucketKey);

if (summary) {
timfish marked this conversation as resolved.
Show resolved Hide resolved
this._measurements.set(bucketKey, {
min: Math.min(summary.min, value),
max: Math.max(summary.max, value),
count: (summary.count += 1),
sum: (summary.sum += value),
tags: summary.tags,
});
} else {
timfish marked this conversation as resolved.
Show resolved Hide resolved
this._measurements.set(bucketKey, {
min: value,
max: value,
count: 1,
sum: value,
tags: tags,
});
}
}

/** @inheritdoc */
public getSummaryJson(): Record<string, MetricSpanSummary> {
const output: Record<string, MetricSpanSummary> = {};

for (const [bucketKey, summary] of this._measurements) {
const [exportKey] = bucketKey.split('\n');
output[exportKey] = dropUndefinedKeys(summary);
}

return output;
}
}
10 changes: 9 additions & 1 deletion packages/core/src/metrics/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,16 @@ function addToMetricsAggregator(
metricTags.transaction = spanToJSON(transaction).description || '';
}

const combinedTags = { ...metricTags, ...tags };

// eslint-disable-next-line deprecation/deprecation
const span = scope.getSpan();
if (span && typeof value === 'number') {
span.getMetricSummaryAggregator().add(metricType, name, value, unit, combinedTags);
}

DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`);
client.metricsAggregator.add(metricType, name, value, unit, { ...metricTags, ...tags }, timestamp);
client.metricsAggregator.add(metricType, name, value, unit, combinedTags, timestamp);
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/tracing/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SpanContext,
SpanContextData,
SpanJSON,
SpanMetricSummaryAggregator,
SpanOrigin,
SpanTimeInput,
TraceContext,
Expand All @@ -16,6 +17,7 @@ import type {
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';

import { DEBUG_BUILD } from '../debug-build';
import { MetricSummaryAggregator } from '../metric-summary';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import { getRootSpan } from '../utils/getRootSpan';
import {
Expand Down Expand Up @@ -114,6 +116,7 @@ export class Span implements SpanInterface {
protected _endTime?: number;
/** Internal keeper of the status */
protected _status?: SpanStatusType | string;
protected _metricSummary: MetricSummaryAggregator | undefined;

private _logMessage?: string;

Expand Down Expand Up @@ -602,6 +605,15 @@ export class Span implements SpanInterface {
return spanToTraceContext(this);
}

/** @inheritdoc */
public getMetricSummaryAggregator(): SpanMetricSummaryAggregator {
if (!this._metricSummary) {
this._metricSummary = new MetricSummaryAggregator();
}

return this._metricSummary;
}

/**
* Get JSON representation of this span.
*
Expand All @@ -624,6 +636,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: this._metricSummary ? this._metricSummary.getSummaryJson() : undefined,
});
}

Expand Down
3 changes: 2 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 as MetricSpanSummary,
} from './span';
export type { StackFrame } from './stackframe';
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
Expand Down Expand Up @@ -150,5 +151,5 @@ 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, SpanMetricSummaryAggregator } from './metrics';
export type { ParameterizedString } from './parameterize';
15 changes: 15 additions & 0 deletions packages/types/src/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MeasurementUnit } from './measurement';
import type { Primitive } from './misc';
import type { MetricSummary } from './span';

/**
* An abstract definition of the minimum required API
Expand Down Expand Up @@ -62,3 +63,17 @@ export interface MetricsAggregator {
*/
toString(): string;
}

export interface SpanMetricSummaryAggregator {
/** Adds a metric to the summary */
add(
metricType: 'c' | 'g' | 's' | 'd',
name: string,
value: number | string,
unit?: MeasurementUnit,
tags?: Record<string, Primitive>,
): void;

/** Gets the JSON representation of the metric summary */
getSummaryJson(): Record<string, MetricSummary>;
}
15 changes: 15 additions & 0 deletions packages/types/src/span.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TraceContext } from './context';
import type { Instrumenter } from './instrumenter';
import type { SpanMetricSummaryAggregator } from './metrics';
import type { Primitive } from './misc';
import type { HrTime } from './opentelemetry';
import type { Transaction } from './transaction';
Expand Down Expand Up @@ -31,6 +32,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 +56,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 Expand Up @@ -387,6 +397,11 @@ export interface Span extends Omit<SpanContext, 'op' | 'status' | 'origin'> {
*/
getTraceContext(): TraceContext;

/**
* Gets the metric summary aggregator for this span
*/
getMetricSummaryAggregator(): SpanMetricSummaryAggregator;

/**
* Convert the object to JSON.
* @deprecated Use `spanToJSON(span)` instead.
Expand Down
Loading