@else42/cf-worker-otel
v0.2.2
Published
Lightweight OTLP metrics client for Cloudflare Workers
Readme
@elsbrock/cf-worker-otel
Push metrics from Cloudflare Workers to Prometheus, Grafana Cloud, or any OTLP-compatible backend — without pulling in the full OpenTelemetry SDK.
- ~200 lines, zero runtime dependencies
- Counters, gauges, and histograms
- OTLP/HTTP JSON with delta temporality
- Built for
waitUntil()— never blocks your response - No-ops silently without config — safe to always instrument
Install
npm install @elsbrock/cf-worker-otelPublished to GitHub Packages. Add
@elsbrock:registry=https://npm.pkg.github.comto your.npmrc.
Quick Start
import { createMetrics } from "@elsbrock/cf-worker-otel";
export default {
async fetch(request, env, ctx) {
const metrics = createMetrics({
serviceName: "my-worker",
endpoint: env.OTLP_ENDPOINT,
token: env.OTLP_AUTH_TOKEN,
});
const start = Date.now();
const response = await handleRequest(request);
metrics.counter("http_requests_total", 1, {
method: request.method,
status: String(response.status),
});
metrics.histogram("http_request_duration_ms", Date.now() - start);
ctx.waitUntil(metrics.flush());
return response;
},
};SvelteKit on Cloudflare
// src/hooks.server.ts
import { createMetrics } from "@elsbrock/cf-worker-otel";
import { sequence } from "@sveltejs/kit/hooks";
import type { Handle } from "@sveltejs/kit";
const metricsHandle: Handle = async ({ event, resolve }) => {
const env = event.platform?.env;
const metrics = createMetrics({
serviceName: "my-app",
endpoint: env?.OTLP_ENDPOINT,
token: env?.OTLP_AUTH_TOKEN,
});
const start = Date.now();
let status = "500";
try {
const response = await resolve(event);
status = String(response.status);
return response;
} finally {
metrics.counter("http_requests_total", 1, {
method: event.request.method,
status,
route: event.route.id ?? "unknown",
});
metrics.histogram("http_request_duration_ms", Date.now() - start);
event.platform?.context?.waitUntil(metrics.flush());
}
};
export const handle = sequence(metricsHandle, sessionHandle);API
createMetrics(config)
Returns a per-request collector. Create one per invocation, flush at the end.
| Option | Type | Required | Description |
|---|---|---|---|
| serviceName | string | yes | Maps to service.name in OTLP |
| endpoint | string | no | OTLP HTTP endpoint URL |
| token | string | no | Bearer token for the Authorization header |
| defaultAttributes | Record<string, string> | no | Merged into every data point |
| histogramBounds | number[] | no | Custom bucket boundaries (default: HTTP latency in ms) |
If endpoint or token is missing, flush() does nothing.
metrics.counter(name, value, attributes?)
Monotonic counter. Same name + same attributes are aggregated within the request.
metrics.gauge(name, value, attributes?)
Point-in-time value. Last write wins per attribute set.
metrics.histogram(name, value, attributes?)
Single observation placed into a bucket. Default bounds:
[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] ms.
metrics.flush()
Serializes everything to OTLP JSON and POSTs it. Pass to ctx.waitUntil().
Prometheus Setup
Enable the OTLP receiver (v2.47+):
--web.enable-otlp-receiverFor delta temporality support (v3+):
--enable-feature=otlp-deltatocumulativeThe receiver listens at POST /api/v1/otlp/v1/metrics on the Prometheus port.
How It Works
Each createMetrics() call creates an isolated collector. Metrics are accumulated in memory during request handling, then serialized to a single OTLP/HTTP JSON payload and POSTed in waitUntil(). One fetch per worker invocation — no batching across requests, no persistent state.
Delta temporality means each push contains only what happened in this request. Prometheus converts deltas to cumulative counters server-side.
