Skip to content

Commit 3c41335

Browse files
authored
Feat/metrics span summary p4 (#1958)
* added local_metrics_aggregator.dart to spans * metrics_aggregator.dart now adds to current span's localMetricsAggregator * added metric_summary.dart * added metricSummary to spans and transaction JSONs
1 parent 3c482e7 commit 3c41335

20 files changed

+479
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'dart:core';
2+
import 'package:meta/meta.dart';
3+
import '../protocol/metric_summary.dart';
4+
import 'metric.dart';
5+
6+
@internal
7+
class LocalMetricsAggregator {
8+
// format: <export key, <metric key, gauge>>
9+
final Map<String, Map<String, GaugeMetric>> _buckets = {};
10+
11+
void add(final Metric metric, final num value) {
12+
final bucket =
13+
_buckets.putIfAbsent(metric.getSpanAggregationKey(), () => {});
14+
15+
bucket.update(metric.getCompositeKey(), (m) => m..add(value),
16+
ifAbsent: () => Metric.fromType(
17+
type: MetricType.gauge,
18+
key: metric.key,
19+
value: value,
20+
unit: metric.unit,
21+
tags: metric.tags) as GaugeMetric);
22+
}
23+
24+
Map<String, List<MetricSummary>> getSummaries() {
25+
final Map<String, List<MetricSummary>> summaries = {};
26+
for (final entry in _buckets.entries) {
27+
final String exportKey = entry.key;
28+
29+
final metricSummaries = entry.value.values
30+
.map((gauge) => MetricSummary.fromGauge(gauge))
31+
.toList();
32+
33+
summaries[exportKey] = metricSummaries;
34+
}
35+
return summaries;
36+
}
37+
}

dart/lib/src/metrics/metric.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ abstract class Metric {
112112
return ('${type.statsdType}_${key}_${unit.name}_$serializedTags');
113113
}
114114

115+
/// Return a key created by [key], [type] and [unit].
116+
/// This key should be used to aggregate the metric locally in a span.
117+
String getSpanAggregationKey() => '${type.statsdType}:$key@${unit.name}';
118+
115119
/// Remove forbidden characters from the metric key and tag key.
116120
String _normalizeKey(String input) =>
117121
input.replaceAll(forbiddenKeyCharsRegex, '_');
@@ -186,13 +190,9 @@ class GaugeMetric extends Metric {
186190

187191
@visibleForTesting
188192
num get last => _last;
189-
@visibleForTesting
190193
num get minimum => _minimum;
191-
@visibleForTesting
192194
num get maximum => _maximum;
193-
@visibleForTesting
194195
num get sum => _sum;
195-
@visibleForTesting
196196
int get count => _count;
197197
}
198198

dart/lib/src/metrics/metrics_aggregator.dart

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ class MetricsAggregator {
100100
ifAbsent: () => metric,
101101
);
102102

103+
// For sets, we only record that a value has been added to the set but not which one.
104+
// See develop docs: https://develop.sentry.dev/sdk/metrics/#sets
105+
_hub
106+
.getSpan()
107+
?.localMetricsAggregator
108+
?.add(metric, metricType == MetricType.set ? addedWeight : value);
109+
103110
// Schedule the metrics flushing.
104111
_scheduleFlush();
105112
}

dart/lib/src/noop_sentry_span.dart

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'metrics/local_metrics_aggregator.dart';
12
import 'protocol.dart';
23
import 'tracing.dart';
34
import 'utils.dart';
@@ -95,4 +96,7 @@ class NoOpSentrySpan extends ISentrySpan {
9596

9697
@override
9798
void scheduleFinish() {}
99+
100+
@override
101+
LocalMetricsAggregator? get localMetricsAggregator => null;
98102
}

dart/lib/src/protocol.dart

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export 'protocol/sentry_device.dart';
88
export 'protocol/dsn.dart';
99
export 'protocol/sentry_gpu.dart';
1010
export 'protocol/mechanism.dart';
11+
export 'protocol/metric_summary.dart';
1112
export 'protocol/sentry_message.dart';
1213
export 'protocol/sentry_operating_system.dart';
1314
export 'protocol/sentry_request.dart';
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import '../metrics/metric.dart';
2+
3+
class MetricSummary {
4+
final num min;
5+
final num max;
6+
final num sum;
7+
final int count;
8+
final Map<String, String>? tags;
9+
10+
MetricSummary.fromGauge(GaugeMetric gauge)
11+
: min = gauge.minimum,
12+
max = gauge.maximum,
13+
sum = gauge.sum,
14+
count = gauge.count,
15+
tags = gauge.tags;
16+
17+
const MetricSummary(
18+
{required this.min,
19+
required this.max,
20+
required this.sum,
21+
required this.count,
22+
required this.tags});
23+
24+
/// Deserializes a [MetricSummary] from JSON [Map].
25+
factory MetricSummary.fromJson(Map<String, dynamic> data) => MetricSummary(
26+
min: data['min'],
27+
max: data['max'],
28+
count: data['count'],
29+
sum: data['sum'],
30+
tags: data['tags']?.cast<String, String>(),
31+
);
32+
33+
/// Produces a [Map] that can be serialized to JSON.
34+
Map<String, dynamic> toJson() {
35+
return <String, dynamic>{
36+
'min': min,
37+
'max': max,
38+
'count': count,
39+
'sum': sum,
40+
if (tags?.isNotEmpty ?? false) 'tags': tags,
41+
};
42+
}
43+
}

