Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
Implements Counter, Gauge and Histogram. Includes a shelf handler to export metrics and a shelf middleware to measure performance.
  • Loading branch information
Fox32 committed Nov 13, 2019
1 parent 5419308 commit 12d7430
Show file tree
Hide file tree
Showing 28 changed files with 1,903 additions and 8 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Dart CI

on: [push, pull_request]

jobs:
build:

runs-on: ubuntu-latest

container:
image: google/dart:latest

steps:
- uses: actions/checkout@v1
- name: Install dependencies
run: pub get
- name: Run tests
run: pub run test
- name: Dart/Flutter Package Analyzer
uses: axel-op/[email protected]
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
10 changes: 4 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
# Remove the following pattern if you wish to check in your lock file
pubspec.lock

# Conventional directory for build outputs
build/

# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 0.1.0

- Initial version
- Implements `Counter`, `Gauge` and `Histogram`.
- Includes a shelf handler to export metrics and a shelf middleware to measure performance.
75 changes: 73 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,73 @@
# prometheus_client
A Dart prometheus client library
prometheus_client
===

This is a simple Dart implementation of the [Prometheus][prometheus] client library, [similar to to libraries for other languages][writing_clientlibs].
It supports the default metric types like gauges, counters, or histograms.
Metrics can be exported using the [text format][text_format].
To expose them in your server application the package comes with a [shelf][shelf] handler.
In addition, it comes with some plug-in ready metrics for the Dart runtime and shelf.

You can find the latest updates in the [changelog][changelog].

## Usage

A simple usage example:

```dart
import 'package:prometheus_client/prometheus_client.dart';
import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics;
import 'package:prometheus_client/shelf_handler.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
main() async {
// Register default runtime metrics
runtime_metrics.register();
// Create a metric of type counter.
// Always register your metric, either at the default registry or a custom one.
final greetingCounter =
Counter('greetings_total', 'The total amount of greetings')..register();
final app = Router();
app.get('/hello', (shelf.Request request) {
// Every time the hello is called, increase the counter by one
greetingCounter.inc();
return shelf.Response.ok('hello-world');
});
// Register a handler to expose the metrics in the Prometheus text format
app.get('/metrics', prometheusHandler());
var handler = const shelf.Pipeline()
.addHandler(app.handler);
var server = await io.serve(handler, 'localhost', 8080);
print('Serving at http://${server.address.host}:${server.port}');
}
```

Start the example application and access the exposed metrics at `http://localhost:8080/metrics`.
For a full usage example, take a look at [`example/prometheus_client.example.dart`][example].

## Planned features

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

* Support `Summary` metric type.
* Support timestamp in samples and text format.
* Split out shelf support into own package to avoid dependencies on shelf.


## Features and bugs

Please file feature requests and bugs at the [issue tracker][tracker].

[tracker]: https://github.com/Fox32/prometheus_client/issues
[writing_clientlibs]: https://prometheus.io/docs/instrumenting/writing_clientlibs/
[prometheus]: https://prometheus.io/
[text_format]: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
[shelf]: https://pub.dev/packages/shelf
[example]: ./example/prometheus_client_example.dart
[changelog]: ./CHANGELOG.md
4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Defines a default set of lint rules enforced for
# projects at Google. For details and rationale,
# see https://github.com/dart-lang/pedantic#enabled-lints.
include: package:pedantic/analysis_options.yaml
60 changes: 60 additions & 0 deletions example/prometheus_client_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'dart:math';

import 'package:prometheus_client/prometheus_client.dart';
import 'package:prometheus_client/runtime_metrics.dart' as runtime_metrics;
import 'package:prometheus_client/shelf_metrics.dart' as shelf_metrics;
import 'package:prometheus_client/shelf_handler.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';

main() async {
runtime_metrics.register();

// Create a labeled gauge metric that stores the last time an endpoint was
// accessed. Always register your metric, either at the default registry or a
// custom one.
final timeGauge = Gauge(
'last_accessed_time', 'The last time the hello endpoint was accessed',
labelNames: ['endpoint'])
..register();
// Create a gauge metric without labels to store the last rolled value
final rollGauge = Gauge('roll_value', 'The last roll value')..register();
// Create a metric of type counter
final greetingCounter =
Counter('greetings_total', 'The total amount of greetings')..register();

final app = Router();

app.get('/hello', (shelf.Request request) {
// Set the current time to the time metric for the label 'hello'
timeGauge..labels(['hello']).setToCurrentTime();
// Every time the hello is called, increase the counter by one
greetingCounter.inc();
return shelf.Response.ok('hello-world');
});

app.get('/roll', (shelf.Request request) {
timeGauge..labels(['roll']).setToCurrentTime();
final value = Random().nextDouble();
// Store the rolled value without labels
rollGauge.value = value;
return shelf.Response.ok('rolled $value');
});

// Register a handler to expose the metrics in the Prometheus text format
app.get('/metrics', prometheusHandler());

app.all('/<ignored|.*>', (shelf.Request request) {
return shelf.Response.notFound('Not Found');
});

var handler = const shelf.Pipeline()
// Register a middleware to track request times
.addMiddleware(shelf_metrics.register())
.addMiddleware(shelf.logRequests())
.addHandler(app.handler);
var server = await io.serve(handler, 'localhost', 8080);

print('Serving at http://${server.address.host}:${server.port}');
}
107 changes: 107 additions & 0 deletions lib/format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/// A library to export metrics in the Prometheus text representation.
library format;

import 'package:prometheus_client/prometheus_client.dart';
import 'package:prometheus_client/src/double_format.dart';

/// Content-type for text version 0.0.4.
const contentType = 'text/plain; version=0.0.4; charset=utf-8';

