A pure Zig implementation of a Prometheus client library with idiomatic API, zero-cost abstractions, and optional thread safety.
- Core Metric Types: Counter, Gauge, and Histogram
- Type-Safe Labels: Compile-time struct labels or runtime dynamic labels
- Zero-Cost Abstractions: No overhead for unlabeled metrics
- Thread Safety: Optional atomic and mutex-based thread-safe metrics
- HTTP Handler: Built-in /metrics endpoint server
- Prometheus Text Format: Standard exposition format output
- Pure Zig: No C dependencies, idiomatic Zig API
const std = @import("std");
const promz = @import("promz");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create a registry
var registry = promz.Registry.init(allocator);
defer registry.deinit();
// Create a metrics collection struct to keep metrics in stable memory
const MyMetrics = struct {
collector: promz.MetricCollector,
requests: promz.Counter(promz.NoLabels, .{}),
fn init(alloc: std.mem.Allocator) !@This() {
return .{
.collector = try promz.MetricCollector.init(alloc, "default"),
.requests = try promz.Counter(promz.NoLabels, .{}).init(
alloc,
"requests_total",
"Total requests",
),
};
}
// IMPORTANT: Register metrics AFTER the struct is in its final location
fn register(self: *@This()) !void {
try self.collector.registerMetric(&self.requests);
}
fn deinit(self: *@This()) void {
self.requests.deinit();
self.collector.deinit();
}
};
// Initialize metrics
var metrics = try MyMetrics.init(allocator);
defer metrics.deinit();
// Register AFTER metrics struct is in final location (critical for pointer stability)
try metrics.register();
try registry.registerCollector(metrics.collector.collector());
// Use metrics
try metrics.requests.inc(.{});
try metrics.requests.add(.{}, 5.0);
// Output metrics to string
const output = try registry.gatherToString();
defer allocator.free(output);
std.debug.print("{s}", .{output});
}Zig structs are value types and get copied when returned from functions. If you register metrics with pointers before the struct is in its final location, those pointers will become invalid (dangling pointers).
Correct Pattern (as shown in Quick Start):
// 1. Create struct with metrics
var metrics = try MyMetrics.init(allocator);
// 2. Register AFTER struct is in final location
try metrics.register(); // Now pointers are stableIncorrect Pattern (will cause crashes):
fn init(allocator: std.mem.Allocator) !MyMetrics {
var result = MyMetrics{ ... };
try result.collector.registerMetric(&result.requests); // ❌ BAD! Pointer becomes invalid when struct is copied
return result; // Struct gets copied, invalidating the registered pointer
}Monotonically increasing metric (can only go up):
// Without labels
var counter = try promz.Counter(promz.NoLabels, .{}).init(
allocator,
"requests_total",
"Total HTTP requests",
);
try counter.inc(.{});
try counter.add(.{}, 5.0);
// With struct labels (compile-time type-safe)
const HttpLabels = struct {
method: []const u8,
status: []const u8,
};
var http_requests = try promz.Counter(HttpLabels, .{}).init(
allocator,
"http_requests_total",
"HTTP requests by method and status",
);
try http_requests.inc(.{ .method = "GET", .status = "200" });Metric that can increase or decrease:
var memory = try promz.Gauge(promz.NoLabels, .{}).init(
allocator,
"memory_bytes",
"Memory usage in bytes",
);
try memory.set(.{}, 1024.0);
try memory.inc(.{});
try memory.dec(.{});
try memory.add(.{}, 512.0);
try memory.sub(.{}, 256.0);Observations bucketed by configurable boundaries:
var latency = try promz.Histogram(promz.NoLabels, .{}).init(
allocator,
"request_duration_seconds",
"Request latency in seconds",
promz.BucketConfig.default(), // or .linear(), .exponential(), .custom()
);
try latency.observe(.{}, 0.123);
try latency.observe(.{}, 0.456);Enable thread-safe metrics with the thread_safe config option:
// Thread-safe counter (uses atomic operations)
var counter = try promz.Counter(promz.NoLabels, .{ .thread_safe = true }).init(
allocator,
"concurrent_requests",
"Requests from multiple threads",
);
// Thread-safe histogram (uses mutex)
var hist = try promz.Histogram(promz.NoLabels, .{ .thread_safe = true }).init(
allocator,
"processing_time",
"Processing time",
promz.BucketConfig.default(),
);For hot paths, pre-register label combinations for O(1) lookup:
const Labels = struct { method: []const u8, status: []const u8 };
var counter = try promz.Counter(Labels, .{}).init(allocator, "requests", "Total requests");
// Pre-register common label combinations
const get_200 = try counter.register(.{ .method = "GET", .status = "200" });
const post_201 = try counter.register(.{ .method = "POST", .status = "201" });
// Use handles for fast updates (O(1) array index vs HashMap lookup)
try counter.incByHandle(get_200);
try counter.addByHandle(post_201, 5.0);Enable thread-local LRU cache for faster repeated label lookups:
// Enable caching with default 32-entry cache
var counter = try promz.Counter(Labels, .{ .thread_local_cache = true }).init(...);
// Or customize cache size for your workload
var counter = try promz.Counter(Labels, .{
.thread_local_cache = true,
.cache_size = 64, // For applications with many label combinations
}).init(...);Cache sizing recommendations:
- Default (32): Handles most applications with up to 32 unique label combinations per thread
- Memory usage: ~24 bytes per cache entry per thread
- Oversizing has no performance penalty - scanning a larger cache is effectively free
Built-in HTTP server for exposing metrics:
// Create registry and register metrics (see Quick Start)
var registry = promz.Registry.init(allocator);
defer registry.deinit();
// Create and start the metrics server
var server = promz.MetricsServer.init(allocator, ®istry);
defer server.deinit();
// Blocking: serves until stopped
try server.serve(.{ .port = 9090 });
// Access metrics at http://127.0.0.1:9090/metricsFor background serving:
// Start server in background thread
try server.serveInBackground(.{ .port = 9090 });
// ... do other work ...
// Stop the server when done
server.stop();Zero-overhead metrics without labels:
var counter = try promz.Counter(promz.NoLabels, .{}).init(...);
try counter.inc(.{});Compile-time type-safe labels:
const Labels = struct {
method: []const u8,
code: []const u8,
};
var counter = try promz.Counter(Labels, .{}).init(...);
try counter.inc(.{ .method = "GET", .code = "200" });Dynamic labels (for when label names are not known at compile time):
var labels = promz.RuntimeLabels.init(allocator);
try labels.add("method", "GET");
try labels.add("code", "200");# Build the library
zig build
# Run tests
zig build test
# Run examples
zig build run-complete
zig build run-http-server
zig build run-threadsafeSee the examples/ directory for complete examples:
complete.zig- All metric types with different label configurationshttp_server.zig- HTTP server exposing /metrics endpointthreadsafe.zig- Multi-threaded metrics with thread safetybenchmark.zig- Performance benchmarks for all optimization strategies
- Zero-Cost Abstractions: NoLabels metrics use a single sample, labeled metrics use HashMap
- Compile-Time Optimization: Label types and thread safety determined at compile time
- Type Erasure: Collectors use vtables for heterogeneous metric storage
- Prometheus Compatible: Generates standard Prometheus text exposition format
MIT License - see LICENSE file for details.