This library is designed for both library and application developers. I do hope to streamline setup when comptime allocations are allowed.
It supports, counters, gauges and histograms and the labeled-variant of each.
Please see the example project. It demonstrates how a library developer, and how an application developer can initialize and output them.
Setup is a bit tedious, and I welcome suggestions for improvement.
Let'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.
const m = @import("metrics");
// defaults to noop metrics, making this safe to use
// whether or not initializeMetrics is called
var metrics = m.initializeNoop(Metrics);
const Metrics = struct {
// counter can be a unsigned integer or floats
hits: m.Counter(u32),
// gauge can be an integer or float
connected: m.Gauge(u16),
};
// meant to be called within the application
pub fn hit() void {
metrics.hits.incr();
}
// meant to be called within the application
pub fn connected(value: u16) void {
metrics.connected.set(value);
}
// meant to be called once on application startup
pub fn initializeMetrics(comptime opts: m.RegistryOpts) !void {
metrics = .{
.hits = m.Counter(u32).init("hits", .{}, opts),
.connected = m.Gauge(u16).init("connected", .{}, opts),
};
}
// thread safe
pub fn writeMetrics(writer: anytype) !void {
return m.write(&metrics, writer);
}
The 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.
For 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.
All 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.
Currently the first option has a single field:
help: ?[]const u8 = nulls
- Used to generate the# HELP $HELP
output line
The second option should has two fields:
prefix: []const u8 = ""
- Appendsprefix
to the start of each metric name.exclude: ?[]const []const u8 = null
- A list of metric names to exclude (not including the prefix).
CounterVec
, GaugeVec
, Histogram
and HistogramVec
also require an allocator.
Library developers are free to change the above as needed. However, having libraries consistently expose an initializeMetrics
and writeMetrics
should help application developers.
Library developers should ask their users to call try initializeMetrics(allocator, .{})
on startup and try writeMetrics(writer)
to generate the metrics.
The RegistryOpts
parameter should be supplied by the application and passed to each metric-initializer as-is.
Every 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.
var metrics = m.initializeNoop(Metrics);
const Metrics = struct {
hits: m.CounterVec(u32, struct{status: u16, name: []const u8}),
};
// All labeled metrics require an allocator
pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
metrics = .{
.hits = try m.CounterVec(u32, struct{status: u16, name: []const u8}).init(allocator, "hits", .{}, opts),
};
}
The labels are strongly types. Valid label types are: ErrorSet
, Enum
, Type
, Bool
, Int
and []const u8
The CounterVec(u32, ...)
has to be typed twice: once in the definition of Metrics
and once in initializeMetrics
. This can be improved slightly.
var metrics = m.initializeNoop(Metrics);
const Metrics = struct {
hits: Hits,
const Hits = m.CounterVec(u32, struct{status: u16, name: []const u8});
};
pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
metrics = .{
.hits = try Metrics.Hits.init(allocator, "hits", .{}, opts),
};
}
// Labels are compile-time checked. Using "anytype" here
// is just lazy so we don't have to declare the label structure
pub fn hit(labels: anytype) !void {
return metrics.hits.incr(labels);
}
The above would be called as:
// import your metrics file
const metrics = @import("metrics.zig");
metrics.hit(.{.status = 200, .path = "/about.txt"});
Internally, 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.
Histograms are setup like Counter
and Gauge
, and have a vectored-variant, but they require a comptime list of buckets:
const Metrics = struct {
latency: Latency,
const Latency = m.Histogram(f32, &.{0.005, 0.01, 0.05, 0.1, 0.25, 1, 5, 10});
};
pub fn initializeMetrics(opts: m.RegistryOpts) !void {
metrics = .{
.latency = Metrics.Latency.init("hits", .{}, opts),
};
}
The 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:
var metrics = m.initializeNoop(Metrics);
const Metrics = struct {
latency: Latency,
const Latency = m.HistogramVec(
u32,
struct{path: []const u8},
&.{5, 10, 25, 50, 100, 250, 500, 1000}
);
};
pub fn initializeMetrics(allocator: Allocator, opts: m.RegistryOpts) !void {
metrics = .{
.latency = try Metrics.Latency.init(allocator, "hits", .{}, opts),
};
}
// Labels are compile-time checked. Using "anytype" here
// is just lazy so we don't have to declare the label structure
// Would be called as:
// @import("metrics.zig").recordLatency(.{.path = "robots.txt"}, 2);
pub fn recordLatency(labels: anytype, value: u32) !void {
return metrics.latency.observe(labels, value);
}
The package exposes the following utility functions.
Creates 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).
initializeNoop(T)
will set any non-metric field to its default value.
This method is designed to allow a global "metrics" instance to exist and be safe to use within libraries.
Calls the write(writer) !void
method on every metric field within metrics
.
Library developers are expected to wrap this method in a writeMetric(writer: anytype) !void
function. This function requires a pointer to your metrics.
A 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)
.
Initializes the counter.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Increments the counter by 1.
Increments the counter by value
.
Writes the counter to writer
.
A 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)
.
init(allocator: Allocator, comptime name: []const, comptim eopts: Opts, comptime ropts: RegistryOpts) !CounterVec(T, L)
Initializes the counter. Name must be given at comptime.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Deallocates the counter
Increments the counter by 1. Vectored metrics can fail.
Increments the counter by value
. Vectored metrics can fail.
Removes the labeled value from the counter. Safe to call if labels
is not an existing label.
Writes the counter to writer
.
A 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)
.
Initializes the gauge. Name must be given at comptime.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Increments the gauge by 1.
Increments the gauge by value
.
Sets the the gauge to value
.
Writes the gauge to writer
.
A 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)
.
init(allocator: Allocator, comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !GaugeVec(T, L)
Initializes the gauge. Name must be given at comptime.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Deallocates the gauge
Increments the gauge by 1. Vectored metrics can fail.
Increments the gauge by value
. Vectored metrics can fail.
Sets the gauge to value
. Vectored metrics can fail.
Removes the labeled value from the gauge. Safe to call if labels
is not an existing label.
Writes the gauge to writer
.
A 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)
.
Observed 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.
Initializes the histogram. Name must be given at comptime.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Observes value
, bucketing it based on the provided comptime buckets.
Writes the histogram to writer
.
A 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)
.
Observed 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.
init(allocator: Allocator, comptime name: []const, comptime opts: Opts, comptime ropts: RegistryOpts) !Histogram(T, L, []T)
Initializes the histogram. Name must be given at comptime.
Opts is:
help: ?[]const
- optional help text to include in the prometheus output
Deallocates the histogram
Observes value
, bucketing it based on the provided comptime buckets.
Removes the labeled value from the histogram. Safe to call if labels
is not an existing label.
Writes the histogram to writer
.