@shaferllc/keel 0.68.0 → 0.77.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.
- package/AGENTS.md +2 -0
- package/README.md +15 -5
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +93 -0
- package/dist/api/resource.js +185 -0
- package/dist/core/application.js +6 -0
- package/dist/core/cache.d.ts +1 -2
- package/dist/core/cache.js +9 -2
- package/dist/core/cli/index.d.ts +28 -0
- package/dist/core/cli/index.js +427 -0
- package/dist/core/cli/stubs.d.ts +14 -0
- package/dist/core/cli/stubs.js +105 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/health.d.ts +2 -2
- package/dist/core/health.js +2 -2
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +25 -4
- package/dist/core/index.js +13 -3
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +28 -1
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +117 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.js +26 -5
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.js +15 -3
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +171 -1
- package/dist/core/testing.js +505 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +22 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +737 -3
- package/docs/api-resources.md +119 -0
- package/docs/configuration.md +74 -0
- package/docs/console.md +217 -13
- package/docs/database.md +101 -0
- package/docs/examples/api-resources.ts +86 -0
- package/docs/examples/configuration.ts +40 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/database.ts +90 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/logger.ts +74 -0
- package/docs/examples/mail.ts +103 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/testing.ts +150 -0
- package/docs/health.md +4 -4
- package/docs/i18n.md +302 -0
- package/docs/logger.md +156 -10
- package/docs/mail.md +73 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/storage.md +5 -3
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +3 -4
- package/docs/testing.md +252 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +2389 -241
- package/llms.txt +13 -1
- package/package.json +32 -6
package/docs/pages.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Pages
|
|
2
|
+
|
|
3
|
+
Page-based routing — **a file is a route**.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
resources/pages/index.tsx → /
|
|
7
|
+
resources/pages/about.tsx → /about
|
|
8
|
+
resources/pages/users/index.tsx → /users
|
|
9
|
+
resources/pages/users/[id].tsx → /users/:id
|
|
10
|
+
resources/pages/docs/[...slug].tsx → /docs/* (catch-all)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// resources/pages/users/[id].tsx
|
|
15
|
+
import { db, type Ctx, type PageProps } from "@shaferllc/keel/core";
|
|
16
|
+
|
|
17
|
+
export const loader = (c: Ctx) => db("users").where("id", c.req.param("id")).first();
|
|
18
|
+
|
|
19
|
+
export default function UserPage({ params, data }: PageProps<{ id: string }, User>) {
|
|
20
|
+
return (
|
|
21
|
+
<Layout>
|
|
22
|
+
<h1>{data.name}</h1>
|
|
23
|
+
<p>User #{params.id}</p>
|
|
24
|
+
</Layout>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's the whole page. No route file to keep in sync, no controller, no wiring.
|
|
30
|
+
|
|
31
|
+
## It doesn't replace the router — it drives it
|
|
32
|
+
|
|
33
|
+
Every page becomes an **ordinary named route**. `url()` finds it, route middleware
|
|
34
|
+
applies to it, and `keel routes` lists it. You can mix pages and hand-written
|
|
35
|
+
routes freely, and reach for a controller the moment a page outgrows a file.
|
|
36
|
+
|
|
37
|
+
That matters, because file-based routing is a lovely default and a bad prison.
|
|
38
|
+
Here it's a *convenience over* the router, not a replacement for it.
|
|
39
|
+
|
|
40
|
+
## Registering them
|
|
41
|
+
|
|
42
|
+
In a service provider's `boot()`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { pages } from "@shaferllc/keel/core";
|
|
46
|
+
|
|
47
|
+
export class PageServiceProvider extends ServiceProvider {
|
|
48
|
+
async boot(): Promise<void> {
|
|
49
|
+
await pages(); // scans resources/pages
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`pages()` reads the filesystem, so it's **Node-only**. On the edge, hand
|
|
55
|
+
`definePages()` a build-time manifest instead — Vite's `import.meta.glob` produces
|
|
56
|
+
exactly the map it wants:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
definePages(import.meta.glob("./pages/**/*.tsx", { eager: true }));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Same behavior, no filesystem.
|
|
63
|
+
|
|
64
|
+
## The file conventions
|
|
65
|
+
|
|
66
|
+
| File | URL |
|
|
67
|
+
|------|-----|
|
|
68
|
+
| `index.tsx` | `/` |
|
|
69
|
+
| `about.tsx` | `/about` |
|
|
70
|
+
| `users/index.tsx` | `/users` |
|
|
71
|
+
| `users/[id].tsx` | `/users/:id` |
|
|
72
|
+
| `users/[id]/edit.tsx` | `/users/:id/edit` |
|
|
73
|
+
| `teams/[team]/users/[id].tsx` | `/teams/:team/users/:id` |
|
|
74
|
+
| `docs/[...slug].tsx` | `/docs/*` — a catch-all; `params.slug` is the whole rest |
|
|
75
|
+
| `_layout.tsx` | **not a route** — a leading `_` keeps a file private |
|
|
76
|
+
|
|
77
|
+
A trailing `index` names its directory rather than a child of it. A leading
|
|
78
|
+
underscore is how layouts, partials, and helpers live *beside* your pages without
|
|
79
|
+
becoming URLs.
|
|
80
|
+
|
|
81
|
+
## Specificity is decided for you
|
|
82
|
+
|
|
83
|
+
This is the part file-based routing usually gets wrong:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
users/[id].tsx → /users/:id
|
|
87
|
+
users/new.tsx → /users/new
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Register `:id` first and `/users/new` is **unreachable forever** — `:id` happily
|
|
91
|
+
matches `"new"`. Whether your app works would come down to the order the
|
|
92
|
+
filesystem happened to hand back.
|
|
93
|
+
|
|
94
|
+
So pages are **sorted before they're registered**: literal segments beat
|
|
95
|
+
parameters, parameters beat catch-alls, and a catch-all is always the last resort.
|
|
96
|
+
`/users/new` wins, and the file layout stops being a trap.
|
|
97
|
+
|
|
98
|
+
## Loading data
|
|
99
|
+
|
|
100
|
+
`loader` runs before the page renders; whatever it returns arrives as `data`.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
export const loader = async (c: Ctx) => {
|
|
104
|
+
const post = await db("posts").where("slug", c.req.param("slug")).first();
|
|
105
|
+
if (!post) throw new NotFoundException();
|
|
106
|
+
return post;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default function PostPage({ data }: PageProps<{ slug: string }, Post>) {
|
|
110
|
+
return <article>{data.body}</article>;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It takes the request context, so it can read params, query, headers, or the
|
|
115
|
+
session. A page with no `loader` simply gets `data: undefined`.
|
|
116
|
+
|
|
117
|
+
## Middleware
|
|
118
|
+
|
|
119
|
+
Per page:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
export const middleware = [authGuard()];
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or for every page at once:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
await pages({ middleware: [authGuard()] });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Both run before the `loader`, so a page that's refused never loads its data.
|
|
132
|
+
|
|
133
|
+
## Names and URLs
|
|
134
|
+
|
|
135
|
+
Each page gets a route name derived from its path — `users/[id].tsx` becomes
|
|
136
|
+
`users.id` — so URL generation works without you naming anything:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
router.url("users.id", { id: 5 }); // "/users/5"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Override it when the derived name is ugly:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
export const name = "users.show";
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Escape hatches
|
|
149
|
+
|
|
150
|
+
Move a page's URL without moving the file:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
export const path = "/pricing"; // even though the file is at marketing/plans.tsx
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Mount every page under a prefix:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await pages({ prefix: "/app" }); // index.tsx is now /app
|
|
160
|
+
await pages({ dir: "app/pages" }); // ...or keep them somewhere else
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## API reference
|
|
166
|
+
|
|
167
|
+
### `pages(options?)`
|
|
168
|
+
|
|
169
|
+
`pages(options?: PagesOptions): Promise<RegisteredPage[]>`
|
|
170
|
+
|
|
171
|
+
Scan a directory and register every page in it. **Node only** — it reads the
|
|
172
|
+
filesystem (`node:fs` is imported dynamically, so the core still loads on the
|
|
173
|
+
edge).
|
|
174
|
+
|
|
175
|
+
### `definePages(modules, options?)`
|
|
176
|
+
|
|
177
|
+
`definePages(modules: Record<string, PageModule>, options?: PagesOptions): RegisteredPage[]`
|
|
178
|
+
|
|
179
|
+
Register pages from a `file path → module` map. The edge-safe half — pair it with
|
|
180
|
+
`import.meta.glob`.
|
|
181
|
+
|
|
182
|
+
### `PagesOptions`
|
|
183
|
+
|
|
184
|
+
| Option | Meaning |
|
|
185
|
+
|--------|---------|
|
|
186
|
+
| `dir` | Where the pages live. Default `"resources/pages"` |
|
|
187
|
+
| `prefix` | Prefix every page's URL |
|
|
188
|
+
| `middleware` | Middleware applied to every page |
|
|
189
|
+
| `router` | The router to register on. Defaults to the application's |
|
|
190
|
+
|
|
191
|
+
### `PageModule`
|
|
192
|
+
|
|
193
|
+
What a page file exports.
|
|
194
|
+
|
|
195
|
+
| Export | Meaning |
|
|
196
|
+
|--------|---------|
|
|
197
|
+
| `default` | **Required.** The component. May be async |
|
|
198
|
+
| `loader` | `(ctx) => data` — runs before the page renders |
|
|
199
|
+
| `middleware` | Middleware for this page alone |
|
|
200
|
+
| `name` | The route name. Defaults to one derived from the path |
|
|
201
|
+
| `path` | Override the URL entirely |
|
|
202
|
+
|
|
203
|
+
### `PageProps<P, D>`
|
|
204
|
+
|
|
205
|
+
What the component receives: `params` (typed by `P`), `data` (whatever `loader`
|
|
206
|
+
returned, typed by `D`), and `ctx`.
|
|
207
|
+
|
|
208
|
+
### `RegisteredPage`
|
|
209
|
+
|
|
210
|
+
`{ file, pattern, name }` — what `pages()` / `definePages()` return, so you can
|
|
211
|
+
see exactly what got mounted.
|
|
212
|
+
|
|
213
|
+
### `routePattern(file)` / `routeName(file)`
|
|
214
|
+
|
|
215
|
+
The two pure functions behind the conventions, exported so you can test or reuse
|
|
216
|
+
them. `routePattern("users/[id].tsx")` → `"/users/:id"`;
|
|
217
|
+
`routeName("users/[id].tsx")` → `"users.id"`.
|
package/docs/storage.md
CHANGED
|
@@ -110,9 +110,11 @@ file.
|
|
|
110
110
|
|
|
111
111
|
> **The disk's `url()` prefix and `basePath` must agree.** `signedUrl()` signs the
|
|
112
112
|
> path the *disk* reports, so if the disk hands out `/storage/…` while
|
|
113
|
-
> `serveStorage` listens on `/private`,
|
|
114
|
-
>
|
|
115
|
-
>
|
|
113
|
+
> `serveStorage` listens on `/private`, no signature could ever match. Rather than
|
|
114
|
+
> 403 every request — which reads as "your link expired" and sends you hunting in
|
|
115
|
+
> the wrong place — `serveStorage` **throws** with the two paths and how to line
|
|
116
|
+
> them up. Give the disk the matching base (`new MemoryDisk("/private")`, or
|
|
117
|
+
> `localDisk("./storage", "/private")`), or keep both on the default `/storage`.
|
|
116
118
|
|
|
117
119
|
## Direct browser uploads
|
|
118
120
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Telemetry
|
|
2
|
+
|
|
3
|
+
Distributed tracing — spans, W3C trace context, and an OTLP exporter — with **no
|
|
4
|
+
SDK**.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { setTelemetry, Tracer, otlpExporter, tracing, trace } from "@shaferllc/keel/core";
|
|
8
|
+
|
|
9
|
+
setTelemetry(
|
|
10
|
+
new Tracer({
|
|
11
|
+
serviceName: "api",
|
|
12
|
+
exporter: otlpExporter({ url: "http://localhost:4318/v1/traces" }),
|
|
13
|
+
sampleRatio: 0.1, // 10% of traces in production
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// in your HTTP kernel
|
|
18
|
+
this.use(tracing()); // a server span per request
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// your own spans, anywhere
|
|
23
|
+
await trace("charge", async (span) => {
|
|
24
|
+
span.setAttributes({ "order.id": order.id });
|
|
25
|
+
await stripe.charge(order);
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why there's no SDK here
|
|
30
|
+
|
|
31
|
+
The OpenTelemetry Node SDK is a large tree of packages that assumes a Node
|
|
32
|
+
process. What a trace actually **is**, though, is small: an id, a parent, a start
|
|
33
|
+
and an end, some attributes — and a documented JSON shape to POST them in.
|
|
34
|
+
|
|
35
|
+
That's what this is. It speaks **OTLP/HTTP over `fetch`**, so it runs on Workers as
|
|
36
|
+
happily as on Node, and any OTLP collector accepts it — Jaeger, Tempo, Honeycomb,
|
|
37
|
+
Grafana, Datadog. You don't get the SDK's auto-instrumentation of every library
|
|
38
|
+
under the sun; you get the part that matters, in about 400 lines you can read.
|
|
39
|
+
|
|
40
|
+
## Spans
|
|
41
|
+
|
|
42
|
+
`trace(name, fn)` opens a span, runs your function inside it, and closes it — even
|
|
43
|
+
if the function throws, in which case the error is recorded on the span and
|
|
44
|
+
rethrown.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const receipt = await trace("charge", async (span) => {
|
|
48
|
+
span.setAttributes({ "order.id": id, currency: "USD" });
|
|
49
|
+
span.addEvent("calling stripe");
|
|
50
|
+
|
|
51
|
+
return stripe.charge(id); // a throw here marks the span failed, then propagates
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Spans nest automatically.** A `trace()` inside another `trace()` becomes its
|
|
56
|
+
child, sharing the trace id — you don't thread anything through:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
await trace("checkout", async () => {
|
|
60
|
+
await trace("reserve-stock", async () => { … }); // child
|
|
61
|
+
await trace("charge", async () => { … }); // sibling of the above
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That works across `await` boundaries, and across **concurrent** traces, because
|
|
66
|
+
the current span is tracked in `AsyncLocalStorage` rather than a global. Two
|
|
67
|
+
requests in flight at once don't get tangled.
|
|
68
|
+
|
|
69
|
+
From anywhere inside a span:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
setAttributes({ tenant: "acme" }); // add to the current span
|
|
73
|
+
addEvent("cache miss", { key }); // a timestamped annotation
|
|
74
|
+
currentSpan(); // the Span itself, or undefined
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
All of these are **no-ops outside a span**, so instrumented code stays safe to call
|
|
78
|
+
from a script or a test.
|
|
79
|
+
|
|
80
|
+
## HTTP requests
|
|
81
|
+
|
|
82
|
+
`tracing()` opens a **server span** per request, records the method, path, and
|
|
83
|
+
status, and closes it when the response is sent.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
this.use(tracing());
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A 5xx marks the span failed; a 404 doesn't — that's a valid answer, not a fault.
|
|
90
|
+
|
|
91
|
+
It also writes a `traceparent` header onto the **response**, so when a user says
|
|
92
|
+
"this page was slow", you can look up their exact trace.
|
|
93
|
+
|
|
94
|
+
`/health/*`, `/metrics`, and `/favicon.ico` are ignored by default — they're noise.
|
|
95
|
+
Change that with `ignore`, and name spans yourself with `name`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
this.use(
|
|
99
|
+
tracing({
|
|
100
|
+
ignore: (path) => path.startsWith("/internal"),
|
|
101
|
+
name: (method, path) => `${method} ${path.replace(/\/\d+/, "/:id")}`,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Following a trace between services
|
|
107
|
+
|
|
108
|
+
This is the point of tracing: one id spanning every service a request touches.
|
|
109
|
+
|
|
110
|
+
**Incoming.** `tracing()` reads the caller's `traceparent` header and makes your
|
|
111
|
+
span a **child of theirs**, so both land in the same trace. A missing or malformed
|
|
112
|
+
header just starts a fresh trace — never an error.
|
|
113
|
+
|
|
114
|
+
**Outgoing.** `injectTraceContext()` puts the current context on your request
|
|
115
|
+
headers, so the service you call joins this trace instead of starting its own:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
await fetch(url, {
|
|
119
|
+
headers: injectTraceContext({ accept: "application/json" }),
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`parseTraceparent()` and `traceparent()` are the two halves on their own, if you
|
|
124
|
+
need to carry the context somewhere odd — a queue payload, say.
|
|
125
|
+
|
|
126
|
+
## Connecting logs to traces
|
|
127
|
+
|
|
128
|
+
`traceIds()` returns the current `trace_id` and `span_id`. Bind them to your logger
|
|
129
|
+
and every line becomes a jumping-off point into the trace it came from:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const log = logger().child(traceIds());
|
|
133
|
+
log.info("charging card", { orderId }); // carries trace_id + span_id
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Sampling
|
|
137
|
+
|
|
138
|
+
Recording every trace in production is expensive. `sampleRatio` records a fraction:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
new Tracer({ sampleRatio: 0.1 }); // 10%
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The decision is made **once, at the root span, and inherited by every child** —
|
|
145
|
+
because half a trace is worse than no trace. An unsampled span still runs (your
|
|
146
|
+
code is unaffected); it just isn't exported.
|
|
147
|
+
|
|
148
|
+
## Exporters
|
|
149
|
+
|
|
150
|
+
| Exporter | Use |
|
|
151
|
+
|----------|-----|
|
|
152
|
+
| `otlpExporter({ url, headers, resource })` | Any OTLP/HTTP collector. Production. |
|
|
153
|
+
| `consoleExporter()` | Prints each span. Local development, no collector needed. |
|
|
154
|
+
| `MemoryExporter` | Collects spans in memory. Tests. |
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
otlpExporter({
|
|
158
|
+
url: "https://api.honeycomb.io/v1/traces",
|
|
159
|
+
headers: { "x-honeycomb-team": env("HONEYCOMB_KEY") },
|
|
160
|
+
resource: { "service.name": "api", "deployment.environment": "prod" },
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Spans are **buffered** and sent in batches (100 by default, via `batchSize`).
|
|
165
|
+
|
|
166
|
+
**Flush before the process — or the isolate — goes away**, or the last few spans
|
|
167
|
+
die with it:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
onShutdown(() => flushTelemetry());
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
On Workers, call it at the end of a request (ideally inside `waitUntil`).
|
|
174
|
+
|
|
175
|
+
## Testing
|
|
176
|
+
|
|
177
|
+
`MemoryExporter` collects spans so you can assert on them:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { Tracer, MemoryExporter, setTelemetry, trace } from "@shaferllc/keel/core";
|
|
181
|
+
|
|
182
|
+
const exporter = new MemoryExporter();
|
|
183
|
+
setTelemetry(new Tracer({ exporter, batchSize: 1 })); // batchSize 1 = export immediately
|
|
184
|
+
|
|
185
|
+
await trace("charge", async (span) => span.setAttributes({ "order.id": 1 }));
|
|
186
|
+
|
|
187
|
+
const span = exporter.named("charge")[0];
|
|
188
|
+
assert.equal(span.status, "unset");
|
|
189
|
+
assert.equal(span.attributes["order.id"], 1);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`exporter.trace(traceId)` returns every span in one trace; `exporter.clear()`
|
|
193
|
+
empties it between tests.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## API reference
|
|
198
|
+
|
|
199
|
+
### `trace(name, fn, options?)`
|
|
200
|
+
|
|
201
|
+
`trace<T>(name: string, fn: (span: Span) => Promise<T> | T, options?: SpanOptions): Promise<T>`
|
|
202
|
+
|
|
203
|
+
Run `fn` inside a span. The span is current for the duration, ends when `fn`
|
|
204
|
+
settles, and records a throw before rethrowing it.
|
|
205
|
+
|
|
206
|
+
### `currentSpan()` / `setAttributes(attrs)` / `addEvent(name, attrs?)`
|
|
207
|
+
|
|
208
|
+
Reach the span in scope. All no-ops outside one.
|
|
209
|
+
|
|
210
|
+
### `traceIds()`
|
|
211
|
+
|
|
212
|
+
`traceIds(): { trace_id?: string; span_id?: string }` — the ids to hang on a log
|
|
213
|
+
line.
|
|
214
|
+
|
|
215
|
+
### `tracing(options?)`
|
|
216
|
+
|
|
217
|
+
`tracing(options?: TracingOptions): MiddlewareHandler`
|
|
218
|
+
|
|
219
|
+
A server span per request. Options: `ignore(path)` (default: `/health`, `/metrics`,
|
|
220
|
+
`/favicon.ico`), `name(method, path)`.
|
|
221
|
+
|
|
222
|
+
### `Tracer`
|
|
223
|
+
|
|
224
|
+
`new Tracer(options: TracerOptions)`
|
|
225
|
+
|
|
226
|
+
| Option | Meaning |
|
|
227
|
+
|--------|---------|
|
|
228
|
+
| `serviceName` | Added to every span as `service.name` |
|
|
229
|
+
| `exporter` | Where finished spans go. Omit and nothing is exported |
|
|
230
|
+
| `sampleRatio` | 0–1, decided once at the root. Default 1 |
|
|
231
|
+
| `enabled` | `false` turns tracing off |
|
|
232
|
+
| `resource` | Attributes describing the service, sent with each batch |
|
|
233
|
+
| `batchSize` | Export once this many spans are buffered. Default 100 |
|
|
234
|
+
|
|
235
|
+
Methods: `startSpan(name, options?)`, `trace(name, fn, options?)`, `flush()`.
|
|
236
|
+
|
|
237
|
+
### `Span`
|
|
238
|
+
|
|
239
|
+
`setAttribute(k, v)` / `setAttributes(attrs)` / `addEvent(name, attrs?)` /
|
|
240
|
+
`setStatus(status, message?)` / `recordException(error)` / `end()`, plus a
|
|
241
|
+
`context` (`{ traceId, spanId, sampled }`).
|
|
242
|
+
|
|
243
|
+
### `setTelemetry(tracer)` / `telemetry()` / `flushTelemetry()`
|
|
244
|
+
|
|
245
|
+
Register the active tracer, read it, and drain its buffer.
|
|
246
|
+
|
|
247
|
+
### Trace context
|
|
248
|
+
|
|
249
|
+
`parseTraceparent(header)` → `SpanContext | null` (null on anything malformed).
|
|
250
|
+
`traceparent(context)` → the header string.
|
|
251
|
+
`injectTraceContext(headers?)` → headers with the current context added.
|
|
252
|
+
|
|
253
|
+
### Exporters
|
|
254
|
+
|
|
255
|
+
`otlpExporter({ url, headers?, resource? })`, `consoleExporter()`, and
|
|
256
|
+
`MemoryExporter` (`.spans`, `.named(name)`, `.trace(traceId)`, `.clear()`).
|
|
257
|
+
|
|
258
|
+
### Interfaces & types
|
|
259
|
+
|
|
260
|
+
`SpanData`, `SpanContext`, `SpanEvent`, `SpanKind`
|
|
261
|
+
(`internal | server | client | producer | consumer`), `SpanStatus`
|
|
262
|
+
(`unset | ok | error`), `SpanAttributes`, `SpanExporter`, `TracerOptions`,
|
|
263
|
+
`TracingOptions`, `OtlpOptions`.
|
package/docs/templates.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Templates
|
|
2
2
|
|
|
3
|
-
A string templating engine
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
alongside) [JSX views](./views.md).
|
|
3
|
+
A string templating engine — `{{ }}` interpolation and `@`-prefixed tags for
|
|
4
|
+
logic, includes, layouts, and components. Reach for it when you want plain-text
|
|
5
|
+
templates instead of (or alongside) [JSX views](./views.md).
|
|
7
6
|
|
|
8
7
|
Unlike engines that compile a template to a function with `eval` /
|
|
9
8
|
`new Function`, Keel **interprets** templates against a small, safe expression
|