Skip to content

Feat/metrics span summary p4 #1958

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

Merged
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
37 changes: 37 additions & 0 deletions dart/lib/src/metrics/local_metrics_aggregator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:core';
import 'package:meta/meta.dart';
import '../protocol/metric_summary.dart';
import 'metric.dart';

@internal
class LocalMetricsAggregator {
// format: <export key, <metric key, gauge>>
final Map<String, Map<String, GaugeMetric>> _buckets = {};

void add(final Metric metric, final num value) {
final bucket =
_buckets.putIfAbsent(metric.getSpanAggregationKey(), () => {});

bucket.update(metric.getCompositeKey(), (m) => m..add(value),
ifAbsent: () => Metric.fromType(
type: MetricType.gauge,
key: metric.key,
value: value,
unit: metric.unit,
tags: metric.tags) as GaugeMetric);
}

Map<String, List<MetricSummary>> getSummaries() {
final Map<String, List<MetricSummary>> summaries = {};
for (final entry in _buckets.entries) {
final String exportKey = entry.key;

final metricSummaries = entry.value.values
.map((gauge) => MetricSummary.fromGauge(gauge))
.toList();

summaries[exportKey] = metricSummaries;
}
return summaries;
}
}
8 changes: 4 additions & 4 deletions dart/lib/src/metrics/metric.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ abstract class Metric {
return ('${type.statsdType}_${key}_${unit.name}_$serializedTags');
}

/// Return a key created by [key], [type] and [unit].
/// This key should be used to aggregate the metric locally in a span.
String getSpanAggregationKey() => '${type.statsdType}:$key@${unit.name}';

/// Remove forbidden characters from the metric key and tag key.
String _normalizeKey(String input) =>
input.replaceAll(forbiddenKeyCharsRegex, '_');
Expand Down Expand Up @@ -186,13 +190,9 @@ class GaugeMetric extends Metric {

@visibleForTesting
num get last => _last;
@visibleForTesting
num get minimum => _minimum;
@visibleForTesting
num get maximum => _maximum;
@visibleForTesting
num get sum => _sum;
@visibleForTesting
int get count => _count;
}

Expand Down
7 changes: 7 additions & 0 deletions dart/lib/src/metrics/metrics_aggregator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ class MetricsAggregator {
ifAbsent: () => metric,
);

// For sets, we only record that a value has been added to the set but not which one.
// See develop docs: https://develop.sentry.dev/sdk/metrics/#sets
_hub
.getSpan()
?.localMetricsAggregator
?.add(metric, metricType == MetricType.set ? addedWeight : value);

// Schedule the metrics flushing.
_scheduleFlush();
}
Expand Down
4 changes: 4 additions & 0 deletions dart/lib/src/noop_sentry_span.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'metrics/local_metrics_aggregator.dart';
import 'protocol.dart';
import 'tracing.dart';
import 'utils.dart';
Expand Down Expand Up @@ -95,4 +96,7 @@ class NoOpSentrySpan extends ISentrySpan {

@override
void scheduleFinish() {}

@override
LocalMetricsAggregator? get localMetricsAggregator => null;
}
1 change: 1 addition & 0 deletions dart/lib/src/protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'protocol/sentry_device.dart';
export 'protocol/dsn.dart';
export 'protocol/sentry_gpu.dart';
export 'protocol/mechanism.dart';
export 'protocol/metric_summary.dart';
export 'protocol/sentry_message.dart';
export 'protocol/sentry_operating_system.dart';
export 'protocol/sentry_request.dart';
Expand Down
43 changes: 43 additions & 0 deletions dart/lib/src/protocol/metric_summary.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import '../metrics/metric.dart';

class MetricSummary {
final num min;
final num max;
final num sum;
final int count;
final Map<String, String>? tags;

MetricSummary.fromGauge(GaugeMetric gauge)
: min = gauge.minimum,
max = gauge.maximum,
sum = gauge.sum,
count = gauge.count,
tags = gauge.tags;

const MetricSummary(
{required this.min,
required this.max,
required this.sum,
required this.count,
required this.tags});

/// Deserializes a [MetricSummary] from JSON [Map].
factory MetricSummary.fromJson(Map<String, dynamic> data) => MetricSummary(
min: data['min'],
max: data['max'],
count: data['count'],
sum: data['sum'],
tags: data['tags']?.cast<String, String>(),
);

/// Produces a [Map] that can be serialized to JSON.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'min': min,
'max': max,
'count': count,
'sum': sum,
if (tags?.isNotEmpty ?? false) 'tags': tags,
};
}
}
20 changes: 20 additions & 0 deletions dart/lib/src/protocol/sentry_span.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import '../hub.dart';
import '../metrics/local_metrics_aggregator.dart';
import '../protocol.dart';

