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 metric summaries to spans#10432
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
a351f919f4fdd71d5fbd88832b7025dcfe69c4f3c1286239b8b16e4dd3f52df1a485c21e8fed742aafacFile 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 |
|---|---|---|
| @@ -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': { | ||
Contributor 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. this needs to be an array of metrics, based on what tags it has. | ||
| 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); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; | ||
| @@ -62,7 +63,11 @@ export class MetricsAggregator implements MetricsAggregatorBase { | ||
| const tags = sanitizeTags(unsanitizedTags); | ||
| const bucketKey = getBucketKey(metricType, name, unit, tags); | ||
timfish marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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? | ||
| @@ -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; | ||
| 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); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.