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

🚧 WIP - feat(metrics): Introduce metrics aggregator #9699

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions packages/core/src/metrics/aggregator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { CounterMetric, DistributionMetric, GaugeMetric, MeasurementUnit, Metric, SetMetric } from '@sentry/types';
import { timestampInSeconds } from '@sentry/utils';

const COUNTER_METRIC_TYPE = 'c';
const GAUGE_METRIC_TYPE = 'g';
const SET_METRIC_TYPE = 's';
const DISTRIBUTION_METRIC_TYPE = 'd';

export type MetricType =
| typeof COUNTER_METRIC_TYPE
| typeof GAUGE_METRIC_TYPE
| typeof SET_METRIC_TYPE
| typeof DISTRIBUTION_METRIC_TYPE;

const DEFAULT_FLUSH_TIMEOUT_IN_MS = 5000;

/**
* A simple metrics aggregator that aggregates metrics in memory and flushes them periodically.
*/
export class SimpleMetricsAggregator {
private _buckets: Map<string, Metric>;
private _intervalId: ReturnType<typeof setInterval>;

public constructor() {
this._buckets = new Map();

this._intervalId = setInterval(() => this.flush(), DEFAULT_FLUSH_TIMEOUT_IN_MS);
}

/** JSDoc */
public add(
metricType: MetricType,
name: string,
value: number,
unit: MeasurementUnit = 'none',
tags: Metric['tags'] = {},
): void {
// In order to generate a stable unique key for the bucket, we need ensure tag key order is consistent, hence the sorting
const stringifiedTags = JSON.stringify(Object.keys(tags).sort());
const bucketKey = [metricType, name, value].concat(stringifiedTags).join('');

const maybeMetric = this._buckets.get(bucketKey);
if (maybeMetric) {
addMetric[metricType](maybeMetric, value);
} else {
createMetric[metricType](bucketKey, name, value, unit, tags);
}

if (!this._metrics.has(bucketKey)) {
this._metrics.set(bucketKey, {
name,
type: COUNTER_METRIC_TYPE,
value,
unit,
tags,
timestamp,
});
} else {
const metric = this._metrics.get(bucketKey);
metric.value += value;
metric.timestamp = timestamp;
}
}

/**
* Flushes metrics from buckets and captures them using client
*/
public flush(): void {
// TODO
}

/** JSDoc */
public close(): void {
clearInterval(this._intervalId);
this.flush();
}
}

function addCounterMetric(metric: CounterMetric, value: number): void {
metric.value += value;
}

function addGaugeMetric(metric: GaugeMetric, value: number): void {
metric.value = value;
metric.last = value;
metric.min = Math.min(metric.min, value);
metric.max = Math.max(metric.max, value);
metric.sum += value;
metric.count += 1;
}

function addSetMetric(metric: SetMetric, value: number): void {
metric.value.add(value);
}

function addDistributionMetric(metric: DistributionMetric, value: number): void {
metric.value.push(value);
}

function createCounterMetric(name: string, value: number, unit: MeasurementUnit, tags: Metric['tags']): CounterMetric {
return {
name,
value,
unit,
timestamp: timestampInSeconds(),
tags,
};
}

