import type * as Log from './internal/Log.js' /** Tag values a metric backend accepts. */ export type Tags = Record /** * The bounded StatsD-style surface the built-in metric sinks emit to, plus an * optional high-cardinality {@link Metrics.analytics} hook. Apps supply a * concrete backend — e.g. {@link cloudflare} — so core carries no metrics-vendor * dependency. Pass it as `metrics` to `App.create` and the webhook poller/queue * handlers. */ export type Metrics = { /** * Optional per-request analytics sink, called once with the request-log entry * for every response. Unlike the StatsD `count`/`gauge`/`histogram` surface * (which stays low-cardinality), this hook carries the full entry — caller * ids, org ids, route, status — for a wide-event store (e.g. ClickHouse). * Attach it via {@link cloudflare}'s `analytics` option (or {@link from} for a * custom backend); omit it to drop analytics. May be async; `App.create` does * not await it. */ analytics?: ((entry: Log.Entry) => Promise | void) | undefined /** Increments a counter; same name+tags are summed. */ count(name: string, value: number, tags?: Tags): void /** Ships accumulated metrics. */ flush(): void /** Sets a gauge; last write wins for the same name+tags. */ gauge(name: string, value: number, tags?: Tags): void /** Records a histogram observation. */ histogram(name: string, value: number, tags?: Tags): void } /** * Creates a {@link Metrics} from a value, type-checking it against the contract. * The escape hatch for bringing your own backend (Datadog, Prometheus, …): * author the implementation inline and have it validated against {@link Metrics}. */ export function from(metrics: metrics): metrics { return metrics } /** Options for the {@link cloudflare} preset and its backend. */ export declare namespace cloudflare { /** Shared context handed to the {@link Options.analytics} factory. */ type Context = { /** Resolved `enabled` flag; the analytics sink should drop events when false. */ enabled: boolean /** Deployment environment, also added as the `environment` global tag. */ environment?: string | undefined /** Service name, also added as the `service` global tag. */ service?: string | undefined } type Options = { /** * Builds the per-request {@link Metrics.analytics} hook from the shared * {@link Context}, so the sink reuses `enabled`/`environment`/`service` * instead of repeating them. Omit to drop analytics. */ analytics?: ((context: Context) => Metrics['analytics']) | undefined /** Set false (e.g. in local `wrangler dev`) to drop every metric and analytics event. Default true. */ enabled?: boolean | undefined /** Deployment environment, added as the `environment` global tag and shared with {@link Options.analytics}. */ environment?: string | undefined /** Service name, added as the `service` global tag and shared with {@link Options.analytics}. */ service?: string | undefined } } /** * A Cloudflare Workers {@link Metrics} — pass it as `metrics` to `App.create` * and the webhook poller/queue handlers. It buffers metrics and emits them on * `flush()` as `cwm-`-prefixed `console.log` lines (one per ≤250 KiB batch) for * a Workers Logs → metrics pipeline to scrape; no external dependency. */ export function cloudflare(options: cloudflare.Options = {}): Metrics { type Entry = { n: string; t: 'c' | 'g' | 'h'; tags: Tags; ts: number; v: number } const enabled = options.enabled !== false const analytics = options.analytics?.({ enabled, environment: options.environment, service: options.service, }) if (!enabled) return { analytics, count() {}, flush() {}, gauge() {}, histogram() {} } const globalTags: Tags = { ...(options.environment === undefined ? {} : { environment: options.environment }), ...(options.service === undefined ? {} : { service: options.service }), } const counts = new Map() const gauges = new Map() const histograms: Entry[] = [] const key = (name: string, tags: Tags) => { let s = name for (const k of Object.keys(tags).sort()) s += `\0${k}\0${tags[k]}` return s } const withGlobal = (tags?: Tags): Tags => ({ ...globalTags, ...tags }) return { analytics, count(name, value, tags) { const merged = withGlobal(tags) const k = key(name, merged) const existing = counts.get(k) if (existing) existing.v += value else counts.set(k, { n: name, t: 'c', tags: merged, ts: 0, v: value }) }, flush() { const ts = Date.now() const entries: Entry[] = [] for (const e of counts.values()) entries.push({ ...e, ts }) for (const e of gauges.values()) entries.push({ ...e, ts }) entries.push(...histograms) counts.clear() gauges.clear() histograms.length = 0 if (!entries.length) return // Emit as `cwm-[…]` lines, splitting batches over the size cap. Serialize // with the field order the Workers Logs pipeline emits (`t,n,v,tags,ts`). const parts = entries.map((e) => JSON.stringify({ t: e.t, n: e.n, v: e.v, tags: e.tags, ts: e.ts }), ) let buf: string[] = [] let bufLen = 0 for (const part of parts) { // 5 bytes for the `cwm-[` / `]` frame, plus one comma per joined part. const wouldBe = 5 + (buf.length + 1) + bufLen + part.length // 250 KiB: the largest `console.log` payload Workers Logs keeps untruncated. if (buf.length > 0 && wouldBe > 250 * 1024) { console.log(`cwm-[${buf.join(',')}]`) buf = [] bufLen = 0 } buf.push(part) bufLen += part.length } if (buf.length) console.log(`cwm-[${buf.join(',')}]`) }, gauge(name, value, tags) { const merged = withGlobal(tags) gauges.set(key(name, merged), { n: name, t: 'g', tags: merged, ts: 0, v: value }) }, histogram(name, value, tags) { histograms.push({ n: name, t: 'h', tags: withGlobal(tags), ts: Date.now(), v: value }) }, } }