Prometheus metrics for library and application developers
14 kB
324 lines
1# Prometheus Metric Library for Zig
2This library is designed for both library and application developers. I do hope to streamline setup when comptime allocations are allowed.
3
4It supports, counters, gauges and histograms and the labeled-variant of each.
5
6Please see the example project. It demonstrates how a <a href="https://github.com/karlseguin/metrics.zig/blob/master/example/lib/metrics.zig">library developer</a>, and how an <a href="https://github.com/karlseguin/metrics.zig/blob/master/example/main.zig">application developer</a> can initialize and output them.</a>
7
8## Zig Version
9This is for Zig 0.16.0. Use the [zig-0.15](https://github.com/karlseguin/metrics.zig/tree/zig-0.15) branch for Zig 0.15.2 or the [dev](https://github.com/karlseguin/metrics.zig/tree/dev) which may or may not be up to date with zig dev.
10
11## Metric Setup
12Setup is a bit tedious, and I welcome suggestions for improvement.
13
14Let's start with a basic example. While the metrics within this library can be used directly, I believe that each library/application should create its own `Metrics` struct that encapsulates all metrics. A global instance of this struct can be created and initialized at comptime into a "noop" state.
15
16```zig
17const m = @import("metrics");
18
19// defaults to noop metrics, making this safe to use
20// whether or not initializeMetrics is called
21var metrics = m.initializeNoop(Metrics);
22
23const Metrics = struct {
24 // counter can be a unsigned integer or floats
25 hits: m.Counter(u32),
26
27 // gauge can be an integer or float
28 connected: m.Gauge(u16),
29};
30
31// meant to be called within the application
32pub fn hit() void {
33 metrics.hits.incr();
34}
35
36// meant to be called within the application
37pub fn connected(value: u16) void {
38 metrics.connected.set(value);
39}
40
41// meant to be called once on application startup
42pub fn initializeMetrics(comptime opts: m.RegistryOpts) !void {
43 metrics = .{
44 .hits = m.Counter(u32).init("hits", .{}, opts),
45 .connected = m.Gauge(u16).init("connected", .{}, opts),
46 };
47}
48
49// thread safe
50pub fn writeMetrics(writer: *std.io.Writer) !void {
51 return m.write(&metrics, writer);
52}
53```
54
55The call to `m.initializeNoop(Metrics)` creates a `Metrics` and initializes each metric (`hits`, `connected` and `latency`) to a "noop" implementation (tagged unions are used). The `initializeMetrics` is called on application startup and sets these metrics to real implementation.
56
57For library developers, this means their global metrics are always safe to use (all methods call noop). For application developers, it gives them control over which metrics to enable.
58
59All metrics take a name and **two options**. Why two options? The first is designed for library developers, the second is designed to give application developers additional control.
60
61Currently the first option has a single field:
62* `help: ?[]const u8 = nulls` - Used to generate the `# HELP $HELP` output line
63
64The second option should has two fields:
65* `prefix: []const u8 = ""` - Appends `prefix` to the start of each metric name.
66* `exclude: ?[]const []const u8 = null` - A list of metric names to exclude (not including the prefix).
67
68`CounterVec`, `GaugeVec`, `Histogram` and `HistogramVec` also require an allocator.
69
70### Note for Library Developers
71Library developers are free to change the above as needed. However, having libraries consistently expose an `initializeMetrics` and `writeMetrics` should help application developers.
72
73Library developers should ask their users to call `try initializeMetrics(allocator, .{})` on startup and `try writeMetrics(writer)` to generate the metrics.
74
75The `RegistryOpts` parameter should be supplied by the application and passed to each metric-initializer as-is.
76
77### Labels (vector-metrics)
78Every metric type supports a vectored variant. This allows labels to be attached to metrics. This metrics require an `std.mem.Allocator` and, as you'll see in the metric API section, most of their methods can fail.
79
80```zig
81var metrics = m.initializeNoop(Metrics);
82
83const Metrics = struct {
84 hits: m.CounterVec(u32, struct{status: u16, name: []const u8}),
85};
86
87// All labeled metrics require an allocator
88pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
89 metrics = .{
90 .hits = try m.CounterVec(u32, struct{status: u16, name: []const u8}).init(allocator, "hits", .{}, opts),
91 };
92}
93```
94
95The labels are strongly types. Valid label types are: `ErrorSet`, `Enum`, `Type`, `Bool`, `Int` and `[]const u8`
96
97The `CounterVec(u32, ...)` has to be typed twice: once in the definition of `Metrics` and once in `initializeMetrics`. This can be improved slightly.
98
99```zig
100var metrics = m.initializeNoop(Metrics);
101
102const Metrics = struct {
103 hits: Hits,
104
105 const Hits = m.CounterVec(u32, struct{status: u16, name: []const u8});
106};
107
108pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
109 metrics = .{
110 .hits = try Metrics.Hits.init(allocator, "hits", .{}, opts),
111 };
112}
113
114// Labels are compile-time checked. Using "anytype" here
115// is just lazy so we don't have to declare the label structure
116pub fn hit(labels: anytype) !void {
117 return metrics.hits.incr(labels);
118}
119```
120
121The above would be called as:
122
123```zig
124// import your metrics file
125const metrics = @import("metrics.zig");
126metrics.hit(.{.status = 200, .path = "/about.txt"});
127```
128
129Internally, every metric is a union between a "noop" and an actual implementation. This allows metrics to be globally initialized as noop and then enabled on startup. The benefit of this approach is that library developers can safely and easily use their metrics whether or not the application has enabled them.
130
131### Histograms
132Histograms are setup like `Counter` and `Gauge`, and have a vectored-variant, but they require a comptime list of buckets:
133
134```zig
135const Metrics = struct {
136 latency: Latency,
137
138 const Latency = m.Histogram(f32, &.{0.005, 0.01, 0.05, 0.1, 0.25, 1, 5, 10});
139};
140
141pub fn initializeMetrics(opts: m.RegistryOpts) !void {
142 metrics = .{
143 .latency = Metrics.Latency.init("hits", .{}, opts),
144 };
145}
146```
147
148The `HistogramVec` is even more verbose, requiring the label struct and bucket list. And, like all vectored metrics, requires an `std.mem.Allocator` and can fail:
149
150```zig
151var metrics = m.initializeNoop(Metrics);
152
153const Metrics = struct {
154 latency: Latency,
155
156 const Latency = m.HistogramVec(
157 u32,
158 struct{path: []const u8},
159 &.{5, 10, 25, 50, 100, 250, 500, 1000}
160 );
161};
162
163pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
164 metrics = .{
165 .latency = try Metrics.Latency.init(allocator, "hits", .{}, opts),
166 };
167}
168
169// Labels are compile-time checked. Using "anytype" here
170// is just lazy so we don't have to declare the label structure
171// Would be called as:
172// @import("metrics.zig").recordLatency(.{.path = "robots.txt"}, 2);
173pub fn recordLatency(labels: anytype, value: u32) !void {
174 return metrics.latency.observe(labels, value);
175}
176```
177
178## Metrics
179
180### Utility
181The package exposes the following utility functions.
182
183#### `initializeNoop(T) T`
184Creates an initializes metric `T` with `noop` implementation of every metric field. `T` should contain only metrics (`Counter`, `Gauge`, `Historgram` or their vectored variants) and primitive fields (int, bool, []const u8, enum, float).
185
186`initializeNoop(T)` will set any non-metric field to its default value.
187
188This method is designed to allow a global "metrics" instance to exist and be safe to use within libraries.
189
190#### `write(metrics: anytype, writer: *std.Io.Writer) !void`
191Calls the `write(writer) !void` method on every metric field within `metrics`.
192
193Library developers are expected to wrap this method in a `writeMetric(writer: *std.io.Writer) !void` function. This function requires a pointer to your metrics.
194
195### Counter(T)
196A `Counter(T)` is used for incrementing values. `T` can be an unsigned integer or a float. Its two main methods are `incr()` and `incrBy(value: T)`. `incr()` is a short version of `incrBy(1)`.
197
198#### `init(comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !Counter(T)`
199Initializes the counter.
200
201Opts is:
202* `help: ?[]const` - optional help text to include in the prometheus output
203
204
205#### `incr(self: *Counter(T)) void`
206Increments the counter by 1.
207
208#### `incrBy(self: *Counter(T), value: T) void`
209Increments the counter by `value`.
210
211#### `write(self: *const Counter(T), writer: *std.io.Writer) !void`
212Writes the counter to `writer`.
213
214### CounterVec(T, L)
215A `CounterVec(T, L)` is used for incrementing values with labels. `T` can be an unsigned integer or a float. `L` must be a struct where the field names and types will define the lables. Its two main methods are `incr(labels: L)` and `incrBy(labels: L, value: T)`. `incr(L)` is a short version of `incrBy(L, 1)`.
216
217#### `init(allocator: Allocator, comptime name: []const, comptim eopts: Opts, comptime ropts: RegistryOpts) !CounterVec(T, L)`
218Initializes the counter. Name must be given at comptime.
219
220Opts is:
221* `help: ?[]const` - optional help text to include in the prometheus output
222
223#### `deinit(self: *CounterVec(T, L)) void`
224Deallocates the counter
225
226#### `incr(self: *CounterVec(T, L), labels: L) !void`
227Increments the counter by 1. Vectored metrics can fail.
228
229#### `incrBy(self: *CounterVec(T, L), labels: L, value: T) !void`
230Increments the counter by `value`. Vectored metrics can fail.
231
232#### `remove(self: *CounterVec(T, L), labels: L) void`
233Removes the labeled value from the counter. Safe to call if `labels` is not an existing label.
234
235#### `write(self: *CounterVec(T, L), writer: *std.io.Writer) !void`
236Writes the counter to `writer`.
237
238### Gauge(T)
239A `Gauge(T)` is used for setting values. `T` can be an integer or a float. Its main methods are `incr()`, `incrBy(value: T)` and `set(value: T)`. `incr()` is a short version of `incrBy(1)`.
240
241#### `init(comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !Gauge(T)`
242Initializes the gauge. Name must be given at comptime.
243
244Opts is:
245* `help: ?[]const` - optional help text to include in the prometheus output
246
247#### `incr(self: *Gauge(T)) void`
248Increments the gauge by 1.
249
250#### `incrBy(self: *Gauge(T), value: T) void`
251Increments the gauge by `value`.
252
253#### `set(self: *Gauge(T), value: T) void`
254Sets the the gauge to `value`.
255
256#### `write(self: *Gauge(T), writer: *std.io.Writer) !void`
257Writes the gauge to `writer`.
258
259### GaugeVec(T, L)
260A `GaugeVec(T, L)` is used for incrementing values with labels. `T` can be an integer or a float. `L` must be a struct where the field names and types will define the lables. Its main methods are `incr(labels: L)`, `incrBy(labels: L, value: T)` and `set(labels: L, value: T)`. `incr(L)` is a short version of `incrBy(L, 1)`.
261
262#### `init(allocator: Allocator, comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !GaugeVec(T, L)`
263Initializes the gauge. Name must be given at comptime.
264
265Opts is:
266* `help: ?[]const` - optional help text to include in the prometheus output
267
268#### `deinit(self: *GaugeVec(T, L)) void`
269Deallocates the gauge
270
271#### `incr(self: *GaugeVec(T, L), labels: L) !void`
272Increments the gauge by 1. Vectored metrics can fail.
273
274#### `incrBy(self: *GaugeVec(T, L), labels: L, value: T) !void`
275Increments the gauge by `value`. Vectored metrics can fail.
276
277#### `set(self: *GaugeVec(T, L), labels: L, value: T) !void`
278Sets the gauge to `value`. Vectored metrics can fail.
279
280#### `remove(self: *GaugeVec(T, L), labels: L) void`
281Removes the labeled value from the gauge. Safe to call if `labels` is not an existing label.
282
283#### `write(self: *GaugeVec(T, L), writer: *std.io.Writer) !void`
284Writes the gauge to `writer`.
285
286### Histogram(T, []T)
287A `Histogram(T, []T)` is used to track the size and frequency of events. `T` can be an unsigned integer or a float. Its main methods is `observe(T)`.
288
289Observed valued will fall within one of the provided buckets, `[]T`. The buckets must be in ascending order. A final "infinite" bucket *should not* be provided.
290
291#### `init(comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !Histogram(T, []T)`
292Initializes the histogram. Name must be given at comptime.
293
294Opts is:
295* `help: ?[]const` - optional help text to include in the prometheus output
296
297#### `observe(self: *Histogram(T, []T), value: T) void`
298Observes `value`, bucketing it based on the provided comptime buckets.
299
300#### `write(self: *Histogram(T, []T), writer: *std.io.Writer) !void`
301Writes the histogram to `writer`.
302
303### Histogram(T, L, []T)
304A `Histogram(T, L, []T)` is used to track the size and frequency of events. `T` can be an unsigned integer or a float. `L` must be a struct where the field names and types will define the lables. Its main methods is `observe(T)`.
305
306Observed valued will fall within one of the provided buckets, `[]T`. The buckets must be in ascending order. A final "infinite" bucket *should not* be provided.
307
308#### `init(allocator: Allocator, comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !Histogram(T, L, []T)`
309Initializes the histogram. Name must be given at comptime.
310
311Opts is:
312* `help: ?[]const` - optional help text to include in the prometheus output
313
314#### `deinit(self: *Histogram(T, L, []T)) void`
315Deallocates the histogram
316
317#### `observe(self: Histogram(T, L, []T), value: T) void`
318Observes `value`, bucketing it based on the provided comptime buckets.
319
320#### `remove(self: *Histogram(T, L, []T), labels: L) void`
321Removes the labeled value from the histogram. Safe to call if `labels` is not an existing label.
322
323#### `write(self: Histogram(T, L, []T), writer: *std.io.Writer) !void`
324Writes the histogram to `writer`.