function createGaugeMetric(name: string, value: number, unit: MeasurementUnit, tags: Metric['tags']): GaugeMetric {
return {
name,
value,
unit,
timestamp: timestampInSeconds(),
tags,
};
}
45 changes: 45 additions & 0 deletions packages/core/src/metrics/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { DsnComponents, DynamicSamplingContext, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types'
import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils';

/**
* Create envelope from a metric aggregate.
*
* @experimental This function may experience breaking changes.
*/
export function createMetricEnvelope(
// TODO(abhi): Add type for this
metricAggregate: string,
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
metadata?: SdkMetadata,
tunnel?: string,
dsn?: DsnComponents,
): StatsdEnvelope {
const headers: StatsdEnvelope[0] = {
sent_at: new Date().toISOString(),
};

if (metadata && metadata.sdk) {
headers.sdk = {
name: metadata.sdk.name,
version: metadata.sdk.version,
};
}

if (!!tunnel && !!dsn) {
headers.dsn = dsnToString(dsn);
}

if (dynamicSamplingContext) {
headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext;
}

const item = createMetricEnvelopeItem(metricAggregate);
return createEnvelope<StatsdEnvelope>(headers, [item]);
}

function createMetricEnvelopeItem(metricAggregate: string): StatsdItem {
const metricHeaders: StatsdItem[0] = {
type: 'statsd',
};
return [metricHeaders, metricAggregate];
}
78 changes: 78 additions & 0 deletions packages/core/src/metrics/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { CounterMetric, DistributionMetric, GaugeMetric, SetMetric } from '@sentry/types';

import { timestampInSeconds } from '@sentry/utils';
import type { MetricData } from './types';

/**
* Increment a counter by a specified value.
*
* @experimental This function may experience breaking changes.
*/
export function increment(name: string, value: number, data: MetricData): void {
// TODO: Implement
const _metric: CounterMetric = {
name,
timestamp: timestampInSeconds(),
value,
...data,
};

// @ts-expect-error TODO: Implement
return _metric;
}

/**
* Add to distribution by a specified value.
*
* @experimental This function may experience breaking changes.
*/
export function distribution(name: string, value: number, data: MetricData): void {
const _metric: DistributionMetric = {
name,
timestamp: timestampInSeconds(),
value: [value],
...data,
};

// @ts-expect-error TODO: Implement
return _metric;
}

/**
* Add to set by a specified value.
*
* @experimental This function may experience breaking changes.
*/
export function set(name: string, value: number, data: MetricData): void {
const _metric: SetMetric = {
name,
timestamp: timestampInSeconds(),
value: new Set([value]),
...data,
};

// @ts-expect-error TODO: Implement
return _metric;
}

/**
* Set a gauge by a specified value.
*
* @experimental This function may experience breaking changes.
*/
export function gauge(name: string, value: number, data: MetricData): void {
const _metric: GaugeMetric = {
name,
timestamp: timestampInSeconds(),
value,
first: value,
min: value,
max: value,
sum: value,
count: 1,
...data,
};

// @ts-expect-error TODO: Implement
return _metric;
}
44 changes: 2 additions & 42 deletions packages/core/src/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,3 @@
import type { DsnComponents, DynamicSamplingContext, SdkMetadata, StatsdEnvelope, StatsdItem } from '@sentry/types';
import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils';
export { createMetricEnvelope } from './envelope';

/**
* Create envelope from a metric aggregate.
*/
export function createMetricEnvelope(
// TODO(abhi): Add type for this
metricAggregate: string,
dynamicSamplingContext?: Partial<DynamicSamplingContext>,
metadata?: SdkMetadata,
tunnel?: string,
dsn?: DsnComponents,
): StatsdEnvelope {
const headers: StatsdEnvelope[0] = {
sent_at: new Date().toISOString(),
};

if (metadata && metadata.sdk) {
headers.sdk = {
name: metadata.sdk.name,
version: metadata.sdk.version,
};
}

if (!!tunnel && !!dsn) {
headers.dsn = dsnToString(dsn);
}

if (dynamicSamplingContext) {
headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext;
}

const item = createMetricEnvelopeItem(metricAggregate);
return createEnvelope<StatsdEnvelope>(headers, [item]);
}

function createMetricEnvelopeItem(metricAggregate: string): StatsdItem {
const metricHeaders: StatsdItem[0] = {
type: 'statsd',
};
return [metricHeaders, metricAggregate];
}
// export function
6 changes: 6 additions & 0 deletions packages/core/src/metrics/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Metric } from '@sentry/types';

export interface MetricData {
tags?: Metric['tags'];
unit?: Metric['unit'];
}
1 change: 1 addition & 0 deletions packages/types/src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface CounterMetric extends BaseMetric {
export interface GaugeMetric extends BaseMetric {
value: number;
first: number;
last: number;
min: number;
max: number;
sum: number;
Expand Down
Loading