EdgeAIG/opencode / .opencode /node_modules /effect /dist /unstable /observability /PrometheusMetrics.js
| import * as Effect from "../../Effect.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Metric from "../../Metric.js"; | |
| import * as HttpRouter from "../http/HttpRouter.js"; | |
| import * as HttpServerResponse from "../http/HttpServerResponse.js"; | |
| /** | |
| * Formats all metrics in the registry to Prometheus exposition format. | |
| * | |
| * **Example** (Formatting metrics) | |
| * | |
| * ```ts | |
| * import { Effect, Metric } from "effect" | |
| * import { PrometheusMetrics } from "effect/unstable/observability" | |
| * | |
| * const program = Effect.gen(function*() { | |
| * const counter = Metric.counter("api_requests_total", { | |
| * description: "Total API requests" | |
| * }) | |
| * const gauge = Metric.gauge("active_connections", { | |
| * description: "Number of active connections" | |
| * }) | |
| * | |
| * yield* Metric.update(counter, 100) | |
| * yield* Metric.update(gauge, 25) | |
| * | |
| * // Format without prefix | |
| * const output1 = yield* PrometheusMetrics.format() | |
| * | |
| * // Format with prefix | |
| * const output2 = yield* PrometheusMetrics.format({ prefix: "myapp" }) | |
| * }) | |
| * ``` | |
| * | |
| * @category formatting | |
| * @since 4.0.0 | |
| */ | |
| export const format = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const services = yield* Effect.context(); | |
| return formatUnsafe(services, options); | |
| }); | |
| /** | |
| * Formats all metrics in the registry to Prometheus exposition format synchronously. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you already have access to the context and need low-level | |
| * synchronous formatting. | |
| * | |
| * @see {@link format} for effectful formatting from the current context | |
| * | |
| * @category formatting | |
| * @since 4.0.0 | |
| */ | |
| export const formatUnsafe = (context, options) => { | |
| const snapshot = Metric.snapshotUnsafe(context); | |
| const prefix = options?.prefix ? sanitizeMetricName(options.prefix) + "_" : ""; | |
| const mapper = options?.metricNameMapper ?? (name => name); | |
| const lines = []; | |
| // Group metrics by base name for proper TYPE/HELP declarations | |
| const metricsByName = new Map(); | |
| for (let i = 0; i < snapshot.length; i++) { | |
| const metric = snapshot[i]; | |
| const name = prefix + sanitizeMetricName(mapper(metric.id)); | |
| const existing = metricsByName.get(name); | |
| if (existing) { | |
| existing.push(metric); | |
| } else { | |
| metricsByName.set(name, [metric]); | |
| } | |
| } | |
| for (const [name, metrics] of metricsByName) { | |
| formatMetricFamily(name, metrics, lines); | |
| } | |
| // Prometheus expects a trailing newline if there's content | |
| return lines.length > 0 ? lines.join("\n") + "\n" : ""; | |
| }; | |
| /** | |
| * Creates a Layer that registers a `/metrics` HTTP endpoint for Prometheus | |
| * scraping. | |
| * | |
| * **Details** | |
| * | |
| * This layer automatically adds a GET route to your HTTP router that serves | |
| * metrics in Prometheus exposition format. By default, the endpoint is | |
| * registered at `/metrics`, but this can be customized via the `path` option. | |
| * | |
| * **Example** (Serving metrics over HTTP) | |
| * | |
| * ```ts | |
| * import { PrometheusMetrics } from "effect/unstable/observability" | |
| * | |
| * // Create a layer that adds /metrics endpoint to the router | |
| * const PrometheusLayer = PrometheusMetrics.layerHttp() | |
| * | |
| * // Or customize the path and add a prefix to all metric names | |
| * const CustomPrometheusLayer = PrometheusMetrics.layerHttp({ | |
| * path: "/prometheus/metrics", | |
| * prefix: "myapp" | |
| * }) | |
| * ``` | |
| * | |
| * @category Http | |
| * @since 4.0.0 | |
| */ | |
| export const layerHttp = options => Layer.effectDiscard(Effect.gen(function* () { | |
| const router = yield* HttpRouter.HttpRouter; | |
| const { | |
| path, | |
| ...formatOptions | |
| } = options ?? {}; | |
| const handler = Effect.gen(function* () { | |
| const body = yield* format(formatOptions); | |
| return HttpServerResponse.text(body, { | |
| contentType: "text/plain; version=0.0.4; charset=utf-8" | |
| }); | |
| }); | |
| yield* router.add("GET", path ?? "/metrics", handler); | |
| })); | |
| // ----------------------------------------------------------------------------- | |
| // Internal | |
| // ----------------------------------------------------------------------------- | |
| /** | |
| * Sanitize a metric name to conform to Prometheus naming rules. | |
| * Valid characters: [a-zA-Z_:][a-zA-Z0-9_:]* | |
| */ | |
| const sanitizeMetricName = name => { | |
| // Replace invalid characters with underscores | |
| let sanitized = name.replace(/[^a-zA-Z0-9_:]/g, "_"); | |
| // Ensure it starts with a letter or underscore (not a digit or colon) | |
| if (/^[0-9:]/.test(sanitized)) { | |
| sanitized = "_" + sanitized; | |
| } | |
| // Remove consecutive underscores | |
| sanitized = sanitized.replace(/_+/g, "_"); | |
| // Remove trailing underscores | |
| sanitized = sanitized.replace(/_$/, ""); | |
| return sanitized; | |
| }; | |
| /** | |
| * Sanitize a label name to conform to Prometheus naming rules. | |
| * Valid characters: [a-zA-Z_][a-zA-Z0-9_]* | |
| */ | |
| const sanitizeLabelName = name => { | |
| // Replace invalid characters with underscores | |
| let sanitized = name.replace(/[^a-zA-Z0-9_]/g, "_"); | |
| // Ensure it starts with a letter or underscore | |
| if (/^[0-9]/.test(sanitized)) { | |
| sanitized = "_" + sanitized; | |
| } | |
| // Remove consecutive underscores | |
| sanitized = sanitized.replace(/_+/g, "_"); | |
| return sanitized; | |
| }; | |
| /** | |
| * Escape special characters in label values. | |
| * Backslash, double-quote, and newline must be escaped. | |
| */ | |
| const escapeLabelValue = value => { | |
| return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n"); | |
| }; | |
| /** | |
| * Escape special characters in HELP text. | |
| * Backslash and newline must be escaped. | |
| */ | |
| const escapeHelp = text => { | |
| return text.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); | |
| }; | |
| /** | |
| * Format a numeric value for Prometheus output. | |
| * Handles special values like NaN and Infinity. | |
| */ | |
| const formatValue = value => { | |
| if (typeof value === "bigint") { | |
| return value.toString(); | |
| } | |
| if (Number.isNaN(value)) { | |
| return "NaN"; | |
| } | |
| if (value === Infinity) { | |
| return "+Inf"; | |
| } | |
| if (value === -Infinity) { | |
| return "-Inf"; | |
| } | |
| return value.toString(); | |
| }; | |
| /** | |
| * Format labels as a Prometheus label string. | |
| * Returns empty string if no labels, otherwise returns {label1="value1",label2="value2"} | |
| */ | |
| const formatLabels = (attributes, extraLabels) => { | |
| const labels = []; | |
| // Add metric attributes as labels | |
| if (attributes) { | |
| for (const [key, value] of Object.entries(attributes)) { | |
| // Skip internal attributes like "unit" and "time_unit" | |
| if (key === "unit" || key === "time_unit") continue; | |
| labels.push(`${sanitizeLabelName(key)}="${escapeLabelValue(value)}"`); | |
| } | |
| } | |
| // Add extra labels (e.g., for histogram buckets, summary quantiles) | |
| if (extraLabels) { | |
| for (const [key, value] of extraLabels) { | |
| labels.push(`${sanitizeLabelName(key)}="${escapeLabelValue(value)}"`); | |
| } | |
| } | |
| return labels.length > 0 ? `{${labels.join(",")}}` : ""; | |
| }; | |
| /** | |
| * Map Effect metric type to Prometheus metric type string. | |
| */ | |
| const mapMetricType = type => { | |
| switch (type) { | |
| case "Counter": | |
| return "counter"; | |
| case "Gauge": | |
| return "gauge"; | |
| case "Histogram": | |
| return "histogram"; | |
| case "Summary": | |
| return "summary"; | |
| case "Frequency": | |
| return "counter"; | |
| } | |
| }; | |
| /** | |
| * Format a metric family (all metrics with the same name). | |
| */ | |
| const formatMetricFamily = (name, metrics, lines) => { | |
| const first = metrics[0]; | |
| const prometheusType = mapMetricType(first.type); | |
| // HELP line (only if description exists) | |
| if (first.description) { | |
| lines.push(`# HELP ${name} ${escapeHelp(first.description)}`); | |
| } | |
| // TYPE line | |
| lines.push(`# TYPE ${name} ${prometheusType}`); | |
| // Data lines for each metric instance | |
| for (let i = 0; i < metrics.length; i++) { | |
| formatMetricData(name, metrics[i], lines); | |
| } | |
| }; | |
| /** | |
| * Format data lines for a single metric snapshot. | |
| */ | |
| const formatMetricData = (name, metric, lines) => { | |
| switch (metric.type) { | |
| case "Counter": | |
| formatCounter(name, metric, lines); | |
| break; | |
| case "Gauge": | |
| formatGauge(name, metric, lines); | |
| break; | |
| case "Histogram": | |
| formatHistogram(name, metric, lines); | |
| break; | |
| case "Summary": | |
| formatSummary(name, metric, lines); | |
| break; | |
| case "Frequency": | |
| formatFrequency(name, metric, lines); | |
| break; | |
| } | |
| }; | |
| /** | |
| * Format a Counter metric. | |
| */ | |
| const formatCounter = (name, metric, lines) => { | |
| const labels = formatLabels(metric.attributes); | |
| const value = formatValue(metric.state.count); | |
| lines.push(`${name}${labels} ${value}`); | |
| }; | |
| /** | |
| * Format a Gauge metric. | |
| */ | |
| const formatGauge = (name, metric, lines) => { | |
| const labels = formatLabels(metric.attributes); | |
| const value = formatValue(metric.state.value); | |
| lines.push(`${name}${labels} ${value}`); | |
| }; | |
| /** | |
| * Format a Histogram metric. | |
| * Produces _bucket, _sum, and _count lines. | |
| */ | |
| const formatHistogram = (name, metric, lines) => { | |
| const state = metric.state; | |
| // Format bucket lines | |
| // Effect buckets are [boundary, cumulativeCount] pairs | |
| for (let i = 0; i < state.buckets.length; i++) { | |
| const [boundary, cumulativeCount] = state.buckets[i]; | |
| const bucketLabels = formatLabels(metric.attributes, [["le", boundary.toString()]]); | |
| lines.push(`${name}_bucket${bucketLabels} ${cumulativeCount}`); | |
| } | |
| // Add +Inf bucket (total count) | |
| const infLabels = formatLabels(metric.attributes, [["le", "+Inf"]]); | |
| lines.push(`${name}_bucket${infLabels} ${state.count}`); | |
| // Sum and count | |
| const baseLabels = formatLabels(metric.attributes); | |
| lines.push(`${name}_sum${baseLabels} ${formatValue(state.sum)}`); | |
| lines.push(`${name}_count${baseLabels} ${state.count}`); | |
| }; | |
| /** | |
| * Format a Summary metric. | |
| * Produces quantile lines, _sum, and _count lines. | |
| */ | |
| const formatSummary = (name, metric, lines) => { | |
| const state = metric.state; | |
| // Format quantile lines | |
| for (let i = 0; i < state.quantiles.length; i++) { | |
| const [quantile, value] = state.quantiles[i]; | |
| // Only output quantiles with defined values | |
| if (value !== undefined) { | |
| const quantileLabels = formatLabels(metric.attributes, [["quantile", quantile.toString()]]); | |
| lines.push(`${name}${quantileLabels} ${formatValue(value)}`); | |
| } | |
| } | |
| // Sum and count | |
| const baseLabels = formatLabels(metric.attributes); | |
| lines.push(`${name}_sum${baseLabels} ${formatValue(state.sum)}`); | |
| lines.push(`${name}_count${baseLabels} ${state.count}`); | |
| }; | |
| /** | |
| * Format a Frequency metric as a counter with key labels. | |
| */ | |
| const formatFrequency = (name, metric, lines) => { | |
| const state = metric.state; | |
| // Each occurrence becomes a separate line with a "key" label | |
| for (const [key, count] of state.occurrences) { | |
| const labels = formatLabels(metric.attributes, [["key", key]]); | |
| lines.push(`${name}${labels} ${count}`); | |
| } | |
| }; | |
| //# sourceMappingURL=PrometheusMetrics.js.map |
Xet Storage Details
- Size:
- 10.6 kB
- Xet hash:
- 2ff328579ede4d19a8fd13574cf8425d3a69db5b5e8881fbb660a24e2c2812c7
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.