diff --git a/src/metrics/instrument.zig b/src/metrics/instrument.zig index 6cf2fd9..5256837 100644 --- a/src/metrics/instrument.zig +++ b/src/metrics/instrument.zig @@ -2,6 +2,7 @@ const std = @import("std"); const protobuf = @import("protobuf"); const ManagedString = protobuf.ManagedString; const pbcommon = @import("../opentelemetry/proto/common/v1.pb.zig"); +const pbmetrics = @import("../opentelemetry/proto/metrics/v1.pb.zig"); const pbutils = @import("../pbutils.zig"); const spec = @import("spec.zig"); @@ -21,9 +22,28 @@ pub const Kind = enum { } }; +const SupportedInstrument = union(enum) { + Counter_u16: Counter(u16), + Counter_u32: Counter(u32), + Counter_u64: Counter(u64), + UpDownCounter_i16: Counter(i16), + UpDownCounter_i32: Counter(i32), + UpDownCounter_i64: Counter(i64), + Histogram_u16: Histogram(u16), + Histogram_u32: Histogram(u32), + Histogram_u64: Histogram(u64), + Histogram_f32: Histogram(f32), + Histogram_f64: Histogram(f64), + Gauge_i16: Gauge(i16), + Gauge_i32: Gauge(i32), + Gauge_i64: Gauge(i64), + Gauge_f32: Gauge(f32), + Gauge_f64: Gauge(f64), +}; + // TODO this should be the abstraction containing all instruments. // We should have a single struct that contains all the instruments. -// The current Counter(T), Histogra(T) and Gauge(T) should be part of the instrument and +// The current Counter(T), Histogram(T) and Gauge(T) should be part of the instrument and // when the Meter wants to create a new instrument, it should call the appropriate method. //In this way, storing the instruments in a single hashmap also contains the concrete type of the instrument. pub const Instrument = struct { @@ -32,24 +52,7 @@ pub const Instrument = struct { allocator: std.mem.Allocator, kind: Kind, opts: InstrumentOptions, - data: union(enum) { - Counter_u16: Counter(u16), - Counter_u32: Counter(u32), - Counter_u64: Counter(u64), - UpDownCounter_i16: Counter(i16), - UpDownCounter_i32: Counter(i32), - UpDownCounter_i64: Counter(i64), - Histogram_u16: Histogram(u16), - Histogram_u32: Histogram(u32), - Histogram_u64: Histogram(u64), - Histogram_f32: Histogram(f32), - Histogram_f64: Histogram(f64), - Gauge_i16: Gauge(i16), - Gauge_i32: Gauge(i32), - Gauge_i64: Gauge(i64), - Gauge_f32: Gauge(f32), - Gauge_f64: Gauge(f64), - }, + data: SupportedInstrument, pub fn Get(kind: Kind, opts: InstrumentOptions, allocator: std.mem.Allocator) !Self { // Validate name, unit anddescription, optionally throw an error if non conformant. @@ -150,21 +153,23 @@ pub const HistogramOptions = struct { pub fn Counter(comptime T: type) type { return struct { const Self = @This(); + allocator: std.mem.Allocator, // We should keep track of the current value of the counter for each unique comibination of attribute. // At the same time, we don't want to allocate memory for each attribute set that comes in. // So we store all the counters in a single array and keep track of the state of each counter. - cumulative: std.AutoHashMap(u64, T), + cumulative: std.AutoHashMap(?pbcommon.KeyValueList, T), pub fn init(allocator: std.mem.Allocator) Self { return Self{ - .cumulative = std.AutoHashMap(u64, T).init(allocator), + .cumulative = std.AutoHashMap(?pbcommon.KeyValueList, T).init(allocator), + .allocator = allocator, }; } /// Add the given delta to the counter, using the provided attributes. pub fn add(self: *Self, delta: T, attributes: ?pbcommon.KeyValueList) !void { - const key = pbutils.hashIdentifyAttributes(attributes); + const key = attributes; if (self.cumulative.getEntry(key)) |c| { c.value_ptr.* += delta; } else { @@ -184,13 +189,13 @@ pub fn Histogram(comptime T: type) type { // Keep track of the current value of the counter for each unique comibination of attribute. // At the same time, don't want allocate memory for each attribute set that comes in. // Store all the counters in a single array and keep track of the state of each counter. - cumulative: std.AutoHashMap(u64, T), + cumulative: std.AutoHashMap(?pbcommon.KeyValueList, T), // Holds the counts of the values falling in each bucket for the histogram. // The buckets are defined by the user if explcitily provided, otherwise the default SDK specification // buckets are used. // Buckets are always defined as f64. buckets: []const f64, - bucket_counts: std.AutoHashMap(u64, []usize), + bucket_counts: std.AutoHashMap(?pbcommon.KeyValueList, []usize), min: ?T = null, max: ?T = null, @@ -203,15 +208,15 @@ pub fn Histogram(comptime T: type) type { return Self{ .options = opts, - .cumulative = std.AutoHashMap(u64, T).init(allocator), + .cumulative = std.AutoHashMap(?pbcommon.KeyValueList, T).init(allocator), .buckets = buckets, - .bucket_counts = std.AutoHashMap(u64, []usize).init(allocator), + .bucket_counts = std.AutoHashMap(?pbcommon.KeyValueList, []usize).init(allocator), }; } /// Add the given value to the histogram, using the provided attributes. pub fn record(self: *Self, value: T, attributes: ?pbcommon.KeyValueList) !void { - const key = pbutils.hashIdentifyAttributes(attributes); + const key = attributes; if (self.cumulative.getEntry(key)) |c| { c.value_ptr.* += value; } else { @@ -266,17 +271,17 @@ pub fn Gauge(comptime T: type) type { return struct { const Self = @This(); - values: std.AutoHashMap(u64, T), + values: std.AutoHashMap(?pbcommon.KeyValueList, T), pub fn init(allocator: std.mem.Allocator) Self { return Self{ - .values = std.AutoHashMap(u64, T).init(allocator), + .values = std.AutoHashMap(?pbcommon.KeyValueList, T).init(allocator), }; } /// Record the given value to the gauge, using the provided attributes. pub fn record(self: *Self, value: T, attributes: ?pbcommon.KeyValueList) !void { - const key = pbutils.hashIdentifyAttributes(attributes); + const key = attributes; try self.values.put(key, value); } }; @@ -286,7 +291,7 @@ const MeterProvider = @import("meter.zig").MeterProvider; test "meter can create counter instrument and record increase without attributes" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var counter = try meter.createCounter(u32, .{ .name = "a-counter" }); @@ -296,7 +301,7 @@ test "meter can create counter instrument and record increase without attributes test "meter can create counter instrument and record increase with attributes" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var counter = try meter.createCounter(u32, .{ .name = "a-counter", @@ -304,10 +309,13 @@ test "meter can create counter instrument and record increase with attributes" { .unit = "KiB", }); - try counter.add(1, null); + try counter.add(100, null); + try counter.add(1000, null); std.debug.assert(counter.cumulative.count() == 1); + std.debug.assert(counter.cumulative.get(null).? == 1100); var attrs = std.ArrayList(pbcommon.KeyValue).init(std.testing.allocator); + // const attrs = try pbutils.WithAttributes(std.testing.allocator, .{ "some-key", "some-value", "another-key", "another-value" }); defer attrs.deinit(); try attrs.append(pbcommon.KeyValue{ .key = .{ .Const = "some-key" }, .value = pbcommon.AnyValue{ .value = .{ .string_value = .{ .Const = "42" } } } }); try attrs.append(pbcommon.KeyValue{ .key = .{ .Const = "another-key" }, .value = pbcommon.AnyValue{ .value = .{ .int_value = 0x123456789 } } }); @@ -318,7 +326,7 @@ test "meter can create counter instrument and record increase with attributes" { test "meter can create histogram instrument and record value without explicit buckets" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var histogram = try meter.createHistogram(u32, .{ .name = "anything" }); @@ -328,7 +336,7 @@ test "meter can create histogram instrument and record value without explicit bu try std.testing.expectEqual(.{ 1, 15 }, .{ histogram.min.?, histogram.max.? }); std.debug.assert(histogram.cumulative.count() == 1); - const counts = histogram.bucket_counts.get(pbutils.hashIdentifyAttributes(null)).?; + const counts = histogram.bucket_counts.get(null).?; std.debug.assert(counts.len == spec.defaultHistogramBucketBoundaries.len); const expected_counts = &[_]usize{ 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; try std.testing.expectEqualSlices(usize, expected_counts, counts); @@ -336,7 +344,7 @@ test "meter can create histogram instrument and record value without explicit bu test "meter can create histogram instrument and record value with explicit buckets" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var histogram = try meter.createHistogram(u32, .{ .name = "a-histogram", .histogramOpts = .{ .explicitBuckets = &.{ 1.0, 10.0, 100.0, 1000.0 } } }); @@ -346,7 +354,7 @@ test "meter can create histogram instrument and record value with explicit bucke try std.testing.expectEqual(.{ 1, 15 }, .{ histogram.min.?, histogram.max.? }); std.debug.assert(histogram.cumulative.count() == 1); - const counts = histogram.bucket_counts.get(pbutils.hashIdentifyAttributes(null)).?; + const counts = histogram.bucket_counts.get(null).?; std.debug.assert(counts.len == 4); const expected_counts = &[_]usize{ 1, 1, 1, 0 }; try std.testing.expectEqualSlices(usize, expected_counts, counts); @@ -354,19 +362,19 @@ test "meter can create histogram instrument and record value with explicit bucke test "meter can create gauge instrument and record value without attributes" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var gauge = try meter.createGauge(i16, .{ .name = "a-gauge" }); try gauge.record(42, null); try gauge.record(-42, null); std.debug.assert(gauge.values.count() == 1); - std.debug.assert(gauge.values.get(0) == -42); + std.debug.assert(gauge.values.get(null) == -42); } test "meter creates upDownCounter and stores value" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); var counter = try meter.createUpDownCounter(i32, .{ .name = "up-down-counter" }); @@ -377,7 +385,7 @@ test "meter creates upDownCounter and stores value" { // Validate the number stored is correct. // Null attributes produce a key hashed == 0. - if (counter.cumulative.get(0)) |c| { + if (counter.cumulative.get(null)) |c| { std.debug.assert(c == 1); } else { std.debug.panic("Counter not found", .{}); diff --git a/src/metrics/meter.zig b/src/metrics/meter.zig index ecede9c..4cc383d 100644 --- a/src/metrics/meter.zig +++ b/src/metrics/meter.zig @@ -6,6 +6,7 @@ const pbutils = @import("../pbutils.zig"); const spec = @import("spec.zig"); const Instrument = @import("instrument.zig").Instrument; +const MetricReader = @import("reader.zig").MetricReader; const Kind = @import("instrument.zig").Kind; const InstrumentOptions = @import("instrument.zig").InstrumentOptions; const Counter = @import("instrument.zig").Counter; @@ -19,6 +20,7 @@ const defaultMeterVersion = "0.1.0"; pub const MeterProvider = struct { allocator: std.mem.Allocator, meters: std.AutoHashMap(u64, Meter), + readers: std.ArrayList(*MetricReader), const Self = @This(); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -29,6 +31,7 @@ pub const MeterProvider = struct { provider.* = Self{ .allocator = alloc, .meters = std.AutoHashMap(u64, Meter).init(alloc), + .readers = std.ArrayList(*MetricReader).init(alloc), }; return provider; @@ -41,8 +44,9 @@ pub const MeterProvider = struct { } /// Delete the meter provider and free up the memory allocated for it. - pub fn deinit(self: *Self) void { + pub fn shutdown(self: *Self) void { self.meters.deinit(); + self.readers.deinit(); self.allocator.destroy(self); } @@ -75,12 +79,18 @@ pub const MeterProvider = struct { fn meterExistsWithDifferentAttributes(self: *Self, identifier: u64, attributes: ?pbcommon.KeyValueList) bool { if (self.meters.get(identifier)) |m| { - if (!std.mem.eql(u8, &std.mem.toBytes(m.attributes), &std.mem.toBytes(attributes))) { - return true; - } + return !std.mem.eql(u8, &std.mem.toBytes(m.attributes), &std.mem.toBytes(attributes)); } return false; } + + pub fn addReader(self: *Self, m: *MetricReader) !void { + if (m.meterProvider != null) { + return spec.ResourceError.MetricReaderAlreadyAttached; + } + m.meterProvider = self; + try self.readers.append(m); + } }; pub const MeterOptions = struct { @@ -147,7 +157,7 @@ const Meter = struct { // Name is case-insensitive. // The remaining are also forming the identifier. fn registerInstrument(self: *Self, instrument: Instrument) !void { - const identifyingName = try spec.instrumentIdentifier( + const id = try spec.instrumentIdentifier( self.allocator, instrument.opts.name, instrument.kind.toString(), @@ -155,27 +165,27 @@ const Meter = struct { instrument.opts.description orelse "", ); - if (self.instruments.contains(identifyingName)) { + if (self.instruments.contains(id)) { std.debug.print( "Instrument with identifying name {s} already exists in meter {s}\n", - .{ identifyingName, self.name }, + .{ id, self.name }, ); return spec.ResourceError.InstrumentExistsWithSameNameAndIdentifyingFields; } - try self.instruments.put(identifyingName, instrument); + try self.instruments.put(id, instrument); } }; test "default meter provider can be fetched" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); std.debug.assert(@intFromPtr(&mp) != 0); } test "custom meter provider can be created" { const mp = try MeterProvider.init(std.testing.allocator); - defer mp.deinit(); + defer mp.shutdown(); std.debug.assert(@intFromPtr(&mp) != 0); } @@ -184,7 +194,7 @@ test "meter can be created from custom provider" { const meter_name = "my-meter"; const meter_version = "my-meter"; const mp = try MeterProvider.init(std.testing.allocator); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = meter_name, .version = meter_version }); @@ -199,7 +209,7 @@ test "meter can be created from default provider with schema url and attributes" const meter_version = "my-meter"; const attributes = pbcommon.KeyValueList{ .values = std.ArrayList(pbcommon.KeyValue).init(std.testing.allocator) }; const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = meter_name, .version = meter_version, .schema_url = "http://foo.bar", .attributes = attributes }); std.debug.assert(std.mem.eql(u8, meter.name, meter_name)); @@ -210,7 +220,7 @@ test "meter can be created from default provider with schema url and attributes" test "meter has default version when creted with no options" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "ameter" }); std.debug.assert(std.mem.eql(u8, meter.version, defaultMeterVersion)); @@ -235,7 +245,7 @@ test "getting same meter with different attributes returns an error" { test "meter register instrument twice with same name fails" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); const i = try Instrument.Get(.Counter, .{ .name = "some-counter" }, std.testing.allocator); @@ -248,7 +258,7 @@ test "meter register instrument twice with same name fails" { test "meter register instrument" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const meter = try mp.getMeter(.{ .name = "my-meter" }); const i = try Instrument.Get(.Counter, .{ .name = "my-counter" }, std.testing.allocator); @@ -272,3 +282,48 @@ test "meter register instrument" { std.debug.assert(false); } } + +test "meter provider adds metric reader" { + const mp = try MeterProvider.init(std.testing.allocator); + defer mp.shutdown(); + var mr = MetricReader{ .allocator = std.testing.allocator }; + try mp.addReader(&mr); + + std.debug.assert(mp.readers.items.len == 1); +} + +test "meter provider adds multiple metric readers" { + const mp = try MeterProvider.init(std.testing.allocator); + defer mp.shutdown(); + var mr1 = MetricReader{ .allocator = std.testing.allocator }; + var mr2 = MetricReader{ .allocator = std.testing.allocator }; + try mp.addReader(&mr1); + try mp.addReader(&mr2); + + std.debug.assert(mp.readers.items.len == 2); +} + +test "same metric reader cannot be registered with multiple providers" { + const mp1 = try MeterProvider.init(std.testing.allocator); + defer mp1.shutdown(); + + const mp2 = try MeterProvider.init(std.testing.allocator); + defer mp2.shutdown(); + + var mr = MetricReader{ .allocator = std.testing.allocator }; + + try mp1.addReader(&mr); + const err = mp2.addReader(&mr); + try std.testing.expectError(spec.ResourceError.MetricReaderAlreadyAttached, err); +} + +test "same metric reader cannot be registered twice on same meter provider" { + const mp1 = try MeterProvider.init(std.testing.allocator); + defer mp1.shutdown(); + + var mr = MetricReader{ .allocator = std.testing.allocator }; + + try mp1.addReader(&mr); + const err = mp1.addReader(&mr); + try std.testing.expectError(spec.ResourceError.MetricReaderAlreadyAttached, err); +} diff --git a/src/metrics/reader.zig b/src/metrics/reader.zig new file mode 100644 index 0000000..98c2d5f --- /dev/null +++ b/src/metrics/reader.zig @@ -0,0 +1,108 @@ +const std = @import("std"); +const protobuf = @import("protobuf"); +const ManagedString = protobuf.ManagedString; +const pbcommon = @import("../opentelemetry/proto/common/v1.pb.zig"); +const pbmetrics = @import("../opentelemetry/proto/metrics/v1.pb.zig"); +const instr = @import("instrument.zig"); +const Instrument = instr.Instrument; +const Kind = instr.Kind; +const MeterProvider = @import("meter.zig").MeterProvider; +const view = @import("view.zig"); + +/// ExportError represents the failure to export data points +/// to a destination. +pub const MetricReadError = error{ + CollectFailedOnMissingMeterProvider, + ExportFailed, +}; + +/// MetricReader reads metrics' data from a MeterProvider. +/// See https://opentelemetry.io/docs/specs/otel/metrics/sdk/#metricreader +pub const MetricReader = struct { + allocator: std.mem.Allocator, + // We can read the instruments' data points from the meters + // stored in meterProvider. + meterProvider: ?*MeterProvider = null, + + temporality: *const fn (Kind) view.Temporality = view.DefaultTemporalityFor, + aggregation: *const fn (Kind) view.Aggregation = view.DefaultAggregationFor, + // Signal that shutdown has been called. + hasShutDown: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + // TODO add exporter + // exporter: MetricExporter, + + const Self = @This(); + + pub fn collect(self: Self) !void { + if (self.hasShutDown.load(.acquire)) { + // Shutdown has already been called so this is a no-op. + return; + } + if (self.meterProvider) |mp| { + // Collect the data from the meter provider. + var mpIter = mp.meters.valueIterator(); + while (mpIter.next()) |meter| { + var instrIter = meter.instruments.valueIterator(); + while (instrIter.next()) |i| { + const metric = try toMetric(self.allocator, i); + defer metric.metadata.deinit(); + } + } + } else { + // No meter provider to collect from. + return MetricReadError.CollectFailedOnMissingMeterProvider; + } + } + + pub fn shutdown(self: *Self) void { + self.hasShutDown.store(true, .release); + } + + fn toMetric(allocator: std.mem.Allocator, i: *Instrument) !pbmetrics.Metric { + return pbmetrics.Metric{ + .name = ManagedString.managed(i.opts.name), + .description = if (i.opts.description) |d| ManagedString.managed(d) else .Empty, + .unit = if (i.opts.unit) |u| ManagedString.managed(u) else .Empty, + .data = null, + // .data = switch (i.data) { + // .Counter_u32 => pbmetrics.Metric.data_union{ .sum = pbmetrics.Sum{ + // .data_points = try sumDataPoints(allocator, u32, i.data.Counter_u32), + // } }, + // else => unreachable, + // }, + // Metadata used for internal translations and we can discard for now. + // Consumers of SDK should not rely on this field. + .metadata = std.ArrayList(pbcommon.KeyValue).init(allocator), + }; + } +}; + +fn sumDataPoints(allocator: std.mem.Allocator, comptime T: type, c: instr.Counter(T)) !std.ArrayList(pbmetrics.NumberDataPoint) { + var dataPoints = std.ArrayList(pbmetrics.NumberDataPoint).init(allocator); + var iter = c.cumulative.iterator(); + while (iter.next()) |measure| { + const dp = pbmetrics.NumberDataPoint{ + // Attributes are stored as key of the hasmap. + .attributes = if (measure.key_ptr.*) |m| m.values else std.ArrayList(pbcommon.KeyValue).init(allocator), + .time_unix_nano = @intCast(std.time.nanoTimestamp()), + .value = .{ .as_int = @intCast(measure.value_ptr.*) }, + + // TODO support exemplars + .exemplars = std.ArrayList(pbmetrics.Exemplar).init(allocator), + }; + try dataPoints.append(dp); + } + return dataPoints; +} + +test "metric reader shutdown prevents collect() to execute" { + var reader = MetricReader{ .allocator = std.testing.allocator }; + const e = reader.collect(); + try std.testing.expectEqual(MetricReadError.CollectFailedOnMissingMeterProvider, e); + reader.shutdown(); + try reader.collect(); +} + +pub const MetricExporter = struct { + exporter: *const fn (pbmetrics.ExportMetricsServiceRequest) MetricReadError!void, +}; diff --git a/src/metrics/spec.zig b/src/metrics/spec.zig index 773c3be..1f01b1b 100644 --- a/src/metrics/spec.zig +++ b/src/metrics/spec.zig @@ -66,7 +66,7 @@ test "instrument name must conform to the OpenTelemetry specification" { test "meter cannot create instrument if name does not conform to the OpenTelemetry specification" { const mp = try MeterProvider.default(); - defer mp.deinit(); + defer mp.shutdown(); const m = try mp.getMeter(.{ .name = "my-meter" }); const invalid_names = &[_][]const u8{ // Does not start with a letter @@ -142,6 +142,7 @@ test "validate description" { pub const ResourceError = error{ MeterExistsWithDifferentAttributes, InstrumentExistsWithSameNameAndIdentifyingFields, + MetricReaderAlreadyAttached, }; /// Generate an identifier for a meter: an existing meter with same diff --git a/src/metrics/view.zig b/src/metrics/view.zig new file mode 100644 index 0000000..b841e38 --- /dev/null +++ b/src/metrics/view.zig @@ -0,0 +1,36 @@ +const Instrument = @import("instrument.zig"); + +/// Defines the ways and means to compute aggregated metrics. +/// See https://opentelemetry.io/docs/specs/otel/metrics/sdk/#aggregation +pub const Aggregation = enum { + Drop, + Default, + Sum, + LastValue, + ExplicitBucketHistogram, +}; + +/// Default aggregation for a given kind of instrument. +pub fn DefaultAggregationFor(kind: Instrument.Kind) Aggregation { + return switch (kind) { + .Counter => Aggregation.Sum, + .UpDownCounter => Aggregation.Sum, + .Gauge => Aggregation.LastValue, + .Histogram => Aggregation.ExplicitBucketHistogram, + }; +} + +// Temporality +pub const Temporality = enum { + Cumulative, + Delta, +}; + +pub fn DefaultTemporalityFor(kind: Instrument.Kind) Temporality { + return switch (kind) { + .Counter => Temporality.Cumulative, + .UpDownCounter => Temporality.Cumulative, + .Gauge => Temporality.Delta, + .Histogram => Temporality.Cumulative, + }; +} diff --git a/src/pbutils.zig b/src/pbutils.zig index 90947a0..cb79349 100644 --- a/src/pbutils.zig +++ b/src/pbutils.zig @@ -2,51 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const protobuf = @import("protobuf"); const pbcommon = @import("opentelemetry/proto/common/v1.pb.zig"); -// const ManagedString = protobuf.ManagedString; - -/// Generate a hash identifier from a set of attributes. The hash is made from both keys and values. -/// The hash is used to identify the counter for a given set of attributes and -/// alow incrementing it without allocating memory for each set of attributes. -/// A limit of 4096 bytes is imposed on the hashed content; there should be no collisions -/// between different sets of attributes with such a limit. -pub fn hashIdentifyAttributes(attributes: ?pbcommon.KeyValueList) u64 { - if (attributes) |a| { - var hash: [4096]u8 = std.mem.zeroes([4096]u8); - var nextInsertIdx: usize = 0; - for (a.values.items) |kv| { - const buf = std.mem.toBytes(kv); - // If the attributes are not going to fit, we stop hashing them. - if (nextInsertIdx + buf.len > 4095) { - break; - } - for (buf) |v| { - hash[nextInsertIdx] = v; - } - nextInsertIdx += buf.len; - } - return std.hash.XxHash3.hash(0, &hash); - } else { - return 0; - } -} - -test "null attributes generate zero hash value" { - const key = hashIdentifyAttributes(null); - try std.testing.expectEqual(0, key); -} - -test "generate map key from attributes" { - var attrs = pbcommon.KeyValueList{ .values = std.ArrayList(pbcommon.KeyValue).init(std.testing.allocator) }; - defer attrs.values.deinit(); - - try attrs.values.append(pbcommon.KeyValue{ .key = .{ .Const = "string_key" }, .value = pbcommon.AnyValue{ .value = .{ .string_value = .{ .Const = "some_string" } } } }); - try attrs.values.append(pbcommon.KeyValue{ .key = .{ .Const = "bool_key" }, .value = pbcommon.AnyValue{ .value = .{ .bool_value = true } } }); - try attrs.values.append(pbcommon.KeyValue{ .key = .{ .Const = "integer_key" }, .value = pbcommon.AnyValue{ .value = .{ .int_value = 42 } } }); - std.debug.assert(attrs.values.items.len == 3); - - const key = hashIdentifyAttributes(attrs); - try std.testing.expectEqual(0x93d76fe148c689ba, key); -} // Converts a key-value pair into a pbcommon.KeyValue. // It only supports a subset of the possible value types available in attributes. @@ -78,18 +33,23 @@ fn keyValue(comptime T: type) type { pub fn WithAttributes(allocator: std.mem.Allocator, args: anytype) !pbcommon.KeyValueList { var attrs = pbcommon.KeyValueList{ .values = std.ArrayList(pbcommon.KeyValue).init(allocator) }; + // Straight copied from the zig std library: std.fmt. + // Check if the argument is a tuple. const ArgsType = @TypeOf(args); const args_type_info = @typeInfo(ArgsType); if (args_type_info != .Struct) { @compileError("expected a tuple argument, found " ++ @typeName(ArgsType)); } + // Then check its length. const fields_info = args_type_info.Struct.fields; if (fields_info.len % 2 != 0) { @compileError("expected an even number of arguments"); } + // Build a key-value pair from the tuple, traversing in order. var key: []const u8 = undefined; comptime var i = 1; + // Unroll the loop at compile time. inline for (std.meta.fields(ArgsType)) |kv| { const e = @field(args, kv.name); if (i % 2 == 0) {