@vivero/stoma 0.1.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable metrics collection for the gateway pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Defines the {@link MetricsCollector} interface and provides an
|
|
5
|
+
* {@link InMemoryMetricsCollector} for testing and development.
|
|
6
|
+
* The {@link toPrometheusText} function serializes a snapshot to
|
|
7
|
+
* Prometheus text exposition format.
|
|
8
|
+
*
|
|
9
|
+
* @module metrics
|
|
10
|
+
*/
|
|
11
|
+
/** A single tagged metric data point. */
|
|
12
|
+
interface TaggedValue {
|
|
13
|
+
value: number;
|
|
14
|
+
tags?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** A histogram data point with accumulated values. */
|
|
17
|
+
interface HistogramEntry {
|
|
18
|
+
values: number[];
|
|
19
|
+
tags?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/** Point-in-time snapshot of all collected metrics. */
|
|
22
|
+
interface MetricsSnapshot {
|
|
23
|
+
counters: Record<string, TaggedValue[]>;
|
|
24
|
+
histograms: Record<string, HistogramEntry[]>;
|
|
25
|
+
gauges: Record<string, TaggedValue[]>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pluggable metrics collector interface.
|
|
29
|
+
*
|
|
30
|
+
* Implementations can ship metrics to Prometheus, Datadog, CloudWatch,
|
|
31
|
+
* or any other backend. The gateway pipeline records request counts,
|
|
32
|
+
* latencies, and error rates through this interface.
|
|
33
|
+
*/
|
|
34
|
+
interface MetricsCollector {
|
|
35
|
+
/** Increment a counter by `value` (default 1). */
|
|
36
|
+
increment(name: string, value?: number, tags?: Record<string, string>): void;
|
|
37
|
+
/** Record a histogram observation. */
|
|
38
|
+
histogram(name: string, value: number, tags?: Record<string, string>): void;
|
|
39
|
+
/** Set a gauge to an absolute value. */
|
|
40
|
+
gauge(name: string, value: number, tags?: Record<string, string>): void;
|
|
41
|
+
/** Return a point-in-time snapshot of all metrics. */
|
|
42
|
+
snapshot(): MetricsSnapshot;
|
|
43
|
+
/** Reset all metrics to zero. */
|
|
44
|
+
reset(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* In-memory metrics collector for testing, development, and admin API.
|
|
48
|
+
*
|
|
49
|
+
* Accumulates counters, histograms, and gauges in plain arrays/maps.
|
|
50
|
+
* Not intended for high-throughput production use - prefer shipping
|
|
51
|
+
* metrics to a dedicated backend for production workloads.
|
|
52
|
+
*/
|
|
53
|
+
declare class InMemoryMetricsCollector implements MetricsCollector {
|
|
54
|
+
private counters;
|
|
55
|
+
private histograms;
|
|
56
|
+
private gauges;
|
|
57
|
+
increment(name: string, value?: number, tags?: Record<string, string>): void;
|
|
58
|
+
histogram(name: string, value: number, tags?: Record<string, string>): void;
|
|
59
|
+
gauge(name: string, value: number, tags?: Record<string, string>): void;
|
|
60
|
+
snapshot(): MetricsSnapshot;
|
|
61
|
+
reset(): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Serialize a metrics snapshot to Prometheus text exposition format.
|
|
65
|
+
*
|
|
66
|
+
* Produces lines like:
|
|
67
|
+
* ```
|
|
68
|
+
* gateway_requests_total{method="GET",status="200"} 42
|
|
69
|
+
* gateway_request_duration_ms_sum{method="GET"} 1234
|
|
70
|
+
* gateway_request_duration_ms_count{method="GET"} 10
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @param snapshot - The metrics snapshot to serialize.
|
|
74
|
+
* @returns Prometheus text exposition format string.
|
|
75
|
+
*/
|
|
76
|
+
declare function toPrometheusText(snapshot: MetricsSnapshot): string;
|
|
77
|
+
|
|
78
|
+
export { type HistogramEntry, InMemoryMetricsCollector, type MetricsCollector, type MetricsSnapshot, type TaggedValue, toPrometheusText };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function tagKey(tags) {
|
|
2
|
+
if (!tags || Object.keys(tags).length === 0) return "";
|
|
3
|
+
return Object.entries(tags).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
|
|
4
|
+
}
|
|
5
|
+
class InMemoryMetricsCollector {
|
|
6
|
+
counters = /* @__PURE__ */ new Map();
|
|
7
|
+
histograms = /* @__PURE__ */ new Map();
|
|
8
|
+
gauges = /* @__PURE__ */ new Map();
|
|
9
|
+
increment(name, value = 1, tags) {
|
|
10
|
+
const key = tagKey(tags);
|
|
11
|
+
let metricMap = this.counters.get(name);
|
|
12
|
+
if (!metricMap) {
|
|
13
|
+
metricMap = /* @__PURE__ */ new Map();
|
|
14
|
+
this.counters.set(name, metricMap);
|
|
15
|
+
}
|
|
16
|
+
const existing = metricMap.get(key);
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.value += value;
|
|
19
|
+
} else {
|
|
20
|
+
metricMap.set(key, { value, tags });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
histogram(name, value, tags) {
|
|
24
|
+
const key = tagKey(tags);
|
|
25
|
+
let metricMap = this.histograms.get(name);
|
|
26
|
+
if (!metricMap) {
|
|
27
|
+
metricMap = /* @__PURE__ */ new Map();
|
|
28
|
+
this.histograms.set(name, metricMap);
|
|
29
|
+
}
|
|
30
|
+
const existing = metricMap.get(key);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.values.push(value);
|
|
33
|
+
} else {
|
|
34
|
+
metricMap.set(key, { values: [value], tags });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
gauge(name, value, tags) {
|
|
38
|
+
const key = tagKey(tags);
|
|
39
|
+
let metricMap = this.gauges.get(name);
|
|
40
|
+
if (!metricMap) {
|
|
41
|
+
metricMap = /* @__PURE__ */ new Map();
|
|
42
|
+
this.gauges.set(name, metricMap);
|
|
43
|
+
}
|
|
44
|
+
metricMap.set(key, { value, tags });
|
|
45
|
+
}
|
|
46
|
+
snapshot() {
|
|
47
|
+
const snap = {
|
|
48
|
+
counters: {},
|
|
49
|
+
histograms: {},
|
|
50
|
+
gauges: {}
|
|
51
|
+
};
|
|
52
|
+
for (const [name, metricMap] of this.counters) {
|
|
53
|
+
snap.counters[name] = Array.from(metricMap.values());
|
|
54
|
+
}
|
|
55
|
+
for (const [name, metricMap] of this.histograms) {
|
|
56
|
+
snap.histograms[name] = Array.from(metricMap.values());
|
|
57
|
+
}
|
|
58
|
+
for (const [name, metricMap] of this.gauges) {
|
|
59
|
+
snap.gauges[name] = Array.from(metricMap.values());
|
|
60
|
+
}
|
|
61
|
+
return snap;
|
|
62
|
+
}
|
|
63
|
+
reset() {
|
|
64
|
+
this.counters.clear();
|
|
65
|
+
this.histograms.clear();
|
|
66
|
+
this.gauges.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function toPrometheusText(snapshot) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const [name, entries] of Object.entries(snapshot.counters)) {
|
|
72
|
+
lines.push(`# TYPE ${name} counter`);
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
lines.push(`${name}${formatLabels(entry.tags)} ${entry.value}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [name, entries] of Object.entries(snapshot.histograms)) {
|
|
78
|
+
lines.push(`# TYPE ${name} histogram`);
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const labels = formatLabels(entry.tags);
|
|
81
|
+
const sum = entry.values.reduce((a, b) => a + b, 0);
|
|
82
|
+
const count = entry.values.length;
|
|
83
|
+
lines.push(`${name}_sum${labels} ${sum}`);
|
|
84
|
+
lines.push(`${name}_count${labels} ${count}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const [name, entries] of Object.entries(snapshot.gauges)) {
|
|
88
|
+
lines.push(`# TYPE ${name} gauge`);
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
lines.push(`${name}${formatLabels(entry.tags)} ${entry.value}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
function escapeLabelValue(value) {
|
|
96
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
97
|
+
}
|
|
98
|
+
function formatLabels(tags) {
|
|
99
|
+
if (!tags || Object.keys(tags).length === 0) return "";
|
|
100
|
+
const parts = Object.entries(tags).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}="${escapeLabelValue(v)}"`);
|
|
101
|
+
return `{${parts.join(",")}}`;
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
InMemoryMetricsCollector,
|
|
105
|
+
toPrometheusText
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/observability/metrics.ts"],"sourcesContent":["/**\n * Pluggable metrics collection for the gateway pipeline.\n *\n * Defines the {@link MetricsCollector} interface and provides an\n * {@link InMemoryMetricsCollector} for testing and development.\n * The {@link toPrometheusText} function serializes a snapshot to\n * Prometheus text exposition format.\n *\n * @module metrics\n */\n\n/** A single tagged metric data point. */\nexport interface TaggedValue {\n value: number;\n tags?: Record<string, string>;\n}\n\n/** A histogram data point with accumulated values. */\nexport interface HistogramEntry {\n values: number[];\n tags?: Record<string, string>;\n}\n\n/** Point-in-time snapshot of all collected metrics. */\nexport interface MetricsSnapshot {\n counters: Record<string, TaggedValue[]>;\n histograms: Record<string, HistogramEntry[]>;\n gauges: Record<string, TaggedValue[]>;\n}\n\n/**\n * Pluggable metrics collector interface.\n *\n * Implementations can ship metrics to Prometheus, Datadog, CloudWatch,\n * or any other backend. The gateway pipeline records request counts,\n * latencies, and error rates through this interface.\n */\nexport interface MetricsCollector {\n /** Increment a counter by `value` (default 1). */\n increment(name: string, value?: number, tags?: Record<string, string>): void;\n /** Record a histogram observation. */\n histogram(name: string, value: number, tags?: Record<string, string>): void;\n /** Set a gauge to an absolute value. */\n gauge(name: string, value: number, tags?: Record<string, string>): void;\n /** Return a point-in-time snapshot of all metrics. */\n snapshot(): MetricsSnapshot;\n /** Reset all metrics to zero. */\n reset(): void;\n}\n\n/**\n * Serialize a tag map to a stable sorted key for deduplication.\n * Tags are sorted alphabetically to ensure consistent keys.\n */\nfunction tagKey(tags?: Record<string, string>): string {\n if (!tags || Object.keys(tags).length === 0) return \"\";\n return Object.entries(tags)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(\",\");\n}\n\n/**\n * In-memory metrics collector for testing, development, and admin API.\n *\n * Accumulates counters, histograms, and gauges in plain arrays/maps.\n * Not intended for high-throughput production use - prefer shipping\n * metrics to a dedicated backend for production workloads.\n */\nexport class InMemoryMetricsCollector implements MetricsCollector {\n private counters = new Map<string, Map<string, TaggedValue>>();\n private histograms = new Map<string, Map<string, HistogramEntry>>();\n private gauges = new Map<string, Map<string, TaggedValue>>();\n\n increment(name: string, value = 1, tags?: Record<string, string>): void {\n const key = tagKey(tags);\n let metricMap = this.counters.get(name);\n if (!metricMap) {\n metricMap = new Map();\n this.counters.set(name, metricMap);\n }\n const existing = metricMap.get(key);\n if (existing) {\n existing.value += value;\n } else {\n metricMap.set(key, { value, tags });\n }\n }\n\n histogram(name: string, value: number, tags?: Record<string, string>): void {\n const key = tagKey(tags);\n let metricMap = this.histograms.get(name);\n if (!metricMap) {\n metricMap = new Map();\n this.histograms.set(name, metricMap);\n }\n const existing = metricMap.get(key);\n if (existing) {\n existing.values.push(value);\n } else {\n metricMap.set(key, { values: [value], tags });\n }\n }\n\n gauge(name: string, value: number, tags?: Record<string, string>): void {\n const key = tagKey(tags);\n let metricMap = this.gauges.get(name);\n if (!metricMap) {\n metricMap = new Map();\n this.gauges.set(name, metricMap);\n }\n metricMap.set(key, { value, tags });\n }\n\n snapshot(): MetricsSnapshot {\n const snap: MetricsSnapshot = {\n counters: {},\n histograms: {},\n gauges: {},\n };\n\n for (const [name, metricMap] of this.counters) {\n snap.counters[name] = Array.from(metricMap.values());\n }\n for (const [name, metricMap] of this.histograms) {\n snap.histograms[name] = Array.from(metricMap.values());\n }\n for (const [name, metricMap] of this.gauges) {\n snap.gauges[name] = Array.from(metricMap.values());\n }\n\n return snap;\n }\n\n reset(): void {\n this.counters.clear();\n this.histograms.clear();\n this.gauges.clear();\n }\n}\n\n/**\n * Serialize a metrics snapshot to Prometheus text exposition format.\n *\n * Produces lines like:\n * ```\n * gateway_requests_total{method=\"GET\",status=\"200\"} 42\n * gateway_request_duration_ms_sum{method=\"GET\"} 1234\n * gateway_request_duration_ms_count{method=\"GET\"} 10\n * ```\n *\n * @param snapshot - The metrics snapshot to serialize.\n * @returns Prometheus text exposition format string.\n */\nexport function toPrometheusText(snapshot: MetricsSnapshot): string {\n const lines: string[] = [];\n\n // Counters\n for (const [name, entries] of Object.entries(snapshot.counters)) {\n lines.push(`# TYPE ${name} counter`);\n for (const entry of entries) {\n lines.push(`${name}${formatLabels(entry.tags)} ${entry.value}`);\n }\n }\n\n // Histograms - emit sum and count\n for (const [name, entries] of Object.entries(snapshot.histograms)) {\n lines.push(`# TYPE ${name} histogram`);\n for (const entry of entries) {\n const labels = formatLabels(entry.tags);\n const sum = entry.values.reduce((a, b) => a + b, 0);\n const count = entry.values.length;\n lines.push(`${name}_sum${labels} ${sum}`);\n lines.push(`${name}_count${labels} ${count}`);\n }\n }\n\n // Gauges\n for (const [name, entries] of Object.entries(snapshot.gauges)) {\n lines.push(`# TYPE ${name} gauge`);\n for (const entry of entries) {\n lines.push(`${name}${formatLabels(entry.tags)} ${entry.value}`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/** Escape a label value per Prometheus exposition format spec. */\nfunction escapeLabelValue(value: string): string {\n return value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\");\n}\n\nfunction formatLabels(tags?: Record<string, string>): string {\n if (!tags || Object.keys(tags).length === 0) return \"\";\n const parts = Object.entries(tags)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=\"${escapeLabelValue(v)}\"`);\n return `{${parts.join(\",\")}}`;\n}\n"],"mappings":"AAsDA,SAAS,OAAO,MAAuC;AACrD,MAAI,CAAC,QAAQ,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO;AACpD,SAAO,OAAO,QAAQ,IAAI,EACvB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,EACrC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAC3B,KAAK,GAAG;AACb;AASO,MAAM,yBAAqD;AAAA,EACxD,WAAW,oBAAI,IAAsC;AAAA,EACrD,aAAa,oBAAI,IAAyC;AAAA,EAC1D,SAAS,oBAAI,IAAsC;AAAA,EAE3D,UAAU,MAAc,QAAQ,GAAG,MAAqC;AACtE,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,YAAY,KAAK,SAAS,IAAI,IAAI;AACtC,QAAI,CAAC,WAAW;AACd,kBAAY,oBAAI,IAAI;AACpB,WAAK,SAAS,IAAI,MAAM,SAAS;AAAA,IACnC;AACA,UAAM,WAAW,UAAU,IAAI,GAAG;AAClC,QAAI,UAAU;AACZ,eAAS,SAAS;AAAA,IACpB,OAAO;AACL,gBAAU,IAAI,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,UAAU,MAAc,OAAe,MAAqC;AAC1E,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,YAAY,KAAK,WAAW,IAAI,IAAI;AACxC,QAAI,CAAC,WAAW;AACd,kBAAY,oBAAI,IAAI;AACpB,WAAK,WAAW,IAAI,MAAM,SAAS;AAAA,IACrC;AACA,UAAM,WAAW,UAAU,IAAI,GAAG;AAClC,QAAI,UAAU;AACZ,eAAS,OAAO,KAAK,KAAK;AAAA,IAC5B,OAAO;AACL,gBAAU,IAAI,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,MAAc,OAAe,MAAqC;AACtE,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,YAAY,KAAK,OAAO,IAAI,IAAI;AACpC,QAAI,CAAC,WAAW;AACd,kBAAY,oBAAI,IAAI;AACpB,WAAK,OAAO,IAAI,MAAM,SAAS;AAAA,IACjC;AACA,cAAU,IAAI,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,EACpC;AAAA,EAEA,WAA4B;AAC1B,UAAM,OAAwB;AAAA,MAC5B,UAAU,CAAC;AAAA,MACX,YAAY,CAAC;AAAA,MACb,QAAQ,CAAC;AAAA,IACX;AAEA,eAAW,CAAC,MAAM,SAAS,KAAK,KAAK,UAAU;AAC7C,WAAK,SAAS,IAAI,IAAI,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,IACrD;AACA,eAAW,CAAC,MAAM,SAAS,KAAK,KAAK,YAAY;AAC/C,WAAK,WAAW,IAAI,IAAI,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,IACvD;AACA,eAAW,CAAC,MAAM,SAAS,KAAK,KAAK,QAAQ;AAC3C,WAAK,OAAO,IAAI,IAAI,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,IACnD;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,QAAc;AACZ,SAAK,SAAS,MAAM;AACpB,SAAK,WAAW,MAAM;AACtB,SAAK,OAAO,MAAM;AAAA,EACpB;AACF;AAeO,SAAS,iBAAiB,UAAmC;AAClE,QAAM,QAAkB,CAAC;AAGzB,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,QAAQ,GAAG;AAC/D,UAAM,KAAK,UAAU,IAAI,UAAU;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,GAAG,IAAI,GAAG,aAAa,MAAM,IAAI,CAAC,IAAI,MAAM,KAAK,EAAE;AAAA,IAChE;AAAA,EACF;AAGA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,UAAU,GAAG;AACjE,UAAM,KAAK,UAAU,IAAI,YAAY;AACrC,eAAW,SAAS,SAAS;AAC3B,YAAM,SAAS,aAAa,MAAM,IAAI;AACtC,YAAM,MAAM,MAAM,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAClD,YAAM,QAAQ,MAAM,OAAO;AAC3B,YAAM,KAAK,GAAG,IAAI,OAAO,MAAM,IAAI,GAAG,EAAE;AACxC,YAAM,KAAK,GAAG,IAAI,SAAS,MAAM,IAAI,KAAK,EAAE;AAAA,IAC9C;AAAA,EACF;AAGA,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,MAAM,GAAG;AAC7D,UAAM,KAAK,UAAU,IAAI,QAAQ;AACjC,eAAW,SAAS,SAAS;AAC3B,YAAM,KAAK,GAAG,IAAI,GAAG,aAAa,MAAM,IAAI,CAAC,IAAI,MAAM,KAAK,EAAE;AAAA,IAChE;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAGA,SAAS,iBAAiB,OAAuB;AAC/C,SAAO,MACJ,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK;AACzB;AAEA,SAAS,aAAa,MAAuC;AAC3D,MAAI,CAAC,QAAQ,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO;AACpD,QAAM,QAAQ,OAAO,QAAQ,IAAI,EAC9B,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,EACrC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,iBAAiB,CAAC,CAAC,GAAG;AAClD,SAAO,IAAI,MAAM,KAAK,GAAG,CAAC;AAC5B;","names":[]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight OpenTelemetry-compatible tracing for edge runtimes.
|
|
3
|
+
*
|
|
4
|
+
* Provides span creation, OTLP/HTTP JSON export via `fetch()`, and
|
|
5
|
+
* head-based sampling - all without any runtime dependencies beyond
|
|
6
|
+
* the Web Platform APIs available in Cloudflare Workers.
|
|
7
|
+
*
|
|
8
|
+
* Follows the OTel data model but uses own lightweight types to avoid
|
|
9
|
+
* pulling in `@opentelemetry/api` (which is not edge-compatible).
|
|
10
|
+
*
|
|
11
|
+
* @module tracing
|
|
12
|
+
*/
|
|
13
|
+
type SpanKind = "SERVER" | "CLIENT" | "INTERNAL";
|
|
14
|
+
type SpanStatusCode = "UNSET" | "OK" | "ERROR";
|
|
15
|
+
/** An immutable representation of a completed span. */
|
|
16
|
+
interface ReadableSpan {
|
|
17
|
+
traceId: string;
|
|
18
|
+
spanId: string;
|
|
19
|
+
parentSpanId?: string;
|
|
20
|
+
name: string;
|
|
21
|
+
kind: SpanKind;
|
|
22
|
+
startTimeMs: number;
|
|
23
|
+
endTimeMs: number;
|
|
24
|
+
attributes: Record<string, string | number | boolean>;
|
|
25
|
+
status: {
|
|
26
|
+
code: SpanStatusCode;
|
|
27
|
+
message?: string;
|
|
28
|
+
};
|
|
29
|
+
events: SpanEvent[];
|
|
30
|
+
}
|
|
31
|
+
/** A timestamped event recorded during a span's lifetime. */
|
|
32
|
+
interface SpanEvent {
|
|
33
|
+
name: string;
|
|
34
|
+
timeMs: number;
|
|
35
|
+
attributes?: Record<string, string | number | boolean>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pluggable span exporter interface.
|
|
39
|
+
*
|
|
40
|
+
* Implementations ship completed spans to a backend (OTLP collector,
|
|
41
|
+
* console, or any custom destination). Export is expected to be called
|
|
42
|
+
* via `waitUntil()` so it does not block the response.
|
|
43
|
+
*/
|
|
44
|
+
interface SpanExporter {
|
|
45
|
+
export(spans: ReadableSpan[]): Promise<void>;
|
|
46
|
+
shutdown?(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** Configuration for gateway-level tracing. */
|
|
49
|
+
interface TracingConfig {
|
|
50
|
+
exporter: SpanExporter;
|
|
51
|
+
serviceName?: string;
|
|
52
|
+
serviceVersion?: string;
|
|
53
|
+
/** Head-based sampling rate [0.0, 1.0]. Default: 1.0 */
|
|
54
|
+
sampleRate?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* OTel semantic convention attribute keys (HTTP subset).
|
|
58
|
+
*
|
|
59
|
+
* Uses the stable HTTP semconv names from the OpenTelemetry specification.
|
|
60
|
+
* @see https://opentelemetry.io/docs/specs/semconv/http/
|
|
61
|
+
*/
|
|
62
|
+
declare const SemConv: {
|
|
63
|
+
readonly HTTP_METHOD: "http.request.method";
|
|
64
|
+
readonly HTTP_ROUTE: "http.route";
|
|
65
|
+
readonly HTTP_STATUS_CODE: "http.response.status_code";
|
|
66
|
+
readonly URL_PATH: "url.path";
|
|
67
|
+
readonly SERVER_ADDRESS: "server.address";
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Mutable span builder - accumulates attributes, events, and status
|
|
71
|
+
* during a request lifecycle. Call {@link end} to produce an immutable
|
|
72
|
+
* {@link ReadableSpan}.
|
|
73
|
+
*/
|
|
74
|
+
declare class SpanBuilder {
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly kind: SpanKind;
|
|
77
|
+
readonly traceId: string;
|
|
78
|
+
readonly spanId: string;
|
|
79
|
+
readonly parentSpanId?: string | undefined;
|
|
80
|
+
readonly startTimeMs: number;
|
|
81
|
+
private _attributes;
|
|
82
|
+
private _events;
|
|
83
|
+
private _status;
|
|
84
|
+
private _endTimeMs?;
|
|
85
|
+
constructor(name: string, kind: SpanKind, traceId: string, spanId: string, parentSpanId?: string | undefined, startTimeMs?: number);
|
|
86
|
+
/** Set a single attribute. Chainable. */
|
|
87
|
+
setAttribute(key: string, value: string | number | boolean): this;
|
|
88
|
+
/** Record a timestamped event with optional attributes. Chainable. */
|
|
89
|
+
addEvent(name: string, attributes?: Record<string, string | number | boolean>): this;
|
|
90
|
+
/** Set the span status. Chainable. */
|
|
91
|
+
setStatus(code: SpanStatusCode, message?: string): this;
|
|
92
|
+
/**
|
|
93
|
+
* Finalize the span and return an immutable {@link ReadableSpan}.
|
|
94
|
+
*
|
|
95
|
+
* Sets `endTimeMs` on first call; subsequent calls return the same
|
|
96
|
+
* snapshot with defensive copies of mutable fields.
|
|
97
|
+
*/
|
|
98
|
+
end(): ReadableSpan;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* OTLP/HTTP JSON span exporter.
|
|
102
|
+
*
|
|
103
|
+
* Ships spans to an OpenTelemetry Collector (or compatible endpoint)
|
|
104
|
+
* using `fetch()` with the OTLP JSON encoding. Designed for edge
|
|
105
|
+
* runtimes - no gRPC, no protobuf, no Node.js dependencies.
|
|
106
|
+
*
|
|
107
|
+
* Export calls should be dispatched via `waitUntil()` so they do not
|
|
108
|
+
* block the response path.
|
|
109
|
+
*/
|
|
110
|
+
declare class OTLPSpanExporter implements SpanExporter {
|
|
111
|
+
private readonly endpoint;
|
|
112
|
+
private readonly headers;
|
|
113
|
+
private readonly timeoutMs;
|
|
114
|
+
private readonly serviceName;
|
|
115
|
+
private readonly serviceVersion?;
|
|
116
|
+
constructor(config: {
|
|
117
|
+
endpoint: string;
|
|
118
|
+
headers?: Record<string, string>;
|
|
119
|
+
timeoutMs?: number;
|
|
120
|
+
serviceName?: string;
|
|
121
|
+
serviceVersion?: string;
|
|
122
|
+
});
|
|
123
|
+
export(spans: ReadableSpan[]): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Console span exporter for development and debugging.
|
|
127
|
+
*
|
|
128
|
+
* Logs each span to `console.debug()` with a compact one-line format
|
|
129
|
+
* showing name, kind, duration, trace/span IDs, and status.
|
|
130
|
+
*/
|
|
131
|
+
declare class ConsoleSpanExporter implements SpanExporter {
|
|
132
|
+
export(spans: ReadableSpan[]): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Determine whether a request should be sampled based on the configured rate.
|
|
136
|
+
*
|
|
137
|
+
* @param sampleRate - Sampling probability in [0.0, 1.0].
|
|
138
|
+
* @returns `true` if the request should be traced.
|
|
139
|
+
*/
|
|
140
|
+
declare function shouldSample(sampleRate: number): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Generate a 16-character lowercase hex span ID (8 random bytes).
|
|
143
|
+
*
|
|
144
|
+
* Uses `crypto.getRandomValues()` which is available in all edge runtimes
|
|
145
|
+
* (Cloudflare Workers, Deno, Bun, Node 19+).
|
|
146
|
+
*/
|
|
147
|
+
declare function generateOtelSpanId(): string;
|
|
148
|
+
|
|
149
|
+
export { ConsoleSpanExporter, OTLPSpanExporter, type ReadableSpan, SemConv, SpanBuilder, type SpanEvent, type SpanExporter, type SpanKind, type SpanStatusCode, type TracingConfig, generateOtelSpanId, shouldSample };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const SemConv = {
|
|
2
|
+
HTTP_METHOD: "http.request.method",
|
|
3
|
+
HTTP_ROUTE: "http.route",
|
|
4
|
+
HTTP_STATUS_CODE: "http.response.status_code",
|
|
5
|
+
URL_PATH: "url.path",
|
|
6
|
+
SERVER_ADDRESS: "server.address"
|
|
7
|
+
};
|
|
8
|
+
class SpanBuilder {
|
|
9
|
+
constructor(name, kind, traceId, spanId, parentSpanId, startTimeMs = Date.now()) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.kind = kind;
|
|
12
|
+
this.traceId = traceId;
|
|
13
|
+
this.spanId = spanId;
|
|
14
|
+
this.parentSpanId = parentSpanId;
|
|
15
|
+
this.startTimeMs = startTimeMs;
|
|
16
|
+
}
|
|
17
|
+
_attributes = {};
|
|
18
|
+
_events = [];
|
|
19
|
+
_status = {
|
|
20
|
+
code: "UNSET"
|
|
21
|
+
};
|
|
22
|
+
_endTimeMs;
|
|
23
|
+
/** Set a single attribute. Chainable. */
|
|
24
|
+
setAttribute(key, value) {
|
|
25
|
+
this._attributes[key] = value;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/** Record a timestamped event with optional attributes. Chainable. */
|
|
29
|
+
addEvent(name, attributes) {
|
|
30
|
+
this._events.push({ name, timeMs: Date.now(), attributes });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/** Set the span status. Chainable. */
|
|
34
|
+
setStatus(code, message) {
|
|
35
|
+
this._status = { code, message };
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Finalize the span and return an immutable {@link ReadableSpan}.
|
|
40
|
+
*
|
|
41
|
+
* Sets `endTimeMs` on first call; subsequent calls return the same
|
|
42
|
+
* snapshot with defensive copies of mutable fields.
|
|
43
|
+
*/
|
|
44
|
+
end() {
|
|
45
|
+
this._endTimeMs = this._endTimeMs ?? Date.now();
|
|
46
|
+
return {
|
|
47
|
+
traceId: this.traceId,
|
|
48
|
+
spanId: this.spanId,
|
|
49
|
+
parentSpanId: this.parentSpanId,
|
|
50
|
+
name: this.name,
|
|
51
|
+
kind: this.kind,
|
|
52
|
+
startTimeMs: this.startTimeMs,
|
|
53
|
+
endTimeMs: this._endTimeMs,
|
|
54
|
+
attributes: { ...this._attributes },
|
|
55
|
+
status: { ...this._status },
|
|
56
|
+
events: [...this._events]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const SPAN_KIND_MAP = {
|
|
61
|
+
INTERNAL: 1,
|
|
62
|
+
// SPAN_KIND_INTERNAL
|
|
63
|
+
SERVER: 2,
|
|
64
|
+
// SPAN_KIND_SERVER
|
|
65
|
+
CLIENT: 3
|
|
66
|
+
// SPAN_KIND_CLIENT
|
|
67
|
+
};
|
|
68
|
+
const STATUS_CODE_MAP = {
|
|
69
|
+
UNSET: 0,
|
|
70
|
+
// STATUS_CODE_UNSET
|
|
71
|
+
OK: 1,
|
|
72
|
+
// STATUS_CODE_OK
|
|
73
|
+
ERROR: 2
|
|
74
|
+
// STATUS_CODE_ERROR
|
|
75
|
+
};
|
|
76
|
+
function toOtlpAttributeValue(value) {
|
|
77
|
+
if (typeof value === "string") return { stringValue: value };
|
|
78
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
79
|
+
if (Number.isInteger(value)) return { intValue: value };
|
|
80
|
+
return { doubleValue: value };
|
|
81
|
+
}
|
|
82
|
+
function toOtlpAttributes(attrs) {
|
|
83
|
+
return Object.entries(attrs).map(([key, value]) => ({
|
|
84
|
+
key,
|
|
85
|
+
value: toOtlpAttributeValue(value)
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
function msToNanos(ms) {
|
|
89
|
+
return String(ms * 1e6);
|
|
90
|
+
}
|
|
91
|
+
function toOtlpPayload(spans, serviceName, serviceVersion) {
|
|
92
|
+
const resourceAttributes = [{ key: "service.name", value: { stringValue: serviceName } }];
|
|
93
|
+
if (serviceVersion) {
|
|
94
|
+
resourceAttributes.push({
|
|
95
|
+
key: "service.version",
|
|
96
|
+
value: { stringValue: serviceVersion }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const otlpSpans = spans.map((span) => {
|
|
100
|
+
const otlpSpan = {
|
|
101
|
+
traceId: span.traceId,
|
|
102
|
+
spanId: span.spanId,
|
|
103
|
+
name: span.name,
|
|
104
|
+
kind: SPAN_KIND_MAP[span.kind],
|
|
105
|
+
startTimeUnixNano: msToNanos(span.startTimeMs),
|
|
106
|
+
endTimeUnixNano: msToNanos(span.endTimeMs),
|
|
107
|
+
attributes: toOtlpAttributes(span.attributes),
|
|
108
|
+
status: {
|
|
109
|
+
code: STATUS_CODE_MAP[span.status.code],
|
|
110
|
+
...span.status.message ? { message: span.status.message } : {}
|
|
111
|
+
},
|
|
112
|
+
events: span.events.map((event) => ({
|
|
113
|
+
name: event.name,
|
|
114
|
+
timeUnixNano: msToNanos(event.timeMs),
|
|
115
|
+
...event.attributes ? { attributes: toOtlpAttributes(event.attributes) } : {}
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
if (span.parentSpanId) {
|
|
119
|
+
otlpSpan.parentSpanId = span.parentSpanId;
|
|
120
|
+
}
|
|
121
|
+
return otlpSpan;
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
resourceSpans: [
|
|
125
|
+
{
|
|
126
|
+
resource: { attributes: resourceAttributes },
|
|
127
|
+
scopeSpans: [
|
|
128
|
+
{
|
|
129
|
+
scope: { name: "stoma-gateway" },
|
|
130
|
+
spans: otlpSpans
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
class OTLPSpanExporter {
|
|
138
|
+
endpoint;
|
|
139
|
+
headers;
|
|
140
|
+
timeoutMs;
|
|
141
|
+
serviceName;
|
|
142
|
+
serviceVersion;
|
|
143
|
+
constructor(config) {
|
|
144
|
+
this.endpoint = config.endpoint;
|
|
145
|
+
this.headers = config.headers ?? {};
|
|
146
|
+
this.timeoutMs = config.timeoutMs ?? 1e4;
|
|
147
|
+
this.serviceName = config.serviceName ?? "stoma-gateway";
|
|
148
|
+
this.serviceVersion = config.serviceVersion;
|
|
149
|
+
}
|
|
150
|
+
async export(spans) {
|
|
151
|
+
if (spans.length === 0) return;
|
|
152
|
+
const payload = toOtlpPayload(spans, this.serviceName, this.serviceVersion);
|
|
153
|
+
await fetch(this.endpoint, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
"content-type": "application/json",
|
|
157
|
+
...this.headers
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(payload),
|
|
160
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
class ConsoleSpanExporter {
|
|
165
|
+
async export(spans) {
|
|
166
|
+
for (const span of spans) {
|
|
167
|
+
console.debug(
|
|
168
|
+
`[trace] ${span.name} ${span.kind} ${span.endTimeMs - span.startTimeMs}ms trace=${span.traceId} span=${span.spanId}` + (span.parentSpanId ? ` parent=${span.parentSpanId}` : "") + ` status=${span.status.code}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function shouldSample(sampleRate) {
|
|
174
|
+
if (sampleRate >= 1) return true;
|
|
175
|
+
if (sampleRate <= 0) return false;
|
|
176
|
+
return Math.random() < sampleRate;
|
|
177
|
+
}
|
|
178
|
+
function generateOtelSpanId() {
|
|
179
|
+
const bytes = new Uint8Array(8);
|
|
180
|
+
crypto.getRandomValues(bytes);
|
|
181
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
ConsoleSpanExporter,
|
|
185
|
+
OTLPSpanExporter,
|
|
186
|
+
SemConv,
|
|
187
|
+
SpanBuilder,
|
|
188
|
+
generateOtelSpanId,
|
|
189
|
+
shouldSample
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=tracing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/observability/tracing.ts"],"sourcesContent":["/**\n * Lightweight OpenTelemetry-compatible tracing for edge runtimes.\n *\n * Provides span creation, OTLP/HTTP JSON export via `fetch()`, and\n * head-based sampling - all without any runtime dependencies beyond\n * the Web Platform APIs available in Cloudflare Workers.\n *\n * Follows the OTel data model but uses own lightweight types to avoid\n * pulling in `@opentelemetry/api` (which is not edge-compatible).\n *\n * @module tracing\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type SpanKind = \"SERVER\" | \"CLIENT\" | \"INTERNAL\";\nexport type SpanStatusCode = \"UNSET\" | \"OK\" | \"ERROR\";\n\n/** An immutable representation of a completed span. */\nexport interface ReadableSpan {\n traceId: string;\n spanId: string;\n parentSpanId?: string;\n name: string;\n kind: SpanKind;\n startTimeMs: number;\n endTimeMs: number;\n attributes: Record<string, string | number | boolean>;\n status: { code: SpanStatusCode; message?: string };\n events: SpanEvent[];\n}\n\n/** A timestamped event recorded during a span's lifetime. */\nexport interface SpanEvent {\n name: string;\n timeMs: number;\n attributes?: Record<string, string | number | boolean>;\n}\n\n/**\n * Pluggable span exporter interface.\n *\n * Implementations ship completed spans to a backend (OTLP collector,\n * console, or any custom destination). Export is expected to be called\n * via `waitUntil()` so it does not block the response.\n */\nexport interface SpanExporter {\n export(spans: ReadableSpan[]): Promise<void>;\n shutdown?(): Promise<void>;\n}\n\n/** Configuration for gateway-level tracing. */\nexport interface TracingConfig {\n exporter: SpanExporter;\n serviceName?: string;\n serviceVersion?: string;\n /** Head-based sampling rate [0.0, 1.0]. Default: 1.0 */\n sampleRate?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Semantic Conventions (HTTP subset)\n// ---------------------------------------------------------------------------\n\n/**\n * OTel semantic convention attribute keys (HTTP subset).\n *\n * Uses the stable HTTP semconv names from the OpenTelemetry specification.\n * @see https://opentelemetry.io/docs/specs/semconv/http/\n */\nexport const SemConv = {\n HTTP_METHOD: \"http.request.method\",\n HTTP_ROUTE: \"http.route\",\n HTTP_STATUS_CODE: \"http.response.status_code\",\n URL_PATH: \"url.path\",\n SERVER_ADDRESS: \"server.address\",\n} as const;\n\n// ---------------------------------------------------------------------------\n// SpanBuilder\n// ---------------------------------------------------------------------------\n\n/**\n * Mutable span builder - accumulates attributes, events, and status\n * during a request lifecycle. Call {@link end} to produce an immutable\n * {@link ReadableSpan}.\n */\nexport class SpanBuilder {\n private _attributes: Record<string, string | number | boolean> = {};\n private _events: SpanEvent[] = [];\n private _status: { code: SpanStatusCode; message?: string } = {\n code: \"UNSET\",\n };\n private _endTimeMs?: number;\n\n constructor(\n public readonly name: string,\n public readonly kind: SpanKind,\n public readonly traceId: string,\n public readonly spanId: string,\n public readonly parentSpanId?: string,\n public readonly startTimeMs: number = Date.now()\n ) {}\n\n /** Set a single attribute. Chainable. */\n setAttribute(key: string, value: string | number | boolean): this {\n this._attributes[key] = value;\n return this;\n }\n\n /** Record a timestamped event with optional attributes. Chainable. */\n addEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n ): this {\n this._events.push({ name, timeMs: Date.now(), attributes });\n return this;\n }\n\n /** Set the span status. Chainable. */\n setStatus(code: SpanStatusCode, message?: string): this {\n this._status = { code, message };\n return this;\n }\n\n /**\n * Finalize the span and return an immutable {@link ReadableSpan}.\n *\n * Sets `endTimeMs` on first call; subsequent calls return the same\n * snapshot with defensive copies of mutable fields.\n */\n end(): ReadableSpan {\n this._endTimeMs = this._endTimeMs ?? Date.now();\n return {\n traceId: this.traceId,\n spanId: this.spanId,\n parentSpanId: this.parentSpanId,\n name: this.name,\n kind: this.kind,\n startTimeMs: this.startTimeMs,\n endTimeMs: this._endTimeMs,\n attributes: { ...this._attributes },\n status: { ...this._status },\n events: [...this._events],\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// OTLP JSON helpers\n// ---------------------------------------------------------------------------\n\n/** Map SpanKind to OTLP numeric kind. */\nconst SPAN_KIND_MAP: Record<SpanKind, number> = {\n INTERNAL: 1, // SPAN_KIND_INTERNAL\n SERVER: 2, // SPAN_KIND_SERVER\n CLIENT: 3, // SPAN_KIND_CLIENT\n};\n\n/** Map SpanStatusCode to OTLP numeric status code. */\nconst STATUS_CODE_MAP: Record<SpanStatusCode, number> = {\n UNSET: 0, // STATUS_CODE_UNSET\n OK: 1, // STATUS_CODE_OK\n ERROR: 2, // STATUS_CODE_ERROR\n};\n\n/**\n * Convert an attribute value to the OTLP `AnyValue` wire format.\n *\n * @returns Object with exactly one of `stringValue`, `intValue`, or `boolValue`.\n */\nfunction toOtlpAttributeValue(\n value: string | number | boolean\n): Record<string, string | number | boolean> {\n if (typeof value === \"string\") return { stringValue: value };\n if (typeof value === \"boolean\") return { boolValue: value };\n // OTLP distinguishes int vs double; we use intValue for integers.\n if (Number.isInteger(value)) return { intValue: value };\n return { doubleValue: value };\n}\n\n/** Convert a flat attributes map to the OTLP `KeyValue[]` format. */\nfunction toOtlpAttributes(\n attrs: Record<string, string | number | boolean>\n): Array<{ key: string; value: Record<string, string | number | boolean> }> {\n return Object.entries(attrs).map(([key, value]) => ({\n key,\n value: toOtlpAttributeValue(value),\n }));\n}\n\n/** Milliseconds to nanoseconds, returned as a string (OTLP convention). */\nfunction msToNanos(ms: number): string {\n // Avoid BigInt for maximum runtime compatibility; ms precision is\n // sufficient and fits safely in a JS number when multiplied by 1e6.\n return String(ms * 1_000_000);\n}\n\n/**\n * Convert an array of {@link ReadableSpan} to the OTLP JSON wire format.\n *\n * Produces the `ExportTraceServiceRequest` structure:\n * `{ resourceSpans: [{ resource, scopeSpans: [{ scope, spans }] }] }`\n *\n * @see https://opentelemetry.io/docs/specs/otlp/#otlphttp-request\n */\nfunction toOtlpPayload(\n spans: ReadableSpan[],\n serviceName: string,\n serviceVersion?: string\n): object {\n const resourceAttributes: Array<{\n key: string;\n value: Record<string, string | number | boolean>;\n }> = [{ key: \"service.name\", value: { stringValue: serviceName } }];\n\n if (serviceVersion) {\n resourceAttributes.push({\n key: \"service.version\",\n value: { stringValue: serviceVersion },\n });\n }\n\n const otlpSpans = spans.map((span) => {\n const otlpSpan: Record<string, unknown> = {\n traceId: span.traceId,\n spanId: span.spanId,\n name: span.name,\n kind: SPAN_KIND_MAP[span.kind],\n startTimeUnixNano: msToNanos(span.startTimeMs),\n endTimeUnixNano: msToNanos(span.endTimeMs),\n attributes: toOtlpAttributes(span.attributes),\n status: {\n code: STATUS_CODE_MAP[span.status.code],\n ...(span.status.message ? { message: span.status.message } : {}),\n },\n events: span.events.map((event) => ({\n name: event.name,\n timeUnixNano: msToNanos(event.timeMs),\n ...(event.attributes\n ? { attributes: toOtlpAttributes(event.attributes) }\n : {}),\n })),\n };\n\n if (span.parentSpanId) {\n otlpSpan.parentSpanId = span.parentSpanId;\n }\n\n return otlpSpan;\n });\n\n return {\n resourceSpans: [\n {\n resource: { attributes: resourceAttributes },\n scopeSpans: [\n {\n scope: { name: \"stoma-gateway\" },\n spans: otlpSpans,\n },\n ],\n },\n ],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Built-in Exporters\n// ---------------------------------------------------------------------------\n\n/**\n * OTLP/HTTP JSON span exporter.\n *\n * Ships spans to an OpenTelemetry Collector (or compatible endpoint)\n * using `fetch()` with the OTLP JSON encoding. Designed for edge\n * runtimes - no gRPC, no protobuf, no Node.js dependencies.\n *\n * Export calls should be dispatched via `waitUntil()` so they do not\n * block the response path.\n */\nexport class OTLPSpanExporter implements SpanExporter {\n private readonly endpoint: string;\n private readonly headers: Record<string, string>;\n private readonly timeoutMs: number;\n private readonly serviceName: string;\n private readonly serviceVersion?: string;\n\n constructor(config: {\n endpoint: string;\n headers?: Record<string, string>;\n timeoutMs?: number;\n serviceName?: string;\n serviceVersion?: string;\n }) {\n this.endpoint = config.endpoint;\n this.headers = config.headers ?? {};\n this.timeoutMs = config.timeoutMs ?? 10_000;\n this.serviceName = config.serviceName ?? \"stoma-gateway\";\n this.serviceVersion = config.serviceVersion;\n }\n\n async export(spans: ReadableSpan[]): Promise<void> {\n if (spans.length === 0) return;\n\n const payload = toOtlpPayload(spans, this.serviceName, this.serviceVersion);\n\n await fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...this.headers,\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n }\n}\n\n/**\n * Console span exporter for development and debugging.\n *\n * Logs each span to `console.debug()` with a compact one-line format\n * showing name, kind, duration, trace/span IDs, and status.\n */\nexport class ConsoleSpanExporter implements SpanExporter {\n async export(spans: ReadableSpan[]): Promise<void> {\n for (const span of spans) {\n console.debug(\n `[trace] ${span.name} ${span.kind} ${span.endTimeMs - span.startTimeMs}ms` +\n ` trace=${span.traceId} span=${span.spanId}` +\n (span.parentSpanId ? ` parent=${span.parentSpanId}` : \"\") +\n ` status=${span.status.code}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Sampling\n// ---------------------------------------------------------------------------\n\n/**\n * Determine whether a request should be sampled based on the configured rate.\n *\n * @param sampleRate - Sampling probability in [0.0, 1.0].\n * @returns `true` if the request should be traced.\n */\nexport function shouldSample(sampleRate: number): boolean {\n if (sampleRate >= 1.0) return true;\n if (sampleRate <= 0.0) return false;\n return Math.random() < sampleRate;\n}\n\n// ---------------------------------------------------------------------------\n// ID Generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a 16-character lowercase hex span ID (8 random bytes).\n *\n * Uses `crypto.getRandomValues()` which is available in all edge runtimes\n * (Cloudflare Workers, Deno, Bun, Node 19+).\n */\nexport function generateOtelSpanId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface ApiKeyAuthConfig extends PolicyConfig {
|
|
7
|
+
/** Header name to read the API key from. Default: "X-API-Key" */
|
|
8
|
+
headerName?: string;
|
|
9
|
+
/** Query parameter name as fallback. Default: undefined (disabled) */
|
|
10
|
+
queryParam?: string;
|
|
11
|
+
/** Validator function - return true if the key is valid */
|
|
12
|
+
validate: (key: string) => boolean | Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* After successful validation, derive an identity string from the key
|
|
15
|
+
* and set it as a request header for upstream consumption.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* apiKeyAuth({
|
|
20
|
+
* validate: (key) => keys.has(key),
|
|
21
|
+
* forwardKeyIdentity: {
|
|
22
|
+
* headerName: "x-api-client",
|
|
23
|
+
* identityFn: (key) => keyToClientMap.get(key) ?? "unknown",
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
forwardKeyIdentity?: {
|
|
29
|
+
/** Header name to set on the request. */
|
|
30
|
+
headerName: string;
|
|
31
|
+
/** Derive an identity string from the validated key. Can be async. */
|
|
32
|
+
identityFn: (key: string) => string | Promise<string>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate API keys from headers or query parameters.
|
|
37
|
+
*
|
|
38
|
+
* Checks the `X-API-Key` header by default, with an optional query parameter
|
|
39
|
+
* fallback. The `validate` function can be async to support remote key lookups.
|
|
40
|
+
*
|
|
41
|
+
* @param config - API key settings with a required `validate` function.
|
|
42
|
+
* @returns A {@link Policy} at priority 10.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // Static key validation
|
|
47
|
+
* apiKeyAuth({
|
|
48
|
+
* validate: (key) => key === env.API_KEY,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Async validation with query parameter fallback
|
|
52
|
+
* apiKeyAuth({
|
|
53
|
+
* headerName: "Authorization",
|
|
54
|
+
* queryParam: "api_key",
|
|
55
|
+
* validate: async (key) => {
|
|
56
|
+
* const result = await kv.get(`api-key:${key}`);
|
|
57
|
+
* return result !== null;
|
|
58
|
+
* },
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare const apiKeyAuth: (config: ApiKeyAuthConfig) => Policy;
|
|
63
|
+
|
|
64
|
+
export { type ApiKeyAuthConfig, apiKeyAuth };
|