@adriangalilea/utils 9.3.0 → 9.5.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/README.md +42 -0
- package/dist/bot/access-control.d.ts +7 -7
- package/dist/bot/language.d.ts +5 -5
- package/dist/bot/llm.d.ts +5 -5
- package/dist/bot/payments/plugin.d.ts +7 -7
- package/dist/metrics/sqlite.d.ts +1 -1
- package/dist/metrics/sqlite.d.ts.map +1 -1
- package/dist/next/log.d.ts +25 -0
- package/dist/next/log.d.ts.map +1 -0
- package/dist/next/log.js +40 -0
- package/dist/next/log.js.map +1 -0
- package/dist/universal/log.d.ts +25 -0
- package/dist/universal/log.d.ts.map +1 -1
- package/dist/universal/log.js +234 -44
- package/dist/universal/log.js.map +1 -1
- package/dist/visits/index.d.ts +96 -0
- package/dist/visits/index.d.ts.map +1 -0
- package/dist/visits/index.js +404 -0
- package/dist/visits/index.js.map +1 -0
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -77,6 +77,24 @@ Enable it only with a deletion and retention policy; no identity is inferred.
|
|
|
77
77
|
`overlaps: [["a", "b"]]` declares unordered audience overlap, not an ordered funnel.
|
|
78
78
|
Daily totals support counts and averages, not percentile latency or event ordering.
|
|
79
79
|
|
|
80
|
+
## Visits
|
|
81
|
+
|
|
82
|
+
A site keeps its own visits: ui's `page-sensor` posts each page's lines (the utils contract, "Web pages") to the site's own route, and `@adriangalilea/utils/visits` puts each page in a visit and keeps the rows the views read (ui's `user-flows`). No other server, no cookie, nothing kept in the browser.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// app/api/events/route.ts
|
|
86
|
+
import { libsqlDriver } from '@adriangalilea/utils/metrics/libsql'
|
|
87
|
+
import { visitsRoute } from '@adriangalilea/utils/visits'
|
|
88
|
+
|
|
89
|
+
export const POST = visitsRoute({ db: libsqlDriver(client), site: 'untitled.garden', services: ['garden-web'] })
|
|
90
|
+
|
|
91
|
+
// a view
|
|
92
|
+
const visits = await readVisits(libsqlDriver(client), { site: 'untitled.garden', days: 14 })
|
|
93
|
+
// [{ id, started, places: ['studio', 'lore'], referrer: 'news.ycombinator.com', country: 'ES', … }]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A visit is one caller's pages, each within 30 minutes of the one before, within one UTC day. The caller is a hash of its IP and user agent with a random salt of the day, deleted when the day is over: a visitor is recognisable within a day, never across two, and once the salt is gone by no one, the site's owner included. The IP and user agent are never stored; the country (two letters, from the platform's header) is. Pages and visits are kept 180 days, a page's other lines (requests, named events, errors) 30, a probe's (an automated browser) and the staff's 7, and both are left out of `readVisits` unless `flagged`. Refused, by line: a service the site did not name, a line outside a page's trace, a field named like a person (`ip`, `user_agent`, `email`, `user`, `name`, `phone`). A crawler's batch is dropped whole. Provision `VISITS_SCHEMA` once (the tables are prefixed `visits_`); the route updates and deletes, so its credential needs both. `readVisit(db, id)` returns one visit's pages and lines, for its trace.
|
|
97
|
+
|
|
80
98
|
## Installation
|
|
81
99
|
|
|
82
100
|
```bash
|
|
@@ -160,6 +178,30 @@ Node, Bun and Deno write every event through `console.error` (stdout stays data,
|
|
|
160
178
|
| `LOG_TIME` | `setLogTime` | `1`, `0` | off. Puts a dim local `HH:MM:SS` before human lines; json always carries `time` |
|
|
161
179
|
| `LOG_LEVEL`, `{SCOPE}_LOG_LEVEL` | `setLogLevel` | the levels | `info` |
|
|
162
180
|
| `LOG_SERVICE` | `setLogService` | a name | the entry script's name without extension. A Worker or browser has none: set it, or the first json event panics |
|
|
181
|
+
| `LOG_SHIP` | `setLogShip` | an http(s) URL | unset. Every event that passes its threshold is also POSTed there (Shipping) |
|
|
182
|
+
| `LOG_SHIP_TOKEN` | `setLogShipToken` | a token | unset. Sent as `Authorization: Bearer` with each batch; a browser sends none, and `setLogShipToken` in one panics |
|
|
183
|
+
|
|
184
|
+
**Shipping.** With `LOG_SHIP` set, the logger also POSTs its events to that URL, so a process ships its stream with no code:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
LOG_SHIP=https://events.untitled.garden/ingest LOG_SHIP_TOKEN=… node server.js
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
What ships is the stream's json line, byte for byte, whatever the console renders (human at a terminal included). A batch is lines joined by `\n`, `Content-Type: text/plain;charset=utf-8`, at most 64 KB and 50 events. It leaves 2s after its first event, at 50 events, when a span opened outside any local span ends (a root span, or a request joined from its caller: its whole trace leaves together), before a Node or Bun process exits on its own (`beforeExit`), and in a browser on `visibilitychange: hidden` and `pagehide`; whatever a hidden page sends goes through `sendBeacon`, the one request a browser finishes after the page is gone. `flushLog()` sends what waits and resolves when nothing is in flight: a Worker hands it to `ctx.waitUntil(flushLog())`, a serverless function to its runtime's `after`, and a process that calls `process.exit()` awaits it first (nothing else runs after `exit`). Shipping never throws and never blocks a write: a refused or lost batch is one `warnOnce` under scope `ship`, an event over 64 KB stays on the console and warns once. The wire and the collector's answers are the contract's, in [utils](https://github.com/adriangalilea/utils#shipping).
|
|
191
|
+
|
|
192
|
+
**A page's trace on a Next.js server** (`@adriangalilea/utils/log/next`, `next` an optional peer). A page that writes the stream (utils README, "Web pages") puts its `traceparent` on every same-origin request; these wrappers make the server's work a child of that request, in the page's trace, and hand `flushLog` to Next's `after` so a frozen instance holds nothing unsent. A request with no `traceparent` is a root span of its own.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { traced, tracedAction } from '@adriangalilea/utils/log/next'
|
|
196
|
+
|
|
197
|
+
export const POST = traced('POST /api/vote', async (s, request) => {
|
|
198
|
+
await s.span('turso insert vote', () => db.execute(insert))
|
|
199
|
+
return Response.json({ ok: true })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// in a "use server" file
|
|
203
|
+
export const vote = tracedAction('vote', async (s, id: string) => { … })
|
|
204
|
+
```
|
|
163
205
|
|
|
164
206
|
**Reading the stream.** `LogEvent` is the wire shape as a type (a line, a span start, an ok end or a failed end, discriminated by `phase` and `status`), and `parseEvent(line)` reads one line back: an event, `null` for a line that is not one (pass it through), or a `Panic` for a JSON object with a `verb` key that breaks the shape.
|
|
165
207
|
|
|
@@ -318,6 +318,13 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
|
|
|
318
318
|
$clear: () => Promise<void>;
|
|
319
319
|
};
|
|
320
320
|
};
|
|
321
|
+
location: {
|
|
322
|
+
session: {
|
|
323
|
+
access?: AccessRecord;
|
|
324
|
+
} & LangSession & {
|
|
325
|
+
$clear: () => Promise<void>;
|
|
326
|
+
};
|
|
327
|
+
};
|
|
321
328
|
channel_post: {
|
|
322
329
|
session: {
|
|
323
330
|
access?: AccessRecord;
|
|
@@ -500,13 +507,6 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
|
|
|
500
507
|
$clear: () => Promise<void>;
|
|
501
508
|
};
|
|
502
509
|
};
|
|
503
|
-
location: {
|
|
504
|
-
session: {
|
|
505
|
-
access?: AccessRecord;
|
|
506
|
-
} & LangSession & {
|
|
507
|
-
$clear: () => Promise<void>;
|
|
508
|
-
};
|
|
509
|
-
};
|
|
510
510
|
passport_data: {
|
|
511
511
|
session: {
|
|
512
512
|
access?: AccessRecord;
|
package/dist/bot/language.d.ts
CHANGED
|
@@ -228,6 +228,11 @@ declare const buildLanguagePlugin: <Lang extends string>(args: {
|
|
|
228
228
|
$clear: () => Promise<void>;
|
|
229
229
|
};
|
|
230
230
|
};
|
|
231
|
+
location: {
|
|
232
|
+
session: SessionLike & {
|
|
233
|
+
$clear: () => Promise<void>;
|
|
234
|
+
};
|
|
235
|
+
};
|
|
231
236
|
channel_post: {
|
|
232
237
|
session: SessionLike & {
|
|
233
238
|
$clear: () => Promise<void>;
|
|
@@ -358,11 +363,6 @@ declare const buildLanguagePlugin: <Lang extends string>(args: {
|
|
|
358
363
|
$clear: () => Promise<void>;
|
|
359
364
|
};
|
|
360
365
|
};
|
|
361
|
-
location: {
|
|
362
|
-
session: SessionLike & {
|
|
363
|
-
$clear: () => Promise<void>;
|
|
364
|
-
};
|
|
365
|
-
};
|
|
366
366
|
passport_data: {
|
|
367
367
|
session: SessionLike & {
|
|
368
368
|
$clear: () => Promise<void>;
|
package/dist/bot/llm.d.ts
CHANGED
|
@@ -243,6 +243,11 @@ declare const buildHistoryPlugin: (args: {
|
|
|
243
243
|
$clear: () => Promise<void>;
|
|
244
244
|
};
|
|
245
245
|
};
|
|
246
|
+
location: {
|
|
247
|
+
session: LLMSessionLike & {
|
|
248
|
+
$clear: () => Promise<void>;
|
|
249
|
+
};
|
|
250
|
+
};
|
|
246
251
|
channel_post: {
|
|
247
252
|
session: LLMSessionLike & {
|
|
248
253
|
$clear: () => Promise<void>;
|
|
@@ -373,11 +378,6 @@ declare const buildHistoryPlugin: (args: {
|
|
|
373
378
|
$clear: () => Promise<void>;
|
|
374
379
|
};
|
|
375
380
|
};
|
|
376
|
-
location: {
|
|
377
|
-
session: LLMSessionLike & {
|
|
378
|
-
$clear: () => Promise<void>;
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
381
|
passport_data: {
|
|
382
382
|
session: LLMSessionLike & {
|
|
383
383
|
$clear: () => Promise<void>;
|
|
@@ -118,6 +118,13 @@ declare const buildPlugin: (args: {
|
|
|
118
118
|
$clear: () => Promise<void>;
|
|
119
119
|
};
|
|
120
120
|
};
|
|
121
|
+
location: {
|
|
122
|
+
session: {
|
|
123
|
+
pay?: import("./types.js").PaymentsSession;
|
|
124
|
+
} & import("../lang.js").LangSession & {
|
|
125
|
+
$clear: () => Promise<void>;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
121
128
|
channel_post: {
|
|
122
129
|
session: {
|
|
123
130
|
pay?: import("./types.js").PaymentsSession;
|
|
@@ -300,13 +307,6 @@ declare const buildPlugin: (args: {
|
|
|
300
307
|
$clear: () => Promise<void>;
|
|
301
308
|
};
|
|
302
309
|
};
|
|
303
|
-
location: {
|
|
304
|
-
session: {
|
|
305
|
-
pay?: import("./types.js").PaymentsSession;
|
|
306
|
-
} & import("../lang.js").LangSession & {
|
|
307
|
-
$clear: () => Promise<void>;
|
|
308
|
-
};
|
|
309
|
-
};
|
|
310
310
|
passport_data: {
|
|
311
311
|
session: {
|
|
312
312
|
pay?: import("./types.js").PaymentsSession;
|
package/dist/metrics/sqlite.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MetricsSchema, MetricsStore } from "./index.js";
|
|
2
2
|
import { type ActiveActors, type Audience, type DailyMetric, type MetricsReport } from "./report.js";
|
|
3
|
-
export type SqlValue = string | number;
|
|
3
|
+
export type SqlValue = string | number | null;
|
|
4
4
|
export interface SqlStatement {
|
|
5
5
|
sql: string;
|
|
6
6
|
args: SqlValue[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/metrics/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/metrics/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAC9C,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,EAAE,CAAC;CACjB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC7B,sCAAsC;IACtC,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3E;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,aAAa;IACnD,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,uFAKjB,CAAC;AAEX,qEAAqE;AACrE,eAAO,MAAM,cAAc,88BAMjB,CAAC;AAsCX,gDAAgD;AAChD,wBAAgB,YAAY,CAC3B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,YAAY,CA0Dd;AAED,4FAA4F;AAC5F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,uFAAuF;AACvF,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,WAAW,EAAE,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,QAAQ,CAAC,CAmDnB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,YAAY,EAAE,CAAC,CAgCzB;AAED,6FAA6F;AAC7F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;CACX,GACC,OAAO,CAAC,aAAa,CAAC,CAkBxB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Next.js server's half of a page's trace (utils README, "Web pages"). A
|
|
3
|
+
* request a page made carries the page's `traceparent` (log/web sets it on
|
|
4
|
+
* every same-origin fetch), so what the server does under these wrappers is
|
|
5
|
+
* a child of that request, in the page's trace:
|
|
6
|
+
*
|
|
7
|
+
* export const POST = traced("POST /api/vote", async (s, request) => {
|
|
8
|
+
* await s.span("turso insert vote", () => db.execute(...))
|
|
9
|
+
* return Response.json({ ok: true })
|
|
10
|
+
* })
|
|
11
|
+
*
|
|
12
|
+
* // in a "use server" file
|
|
13
|
+
* export const vote = tracedAction("vote", async (s, id: string) => { ... })
|
|
14
|
+
*
|
|
15
|
+
* A request with no traceparent is a root span of its own. Each wrapper hands
|
|
16
|
+
* flushLog to `after`, so a serverless instance frozen once its response is
|
|
17
|
+
* sent never holds unsent events (LOG_SHIP, set on the deployment, is where
|
|
18
|
+
* they go).
|
|
19
|
+
*/
|
|
20
|
+
import { type Span } from "../universal/log.js";
|
|
21
|
+
/** A route handler whose work is a span, joined to the caller's trace. */
|
|
22
|
+
export declare function traced<A extends unknown[], R>(name: string, handler: (s: Span, request: Request, ...rest: A) => Promise<R>): (request: Request, ...rest: A) => Promise<R>;
|
|
23
|
+
/** A server action whose work is a span, joined to the trace of the page that called it. */
|
|
24
|
+
export declare function tracedAction<A extends unknown[], R>(name: string, action: (s: Span, ...args: A) => Promise<R>): (...args: A) => Promise<R>;
|
|
25
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/next/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE/D,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5D,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAO9C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAClD,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAM5B"}
|
package/dist/next/log.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Next.js server's half of a page's trace (utils README, "Web pages"). A
|
|
3
|
+
* request a page made carries the page's `traceparent` (log/web sets it on
|
|
4
|
+
* every same-origin fetch), so what the server does under these wrappers is
|
|
5
|
+
* a child of that request, in the page's trace:
|
|
6
|
+
*
|
|
7
|
+
* export const POST = traced("POST /api/vote", async (s, request) => {
|
|
8
|
+
* await s.span("turso insert vote", () => db.execute(...))
|
|
9
|
+
* return Response.json({ ok: true })
|
|
10
|
+
* })
|
|
11
|
+
*
|
|
12
|
+
* // in a "use server" file
|
|
13
|
+
* export const vote = tracedAction("vote", async (s, id: string) => { ... })
|
|
14
|
+
*
|
|
15
|
+
* A request with no traceparent is a root span of its own. Each wrapper hands
|
|
16
|
+
* flushLog to `after`, so a serverless instance frozen once its response is
|
|
17
|
+
* sent never holds unsent events (LOG_SHIP, set on the deployment, is where
|
|
18
|
+
* they go).
|
|
19
|
+
*/
|
|
20
|
+
import { headers } from "next/headers";
|
|
21
|
+
import { after } from "next/server";
|
|
22
|
+
import { flushLog, log } from "../universal/log.js";
|
|
23
|
+
/** A route handler whose work is a span, joined to the caller's trace. */
|
|
24
|
+
export function traced(name, handler) {
|
|
25
|
+
return (request, ...rest) => {
|
|
26
|
+
after(flushLog);
|
|
27
|
+
return log
|
|
28
|
+
.join(request.headers.get("traceparent"))
|
|
29
|
+
.span(name, (s) => handler(s, request, ...rest));
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A server action whose work is a span, joined to the trace of the page that called it. */
|
|
33
|
+
export function tracedAction(name, action) {
|
|
34
|
+
return async (...args) => {
|
|
35
|
+
after(flushLog);
|
|
36
|
+
const parent = (await headers()).get("traceparent");
|
|
37
|
+
return log.join(parent).span(name, (s) => action(s, ...args));
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/next/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAa,MAAM,qBAAqB,CAAC;AAE/D,0EAA0E;AAC1E,MAAM,UAAU,MAAM,CACrB,IAAY,EACZ,OAA8D;IAE9D,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE;QAC3B,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChB,OAAO,GAAG;aACR,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;aACxC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAC3B,IAAY,EACZ,MAA2C;IAE3C,OAAO,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE;QACxB,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChB,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC;AACH,CAAC"}
|
package/dist/universal/log.d.ts
CHANGED
|
@@ -76,6 +76,21 @@
|
|
|
76
76
|
* log, stdout stays data, and cli/live's console patch keeps lines above
|
|
77
77
|
* a live region. Browsers and Workers use the level's console method.
|
|
78
78
|
*
|
|
79
|
+
* Shipping
|
|
80
|
+
* LOG_SHIP=<url> (setLogShip) also POSTs every event that passes its
|
|
81
|
+
* threshold there: the json line, whatever the console renders, in
|
|
82
|
+
* batches of lines joined by "\n" (text/plain;charset=utf-8, at most
|
|
83
|
+
* 64 KB and 50 events). A batch leaves 2s after its first event, at 50,
|
|
84
|
+
* when a span opened outside any local span ends (a request's trace
|
|
85
|
+
* leaves together), before a Node/Bun process exits (beforeExit), and in
|
|
86
|
+
* a browser on hidden or pagehide; whatever a hidden page sends goes
|
|
87
|
+
* through sendBeacon, which outlives the page. LOG_SHIP_TOKEN
|
|
88
|
+
* (setLogShipToken) is a bearer for servers; a browser sends none.
|
|
89
|
+
* flushLog() sends what waits and resolves when nothing is in flight:
|
|
90
|
+
* a Worker hands it to waitUntil, a process that calls process.exit()
|
|
91
|
+
* awaits it first. Shipping never throws and never blocks a write: a
|
|
92
|
+
* refused or lost batch is one warnOnce, scope ["ship"].
|
|
93
|
+
*
|
|
79
94
|
* The wire shape is exported as LogEvent, and parseEvent(line) reads one back.
|
|
80
95
|
*/
|
|
81
96
|
export type LogLevel = "silent" | "error" | "warn" | "info" | "debug" | "trace";
|
|
@@ -147,6 +162,16 @@ export declare function setLogFormat(format: LogFormat): void;
|
|
|
147
162
|
export declare function setLogTime(on: boolean): void;
|
|
148
163
|
/** The program's name in every event. LOG_SERVICE overrides it. */
|
|
149
164
|
export declare function setLogService(name: string): void;
|
|
165
|
+
/** Where events are also POSTed, in batches. LOG_SHIP overrides it. */
|
|
166
|
+
export declare function setLogShip(url: string): void;
|
|
167
|
+
/** The bearer sent with each batch (servers only). LOG_SHIP_TOKEN overrides it. */
|
|
168
|
+
export declare function setLogShipToken(token: string): void;
|
|
169
|
+
/**
|
|
170
|
+
* Send what waits to LOG_SHIP and resolve when no batch is in flight. A
|
|
171
|
+
* Worker hands it to waitUntil; a process that calls process.exit() awaits it
|
|
172
|
+
* first. Never rejects: a lost batch is a warnOnce.
|
|
173
|
+
*/
|
|
174
|
+
export declare function flushLog(): Promise<void>;
|
|
150
175
|
export interface Logger {
|
|
151
176
|
error(message: string, fields?: Fields): void;
|
|
152
177
|
fail(message: string, fields?: Fields): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/universal/log.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/universal/log.ts"],"names":[],"mappings":"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"}
|
package/dist/universal/log.js
CHANGED
|
@@ -76,6 +76,21 @@
|
|
|
76
76
|
* log, stdout stays data, and cli/live's console patch keeps lines above
|
|
77
77
|
* a live region. Browsers and Workers use the level's console method.
|
|
78
78
|
*
|
|
79
|
+
* Shipping
|
|
80
|
+
* LOG_SHIP=<url> (setLogShip) also POSTs every event that passes its
|
|
81
|
+
* threshold there: the json line, whatever the console renders, in
|
|
82
|
+
* batches of lines joined by "\n" (text/plain;charset=utf-8, at most
|
|
83
|
+
* 64 KB and 50 events). A batch leaves 2s after its first event, at 50,
|
|
84
|
+
* when a span opened outside any local span ends (a request's trace
|
|
85
|
+
* leaves together), before a Node/Bun process exits (beforeExit), and in
|
|
86
|
+
* a browser on hidden or pagehide; whatever a hidden page sends goes
|
|
87
|
+
* through sendBeacon, which outlives the page. LOG_SHIP_TOKEN
|
|
88
|
+
* (setLogShipToken) is a bearer for servers; a browser sends none.
|
|
89
|
+
* flushLog() sends what waits and resolves when nothing is in flight:
|
|
90
|
+
* a Worker hands it to waitUntil, a process that calls process.exit()
|
|
91
|
+
* awaits it first. Shipping never throws and never blocks a write: a
|
|
92
|
+
* refused or lost batch is one warnOnce, scope ["ship"].
|
|
93
|
+
*
|
|
79
94
|
* The wire shape is exported as LogEvent, and parseEvent(line) reads one back.
|
|
80
95
|
*/
|
|
81
96
|
import { assert, panic, SourcedError } from "../offensive.js";
|
|
@@ -468,6 +483,7 @@ function emit(d) {
|
|
|
468
483
|
// Every knob is read on every event, so a bad value screams whichever rendering is on.
|
|
469
484
|
const format = currentFormat();
|
|
470
485
|
const clock = timeEnabled();
|
|
486
|
+
const target = shipTarget();
|
|
471
487
|
const text = format === "json" ? json(d, now) : human(d, now, clock);
|
|
472
488
|
// Node: everything to stderr through console.error, which cli/live patches
|
|
473
489
|
// to keep lines above an active region. Elsewhere the level's method, so
|
|
@@ -476,6 +492,173 @@ function emit(d) {
|
|
|
476
492
|
console.error(text);
|
|
477
493
|
else
|
|
478
494
|
console[CONSOLE_METHOD[VERBS[d.verb].level]](text);
|
|
495
|
+
// What ships is the stream's json line, whatever the console renders.
|
|
496
|
+
if (target !== undefined)
|
|
497
|
+
ship(target, format === "json" ? text : json(d, now));
|
|
498
|
+
}
|
|
499
|
+
// --- shipping ----------------------------------------------------------------
|
|
500
|
+
/** A batch's ceiling in UTF-8 bytes: navigator.sendBeacon's. */
|
|
501
|
+
const SHIP_MAX_BYTES = 64 * 1024;
|
|
502
|
+
const SHIP_MAX_EVENTS = 50;
|
|
503
|
+
const SHIP_AFTER_MS = 2000;
|
|
504
|
+
let shipSetting;
|
|
505
|
+
let shipTokenSetting;
|
|
506
|
+
function parseShip(raw, source) {
|
|
507
|
+
if (!URL.canParse(raw))
|
|
508
|
+
panic(`unknown ${source}:`, raw, "(want a URL)");
|
|
509
|
+
const url = new URL(raw);
|
|
510
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
511
|
+
panic(`unknown ${source}:`, raw, "(want an http(s) URL)");
|
|
512
|
+
return url.href;
|
|
513
|
+
}
|
|
514
|
+
/** Where events are also POSTed, in batches. LOG_SHIP overrides it. */
|
|
515
|
+
export function setLogShip(url) {
|
|
516
|
+
shipSetting = parseShip(url, "log ship");
|
|
517
|
+
}
|
|
518
|
+
/** The bearer sent with each batch (servers only). LOG_SHIP_TOKEN overrides it. */
|
|
519
|
+
export function setLogShipToken(token) {
|
|
520
|
+
assert(sink !== "browser", "a browser ships without a token: any token in a page is public");
|
|
521
|
+
if (typeof token !== "string" || token.length === 0)
|
|
522
|
+
panic("setLogShipToken wants a non-empty token, got:", token);
|
|
523
|
+
shipTokenSetting = token;
|
|
524
|
+
}
|
|
525
|
+
// LOG_SHIP is read live like every variable; the parse is kept per value.
|
|
526
|
+
let shipParsed;
|
|
527
|
+
function shipTarget() {
|
|
528
|
+
const fromEnv = env("LOG_SHIP");
|
|
529
|
+
const raw = fromEnv ?? shipSetting;
|
|
530
|
+
if (raw === undefined)
|
|
531
|
+
return undefined;
|
|
532
|
+
if (shipParsed?.raw !== raw)
|
|
533
|
+
shipParsed = {
|
|
534
|
+
raw,
|
|
535
|
+
href: fromEnv === undefined ? raw : parseShip(raw, "LOG_SHIP"),
|
|
536
|
+
};
|
|
537
|
+
return shipParsed.href;
|
|
538
|
+
}
|
|
539
|
+
const shipLog = () => log.scope("ship");
|
|
540
|
+
const utf8 = new TextEncoder();
|
|
541
|
+
let batch;
|
|
542
|
+
let batchTimer;
|
|
543
|
+
const inflight = new Set();
|
|
544
|
+
let exitHooked = false;
|
|
545
|
+
function ship(target, text) {
|
|
546
|
+
hookExit();
|
|
547
|
+
const bytes = utf8.encode(text).length;
|
|
548
|
+
if (bytes > SHIP_MAX_BYTES) {
|
|
549
|
+
shipLog().warnOnce("event too large to ship", {
|
|
550
|
+
bytes,
|
|
551
|
+
limit: SHIP_MAX_BYTES,
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (batch !== undefined &&
|
|
556
|
+
(batch.target !== target || batch.bytes + 1 + bytes > SHIP_MAX_BYTES))
|
|
557
|
+
send();
|
|
558
|
+
if (batch === undefined) {
|
|
559
|
+
batch = { target, lines: [], bytes: -1 };
|
|
560
|
+
// A pending batch never keeps a process alive: beforeExit sends it.
|
|
561
|
+
const timer = setTimeout(send, SHIP_AFTER_MS);
|
|
562
|
+
timer.unref?.();
|
|
563
|
+
batchTimer = timer;
|
|
564
|
+
}
|
|
565
|
+
batch.lines.push(text);
|
|
566
|
+
batch.bytes += 1 + bytes;
|
|
567
|
+
if (batch.lines.length >= SHIP_MAX_EVENTS)
|
|
568
|
+
send();
|
|
569
|
+
}
|
|
570
|
+
/** Take the waiting batch, if any, and stop its timer. */
|
|
571
|
+
function takeBatch() {
|
|
572
|
+
if (batchTimer !== undefined)
|
|
573
|
+
clearTimeout(batchTimer);
|
|
574
|
+
batchTimer = undefined;
|
|
575
|
+
const taken = batch;
|
|
576
|
+
batch = undefined;
|
|
577
|
+
return taken;
|
|
578
|
+
}
|
|
579
|
+
function send() {
|
|
580
|
+
// A hidden page may be gone before a fetch answers: the beacon outlives it.
|
|
581
|
+
if (sink === "browser" &&
|
|
582
|
+
globalThis.document.visibilityState ===
|
|
583
|
+
"hidden") {
|
|
584
|
+
beacon();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const b = takeBatch();
|
|
588
|
+
if (b === undefined)
|
|
589
|
+
return;
|
|
590
|
+
const p = post(b.target, b.lines.join("\n")).finally(() => inflight.delete(p));
|
|
591
|
+
inflight.add(p);
|
|
592
|
+
}
|
|
593
|
+
async function post(target, body) {
|
|
594
|
+
const token = sink === "browser"
|
|
595
|
+
? undefined
|
|
596
|
+
: (env("LOG_SHIP_TOKEN") ?? shipTokenSetting);
|
|
597
|
+
const headers = {
|
|
598
|
+
"content-type": "text/plain;charset=utf-8",
|
|
599
|
+
};
|
|
600
|
+
if (token !== undefined)
|
|
601
|
+
headers.authorization = `Bearer ${token}`;
|
|
602
|
+
try {
|
|
603
|
+
const res = await fetch(target, {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers,
|
|
606
|
+
body,
|
|
607
|
+
keepalive: sink === "browser",
|
|
608
|
+
});
|
|
609
|
+
if (res.status === 204)
|
|
610
|
+
return;
|
|
611
|
+
shipLog().warnOnce("log batch refused", {
|
|
612
|
+
status: res.status,
|
|
613
|
+
answer: (await res.text()).slice(0, 1000),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
shipLog().warnOnce("log batch not delivered", { err });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Hidden is the last moment a page reliably sends anything; sendBeacon is the
|
|
621
|
+
// one request the browser finishes after the page is gone.
|
|
622
|
+
function beacon() {
|
|
623
|
+
const b = takeBatch();
|
|
624
|
+
if (b === undefined)
|
|
625
|
+
return;
|
|
626
|
+
const page = globalThis;
|
|
627
|
+
if (!page.navigator.sendBeacon(b.target, b.lines.join("\n")))
|
|
628
|
+
shipLog().warnOnce("log batch refused by the browser", {
|
|
629
|
+
events: b.lines.length,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// Hooked on the first shipped event, so importing the logger has no effect.
|
|
633
|
+
function hookExit() {
|
|
634
|
+
if (exitHooked)
|
|
635
|
+
return;
|
|
636
|
+
exitHooked = true;
|
|
637
|
+
if (runtime.isNode || runtime.isBun) {
|
|
638
|
+
process.on("beforeExit", () => send());
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (sink !== "browser")
|
|
642
|
+
return;
|
|
643
|
+
const page = globalThis;
|
|
644
|
+
page.document.addEventListener("visibilitychange", () => {
|
|
645
|
+
if (page.document.visibilityState === "hidden")
|
|
646
|
+
beacon();
|
|
647
|
+
});
|
|
648
|
+
page.addEventListener("pagehide", beacon);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Send what waits to LOG_SHIP and resolve when no batch is in flight. A
|
|
652
|
+
* Worker hands it to waitUntil; a process that calls process.exit() awaits it
|
|
653
|
+
* first. Never rejects: a lost batch is a warnOnce.
|
|
654
|
+
*/
|
|
655
|
+
export async function flushLog() {
|
|
656
|
+
send();
|
|
657
|
+
while (inflight.size > 0) {
|
|
658
|
+
await Promise.all(inflight);
|
|
659
|
+
// A refused batch's warning is an event too: it ships next.
|
|
660
|
+
send();
|
|
661
|
+
}
|
|
479
662
|
}
|
|
480
663
|
const BOUND = 1024;
|
|
481
664
|
const warned = new Set();
|
|
@@ -531,6 +714,52 @@ function open(ctx, name, initial) {
|
|
|
531
714
|
parent,
|
|
532
715
|
fields,
|
|
533
716
|
});
|
|
717
|
+
function finish(err) {
|
|
718
|
+
const dur = performance.now() - began;
|
|
719
|
+
const failed = err !== undefined && err !== null;
|
|
720
|
+
const printed = err instanceof Error && caught.has(err) ? err : undefined;
|
|
721
|
+
// What spans inside this one printed stays printed for the spans around it.
|
|
722
|
+
for (const e of caught)
|
|
723
|
+
ctx.caught?.add(e);
|
|
724
|
+
if (!shown) {
|
|
725
|
+
// The end follows the decision made at begin: no end without its
|
|
726
|
+
// start. A failure is never lost to it: one line of the nearest
|
|
727
|
+
// shown span says the span failed.
|
|
728
|
+
if (!failed || !admits(ctx.scope, "error"))
|
|
729
|
+
return;
|
|
730
|
+
if (err instanceof Error)
|
|
731
|
+
ctx.caught?.add(err);
|
|
732
|
+
emit({
|
|
733
|
+
verb: "fail",
|
|
734
|
+
msg: name,
|
|
735
|
+
scope: ctx.scope,
|
|
736
|
+
depth: inner.depth,
|
|
737
|
+
trace: inner.owner === undefined ? undefined : trace,
|
|
738
|
+
span: inner.owner,
|
|
739
|
+
fields: { ...fields, ...added, err, dur_ms: round3(dur) },
|
|
740
|
+
printed,
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (failed && err instanceof Error)
|
|
745
|
+
ctx.caught?.add(err);
|
|
746
|
+
const shows = failed ? { ...added, err } : added;
|
|
747
|
+
emit({
|
|
748
|
+
verb: failed ? "fail" : "success",
|
|
749
|
+
msg: name,
|
|
750
|
+
scope: ctx.scope,
|
|
751
|
+
depth: ctx.depth,
|
|
752
|
+
phase: "end",
|
|
753
|
+
trace,
|
|
754
|
+
span: id,
|
|
755
|
+
parent,
|
|
756
|
+
status: failed ? "error" : "ok",
|
|
757
|
+
dur,
|
|
758
|
+
fields: { ...fields, ...shows },
|
|
759
|
+
shows,
|
|
760
|
+
printed,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
534
763
|
// A hidden span is transparent: its lines and children attach to the nearest
|
|
535
764
|
// shown span, a joined remote one included (it is shown in its own process's
|
|
536
765
|
// stream). With none, its lines carry no ids, as lines outside any span.
|
|
@@ -565,50 +794,11 @@ function open(ctx, name, initial) {
|
|
|
565
794
|
end(err) {
|
|
566
795
|
assert(!ended, "span", name, "ended twice");
|
|
567
796
|
ended = true;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
ctx.caught?.add(e);
|
|
574
|
-
if (!shown) {
|
|
575
|
-
// The end follows the decision made at begin: no end without its
|
|
576
|
-
// start. A failure is never lost to it: one line of the nearest
|
|
577
|
-
// shown span says the span failed.
|
|
578
|
-
if (!failed || !admits(ctx.scope, "error"))
|
|
579
|
-
return;
|
|
580
|
-
if (err instanceof Error)
|
|
581
|
-
ctx.caught?.add(err);
|
|
582
|
-
emit({
|
|
583
|
-
verb: "fail",
|
|
584
|
-
msg: name,
|
|
585
|
-
scope: ctx.scope,
|
|
586
|
-
depth: inner.depth,
|
|
587
|
-
trace: inner.owner === undefined ? undefined : trace,
|
|
588
|
-
span: inner.owner,
|
|
589
|
-
fields: { ...fields, ...added, err, dur_ms: round3(dur) },
|
|
590
|
-
printed,
|
|
591
|
-
});
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (failed && err instanceof Error)
|
|
595
|
-
ctx.caught?.add(err);
|
|
596
|
-
const shows = failed ? { ...added, err } : added;
|
|
597
|
-
emit({
|
|
598
|
-
verb: failed ? "fail" : "success",
|
|
599
|
-
msg: name,
|
|
600
|
-
scope: ctx.scope,
|
|
601
|
-
depth: ctx.depth,
|
|
602
|
-
phase: "end",
|
|
603
|
-
trace,
|
|
604
|
-
span: id,
|
|
605
|
-
parent,
|
|
606
|
-
status: failed ? "error" : "ok",
|
|
607
|
-
dur,
|
|
608
|
-
fields: { ...fields, ...shows },
|
|
609
|
-
shows,
|
|
610
|
-
printed,
|
|
611
|
-
});
|
|
797
|
+
finish(err);
|
|
798
|
+
// A span opened outside any local span (a root, or a request joined
|
|
799
|
+
// from its caller) ends its trace here: what it wrote ships together.
|
|
800
|
+
if (ctx.caught === undefined)
|
|
801
|
+
send();
|
|
612
802
|
},
|
|
613
803
|
});
|
|
614
804
|
}
|