/// Write out the text version 0.0.4 of the given [MetricFamilySamples].
void write004(
StringSink sink, Iterable<MetricFamilySamples> metricFamilySamples) {
// See http://prometheus.io/docs/instrumenting/exposition_formats/
// for the output format specification
for (var metricFamilySample in metricFamilySamples) {
sink.write('# HELP ');
sink.write(metricFamilySample.name);
sink.write(' ');
_writeEscapedHelp(sink, metricFamilySample.help);
sink.write('\n');

sink.write('# TYPE ');
sink.write(metricFamilySample.name);
sink.write(' ');
_writeMetricType(sink, metricFamilySample.type);
sink.write('\n');

for (var sample in metricFamilySample.samples) {
sink.write(sample.name);
if (sample.labelNames.isNotEmpty) {
sink.write('{');
for (var i = 0; i < sample.labelNames.length; ++i) {
sink.write(sample.labelNames[i]);
sink.write('="');
_writeEscapedLabelValue(sink, sample.labelValues[i]);
sink.write('\",');
}
sink.write('}');
}
sink.write(' ');
sink.write(formatDouble(sample.value));
// TODO: Write Timestamp
sink.writeln();
}
}
}

void _writeMetricType(StringSink sink, MetricType type) {
switch (type) {
case MetricType.counter:
sink.write('counter');
break;
case MetricType.gauge:
sink.write('gauge');
break;
case MetricType.summary:
sink.write('summary');
break;
case MetricType.histogram:
sink.write('histogram');
break;
case MetricType.untyped:
sink.write('untyped');
break;
}
}

const _codeUnitLineFeed = 10; // \n
const _codeUnitBackslash = 92; // \
const _codeUnitDoubleQuotes = 34; // "

void _writeEscapedHelp(StringSink sink, String help) {
for (var i = 0; i < help.length; ++i) {
var c = help.codeUnitAt(i);
switch (c) {
case _codeUnitBackslash:
sink.write('\\\\');
break;
case _codeUnitLineFeed:
sink.write('\\n');
break;
default:
sink.writeCharCode(c);
break;
}
}
}

void _writeEscapedLabelValue(StringSink sink, String labelValue) {
for (var i = 0; i < labelValue.length; ++i) {
var c = labelValue.codeUnitAt(i);
switch (c) {
case _codeUnitBackslash:
sink.write('\\\\');
break;
case _codeUnitDoubleQuotes:
sink.write('\\"');
break;
case _codeUnitLineFeed:
sink.write('\\n');
break;
default:
sink.writeCharCode(c);
break;
}
}
}
23 changes: 23 additions & 0 deletions lib/prometheus_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// A library containing the core elements of the Prometheus client, like the
/// [CollectorRegistry] and different types of metrics like [Counter], [Gauge]
/// and [Histogram].
library prometheus_client;

import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;

import "package:collection/collection.dart";
import 'package:prometheus_client/src/double_format.dart';

part 'src/prometheus_client/collector.dart';

part 'src/prometheus_client/counter.dart';

part 'src/prometheus_client/gauge.dart';

part 'src/prometheus_client/helper.dart';

part 'src/prometheus_client/histogram.dart';

part 'src/prometheus_client/simple_collector.dart';
44 changes: 44 additions & 0 deletions lib/runtime_metrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/// A library exposing metrics of the Dart runtime.
library runtime_metrics;

import 'dart:io';
import 'package:prometheus_client/prometheus_client.dart';

// This is not the actual startup time of the process, but the time the first
// collector was created. Dart's lazy initialization of globals doesn't allow
// for a better timing...

/// Collector for runtime metrics. Exposes the `dart_info` and
/// `process_resident_memory_bytes` metric.
class RuntimeCollector extends Collector {
static final _startupTime =
DateTime.now().millisecondsSinceEpoch / Duration.millisecondsPerSecond;

@override
Iterable<MetricFamilySamples> collect() sync* {
yield MetricFamilySamples("dart_info", MetricType.gauge,
"Information about the Dart environment.", [
Sample("dart_info", const ["version"], [Platform.version], 1)
]);

yield MetricFamilySamples("process_resident_memory_bytes", MetricType.gauge,
"Resident memory size in bytes.", [
Sample("process_resident_memory_bytes", const [], const [],
ProcessInfo.currentRss.toDouble())
]);

yield MetricFamilySamples("process_start_time_seconds", MetricType.gauge,
"Start time of the process since unix epoch in seconds.", [
Sample("process_start_time_seconds", const [], const [],
_startupTime.toDouble())
]);
}
}

/// Register default metrics for the Dart runtime. If no [registry] is provided,
/// the [CollectorRegistry.defaultRegistry] is used.
void register([CollectorRegistry registry]) {
registry ??= CollectorRegistry.defaultRegistry;

registry.register(RuntimeCollector());
}
22 changes: 22 additions & 0 deletions lib/shelf_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// A library containing a shelf handler that exposes metrics in the Prometheus
/// text format.
library shelf_handler;

import 'package:shelf/shelf.dart' as shelf;
import 'package:prometheus_client/prometheus_client.dart';
import 'package:prometheus_client/format.dart' as format;

/// Create a shelf handler that returns the metrics in the prometheus text
/// representation. If no [registry] is provided, the
/// [CollectorRegistry.defaultRegistry] is used.
prometheusHandler([CollectorRegistry registry]) {
registry ??= CollectorRegistry.defaultRegistry;

return (shelf.Request request) {
// TODO: Instead of using a StringBuffer we could directly stream to network
final buffer = StringBuffer();
format.write004(buffer, registry.collectMetricFamilySamples());
return shelf.Response.ok(buffer.toString(),
headers: {"Content-Type": format.contentType});
};
}
Loading

0 comments on commit 12d7430

Please sign in to comment.