@shaferllc/keel 0.68.0 → 0.74.0

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.
Files changed (140) hide show
  1. package/AGENTS.md +2 -0
  2. package/README.md +14 -5
  3. package/dist/api/api.config.stub +9 -0
  4. package/dist/api/config.d.ts +13 -0
  5. package/dist/api/config.js +14 -0
  6. package/dist/api/index.d.ts +16 -0
  7. package/dist/api/index.js +13 -0
  8. package/dist/api/provider.d.ts +10 -0
  9. package/dist/api/provider.js +17 -0
  10. package/dist/api/query.d.ts +35 -0
  11. package/dist/api/query.js +42 -0
  12. package/dist/api/resource.d.ts +91 -0
  13. package/dist/api/resource.js +188 -0
  14. package/dist/core/application.js +6 -0
  15. package/dist/core/cache.d.ts +1 -2
  16. package/dist/core/cache.js +9 -2
  17. package/dist/core/cli/stubs.d.ts +14 -0
  18. package/dist/core/cli/stubs.js +105 -0
  19. package/dist/core/console-prompt.d.ts +79 -0
  20. package/dist/core/console-prompt.js +239 -0
  21. package/dist/core/console-ui.d.ts +96 -0
  22. package/dist/core/console-ui.js +187 -0
  23. package/dist/core/console.d.ts +188 -0
  24. package/dist/core/console.js +395 -0
  25. package/dist/core/database.d.ts +70 -1
  26. package/dist/core/database.js +174 -15
  27. package/dist/core/env.d.ts +96 -0
  28. package/dist/core/env.js +140 -0
  29. package/dist/core/health.d.ts +2 -2
  30. package/dist/core/health.js +2 -2
  31. package/dist/core/http/kernel.d.ts +2 -0
  32. package/dist/core/http/kernel.js +48 -0
  33. package/dist/core/http/router.d.ts +5 -5
  34. package/dist/core/http/router.js +5 -5
  35. package/dist/core/i18n.d.ts +162 -0
  36. package/dist/core/i18n.js +472 -0
  37. package/dist/core/index.d.ts +25 -4
  38. package/dist/core/index.js +13 -3
  39. package/dist/core/instrumentation.d.ts +113 -0
  40. package/dist/core/instrumentation.js +52 -0
  41. package/dist/core/logger.d.ts +7 -0
  42. package/dist/core/logger.js +28 -1
  43. package/dist/core/notification.js +10 -1
  44. package/dist/core/package.d.ts +120 -0
  45. package/dist/core/package.js +169 -0
  46. package/dist/core/pages.d.ts +108 -0
  47. package/dist/core/pages.js +199 -0
  48. package/dist/core/queue.js +26 -5
  49. package/dist/core/repl.d.ts +33 -0
  50. package/dist/core/repl.js +88 -0
  51. package/dist/core/scheduler.js +6 -0
  52. package/dist/core/social.d.ts +4 -4
  53. package/dist/core/social.js +4 -4
  54. package/dist/core/storage.js +15 -3
  55. package/dist/core/telemetry.d.ts +208 -0
  56. package/dist/core/telemetry.js +383 -0
  57. package/dist/core/template.d.ts +2 -3
  58. package/dist/core/template.js +2 -3
  59. package/dist/core/testing.d.ts +170 -1
  60. package/dist/core/testing.js +504 -2
  61. package/dist/db/d1.js +13 -0
  62. package/dist/db/pg.d.ts +13 -0
  63. package/dist/db/pg.js +46 -4
  64. package/dist/openapi/config.d.ts +28 -0
  65. package/dist/openapi/config.js +25 -0
  66. package/dist/openapi/doc.d.ts +40 -0
  67. package/dist/openapi/doc.js +20 -0
  68. package/dist/openapi/export.d.ts +8 -0
  69. package/dist/openapi/export.js +19 -0
  70. package/dist/openapi/gate.d.ts +15 -0
  71. package/dist/openapi/gate.js +27 -0
  72. package/dist/openapi/index.d.ts +19 -0
  73. package/dist/openapi/index.js +15 -0
  74. package/dist/openapi/openapi.config.stub +29 -0
  75. package/dist/openapi/provider.d.ts +18 -0
  76. package/dist/openapi/provider.js +35 -0
  77. package/dist/openapi/routes.d.ts +9 -0
  78. package/dist/openapi/routes.js +23 -0
  79. package/dist/openapi/spec.d.ts +23 -0
  80. package/dist/openapi/spec.js +132 -0
  81. package/dist/openapi/ui.d.ts +8 -0
  82. package/dist/openapi/ui.js +31 -0
  83. package/dist/openapi/zod.d.ts +12 -0
  84. package/dist/openapi/zod.js +46 -0
  85. package/dist/watch/config.d.ts +33 -0
  86. package/dist/watch/config.js +38 -0
  87. package/dist/watch/entry.d.ts +53 -0
  88. package/dist/watch/entry.js +105 -0
  89. package/dist/watch/gate.d.ts +20 -0
  90. package/dist/watch/gate.js +32 -0
  91. package/dist/watch/index.d.ts +21 -0
  92. package/dist/watch/index.js +17 -0
  93. package/dist/watch/migration.d.ts +7 -0
  94. package/dist/watch/migration.js +20 -0
  95. package/dist/watch/provider.d.ts +22 -0
  96. package/dist/watch/provider.js +58 -0
  97. package/dist/watch/prune.d.ts +11 -0
  98. package/dist/watch/prune.js +20 -0
  99. package/dist/watch/recorder.d.ts +24 -0
  100. package/dist/watch/recorder.js +39 -0
  101. package/dist/watch/routes.d.ts +13 -0
  102. package/dist/watch/routes.js +55 -0
  103. package/dist/watch/store.d.ts +54 -0
  104. package/dist/watch/store.js +158 -0
  105. package/dist/watch/ui/dist/watch.css +1 -0
  106. package/dist/watch/ui/dist/watch.js +555 -0
  107. package/dist/watch/ui-shell.d.ts +12 -0
  108. package/dist/watch/ui-shell.js +24 -0
  109. package/dist/watch/watch.config.stub +47 -0
  110. package/dist/watch/watchers.d.ts +12 -0
  111. package/dist/watch/watchers.js +156 -0
  112. package/docs/ai-manifest.json +737 -3
  113. package/docs/api-resources.md +118 -0
  114. package/docs/configuration.md +74 -0
  115. package/docs/console.md +193 -13
  116. package/docs/database.md +101 -0
  117. package/docs/examples/configuration.ts +40 -0
  118. package/docs/examples/console.ts +134 -0
  119. package/docs/examples/database.ts +90 -0
  120. package/docs/examples/i18n.ts +117 -0
  121. package/docs/examples/logger.ts +74 -0
  122. package/docs/examples/mail.ts +103 -0
  123. package/docs/examples/pages.ts +82 -0
  124. package/docs/examples/telemetry.ts +127 -0
  125. package/docs/examples/testing.ts +149 -0
  126. package/docs/health.md +4 -4
  127. package/docs/i18n.md +302 -0
  128. package/docs/logger.md +156 -10
  129. package/docs/mail.md +73 -0
  130. package/docs/openapi.md +111 -0
  131. package/docs/packages.md +118 -0
  132. package/docs/pages.md +217 -0
  133. package/docs/storage.md +5 -3
  134. package/docs/telemetry.md +263 -0
  135. package/docs/templates.md +3 -4
  136. package/docs/testing.md +251 -0
  137. package/docs/watch.md +118 -0
  138. package/llms-full.txt +2370 -248
  139. package/llms.txt +12 -1
  140. package/package.json +18 -2
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Tracing — spans, W3C trace context, and an OTLP exporter, with **no SDK**.
3
+ *
4
+ * setTelemetry(new Tracer({ serviceName: "api", exporter: otlpExporter({ url }) }));
5
+ * this.use(tracing()); // a server span per request
6
+ *
7
+ * await trace("charge", async (span) => { // your own spans, nested
8
+ * span.setAttributes({ "order.id": id });
9
+ * await stripe.charge(id);
10
+ * });
11
+ *
12
+ * The OpenTelemetry Node SDK is a large tree of packages that assumes a Node
13
+ * process. What a trace actually *is*, though, is small: an id, a parent, a
14
+ * start and end, some attributes — and a documented JSON shape to POST them in.
15
+ * That's what this is. It speaks OTLP/HTTP over `fetch`, so it runs on Workers
16
+ * as happily as on Node, and any OTLP collector (Jaeger, Tempo, Honeycomb,
17
+ * Grafana, Datadog) accepts it.
18
+ *
19
+ * Spans nest through `AsyncLocalStorage`, so `currentSpan()` works inside an
20
+ * `await` chain without threading a span through every call.
21
+ */
22
+ import type { MiddlewareHandler } from "hono";
23
+ /** The three things that identify a span across a process boundary. */
24
+ export interface SpanContext {
25
+ traceId: string;
26
+ spanId: string;
27
+ /** Whether this trace is being recorded. Propagates to children. */
28
+ sampled: boolean;
29
+ }
30
+ /**
31
+ * Parse a W3C `traceparent` header. Returns null if it's absent or malformed —
32
+ * a bad header must start a fresh trace, never throw a request away.
33
+ */
34
+ export declare function parseTraceparent(header: string | null | undefined): SpanContext | null;
35
+ /** Format a span context as a W3C `traceparent` header. */
36
+ export declare function traceparent(context: SpanContext): string;
37
+ /**
38
+ * Add the current trace context to outgoing request headers, so the service you
39
+ * call joins **this** trace instead of starting its own.
40
+ *
41
+ * await fetch(url, { headers: injectTraceContext({ accept: "application/json" }) });
42
+ */
43
+ export declare function injectTraceContext(headers?: Record<string, string>): Record<string, string>;
44
+ export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer";
45
+ export type SpanStatus = "unset" | "ok" | "error";
46
+ export type SpanAttributeValue = string | number | boolean;
47
+ export type SpanAttributes = Record<string, SpanAttributeValue>;
48
+ /** A timestamped annotation inside a span. */
49
+ export interface SpanEvent {
50
+ name: string;
51
+ /** Epoch milliseconds. */
52
+ time: number;
53
+ attributes?: SpanAttributes;
54
+ }
55
+ /** A finished span, as handed to an exporter. */
56
+ export interface SpanData {
57
+ name: string;
58
+ kind: SpanKind;
59
+ traceId: string;
60
+ spanId: string;
61
+ parentSpanId?: string;
62
+ /** Epoch milliseconds. */
63
+ startTime: number;
64
+ endTime: number;
65
+ attributes: SpanAttributes;
66
+ events: SpanEvent[];
67
+ status: SpanStatus;
68
+ statusMessage?: string;
69
+ }
70
+ export interface SpanOptions {
71
+ kind?: SpanKind;
72
+ attributes?: SpanAttributes;
73
+ /** Force a parent instead of using the current span. */
74
+ parent?: SpanContext;
75
+ }
76
+ export declare class Span {
77
+ readonly name: string;
78
+ readonly kind: SpanKind;
79
+ private readonly tracer;
80
+ readonly context: SpanContext;
81
+ readonly parentSpanId?: string;
82
+ private readonly startTime;
83
+ private attributes;
84
+ private events;
85
+ private status;
86
+ private statusMessage?;
87
+ private ended;
88
+ constructor(name: string, kind: SpanKind, context: SpanContext, parentSpanId: string | undefined, tracer: Tracer, attributes?: SpanAttributes);
89
+ setAttribute(key: string, value: SpanAttributeValue): this;
90
+ setAttributes(attributes: SpanAttributes): this;
91
+ /** A timestamped annotation — "cache miss", "retrying" — inside this span. */
92
+ addEvent(name: string, attributes?: SpanAttributes): this;
93
+ setStatus(status: SpanStatus, message?: string): this;
94
+ /** Mark the span failed and record the error as an event. */
95
+ recordException(error: unknown): this;
96
+ /** Finish the span and hand it to the exporter. Ending twice is a no-op. */
97
+ end(): void;
98
+ }
99
+ /** Where finished spans go. */
100
+ export interface SpanExporter {
101
+ export(spans: SpanData[]): Promise<void> | void;
102
+ }
103
+ /** Collects spans in memory — for tests. Assert on `.spans`. */
104
+ export declare class MemoryExporter implements SpanExporter {
105
+ readonly spans: SpanData[];
106
+ export(spans: SpanData[]): void;
107
+ /** The spans with a given name. */
108
+ named(name: string): SpanData[];
109
+ /** Every span in one trace, in the order they finished. */
110
+ trace(traceId: string): SpanData[];
111
+ clear(): void;
112
+ }
113
+ /** Prints each span — for local development, when you have no collector. */
114
+ export declare function consoleExporter(): SpanExporter;
115
+ export interface OtlpOptions {
116
+ /** The collector's trace endpoint, e.g. `http://localhost:4318/v1/traces`. */
117
+ url: string;
118
+ /** Extra headers — an API key, usually. */
119
+ headers?: Record<string, string>;
120
+ /** Resource attributes describing this service. */
121
+ resource?: SpanAttributes;
122
+ }
123
+ /**
124
+ * POST spans to any OTLP/HTTP collector as JSON — Jaeger, Tempo, Honeycomb,
125
+ * Grafana, Datadog. Uses `fetch`, so it works on Node and the edge.
126
+ */
127
+ export declare function otlpExporter(options: OtlpOptions): SpanExporter;
128
+ export interface TracerOptions {
129
+ /** Shows up as `service.name` on every span. */
130
+ serviceName?: string;
131
+ /** Where finished spans go. Omit and spans are recorded but dropped. */
132
+ exporter?: SpanExporter;
133
+ /**
134
+ * The fraction of traces to record, 0–1. Default: 1 (everything). A sampling
135
+ * decision is made once, at the root, and **inherited** by every child — half a
136
+ * trace is worse than none.
137
+ */
138
+ sampleRatio?: number;
139
+ /** Turn tracing off entirely. */
140
+ enabled?: boolean;
141
+ /** SpanAttributes describing this service, sent with every batch. */
142
+ resource?: SpanAttributes;
143
+ /** Export once this many spans are buffered. Default: 100. */
144
+ batchSize?: number;
145
+ }
146
+ export declare class Tracer {
147
+ private options;
148
+ private buffer;
149
+ constructor(options?: TracerOptions);
150
+ get enabled(): boolean;
151
+ /** @internal — called by `Span.end()`. */
152
+ record(span: SpanData): void;
153
+ /**
154
+ * Send buffered spans to the exporter. Call it before a Worker isolate is torn
155
+ * down (or on shutdown) — otherwise the last few spans die with the process.
156
+ */
157
+ flush(): Promise<void>;
158
+ /** Whether a new root trace should be recorded. */
159
+ private sample;
160
+ /**
161
+ * Start a span. It becomes the current span for anything run inside
162
+ * `trace()`; a bare `startSpan()` does not, so you must `end()` it yourself.
163
+ */
164
+ startSpan(name: string, options?: SpanOptions): Span;
165
+ /**
166
+ * Run `fn` inside a span: it's the current span for the duration, it ends when
167
+ * `fn` settles, and a throw is recorded on it and rethrown.
168
+ */
169
+ trace<T>(name: string, fn: (span: Span) => Promise<T> | T, options?: SpanOptions): Promise<T>;
170
+ }
171
+ /** Register the active tracer. */
172
+ export declare function setTelemetry(next: Tracer): Tracer;
173
+ /** The active tracer. */
174
+ export declare function telemetry(): Tracer;
175
+ /** The span currently in scope, if any. */
176
+ export declare function currentSpan(): Span | undefined;
177
+ /** Run `fn` inside a span on the active tracer. */
178
+ export declare function trace<T>(name: string, fn: (span: Span) => Promise<T> | T, options?: SpanOptions): Promise<T>;
179
+ /** Add attributes to the current span. A no-op outside one. */
180
+ export declare function setAttributes(attributes: SpanAttributes): void;
181
+ /** Add an event to the current span. A no-op outside one. */
182
+ export declare function addEvent(name: string, attributes?: SpanAttributes): void;
183
+ /** Send every buffered span to the exporter. */
184
+ export declare function flushTelemetry(): Promise<void>;
185
+ /**
186
+ * The current trace and span ids, for putting on a log line — this is what lets
187
+ * you jump from a log to the trace it belongs to.
188
+ *
189
+ * logger().child(traceIds()).info("charging card");
190
+ */
191
+ export declare function traceIds(): {
192
+ trace_id?: string;
193
+ span_id?: string;
194
+ };
195
+ export interface TracingOptions {
196
+ /** Paths to skip — health checks and favicons are noise. Default: those. */
197
+ ignore?: (path: string) => boolean;
198
+ /** Name the span. Default: `"GET /users/:id"` (the route pattern when known). */
199
+ name?: (method: string, path: string) => string;
200
+ }
201
+ /**
202
+ * A server span for every request, joined to the caller's trace when they send a
203
+ * `traceparent`. Records method, path, and status; a 5xx or a thrown error marks
204
+ * the span failed.
205
+ *
206
+ * this.use(tracing());
207
+ */
208
+ export declare function tracing(options?: TracingOptions): MiddlewareHandler;
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Tracing — spans, W3C trace context, and an OTLP exporter, with **no SDK**.
3
+ *
4
+ * setTelemetry(new Tracer({ serviceName: "api", exporter: otlpExporter({ url }) }));
5
+ * this.use(tracing()); // a server span per request
6
+ *
7
+ * await trace("charge", async (span) => { // your own spans, nested
8
+ * span.setAttributes({ "order.id": id });
9
+ * await stripe.charge(id);
10
+ * });
11
+ *
12
+ * The OpenTelemetry Node SDK is a large tree of packages that assumes a Node
13
+ * process. What a trace actually *is*, though, is small: an id, a parent, a
14
+ * start and end, some attributes — and a documented JSON shape to POST them in.
15
+ * That's what this is. It speaks OTLP/HTTP over `fetch`, so it runs on Workers
16
+ * as happily as on Node, and any OTLP collector (Jaeger, Tempo, Honeycomb,
17
+ * Grafana, Datadog) accepts it.
18
+ *
19
+ * Spans nest through `AsyncLocalStorage`, so `currentSpan()` works inside an
20
+ * `await` chain without threading a span through every call.
21
+ */
22
+ import { AsyncLocalStorage } from "node:async_hooks";
23
+ /* --------------------------------- ids ------------------------------------ */
24
+ function hex(bytes) {
25
+ const out = new Uint8Array(bytes);
26
+ crypto.getRandomValues(out);
27
+ return [...out].map((b) => b.toString(16).padStart(2, "0")).join("");
28
+ }
29
+ /** 32 hex chars. */
30
+ const newTraceId = () => hex(16);
31
+ /** 16 hex chars. */
32
+ const newSpanId = () => hex(8);
33
+ const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
34
+ /**
35
+ * Parse a W3C `traceparent` header. Returns null if it's absent or malformed —
36
+ * a bad header must start a fresh trace, never throw a request away.
37
+ */
38
+ export function parseTraceparent(header) {
39
+ const match = TRACEPARENT.exec((header ?? "").trim());
40
+ if (!match)
41
+ return null;
42
+ return {
43
+ traceId: match[1],
44
+ spanId: match[2],
45
+ sampled: (parseInt(match[3], 16) & 1) === 1,
46
+ };
47
+ }
48
+ /** Format a span context as a W3C `traceparent` header. */
49
+ export function traceparent(context) {
50
+ return `00-${context.traceId}-${context.spanId}-${context.sampled ? "01" : "00"}`;
51
+ }
52
+ /**
53
+ * Add the current trace context to outgoing request headers, so the service you
54
+ * call joins **this** trace instead of starting its own.
55
+ *
56
+ * await fetch(url, { headers: injectTraceContext({ accept: "application/json" }) });
57
+ */
58
+ export function injectTraceContext(headers = {}) {
59
+ const merged = { ...headers };
60
+ const span = currentSpan();
61
+ if (span)
62
+ merged.traceparent = traceparent(span.context);
63
+ return merged;
64
+ }
65
+ export class Span {
66
+ name;
67
+ kind;
68
+ tracer;
69
+ context;
70
+ parentSpanId;
71
+ startTime = Date.now();
72
+ attributes = {};
73
+ events = [];
74
+ status = "unset";
75
+ statusMessage;
76
+ ended = false;
77
+ constructor(name, kind, context, parentSpanId, tracer, attributes = {}) {
78
+ this.name = name;
79
+ this.kind = kind;
80
+ this.tracer = tracer;
81
+ this.context = context;
82
+ this.parentSpanId = parentSpanId;
83
+ this.attributes = { ...attributes };
84
+ }
85
+ setAttribute(key, value) {
86
+ this.attributes[key] = value;
87
+ return this;
88
+ }
89
+ setAttributes(attributes) {
90
+ Object.assign(this.attributes, attributes);
91
+ return this;
92
+ }
93
+ /** A timestamped annotation — "cache miss", "retrying" — inside this span. */
94
+ addEvent(name, attributes) {
95
+ this.events.push({ name, time: Date.now(), ...(attributes ? { attributes } : {}) });
96
+ return this;
97
+ }
98
+ setStatus(status, message) {
99
+ this.status = status;
100
+ this.statusMessage = message;
101
+ return this;
102
+ }
103
+ /** Mark the span failed and record the error as an event. */
104
+ recordException(error) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ this.addEvent("exception", {
107
+ "exception.message": message,
108
+ "exception.type": error instanceof Error ? error.name : typeof error,
109
+ });
110
+ return this.setStatus("error", message);
111
+ }
112
+ /** Finish the span and hand it to the exporter. Ending twice is a no-op. */
113
+ end() {
114
+ if (this.ended)
115
+ return;
116
+ this.ended = true;
117
+ // An unsampled span is measured but never exported — that's what sampling is.
118
+ if (!this.context.sampled)
119
+ return;
120
+ this.tracer.record({
121
+ name: this.name,
122
+ kind: this.kind,
123
+ traceId: this.context.traceId,
124
+ spanId: this.context.spanId,
125
+ ...(this.parentSpanId ? { parentSpanId: this.parentSpanId } : {}),
126
+ startTime: this.startTime,
127
+ endTime: Date.now(),
128
+ attributes: this.attributes,
129
+ events: this.events,
130
+ status: this.status,
131
+ ...(this.statusMessage ? { statusMessage: this.statusMessage } : {}),
132
+ });
133
+ }
134
+ }
135
+ /** Collects spans in memory — for tests. Assert on `.spans`. */
136
+ export class MemoryExporter {
137
+ spans = [];
138
+ export(spans) {
139
+ this.spans.push(...spans);
140
+ }
141
+ /** The spans with a given name. */
142
+ named(name) {
143
+ return this.spans.filter((s) => s.name === name);
144
+ }
145
+ /** Every span in one trace, in the order they finished. */
146
+ trace(traceId) {
147
+ return this.spans.filter((s) => s.traceId === traceId);
148
+ }
149
+ clear() {
150
+ this.spans.length = 0;
151
+ }
152
+ }
153
+ /** Prints each span — for local development, when you have no collector. */
154
+ export function consoleExporter() {
155
+ return {
156
+ export(spans) {
157
+ for (const span of spans) {
158
+ const ms = span.endTime - span.startTime;
159
+ console.log(`[trace] ${span.name} ${ms}ms ${span.status}`, { traceId: span.traceId, spanId: span.spanId, ...span.attributes });
160
+ }
161
+ },
162
+ };
163
+ }
164
+ const KIND_CODE = {
165
+ internal: 1,
166
+ server: 2,
167
+ client: 3,
168
+ producer: 4,
169
+ consumer: 5,
170
+ };
171
+ const STATUS_CODE = { unset: 0, ok: 1, error: 2 };
172
+ /** OTLP attributes are a list of typed key/value pairs, not a plain object. */
173
+ function otlpAttributes(attributes) {
174
+ return Object.entries(attributes).map(([key, value]) => ({
175
+ key,
176
+ value: typeof value === "number"
177
+ ? Number.isInteger(value)
178
+ ? { intValue: value }
179
+ : { doubleValue: value }
180
+ : typeof value === "boolean"
181
+ ? { boolValue: value }
182
+ : { stringValue: String(value) },
183
+ }));
184
+ }
185
+ const nanos = (ms) => `${Math.round(ms * 1e6)}`;
186
+ /**
187
+ * POST spans to any OTLP/HTTP collector as JSON — Jaeger, Tempo, Honeycomb,
188
+ * Grafana, Datadog. Uses `fetch`, so it works on Node and the edge.
189
+ */
190
+ export function otlpExporter(options) {
191
+ return {
192
+ async export(spans) {
193
+ const body = {
194
+ resourceSpans: [
195
+ {
196
+ resource: { attributes: otlpAttributes(options.resource ?? {}) },
197
+ scopeSpans: [
198
+ {
199
+ scope: { name: "keel" },
200
+ spans: spans.map((span) => ({
201
+ traceId: span.traceId,
202
+ spanId: span.spanId,
203
+ ...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
204
+ name: span.name,
205
+ kind: KIND_CODE[span.kind],
206
+ startTimeUnixNano: nanos(span.startTime),
207
+ endTimeUnixNano: nanos(span.endTime),
208
+ attributes: otlpAttributes(span.attributes),
209
+ events: span.events.map((event) => ({
210
+ name: event.name,
211
+ timeUnixNano: nanos(event.time),
212
+ attributes: otlpAttributes(event.attributes ?? {}),
213
+ })),
214
+ status: {
215
+ code: STATUS_CODE[span.status],
216
+ ...(span.statusMessage ? { message: span.statusMessage } : {}),
217
+ },
218
+ })),
219
+ },
220
+ ],
221
+ },
222
+ ],
223
+ };
224
+ await fetch(options.url, {
225
+ method: "POST",
226
+ headers: { "content-type": "application/json", ...options.headers },
227
+ body: JSON.stringify(body),
228
+ });
229
+ },
230
+ };
231
+ }
232
+ const storage = new AsyncLocalStorage();
233
+ export class Tracer {
234
+ options;
235
+ buffer = [];
236
+ constructor(options = {}) {
237
+ this.options = options;
238
+ }
239
+ get enabled() {
240
+ return this.options.enabled !== false;
241
+ }
242
+ /** @internal — called by `Span.end()`. */
243
+ record(span) {
244
+ if (!this.options.exporter)
245
+ return;
246
+ this.buffer.push(span);
247
+ if (this.buffer.length >= (this.options.batchSize ?? 100))
248
+ void this.flush();
249
+ }
250
+ /**
251
+ * Send buffered spans to the exporter. Call it before a Worker isolate is torn
252
+ * down (or on shutdown) — otherwise the last few spans die with the process.
253
+ */
254
+ async flush() {
255
+ if (!this.options.exporter || !this.buffer.length)
256
+ return;
257
+ const batch = this.buffer;
258
+ this.buffer = [];
259
+ await this.options.exporter.export(batch);
260
+ }
261
+ /** Whether a new root trace should be recorded. */
262
+ sample() {
263
+ const ratio = this.options.sampleRatio ?? 1;
264
+ return ratio >= 1 ? true : ratio <= 0 ? false : Math.random() < ratio;
265
+ }
266
+ /**
267
+ * Start a span. It becomes the current span for anything run inside
268
+ * `trace()`; a bare `startSpan()` does not, so you must `end()` it yourself.
269
+ */
270
+ startSpan(name, options = {}) {
271
+ const parent = options.parent ?? currentSpan()?.context;
272
+ const context = parent
273
+ ? { traceId: parent.traceId, spanId: newSpanId(), sampled: parent.sampled }
274
+ : { traceId: newTraceId(), spanId: newSpanId(), sampled: this.enabled && this.sample() };
275
+ const attributes = { ...options.attributes };
276
+ if (this.options.serviceName)
277
+ attributes["service.name"] = this.options.serviceName;
278
+ return new Span(name, options.kind ?? "internal", context, parent?.spanId, this, attributes);
279
+ }
280
+ /**
281
+ * Run `fn` inside a span: it's the current span for the duration, it ends when
282
+ * `fn` settles, and a throw is recorded on it and rethrown.
283
+ */
284
+ async trace(name, fn, options) {
285
+ const span = this.startSpan(name, options);
286
+ try {
287
+ return await storage.run(span, () => fn(span));
288
+ }
289
+ catch (error) {
290
+ span.recordException(error);
291
+ throw error;
292
+ }
293
+ finally {
294
+ span.end();
295
+ }
296
+ }
297
+ }
298
+ /* --------------------------------- global --------------------------------- */
299
+ let tracer = new Tracer();
300
+ /** Register the active tracer. */
301
+ export function setTelemetry(next) {
302
+ tracer = next;
303
+ return tracer;
304
+ }
305
+ /** The active tracer. */
306
+ export function telemetry() {
307
+ return tracer;
308
+ }
309
+ /** The span currently in scope, if any. */
310
+ export function currentSpan() {
311
+ return storage.getStore();
312
+ }
313
+ /** Run `fn` inside a span on the active tracer. */
314
+ export function trace(name, fn, options) {
315
+ return tracer.trace(name, fn, options);
316
+ }
317
+ /** Add attributes to the current span. A no-op outside one. */
318
+ export function setAttributes(attributes) {
319
+ currentSpan()?.setAttributes(attributes);
320
+ }
321
+ /** Add an event to the current span. A no-op outside one. */
322
+ export function addEvent(name, attributes) {
323
+ currentSpan()?.addEvent(name, attributes);
324
+ }
325
+ /** Send every buffered span to the exporter. */
326
+ export function flushTelemetry() {
327
+ return tracer.flush();
328
+ }
329
+ /**
330
+ * The current trace and span ids, for putting on a log line — this is what lets
331
+ * you jump from a log to the trace it belongs to.
332
+ *
333
+ * logger().child(traceIds()).info("charging card");
334
+ */
335
+ export function traceIds() {
336
+ const span = currentSpan();
337
+ if (!span)
338
+ return {};
339
+ return { trace_id: span.context.traceId, span_id: span.context.spanId };
340
+ }
341
+ const IGNORED = /^\/(health|metrics|favicon\.ico)/;
342
+ /**
343
+ * A server span for every request, joined to the caller's trace when they send a
344
+ * `traceparent`. Records method, path, and status; a 5xx or a thrown error marks
345
+ * the span failed.
346
+ *
347
+ * this.use(tracing());
348
+ */
349
+ export function tracing(options = {}) {
350
+ const ignore = options.ignore ?? ((path) => IGNORED.test(path));
351
+ return async (c, next) => {
352
+ const url = new URL(c.req.url);
353
+ if (!tracer.enabled || ignore(url.pathname))
354
+ return next();
355
+ const method = c.req.method;
356
+ const parent = parseTraceparent(c.req.header("traceparent")) ?? undefined;
357
+ const name = options.name?.(method, url.pathname) ?? `${method} ${url.pathname}`;
358
+ await tracer.trace(name, async (span) => {
359
+ try {
360
+ await next();
361
+ }
362
+ catch (error) {
363
+ span.recordException(error);
364
+ throw error;
365
+ }
366
+ const status = c.res.status;
367
+ span.setAttribute("http.status_code", status);
368
+ // 5xx is the server's fault, so it fails the span. A 404 is a valid answer.
369
+ span.setStatus(status >= 500 ? "error" : "ok");
370
+ // Hand the trace id back, so a user reporting a slow page can be looked up.
371
+ c.header("traceparent", traceparent(span.context));
372
+ }, {
373
+ kind: "server",
374
+ parent,
375
+ attributes: {
376
+ "http.request.method": method,
377
+ "url.path": url.pathname,
378
+ "url.scheme": url.protocol.replace(":", ""),
379
+ ...(c.req.header("user-agent") ? { "user_agent.original": c.req.header("user-agent") } : {}),
380
+ },
381
+ });
382
+ };
383
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
- * A string templating engine in the spirit of AdonisJS Edge — `{{ }}`
3
- * interpolation and `@`-prefixed tags for logic, includes, layouts, and
4
- * components.
2
+ * A string templating engine — `{{ }}` interpolation and `@`-prefixed tags for
3
+ * logic, includes, layouts, and components.
5
4
  *
6
5
  * const t = new TemplateEngine();
7
6
  * t.register("hello", "Hello, {{ name }}!");
@@ -1,7 +1,6 @@
1
1
  /**
2
- * A string templating engine in the spirit of AdonisJS Edge — `{{ }}`
3
- * interpolation and `@`-prefixed tags for logic, includes, layouts, and
4
- * components.
2
+ * A string templating engine — `{{ }}` interpolation and `@`-prefixed tags for
3
+ * logic, includes, layouts, and components.
5
4
  *
6
5
  * const t = new TemplateEngine();
7
6
  * t.register("hello", "Hello, {{ name }}!");