diff --git a/build.zig b/build.zig index 243ec08..d58aa26 100644 --- a/build.zig +++ b/build.zig @@ -289,6 +289,18 @@ pub fn build(b: *std.Build) void { const run_hdr_histogram_tests = b.addRunArtifact(hdr_histogram_tests); test_step.dependOn(&run_hdr_histogram_tests.step); + // Metrics Reducer tests + const metrics_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("tests/unit/metrics_test.zig"), + .target = target, + .optimize = optimize, + }), + }); + metrics_tests.root_module.addImport("z6", z6_module); + const run_metrics_tests = b.addRunArtifact(metrics_tests); + test_step.dependOn(&run_metrics_tests.step); + // Integration tests const integration_test_step = b.step("test-integration", "Run integration tests"); diff --git a/src/metrics.zig b/src/metrics.zig new file mode 100644 index 0000000..0276f90 --- /dev/null +++ b/src/metrics.zig @@ -0,0 +1,488 @@ +//! Metrics Reducer +//! +//! Post-run metrics computation from event log. +//! Single-pass O(N) algorithm for computing request counts, latency distribution, +//! throughput, connection metrics, and error rates. +//! +//! Tiger Style: +//! - Minimum 2 assertions per function +//! - All loops bounded by event count +//! - No unbounded allocations after init + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const math = std.math; + +const Event = @import("event.zig").Event; +const EventType = @import("event.zig").EventType; +const HdrHistogram = @import("hdr_histogram.zig").HdrHistogram; + +// ============================================================================= +// Payload Structures (for casting from Event.payload) +// ============================================================================= + +/// Request issued payload structure +pub const RequestPayload = extern struct { + request_id: u64, + method: [8]u8, + url_hash: u64, + header_count: u16, + _pad1: u16, + body_size: u32, + _reserved: [208]u8, + + comptime { + assert(@sizeOf(RequestPayload) == 240); + } +}; + +/// Response received payload structure +/// Layout: request_id(8) + status_code(2) + _pad1(2) + header_size(4) + body_size(4) + _pad2(4) + latency_ns(8) + _reserved(208) = 240 +pub const ResponsePayload = extern struct { + request_id: u64, + status_code: u16, + _pad1: u16, + header_size: u32, + body_size: u32, + _pad2: u32, + latency_ns: u64, + _reserved: [208]u8, + + comptime { + assert(@sizeOf(ResponsePayload) == 240); + } +}; + +/// Connection established payload structure +/// Layout: conn_id(8) + remote_addr_hash(8) + protocol(1) + tls(1) + _pad(6) + conn_time_ns(8) + _reserved(208) = 240 +pub const ConnectionPayload = extern struct { + conn_id: u64, + remote_addr_hash: u64, + protocol: u8, + tls: u8, + _pad: [6]u8, + conn_time_ns: u64, + _reserved: [208]u8, + + comptime { + assert(@sizeOf(ConnectionPayload) == 240); + } +}; + +// ============================================================================= +// Metrics Output Structures +// ============================================================================= + +/// HTTP method enumeration for breakdown +pub const HttpMethod = enum(u8) { + GET = 0, + POST = 1, + PUT = 2, + DELETE = 3, + PATCH = 4, + HEAD = 5, + OPTIONS = 6, + OTHER = 7, +}; + +/// Request metrics +pub const RequestMetrics = struct { + total: u64, + success: u64, + failed: u64, + success_rate: f64, + by_method: [8]u64, + by_status_class: [6]u64, // 1xx, 2xx, 3xx, 4xx, 5xx, other +}; + +/// Latency metrics (nanoseconds) +pub const LatencyMetrics = struct { + min_ns: u64, + max_ns: u64, + mean_ns: f64, + p50_ns: u64, + p90_ns: u64, + p95_ns: u64, + p99_ns: u64, + p999_ns: u64, + sample_count: u64, +}; + +/// Throughput metrics +pub const ThroughputMetrics = struct { + total_duration_ticks: u64, + requests_per_tick: f64, + response_count: u64, +}; + +/// Connection metrics +pub const ConnectionMetrics = struct { + total_connections: u64, + connection_errors: u64, + avg_connection_time_ns: u64, + total_connection_time_ns: u64, +}; + +/// Error metrics +pub const ErrorMetrics = struct { + total_errors: u64, + dns_errors: u64, + tcp_errors: u64, + tls_errors: u64, + http_errors: u64, + timeout_errors: u64, + protocol_errors: u64, + resource_errors: u64, + error_rate: f64, +}; + +/// Complete metrics result +pub const Metrics = struct { + requests: RequestMetrics, + latency: LatencyMetrics, + throughput: ThroughputMetrics, + connections: ConnectionMetrics, + errors: ErrorMetrics, + start_tick: u64, + end_tick: u64, +}; + +// ============================================================================= +// MetricsReducer +// ============================================================================= + +pub const MetricsReducer = struct { + allocator: Allocator, + histogram: HdrHistogram, + + // Request counters + request_count: u64, + success_count: u64, + response_count: u64, + by_method: [8]u64, + by_status_class: [6]u64, + + // Connection counters + connection_count: u64, + connection_errors: u64, + total_conn_time: u64, + + // Error counters (7 types) + error_counts: [7]u64, + + // Time range + start_tick: u64, + end_tick: u64, + + /// Initialize a new MetricsReducer + pub fn init(allocator: Allocator) !MetricsReducer { + // Tiger Style: Assert preconditions + assert(@sizeOf(Event) == 272); + assert(@sizeOf(RequestPayload) == 240); + + const histogram = try HdrHistogram.init(allocator, .{ + .lowest_trackable_value = 1, + .highest_trackable_value = 3_600_000_000_000, // 1 hour in ns + .significant_figures = 3, + }); + + return MetricsReducer{ + .allocator = allocator, + .histogram = histogram, + .request_count = 0, + .success_count = 0, + .response_count = 0, + .by_method = [_]u64{0} ** 8, + .by_status_class = [_]u64{0} ** 6, + .connection_count = 0, + .connection_errors = 0, + .total_conn_time = 0, + .error_counts = [_]u64{0} ** 7, + .start_tick = math.maxInt(u64), + .end_tick = 0, + }; + } + + /// Free resources + pub fn deinit(self: *MetricsReducer) void { + // Tiger Style: Assert valid state + assert(self.histogram.counts.len > 0); + assert(self.response_count >= self.success_count); + + self.histogram.deinit(); + } + + /// Reset reducer to initial state + pub fn reset(self: *MetricsReducer) void { + // Tiger Style: Assert valid state + assert(self.histogram.counts.len > 0); + assert(@sizeOf(Event) == 272); + + self.histogram.reset(); + self.request_count = 0; + self.success_count = 0; + self.response_count = 0; + self.by_method = [_]u64{0} ** 8; + self.by_status_class = [_]u64{0} ** 6; + self.connection_count = 0; + self.connection_errors = 0; + self.total_conn_time = 0; + self.error_counts = [_]u64{0} ** 7; + self.start_tick = math.maxInt(u64); + self.end_tick = 0; + } + + /// Process a single event + pub fn processEvent(self: *MetricsReducer, event: *const Event) !void { + // Tiger Style: Assert valid state + assert(self.histogram.counts.len > 0); + assert(event.payload.len == 240); + + // Track time range + if (event.header.tick < self.start_tick) { + self.start_tick = event.header.tick; + } + if (event.header.tick > self.end_tick) { + self.end_tick = event.header.tick; + } + + switch (event.header.event_type) { + .request_issued => { + self.request_count += 1; + const payload = castPayload(RequestPayload, &event.payload); + const method_idx = methodToIndex(&payload.method); + self.by_method[method_idx] += 1; + }, + .response_received => { + self.response_count += 1; + const payload = castPayload(ResponsePayload, &event.payload); + + // Record latency + if (payload.latency_ns > 0) { + self.histogram.recordValue(payload.latency_ns) catch { + // Value out of range, skip + }; + } + + // Track status class + const status_class = payload.status_code / 100; + if (status_class >= 1 and status_class <= 5) { + self.by_status_class[status_class - 1] += 1; + } else { + self.by_status_class[5] += 1; // other + } + + // Track success + if (payload.status_code < 400) { + self.success_count += 1; + } + }, + .conn_established => { + self.connection_count += 1; + const payload = castPayload(ConnectionPayload, &event.payload); + self.total_conn_time += payload.conn_time_ns; + }, + .conn_error => { + self.connection_errors += 1; + }, + .error_dns => self.error_counts[0] += 1, + .error_tcp => self.error_counts[1] += 1, + .error_tls => self.error_counts[2] += 1, + .error_http => self.error_counts[3] += 1, + .error_timeout => self.error_counts[4] += 1, + .error_protocol_violation => self.error_counts[5] += 1, + .error_resource_exhausted => self.error_counts[6] += 1, + else => {}, // Ignore other event types + } + } + + /// Compute final metrics from accumulated data + pub fn compute(self: *const MetricsReducer) Metrics { + // Tiger Style: Assert valid state + assert(self.histogram.counts.len > 0); + assert(self.response_count >= self.success_count); + + // Calculate total errors + var total_errors: u64 = 0; + for (self.error_counts) |count| { + total_errors += count; + } + + // Calculate failed requests (responses with status >= 400 or errors) + const failed = if (self.response_count > self.success_count) + self.response_count - self.success_count + else + 0; + + // Calculate rates + const success_rate = if (self.response_count > 0) + @as(f64, @floatFromInt(self.success_count)) / @as(f64, @floatFromInt(self.response_count)) + else + 0.0; + + const error_rate = if (self.request_count > 0) + @as(f64, @floatFromInt(total_errors)) / @as(f64, @floatFromInt(self.request_count)) + else + 0.0; + + // Calculate throughput + const duration = if (self.end_tick > self.start_tick) + self.end_tick - self.start_tick + else + 0; + + const requests_per_tick = if (duration > 0) + @as(f64, @floatFromInt(self.response_count)) / @as(f64, @floatFromInt(duration)) + else + 0.0; + + // Calculate average connection time + const avg_conn_time = if (self.connection_count > 0) + self.total_conn_time / self.connection_count + else + 0; + + return Metrics{ + .requests = RequestMetrics{ + .total = self.request_count, + .success = self.success_count, + .failed = failed, + .success_rate = success_rate, + .by_method = self.by_method, + .by_status_class = self.by_status_class, + }, + .latency = LatencyMetrics{ + .min_ns = self.histogram.min(), + .max_ns = self.histogram.max(), + .mean_ns = self.histogram.mean(), + .p50_ns = self.histogram.valueAtPercentile(50.0), + .p90_ns = self.histogram.valueAtPercentile(90.0), + .p95_ns = self.histogram.valueAtPercentile(95.0), + .p99_ns = self.histogram.valueAtPercentile(99.0), + .p999_ns = self.histogram.valueAtPercentile(99.9), + .sample_count = self.histogram.totalCount(), + }, + .throughput = ThroughputMetrics{ + .total_duration_ticks = duration, + .requests_per_tick = requests_per_tick, + .response_count = self.response_count, + }, + .connections = ConnectionMetrics{ + .total_connections = self.connection_count, + .connection_errors = self.connection_errors, + .avg_connection_time_ns = avg_conn_time, + .total_connection_time_ns = self.total_conn_time, + }, + .errors = ErrorMetrics{ + .total_errors = total_errors, + .dns_errors = self.error_counts[0], + .tcp_errors = self.error_counts[1], + .tls_errors = self.error_counts[2], + .http_errors = self.error_counts[3], + .timeout_errors = self.error_counts[4], + .protocol_errors = self.error_counts[5], + .resource_errors = self.error_counts[6], + .error_rate = error_rate, + }, + .start_tick = self.start_tick, + .end_tick = self.end_tick, + }; + } +}; + +// ============================================================================= +// Convenience Functions +// ============================================================================= + +/// Reduce a slice of events to metrics in a single pass +pub fn reduce(allocator: Allocator, events: []const Event) !Metrics { + // Tiger Style: Assert preconditions + assert(@sizeOf(Event) == 272); + assert(events.len <= 10_000_000); // Max 10M events per METRICS.md + + var reducer = try MetricsReducer.init(allocator); + defer reducer.deinit(); + + // Single pass over all events (bounded by events.len) + for (events) |*event| { + try reducer.processEvent(event); + } + + return reducer.compute(); +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/// Cast payload bytes to typed structure +fn castPayload(comptime T: type, payload: *const [240]u8) *const T { + assert(@sizeOf(T) == 240); + return @ptrCast(@alignCast(payload)); +} + +/// Map HTTP method string to index +fn methodToIndex(method: *const [8]u8) u8 { + // Check common methods first for performance + if (method[0] == 'G' and method[1] == 'E' and method[2] == 'T') { + return @intFromEnum(HttpMethod.GET); + } + if (method[0] == 'P' and method[1] == 'O' and method[2] == 'S' and method[3] == 'T') { + return @intFromEnum(HttpMethod.POST); + } + if (method[0] == 'P' and method[1] == 'U' and method[2] == 'T') { + return @intFromEnum(HttpMethod.PUT); + } + if (method[0] == 'D' and method[1] == 'E' and method[2] == 'L') { + return @intFromEnum(HttpMethod.DELETE); + } + if (method[0] == 'P' and method[1] == 'A' and method[2] == 'T') { + return @intFromEnum(HttpMethod.PATCH); + } + if (method[0] == 'H' and method[1] == 'E' and method[2] == 'A') { + return @intFromEnum(HttpMethod.HEAD); + } + if (method[0] == 'O' and method[1] == 'P' and method[2] == 'T') { + return @intFromEnum(HttpMethod.OPTIONS); + } + return @intFromEnum(HttpMethod.OTHER); +} + +// ============================================================================= +// Tests +// ============================================================================= + +test "metrics: method index mapping" { + var method: [8]u8 = undefined; + + @memcpy(method[0..3], "GET"); + try std.testing.expectEqual(@as(u8, 0), methodToIndex(&method)); + + @memcpy(method[0..4], "POST"); + try std.testing.expectEqual(@as(u8, 1), methodToIndex(&method)); + + @memcpy(method[0..3], "PUT"); + try std.testing.expectEqual(@as(u8, 2), methodToIndex(&method)); + + @memcpy(method[0..6], "DELETE"); + try std.testing.expectEqual(@as(u8, 3), methodToIndex(&method)); + + @memcpy(method[0..5], "PATCH"); + try std.testing.expectEqual(@as(u8, 4), methodToIndex(&method)); + + @memcpy(method[0..4], "HEAD"); + try std.testing.expectEqual(@as(u8, 5), methodToIndex(&method)); + + @memcpy(method[0..7], "OPTIONS"); + try std.testing.expectEqual(@as(u8, 6), methodToIndex(&method)); + + @memcpy(method[0..5], "XYZAB"); + try std.testing.expectEqual(@as(u8, 7), methodToIndex(&method)); +} + +test "metrics: payload size checks" { + try std.testing.expectEqual(@as(usize, 240), @sizeOf(RequestPayload)); + try std.testing.expectEqual(@as(usize, 240), @sizeOf(ResponsePayload)); + try std.testing.expectEqual(@as(usize, 240), @sizeOf(ConnectionPayload)); +} diff --git a/src/z6.zig b/src/z6.zig index 6046c98..e99da24 100644 --- a/src/z6.zig +++ b/src/z6.zig @@ -126,3 +126,16 @@ pub const DEFAULT_THINK_TIME_TICKS = @import("vu_engine.zig").DEFAULT_THINK_TIME // HDR Histogram pub const HdrHistogram = @import("hdr_histogram.zig").HdrHistogram; pub const HdrError = @import("hdr_histogram.zig").HdrError; + +// Metrics Reducer +pub const MetricsReducer = @import("metrics.zig").MetricsReducer; +pub const Metrics = @import("metrics.zig").Metrics; +pub const RequestMetrics = @import("metrics.zig").RequestMetrics; +pub const LatencyMetrics = @import("metrics.zig").LatencyMetrics; +pub const ThroughputMetrics = @import("metrics.zig").ThroughputMetrics; +pub const ConnectionMetrics = @import("metrics.zig").ConnectionMetrics; +pub const ErrorMetrics = @import("metrics.zig").ErrorMetrics; +pub const RequestPayload = @import("metrics.zig").RequestPayload; +pub const ResponsePayload = @import("metrics.zig").ResponsePayload; +pub const ConnectionPayload = @import("metrics.zig").ConnectionPayload; +pub const reduce = @import("metrics.zig").reduce; diff --git a/tests/unit/metrics_test.zig b/tests/unit/metrics_test.zig new file mode 100644 index 0000000..b1c7f3b --- /dev/null +++ b/tests/unit/metrics_test.zig @@ -0,0 +1,504 @@ +//! Metrics Reducer Tests +//! +//! Test-Driven Development: These tests verify metrics computation. +//! Following Tiger Style: Test before implement. + +const std = @import("std"); +const testing = std.testing; +const z6 = @import("z6"); + +const MetricsReducer = z6.MetricsReducer; +const Metrics = z6.Metrics; +const Event = z6.Event; +const EventType = z6.EventType; +const RequestPayload = z6.RequestPayload; +const ResponsePayload = z6.ResponsePayload; +const ConnectionPayload = z6.ConnectionPayload; + +// ============================================================================= +// Test Helpers +// ============================================================================= + +fn createEvent(event_type: EventType, tick: u64, vu_id: u32) Event { + var event: Event = undefined; + @memset(&event.payload, 0); + event.header = .{ + .tick = tick, + .vu_id = vu_id, + .event_type = event_type, + ._padding = 0, + ._reserved = 0, + }; + event.checksum = 0; + return event; +} + +fn createRequestEvent(tick: u64, vu_id: u32, method: []const u8) Event { + var event = createEvent(.request_issued, tick, vu_id); + const payload = @as(*RequestPayload, @ptrCast(@alignCast(&event.payload))); + payload.request_id = tick; + @memset(&payload.method, 0); + @memcpy(payload.method[0..method.len], method); + return event; +} + +fn createResponseEvent(tick: u64, vu_id: u32, status_code: u16, latency_ns: u64) Event { + var event = createEvent(.response_received, tick, vu_id); + const payload = @as(*ResponsePayload, @ptrCast(@alignCast(&event.payload))); + payload.request_id = tick; + payload.status_code = status_code; + payload.latency_ns = latency_ns; + return event; +} + +fn createConnectionEvent(tick: u64, vu_id: u32, conn_time_ns: u64) Event { + var event = createEvent(.conn_established, tick, vu_id); + const payload = @as(*ConnectionPayload, @ptrCast(@alignCast(&event.payload))); + payload.conn_id = tick; + payload.conn_time_ns = conn_time_ns; + return event; +} + +// ============================================================================= +// Init/Deinit Tests +// ============================================================================= + +test "metrics: reducer init and deinit" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Should start with zero counts + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 0), metrics.requests.total); + try testing.expectEqual(@as(u64, 0), metrics.latency.sample_count); + try testing.expectEqual(@as(u64, 0), metrics.connections.total_connections); + try testing.expectEqual(@as(u64, 0), metrics.errors.total_errors); +} + +test "metrics: reducer reset" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Process some events + var req_event = createRequestEvent(100, 1, "GET"); + try reducer.processEvent(&req_event); + + var resp_event = createResponseEvent(200, 1, 200, 1_000_000); + try reducer.processEvent(&resp_event); + + try testing.expectEqual(@as(u64, 1), reducer.request_count); + + // Reset + reducer.reset(); + try testing.expectEqual(@as(u64, 0), reducer.request_count); + try testing.expectEqual(@as(u64, 0), reducer.response_count); +} + +// ============================================================================= +// Request Count Tests +// ============================================================================= + +test "metrics: empty event list" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 0), metrics.requests.total); + try testing.expectEqual(@as(f64, 0.0), metrics.requests.success_rate); +} + +test "metrics: single request" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var event = createRequestEvent(100, 1, "GET"); + try reducer.processEvent(&event); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 1), metrics.requests.total); + try testing.expectEqual(@as(u64, 1), metrics.requests.by_method[0]); // GET +} + +test "metrics: multiple requests with different methods" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var get_event = createRequestEvent(100, 1, "GET"); + try reducer.processEvent(&get_event); + + var post_event = createRequestEvent(200, 1, "POST"); + try reducer.processEvent(&post_event); + + var put_event = createRequestEvent(300, 1, "PUT"); + try reducer.processEvent(&put_event); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 3), metrics.requests.total); + try testing.expectEqual(@as(u64, 1), metrics.requests.by_method[0]); // GET + try testing.expectEqual(@as(u64, 1), metrics.requests.by_method[1]); // POST + try testing.expectEqual(@as(u64, 1), metrics.requests.by_method[2]); // PUT +} + +test "metrics: request method breakdown" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // 5 GET, 3 POST, 2 DELETE + var i: u64 = 0; + while (i < 5) : (i += 1) { + var event = createRequestEvent(i * 10, 1, "GET"); + try reducer.processEvent(&event); + } + while (i < 8) : (i += 1) { + var event = createRequestEvent(i * 10, 1, "POST"); + try reducer.processEvent(&event); + } + while (i < 10) : (i += 1) { + var event = createRequestEvent(i * 10, 1, "DELETE"); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 10), metrics.requests.total); + try testing.expectEqual(@as(u64, 5), metrics.requests.by_method[0]); // GET + try testing.expectEqual(@as(u64, 3), metrics.requests.by_method[1]); // POST + try testing.expectEqual(@as(u64, 2), metrics.requests.by_method[3]); // DELETE +} + +// ============================================================================= +// Latency Tests +// ============================================================================= + +test "metrics: single response latency" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var event = createResponseEvent(100, 1, 200, 5_000_000); // 5ms + try reducer.processEvent(&event); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 1), metrics.latency.sample_count); + // Min and max should be close to recorded value (within HDR precision) + try testing.expect(metrics.latency.min_ns <= 5_000_000); + try testing.expect(metrics.latency.max_ns >= 5_000_000); +} + +test "metrics: multiple response latencies" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Record latencies: 1ms, 2ms, 3ms, 4ms, 5ms + var i: u64 = 1; + while (i <= 5) : (i += 1) { + var event = createResponseEvent(i * 100, 1, 200, i * 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 5), metrics.latency.sample_count); + try testing.expect(metrics.latency.min_ns <= 1_000_000); + try testing.expect(metrics.latency.max_ns >= 5_000_000); + // Mean should be around 3ms + try testing.expect(metrics.latency.mean_ns >= 2_500_000); + try testing.expect(metrics.latency.mean_ns <= 3_500_000); +} + +test "metrics: percentile calculations" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Record 100 latencies: 1ms, 2ms, ..., 100ms + var i: u64 = 1; + while (i <= 100) : (i += 1) { + var event = createResponseEvent(i * 100, 1, 200, i * 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 100), metrics.latency.sample_count); + + // p50 should be around 50ms + try testing.expect(metrics.latency.p50_ns >= 49_000_000); + try testing.expect(metrics.latency.p50_ns <= 51_000_000); + + // p99 should be around 99ms + try testing.expect(metrics.latency.p99_ns >= 98_000_000); + try testing.expect(metrics.latency.p99_ns <= 100_000_000); +} + +// ============================================================================= +// Success/Failure Tests +// ============================================================================= + +test "metrics: all successful responses" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var i: u64 = 0; + while (i < 10) : (i += 1) { + var event = createResponseEvent(i * 100, 1, 200, 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 10), metrics.requests.success); + try testing.expectEqual(@as(u64, 0), metrics.requests.failed); + try testing.expectEqual(@as(f64, 1.0), metrics.requests.success_rate); +} + +test "metrics: mixed success and failure responses" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // 8 success (200), 2 failures (500) + var i: u64 = 0; + while (i < 8) : (i += 1) { + var event = createResponseEvent(i * 100, 1, 200, 1_000_000); + try reducer.processEvent(&event); + } + while (i < 10) : (i += 1) { + var event = createResponseEvent(i * 100, 1, 500, 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 8), metrics.requests.success); + try testing.expectEqual(@as(u64, 2), metrics.requests.failed); + try testing.expectEqual(@as(f64, 0.8), metrics.requests.success_rate); +} + +test "metrics: status class breakdown" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Various status codes + var e1 = createResponseEvent(100, 1, 200, 1_000_000); // 2xx + var e2 = createResponseEvent(200, 1, 201, 1_000_000); // 2xx + var e3 = createResponseEvent(300, 1, 301, 1_000_000); // 3xx + var e4 = createResponseEvent(400, 1, 404, 1_000_000); // 4xx + var e5 = createResponseEvent(500, 1, 500, 1_000_000); // 5xx + + try reducer.processEvent(&e1); + try reducer.processEvent(&e2); + try reducer.processEvent(&e3); + try reducer.processEvent(&e4); + try reducer.processEvent(&e5); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 2), metrics.requests.by_status_class[1]); // 2xx + try testing.expectEqual(@as(u64, 1), metrics.requests.by_status_class[2]); // 3xx + try testing.expectEqual(@as(u64, 1), metrics.requests.by_status_class[3]); // 4xx + try testing.expectEqual(@as(u64, 1), metrics.requests.by_status_class[4]); // 5xx +} + +// ============================================================================= +// Connection Tests +// ============================================================================= + +test "metrics: connection count" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var i: u64 = 0; + while (i < 5) : (i += 1) { + var event = createConnectionEvent(i * 100, 1, 10_000_000); // 10ms each + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 5), metrics.connections.total_connections); + try testing.expectEqual(@as(u64, 50_000_000), metrics.connections.total_connection_time_ns); + try testing.expectEqual(@as(u64, 10_000_000), metrics.connections.avg_connection_time_ns); +} + +test "metrics: connection errors" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var conn_event = createConnectionEvent(100, 1, 10_000_000); + try reducer.processEvent(&conn_event); + + var err_event = createEvent(.conn_error, 200, 1); + try reducer.processEvent(&err_event); + + var err_event2 = createEvent(.conn_error, 300, 1); + try reducer.processEvent(&err_event2); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 1), metrics.connections.total_connections); + try testing.expectEqual(@as(u64, 2), metrics.connections.connection_errors); +} + +// ============================================================================= +// Error Tests +// ============================================================================= + +test "metrics: no errors" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var event = createResponseEvent(100, 1, 200, 1_000_000); + try reducer.processEvent(&event); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 0), metrics.errors.total_errors); + try testing.expectEqual(@as(f64, 0.0), metrics.errors.error_rate); +} + +test "metrics: error type breakdown" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // One request to establish baseline + var req = createRequestEvent(50, 1, "GET"); + try reducer.processEvent(&req); + + // Various error types + var dns_err = createEvent(.error_dns, 100, 1); + var tcp_err = createEvent(.error_tcp, 200, 1); + var tls_err = createEvent(.error_tls, 300, 1); + var http_err = createEvent(.error_http, 400, 1); + var timeout_err = createEvent(.error_timeout, 500, 1); + + try reducer.processEvent(&dns_err); + try reducer.processEvent(&tcp_err); + try reducer.processEvent(&tls_err); + try reducer.processEvent(&http_err); + try reducer.processEvent(&timeout_err); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 5), metrics.errors.total_errors); + try testing.expectEqual(@as(u64, 1), metrics.errors.dns_errors); + try testing.expectEqual(@as(u64, 1), metrics.errors.tcp_errors); + try testing.expectEqual(@as(u64, 1), metrics.errors.tls_errors); + try testing.expectEqual(@as(u64, 1), metrics.errors.http_errors); + try testing.expectEqual(@as(u64, 1), metrics.errors.timeout_errors); +} + +test "metrics: error rate calculation" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // 10 requests, 2 errors + var i: u64 = 0; + while (i < 10) : (i += 1) { + var event = createRequestEvent(i * 100, 1, "GET"); + try reducer.processEvent(&event); + } + + var err1 = createEvent(.error_timeout, 1000, 1); + var err2 = createEvent(.error_tcp, 1100, 1); + try reducer.processEvent(&err1); + try reducer.processEvent(&err2); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 2), metrics.errors.total_errors); + try testing.expectEqual(@as(f64, 0.2), metrics.errors.error_rate); +} + +// ============================================================================= +// Throughput Tests +// ============================================================================= + +test "metrics: throughput calculation" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // 10 responses over 100 ticks (tick 0 to tick 100) + var i: u64 = 0; + while (i < 10) : (i += 1) { + var event = createResponseEvent(i * 10, 1, 200, 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 10), metrics.throughput.response_count); + try testing.expectEqual(@as(u64, 90), metrics.throughput.total_duration_ticks); // 90 - 0 = 90 + // 10 responses / 90 ticks ≈ 0.111 + try testing.expect(metrics.throughput.requests_per_tick > 0.1); + try testing.expect(metrics.throughput.requests_per_tick < 0.12); +} + +test "metrics: zero duration throughput" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // All events at same tick + var i: u64 = 0; + while (i < 5) : (i += 1) { + var event = createResponseEvent(100, @intCast(i), 200, 1_000_000); + try reducer.processEvent(&event); + } + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 5), metrics.throughput.response_count); + try testing.expectEqual(@as(u64, 0), metrics.throughput.total_duration_ticks); + try testing.expectEqual(@as(f64, 0.0), metrics.throughput.requests_per_tick); +} + +// ============================================================================= +// Time Range Tests +// ============================================================================= + +test "metrics: time range tracking" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + var e1 = createRequestEvent(500, 1, "GET"); + var e2 = createRequestEvent(100, 1, "GET"); + var e3 = createRequestEvent(900, 1, "GET"); + var e4 = createRequestEvent(300, 1, "GET"); + + try reducer.processEvent(&e1); + try reducer.processEvent(&e2); + try reducer.processEvent(&e3); + try reducer.processEvent(&e4); + + const metrics = reducer.compute(); + try testing.expectEqual(@as(u64, 100), metrics.start_tick); + try testing.expectEqual(@as(u64, 900), metrics.end_tick); +} + +// ============================================================================= +// Reduce Function Tests +// ============================================================================= + +test "metrics: reduce convenience function" { + var events: [5]Event = undefined; + events[0] = createRequestEvent(100, 1, "GET"); + events[1] = createResponseEvent(200, 1, 200, 5_000_000); + events[2] = createRequestEvent(300, 1, "POST"); + events[3] = createResponseEvent(400, 1, 201, 10_000_000); + events[4] = createConnectionEvent(50, 1, 1_000_000); + + const metrics = try z6.reduce(testing.allocator, &events); + + try testing.expectEqual(@as(u64, 2), metrics.requests.total); + try testing.expectEqual(@as(u64, 2), metrics.latency.sample_count); + try testing.expectEqual(@as(u64, 1), metrics.connections.total_connections); +} + +// ============================================================================= +// Tiger Style Tests +// ============================================================================= + +test "metrics: Tiger Style - assertions verified" { + var reducer = try MetricsReducer.init(testing.allocator); + defer reducer.deinit(); + + // Process various events + var req = createRequestEvent(100, 1, "GET"); + var resp = createResponseEvent(200, 1, 200, 1_000_000); + var conn = createConnectionEvent(50, 1, 500_000); + + try reducer.processEvent(&req); + try reducer.processEvent(&resp); + try reducer.processEvent(&conn); + + const metrics = reducer.compute(); + + // Verify all metrics are populated + try testing.expect(metrics.requests.total > 0); + try testing.expect(metrics.latency.sample_count > 0); + try testing.expect(metrics.connections.total_connections > 0); +}