├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples └── basic │ └── main.zig ├── src ├── Counter.zig ├── Gauge.zig ├── Histogram.zig ├── main.zig └── metric.zig ├── zig.mod └── zigmod.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report 3 | labels: 4 | - bug 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | # A bug means something doesn't work as expected 11 | Remember to include as much detail as possible. 12 | 13 | - type: input 14 | id: commit 15 | attributes: 16 | label: zig-prometheus commit 17 | description: "The git commit of zig-prometheus" 18 | validations: 19 | required: true 20 | 21 | - type: input 22 | id: zig_version 23 | attributes: 24 | label: Zig version 25 | description: "The output of `zig version`" 26 | placeholder: "0.11.0-dev.3335+3085e2af4" 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: repro 32 | attributes: 33 | label: Steps to reproduce 34 | description: How can someone reproduce the problem you encountered ? Include a self-contained reproducer if possible 35 | validations: 36 | required: true 37 | 38 | - type: textarea 39 | id: expected 40 | attributes: 41 | label: Expected behaviour 42 | description: What did you expect to happen? 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please describe the changes you want to make and why. Please also provide an explanation of the implementation. 4 | As a rule of thumb, give as much detail as you would want to see if you were to review this PR. 5 | 6 | If this PR closes an issue, please reference it with something like "Closes #issue". -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | schedule: 6 | - cron: "0 13 * * *" 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: goto-bus-stop/setup-zig@v2 19 | with: 20 | version: master 21 | - run: zig fmt --check *.zig src/*.zig 22 | 23 | build: 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | submodules: true 32 | - uses: goto-bus-stop/setup-zig@v2 33 | with: 34 | version: master 35 | 36 | - name: Build examples 37 | run: zig build run-example-basic 38 | 39 | - name: Build and test 40 | run: zig build test --summary all 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-* 2 | .zig-cache 3 | .zigmod 4 | deps.zig 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vincent Rischmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-prometheus 2 | 3 | This is a [Zig](https://ziglang.org/) library to add [Prometheus](https://prometheus.io/docs/concepts/data_model/)-inspired metrics to a library or application. 4 | 5 | "Inspired" because it is not strictly compatible with Prometheus, the `Histogram` type is tailored for [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). 6 | See [this blog post](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350) from the creator of `VictoriaMetrics` for details. 7 | 8 | # Requirements 9 | 10 | [Zig master](https://ziglang.org/download/) is the only required dependency. 11 | 12 | # Introduction 13 | 14 | This library only provides the following types: 15 | * A `Registry` holding a number of metrics 16 | * A `Counter` metric type 17 | * A `Gauge` metric type 18 | * A `Histogram` metric type 19 | 20 | # Examples 21 | 22 | If you want a quick overview of how to use this library check the [basic example program](examples/basic/main.zig). It showcases everything. 23 | 24 | # Reference 25 | 26 | ## Registry 27 | 28 | The `Registry` is the entry point to obtain a metric type, as well as the type capable of serializing the metrics to a writer. 29 | 30 | In an application it might be useful to have a default, global registry; in a library you probably should take one as a parameter. 31 | 32 | ### Creation 33 | 34 | Here is how to get a registry: 35 | ```zig 36 | var registry = try prometheus.Registry(.{}).create(allocator); 37 | defer registry.destroy(); 38 | ... 39 | ``` 40 | 41 | You can also configure some options for the registry: 42 | ```zig 43 | var registry = try prometheus.Registry(.{ .max_metrics = 40, .max_name_len = 300 }).create(allocator); 44 | defer registry.destroy(); 45 | ... 46 | ``` 47 | 48 | If you want to store the registry in a variable you probably want to do something like this: 49 | ```zig 50 | const Registry = prometheus.Registry(.{ .max_metrics = 40, .max_name_len = 300 }); 51 | var registry = Registry.create(allocator); 52 | defer registry.destroy(); 53 | ... 54 | ``` 55 | 56 | Now you can get metric objects which we will describe later. 57 | 58 | ### Serializing the metrics 59 | 60 | Once you have a registry you can serialize its metrics to a writer: 61 | ```zig 62 | var registry = try prometheus.Registry(.{}).create(allocator); 63 | defer registry.destroy(); 64 | 65 | ... 66 | 67 | var file = try std.fs.cwd().createFile("metrics.txt", .{}); 68 | defer file.close(); 69 | 70 | try registry.write(allocator, file.writer()); 71 | ``` 72 | 73 | The `write` method is thread safe. 74 | 75 | ## Counter 76 | 77 | The `Counter` type is an atomic integer counter. 78 | 79 | Here is an example of how to use a counter: 80 | 81 | ```zig 82 | var registry = try prometheus.Registry(.{}).create(allocator); 83 | defer registry.destroy(); 84 | 85 | var total_counter = try registry.getOrCreateCounter("http_requests_total"); 86 | var api_users_counter = try registry.getOrCreateCounter( 87 | \\http_requests{route="/api/v1/users"} 88 | ); 89 | var api_articles_counter = try registry.getOrCreateCounter( 90 | \\http_requests{route="/api/v1/articles"} 91 | ); 92 | 93 | total_counter.inc(); 94 | total_counter.dec(); 95 | total_counter.add(200); 96 | total_counter.set(2400); 97 | const counter_value = total_counter.get(); 98 | ``` 99 | 100 | All methods on a `Counter` are thread safe. 101 | 102 | ## Gauge 103 | 104 | The `Gauge` type represents a numerical value that is provided by calling a user-supplied function. 105 | 106 | A `Gauge` is created with a _state_ and a _function_ which is given that state every time it is called. 107 | 108 | For example, you can imagine a gauge returning the number of connections in a connection pool, the amount of memory allocated, etc. 109 | Basically anytime the value is instantly queryable it could be a gauge. 110 | 111 | Of course, nothing stops you from using a counter to simulate a gauge and calling `set` on it; it's up to you. 112 | 113 | Here is an example gauge: 114 | ```zig 115 | var registry = try prometheus.Registry(.{}).create(allocator); 116 | defer registry.destroy(); 117 | 118 | const Conn = struct {}; 119 | const ConnPool = struct { 120 | conns: std.ArrayList(Conn), 121 | }; 122 | var pool = ConnPool{ .conns = std.ArrayList.init(allocator) }; 123 | 124 | _ = try registry.getOrCreateGauge( 125 | "http_conn_pool_size", 126 | &pool, 127 | struct { 128 | fn get(p: *Pool) f64 { 129 | return @intToFloat(f64, p.conns.items.len); 130 | } 131 | }.get, 132 | ); 133 | ``` 134 | 135 | ## Histogram 136 | 137 | The `Histogram` type samples observations and counts them in automatically created buckets. 138 | 139 | It can be used to observe things like request duration, request size, etc. 140 | 141 | Here is a (contrived) example on how to use an histogram: 142 | ```zig 143 | var registry = try prometheus.Registry(.{}).create(allocator); 144 | defer registry.destroy(); 145 | 146 | var request_duration_histogram = try registry.getOrCreateHistogram("http_request_duration"); 147 | 148 | // Make 100 observations of some expensive operation. 149 | var i: usize = 0; 150 | while (i < 100) : (i += 1) { 151 | const start = std.time.milliTimestamp(); 152 | 153 | var j: usize = 0; 154 | var sum: usize = 0; 155 | while (j < 2000000) : (j += 1) { 156 | sum *= j; 157 | } 158 | 159 | request_duration_histogram.update(@intToFloat(f64, std.time.milliTimestamp() - start)); 160 | } 161 | ``` 162 | 163 | ## Using labels 164 | 165 | If you're read the [Prometheus data model](https://prometheus.io/docs/concepts/data_model/#notation), you've seen that a metric can have labels. 166 | 167 | Other Prometheus clients provide helpers for this, but not this library: you need to build the proper name yourself. 168 | 169 | If you have static labels then it's easy, just write the label directly like this: 170 | ```zig 171 | var http_requests_route_home = try registry.getOrCreateCounter( 172 | \\http_requests{route="/home"} 173 | ); 174 | var http_requests_route_login = try registry.getOrCreateCounter( 175 | \\http_requests{route="/login"} 176 | ); 177 | var http_requests_route_logout = try registry.getOrCreateCounter( 178 | \\http_requests{route="/logout"} 179 | ); 180 | ... 181 | ``` 182 | 183 | If you have dynamic labels you could write a helper function like this: 184 | ```zig 185 | fn getHTTPRequestsCounter( 186 | allocator: *mem.Allocator, 187 | registry: *Registry, 188 | route: []const u8, 189 | ) !*prometheus.Counter { 190 | const name = try std.fmt.allocPrint(allocator, "http_requests{{route=\"{s}\"}}", .{ 191 | route, 192 | }); 193 | return try registry.getOrCreateCounter(name); 194 | } 195 | 196 | fn handler(route: []const u8) void { 197 | var counter = getHTTPRequestsCounter(allocator, registry, route); 198 | counter.inc(); 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const lib = b.addStaticLibrary(.{ 8 | .name = "zig-prometheus", 9 | .root_source_file = b.path("src/main.zig"), 10 | .target = target, 11 | .optimize = optimize, 12 | }); 13 | b.installArtifact(lib); 14 | const main_tests = b.addTest(.{ 15 | .name = "main", 16 | .root_source_file = b.path("src/main.zig"), 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | const run_main_tests = b.addRunArtifact(main_tests); 21 | 22 | const module = b.addModule("prometheus", .{ 23 | .root_source_file = b.path("src/main.zig"), 24 | }); 25 | 26 | const examples = &[_][]const u8{ 27 | "basic", 28 | }; 29 | 30 | inline for (examples) |name| { 31 | var exe = b.addExecutable(.{ 32 | .name = "example-" ++ name, 33 | .root_source_file = b.path("examples/" ++ name ++ "/main.zig"), 34 | .target = target, 35 | .optimize = optimize, 36 | }); 37 | exe.root_module.addImport("prometheus", module); 38 | b.installArtifact(exe); 39 | 40 | const run_cmd = b.addRunArtifact(exe); 41 | run_cmd.step.dependOn(b.getInstallStep()); 42 | 43 | const run_step = b.step("run-example-" ++ name, "Run the example " ++ name); 44 | run_step.dependOn(&run_cmd.step); 45 | } 46 | 47 | const test_step = b.step("test", "Run library tests"); 48 | test_step.dependOn(&run_main_tests.step); 49 | } 50 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .prometheus, 3 | .fingerprint = 0x579d3a7b0adc830e, 4 | .version = "0.0.1", 5 | .minimum_zig_version = "0.14.0", 6 | .paths = .{ 7 | "examples", 8 | "src", 9 | "build.zig", 10 | "build.zig.zon", 11 | "LICENSE", 12 | "README.md", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const prometheus = @import("prometheus"); 4 | 5 | fn getRandomString(allocator: std.mem.Allocator, random: std.Random, n: usize) ![]const u8 { 6 | const alphabet = "abcdefghijklmnopqrstuvwxyz"; 7 | 8 | const items = try allocator.alloc(u8, n); 9 | for (items) |*item| { 10 | const random_pos = random.intRangeLessThan(usize, 0, alphabet.len); 11 | item.* = alphabet[random_pos]; 12 | } 13 | 14 | return items; 15 | } 16 | 17 | pub fn main() anyerror!void { 18 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 19 | const allocator = arena.allocator(); 20 | 21 | var prng = std.Random.DefaultPrng.init(@bitCast(std.time.milliTimestamp())); 22 | const random = prng.random(); 23 | 24 | // Initialize a registry 25 | var registry = try prometheus.Registry(.{}).create(allocator); 26 | defer registry.destroy(); 27 | 28 | // Get some counters 29 | { 30 | var i: usize = 0; 31 | while (i < 5) : (i += 1) { 32 | const name = try std.fmt.allocPrint(allocator, "http_requests_total{{route=\"/{s}\"}}", .{ 33 | try getRandomString(allocator, random, 20), 34 | }); 35 | 36 | var counter = try registry.getOrCreateCounter(name); 37 | counter.add(random.intRangeAtMost(u64, 0, 450000)); 38 | } 39 | } 40 | 41 | // Get some gauges sharing the same state. 42 | { 43 | const State = struct { 44 | random: std.Random, 45 | }; 46 | var state = State{ .random = random }; 47 | 48 | var i: usize = 0; 49 | while (i < 5) : (i += 1) { 50 | const name = try std.fmt.allocPrint(allocator, "http_conn_pool_size{{name=\"{s}\"}}", .{ 51 | try getRandomString(allocator, random, 5), 52 | }); 53 | 54 | _ = try registry.getOrCreateGauge( 55 | name, 56 | &state, 57 | struct { 58 | fn get(s: *State) f64 { 59 | const n = s.random.intRangeAtMost(usize, 0, 2000); 60 | const f = s.random.float(f64); 61 | return f * @as(f64, @floatFromInt(n)); 62 | } 63 | }.get, 64 | ); 65 | } 66 | } 67 | 68 | // Get a histogram 69 | { 70 | const name = try std.fmt.allocPrint(allocator, "http_requests_latency{{route=\"/{s}\"}}", .{ 71 | try getRandomString(allocator, random, 20), 72 | }); 73 | 74 | var histogram = try registry.getOrCreateHistogram(name); 75 | 76 | var i: usize = 0; 77 | while (i < 200) : (i += 1) { 78 | const duration = random.intRangeAtMost(usize, 0, 10000); 79 | histogram.update(@floatFromInt(duration)); 80 | } 81 | } 82 | 83 | // Finally serialize the metrics to stdout 84 | 85 | try registry.write(allocator, std.io.getStdOut().writer()); 86 | } 87 | -------------------------------------------------------------------------------- /src/Counter.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const testing = std.testing; 4 | 5 | const Metric = @import("metric.zig").Metric; 6 | 7 | const Self = @This(); 8 | 9 | metric: Metric = Metric{ 10 | .getResultFn = getResult, 11 | }, 12 | value: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), 13 | 14 | pub fn init(allocator: mem.Allocator) !*Self { 15 | const self = try allocator.create(Self); 16 | 17 | self.* = .{}; 18 | 19 | return self; 20 | } 21 | 22 | pub fn inc(self: *Self) void { 23 | _ = self.value.fetchAdd(1, .seq_cst); 24 | } 25 | 26 | pub fn dec(self: *Self) void { 27 | _ = self.value.fetchSub(1, .seq_cst); 28 | } 29 | 30 | pub fn add(self: *Self, value: anytype) void { 31 | switch (@typeInfo(@TypeOf(value))) { 32 | .int, .float, .comptime_int, .comptime_float => {}, 33 | else => @compileError("can't add a non-number"), 34 | } 35 | 36 | _ = self.value.fetchAdd(@intCast(value), .seq_cst); 37 | } 38 | 39 | pub fn get(self: *const Self) u64 { 40 | return self.value.load(.seq_cst); 41 | } 42 | 43 | pub fn set(self: *Self, value: anytype) void { 44 | switch (@typeInfo(@TypeOf(value))) { 45 | .int, .float, .comptime_int, .comptime_float => {}, 46 | else => @compileError("can't set a non-number"), 47 | } 48 | 49 | _ = self.value.store(@intCast(value), .seq_cst); 50 | } 51 | 52 | fn getResult(metric: *Metric, allocator: mem.Allocator) Metric.Error!Metric.Result { 53 | _ = allocator; 54 | 55 | const self: *Self = @fieldParentPtr("metric", metric); 56 | 57 | return Metric.Result{ .counter = self.get() }; 58 | } 59 | 60 | test "counter: inc/add/dec/set/get" { 61 | var buffer = std.ArrayList(u8).init(testing.allocator); 62 | defer buffer.deinit(); 63 | 64 | var counter = try Self.init(testing.allocator); 65 | defer testing.allocator.destroy(counter); 66 | 67 | try testing.expectEqual(@as(u64, 0), counter.get()); 68 | 69 | counter.inc(); 70 | try testing.expectEqual(@as(u64, 1), counter.get()); 71 | 72 | counter.add(200); 73 | try testing.expectEqual(@as(u64, 201), counter.get()); 74 | 75 | counter.dec(); 76 | try testing.expectEqual(@as(u64, 200), counter.get()); 77 | 78 | counter.set(43); 79 | try testing.expectEqual(@as(u64, 43), counter.get()); 80 | } 81 | 82 | test "counter: concurrent" { 83 | var counter = try Self.init(testing.allocator); 84 | defer testing.allocator.destroy(counter); 85 | 86 | var threads: [4]std.Thread = undefined; 87 | for (&threads) |*thread| { 88 | thread.* = try std.Thread.spawn( 89 | .{}, 90 | struct { 91 | fn run(c: *Self) void { 92 | var i: usize = 0; 93 | while (i < 20) : (i += 1) { 94 | c.inc(); 95 | } 96 | } 97 | }.run, 98 | .{counter}, 99 | ); 100 | } 101 | 102 | for (&threads) |*thread| thread.join(); 103 | 104 | try testing.expectEqual(@as(u64, 80), counter.get()); 105 | } 106 | 107 | test "counter: write" { 108 | var counter = try Self.init(testing.allocator); 109 | defer testing.allocator.destroy(counter); 110 | counter.set(340); 111 | 112 | var buffer = std.ArrayList(u8).init(testing.allocator); 113 | defer buffer.deinit(); 114 | 115 | var metric = &counter.metric; 116 | try metric.write(testing.allocator, buffer.writer(), "mycounter"); 117 | 118 | try testing.expectEqualStrings("mycounter 340\n", buffer.items); 119 | } 120 | -------------------------------------------------------------------------------- /src/Gauge.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const testing = std.testing; 4 | 5 | const Metric = @import("metric.zig").Metric; 6 | 7 | pub fn GaugeCallFnType(comptime StateType: type, comptime Return: type) type { 8 | const CallFnArgType = switch (@typeInfo(StateType)) { 9 | .pointer => StateType, 10 | .optional => |opt| opt.child, 11 | .void => void, 12 | else => *StateType, 13 | }; 14 | 15 | return *const fn (state: CallFnArgType) Return; 16 | } 17 | 18 | pub fn Gauge(comptime StateType: type, comptime Return: type) type { 19 | const CallFnType = GaugeCallFnType(StateType, Return); 20 | 21 | return struct { 22 | const Self = @This(); 23 | 24 | metric: Metric = .{ 25 | .getResultFn = getResult, 26 | }, 27 | callFn: CallFnType = undefined, 28 | state: StateType = undefined, 29 | 30 | pub fn init(allocator: mem.Allocator, callFn: CallFnType, state: StateType) !*Self { 31 | const self = try allocator.create(Self); 32 | 33 | self.* = .{}; 34 | self.callFn = callFn; 35 | self.state = state; 36 | 37 | return self; 38 | } 39 | 40 | pub fn get(self: *Self) Return { 41 | const TypeInfo = @typeInfo(StateType); 42 | switch (TypeInfo) { 43 | .pointer, .void => { 44 | return self.callFn(self.state); 45 | }, 46 | .optional => { 47 | if (self.state) |state| { 48 | return self.callFn(state); 49 | } 50 | return 0; 51 | }, 52 | else => { 53 | return self.callFn(&self.state); 54 | }, 55 | } 56 | } 57 | 58 | fn getResult(metric: *Metric, allocator: mem.Allocator) Metric.Error!Metric.Result { 59 | _ = allocator; 60 | 61 | const self: *Self = @fieldParentPtr("metric", metric); 62 | 63 | return switch (Return) { 64 | f64 => Metric.Result{ .gauge = self.get() }, 65 | u64 => Metric.Result{ .gauge_int = self.get() }, 66 | else => unreachable, // Gauge Return may only be 'f64' or 'u64' 67 | }; 68 | } 69 | }; 70 | } 71 | 72 | test "gauge: get" { 73 | const TestCase = struct { 74 | state_type: type, 75 | typ: type, 76 | }; 77 | 78 | const testCases = [_]TestCase{ 79 | .{ 80 | .state_type = struct { 81 | value: f64, 82 | }, 83 | .typ = f64, 84 | }, 85 | }; 86 | 87 | inline for (testCases) |tc| { 88 | const State = tc.state_type; 89 | const InnerType = tc.typ; 90 | 91 | var state = State{ .value = 20 }; 92 | 93 | var gauge = try Gauge(*State, InnerType).init( 94 | testing.allocator, 95 | struct { 96 | fn get(s: *State) InnerType { 97 | return s.value + 1; 98 | } 99 | }.get, 100 | &state, 101 | ); 102 | defer testing.allocator.destroy(gauge); 103 | 104 | try testing.expectEqual(@as(InnerType, 21), gauge.get()); 105 | } 106 | } 107 | 108 | test "gauge: optional state" { 109 | const State = struct { 110 | value: f64, 111 | }; 112 | var state = State{ .value = 20.0 }; 113 | 114 | var gauge = try Gauge(?*State, f64).init( 115 | testing.allocator, 116 | struct { 117 | fn get(s: *State) f64 { 118 | return s.value + 1.0; 119 | } 120 | }.get, 121 | &state, 122 | ); 123 | defer testing.allocator.destroy(gauge); 124 | 125 | try testing.expectEqual(@as(f64, 21.0), gauge.get()); 126 | } 127 | 128 | test "gauge: non-pointer state" { 129 | var gauge = try Gauge(f64, f64).init( 130 | testing.allocator, 131 | struct { 132 | fn get(s: *f64) f64 { 133 | s.* += 1.0; 134 | return s.*; 135 | } 136 | }.get, 137 | 0.0, 138 | ); 139 | defer testing.allocator.destroy(gauge); 140 | 141 | try testing.expectEqual(@as(f64, 1.0), gauge.get()); 142 | } 143 | 144 | test "gauge: shared state" { 145 | const State = struct { 146 | mutex: std.Thread.Mutex = .{}, 147 | items: std.ArrayList(usize) = std.ArrayList(usize).init(testing.allocator), 148 | }; 149 | var shared_state = State{}; 150 | defer shared_state.items.deinit(); 151 | 152 | var gauge = try Gauge(*State, f64).init( 153 | testing.allocator, 154 | struct { 155 | fn get(state: *State) f64 { 156 | return @floatFromInt(state.items.items.len); 157 | } 158 | }.get, 159 | &shared_state, 160 | ); 161 | defer testing.allocator.destroy(gauge); 162 | 163 | var threads: [4]std.Thread = undefined; 164 | for (&threads, 0..) |*thread, thread_index| { 165 | thread.* = try std.Thread.spawn( 166 | .{}, 167 | struct { 168 | fn run(thread_idx: usize, state: *State) !void { 169 | var i: usize = 0; 170 | while (i < 4) : (i += 1) { 171 | state.mutex.lock(); 172 | defer state.mutex.unlock(); 173 | try state.items.append(thread_idx + i); 174 | } 175 | } 176 | }.run, 177 | .{ thread_index, &shared_state }, 178 | ); 179 | } 180 | 181 | for (&threads) |*thread| thread.join(); 182 | 183 | try testing.expectEqual(@as(usize, 16), @as(usize, @intFromFloat(gauge.get()))); 184 | } 185 | 186 | test "gauge: write" { 187 | var gauge = try Gauge(usize, f64).init( 188 | testing.allocator, 189 | struct { 190 | fn get(state: *usize) f64 { 191 | state.* += 340; 192 | return @floatFromInt(state.*); 193 | } 194 | }.get, 195 | @as(usize, 0), 196 | ); 197 | defer testing.allocator.destroy(gauge); 198 | 199 | var buffer = std.ArrayList(u8).init(testing.allocator); 200 | defer buffer.deinit(); 201 | 202 | var metric = &gauge.metric; 203 | try metric.write(testing.allocator, buffer.writer(), "mygauge"); 204 | 205 | try testing.expectEqualStrings("mygauge 340.000000\n", buffer.items); 206 | } 207 | -------------------------------------------------------------------------------- /src/Histogram.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const math = std.math; 4 | const mem = std.mem; 5 | const testing = std.testing; 6 | 7 | const Metric = @import("metric.zig").Metric; 8 | const HistogramResult = @import("metric.zig").HistogramResult; 9 | 10 | const e10_min = -9; 11 | const e10_max = 18; 12 | const buckets_per_decimal = 18; 13 | const decimal_buckets_count = e10_max - e10_min; 14 | const buckets_count = decimal_buckets_count * buckets_per_decimal; 15 | 16 | const lower_bucket_range = fmt.comptimePrint("0...{e:.3}", .{math.pow(f64, 10, e10_min)}); 17 | const upper_bucket_range = fmt.comptimePrint("{e:.3}...+Inf", .{math.pow(f64, 10, e10_max)}); 18 | 19 | const bucket_ranges: [buckets_count][]const u8 = blk: { 20 | const bucket_multiplier = math.pow(f64, 10.0, 1.0 / @as(f64, buckets_per_decimal)); 21 | 22 | var v = math.pow(f64, 10, e10_min); 23 | 24 | var start = blk2: { 25 | var buf: [64]u8 = undefined; 26 | break :blk2 fmt.bufPrint(&buf, "{e:.3}", .{v}) catch unreachable; 27 | }; 28 | 29 | var result: [buckets_count][]const u8 = undefined; 30 | for (&result) |*range| { 31 | v *= bucket_multiplier; 32 | 33 | const end = blk3: { 34 | var buf: [64]u8 = undefined; 35 | break :blk3 fmt.bufPrint(&buf, "{e:.3}", .{v}) catch unreachable; 36 | }; 37 | 38 | range.* = start ++ "..." ++ end; 39 | 40 | start = end; 41 | } 42 | 43 | break :blk result; 44 | }; 45 | 46 | test "bucket ranges" { 47 | try testing.expectEqualStrings("0...1.000e-9", lower_bucket_range); 48 | try testing.expectEqualStrings("1.000e18...+Inf", upper_bucket_range); 49 | 50 | try testing.expectEqualStrings("1.000e-9...1.136e-9", bucket_ranges[0]); 51 | try testing.expectEqualStrings("1.136e-9...1.292e-9", bucket_ranges[1]); 52 | try testing.expectEqualStrings("8.799e-9...1.000e-8", bucket_ranges[buckets_per_decimal - 1]); 53 | try testing.expectEqualStrings("1.000e-8...1.136e-8", bucket_ranges[buckets_per_decimal]); 54 | try testing.expectEqualStrings("8.799e-1...1.000e0", bucket_ranges[buckets_per_decimal * (-e10_min) - 1]); 55 | try testing.expectEqualStrings("1.000e0...1.136e0", bucket_ranges[buckets_per_decimal * (-e10_min)]); 56 | try testing.expectEqualStrings("8.799e17...1.000e18", bucket_ranges[buckets_per_decimal * (e10_max - e10_min) - 1]); 57 | } 58 | 59 | /// Histogram based on https://github.com/VictoriaMetrics/metrics/blob/master/histogram.go. 60 | pub const Histogram = struct { 61 | const Self = @This(); 62 | 63 | metric: Metric = .{ 64 | .getResultFn = getResult, 65 | }, 66 | 67 | mutex: std.Thread.Mutex = .{}, 68 | decimal_buckets: [decimal_buckets_count][buckets_per_decimal]u64 = undefined, 69 | 70 | lower: u64 = 0, 71 | upper: u64 = 0, 72 | 73 | sum: f64 = 0.0, 74 | 75 | pub fn init(allocator: mem.Allocator) !*Self { 76 | const self = try allocator.create(Self); 77 | 78 | self.* = .{}; 79 | for (&self.decimal_buckets) |*bucket| { 80 | @memset(bucket, 0); 81 | } 82 | 83 | return self; 84 | } 85 | 86 | pub fn update(self: *Self, value: f64) void { 87 | if (math.isNan(value) or value < 0) { 88 | return; 89 | } 90 | 91 | const bucket_idx: f64 = (math.log10(value) - e10_min) * buckets_per_decimal; 92 | 93 | // Keep a lock while updating the histogram. 94 | self.mutex.lock(); 95 | defer self.mutex.unlock(); 96 | 97 | self.sum += value; 98 | 99 | if (bucket_idx < 0) { 100 | self.lower += 1; 101 | } else if (bucket_idx >= buckets_count) { 102 | self.upper += 1; 103 | } else { 104 | const idx: usize = blk: { 105 | const tmp: usize = @intFromFloat(bucket_idx); 106 | 107 | if (bucket_idx == @as(f64, @floatFromInt(tmp)) and tmp > 0) { 108 | // Edge case for 10^n values, which must go to the lower bucket 109 | // according to Prometheus logic for `le`-based histograms. 110 | break :blk tmp - 1; 111 | } else { 112 | break :blk tmp; 113 | } 114 | }; 115 | 116 | const decimal_bucket_idx = idx / buckets_per_decimal; 117 | const offset = idx % buckets_per_decimal; 118 | 119 | var bucket: []u64 = &self.decimal_buckets[decimal_bucket_idx]; 120 | bucket[offset] += 1; 121 | } 122 | } 123 | 124 | pub fn get(self: *const Self) u64 { 125 | _ = self; 126 | return 0; 127 | } 128 | 129 | fn isBucketAllZero(bucket: []const u64) bool { 130 | for (bucket) |v| { 131 | if (v != 0) return false; 132 | } 133 | return true; 134 | } 135 | 136 | fn getResult(metric: *Metric, allocator: mem.Allocator) Metric.Error!Metric.Result { 137 | const self: *Histogram = @fieldParentPtr("metric", metric); 138 | 139 | // Arbitrary maximum capacity 140 | var buckets = try std.ArrayList(HistogramResult.Bucket).initCapacity(allocator, 16); 141 | var count_total: u64 = 0; 142 | 143 | // Keep a lock while querying the histogram. 144 | self.mutex.lock(); 145 | defer self.mutex.unlock(); 146 | 147 | if (self.lower > 0) { 148 | try buckets.append(.{ 149 | .vmrange = lower_bucket_range, 150 | .count = self.lower, 151 | }); 152 | count_total += self.lower; 153 | } 154 | 155 | for (&self.decimal_buckets, 0..) |bucket, decimal_bucket_idx| { 156 | if (isBucketAllZero(&bucket)) continue; 157 | 158 | for (bucket, 0..) |count, offset| { 159 | if (count <= 0) continue; 160 | 161 | const bucket_idx = (decimal_bucket_idx * buckets_per_decimal) + offset; 162 | const vmrange = bucket_ranges[bucket_idx]; 163 | 164 | try buckets.append(.{ 165 | .vmrange = vmrange, 166 | .count = count, 167 | }); 168 | count_total += count; 169 | } 170 | } 171 | 172 | if (self.upper > 0) { 173 | try buckets.append(.{ 174 | .vmrange = upper_bucket_range, 175 | .count = self.upper, 176 | }); 177 | count_total += self.upper; 178 | } 179 | 180 | return Metric.Result{ 181 | .histogram = .{ 182 | .buckets = try buckets.toOwnedSlice(), 183 | .sum = .{ .value = self.sum }, 184 | .count = count_total, 185 | }, 186 | }; 187 | } 188 | }; 189 | 190 | test "write empty" { 191 | var histogram = try Histogram.init(testing.allocator); 192 | defer testing.allocator.destroy(histogram); 193 | 194 | var buffer = std.ArrayList(u8).init(testing.allocator); 195 | defer buffer.deinit(); 196 | 197 | var metric = &histogram.metric; 198 | try metric.write(testing.allocator, buffer.writer(), "myhistogram"); 199 | 200 | try testing.expectEqual(@as(usize, 0), buffer.items.len); 201 | } 202 | 203 | test "update then write" { 204 | var histogram = try Histogram.init(testing.allocator); 205 | defer testing.allocator.destroy(histogram); 206 | 207 | var i: usize = 98; 208 | while (i < 218) : (i += 1) { 209 | histogram.update(@floatFromInt(i)); 210 | } 211 | 212 | var buffer = std.ArrayList(u8).init(testing.allocator); 213 | defer buffer.deinit(); 214 | 215 | var metric = &histogram.metric; 216 | try metric.write(testing.allocator, buffer.writer(), "myhistogram"); 217 | 218 | const exp = 219 | \\myhistogram_bucket{vmrange="8.799e1...1.000e2"} 3 220 | \\myhistogram_bucket{vmrange="1.000e2...1.136e2"} 13 221 | \\myhistogram_bucket{vmrange="1.136e2...1.292e2"} 16 222 | \\myhistogram_bucket{vmrange="1.292e2...1.468e2"} 17 223 | \\myhistogram_bucket{vmrange="1.468e2...1.668e2"} 20 224 | \\myhistogram_bucket{vmrange="1.668e2...1.896e2"} 23 225 | \\myhistogram_bucket{vmrange="1.896e2...2.154e2"} 26 226 | \\myhistogram_bucket{vmrange="2.154e2...2.448e2"} 2 227 | \\myhistogram_sum 18900 228 | \\myhistogram_count 120 229 | \\ 230 | ; 231 | 232 | try testing.expectEqualStrings(exp, buffer.items); 233 | } 234 | 235 | test "update then write with labels" { 236 | var histogram = try Histogram.init(testing.allocator); 237 | defer testing.allocator.destroy(histogram); 238 | 239 | var i: usize = 98; 240 | while (i < 218) : (i += 1) { 241 | histogram.update(@floatFromInt(i)); 242 | } 243 | 244 | var buffer = std.ArrayList(u8).init(testing.allocator); 245 | defer buffer.deinit(); 246 | 247 | var metric = &histogram.metric; 248 | try metric.write(testing.allocator, buffer.writer(), "myhistogram{route=\"/api/v2/users\"}"); 249 | 250 | const exp = 251 | \\myhistogram_bucket{route="/api/v2/users",vmrange="8.799e1...1.000e2"} 3 252 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.000e2...1.136e2"} 13 253 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.136e2...1.292e2"} 16 254 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.292e2...1.468e2"} 17 255 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.468e2...1.668e2"} 20 256 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.668e2...1.896e2"} 23 257 | \\myhistogram_bucket{route="/api/v2/users",vmrange="1.896e2...2.154e2"} 26 258 | \\myhistogram_bucket{route="/api/v2/users",vmrange="2.154e2...2.448e2"} 2 259 | \\myhistogram_sum{route="/api/v2/users"} 18900 260 | \\myhistogram_count{route="/api/v2/users"} 120 261 | \\ 262 | ; 263 | 264 | try testing.expectEqualStrings(exp, buffer.items); 265 | } 266 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const hash_map = std.hash_map; 4 | const heap = std.heap; 5 | const mem = std.mem; 6 | const testing = std.testing; 7 | 8 | const Metric = @import("metric.zig").Metric; 9 | pub const Counter = @import("Counter.zig"); 10 | pub const Gauge = @import("Gauge.zig").Gauge; 11 | pub const Histogram = @import("Histogram.zig").Histogram; 12 | pub const GaugeCallFnType = @import("Gauge.zig").GaugeCallFnType; 13 | 14 | pub const GetMetricError = error{ 15 | // Returned when trying to add a metric to an already full registry. 16 | TooManyMetrics, 17 | // Returned when the name of name is bigger than the configured max_name_len. 18 | NameTooLong, 19 | 20 | OutOfMemory, 21 | }; 22 | 23 | const RegistryOptions = struct { 24 | max_metrics: comptime_int = 8192, 25 | max_name_len: comptime_int = 1024, 26 | }; 27 | 28 | pub fn Registry(comptime options: RegistryOptions) type { 29 | return struct { 30 | const Self = @This(); 31 | 32 | const MetricMap = hash_map.StringHashMapUnmanaged(*Metric); 33 | 34 | root_allocator: mem.Allocator, 35 | 36 | arena_state: heap.ArenaAllocator, 37 | 38 | mutex: std.Thread.Mutex, 39 | metrics: MetricMap, 40 | 41 | pub fn create(allocator: mem.Allocator) !*Self { 42 | const self = try allocator.create(Self); 43 | 44 | self.* = .{ 45 | .root_allocator = allocator, 46 | .arena_state = heap.ArenaAllocator.init(allocator), 47 | .mutex = .{}, 48 | .metrics = MetricMap{}, 49 | }; 50 | 51 | return self; 52 | } 53 | 54 | pub fn destroy(self: *Self) void { 55 | self.arena_state.deinit(); 56 | self.root_allocator.destroy(self); 57 | } 58 | 59 | fn nbMetrics(self: *const Self) usize { 60 | return self.metrics.count(); 61 | } 62 | 63 | pub fn getOrCreateCounter(self: *Self, name: []const u8) GetMetricError!*Counter { 64 | if (self.nbMetrics() >= options.max_metrics) return error.TooManyMetrics; 65 | if (name.len > options.max_name_len) return error.NameTooLong; 66 | 67 | var allocator = self.arena_state.allocator(); 68 | 69 | const duped_name = try allocator.dupe(u8, name); 70 | 71 | self.mutex.lock(); 72 | defer self.mutex.unlock(); 73 | 74 | const gop = try self.metrics.getOrPut(allocator, duped_name); 75 | if (!gop.found_existing) { 76 | var real_metric = try Counter.init(allocator); 77 | gop.value_ptr.* = &real_metric.metric; 78 | } 79 | 80 | return @fieldParentPtr("metric", gop.value_ptr.*); 81 | } 82 | 83 | pub fn getOrCreateHistogram(self: *Self, name: []const u8) GetMetricError!*Histogram { 84 | if (self.nbMetrics() >= options.max_metrics) return error.TooManyMetrics; 85 | if (name.len > options.max_name_len) return error.NameTooLong; 86 | 87 | var allocator = self.arena_state.allocator(); 88 | 89 | const duped_name = try allocator.dupe(u8, name); 90 | 91 | self.mutex.lock(); 92 | defer self.mutex.unlock(); 93 | 94 | const gop = try self.metrics.getOrPut(allocator, duped_name); 95 | if (!gop.found_existing) { 96 | var real_metric = try Histogram.init(allocator); 97 | gop.value_ptr.* = &real_metric.metric; 98 | } 99 | 100 | return @fieldParentPtr("metric", gop.value_ptr.*); 101 | } 102 | 103 | pub fn getOrCreateGauge( 104 | self: *Self, 105 | name: []const u8, 106 | state: anytype, 107 | callFn: GaugeCallFnType(@TypeOf(state), f64), 108 | ) GetMetricError!*Gauge(@TypeOf(state), f64) { 109 | if (self.nbMetrics() >= options.max_metrics) return error.TooManyMetrics; 110 | if (name.len > options.max_name_len) return error.NameTooLong; 111 | 112 | var allocator = self.arena_state.allocator(); 113 | 114 | const duped_name = try allocator.dupe(u8, name); 115 | 116 | self.mutex.lock(); 117 | defer self.mutex.unlock(); 118 | 119 | const gop = try self.metrics.getOrPut(allocator, duped_name); 120 | if (!gop.found_existing) { 121 | var real_metric = try Gauge(@TypeOf(state), f64).init(allocator, callFn, state); 122 | gop.value_ptr.* = &real_metric.metric; 123 | } 124 | 125 | return @fieldParentPtr("metric", gop.value_ptr.*); 126 | } 127 | 128 | pub fn getOrCreateGaugeInt( 129 | self: *Self, 130 | name: []const u8, 131 | state: anytype, 132 | callFn: GaugeCallFnType(@TypeOf(state), u64), 133 | ) GetMetricError!*Gauge(@TypeOf(state), u64) { 134 | if (self.nbMetrics() >= options.max_metrics) return error.TooManyMetrics; 135 | if (name.len > options.max_name_len) return error.NameTooLong; 136 | 137 | var allocator = self.arena_state.allocator(); 138 | 139 | const duped_name = try allocator.dupe(u8, name); 140 | 141 | self.mutex.lock(); 142 | defer self.mutex.unlock(); 143 | 144 | const gop = try self.metrics.getOrPut(allocator, duped_name); 145 | if (!gop.found_existing) { 146 | var real_metric = try Gauge(@TypeOf(state), u64).init(allocator, callFn, state); 147 | gop.value_ptr.* = &real_metric.metric; 148 | } 149 | 150 | return @fieldParentPtr("metric", gop.value_ptr.*); 151 | } 152 | 153 | pub fn write(self: *Self, allocator: mem.Allocator, writer: anytype) !void { 154 | var arena_state = heap.ArenaAllocator.init(allocator); 155 | defer arena_state.deinit(); 156 | 157 | self.mutex.lock(); 158 | defer self.mutex.unlock(); 159 | 160 | try writeMetrics(arena_state.allocator(), self.metrics, writer); 161 | } 162 | 163 | fn writeMetrics(allocator: mem.Allocator, map: MetricMap, writer: anytype) !void { 164 | // Get the keys, sorted 165 | const keys = blk: { 166 | var key_list = try std.ArrayList([]const u8).initCapacity(allocator, map.count()); 167 | 168 | var key_iter = map.keyIterator(); 169 | while (key_iter.next()) |key| { 170 | key_list.appendAssumeCapacity(key.*); 171 | } 172 | 173 | break :blk key_list.items; 174 | }; 175 | defer allocator.free(keys); 176 | 177 | std.mem.sort([]const u8, keys, {}, stringLessThan); 178 | 179 | // Write each metric in key order 180 | for (keys) |key| { 181 | var metric = map.get(key) orelse unreachable; 182 | try metric.write(allocator, writer, key); 183 | } 184 | } 185 | }; 186 | } 187 | 188 | fn stringLessThan(context: void, lhs: []const u8, rhs: []const u8) bool { 189 | _ = context; 190 | return mem.lessThan(u8, lhs, rhs); 191 | } 192 | 193 | test "registry getOrCreateCounter" { 194 | var registry = try Registry(.{}).create(testing.allocator); 195 | defer registry.destroy(); 196 | 197 | const name = try fmt.allocPrint(testing.allocator, "http_requests{{status=\"{d}\"}}", .{500}); 198 | defer testing.allocator.free(name); 199 | 200 | var i: usize = 0; 201 | while (i < 10) : (i += 1) { 202 | var counter = try registry.getOrCreateCounter(name); 203 | counter.inc(); 204 | } 205 | 206 | var counter = try registry.getOrCreateCounter(name); 207 | try testing.expectEqual(@as(u64, 10), counter.get()); 208 | } 209 | 210 | test "registry write" { 211 | const TestCase = struct { 212 | counter_name: []const u8, 213 | gauge_name: []const u8, 214 | histogram_name: []const u8, 215 | exp: []const u8, 216 | }; 217 | 218 | const exp1 = 219 | \\http_conn_pool_size 4.000000 220 | \\http_request_size_bucket{vmrange="1.292e2...1.468e2"} 1 221 | \\http_request_size_bucket{vmrange="4.642e2...5.275e2"} 1 222 | \\http_request_size_bucket{vmrange="1.136e3...1.292e3"} 1 223 | \\http_request_size_sum 1870.360000 224 | \\http_request_size_count 3 225 | \\http_requests 2 226 | \\ 227 | ; 228 | 229 | const exp2 = 230 | \\http_conn_pool_size{route="/api/v2/users"} 4.000000 231 | \\http_request_size_bucket{route="/api/v2/users",vmrange="1.292e2...1.468e2"} 1 232 | \\http_request_size_bucket{route="/api/v2/users",vmrange="4.642e2...5.275e2"} 1 233 | \\http_request_size_bucket{route="/api/v2/users",vmrange="1.136e3...1.292e3"} 1 234 | \\http_request_size_sum{route="/api/v2/users"} 1870.360000 235 | \\http_request_size_count{route="/api/v2/users"} 3 236 | \\http_requests{route="/api/v2/users"} 2 237 | \\ 238 | ; 239 | 240 | const test_cases = &[_]TestCase{ 241 | .{ 242 | .counter_name = "http_requests", 243 | .gauge_name = "http_conn_pool_size", 244 | .histogram_name = "http_request_size", 245 | .exp = exp1, 246 | }, 247 | .{ 248 | .counter_name = "http_requests{route=\"/api/v2/users\"}", 249 | .gauge_name = "http_conn_pool_size{route=\"/api/v2/users\"}", 250 | .histogram_name = "http_request_size{route=\"/api/v2/users\"}", 251 | .exp = exp2, 252 | }, 253 | }; 254 | 255 | inline for (test_cases) |tc| { 256 | var registry = try Registry(.{}).create(testing.allocator); 257 | defer registry.destroy(); 258 | 259 | // Add some counters 260 | { 261 | var counter = try registry.getOrCreateCounter(tc.counter_name); 262 | counter.set(2); 263 | } 264 | 265 | // Add some gauges 266 | { 267 | _ = try registry.getOrCreateGauge( 268 | tc.gauge_name, 269 | @as(f64, 4.0), 270 | struct { 271 | fn get(s: *f64) f64 { 272 | return s.*; 273 | } 274 | }.get, 275 | ); 276 | } 277 | 278 | // Add an histogram 279 | { 280 | var histogram = try registry.getOrCreateHistogram(tc.histogram_name); 281 | 282 | histogram.update(500.12); 283 | histogram.update(1230.240); 284 | histogram.update(140); 285 | } 286 | 287 | // Write to a buffer 288 | { 289 | var buffer = std.ArrayList(u8).init(testing.allocator); 290 | defer buffer.deinit(); 291 | 292 | try registry.write(testing.allocator, buffer.writer()); 293 | 294 | try testing.expectEqualStrings(tc.exp, buffer.items); 295 | } 296 | 297 | // Write to a file 298 | { 299 | const filename = "prometheus_metrics.txt"; 300 | var file = try std.fs.cwd().createFile(filename, .{ .read = true }); 301 | defer { 302 | file.close(); 303 | std.fs.cwd().deleteFile(filename) catch {}; 304 | } 305 | 306 | try registry.write(testing.allocator, file.writer()); 307 | 308 | try file.seekTo(0); 309 | const file_data = try file.readToEndAlloc(testing.allocator, std.math.maxInt(usize)); 310 | defer testing.allocator.free(file_data); 311 | 312 | try testing.expectEqualStrings(tc.exp, file_data); 313 | } 314 | } 315 | } 316 | 317 | test "registry options" { 318 | var registry = try Registry(.{ .max_metrics = 1, .max_name_len = 4 }).create(testing.allocator); 319 | defer registry.destroy(); 320 | 321 | { 322 | try testing.expectError(error.NameTooLong, registry.getOrCreateCounter("hello")); 323 | _ = try registry.getOrCreateCounter("foo"); 324 | } 325 | 326 | { 327 | try testing.expectError(error.TooManyMetrics, registry.getOrCreateCounter("bar")); 328 | } 329 | } 330 | 331 | test { 332 | testing.refAllDecls(@This()); 333 | } 334 | -------------------------------------------------------------------------------- /src/metric.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const mem = std.mem; 4 | const testing = std.testing; 5 | 6 | pub const HistogramResult = struct { 7 | pub const Bucket = struct { 8 | vmrange: []const u8, 9 | count: u64, 10 | }; 11 | 12 | pub const SumValue = struct { 13 | value: f64 = 0, 14 | 15 | pub fn format(self: @This(), comptime format_string: []const u8, options: fmt.FormatOptions, writer: anytype) !void { 16 | _ = format_string; 17 | 18 | const as_int: u64 = @intFromFloat(self.value); 19 | if (@as(f64, @floatFromInt(as_int)) == self.value) { 20 | try fmt.formatInt(as_int, 10, .lower, options, writer); 21 | } else { 22 | // FIXME: Use the writer directly once the formatFloat API accepts a writer 23 | var buf: [fmt.format_float.bufferSize(.decimal, @TypeOf(self.value))]u8 = undefined; 24 | const formatted = try fmt.formatFloat(&buf, self.value, .{ 25 | .mode = .decimal, 26 | .precision = options.precision, 27 | }); 28 | try writer.writeAll(formatted); 29 | } 30 | } 31 | }; 32 | 33 | buckets: []Bucket, 34 | sum: SumValue, 35 | count: u64, 36 | }; 37 | 38 | pub const Metric = struct { 39 | pub const Error = error{OutOfMemory} || std.posix.WriteError || std.http.Server.Response.WriteError; 40 | 41 | pub const Result = union(enum) { 42 | const Self = @This(); 43 | 44 | counter: u64, 45 | gauge: f64, 46 | gauge_int: u64, 47 | histogram: HistogramResult, 48 | 49 | pub fn deinit(self: Self, allocator: mem.Allocator) void { 50 | switch (self) { 51 | .histogram => |v| { 52 | allocator.free(v.buckets); 53 | }, 54 | else => {}, 55 | } 56 | } 57 | }; 58 | 59 | getResultFn: *const fn (self: *Metric, allocator: mem.Allocator) Error!Result, 60 | 61 | pub fn write(self: *Metric, allocator: mem.Allocator, writer: anytype, name: []const u8) Error!void { 62 | const result = try self.getResultFn(self, allocator); 63 | defer result.deinit(allocator); 64 | 65 | switch (result) { 66 | .counter, .gauge_int => |v| { 67 | return try writer.print("{s} {d}\n", .{ name, v }); 68 | }, 69 | .gauge => |v| { 70 | return try writer.print("{s} {d:.6}\n", .{ name, v }); 71 | }, 72 | .histogram => |v| { 73 | if (v.buckets.len <= 0) return; 74 | 75 | const name_and_labels = splitName(name); 76 | 77 | if (name_and_labels.labels.len > 0) { 78 | for (v.buckets) |bucket| { 79 | try writer.print("{s}_bucket{{{s},vmrange=\"{s}\"}} {d:.6}\n", .{ 80 | name_and_labels.name, 81 | name_and_labels.labels, 82 | bucket.vmrange, 83 | bucket.count, 84 | }); 85 | } 86 | try writer.print("{s}_sum{{{s}}} {:.6}\n", .{ 87 | name_and_labels.name, 88 | name_and_labels.labels, 89 | v.sum, 90 | }); 91 | try writer.print("{s}_count{{{s}}} {d}\n", .{ 92 | name_and_labels.name, 93 | name_and_labels.labels, 94 | v.count, 95 | }); 96 | } else { 97 | for (v.buckets) |bucket| { 98 | try writer.print("{s}_bucket{{vmrange=\"{s}\"}} {d:.6}\n", .{ 99 | name_and_labels.name, 100 | bucket.vmrange, 101 | bucket.count, 102 | }); 103 | } 104 | try writer.print("{s}_sum {:.6}\n", .{ 105 | name_and_labels.name, 106 | v.sum, 107 | }); 108 | try writer.print("{s}_count {d}\n", .{ 109 | name_and_labels.name, 110 | v.count, 111 | }); 112 | } 113 | }, 114 | } 115 | } 116 | }; 117 | 118 | const NameAndLabels = struct { 119 | name: []const u8, 120 | labels: []const u8 = "", 121 | }; 122 | 123 | fn splitName(name: []const u8) NameAndLabels { 124 | const bracket_pos = mem.indexOfScalar(u8, name, '{'); 125 | if (bracket_pos) |pos| { 126 | return NameAndLabels{ 127 | .name = name[0..pos], 128 | .labels = name[pos + 1 .. name.len - 1], 129 | }; 130 | } else { 131 | return NameAndLabels{ 132 | .name = name, 133 | }; 134 | } 135 | } 136 | 137 | test "splitName" { 138 | const TestCase = struct { 139 | input: []const u8, 140 | exp: NameAndLabels, 141 | }; 142 | 143 | const test_cases = &[_]TestCase{ 144 | .{ 145 | .input = "foobar", 146 | .exp = .{ 147 | .name = "foobar", 148 | }, 149 | }, 150 | .{ 151 | .input = "foobar{route=\"/home\"}", 152 | .exp = .{ 153 | .name = "foobar", 154 | .labels = "route=\"/home\"", 155 | }, 156 | }, 157 | .{ 158 | .input = "foobar{route=\"/home\",status=\"500\"}", 159 | .exp = .{ 160 | .name = "foobar", 161 | .labels = "route=\"/home\",status=\"500\"", 162 | }, 163 | }, 164 | }; 165 | 166 | inline for (test_cases) |tc| { 167 | const res = splitName(tc.input); 168 | 169 | try testing.expectEqualStrings(tc.exp.name, res.name); 170 | try testing.expectEqualStrings(tc.exp.labels, res.labels); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: fjo426m4bfzytdolx8yx7vnd96rcugst93qjnvpwk0c2yi4g 2 | name: prometheus 3 | main: src/main.zig 4 | license: MIT 5 | description: Prometheus client library 6 | root_dependencies: [] 7 | -------------------------------------------------------------------------------- /zigmod.lock: -------------------------------------------------------------------------------- 1 | 2 2 | git https://github.com/Luukdegram/apple_pie commit-5eaaabdced4f9b8d6cee947b465e7ea16ea61f42 3 | --------------------------------------------------------------------------------