Prometheus metrics for library and application developers
0

Configure Feed

Select the types of activity you want to include in your feed.

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`.