Skip to content

Commit 8587ffc

Browse files
committed
Implement Summary metric type
1 parent 7144e39 commit 8587ffc

File tree

11 files changed

+650
-17
lines changed

11 files changed

+650
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77
## 0.2.0
88

99
- Support timestamp for samples.
10+
11+
## 0.3.0
12+
13+
- Implement `Summary` metric type

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ prometheus_client
22
===
33

44
This is a simple Dart implementation of the [Prometheus][prometheus] client library, [similar to to libraries for other languages][writing_clientlibs].
5-
It supports the default metric types like gauges, counters, or histograms.
5+
It supports the default metric types like gauges, counters, summaries, or histograms.
66
Metrics can be exported using the [text format][text_format].
77
To expose them in your server application the package comes with a [shelf][shelf] handler.
88
In addition, it comes with some plug-in ready metrics for the Dart runtime and shelf.
@@ -55,7 +55,6 @@ For a full usage example, take a look at [`example/prometheus_client.example.dar
5555

5656
To achieve the requirements from the Prometheus [Writing Client Libraries][writing_clientlibs] documentation, some features still have to be implemented:
5757

58-
* Support `Summary` metric type.
5958
* Split out shelf support into own package to avoid dependencies on shelf.
6059

6160

lib/prometheus_client.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import 'dart:math' as math;
99

1010
import "package:collection/collection.dart";
1111
import 'package:prometheus_client/src/double_format.dart';
12+
import 'package:prometheus_client/src/quantiles.dart';
13+
14+
export 'package:prometheus_client/src/quantiles.dart' show Quantile;
1215

1316
part 'src/prometheus_client/collector.dart';
1417

@@ -21,3 +24,5 @@ part 'src/prometheus_client/helper.dart';
2124
part 'src/prometheus_client/histogram.dart';
2225

2326
part 'src/prometheus_client/simple_collector.dart';
27+
28+
part 'src/prometheus_client/summary.dart';

lib/src/prometheus_client/histogram.dart

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ part of prometheus_client;
33
/// [Histogram] allows aggregatable distributions of events, such as request
44
/// latencies.
55
class Histogram extends _SimpleCollector<HistogramChild> {
6+
/// Name of the 'le' label.
7+
static const leLabel = 'le';
8+
69
/// The default upper bounds for histogram buckets.
710
static const defaultBuckets = <double>[
811
.005,
@@ -32,9 +35,9 @@ class Histogram extends _SimpleCollector<HistogramChild> {
3235
Histogram(String name, String help,
3336
{List<String> labelNames = const [],
3437
List<double> buckets = defaultBuckets})
35-
: buckets = _sanitizeBuckets(buckets),
38+
: buckets = List.unmodifiable(_sanitizeBuckets(buckets)),
3639
super(name, help, labelNames: labelNames) {
37-
if (labelNames.contains('le')) {
40+
if (labelNames.contains(leLabel)) {
3841
throw ArgumentError.value(labelNames, 'labelNames',
3942
'"le" is a reseved label name for a histogram.');
4043
}
@@ -49,7 +52,8 @@ class Histogram extends _SimpleCollector<HistogramChild> {
4952
{List<String> labelNames = const []})
5053
: this(name, help,
5154
labelNames: labelNames,
52-
buckets: _generateLinearBuckets(start, width, count));
55+
buckets:
56+
List.unmodifiable(_generateLinearBuckets(start, width, count)));
5357

5458
/// Construct a new [Histogram] with a [name], [help] text, and optional
5559
/// [labelNames]. The [count] buckets are exponential distributed starting at
@@ -60,7 +64,8 @@ class Histogram extends _SimpleCollector<HistogramChild> {
6064
{List<String> labelNames = const []})
6165
: this(name, help,
6266
labelNames: labelNames,
63-
buckets: _generateExponentialBuckets(start, factor, count));
67+
buckets: List.unmodifiable(
68+
_generateExponentialBuckets(start, factor, count)));
6469

6570
/// Observe a new value [v] and store it in the corresponding buckets of a
6671
/// histogram without labels.
@@ -94,7 +99,7 @@ class Histogram extends _SimpleCollector<HistogramChild> {
9499
final samples = <Sample>[];
95100

96101
_children.forEach((labelValues, child) {
97-
final labelNamesWithLe = List.of(labelNames)..add('le');
102+
final labelNamesWithLe = List.of(labelNames)..add(leLabel);
98103

99104
for (var i = 0; i < buckets.length; ++i) {
100105
samples.add(Sample(
@@ -133,7 +138,7 @@ class Histogram extends _SimpleCollector<HistogramChild> {
133138
buckets.add(double.infinity);
134139
}
135140

136-
return List.of(buckets);
141+
return buckets;
137142
}
138143

139144
static List<double> _generateLinearBuckets(
@@ -147,7 +152,9 @@ class Histogram extends _SimpleCollector<HistogramChild> {
147152

148153
/// Defines a [HistogramChild] of a [Histogram] with assigned [labelValues].
149154
class HistogramChild {
155+
/// The upper bounds of the buckets.
150156
final List<double> buckets;
157+
151158
final List<double> _bucketValues;
152159
double _sum = 0;
153160

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
part of prometheus_client;
2+
3+
/// Similar to a [Histogram], a [Summary] samples observations (usually things
4+
/// like request durations and response sizes). While it also provides a total
5+
/// count of observations and a sum of all observed values, it calculates
6+
/// configurable quantiles over a sliding time window.
7+
class Summary extends _SimpleCollector<SummaryChild> {
8+
static const quantileLabel = 'quantile';
9+
10+
/// Quantiles to observe by the summary.
11+
final List<Quantile> quantiles;
12+
13+
/// Set the duration of the time window is, i.e. how long observations are
14+
/// kept before they are discarded.
15+
final Duration maxAge;
16+
17+
/// Set the number of buckets used to implement the sliding time window. If
18+
/// your time window is 10 minutes, and you have ageBuckets=5, buckets will
19+
/// be switched every 2 minutes. The value is a trade-off between resources
20+
/// (memory and cpu for maintaining the bucket) and how smooth the time window
21+
/// is moved.
22+
final int ageBuckets;
23+
24+
/// Construct a new [Summary] with a [name], [help] text, optional
25+
/// [labelNames], optional [quantiles], optional [maxAge] and optional
26+
/// [ageBuckets].
27+
/// If [labelNames] are provided, use [labels(...)] to assign label values.
28+
/// If no [quantiles] are provided the summary only has a count and sum.
29+
/// If not provided, [maxAge] defaults to 10 minutes and [ageBuckets] to 5.
30+
Summary(String name, String help,
31+
{List<String> labelNames = const [],
32+
List<Quantile> quantiles = const [],
33+
this.maxAge = const Duration(minutes: 10),
34+
this.ageBuckets = 5})
35+
: quantiles = List.unmodifiable(quantiles),
36+
super(name, help, labelNames: labelNames) {
37+
if (labelNames.contains(quantileLabel)) {
38+
throw ArgumentError.value(labelNames, 'labelNames',
39+
'"quantile" is a reseved label name for a summary.');
40+
}
41+
}
42+
43+
/// Observe a new value [v] and store it in the summary without labels.
44+
void observe(double v) {
45+
_noLabelChild.observe(v);
46+
}
47+
48+
/// Observe the duration of [callback] and store it in the summary without
49+
/// labels.
50+
T observeDurationSync<T>(T callback()) {
51+
return _noLabelChild.observeDurationSync(callback);
52+
}
53+
54+
/// Observe the duration of the [Future] [f] and store it in the summary
55+
/// without labels.
56+
Future<T> observeDuration<T>(Future<T> f) {
57+
return _noLabelChild.observeDuration(f);
58+
}
59+
60+
/// Access the count of elements in a summary without labels.
61+
double get count => _noLabelChild.count;
62+
63+
/// Access the total sum of the elements in a summary without labels.
64+
double get sum => _noLabelChild.sum;
65+
66+
/// Access the value of each quantile of a summary without labels.
67+
Map get values => _noLabelChild.values;
68+
69+
@override
70+
SummaryChild _createChild() => SummaryChild._(quantiles, maxAge, ageBuckets);
71+
72+
@override
73+
Iterable<MetricFamilySamples> collect() sync* {
74+
final samples = <Sample>[];
75+
76+
_children.forEach((labelValues, child) {
77+
final labelNamesWithQuantile = List.of(labelNames)..add(quantileLabel);
78+
final values = child.values;
79+
80+
for (var i = 0; i < quantiles.length; ++i) {
81+
final q = quantiles[i].quantile;
82+
samples.add(Sample(name, labelNamesWithQuantile,
83+
List.of(labelValues)..add(formatDouble(q)), values[q]));
84+
}
85+
86+
samples
87+
.add(Sample(name + '_count', labelNames, labelValues, child.count));
88+
samples.add(Sample(name + '_sum', labelNames, labelValues, child.sum));
89+
});
90+
91+
yield MetricFamilySamples(name, MetricType.summary, help, samples);
92+
}
93+
}
94+
95+
/// Defines a [SummaryChild] of a [Summary] with assigned [labelValues].
96+
class SummaryChild {
97+
/// Quantiles to observe by the summary.
98+
final List<Quantile> quantiles;
99+
100+
double _count = 0;
101+
double _sum = 0;
102+
final TimeWindowQuantiles _quantileValues;
103+
104+
SummaryChild._(this.quantiles, Duration maxAge, int ageBuckets)
105+
: _quantileValues = quantiles.isEmpty
106+
? null
107+
: TimeWindowQuantiles(quantiles, maxAge, ageBuckets);
108+
109+
/// Observe a new value [v] and store it in the summary with labels.
110+
void observe(double v) {
111+
_count += 1;
112+
_sum += v;
113+
if (_quantileValues != null) {
114+
_quantileValues.insert(v);
115+
}
116+
}
117+
118+
/// Observe the duration of [callback] and store it in the summary with
119+
/// labels.
120+
T observeDurationSync<T>(T callback()) {
121+
final stopwatch = Stopwatch()..start();
122+
try {
123+
return callback();
124+
} finally {
125+
observe(stopwatch.elapsedMicroseconds / Duration.microsecondsPerSecond);
126+
}
127+
}
128+
129+
/// Observe the duration of the [Future] [f] and store it in the summary with
130+
/// labels.
131+
Future<T> observeDuration<T>(Future<T> f) async {
132+
final stopwatch = Stopwatch()..start();
133+
try {
134+
return await f;
135+
} finally {
136+
observe(stopwatch.elapsedMicroseconds / Duration.microsecondsPerSecond);
137+
}
138+
}
139+
140+
/// Access the count of elements in a summary with labels.
141+
double get count => _count;
142+
143+
/// Access the total sum of the elements in a summary with labels.
144+
double get sum => _sum;
145+
146+
/// Access the value of each quantile of a summary with labels.
147+
Map get values => Map.fromIterable(quantiles,
148+
key: (q) => q.quantile,
149+
value: (q) => _quantileValues.retrieve(q.quantile));
150+
}

0 commit comments

Comments
 (0)