EdgeAIG/opencode / .opencode /node_modules /effect /src /unstable /observability /PrometheusMetrics.ts
| /** | |
| * Formats Effect metrics for Prometheus. | |
| * | |
| * This module reads metrics from the current Effect context and renders them in | |
| * the Prometheus text format. It can also register a pull-based HTTP endpoint, | |
| * such as `/metrics`, for Prometheus to scrape. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import type * as Context from "../../Context.ts" | |
| import * as Effect from "../../Effect.ts" | |
| import * as Layer from "../../Layer.ts" | |
| import * as Metric from "../../Metric.ts" | |
| import * as HttpRouter from "../http/HttpRouter.ts" | |
| import * as HttpServerResponse from "../http/HttpServerResponse.ts" | |
| /** | |
| * A function that transforms metric names before formatting. | |
| * | |
| * **Example** (Mapping metric names) | |
| * | |
| * ```ts | |
| * import type { PrometheusMetrics } from "effect/unstable/observability" | |
| * | |
| * // Convert camelCase to snake_case | |
| * const mapper: PrometheusMetrics.MetricNameMapper = (name) => | |
| * name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() | |
| * ``` | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type MetricNameMapper = (name: string) => string | |
| /** | |
| * Options for formatting metrics. | |
| * | |
| * @category options | |
| * @since 4.0.0 | |
| */ | |
| export interface FormatOptions { | |
| /** | |
| * Optional prefix to prepend to all metric names. | |
| * The prefix will be sanitized and joined with an underscore. | |
| */ | |
| readonly prefix?: string | undefined | |
| /** | |
| * Optional function to transform metric names before sanitization. | |
| */ | |
| readonly metricNameMapper?: MetricNameMapper | undefined | |
| } | |
| /** | |
| * Options for exporting Prometheus metrics over HTTP. | |
| * | |
| * @category options | |
| * @since 4.0.0 | |
| */ | |
| export interface HttpOptions extends FormatOptions { | |
| /** | |
| * The path to the HTTP route on which Prometheus metrics should be served. | |
| */ | |
| readonly path?: HttpRouter.PathInput | undefined | |
| } | |
| /** | |
| * 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: (options?: FormatOptions | undefined) => Effect.Effect<string> = Effect.fnUntraced( | |
| function*(options) { | |
| const services = yield* Effect.context<never>() | |
| 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: Context.Context<never>, | |
| options?: FormatOptions | undefined | |
| ): string => { | |
| const snapshot = Metric.snapshotUnsafe(context) | |
| const prefix = options?.prefix ? sanitizeMetricName(options.prefix) + "_" : "" | |
| const mapper = options?.metricNameMapper ?? ((name: string) => name) | |
| const lines: Array<string> = [] | |
| // Group metrics by base name for proper TYPE/HELP declarations | |
| const metricsByName = new Map<string, Array<Metric.Metric.Snapshot>>() | |
| 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?: HttpOptions | undefined | |
| ): Layer.Layer<never, never, HttpRouter.HttpRouter> => | |
| 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: string): string => { | |
| // 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: string): string => { | |
| // 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: string): string => { | |
| 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: string): string => { | |
| 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: number | bigint): string => { | |
| 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: Metric.Metric.AttributeSet | undefined, | |
| extraLabels?: Array<[string, string]> | |
| ): string => { | |
| const labels: Array<string> = [] | |
| // 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: Metric.Metric.Type): string => { | |
| 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: string, | |
| metrics: Array<Metric.Metric.Snapshot>, | |
| lines: Array<string> | |
| ): void => { | |
| 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: string, | |
| metric: Metric.Metric.Snapshot, | |
| lines: Array<string> | |
| ): void => { | |
| 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: string, | |
| metric: Metric.Metric.SnapshotProto<"Counter", Metric.CounterState<number | bigint>>, | |
| lines: Array<string> | |
| ): void => { | |
| const labels = formatLabels(metric.attributes) | |
| const value = formatValue(metric.state.count) | |
| lines.push(`${name}${labels} ${value}`) | |
| } | |
| /** | |
| * Format a Gauge metric. | |
| */ | |
| const formatGauge = ( | |
| name: string, | |
| metric: Metric.Metric.SnapshotProto<"Gauge", Metric.GaugeState<number | bigint>>, | |
| lines: Array<string> | |
| ): void => { | |
| 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: string, | |
| metric: Metric.Metric.SnapshotProto<"Histogram", Metric.HistogramState>, | |
| lines: Array<string> | |
| ): void => { | |
| 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: string, | |
| metric: Metric.Metric.SnapshotProto<"Summary", Metric.SummaryState>, | |
| lines: Array<string> | |
| ): void => { | |
| 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: string, | |
| metric: Metric.Metric.SnapshotProto<"Frequency", Metric.FrequencyState>, | |
| lines: Array<string> | |
| ): void => { | |
| 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}`) | |
| } | |
| } | |
Xet Storage Details
- Size:
- 13.2 kB
- Xet hash:
- 60223623a12c5a8b618b7c625db0d205fa98cf00b0620c1328db07006499c2af
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.