dart/lib/src/protocol/sentry_span.dart

+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import '../hub.dart';
4+
import '../metrics/local_metrics_aggregator.dart';
45
import '../protocol.dart';
56

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

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

2629
@override
2730
final SentryTracesSamplingDecision? samplingDecision;
@@ -37,6 +40,9 @@ class SentrySpan extends ISentrySpan {
3740
_startTimestamp = startTimestamp?.toUtc() ?? _hub.options.clock();
3841
_finishedCallback = finishedCallback;
3942
_origin = _context.origin;
43+
_localMetricsAggregator = _hub.options.enableSpanLocalMetricAggregation
44+
? LocalMetricsAggregator()
45+
: null;
4046
}
4147

4248
@override
@@ -65,6 +71,7 @@ class SentrySpan extends ISentrySpan {
6571
if (_throwable != null) {
6672
_hub.setSpanContext(_throwable, this, _tracer.name);
6773
}
74+
_metricSummaries = _localMetricsAggregator?.getSummaries();
6875
await _finishedCallback?.call(endTimestamp: _endTimestamp);
6976
return super.finish(status: status, endTimestamp: _endTimestamp);
7077
}
@@ -154,6 +161,9 @@ class SentrySpan extends ISentrySpan {
154161
@override
155162
set origin(String? origin) => _origin = origin;
156163

164+
@override
165+
LocalMetricsAggregator? get localMetricsAggregator => _localMetricsAggregator;
166+
157167
Map<String, dynamic> toJson() {
158168
final json = _context.toJson();
159169
json['start_timestamp'] =
@@ -174,6 +184,16 @@ class SentrySpan extends ISentrySpan {
174184
if (_origin != null) {
175185
json['origin'] = _origin;
176186
}
187+
188+
final metricSummariesMap = _metricSummaries?.entries ?? Iterable.empty();
189+
if (metricSummariesMap.isNotEmpty) {
190+
final map = <String, dynamic>{};
191+
for (final entry in metricSummariesMap) {
192+
final summary = entry.value.map((e) => e.toJson());
193+
map[entry.key] = summary.toList(growable: false);
194+
}
195+
json['_metrics_summary'] = map;
196+
}
177197
return json;
178198
}
179199

dart/lib/src/protocol/sentry_transaction.dart

+18
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class SentryTransaction extends SentryEvent {
1313
@internal
1414
final SentryTracer tracer;
1515
late final Map<String, SentryMeasurement> measurements;
16+
late final Map<String, List<MetricSummary>>? metricSummaries;
1617
late final SentryTransactionInfo? transactionInfo;
1718

1819
SentryTransaction(
@@ -37,6 +38,7 @@ class SentryTransaction extends SentryEvent {
3738
super.request,
3839
String? type,
3940
Map<String, SentryMeasurement>? measurements,
41+
Map<String, List<MetricSummary>>? metricSummaries,
4042
SentryTransactionInfo? transactionInfo,
4143
}) : super(
4244
timestamp: timestamp ?? tracer.endTimestamp,
@@ -52,6 +54,8 @@ class SentryTransaction extends SentryEvent {
5254
final spanContext = tracer.context;
5355
spans = tracer.children;
5456
this.measurements = measurements ?? {};
57+
this.metricSummaries =
58+
metricSummaries ?? tracer.localMetricsAggregator?.getSummaries();
5559

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

92+
final metricSummariesMap = metricSummaries?.entries ?? Iterable.empty();
93+
if (metricSummariesMap.isNotEmpty) {
94+
final map = <String, dynamic>{};
95+
for (final entry in metricSummariesMap) {
96+
final summary = entry.value.map((e) => e.toJson());
97+
map[entry.key] = summary.toList(growable: false);
98+
}
99+
json['_metrics_summary'] = map;
100+
}
101+
88102
return json;
89103
}
90104

@@ -123,6 +137,7 @@ class SentryTransaction extends SentryEvent {
123137
List<SentryThread>? threads,
124138
String? type,
125139
Map<String, SentryMeasurement>? measurements,
140+
Map<String, List<MetricSummary>>? metricSummaries,
126141
SentryTransactionInfo? transactionInfo,
127142
}) =>
128143
SentryTransaction(
@@ -148,6 +163,9 @@ class SentryTransaction extends SentryEvent {
148163
type: type ?? this.type,
149164
measurements: (measurements != null ? Map.from(measurements) : null) ??
150165
this.measurements,
166+
metricSummaries:
167+
(metricSummaries != null ? Map.from(metricSummaries) : null) ??
168+
this.metricSummaries,
151169
transactionInfo: transactionInfo ?? this.transactionInfo,
152170
);
153171
}

dart/lib/src/sentry_options.dart

+17-1
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ class SentryOptions {
394394
bool enableMetrics = false;
395395

396396
@experimental
397-
bool _enableDefaultTagsForMetrics = false;
397+
bool _enableDefaultTagsForMetrics = true;
398398

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

413+
@experimental
414+
bool _enableSpanLocalMetricAggregation = true;
415+
416+
/// Enables span metrics aggregation. Requires [enableMetrics].
417+
/// More on https://develop.sentry.dev/sdk/metrics/#span-aggregation
418+
@experimental
419+
bool get enableSpanLocalMetricAggregation =>
420+
enableMetrics && _enableSpanLocalMetricAggregation;
421+
422+
/// Enables span metrics aggregation. Requires [enableMetrics].
423+
/// More on https://develop.sentry.dev/sdk/metrics/#span-aggregation
424+
@experimental
425+
set enableSpanLocalMetricAggregation(
426+
final bool enableSpanLocalMetricAggregation) =>
427+
_enableSpanLocalMetricAggregation = enableSpanLocalMetricAggregation;
428+
413429
/// Only for internal use. Changed SDK behaviour when set to true:
414430
/// - Rethrow exceptions that occur in user provided closures
415431
@internal

dart/lib/src/sentry_span_interface.dart

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:meta/meta.dart';
22

3+
import 'metrics/local_metrics_aggregator.dart';
34
import 'protocol.dart';
45
import 'tracing.dart';
56

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

50+
@internal
51+
LocalMetricsAggregator? get localMetricsAggregator;
52+
4953
/// Returns the end timestamp if finished
5054
DateTime? get endTimestamp;
5155

dart/lib/src/sentry_tracer.dart

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:meta/meta.dart';
44

55
import '../sentry.dart';
6+
import 'metrics/local_metrics_aggregator.dart';
67
import 'profiling.dart';
78
import 'sentry_tracer_finish_status.dart';
89
import 'utils/sample_rate_format.dart';
@@ -413,4 +414,8 @@ class SentryTracer extends ISentrySpan {
413414
});
414415
}
415416
}
417+
418+
@override
419+
LocalMetricsAggregator? get localMetricsAggregator =>
420+
_rootSpan.localMetricsAggregator;
416421
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:sentry/src/metrics/local_metrics_aggregator.dart';
2+
import 'package:test/expect.dart';
3+
import 'package:test/scaffolding.dart';
4+
5+
import '../mocks.dart';
6+
7+
void main() {
8+
group('add', () {
9+
late LocalMetricsAggregator aggregator;
10+
11+
setUp(() {
12+
aggregator = LocalMetricsAggregator();
13+
});
14+
15+
test('same metric multiple times aggregates them', () async {
16+
aggregator.add(fakeMetric, 1);
17+
aggregator.add(fakeMetric, 2);
18+
final summaries = aggregator.getSummaries();
19+
expect(summaries.length, 1);
20+
final summary = summaries.values.first;
21+
expect(summary.length, 1);
22+
});
23+
24+
test('same metric different tags aggregates summary bucket', () async {
25+
aggregator.add(fakeMetric, 1);
26+
aggregator.add(fakeMetric..tags.clear(), 2);
27+
final summaries = aggregator.getSummaries();
28+
expect(summaries.length, 1);
29+
final summary = summaries.values.first;
30+
expect(summary.length, 2);
31+
});
32+
33+
test('different metrics does not aggregate them', () async {
34+
aggregator.add(fakeMetric, 1);
35+
aggregator.add(fakeMetric2, 2);
36+
final summaries = aggregator.getSummaries();
37+
expect(summaries.length, 2);
38+
});
39+
});
40+
}

0 commit comments

Comments
 (0)