import '../sentry_tracer.dart';
Expand All @@ -12,6 +13,7 @@ typedef OnFinishedCallback = Future<void> Function({DateTime? endTimestamp});
class SentrySpan extends ISentrySpan {
final SentrySpanContext _context;
DateTime? _endTimestamp;
Map<String, List<MetricSummary>>? _metricSummaries;
late final DateTime _startTimestamp;
final Hub _hub;

Expand All @@ -22,6 +24,7 @@ class SentrySpan extends ISentrySpan {
SpanStatus? _status;
final Map<String, String> _tags = {};
OnFinishedCallback? _finishedCallback;
late final LocalMetricsAggregator? _localMetricsAggregator;

@override
final SentryTracesSamplingDecision? samplingDecision;
Expand All @@ -37,6 +40,9 @@ class SentrySpan extends ISentrySpan {
_startTimestamp = startTimestamp?.toUtc() ?? _hub.options.clock();
_finishedCallback = finishedCallback;
_origin = _context.origin;
_localMetricsAggregator = _hub.options.enableSpanLocalMetricAggregation
? LocalMetricsAggregator()
: null;
}

@override
Expand Down Expand Up @@ -65,6 +71,7 @@ class SentrySpan extends ISentrySpan {
if (_throwable != null) {
_hub.setSpanContext(_throwable, this, _tracer.name);
}
_metricSummaries = _localMetricsAggregator?.getSummaries();
await _finishedCallback?.call(endTimestamp: _endTimestamp);
return super.finish(status: status, endTimestamp: _endTimestamp);
}
Expand Down Expand Up @@ -154,6 +161,9 @@ class SentrySpan extends ISentrySpan {
@override
set origin(String? origin) => _origin = origin;

@override
LocalMetricsAggregator? get localMetricsAggregator => _localMetricsAggregator;

Map<String, dynamic> toJson() {
final json = _context.toJson();
json['start_timestamp'] =
Expand All @@ -174,6 +184,16 @@ class SentrySpan extends ISentrySpan {
if (_origin != null) {
json['origin'] = _origin;
}

final metricSummariesMap = _metricSummaries?.entries ?? Iterable.empty();
if (metricSummariesMap.isNotEmpty) {
final map = <String, dynamic>{};
for (final entry in metricSummariesMap) {
final summary = entry.value.map((e) => e.toJson());
map[entry.key] = summary.toList(growable: false);
}
json['_metrics_summary'] = map;
}
return json;
}

Expand Down
18 changes: 18 additions & 0 deletions dart/lib/src/protocol/sentry_transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class SentryTransaction extends SentryEvent {
@internal
final SentryTracer tracer;
late final Map<String, SentryMeasurement> measurements;
late final Map<String, List<MetricSummary>>? metricSummaries;
late final SentryTransactionInfo? transactionInfo;

SentryTransaction(
Expand All @@ -37,6 +38,7 @@ class SentryTransaction extends SentryEvent {
super.request,
String? type,
Map<String, SentryMeasurement>? measurements,
Map<String, List<MetricSummary>>? metricSummaries,
SentryTransactionInfo? transactionInfo,
}) : super(
timestamp: timestamp ?? tracer.endTimestamp,
Expand All @@ -52,6 +54,8 @@ class SentryTransaction extends SentryEvent {
final spanContext = tracer.context;
spans = tracer.children;
this.measurements = measurements ?? {};
this.metricSummaries =
metricSummaries ?? tracer.localMetricsAggregator?.getSummaries();

contexts.trace = spanContext.toTraceContext(
sampled: tracer.samplingDecision?.sampled,
Expand Down Expand Up @@ -85,6 +89,16 @@ class SentryTransaction extends SentryEvent {
json['transaction_info'] = transactionInfo.toJson();
}

final metricSummariesMap = metricSummaries?.entries ?? Iterable.empty();
if (metricSummariesMap.isNotEmpty) {
final map = <String, dynamic>{};
for (final entry in metricSummariesMap) {
final summary = entry.value.map((e) => e.toJson());
map[entry.key] = summary.toList(growable: false);
}
json['_metrics_summary'] = map;
}

return json;
}

Expand Down Expand Up @@ -123,6 +137,7 @@ class SentryTransaction extends SentryEvent {
List<SentryThread>? threads,
String? type,
Map<String, SentryMeasurement>? measurements,
Map<String, List<MetricSummary>>? metricSummaries,
SentryTransactionInfo? transactionInfo,
}) =>
SentryTransaction(
Expand All @@ -148,6 +163,9 @@ class SentryTransaction extends SentryEvent {
type: type ?? this.type,
measurements: (measurements != null ? Map.from(measurements) : null) ??
this.measurements,
metricSummaries:
(metricSummaries != null ? Map.from(metricSummaries) : null) ??
this.metricSummaries,
transactionInfo: transactionInfo ?? this.transactionInfo,
);
}
18 changes: 17 additions & 1 deletion dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ class SentryOptions {
bool enableMetrics = false;

@experimental
bool _enableDefaultTagsForMetrics = false;
bool _enableDefaultTagsForMetrics = true;

/// Enables enriching metrics with default tags. Requires [enableMetrics].
/// More on https://develop.sentry.dev/delightful-developer-metrics/sending-metrics-sdk/#automatic-tags-extraction
Expand All @@ -410,6 +410,22 @@ class SentryOptions {
set enableDefaultTagsForMetrics(final bool enableDefaultTagsForMetrics) =>
_enableDefaultTagsForMetrics = enableDefaultTagsForMetrics;

@experimental
bool _enableSpanLocalMetricAggregation = true;

/// Enables span metrics aggregation. Requires [enableMetrics].
/// More on https://develop.sentry.dev/sdk/metrics/#span-aggregation
@experimental
bool get enableSpanLocalMetricAggregation =>
enableMetrics && _enableSpanLocalMetricAggregation;

/// Enables span metrics aggregation. Requires [enableMetrics].
/// More on https://develop.sentry.dev/sdk/metrics/#span-aggregation
@experimental
set enableSpanLocalMetricAggregation(
final bool enableSpanLocalMetricAggregation) =>
_enableSpanLocalMetricAggregation = enableSpanLocalMetricAggregation;

/// Only for internal use. Changed SDK behaviour when set to true:
/// - Rethrow exceptions that occur in user provided closures
@internal
Expand Down
4 changes: 4 additions & 0 deletions dart/lib/src/sentry_span_interface.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:meta/meta.dart';

import 'metrics/local_metrics_aggregator.dart';
import 'protocol.dart';
import 'tracing.dart';

Expand Down Expand Up @@ -46,6 +47,9 @@ abstract class ISentrySpan {
/// See https://develop.sentry.dev/sdk/performance/trace-origin
set origin(String? origin);

@internal
LocalMetricsAggregator? get localMetricsAggregator;

/// Returns the end timestamp if finished
DateTime? get endTimestamp;

Expand Down
5 changes: 5 additions & 0 deletions dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:meta/meta.dart';

import '../sentry.dart';
import 'metrics/local_metrics_aggregator.dart';
import 'profiling.dart';
import 'sentry_tracer_finish_status.dart';
import 'utils/sample_rate_format.dart';
Expand Down Expand Up @@ -413,4 +414,8 @@ class SentryTracer extends ISentrySpan {
});
}
}

@override
LocalMetricsAggregator? get localMetricsAggregator =>
_rootSpan.localMetricsAggregator;
}
40 changes: 40 additions & 0 deletions dart/test/metrics/local_metrics_aggregator_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:sentry/src/metrics/local_metrics_aggregator.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';

import '../mocks.dart';

void main() {
group('add', () {
late LocalMetricsAggregator aggregator;

setUp(() {
aggregator = LocalMetricsAggregator();
});

test('same metric multiple times aggregates them', () async {
aggregator.add(fakeMetric, 1);
aggregator.add(fakeMetric, 2);
final summaries = aggregator.getSummaries();
expect(summaries.length, 1);
final summary = summaries.values.first;
expect(summary.length, 1);
});

test('same metric different tags aggregates summary bucket', () async {
aggregator.add(fakeMetric, 1);
aggregator.add(fakeMetric..tags.clear(), 2);
final summaries = aggregator.getSummaries();
expect(summaries.length, 1);
final summary = summaries.values.first;
expect(summary.length, 2);
});

test('different metrics does not aggregate them', () async {
aggregator.add(fakeMetric, 1);
aggregator.add(fakeMetric2, 2);
final summaries = aggregator.getSummaries();
expect(summaries.length, 2);
});
});
}
Loading
Loading