/* OTLP/HTTP JSON envelope encoding for metrics (RFC-0337). Produces the exact wire format consumed by SigNoz. Do not import node: modules — must be Workers-compatible. Do not perform HTTP — that lives in pusher.ts. RFC-0337: initial implementation. */ import type { OtlpKeyValue } from "./conventions.ts"; export type UnixNanoString = string; export function nowUnixNano(): UnixNanoString { return String(Date.now() * 1_000_000); } export interface OtlpGaugePoint { asDouble: number; timeUnixNano: UnixNanoString; attributes?: OtlpKeyValue[]; } export interface OtlpSumPoint { asDouble: number; startTimeUnixNano: UnixNanoString; timeUnixNano: UnixNanoString; attributes?: OtlpKeyValue[]; } export interface OtlpHistogramPoint { startTimeUnixNano: UnixNanoString; timeUnixNano: UnixNanoString; count: string; sum: number; bucketCounts: string[]; explicitBounds: number[]; attributes?: OtlpKeyValue[]; } export interface OtlpMetricPoint { name: string; kind: "gauge" | "sum" | "histogram"; unit?: string; isMonotonic?: boolean; gaugePoints?: OtlpGaugePoint[]; sumPoints?: OtlpSumPoint[]; histogramPoints?: OtlpHistogramPoint[]; buckets?: readonly number[]; } export interface OtlpMetricsEnvelope { resourceMetrics: Array<{ resource: { attributes: OtlpKeyValue[] }; scopeMetrics: Array<{ scope: { name: string; version: string }; metrics: unknown[]; }>; }>; } export function encodeOtlpMetrics( resourceAttributes: OtlpKeyValue[], points: OtlpMetricPoint[], scopeName: string, scopeVersion: string, ): OtlpMetricsEnvelope { const metrics: unknown[] = []; for (const point of points) { if (point.kind === "gauge" && point.gaugePoints) { metrics.push({ name: point.name, ...(point.unit ? { unit: point.unit } : {}), gauge: { dataPoints: point.gaugePoints.map((dp) => ({ asDouble: dp.asDouble, timeUnixNano: dp.timeUnixNano, ...(dp.attributes ? { attributes: dp.attributes } : {}), })), }, }); } else if (point.kind === "sum" && point.sumPoints) { metrics.push({ name: point.name, ...(point.unit ? { unit: point.unit } : {}), sum: { aggregationTemporality: 1, isMonotonic: point.isMonotonic ?? true, dataPoints: point.sumPoints.map((dp) => ({ asDouble: dp.asDouble, startTimeUnixNano: dp.startTimeUnixNano, timeUnixNano: dp.timeUnixNano, ...(dp.attributes ? { attributes: dp.attributes } : {}), })), }, }); } else if (point.kind === "histogram" && point.histogramPoints) { metrics.push({ name: point.name, ...(point.unit ? { unit: point.unit } : {}), histogram: { aggregationTemporality: 1, dataPoints: point.histogramPoints.map((dp) => ({ startTimeUnixNano: dp.startTimeUnixNano, timeUnixNano: dp.timeUnixNano, count: dp.count, sum: dp.sum, bucketCounts: dp.bucketCounts, explicitBounds: dp.explicitBounds, ...(dp.attributes ? { attributes: dp.attributes } : {}), })), }, }); } } return { resourceMetrics: [ { resource: { attributes: resourceAttributes }, scopeMetrics: [ { scope: { name: scopeName, version: scopeVersion }, metrics, }, ], }, ], }; }