import { assert, describe, it } from "@effect/vitest" import { Effect, Metric } from "effect" import { PrometheusMetrics } from "effect/unstable/observability" describe("PrometheusMetrics", () => { describe("format", () => { describe("Counter", () => { it.effect("formats a simple counter", () => Effect.gen(function*() { const counter = Metric.counter("test_counter", { description: "A test counter" }) yield* Metric.update(counter, 42) const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP test_counter A test counter") assert.include(output, "# TYPE test_counter counter") assert.include(output, "test_counter 42") })) it.effect("formats a counter with bigint value", () => Effect.gen(function*() { const counter = Metric.counter("bigint_counter", { description: "A bigint counter", bigint: true }) yield* Metric.update(counter, 9007199254740993n) const output = yield* PrometheusMetrics.format() assert.include(output, "# TYPE bigint_counter counter") assert.include(output, "bigint_counter 9007199254740993") })) it.effect("formats a counter with attributes", () => Effect.gen(function*() { const counter = Metric.counter("labeled_counter", { description: "Counter with labels", attributes: { method: "GET", endpoint: "/api/users" } }) yield* Metric.update(counter, 10) const output = yield* PrometheusMetrics.format() assert.include(output, "# TYPE labeled_counter counter") assert.include(output, "labeled_counter{method=\"GET\",endpoint=\"/api/users\"} 10") })) it.effect("formats a counter without description", () => Effect.gen(function*() { const counter = Metric.counter("no_desc_counter") yield* Metric.update(counter, 5) const output = yield* PrometheusMetrics.format() assert.notInclude(output, "# HELP no_desc_counter") assert.include(output, "# TYPE no_desc_counter counter") assert.include(output, "no_desc_counter 5") })) }) describe("Gauge", () => { it.effect("formats a simple gauge", () => Effect.gen(function*() { const gauge = Metric.gauge("test_gauge", { description: "A test gauge" }) yield* Metric.update(gauge, 123.45) const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP test_gauge A test gauge") assert.include(output, "# TYPE test_gauge gauge") assert.include(output, "test_gauge 123.45") })) it.effect("formats a gauge with negative value", () => Effect.gen(function*() { const gauge = Metric.gauge("negative_gauge", { description: "A gauge with negative value" }) yield* Metric.update(gauge, -50) const output = yield* PrometheusMetrics.format() assert.include(output, "negative_gauge -50") })) it.effect("formats a gauge with bigint value", () => Effect.gen(function*() { const gauge = Metric.gauge("bigint_gauge", { description: "A bigint gauge", bigint: true }) yield* Metric.update(gauge, 1000000000000n) const output = yield* PrometheusMetrics.format() assert.include(output, "bigint_gauge 1000000000000") })) }) describe("Histogram", () => { it.effect("formats a histogram with buckets", () => Effect.gen(function*() { const histogram = Metric.histogram("test_histogram", { description: "A test histogram", boundaries: [10, 50, 100, 500] }) yield* Metric.update(histogram, 5) yield* Metric.update(histogram, 25) yield* Metric.update(histogram, 75) yield* Metric.update(histogram, 200) yield* Metric.update(histogram, 1000) const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP test_histogram A test histogram") assert.include(output, "# TYPE test_histogram histogram") assert.include(output, "test_histogram_bucket{le=\"10\"} 1") assert.include(output, "test_histogram_bucket{le=\"50\"} 2") assert.include(output, "test_histogram_bucket{le=\"100\"} 3") assert.include(output, "test_histogram_bucket{le=\"500\"} 4") assert.include(output, "test_histogram_bucket{le=\"+Inf\"} 5") assert.include(output, "test_histogram_sum 1305") assert.include(output, "test_histogram_count 5") })) it.effect("formats a histogram with attributes", () => Effect.gen(function*() { const histogram = Metric.histogram("labeled_histogram", { description: "Histogram with labels", boundaries: [100, 500], attributes: { service: "api" } }) yield* Metric.update(histogram, 250) const output = yield* PrometheusMetrics.format() assert.include(output, "labeled_histogram_bucket{service=\"api\",le=\"100\"} 0") assert.include(output, "labeled_histogram_bucket{service=\"api\",le=\"500\"} 1") assert.include(output, "labeled_histogram_bucket{service=\"api\",le=\"+Inf\"} 1") assert.include(output, "labeled_histogram_sum{service=\"api\"} 250") assert.include(output, "labeled_histogram_count{service=\"api\"} 1") })) }) describe("Summary", () => { it.effect("formats a summary with quantiles", () => Effect.gen(function*() { const summary = Metric.summary("test_summary", { description: "A test summary", maxAge: "1 minute", maxSize: 100, quantiles: [0.5, 0.9, 0.99] }) // Add enough values to have meaningful quantiles for (let i = 1; i <= 100; i++) { yield* Metric.update(summary, i) } const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP test_summary A test summary") assert.include(output, "# TYPE test_summary summary") assert.include(output, "test_summary{quantile=\"0.5\"}") assert.include(output, "test_summary{quantile=\"0.9\"}") assert.include(output, "test_summary{quantile=\"0.99\"}") assert.include(output, "test_summary_sum 5050") assert.include(output, "test_summary_count 100") })) }) describe("Frequency", () => { it.effect("formats a frequency metric", () => Effect.gen(function*() { const frequency = Metric.frequency("test_frequency", { description: "A test frequency" }) yield* Metric.update(frequency, "success") yield* Metric.update(frequency, "success") yield* Metric.update(frequency, "failure") yield* Metric.update(frequency, "success") const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP test_frequency A test frequency") assert.include(output, "# TYPE test_frequency counter") assert.include(output, "test_frequency{key=\"success\"} 3") assert.include(output, "test_frequency{key=\"failure\"} 1") })) it.effect("formats a frequency metric with attributes", () => Effect.gen(function*() { const frequency = Metric.frequency("labeled_frequency", { description: "Frequency with labels", attributes: { service: "api" } }) yield* Metric.update(frequency, "200") yield* Metric.update(frequency, "404") const output = yield* PrometheusMetrics.format() assert.include(output, "labeled_frequency{service=\"api\",key=\"200\"} 1") assert.include(output, "labeled_frequency{service=\"api\",key=\"404\"} 1") })) }) describe("options", () => { it.effect("applies prefix to metric names", () => Effect.gen(function*() { const counter = Metric.counter("requests_total", { description: "Total requests" }) yield* Metric.update(counter, 100) const output = yield* PrometheusMetrics.format({ prefix: "myapp" }) assert.include(output, "# HELP myapp_requests_total Total requests") assert.include(output, "# TYPE myapp_requests_total counter") assert.include(output, "myapp_requests_total 100") })) it.effect("applies custom metric name mapper", () => Effect.gen(function*() { const counter = Metric.counter("requestCount", { description: "Request count" }) yield* Metric.update(counter, 50) // Convert camelCase to snake_case const mapper = (name: string) => name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() const output = yield* PrometheusMetrics.format({ metricNameMapper: mapper }) assert.include(output, "# HELP request_count Request count") assert.include(output, "# TYPE request_count counter") assert.include(output, "request_count 50") })) it.effect("combines prefix and mapper", () => Effect.gen(function*() { const counter = Metric.counter("httpRequests", { description: "HTTP requests" }) yield* Metric.update(counter, 25) const mapper = (name: string) => name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() const output = yield* PrometheusMetrics.format({ prefix: "app", metricNameMapper: mapper }) assert.include(output, "app_http_requests 25") })) }) describe("sanitization", () => { it.effect("sanitizes invalid metric name characters", () => Effect.gen(function*() { const counter = Metric.counter("metric-with-dashes.and.dots", { description: "Metric with special chars" }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() assert.include(output, "metric_with_dashes_and_dots 1") })) it.effect("handles metric names starting with digits", () => Effect.gen(function*() { const counter = Metric.counter("123_metric", { description: "Metric starting with number" }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() assert.include(output, "_123_metric 1") })) it.effect("escapes special characters in label values", () => Effect.gen(function*() { const counter = Metric.counter("escape_test", { description: "Test escaping", attributes: { path: "/api/users\"quoted\"", message: "line1\nline2" } }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() assert.include(output, "path=\"/api/users\\\"quoted\\\"\"") assert.include(output, "message=\"line1\\nline2\"") })) it.effect("escapes backslashes in label values", () => Effect.gen(function*() { const counter = Metric.counter("backslash_test", { description: "Test backslash escaping", attributes: { path: "C:\\Users\\test" } }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() assert.include(output, "path=\"C:\\\\Users\\\\test\"") })) it.effect("escapes special characters in HELP text", () => Effect.gen(function*() { const counter = Metric.counter("help_escape", { description: "Line 1\nLine 2 with\\backslash" }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() assert.include(output, "# HELP help_escape Line 1\\nLine 2 with\\\\backslash") })) }) describe("special values", () => { it.effect("formats NaN values", () => Effect.gen(function*() { const gauge = Metric.gauge("nan_gauge", { description: "Gauge with NaN" }) yield* Metric.update(gauge, NaN) const output = yield* PrometheusMetrics.format() assert.include(output, "nan_gauge NaN") })) it.effect("formats Infinity values", () => Effect.gen(function*() { const gauge = Metric.gauge("inf_gauge", { description: "Gauge with Infinity" }) yield* Metric.update(gauge, Infinity) const output = yield* PrometheusMetrics.format() assert.include(output, "inf_gauge +Inf") })) it.effect("formats negative Infinity values", () => Effect.gen(function*() { const gauge = Metric.gauge("neg_inf_gauge", { description: "Gauge with -Infinity" }) yield* Metric.update(gauge, -Infinity) const output = yield* PrometheusMetrics.format() assert.include(output, "neg_inf_gauge -Inf") })) }) describe("empty output", () => { it.effect("returns empty string when no metrics are registered", () => Effect.gen(function*() { const output = yield* PrometheusMetrics.format() // Output may contain other metrics from the test environment, // but at minimum it should be a valid string assert.isString(output) })) }) describe("output format", () => { it.effect("ends with a newline when there are metrics", () => Effect.gen(function*() { const counter = Metric.counter("newline_test", { description: "Test newline" }) yield* Metric.update(counter, 1) const output = yield* PrometheusMetrics.format() if (output.length > 0) { assert.isTrue(output.endsWith("\n")) } })) }) }) })