Skip to content

Commit 12d7430

Browse files
committed
Initial version
Implements Counter, Gauge and Histogram. Includes a shelf handler to export metrics and a shelf middleware to measure performance.
1 parent 5419308 commit 12d7430

28 files changed

+1903
-8
lines changed

.github/workflows/dart.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Dart CI
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
8+
runs-on: ubuntu-latest
9+
10+
container:
11+
image: google/dart:latest
12+
13+
steps:
14+
- uses: actions/checkout@v1
15+
- name: Install dependencies
16+
run: pub get
17+
- name: Run tests
18+
run: pub run test
19+
- name: Dart/Flutter Package Analyzer
20+
uses: axel-op/dart_package_analyzer@v2.0.0
21+
with:
22+
githubToken: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
# See https://www.dartlang.org/guides/libraries/private-files
2-
31
# Files and directories created by pub
42
.dart_tool/
53
.packages
6-
.pub/
7-
build/
8-
# If you're building an application, you may want to check-in your pubspec.lock
4+
# Remove the following pattern if you wish to check in your lock file
95
pubspec.lock
106

7+
# Conventional directory for build outputs
8+
build/
9+
1110
# Directory created by dartdoc
12-
# If you don't generate documentation locally you can remove this line.
1311
doc/api/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 0.1.0
2+
3+
- Initial version
4+
- Implements `Counter`, `Gauge` and `Histogram`.
5+
- Includes a shelf handler to export metrics and a shelf middleware to measure performance.

README.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,73 @@
1-
# prometheus_client
2-
A Dart prometheus client library
1+
prometheus_client
2+
===
3+
4+
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.
6+
Metrics can be exported using the [text format][text_format].
7+
To expose them in your server application the package comes with a [shelf][shelf] handler.
8+
In addition, it comes with some plug-in ready metrics for the Dart runtime and shelf.
9+
10+
You can find the latest updates in the [changelog][changelog].
11+
12+
## Usage
13+
14+
A simple usage example:
15+
16+
```dart
17+
import 'package:prometheus_client/prometheus_client.dart';
18+
import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics;
19+
import 'package:prometheus_client/shelf_handler.dart';
20+
import 'package:shelf/shelf.dart' as shelf;
21+
import 'package:shelf/shelf_io.dart' as io;
22+
import 'package:shelf_router/shelf_router.dart';
23+
24+
main() async {
25+
// Register default runtime metrics
26+
runtime_metrics.register();
27+
28+
// Create a metric of type counter.
29+
// Always register your metric, either at the default registry or a custom one.
30+
final greetingCounter =
31+
Counter('greetings_total', 'The total amount of greetings')..register();
32+
final app = Router();
33+
34+
app.get('/hello', (shelf.Request request) {
35+
// Every time the hello is called, increase the counter by one
36+
greetingCounter.inc();
37+
return shelf.Response.ok('hello-world');
38+
});
39+
40+
// Register a handler to expose the metrics in the Prometheus text format
41+
app.get('/metrics', prometheusHandler());
42+
43+
var handler = const shelf.Pipeline()
44+
.addHandler(app.handler);
45+
var server = await io.serve(handler, 'localhost', 8080);
46+
47+
print('Serving at http://${server.address.host}:${server.port}');
48+
}
49+
```
50+
51+
Start the example application and access the exposed metrics at `http://localhost:8080/metrics`.
52+
For a full usage example, take a look at [`example/prometheus_client.example.dart`][example].
53+
54+
## Planned features
55+
56+
To achieve the requirements from the Prometheus [Writing Client Libraries][writing_clientlibs] documentation, some features still have to be implemented:
57+
58+
* Support `Summary` metric type.
59+
* Support timestamp in samples and text format.
60+
* Split out shelf support into own package to avoid dependencies on shelf.
61+
62+
63+
## Features and bugs
64+
65+
Please file feature requests and bugs at the [issue tracker][tracker].
66+
67+
[tracker]: https://github.com/Fox32/prometheus_client/issues
68+
[writing_clientlibs]: https://prometheus.io/docs/instrumenting/writing_clientlibs/
69+
[prometheus]: https://prometheus.io/
70+
[text_format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
71+
[shelf]: https://pub.dev/packages/shelf
72+
[example]: ./example/prometheus_client_example.dart
73+
[changelog]: ./CHANGELOG.md

analysis_options.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Defines a default set of lint rules enforced for
2+
# projects at Google. For details and rationale,
3+
# see https://github.com/dart-lang/pedantic#enabled-lints.
4+
include: package:pedantic/analysis_options.yaml
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'dart:math';
2+
3+
import 'package:prometheus_client/prometheus_client.dart';
4+
import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics;
5+
import 'package:prometheus_client/shelf_metrics.dart' as shelf_metrics;
6+
import 'package:prometheus_client/shelf_handler.dart';
7+
import 'package:shelf/shelf.dart' as shelf;
8+
import 'package:shelf/shelf_io.dart' as io;
9+
import 'package:shelf_router/shelf_router.dart';
10+
11+
main() async {
12+
runtime_metrics.register();
13+
14+
// Create a labeled gauge metric that stores the last time an endpoint was
15+
// accessed. Always register your metric, either at the default registry or a
16+
// custom one.
17+
final timeGauge = Gauge(
18+
'last_accessed_time', 'The last time the hello endpoint was accessed',
19+
labelNames: ['endpoint'])
20+
..register();
21+
// Create a gauge metric without labels to store the last rolled value
22+
final rollGauge = Gauge('roll_value', 'The last roll value')..register();
23+
// Create a metric of type counter
24+
final greetingCounter =
25+
Counter('greetings_total', 'The total amount of greetings')..register();
26+
27+
final app = Router();
28+
29+
app.get('/hello', (shelf.Request request) {
30+
// Set the current time to the time metric for the label 'hello'
31+
timeGauge..labels(['hello']).setToCurrentTime();
32+
// Every time the hello is called, increase the counter by one
33+
greetingCounter.inc();
34+
return shelf.Response.ok('hello-world');
35+
});
36+
37+
app.get('/roll', (shelf.Request request) {
38+
timeGauge..labels(['roll']).setToCurrentTime();
39+
final value = Random().nextDouble();
40+
// Store the rolled value without labels
41+
rollGauge.value = value;
42+
return shelf.Response.ok('rolled $value');
43+
});
44+
45+
// Register a handler to expose the metrics in the Prometheus text format
46+
app.get('/metrics', prometheusHandler());
47+
48+
app.all('/<ignored|.*>', (shelf.Request request) {
49+
return shelf.Response.notFound('Not Found');
50+
});
51+
52+
var handler = const shelf.Pipeline()
53+
// Register a middleware to track request times
54+
.addMiddleware(shelf_metrics.register())
55+
.addMiddleware(shelf.logRequests())
56+
.addHandler(app.handler);
57+
var server = await io.serve(handler, 'localhost', 8080);
58+
59+
print('Serving at http://${server.address.host}:${server.port}');
60+
}

lib/format.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/// A library to export metrics in the Prometheus text representation.
2+
library format;
3+
4+
import 'package:prometheus_client/prometheus_client.dart';
5+
import 'package:prometheus_client/src/double_format.dart';
6+
7+
/// Content-type for text version 0.0.4.
8+
const contentType = 'text/plain; version=0.0.4; charset=utf-8';
9+
10+
/// Write out the text version 0.0.4 of the given [MetricFamilySamples].
11+
void write004(
12+
StringSink sink, Iterable<MetricFamilySamples> metricFamilySamples) {
13+
// See http://prometheus.io/docs/instrumenting/exposition_formats/
14+
// for the output format specification
15+
for (var metricFamilySample in metricFamilySamples) {
16+
sink.write('# HELP ');
17+
sink.write(metricFamilySample.name);
18+
sink.write(' ');
19+
_writeEscapedHelp(sink, metricFamilySample.help);
20+
sink.write('\n');
21+
22+
sink.write('# TYPE ');
23+
sink.write(metricFamilySample.name);
24+
sink.write(' ');
25+
_writeMetricType(sink, metricFamilySample.type);
26+
sink.write('\n');
27+
28+
for (var sample in metricFamilySample.samples) {
29+
sink.write(sample.name);
30+
if (sample.labelNames.isNotEmpty) {
31+
sink.write('{');
32+
for (var i = 0; i < sample.labelNames.length; ++i) {
33+
sink.write(sample.labelNames[i]);
34+
sink.write('="');
35+
_writeEscapedLabelValue(sink, sample.labelValues[i]);
36+
sink.write('\",');
37+
}
38+
sink.write('}');
39+
}
40+
sink.write(' ');
41+
sink.write(formatDouble(sample.value));
42+
// TODO: Write Timestamp
43+
sink.writeln();
44+
}
45+
}
46+
}
47+
48+
void _writeMetricType(StringSink sink, MetricType type) {
49+
switch (type) {
50+
case MetricType.counter:
51+
sink.write('counter');
52+
break;
53+
case MetricType.gauge:
54+
sink.write('gauge');
55+
break;
56+
case MetricType.summary:
57+
sink.write('summary');
58+
break;
59+
case MetricType.histogram:
60+
sink.write('histogram');
61+
break;
62+
case MetricType.untyped:
63+
sink.write('untyped');
64+
break;
65+
}
66+
}
67+
68+
const _codeUnitLineFeed = 10; // \n
69+
const _codeUnitBackslash = 92; // \
70+
const _codeUnitDoubleQuotes = 34; // "
71+
72+
void _writeEscapedHelp(StringSink sink, String help) {
73+
for (var i = 0; i < help.length; ++i) {
74+
var c = help.codeUnitAt(i);
75+
switch (c) {
76+
case _codeUnitBackslash:
77+
sink.write('\\\\');
78+
break;
79+
case _codeUnitLineFeed:
80+
sink.write('\\n');
81+
break;
82+
default:
83+
sink.writeCharCode(c);
84+
break;
85+
}
86+
}
87+
}
88+
89+
void _writeEscapedLabelValue(StringSink sink, String labelValue) {
90+
for (var i = 0; i < labelValue.length; ++i) {
91+
var c = labelValue.codeUnitAt(i);
92+
switch (c) {
93+
case _codeUnitBackslash:
94+
sink.write('\\\\');
95+
break;
96+
case _codeUnitDoubleQuotes:
97+
sink.write('\\"');
98+
break;
99+
case _codeUnitLineFeed:
100+
sink.write('\\n');
101+
break;
102+
default:
103+
sink.writeCharCode(c);
104+
break;
105+
}
106+
}
107+
}

lib/prometheus_client.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// A library containing the core elements of the Prometheus client, like the
2+
/// [CollectorRegistry] and different types of metrics like [Counter], [Gauge]
3+
/// and [Histogram].
4+
library prometheus_client;
5+
6+
import 'dart:async';
7+
import 'dart:collection';
8+
import 'dart:math' as math;
9+
10+
import "package:collection/collection.dart";
11+
import 'package:prometheus_client/src/double_format.dart';
12+
13+
part 'src/prometheus_client/collector.dart';
14+
15+
part 'src/prometheus_client/counter.dart';
16+
17+
part 'src/prometheus_client/gauge.dart';
18+
19+
part 'src/prometheus_client/helper.dart';
20+
21+
part 'src/prometheus_client/histogram.dart';
22+
23+
part 'src/prometheus_client/simple_collector.dart';

lib/runtime_metrics.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/// A library exposing metrics of the Dart runtime.
2+
library runtime_metrics;
3+
4+
import 'dart:io';
5+
import 'package:prometheus_client/prometheus_client.dart';
6+
7+
// This is not the actual startup time of the process, but the time the first
8+
// collector was created. Dart's lazy initialization of globals doesn't allow
9+
// for a better timing...
10+
11+
/// Collector for runtime metrics. Exposes the `dart_info` and
12+
/// `process_resident_memory_bytes` metric.
13+
class RuntimeCollector extends Collector {
14+
static final _startupTime =
15+
DateTime.now().millisecondsSinceEpoch / Duration.millisecondsPerSecond;
16+
17+
@override
18+
Iterable<MetricFamilySamples> collect() sync* {
19+
yield MetricFamilySamples("dart_info", MetricType.gauge,
20+
"Information about the Dart environment.", [
21+
Sample("dart_info", const ["version"], [Platform.version], 1)
22+
]);
23+
24+
yield MetricFamilySamples("process_resident_memory_bytes", MetricType.gauge,
25+
"Resident memory size in bytes.", [
26+
Sample("process_resident_memory_bytes", const [], const [],
27+
ProcessInfo.currentRss.toDouble())
28+
]);
29+
30+
yield MetricFamilySamples("process_start_time_seconds", MetricType.gauge,
31+
"Start time of the process since unix epoch in seconds.", [
32+
Sample("process_start_time_seconds", const [], const [],
33+
_startupTime.toDouble())
34+
]);
35+
}
36+
}
37+
38+
/// Register default metrics for the Dart runtime. If no [registry] is provided,
39+
/// the [CollectorRegistry.defaultRegistry] is used.
40+
void register([CollectorRegistry registry]) {
41+
registry ??= CollectorRegistry.defaultRegistry;
42+
43+
registry.register(RuntimeCollector());
44+
}

lib/shelf_handler.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// A library containing a shelf handler that exposes metrics in the Prometheus
2+
/// text format.
3+
library shelf_handler;
4+
5+
import 'package:shelf/shelf.dart' as shelf;
6+
import 'package:prometheus_client/prometheus_client.dart';
7+
import 'package:prometheus_client/format.dart' as format;
8+
9+
/// Create a shelf handler that returns the metrics in the prometheus text
10+
/// representation. If no [registry] is provided, the
11+
/// [CollectorRegistry.defaultRegistry] is used.
12+
prometheusHandler([CollectorRegistry registry]) {
13+
registry ??= CollectorRegistry.defaultRegistry;
14+
15+
return (shelf.Request request) {
16+
// TODO: Instead of using a StringBuffer we could directly stream to network
17+
final buffer = StringBuffer();
18+
format.write004(buffer, registry.collectMetricFamilySamples());
19+
return shelf.Response.ok(buffer.toString(),
20+
headers: {"Content-Type": format.contentType});
21+
};
22+
}

0 commit comments

Comments
 (0)