@astroscope/node 1.2.2 → 1.3.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 +20 -0
- package/dist/index.js +2 -2
- package/dist/log/index.d.ts +32 -1
- package/dist/log/index.d.ts.map +1 -1
- package/dist/log/index.js +3 -2
- package/dist/{log-CSJlKaxY.js → log-B69HEBvg.js} +2 -2
- package/dist/{log-CSJlKaxY.js.map → log-B69HEBvg.js.map} +1 -1
- package/dist/{native-mount-hhwWdLtL.js → native-mount-DjYEnO4X.js} +4 -13
- package/dist/native-mount-DjYEnO4X.js.map +1 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +1 -1
- package/dist/{prepare-DQEf2Bnt.js → prepare-CXZsyAVk.js} +6 -4
- package/dist/prepare-CXZsyAVk.js.map +1 -0
- package/dist/request-route-DcnZOOM4.js +92 -0
- package/dist/request-route-DcnZOOM4.js.map +1 -0
- package/dist/route-middleware-entrypoint.d.ts +3 -1
- package/dist/route-middleware-entrypoint.d.ts.map +1 -1
- package/dist/route-middleware-entrypoint.js +5 -11
- package/dist/route-middleware-entrypoint.js.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +1 -1
- package/dist/native-mount-hhwWdLtL.js.map +0 -1
- package/dist/prepare-DQEf2Bnt.js.map +0 -1
- package/dist/store-BIUF4lqk.js +0 -29
- package/dist/store-BIUF4lqk.js.map +0 -1
package/README.md
CHANGED
|
@@ -144,6 +144,26 @@ Entries logged before the logger is constructed (env loading, config, instrument
|
|
|
144
144
|
|
|
145
145
|
Request logging happens at the native handler on `finish`/`close`: real status code, response size, `ttfb`, aborted-vs-completed, and the route pattern (fed back by an internal astro middleware). An incoming `x-request-id` header is passed through (and echoed on the response); otherwise a short id is generated.
|
|
146
146
|
|
|
147
|
+
### Reporting the route yourself
|
|
148
|
+
|
|
149
|
+
The route comes from the route astro matched, which is wrong for a middleware that serves a request astro has no page for — one that rewrites via `next(url)`, or answers itself. Astro matches `/404` there, so the request is logged as `/404` despite its `200`, and every such request collapses into one `/404` bucket in the request duration metric. `overrideRequestRoute` lets the middleware report what it actually served:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { overrideRequestRoute } from '@astroscope/node/log';
|
|
153
|
+
|
|
154
|
+
export const onRequest: MiddlewareHandler = (ctx, next) => {
|
|
155
|
+
const page = lookupPage(ctx.url.pathname);
|
|
156
|
+
|
|
157
|
+
if (!page) return next();
|
|
158
|
+
|
|
159
|
+
overrideRequestRoute('/cms/pages/[id]');
|
|
160
|
+
|
|
161
|
+
return next(`/cms/pages/${page.id}`);
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This fixes the log line, the metric and the server span name together. Pass a templated label, not a concrete path, so metric cardinality stays bounded. An overridden route always wins over the one astro matched, regardless of middleware order.
|
|
166
|
+
|
|
147
167
|
The adapter itself emits exactly one info line on startup — `server ready { host, port, health, bootMs, warmupMs, totalMs }` — and `draining` / `shutdown complete { drainMs }` on the way down. Everything else is debug or error level.
|
|
148
168
|
|
|
149
169
|
## Telemetry
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as setBootContext } from "./context-Bkg-FnnQ.js";
|
|
2
|
-
import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t as preparePlatform } from "./prepare-
|
|
3
|
-
import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-
|
|
2
|
+
import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t as preparePlatform } from "./prepare-CXZsyAVk.js";
|
|
3
|
+
import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-DjYEnO4X.js";
|
|
4
4
|
import { n as getCurrentGeneration, r as incrementGeneration, t as GEN_HEADER } from "./generation-Bp2IA0jf.js";
|
|
5
5
|
import { r as RECOMMENDED_EXCLUDES } from "./excludes-pE23EbmQ.js";
|
|
6
6
|
import fs, { readFileSync } from "node:fs";
|
package/dist/log/index.d.ts
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import { t as LoggerOptionsFactory } from "../construct-DgB-jR0a.js";
|
|
2
2
|
import { Bindings, Bindings as Bindings$1, Logger, Logger as Logger$1, LoggerOptions } from "pino";
|
|
3
3
|
|
|
4
|
+
//#region src/observability/request-route.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Report the route that actually served the current request, overriding the one
|
|
7
|
+
* astro's routing matched.
|
|
8
|
+
*
|
|
9
|
+
* A middleware that rewrites (`next(url)`) or answers with its own response
|
|
10
|
+
* serves a request astro has no page for, so routing matches `/404` and that is
|
|
11
|
+
* what the request is logged and measured as — every such request collapsing
|
|
12
|
+
* into one `/404` bucket in the request metrics. Calling this corrects the log
|
|
13
|
+
* line, the metric and the server span name together.
|
|
14
|
+
*
|
|
15
|
+
* Pass a templated label rather than a concrete path, so metric cardinality
|
|
16
|
+
* stays bounded. No-op outside instrumented requests.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { overrideRequestRoute } from '@astroscope/node/log';
|
|
21
|
+
*
|
|
22
|
+
* export const onRequest: MiddlewareHandler = (ctx, next) => {
|
|
23
|
+
* const page = lookupPage(ctx.url.pathname);
|
|
24
|
+
*
|
|
25
|
+
* if (!page) return next();
|
|
26
|
+
*
|
|
27
|
+
* overrideRequestRoute('/cms/pages/[id]');
|
|
28
|
+
*
|
|
29
|
+
* return next(`/cms/pages/${page.id}`);
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function overrideRequestRoute(route: string): void;
|
|
34
|
+
//#endregion
|
|
4
35
|
//#region src/observability/log/index.d.ts
|
|
5
36
|
/**
|
|
6
37
|
* Generate a short request ID.
|
|
@@ -51,5 +82,5 @@ interface LogProxy {
|
|
|
51
82
|
*/
|
|
52
83
|
declare const log: LogProxy;
|
|
53
84
|
//#endregion
|
|
54
|
-
export { type Bindings, LogProxy, type Logger, type LoggerOptions, type LoggerOptionsFactory, generateReqId, log };
|
|
85
|
+
export { type Bindings, LogProxy, type Logger, type LoggerOptions, type LoggerOptionsFactory, generateReqId, log, overrideRequestRoute };
|
|
55
86
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/log/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/observability/log/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/observability/request-route.ts","../../src/observability/log/index.ts"],"mappings":";;;;;;;;AA+DA;;;;AAAkD;;;;AC5ClD;;;;AAA6B;AAiB7B;;;;;;;;;;;iBD2BgB,oBAAA,CAAqB,KAAa;;;;;;AAAlD;iBC5CgB,aAAA;;;AD4CkC;UC3BjC,QAAA;;WAEN,KAAA,EAAO,QAAA;EAnBF;EAAA,SAqBL,KAAA,EAAO,QAAA;;WAEP,IAAA,EAAM,QAAA;EAvBY;EAAA,SAyBlB,IAAA,EAAM,QAAA;EARQ;EAAA,SAUd,KAAA,EAAO,QAAA;EARA;EAAA,SAUP,KAAA,EAAO,QAAA;EAND;EAQf,KAAA,CAAM,QAAA,EAAU,UAAA,GAAW,QAAA;EAJX;EAAA,SAMP,GAAA,EAAK,QAAA;EAFE;EAAA,SAIP,IAAA,EAAM,QAAA;AAAA;;;;;;;;;;;;;;;;;;;;cAgGJ,GAAA,EAAK,QAA6B"}
|
package/dist/log/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { t as overrideRequestRoute } from "../request-route-DcnZOOM4.js";
|
|
2
|
+
import { n as log, t as generateReqId } from "../log-B69HEBvg.js";
|
|
3
|
+
export { generateReqId, log, overrideRequestRoute };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as getLogStore } from "./request-route-DcnZOOM4.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import pino from "pino";
|
|
4
4
|
//#region src/observability/log/index.ts
|
|
@@ -109,4 +109,4 @@ const log = createLogProxy([]);
|
|
|
109
109
|
//#endregion
|
|
110
110
|
export { log as n, generateReqId as t };
|
|
111
111
|
|
|
112
|
-
//# sourceMappingURL=log-
|
|
112
|
+
//# sourceMappingURL=log-B69HEBvg.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-
|
|
1
|
+
{"version":3,"file":"log-B69HEBvg.js","names":[],"sources":["../src/observability/log/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport pino, { type Bindings, type Logger } from 'pino';\nimport { type BufferedEntry, EARLY_LOG_BUFFER_CAP, type LogStore, getLogStore } from './store.js';\n\nlet fallbackLogger: Logger | undefined;\n\n/**\n * Get the current request logger, root logger, or a plain fallback instance\n * when the root logger hasn't been constructed yet.\n * @internal\n */\nfunction contextLogger(store: LogStore): Logger {\n return store.requestStorage.getStore()?.logger ?? store.root ?? (fallbackLogger ??= pino({ level: 'info' }));\n}\n\n/**\n * Generate a short request ID.\n * @internal\n */\nexport function generateReqId(): string {\n return randomUUID().slice(0, 8);\n}\n\nfunction bufferEntry(store: LogStore, level: BufferedEntry['level'], bindings: Bindings[], args: unknown[]): void {\n if (store.buffer.length >= EARLY_LOG_BUFFER_CAP) {\n store.dropped += 1;\n\n return;\n }\n\n store.buffer.push({ level, bindings, args, time: Date.now() });\n}\n\n/**\n * Log proxy interface — context-aware logging via getters.\n */\nexport interface LogProxy {\n /** Log at trace level */\n readonly trace: Logger['trace'];\n /** Log at debug level */\n readonly debug: Logger['debug'];\n /** Log at info level */\n readonly info: Logger['info'];\n /** Log at warn level */\n readonly warn: Logger['warn'];\n /** Log at error level */\n readonly error: Logger['error'];\n /** Log at fatal level */\n readonly fatal: Logger['fatal'];\n /** Create a child logger with additional bindings */\n child(bindings: Bindings): LogProxy;\n /** Access the current context's raw pino Logger */\n readonly raw: Logger;\n /** Access the root logger (no request context) */\n readonly root: Logger;\n}\n\n/**\n * Create a log proxy carrying accumulated child bindings. Before the root\n * logger is constructed, entries are buffered (with their bindings) and\n * replayed through the real logger on construction.\n */\nfunction createLogProxy(bindings: Bindings[]): LogProxy {\n const store = getLogStore();\n\n // cache the derived child against the logger it was derived from, so a\n // proxy created before construction transparently rebinds afterwards\n let cachedBase: Logger | undefined;\n let cachedChild: Logger | undefined;\n\n const resolve = (): Logger => {\n const base = contextLogger(store);\n\n if (!bindings.length) return base;\n\n if (cachedBase !== base) {\n cachedBase = base;\n cachedChild = base.child(Object.assign({}, ...bindings) as Bindings);\n }\n\n return cachedChild!;\n };\n\n const method = (level: BufferedEntry['level']) => {\n if (!store.root) {\n const record = store.requestStorage.getStore();\n\n // request-scoped loggers exist only after construction; buffer otherwise\n if (!record?.logger) {\n return (...args: unknown[]) => bufferEntry(store, level, bindings, args);\n }\n }\n\n const logger = resolve();\n\n return logger[level].bind(logger);\n };\n\n return {\n get trace() {\n return method('trace') as Logger['trace'];\n },\n get debug() {\n return method('debug') as Logger['debug'];\n },\n get info() {\n return method('info') as Logger['info'];\n },\n get warn() {\n return method('warn') as Logger['warn'];\n },\n get error() {\n return method('error') as Logger['error'];\n },\n get fatal() {\n return method('fatal') as Logger['fatal'];\n },\n child(childBindings: Bindings): LogProxy {\n return createLogProxy([...bindings, childBindings]);\n },\n get raw() {\n return resolve();\n },\n get root() {\n const base = getLogStore().root ?? (fallbackLogger ??= pino({ level: 'info' }));\n\n return bindings.length ? base.child(Object.assign({}, ...bindings) as Bindings) : base;\n },\n };\n}\n\n/**\n * Context-aware logger. Inside a request, entries carry the request bindings\n * (`reqId`, `req`); outside they go to the root logger. Entries logged before\n * the root logger is constructed (env loading, config, instrumentation) are\n * buffered and replayed once construction completes — the original timestamp\n * is kept as a `bufferedTime` field.\n *\n * @example\n * ```ts\n * import { log } from '@astroscope/node/log';\n *\n * log.info('handling request');\n * log.info({ userId: 123 }, 'user logged in');\n * log.error(err, 'operation failed');\n *\n * const dbLog = log.child({ component: 'db' });\n * dbLog.debug('executing query');\n * ```\n */\nexport const log: LogProxy = createLogProxy([]);\n\n// public surface for middleware that serves a route astro never matched\nexport { overrideRequestRoute } from '../request-route.js';\n\n// re-exported so apps don't need a direct pino dependency for typing\nexport type { Logger, LoggerOptions, Bindings } from 'pino';\n\n// contract of the src/log.ts entry seam\nexport type { LoggerOptionsFactory } from './construct.js';\n"],"mappings":";;;;AAIA,IAAI;;;;;;AAOJ,SAAS,cAAc,OAAyB;CAC9C,OAAO,MAAM,eAAe,SAAS,CAAC,EAAE,UAAU,MAAM,SAAS,mBAAmB,KAAK,EAAE,OAAO,OAAO,CAAC;AAC5G;;;;;AAMA,SAAgB,gBAAwB;CACtC,OAAO,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;AAChC;AAEA,SAAS,YAAY,OAAiB,OAA+B,UAAsB,MAAuB;CAChH,IAAI,MAAM,OAAO,UAAA,KAAgC;EAC/C,MAAM,WAAW;EAEjB;CACF;CAEA,MAAM,OAAO,KAAK;EAAE;EAAO;EAAU;EAAM,MAAM,KAAK,IAAI;CAAE,CAAC;AAC/D;;;;;;AA+BA,SAAS,eAAe,UAAgC;CACtD,MAAM,QAAQ,YAAY;CAI1B,IAAI;CACJ,IAAI;CAEJ,MAAM,gBAAwB;EAC5B,MAAM,OAAO,cAAc,KAAK;EAEhC,IAAI,CAAC,SAAS,QAAQ,OAAO;EAE7B,IAAI,eAAe,MAAM;GACvB,aAAa;GACb,cAAc,KAAK,MAAM,OAAO,OAAO,CAAC,GAAG,GAAG,QAAQ,CAAa;EACrE;EAEA,OAAO;CACT;CAEA,MAAM,UAAU,UAAkC;EAChD,IAAI,CAAC,MAAM;OAIL,CAHW,MAAM,eAAe,SAG1B,CAAC,EAAE,QACX,QAAQ,GAAG,SAAoB,YAAY,OAAO,OAAO,UAAU,IAAI;EAAA;EAI3E,MAAM,SAAS,QAAQ;EAEvB,OAAO,OAAO,MAAM,CAAC,KAAK,MAAM;CAClC;CAEA,OAAO;EACL,IAAI,QAAQ;GACV,OAAO,OAAO,OAAO;EACvB;EACA,IAAI,QAAQ;GACV,OAAO,OAAO,OAAO;EACvB;EACA,IAAI,OAAO;GACT,OAAO,OAAO,MAAM;EACtB;EACA,IAAI,OAAO;GACT,OAAO,OAAO,MAAM;EACtB;EACA,IAAI,QAAQ;GACV,OAAO,OAAO,OAAO;EACvB;EACA,IAAI,QAAQ;GACV,OAAO,OAAO,OAAO;EACvB;EACA,MAAM,eAAmC;GACvC,OAAO,eAAe,CAAC,GAAG,UAAU,aAAa,CAAC;EACpD;EACA,IAAI,MAAM;GACR,OAAO,QAAQ;EACjB;EACA,IAAI,OAAO;GACT,MAAM,OAAO,YAAY,CAAC,CAAC,SAAS,mBAAmB,KAAK,EAAE,OAAO,OAAO,CAAC;GAE7E,OAAO,SAAS,SAAS,KAAK,MAAM,OAAO,OAAO,CAAC,GAAG,GAAG,QAAQ,CAAa,IAAI;EACpF;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,MAAgB,eAAe,CAAC,CAAC"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as log } from "./log-
|
|
3
|
-
import { trace } from "@opentelemetry/api";
|
|
1
|
+
import { t as overrideRequestRoute } from "./request-route-DcnZOOM4.js";
|
|
2
|
+
import { n as log } from "./log-B69HEBvg.js";
|
|
4
3
|
//#region src/server/native-mount.ts
|
|
5
4
|
/**
|
|
6
5
|
* Native mounts: raw `(req, res)` handlers dispatched before static/astro,
|
|
@@ -93,15 +92,7 @@ function failResponse(res) {
|
|
|
93
92
|
function dispatchNativeMount(req, res) {
|
|
94
93
|
const mount = findMount(req);
|
|
95
94
|
if (!mount) return false;
|
|
96
|
-
if (mount.name)
|
|
97
|
-
const record = getRequestRecord();
|
|
98
|
-
if (record && !record.route) record.route = mount.name;
|
|
99
|
-
const span = trace.getActiveSpan();
|
|
100
|
-
if (span?.isRecording()) {
|
|
101
|
-
span.setAttribute("http.route", mount.name);
|
|
102
|
-
span.updateName(`${req.method ?? "GET"} ${mount.name}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
95
|
+
if (mount.name) overrideRequestRoute(mount.name);
|
|
105
96
|
try {
|
|
106
97
|
const result = mount.handler(req, res);
|
|
107
98
|
if (result instanceof Promise) result.catch((err) => {
|
|
@@ -117,4 +108,4 @@ function dispatchNativeMount(req, res) {
|
|
|
117
108
|
//#endregion
|
|
118
109
|
export { dispatchNativeMount as n, mountNativeHandler as r, clearNativeMounts as t };
|
|
119
110
|
|
|
120
|
-
//# sourceMappingURL=native-mount-
|
|
111
|
+
//# sourceMappingURL=native-mount-DjYEnO4X.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-mount-DjYEnO4X.js","names":[],"sources":["../src/server/native-mount.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { log } from '../observability/log/index.js';\nimport { overrideRequestRoute } from '../observability/request-route.js';\n\n/**\n * Native mounts: raw `(req, res)` handlers dispatched before static/astro,\n * for Node libraries that need the real request and response (e.g.\n * `oidc-provider`'s `callback()`). Mounted requests bypass astro middleware\n * entirely but stay inside request logging and tracing.\n *\n * Keyed on `globalThis` because registration (boot file, vite runner in dev)\n * and dispatch (server runtime) may live in different module instances.\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/native-mounts');\n\nexport type NativeHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n\nexport interface NativeMountMatcher {\n /** match requests whose pathname starts with this prefix */\n prefix?: string | undefined;\n\n /** predicate over the native request; evaluated when no prefix mount matches */\n match?: ((req: IncomingMessage) => boolean) | undefined;\n\n /** route label for logs, metrics and span names; defaults to the prefix */\n name?: string | undefined;\n}\n\ninterface Mount {\n prefix: string | undefined;\n match: ((req: IncomingMessage) => boolean) | undefined;\n name: string | undefined;\n handler: NativeHandler;\n}\n\ninterface Store {\n mounts: Mount[];\n}\n\nfunction getStore(): Store {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as Store | undefined;\n\n if (!store) {\n store = { mounts: [] };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nfunction matchesPrefix(pathname: string, prefix: string): boolean {\n if (!pathname.startsWith(prefix)) return false;\n\n const rest = pathname.slice(prefix.length);\n\n return rest === '' || rest.startsWith('/') || rest.startsWith('?');\n}\n\n/**\n * Mount a native `(req, res)` handler on the adapter's server. The handler\n * owns the response completely — matched requests never reach astro\n * middleware or rendering. Dispatch happens before static file serving, in\n * production and dev alike.\n *\n * Call from `onStartup`. Returns an unregister function; mounts still\n * registered after `onShutdown` are removed automatically.\n *\n * When several prefix mounts match, the longest prefix wins; predicate\n * mounts are consulted afterwards in registration order.\n *\n * @example\n * ```ts\n * // src/boot.ts\n * import { mountNativeHandler } from '@astroscope/node/native';\n *\n * export function onStartup() {\n * mountNativeHandler({ prefix: '/oidc', name: 'oidc' }, getOidcProvider().callback());\n * }\n * ```\n */\nexport function mountNativeHandler(matcher: NativeMountMatcher, handler: NativeHandler): () => void {\n if (!matcher.prefix && !matcher.match) {\n throw new Error('[@astroscope/node] mountNativeHandler requires a prefix or a match predicate');\n }\n\n const mount: Mount = {\n prefix: matcher.prefix,\n match: matcher.match,\n name: matcher.name ?? matcher.prefix,\n handler,\n };\n\n const store = getStore();\n\n store.mounts.push(mount);\n\n return () => {\n const index = store.mounts.indexOf(mount);\n\n if (index !== -1) store.mounts.splice(index, 1);\n };\n}\n\n/**\n * Remove every registered mount. Runs after `onShutdown` (prod) and between\n * dev generations, so re-running `onStartup` never stacks duplicates.\n */\nexport function clearNativeMounts(): void {\n getStore().mounts.length = 0;\n}\n\nfunction findMount(req: IncomingMessage): Mount | undefined {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n\n let best: Mount | undefined;\n\n for (const mount of getStore().mounts) {\n if (mount.prefix && matchesPrefix(pathname, mount.prefix)) {\n if (!best?.prefix || mount.prefix.length > best.prefix.length) {\n best = mount;\n }\n }\n }\n\n if (best) return best;\n\n return getStore().mounts.find((mount) => mount.match?.(req));\n}\n\nfunction failResponse(res: ServerResponse): void {\n if (res.writableEnded) return;\n\n if (!res.headersSent) {\n res.writeHead(500, { 'content-type': 'text/plain' });\n }\n\n res.end('Internal Server Error');\n}\n\n/**\n * Dispatch a request to a matching mount. Returns `false` when no mount\n * matches — the caller continues with static/astro handling.\n */\nexport function dispatchNativeMount(req: IncomingMessage, res: ServerResponse): boolean {\n const mount = findMount(req);\n\n if (!mount) return false;\n\n // the mount, not astro's routing, is what serves this request\n if (mount.name) {\n overrideRequestRoute(mount.name);\n }\n\n try {\n const result = mount.handler(req, res);\n\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n });\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,YAAY,OAAO,IAAI,gCAAgC;AA0B7D,SAAS,WAAkB;CACzB,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ,EAAE,QAAQ,CAAC,EAAE;EACrB,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAS,cAAc,UAAkB,QAAyB;CAChE,IAAI,CAAC,SAAS,WAAW,MAAM,GAAG,OAAO;CAEzC,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM;CAEzC,OAAO,SAAS,MAAM,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG;AACnE;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBAAmB,SAA6B,SAAoC;CAClG,IAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,OAC9B,MAAM,IAAI,MAAM,8EAA8E;CAGhG,MAAM,QAAe;EACnB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,MAAM,QAAQ,QAAQ,QAAQ;EAC9B;CACF;CAEA,MAAM,QAAQ,SAAS;CAEvB,MAAM,OAAO,KAAK,KAAK;CAEvB,aAAa;EACX,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK;EAExC,IAAI,UAAU,IAAI,MAAM,OAAO,OAAO,OAAO,CAAC;CAChD;AACF;;;;;AAMA,SAAgB,oBAA0B;CACxC,SAAS,CAAC,CAAC,OAAO,SAAS;AAC7B;AAEA,SAAS,UAAU,KAAyC;CAC1D,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;CAElE,IAAI;CAEJ,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC,QAC7B,IAAI,MAAM,UAAU,cAAc,UAAU,MAAM,MAAM;MAClD,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,KAAK,OAAO,QACrD,OAAO;CAAA;CAKb,IAAI,MAAM,OAAO;CAEjB,OAAO,SAAS,CAAC,CAAC,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,CAAC;AAC7D;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,IAAI,eAAe;CAEvB,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CAGrD,IAAI,IAAI,uBAAuB;AACjC;;;;;AAMA,SAAgB,oBAAoB,KAAsB,KAA8B;CACtF,MAAM,QAAQ,UAAU,GAAG;CAE3B,IAAI,CAAC,OAAO,OAAO;CAGnB,IAAI,MAAM,MACR,qBAAqB,MAAM,IAAI;CAGjC,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;EAErC,IAAI,kBAAkB,SACpB,OAAO,OAAO,QAAiB;GAC7B,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;GACzF,aAAa,GAAG;EAClB,CAAC;CAEL,SAAS,KAAK;EACZ,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;EACzF,aAAa,GAAG;CAClB;CAEA,OAAO;AACT"}
|
package/dist/native.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","names":[],"sources":["../src/server/native-mount.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"native.d.ts","names":[],"sources":["../src/server/native-mount.ts"],"mappings":";;;KAgBY,aAAA,IAAiB,GAAA,EAAK,eAAA,EAAiB,GAAA,EAAK,cAAA,YAA0B,OAAA;AAAA,UAEjE,kBAAA;EAFL;EAIV,MAAA;;EAGA,KAAA,KAAU,GAAA,EAAK,eAAe;EAPwB;EAUtD,IAAA;AAAA;;;;;;;;AAVuF;AAEzF;;;;;;;;;;AAQM;AAwDN;;;iBAAgB,kBAAA,CAAmB,OAAA,EAAS,kBAAA,EAAoB,OAAA,EAAS,aAAa;;;;;iBA2BtE,iBAAA;AA3BsE;AA2BtF;;;AA3BsF,iBAiEtE,mBAAA,CAAoB,GAAA,EAAK,eAAA,EAAiB,GAAA,EAAK,cAAc"}
|
package/dist/native.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as dispatchNativeMount, r as mountNativeHandler, t as clearNativeMounts } from "./native-mount-
|
|
1
|
+
import { n as dispatchNativeMount, r as mountNativeHandler, t as clearNativeMounts } from "./native-mount-DjYEnO4X.js";
|
|
2
2
|
export { clearNativeMounts, dispatchNativeMount, mountNativeHandler };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as emit } from "./events-CgoM3Fvu.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as log, t as generateReqId } from "./log-
|
|
2
|
+
import { r as getLogStore } from "./request-route-DcnZOOM4.js";
|
|
3
|
+
import { n as log, t as generateReqId } from "./log-B69HEBvg.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
-
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
|
|
6
5
|
import pino from "pino";
|
|
6
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
|
|
7
7
|
import { createMatcher } from "@entwico/dash/match";
|
|
8
8
|
//#region src/lifecycle/lifecycle.ts
|
|
9
9
|
async function runStartup(boot, context) {
|
|
@@ -137,7 +137,9 @@ function createRequestInstrumentation(config) {
|
|
|
137
137
|
const record = {
|
|
138
138
|
logger: requestLogger,
|
|
139
139
|
url,
|
|
140
|
+
method,
|
|
140
141
|
route: void 0,
|
|
142
|
+
routeOverride: false,
|
|
141
143
|
actionName: isAction ? pathname.slice(10).replace(/\/$/, "") : void 0
|
|
142
144
|
};
|
|
143
145
|
let span;
|
|
@@ -406,4 +408,4 @@ async function preparePlatform(options) {
|
|
|
406
408
|
//#endregion
|
|
407
409
|
export { runShutdown as a, createRequestInstrumentation as i, shutdownTelemetry as n, runStartup as o, dumpEarlyLogs as r, preparePlatform as t };
|
|
408
410
|
|
|
409
|
-
//# sourceMappingURL=prepare-
|
|
411
|
+
//# sourceMappingURL=prepare-CXZsyAVk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepare-CXZsyAVk.js","names":["LIB_NAME"],"sources":["../src/lifecycle/lifecycle.ts","../src/observability/telemetry/metrics.ts","../src/observability/instrument.ts","../src/observability/log/construct.ts","../src/observability/telemetry/sdk.ts","../src/platform/env.ts","../src/platform/prepare.ts"],"sourcesContent":["import { emit } from './events.js';\nimport type { BootContext } from './types.js';\n\nexport interface BootModule {\n onStartup?: ((context: BootContext) => Promise<void> | void) | undefined;\n onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;\n}\n\nexport async function runStartup(boot: BootModule, context: BootContext): Promise<void> {\n await emit('beforeOnStartup', context);\n await boot.onStartup?.(context);\n await emit('afterOnStartup', context);\n}\n\nexport async function runShutdown(boot: BootModule, context: BootContext): Promise<void> {\n try {\n await emit('beforeOnShutdown', context);\n await boot.onShutdown?.(context);\n } finally {\n await emit('afterOnShutdown', context);\n }\n}\n","import { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\n/**\n * Record the start of an HTTP request. Returns a function to call when the\n * request ends. Route is unknown at the native-handler level, so active\n * requests carry only the method.\n */\nexport function recordHttpRequestStart(method: string): () => void {\n getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().add(-1, { 'http.request.method': method });\n };\n}\n\nexport function recordHttpRequestDuration(\n attributes: { method: string; route: string | undefined; status: number },\n durationMs: number,\n): void {\n getHttpRequestDuration().record(durationMs / 1000, {\n 'http.request.method': attributes.method,\n 'http.route': attributes.route ?? '',\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordActionDuration(attributes: { name: string; status: number }, durationMs: number): void {\n getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createMatcher } from '@entwico/dash/match';\nimport { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api';\nimport type { Logger } from 'pino';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { generateReqId } from './log/index.js';\nimport { type RequestRecord, getLogStore } from './log/store.js';\nimport { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } from './telemetry/metrics.js';\n\nconst LIB_NAME = '@astroscope/node';\nconst ACTIONS_PREFIX = '/_actions/';\nconst REQUEST_ID_PATTERN = /^[\\w.-]{1,64}$/;\n\nconst roundTime = (n: number) => Math.round(n * 100) / 100;\n\nexport interface RequestLoggingConfig {\n exclude: ExcludePattern[];\n extended: boolean;\n}\n\nexport interface RequestTelemetryConfig {\n exclude: ExcludePattern[];\n}\n\nexport interface RequestInstrumentationConfig {\n logging: RequestLoggingConfig | false;\n telemetry: RequestTelemetryConfig | false;\n}\n\nfunction getClientIp(req: IncomingMessage): string | undefined {\n const forwarded = req.headers['x-forwarded-for'];\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n\n return (\n first?.split(',')[0]?.trim() ??\n (req.headers['x-real-ip'] as string | undefined) ??\n (req.headers['cf-connecting-ip'] as string | undefined)\n );\n}\n\nfunction resolveReqId(req: IncomingMessage): string {\n const incoming = req.headers['x-request-id'];\n const value = Array.isArray(incoming) ? incoming[0] : incoming;\n\n return value && REQUEST_ID_PATTERN.test(value) ? value : generateReqId();\n}\n\nfunction chunkSize(chunk: unknown): number {\n if (chunk == null) return 0;\n if (ArrayBuffer.isView(chunk)) return chunk.byteLength;\n if (typeof chunk === 'string') return Buffer.byteLength(chunk);\n\n return 0;\n}\n\n/**\n * Wraps the native request/response with logging and telemetry: a request\n * logger in async context (real status, response size, aborted-vs-completed\n * on `finish`/`close`), a SERVER span with propagation extraction, and\n * request metrics. Both concerns honor their own exclude patterns; when both\n * are excluded the request passes through untouched.\n */\nexport function createRequestInstrumentation(config: RequestInstrumentationConfig) {\n const tracer = trace.getTracer(LIB_NAME);\n const store = getLogStore();\n const loggingExcluded = config.logging ? createMatcher(config.logging.exclude) : () => true;\n const telemetryExcluded = config.telemetry ? createMatcher(config.telemetry.exclude) : () => true;\n\n return (req: IncomingMessage, res: ServerResponse, inner: () => void): void => {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n const method = req.method ?? 'GET';\n\n const logging = config.logging && !loggingExcluded(pathname) ? config.logging : false;\n const telemetry = config.telemetry && !telemetryExcluded(pathname) ? config.telemetry : false;\n\n if (!logging && !telemetry) {\n inner();\n\n return;\n }\n\n const startTime = performance.now();\n const isAction = pathname.startsWith(ACTIONS_PREFIX);\n\n let requestLogger: Logger | undefined;\n\n if (logging && store.root) {\n const reqId = resolveReqId(req);\n const reqData: Record<string, unknown> = { method, url: pathname };\n\n // extended logging includes potentially sensitive data\n if (logging.extended) {\n reqData['query'] = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n reqData['headers'] = req.headers;\n reqData['remoteAddress'] = getClientIp(req) ?? req.socket.remoteAddress;\n }\n\n requestLogger = store.root.child({ reqId, req: reqData });\n\n res.setHeader('x-request-id', reqId);\n }\n\n const record: RequestRecord = {\n logger: requestLogger,\n url,\n method,\n route: undefined,\n routeOverride: false,\n actionName: isAction ? pathname.slice(ACTIONS_PREFIX.length).replace(/\\/$/, '') : undefined,\n };\n\n let span: ReturnType<typeof tracer.startSpan> | undefined;\n let firstByteSpan: ReturnType<typeof tracer.startSpan> | undefined;\n let endActiveRequest: (() => void) | undefined;\n\n if (telemetry) {\n const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);\n const contentLength = req.headers['content-length'];\n const clientIp = getClientIp(req);\n const host = req.headers['host'];\n\n span = tracer.startSpan(\n isAction ? `ACTION ${record.actionName}` : method,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.request.method': method,\n 'url.path': pathname,\n 'url.query': queryIndex === -1 ? '' : url.slice(queryIndex + 1),\n 'url.scheme': 'http',\n 'user_agent.original': req.headers['user-agent'] ?? '',\n ...(host && { 'server.address': host }),\n ...(contentLength && { 'http.request.body.size': parseInt(contentLength) }),\n ...(clientIp && { 'client.address': clientIp }),\n },\n },\n parentContext,\n );\n\n firstByteSpan = tracer.startSpan('response:first-byte', undefined, trace.setSpan(parentContext, span));\n\n endActiveRequest = recordHttpRequestStart(method);\n }\n\n let responseSize = 0;\n let firstByteTime: number | undefined;\n\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n\n const markFirstByte = (): void => {\n if (firstByteTime !== undefined) return;\n\n firstByteTime = performance.now();\n\n if (firstByteSpan) {\n firstByteSpan.setAttribute('http.response.status_code', res.statusCode);\n firstByteSpan.end();\n }\n };\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);\n }) as typeof res.write;\n\n res.end = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalEnd as (...args: unknown[]) => ServerResponse)(chunk, ...rest);\n }) as typeof res.end;\n\n let finalized = false;\n\n const finalize = (aborted: boolean): void => {\n if (finalized) return;\n\n finalized = true;\n\n const status = res.statusCode;\n const responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n\n requestLogger[level](\n {\n res: { statusCode: status },\n responseTime: roundTime(responseTime),\n ttfb,\n responseSize,\n ...(record.route && { route: record.route }),\n ...(aborted && { aborted: true }),\n },\n aborted ? 'request aborted' : 'request completed',\n );\n }\n\n if (firstByteSpan && firstByteTime === undefined) {\n firstByteSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'request aborted' });\n firstByteSpan.end();\n }\n\n if (span) {\n span.setAttribute('http.response.status_code', status);\n span.setAttribute('http.response.body.size', responseSize);\n span.setAttribute('ttfb', ttfb);\n\n if (aborted || status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: aborted ? 'request aborted' : `HTTP ${status}` });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n }\n\n if (telemetry) {\n endActiveRequest?.();\n recordHttpRequestDuration({ method, route: record.route, status }, responseTime);\n\n if (record.actionName) {\n recordActionDuration({ name: record.actionName, status }, responseTime);\n }\n }\n };\n\n res.once('finish', () => finalize(false));\n res.once('close', () => finalize(!res.writableFinished));\n\n const run = (): void => store.requestStorage.run(record, inner);\n\n if (span) {\n context.with(trace.setSpan(context.active(), span), run);\n } else {\n run();\n }\n };\n}\n","import { isSpanContextValid, trace } from '@opentelemetry/api';\nimport pino, { type Bindings, type Logger, type LoggerOptions } from 'pino';\nimport { getLogStore } from './store.js';\n\n/**\n * Contract of the `src/log.ts` entry seam: pino logger options, or a factory\n * producing them. Never a logger instance — the platform constructs the\n * logger itself (after instrumentation, so trace correlation works).\n */\nexport type LoggerOptionsFactory = LoggerOptions | ((ctx: { dev: boolean }) => LoggerOptions | Promise<LoggerOptions>);\n\n/**\n * Compose the user mixin (if any) with platform trace correlation: when a\n * span is active, every entry carries `trace_id` / `span_id` / `trace_flags`.\n */\nfunction composeMixin(userMixin: LoggerOptions['mixin']): NonNullable<LoggerOptions['mixin']> {\n return (mergeObject, level, logger) => {\n const user = userMixin ? userMixin(mergeObject, level, logger) : {};\n const spanContext = trace.getActiveSpan()?.spanContext();\n\n if (!spanContext || !isSpanContextValid(spanContext)) return user;\n\n return {\n ...user,\n trace_id: spanContext.traceId,\n span_id: spanContext.spanId,\n trace_flags: `0${spanContext.traceFlags.toString(16)}`,\n };\n };\n}\n\n/**\n * Construct the root logger from the app's options seam and replay any logs\n * buffered before construction (original timestamps kept as `bufferedTime`).\n * In dev this runs once per generation; the buffer only exists the first time.\n */\nexport async function constructRootLogger(\n factory: LoggerOptionsFactory | undefined,\n ctx: { dev: boolean },\n): Promise<Logger> {\n const store = getLogStore();\n const options = (typeof factory === 'function' ? await factory(ctx) : factory) ?? {};\n const root = pino({ level: 'info', ...options, mixin: composeMixin(options.mixin) });\n\n store.root = root;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? (Object.assign({}, ...entry.bindings) as Bindings) : {};\n const target = root.child({ ...bindings, bufferedTime: new Date(entry.time).toISOString() });\n\n (target[entry.level] as (...args: unknown[]) => void)(...entry.args);\n }\n\n if (store.dropped > 0) {\n root.warn({ dropped: store.dropped }, 'early log buffer overflowed, entries dropped');\n store.dropped = 0;\n }\n\n return root;\n}\n\n/**\n * Failure path for startups that die before the logger exists: dump the\n * buffered entries to the console so no phase is silent.\n */\nexport function dumpEarlyLogs(): void {\n const store = getLogStore();\n\n if (store.root) return;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? Object.assign({}, ...entry.bindings) : undefined;\n\n console.error(\n new Date(entry.time).toISOString(),\n entry.level.toUpperCase(),\n ...(bindings ? [bindings] : []),\n ...entry.args,\n );\n }\n\n if (store.dropped > 0) {\n console.error(`(${store.dropped} early log entries dropped)`);\n store.dropped = 0;\n }\n}\n","import { log } from '../log/index.js';\n\n/**\n * Platform-owned telemetry bundle: NodeSDK with undici (fetch) and node\n * runtime instrumentation, host metrics, and a Prometheus reader. Trace\n * exporters are driven by standard `OTEL_*` env vars; without any of them\n * traces stay off (no failing localhost OTLP exports).\n *\n * Guarded per process (dev restarts are in-process; a re-created NodeSDK\n * would double-register instrumentations and leak the Prometheus port).\n */\n\nconst TELEMETRY_KEY = Symbol.for('@astroscope/node/telemetry');\n\ninterface TelemetryHandle {\n shutdown: () => Promise<void>;\n}\n\nexport interface TelemetrySdkOptions {\n prometheus: { host?: string | undefined; port?: number | undefined } | false;\n}\n\nfunction getHandle(): TelemetryHandle | undefined {\n return (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY] as TelemetryHandle | undefined;\n}\n\nfunction defaultEnv(key: string, value: string): void {\n if (!process.env[key]) process.env[key] = value;\n}\n\nexport async function startTelemetry(options: TelemetrySdkOptions): Promise<void> {\n const g = globalThis as Record<symbol, unknown>;\n\n if (g[TELEMETRY_KEY]) return;\n\n if (process.env['OTEL_SDK_DISABLED'] === 'true') {\n log.debug('telemetry disabled via OTEL_SDK_DISABLED');\n\n return;\n }\n\n // without an explicitly configured exporter target, exporting traces to the\n // default localhost OTLP endpoint would fail on every flush\n if (!process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] && !process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']) {\n defaultEnv('OTEL_TRACES_EXPORTER', 'none');\n }\n\n defaultEnv('OTEL_METRICS_EXPORTER', 'none');\n defaultEnv('OTEL_LOGS_EXPORTER', 'none');\n\n const [\n { NodeSDK },\n { UndiciInstrumentation },\n { RuntimeNodeInstrumentation },\n { PrometheusExporter },\n { HostMetrics },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-undici'),\n import('@opentelemetry/instrumentation-runtime-node'),\n import('@opentelemetry/exporter-prometheus'),\n import('@opentelemetry/host-metrics'),\n ]);\n\n const prometheus = options.prometheus\n ? {\n host: process.env['OTEL_EXPORTER_PROMETHEUS_HOST'] ?? options.prometheus.host ?? '0.0.0.0',\n port: process.env['OTEL_EXPORTER_PROMETHEUS_PORT']\n ? Number(process.env['OTEL_EXPORTER_PROMETHEUS_PORT'])\n : (options.prometheus.port ?? 9464),\n }\n : false;\n\n const sdk = new NodeSDK({\n instrumentations: [new UndiciInstrumentation(), new RuntimeNodeInstrumentation()],\n ...(prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }),\n });\n\n sdk.start();\n\n const hostMetrics = new HostMetrics();\n\n hostMetrics.start();\n\n g[TELEMETRY_KEY] = {\n shutdown: () => sdk.shutdown(),\n } satisfies TelemetryHandle;\n\n if (prometheus) {\n log.debug({ host: prometheus.host, port: prometheus.port }, 'prometheus metrics listening');\n }\n}\n\n/**\n * Flush and shut the SDK down. Prod-only (dev keeps the SDK for the process\n * lifetime across generations).\n */\nexport async function shutdownTelemetry(): Promise<void> {\n const handle = getHandle();\n\n if (!handle) return;\n\n delete (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY];\n\n await handle.shutdown();\n}\n","import fs from 'node:fs';\nimport { log } from '../observability/log/index.js';\n\n/**\n * Platform env loading (position −1, before the config seam):\n * `CONFIG_PATH` → `./.env` → none. Existing process env vars win\n */\nexport function loadEnvFiles(): void {\n const configPath = process.env['CONFIG_PATH'];\n\n if (configPath) {\n process.loadEnvFile(configPath);\n\n log.debug({ path: configPath }, 'loaded env file from CONFIG_PATH');\n\n return;\n }\n\n if (fs.existsSync('.env')) {\n process.loadEnvFile('.env');\n log.debug({ path: '.env' }, 'loaded env file');\n\n return;\n }\n\n log.debug('no env file loaded');\n}\n","import { type LoggerOptionsFactory, constructRootLogger } from '../observability/log/construct.js';\nimport { type TelemetrySdkOptions, startTelemetry } from '../observability/telemetry/sdk.js';\nimport { loadEnvFiles } from './env.js';\n\nconst INSTRUMENTATION_KEY = Symbol.for('@astroscope/node/instrumentation');\n\nexport interface InstrumentationContext {\n dev: boolean;\n}\n\ninterface InstrumentationSeam {\n register?: ((ctx: InstrumentationContext) => void | Promise<void>) | undefined;\n}\n\ninterface LogSeam {\n default?: LoggerOptionsFactory | undefined;\n}\n\nexport interface PlatformSeams {\n /** `src/config.ts` — validation runs at import; a throw fails the startup */\n config?: (() => Promise<unknown>) | undefined;\n /** `src/instrumentation.ts` — extra instrumentation, once per process */\n instrumentation?: (() => Promise<InstrumentationSeam>) | undefined;\n /** `src/log.ts` — pino logger options (or a factory), never an instance */\n log?: (() => Promise<LogSeam>) | undefined;\n}\n\nexport interface PreparePlatformOptions {\n dev: boolean;\n telemetry: TelemetrySdkOptions | false;\n seams: PlatformSeams;\n}\n\n/**\n * The platform sequence in front of the boot lifecycle:\n * env → config → instrumentation (platform SDK + `register`, once per\n * process) → logger construction (after instrumentation, so entries carry\n * trace correlation). Prod runs it once in `startServer()`; dev re-runs it\n * per generation with the once-per-process parts guarded.\n */\nexport async function preparePlatform(options: PreparePlatformOptions): Promise<void> {\n loadEnvFiles();\n\n await options.seams.config?.();\n\n const g = globalThis as Record<symbol, unknown>;\n\n if (!g[INSTRUMENTATION_KEY]) {\n g[INSTRUMENTATION_KEY] = true;\n\n if (options.telemetry) {\n await startTelemetry(options.telemetry);\n }\n\n const instrumentation = await options.seams.instrumentation?.();\n\n await instrumentation?.register?.({ dev: options.dev });\n }\n\n const logSeam = await options.seams.log?.();\n\n await constructRootLogger(logSeam?.default, { dev: options.dev });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { trace } from "@opentelemetry/api";
|
|
3
|
+
//#region src/observability/log/store.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shared state between the public `log` proxy and the server runtime. Keyed on
|
|
6
|
+
* `globalThis` because the two sides may live in different module instances
|
|
7
|
+
* (bundled app vs vite module runner in dev).
|
|
8
|
+
*/
|
|
9
|
+
const STORE_KEY = Symbol.for("@astroscope/node/log");
|
|
10
|
+
function getLogStore() {
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
let store = g[STORE_KEY];
|
|
13
|
+
if (!store) {
|
|
14
|
+
store = {
|
|
15
|
+
root: void 0,
|
|
16
|
+
requestStorage: new AsyncLocalStorage(),
|
|
17
|
+
buffer: [],
|
|
18
|
+
dropped: 0
|
|
19
|
+
};
|
|
20
|
+
g[STORE_KEY] = store;
|
|
21
|
+
}
|
|
22
|
+
return store;
|
|
23
|
+
}
|
|
24
|
+
function getRequestRecord() {
|
|
25
|
+
return getLogStore().requestStorage.getStore();
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/observability/request-route.ts
|
|
29
|
+
/**
|
|
30
|
+
* Apply a route label to the request record (final log line, request duration
|
|
31
|
+
* metric) and the active server span.
|
|
32
|
+
*
|
|
33
|
+
* Last write wins — astro's routing stamps every pass, so a rewrite lands the
|
|
34
|
+
* route that rendered. An override comes from the middleware that knows what it
|
|
35
|
+
* served and outranks routing, whatever the middleware order.
|
|
36
|
+
*/
|
|
37
|
+
function applyRoute(route, method, override) {
|
|
38
|
+
const record = getRequestRecord();
|
|
39
|
+
if (record) {
|
|
40
|
+
if (!override && record.routeOverride) return;
|
|
41
|
+
record.route = route;
|
|
42
|
+
if (override) record.routeOverride = true;
|
|
43
|
+
}
|
|
44
|
+
const span = trace.getActiveSpan();
|
|
45
|
+
if (span?.isRecording()) {
|
|
46
|
+
span.setAttribute("http.route", route);
|
|
47
|
+
if (!record?.actionName) span.updateName(`${method} ${route}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Report the route that actually served the current request, overriding the one
|
|
52
|
+
* astro's routing matched.
|
|
53
|
+
*
|
|
54
|
+
* A middleware that rewrites (`next(url)`) or answers with its own response
|
|
55
|
+
* serves a request astro has no page for, so routing matches `/404` and that is
|
|
56
|
+
* what the request is logged and measured as — every such request collapsing
|
|
57
|
+
* into one `/404` bucket in the request metrics. Calling this corrects the log
|
|
58
|
+
* line, the metric and the server span name together.
|
|
59
|
+
*
|
|
60
|
+
* Pass a templated label rather than a concrete path, so metric cardinality
|
|
61
|
+
* stays bounded. No-op outside instrumented requests.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { overrideRequestRoute } from '@astroscope/node/log';
|
|
66
|
+
*
|
|
67
|
+
* export const onRequest: MiddlewareHandler = (ctx, next) => {
|
|
68
|
+
* const page = lookupPage(ctx.url.pathname);
|
|
69
|
+
*
|
|
70
|
+
* if (!page) return next();
|
|
71
|
+
*
|
|
72
|
+
* overrideRequestRoute('/cms/pages/[id]');
|
|
73
|
+
*
|
|
74
|
+
* return next(`/cms/pages/${page.id}`);
|
|
75
|
+
* };
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function overrideRequestRoute(route) {
|
|
79
|
+
applyRoute(route, getRequestRecord()?.method ?? "GET", true);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Stamp the route astro's routing matched, unless it was overridden by the
|
|
83
|
+
* middleware that served the request.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
function setRequestRoute(route, method) {
|
|
87
|
+
applyRoute(route, method, false);
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { getRequestRecord as i, setRequestRoute as n, getLogStore as r, overrideRequestRoute as t };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=request-route-DcnZOOM4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-route-DcnZOOM4.js","names":[],"sources":["../src/observability/log/store.ts","../src/observability/request-route.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Logger } from 'pino';\n\n/**\n * Shared state between the public `log` proxy and the server runtime. Keyed on\n * `globalThis` because the two sides may live in different module instances\n * (bundled app vs vite module runner in dev).\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/log');\n\nexport const EARLY_LOG_BUFFER_CAP = 100;\n\nexport interface BufferedEntry {\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';\n bindings: Record<string, unknown>[];\n args: unknown[];\n time: number;\n}\n\nexport interface RequestRecord {\n logger: Logger | undefined;\n url: string;\n method: string;\n route: string | undefined;\n routeOverride: boolean;\n actionName: string | undefined;\n}\n\nexport interface LogStore {\n root: Logger | undefined;\n requestStorage: AsyncLocalStorage<RequestRecord>;\n buffer: BufferedEntry[];\n dropped: number;\n}\n\nexport function getLogStore(): LogStore {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as LogStore | undefined;\n\n if (!store) {\n store = { root: undefined, requestStorage: new AsyncLocalStorage(), buffer: [], dropped: 0 };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nexport function getRequestRecord(): RequestRecord | undefined {\n return getLogStore().requestStorage.getStore();\n}\n","import { trace } from '@opentelemetry/api';\nimport { getRequestRecord } from './log/store.js';\n\n/**\n * Apply a route label to the request record (final log line, request duration\n * metric) and the active server span.\n *\n * Last write wins — astro's routing stamps every pass, so a rewrite lands the\n * route that rendered. An override comes from the middleware that knows what it\n * served and outranks routing, whatever the middleware order.\n */\nfunction applyRoute(route: string, method: string, override: boolean): void {\n const record = getRequestRecord();\n\n if (record) {\n if (!override && record.routeOverride) return;\n\n record.route = route;\n\n if (override) {\n record.routeOverride = true;\n }\n }\n\n const span = trace.getActiveSpan();\n\n if (span?.isRecording()) {\n span.setAttribute('http.route', route);\n\n if (!record?.actionName) {\n span.updateName(`${method} ${route}`);\n }\n }\n}\n\n/**\n * Report the route that actually served the current request, overriding the one\n * astro's routing matched.\n *\n * A middleware that rewrites (`next(url)`) or answers with its own response\n * serves a request astro has no page for, so routing matches `/404` and that is\n * what the request is logged and measured as — every such request collapsing\n * into one `/404` bucket in the request metrics. Calling this corrects the log\n * line, the metric and the server span name together.\n *\n * Pass a templated label rather than a concrete path, so metric cardinality\n * stays bounded. No-op outside instrumented requests.\n *\n * @example\n * ```ts\n * import { overrideRequestRoute } from '@astroscope/node/log';\n *\n * export const onRequest: MiddlewareHandler = (ctx, next) => {\n * const page = lookupPage(ctx.url.pathname);\n *\n * if (!page) return next();\n *\n * overrideRequestRoute('/cms/pages/[id]');\n *\n * return next(`/cms/pages/${page.id}`);\n * };\n * ```\n */\nexport function overrideRequestRoute(route: string): void {\n applyRoute(route, getRequestRecord()?.method ?? 'GET', true);\n}\n\n/**\n * Stamp the route astro's routing matched, unless it was overridden by the\n * middleware that served the request.\n * @internal\n */\nexport function setRequestRoute(route: string, method: string): void {\n applyRoute(route, method, false);\n}\n"],"mappings":";;;;;;;;AASA,MAAM,YAAY,OAAO,IAAI,sBAAsB;AA2BnD,SAAgB,cAAwB;CACtC,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ;GAAE,MAAM,KAAA;GAAW,gBAAgB,IAAI,kBAAkB;GAAG,QAAQ,CAAC;GAAG,SAAS;EAAE;EAC3F,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAgB,mBAA8C;CAC5D,OAAO,YAAY,CAAC,CAAC,eAAe,SAAS;AAC/C;;;;;;;;;;;ACvCA,SAAS,WAAW,OAAe,QAAgB,UAAyB;CAC1E,MAAM,SAAS,iBAAiB;CAEhC,IAAI,QAAQ;EACV,IAAI,CAAC,YAAY,OAAO,eAAe;EAEvC,OAAO,QAAQ;EAEf,IAAI,UACF,OAAO,gBAAgB;CAE3B;CAEA,MAAM,OAAO,MAAM,cAAc;CAEjC,IAAI,MAAM,YAAY,GAAG;EACvB,KAAK,aAAa,cAAc,KAAK;EAErC,IAAI,CAAC,QAAQ,YACX,KAAK,WAAW,GAAG,OAAO,GAAG,OAAO;CAExC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,qBAAqB,OAAqB;CACxD,WAAW,OAAO,iBAAiB,CAAC,EAAE,UAAU,OAAO,IAAI;AAC7D;;;;;;AAOA,SAAgB,gBAAgB,OAAe,QAAsB;CACnE,WAAW,OAAO,QAAQ,KAAK;AACjC"}
|
|
@@ -5,7 +5,9 @@ import { MiddlewareHandler } from "astro";
|
|
|
5
5
|
* Route enrichment: the native handler starts spans and request logging
|
|
6
6
|
* before routing, so the route pattern is the one thing only astro knows.
|
|
7
7
|
* Feeds it back into the request record (final log line, metrics) and the
|
|
8
|
-
* active server span.
|
|
8
|
+
* active server span. A middleware that serves a route astro never matched
|
|
9
|
+
* reports it itself via `overrideRequestRoute`, which wins over this. No-op
|
|
10
|
+
* outside instrumented requests.
|
|
9
11
|
*/
|
|
10
12
|
declare const onRequest: MiddlewareHandler;
|
|
11
13
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"route-middleware-entrypoint.d.ts","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"mappings":";;;;;AAWA;;;;AAMC;;cANY,SAAA,EAAW,iBAMvB"}
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { trace } from "@opentelemetry/api";
|
|
1
|
+
import { n as setRequestRoute } from "./request-route-DcnZOOM4.js";
|
|
3
2
|
//#region src/observability/route-middleware-entrypoint.ts
|
|
4
3
|
/**
|
|
5
4
|
* Route enrichment: the native handler starts spans and request logging
|
|
6
5
|
* before routing, so the route pattern is the one thing only astro knows.
|
|
7
6
|
* Feeds it back into the request record (final log line, metrics) and the
|
|
8
|
-
* active server span.
|
|
7
|
+
* active server span. A middleware that serves a route astro never matched
|
|
8
|
+
* reports it itself via `overrideRequestRoute`, which wins over this. No-op
|
|
9
|
+
* outside instrumented requests.
|
|
9
10
|
*/
|
|
10
11
|
const onRequest = (ctx, next) => {
|
|
11
|
-
|
|
12
|
-
const route = ctx.routePattern;
|
|
13
|
-
if (record && !record.route) record.route = route;
|
|
14
|
-
const span = trace.getActiveSpan();
|
|
15
|
-
if (span?.isRecording() && route) {
|
|
16
|
-
span.setAttribute("http.route", route);
|
|
17
|
-
if (!record?.actionName) span.updateName(`${ctx.request.method} ${route}`);
|
|
18
|
-
}
|
|
12
|
+
if (ctx.routePattern) setRequestRoute(ctx.routePattern, ctx.request.method);
|
|
19
13
|
return next();
|
|
20
14
|
};
|
|
21
15
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-middleware-entrypoint.js","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"route-middleware-entrypoint.js","names":[],"sources":["../src/observability/route-middleware-entrypoint.ts"],"sourcesContent":["import type { MiddlewareHandler } from 'astro';\nimport { setRequestRoute } from './request-route.js';\n\n/**\n * Route enrichment: the native handler starts spans and request logging\n * before routing, so the route pattern is the one thing only astro knows.\n * Feeds it back into the request record (final log line, metrics) and the\n * active server span. A middleware that serves a route astro never matched\n * reports it itself via `overrideRequestRoute`, which wins over this. No-op\n * outside instrumented requests.\n */\nexport const onRequest: MiddlewareHandler = (ctx, next) => {\n if (ctx.routePattern) {\n setRequestRoute(ctx.routePattern, ctx.request.method);\n }\n\n return next();\n};\n"],"mappings":";;;;;;;;;;AAWA,MAAa,aAAgC,KAAK,SAAS;CACzD,IAAI,IAAI,cACN,gBAAgB,IAAI,cAAc,IAAI,QAAQ,MAAM;CAGtD,OAAO,KAAK;AACd"}
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as setBootContext } from "./context-Bkg-FnnQ.js";
|
|
2
|
-
import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelemetry, o as runStartup, r as dumpEarlyLogs, t as preparePlatform } from "./prepare-
|
|
3
|
-
import {
|
|
4
|
-
import { n as log } from "./log-
|
|
5
|
-
import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-
|
|
2
|
+
import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelemetry, o as runStartup, r as dumpEarlyLogs, t as preparePlatform } from "./prepare-CXZsyAVk.js";
|
|
3
|
+
import { i as getRequestRecord } from "./request-route-DcnZOOM4.js";
|
|
4
|
+
import { n as log } from "./log-B69HEBvg.js";
|
|
5
|
+
import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-DjYEnO4X.js";
|
|
6
6
|
import { n as deactivateHealthChecks, t as activateHealthChecks } from "./store-8pnTxM1x.js";
|
|
7
7
|
import { n as MIME_TYPES, t as COMPRESSIBLE } from "./mime-C_GwZovh.js";
|
|
8
8
|
import fs, { createReadStream } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probes, request logging, telemetry, CSRF and static serving run as plain code around server.listen()",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"native-mount-hhwWdLtL.js","names":[],"sources":["../src/server/native-mount.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { trace } from '@opentelemetry/api';\nimport { log } from '../observability/log/index.js';\nimport { getRequestRecord } from '../observability/log/store.js';\n\n/**\n * Native mounts: raw `(req, res)` handlers dispatched before static/astro,\n * for Node libraries that need the real request and response (e.g.\n * `oidc-provider`'s `callback()`). Mounted requests bypass astro middleware\n * entirely but stay inside request logging and tracing.\n *\n * Keyed on `globalThis` because registration (boot file, vite runner in dev)\n * and dispatch (server runtime) may live in different module instances.\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/native-mounts');\n\nexport type NativeHandler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\n\nexport interface NativeMountMatcher {\n /** match requests whose pathname starts with this prefix */\n prefix?: string | undefined;\n\n /** predicate over the native request; evaluated when no prefix mount matches */\n match?: ((req: IncomingMessage) => boolean) | undefined;\n\n /** route label for logs, metrics and span names; defaults to the prefix */\n name?: string | undefined;\n}\n\ninterface Mount {\n prefix: string | undefined;\n match: ((req: IncomingMessage) => boolean) | undefined;\n name: string | undefined;\n handler: NativeHandler;\n}\n\ninterface Store {\n mounts: Mount[];\n}\n\nfunction getStore(): Store {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as Store | undefined;\n\n if (!store) {\n store = { mounts: [] };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nfunction matchesPrefix(pathname: string, prefix: string): boolean {\n if (!pathname.startsWith(prefix)) return false;\n\n const rest = pathname.slice(prefix.length);\n\n return rest === '' || rest.startsWith('/') || rest.startsWith('?');\n}\n\n/**\n * Mount a native `(req, res)` handler on the adapter's server. The handler\n * owns the response completely — matched requests never reach astro\n * middleware or rendering. Dispatch happens before static file serving, in\n * production and dev alike.\n *\n * Call from `onStartup`. Returns an unregister function; mounts still\n * registered after `onShutdown` are removed automatically.\n *\n * When several prefix mounts match, the longest prefix wins; predicate\n * mounts are consulted afterwards in registration order.\n *\n * @example\n * ```ts\n * // src/boot.ts\n * import { mountNativeHandler } from '@astroscope/node/native';\n *\n * export function onStartup() {\n * mountNativeHandler({ prefix: '/oidc', name: 'oidc' }, getOidcProvider().callback());\n * }\n * ```\n */\nexport function mountNativeHandler(matcher: NativeMountMatcher, handler: NativeHandler): () => void {\n if (!matcher.prefix && !matcher.match) {\n throw new Error('[@astroscope/node] mountNativeHandler requires a prefix or a match predicate');\n }\n\n const mount: Mount = {\n prefix: matcher.prefix,\n match: matcher.match,\n name: matcher.name ?? matcher.prefix,\n handler,\n };\n\n const store = getStore();\n\n store.mounts.push(mount);\n\n return () => {\n const index = store.mounts.indexOf(mount);\n\n if (index !== -1) store.mounts.splice(index, 1);\n };\n}\n\n/**\n * Remove every registered mount. Runs after `onShutdown` (prod) and between\n * dev generations, so re-running `onStartup` never stacks duplicates.\n */\nexport function clearNativeMounts(): void {\n getStore().mounts.length = 0;\n}\n\nfunction findMount(req: IncomingMessage): Mount | undefined {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n\n let best: Mount | undefined;\n\n for (const mount of getStore().mounts) {\n if (mount.prefix && matchesPrefix(pathname, mount.prefix)) {\n if (!best?.prefix || mount.prefix.length > best.prefix.length) {\n best = mount;\n }\n }\n }\n\n if (best) return best;\n\n return getStore().mounts.find((mount) => mount.match?.(req));\n}\n\nfunction failResponse(res: ServerResponse): void {\n if (res.writableEnded) return;\n\n if (!res.headersSent) {\n res.writeHead(500, { 'content-type': 'text/plain' });\n }\n\n res.end('Internal Server Error');\n}\n\n/**\n * Dispatch a request to a matching mount. Returns `false` when no mount\n * matches — the caller continues with static/astro handling.\n */\nexport function dispatchNativeMount(req: IncomingMessage, res: ServerResponse): boolean {\n const mount = findMount(req);\n\n if (!mount) return false;\n\n if (mount.name) {\n const record = getRequestRecord();\n\n if (record && !record.route) {\n record.route = mount.name;\n }\n\n const span = trace.getActiveSpan();\n\n if (span?.isRecording()) {\n span.setAttribute('http.route', mount.name);\n span.updateName(`${req.method ?? 'GET'} ${mount.name}`);\n }\n }\n\n try {\n const result = mount.handler(req, res);\n\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n });\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'native mount handler failed');\n failResponse(res);\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,YAAY,OAAO,IAAI,gCAAgC;AA0B7D,SAAS,WAAkB;CACzB,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ,EAAE,QAAQ,CAAC,EAAE;EACrB,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAS,cAAc,UAAkB,QAAyB;CAChE,IAAI,CAAC,SAAS,WAAW,MAAM,GAAG,OAAO;CAEzC,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM;CAEzC,OAAO,SAAS,MAAM,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG;AACnE;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBAAmB,SAA6B,SAAoC;CAClG,IAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,OAC9B,MAAM,IAAI,MAAM,8EAA8E;CAGhG,MAAM,QAAe;EACnB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,MAAM,QAAQ,QAAQ,QAAQ;EAC9B;CACF;CAEA,MAAM,QAAQ,SAAS;CAEvB,MAAM,OAAO,KAAK,KAAK;CAEvB,aAAa;EACX,MAAM,QAAQ,MAAM,OAAO,QAAQ,KAAK;EAExC,IAAI,UAAU,IAAI,MAAM,OAAO,OAAO,OAAO,CAAC;CAChD;AACF;;;;;AAMA,SAAgB,oBAA0B;CACxC,SAAS,CAAC,CAAC,OAAO,SAAS;AAC7B;AAEA,SAAS,UAAU,KAAyC;CAC1D,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;CAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;CAElE,IAAI;CAEJ,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC,QAC7B,IAAI,MAAM,UAAU,cAAc,UAAU,MAAM,MAAM;MAClD,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,KAAK,OAAO,QACrD,OAAO;CAAA;CAKb,IAAI,MAAM,OAAO;CAEjB,OAAO,SAAS,CAAC,CAAC,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG,CAAC;AAC7D;AAEA,SAAS,aAAa,KAA2B;CAC/C,IAAI,IAAI,eAAe;CAEvB,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;CAGrD,IAAI,IAAI,uBAAuB;AACjC;;;;;AAMA,SAAgB,oBAAoB,KAAsB,KAA8B;CACtF,MAAM,QAAQ,UAAU,GAAG;CAE3B,IAAI,CAAC,OAAO,OAAO;CAEnB,IAAI,MAAM,MAAM;EACd,MAAM,SAAS,iBAAiB;EAEhC,IAAI,UAAU,CAAC,OAAO,OACpB,OAAO,QAAQ,MAAM;EAGvB,MAAM,OAAO,MAAM,cAAc;EAEjC,IAAI,MAAM,YAAY,GAAG;GACvB,KAAK,aAAa,cAAc,MAAM,IAAI;GAC1C,KAAK,WAAW,GAAG,IAAI,UAAU,MAAM,GAAG,MAAM,MAAM;EACxD;CACF;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;EAErC,IAAI,kBAAkB,SACpB,OAAO,OAAO,QAAiB;GAC7B,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;GACzF,aAAa,GAAG;EAClB,CAAC;CAEL,SAAS,KAAK;EACZ,IAAI,MAAM,eAAe,QAAQ,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,GAAG,6BAA6B;EACzF,aAAa,GAAG;CAClB;CAEA,OAAO;AACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prepare-DQEf2Bnt.js","names":["LIB_NAME"],"sources":["../src/lifecycle/lifecycle.ts","../src/observability/telemetry/metrics.ts","../src/observability/instrument.ts","../src/observability/log/construct.ts","../src/observability/telemetry/sdk.ts","../src/platform/env.ts","../src/platform/prepare.ts"],"sourcesContent":["import { emit } from './events.js';\nimport type { BootContext } from './types.js';\n\nexport interface BootModule {\n onStartup?: ((context: BootContext) => Promise<void> | void) | undefined;\n onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;\n}\n\nexport async function runStartup(boot: BootModule, context: BootContext): Promise<void> {\n await emit('beforeOnStartup', context);\n await boot.onStartup?.(context);\n await emit('afterOnStartup', context);\n}\n\nexport async function runShutdown(boot: BootModule, context: BootContext): Promise<void> {\n try {\n await emit('beforeOnShutdown', context);\n await boot.onShutdown?.(context);\n } finally {\n await emit('afterOnShutdown', context);\n }\n}\n","import { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\n/**\n * Record the start of an HTTP request. Returns a function to call when the\n * request ends. Route is unknown at the native-handler level, so active\n * requests carry only the method.\n */\nexport function recordHttpRequestStart(method: string): () => void {\n getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().add(-1, { 'http.request.method': method });\n };\n}\n\nexport function recordHttpRequestDuration(\n attributes: { method: string; route: string | undefined; status: number },\n durationMs: number,\n): void {\n getHttpRequestDuration().record(durationMs / 1000, {\n 'http.request.method': attributes.method,\n 'http.route': attributes.route ?? '',\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordActionDuration(attributes: { name: string; status: number }, durationMs: number): void {\n getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createMatcher } from '@entwico/dash/match';\nimport { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api';\nimport type { Logger } from 'pino';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { generateReqId } from './log/index.js';\nimport { type RequestRecord, getLogStore } from './log/store.js';\nimport { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } from './telemetry/metrics.js';\n\nconst LIB_NAME = '@astroscope/node';\nconst ACTIONS_PREFIX = '/_actions/';\nconst REQUEST_ID_PATTERN = /^[\\w.-]{1,64}$/;\n\nconst roundTime = (n: number) => Math.round(n * 100) / 100;\n\nexport interface RequestLoggingConfig {\n exclude: ExcludePattern[];\n extended: boolean;\n}\n\nexport interface RequestTelemetryConfig {\n exclude: ExcludePattern[];\n}\n\nexport interface RequestInstrumentationConfig {\n logging: RequestLoggingConfig | false;\n telemetry: RequestTelemetryConfig | false;\n}\n\nfunction getClientIp(req: IncomingMessage): string | undefined {\n const forwarded = req.headers['x-forwarded-for'];\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n\n return (\n first?.split(',')[0]?.trim() ??\n (req.headers['x-real-ip'] as string | undefined) ??\n (req.headers['cf-connecting-ip'] as string | undefined)\n );\n}\n\nfunction resolveReqId(req: IncomingMessage): string {\n const incoming = req.headers['x-request-id'];\n const value = Array.isArray(incoming) ? incoming[0] : incoming;\n\n return value && REQUEST_ID_PATTERN.test(value) ? value : generateReqId();\n}\n\nfunction chunkSize(chunk: unknown): number {\n if (chunk == null) return 0;\n if (ArrayBuffer.isView(chunk)) return chunk.byteLength;\n if (typeof chunk === 'string') return Buffer.byteLength(chunk);\n\n return 0;\n}\n\n/**\n * Wraps the native request/response with logging and telemetry: a request\n * logger in async context (real status, response size, aborted-vs-completed\n * on `finish`/`close`), a SERVER span with propagation extraction, and\n * request metrics. Both concerns honor their own exclude patterns; when both\n * are excluded the request passes through untouched.\n */\nexport function createRequestInstrumentation(config: RequestInstrumentationConfig) {\n const tracer = trace.getTracer(LIB_NAME);\n const store = getLogStore();\n const loggingExcluded = config.logging ? createMatcher(config.logging.exclude) : () => true;\n const telemetryExcluded = config.telemetry ? createMatcher(config.telemetry.exclude) : () => true;\n\n return (req: IncomingMessage, res: ServerResponse, inner: () => void): void => {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n const method = req.method ?? 'GET';\n\n const logging = config.logging && !loggingExcluded(pathname) ? config.logging : false;\n const telemetry = config.telemetry && !telemetryExcluded(pathname) ? config.telemetry : false;\n\n if (!logging && !telemetry) {\n inner();\n\n return;\n }\n\n const startTime = performance.now();\n const isAction = pathname.startsWith(ACTIONS_PREFIX);\n\n let requestLogger: Logger | undefined;\n\n if (logging && store.root) {\n const reqId = resolveReqId(req);\n const reqData: Record<string, unknown> = { method, url: pathname };\n\n // extended logging includes potentially sensitive data\n if (logging.extended) {\n reqData['query'] = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n reqData['headers'] = req.headers;\n reqData['remoteAddress'] = getClientIp(req) ?? req.socket.remoteAddress;\n }\n\n requestLogger = store.root.child({ reqId, req: reqData });\n\n res.setHeader('x-request-id', reqId);\n }\n\n const record: RequestRecord = {\n logger: requestLogger,\n url,\n route: undefined,\n actionName: isAction ? pathname.slice(ACTIONS_PREFIX.length).replace(/\\/$/, '') : undefined,\n };\n\n let span: ReturnType<typeof tracer.startSpan> | undefined;\n let firstByteSpan: ReturnType<typeof tracer.startSpan> | undefined;\n let endActiveRequest: (() => void) | undefined;\n\n if (telemetry) {\n const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);\n const contentLength = req.headers['content-length'];\n const clientIp = getClientIp(req);\n const host = req.headers['host'];\n\n span = tracer.startSpan(\n isAction ? `ACTION ${record.actionName}` : method,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.request.method': method,\n 'url.path': pathname,\n 'url.query': queryIndex === -1 ? '' : url.slice(queryIndex + 1),\n 'url.scheme': 'http',\n 'user_agent.original': req.headers['user-agent'] ?? '',\n ...(host && { 'server.address': host }),\n ...(contentLength && { 'http.request.body.size': parseInt(contentLength) }),\n ...(clientIp && { 'client.address': clientIp }),\n },\n },\n parentContext,\n );\n\n firstByteSpan = tracer.startSpan('response:first-byte', undefined, trace.setSpan(parentContext, span));\n\n endActiveRequest = recordHttpRequestStart(method);\n }\n\n let responseSize = 0;\n let firstByteTime: number | undefined;\n\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n\n const markFirstByte = (): void => {\n if (firstByteTime !== undefined) return;\n\n firstByteTime = performance.now();\n\n if (firstByteSpan) {\n firstByteSpan.setAttribute('http.response.status_code', res.statusCode);\n firstByteSpan.end();\n }\n };\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);\n }) as typeof res.write;\n\n res.end = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalEnd as (...args: unknown[]) => ServerResponse)(chunk, ...rest);\n }) as typeof res.end;\n\n let finalized = false;\n\n const finalize = (aborted: boolean): void => {\n if (finalized) return;\n\n finalized = true;\n\n const status = res.statusCode;\n const responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n\n requestLogger[level](\n {\n res: { statusCode: status },\n responseTime: roundTime(responseTime),\n ttfb,\n responseSize,\n ...(record.route && { route: record.route }),\n ...(aborted && { aborted: true }),\n },\n aborted ? 'request aborted' : 'request completed',\n );\n }\n\n if (firstByteSpan && firstByteTime === undefined) {\n firstByteSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'request aborted' });\n firstByteSpan.end();\n }\n\n if (span) {\n span.setAttribute('http.response.status_code', status);\n span.setAttribute('http.response.body.size', responseSize);\n span.setAttribute('ttfb', ttfb);\n\n if (aborted || status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: aborted ? 'request aborted' : `HTTP ${status}` });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n }\n\n if (telemetry) {\n endActiveRequest?.();\n recordHttpRequestDuration({ method, route: record.route, status }, responseTime);\n\n if (record.actionName) {\n recordActionDuration({ name: record.actionName, status }, responseTime);\n }\n }\n };\n\n res.once('finish', () => finalize(false));\n res.once('close', () => finalize(!res.writableFinished));\n\n const run = (): void => store.requestStorage.run(record, inner);\n\n if (span) {\n context.with(trace.setSpan(context.active(), span), run);\n } else {\n run();\n }\n };\n}\n","import { isSpanContextValid, trace } from '@opentelemetry/api';\nimport pino, { type Bindings, type Logger, type LoggerOptions } from 'pino';\nimport { getLogStore } from './store.js';\n\n/**\n * Contract of the `src/log.ts` entry seam: pino logger options, or a factory\n * producing them. Never a logger instance — the platform constructs the\n * logger itself (after instrumentation, so trace correlation works).\n */\nexport type LoggerOptionsFactory = LoggerOptions | ((ctx: { dev: boolean }) => LoggerOptions | Promise<LoggerOptions>);\n\n/**\n * Compose the user mixin (if any) with platform trace correlation: when a\n * span is active, every entry carries `trace_id` / `span_id` / `trace_flags`.\n */\nfunction composeMixin(userMixin: LoggerOptions['mixin']): NonNullable<LoggerOptions['mixin']> {\n return (mergeObject, level, logger) => {\n const user = userMixin ? userMixin(mergeObject, level, logger) : {};\n const spanContext = trace.getActiveSpan()?.spanContext();\n\n if (!spanContext || !isSpanContextValid(spanContext)) return user;\n\n return {\n ...user,\n trace_id: spanContext.traceId,\n span_id: spanContext.spanId,\n trace_flags: `0${spanContext.traceFlags.toString(16)}`,\n };\n };\n}\n\n/**\n * Construct the root logger from the app's options seam and replay any logs\n * buffered before construction (original timestamps kept as `bufferedTime`).\n * In dev this runs once per generation; the buffer only exists the first time.\n */\nexport async function constructRootLogger(\n factory: LoggerOptionsFactory | undefined,\n ctx: { dev: boolean },\n): Promise<Logger> {\n const store = getLogStore();\n const options = (typeof factory === 'function' ? await factory(ctx) : factory) ?? {};\n const root = pino({ level: 'info', ...options, mixin: composeMixin(options.mixin) });\n\n store.root = root;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? (Object.assign({}, ...entry.bindings) as Bindings) : {};\n const target = root.child({ ...bindings, bufferedTime: new Date(entry.time).toISOString() });\n\n (target[entry.level] as (...args: unknown[]) => void)(...entry.args);\n }\n\n if (store.dropped > 0) {\n root.warn({ dropped: store.dropped }, 'early log buffer overflowed, entries dropped');\n store.dropped = 0;\n }\n\n return root;\n}\n\n/**\n * Failure path for startups that die before the logger exists: dump the\n * buffered entries to the console so no phase is silent.\n */\nexport function dumpEarlyLogs(): void {\n const store = getLogStore();\n\n if (store.root) return;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? Object.assign({}, ...entry.bindings) : undefined;\n\n console.error(\n new Date(entry.time).toISOString(),\n entry.level.toUpperCase(),\n ...(bindings ? [bindings] : []),\n ...entry.args,\n );\n }\n\n if (store.dropped > 0) {\n console.error(`(${store.dropped} early log entries dropped)`);\n store.dropped = 0;\n }\n}\n","import { log } from '../log/index.js';\n\n/**\n * Platform-owned telemetry bundle: NodeSDK with undici (fetch) and node\n * runtime instrumentation, host metrics, and a Prometheus reader. Trace\n * exporters are driven by standard `OTEL_*` env vars; without any of them\n * traces stay off (no failing localhost OTLP exports).\n *\n * Guarded per process (dev restarts are in-process; a re-created NodeSDK\n * would double-register instrumentations and leak the Prometheus port).\n */\n\nconst TELEMETRY_KEY = Symbol.for('@astroscope/node/telemetry');\n\ninterface TelemetryHandle {\n shutdown: () => Promise<void>;\n}\n\nexport interface TelemetrySdkOptions {\n prometheus: { host?: string | undefined; port?: number | undefined } | false;\n}\n\nfunction getHandle(): TelemetryHandle | undefined {\n return (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY] as TelemetryHandle | undefined;\n}\n\nfunction defaultEnv(key: string, value: string): void {\n if (!process.env[key]) process.env[key] = value;\n}\n\nexport async function startTelemetry(options: TelemetrySdkOptions): Promise<void> {\n const g = globalThis as Record<symbol, unknown>;\n\n if (g[TELEMETRY_KEY]) return;\n\n if (process.env['OTEL_SDK_DISABLED'] === 'true') {\n log.debug('telemetry disabled via OTEL_SDK_DISABLED');\n\n return;\n }\n\n // without an explicitly configured exporter target, exporting traces to the\n // default localhost OTLP endpoint would fail on every flush\n if (!process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] && !process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']) {\n defaultEnv('OTEL_TRACES_EXPORTER', 'none');\n }\n\n defaultEnv('OTEL_METRICS_EXPORTER', 'none');\n defaultEnv('OTEL_LOGS_EXPORTER', 'none');\n\n const [\n { NodeSDK },\n { UndiciInstrumentation },\n { RuntimeNodeInstrumentation },\n { PrometheusExporter },\n { HostMetrics },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-undici'),\n import('@opentelemetry/instrumentation-runtime-node'),\n import('@opentelemetry/exporter-prometheus'),\n import('@opentelemetry/host-metrics'),\n ]);\n\n const prometheus = options.prometheus\n ? {\n host: process.env['OTEL_EXPORTER_PROMETHEUS_HOST'] ?? options.prometheus.host ?? '0.0.0.0',\n port: process.env['OTEL_EXPORTER_PROMETHEUS_PORT']\n ? Number(process.env['OTEL_EXPORTER_PROMETHEUS_PORT'])\n : (options.prometheus.port ?? 9464),\n }\n : false;\n\n const sdk = new NodeSDK({\n instrumentations: [new UndiciInstrumentation(), new RuntimeNodeInstrumentation()],\n ...(prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }),\n });\n\n sdk.start();\n\n const hostMetrics = new HostMetrics();\n\n hostMetrics.start();\n\n g[TELEMETRY_KEY] = {\n shutdown: () => sdk.shutdown(),\n } satisfies TelemetryHandle;\n\n if (prometheus) {\n log.debug({ host: prometheus.host, port: prometheus.port }, 'prometheus metrics listening');\n }\n}\n\n/**\n * Flush and shut the SDK down. Prod-only (dev keeps the SDK for the process\n * lifetime across generations).\n */\nexport async function shutdownTelemetry(): Promise<void> {\n const handle = getHandle();\n\n if (!handle) return;\n\n delete (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY];\n\n await handle.shutdown();\n}\n","import fs from 'node:fs';\nimport { log } from '../observability/log/index.js';\n\n/**\n * Platform env loading (position −1, before the config seam):\n * `CONFIG_PATH` → `./.env` → none. Existing process env vars win\n */\nexport function loadEnvFiles(): void {\n const configPath = process.env['CONFIG_PATH'];\n\n if (configPath) {\n process.loadEnvFile(configPath);\n\n log.debug({ path: configPath }, 'loaded env file from CONFIG_PATH');\n\n return;\n }\n\n if (fs.existsSync('.env')) {\n process.loadEnvFile('.env');\n log.debug({ path: '.env' }, 'loaded env file');\n\n return;\n }\n\n log.debug('no env file loaded');\n}\n","import { type LoggerOptionsFactory, constructRootLogger } from '../observability/log/construct.js';\nimport { type TelemetrySdkOptions, startTelemetry } from '../observability/telemetry/sdk.js';\nimport { loadEnvFiles } from './env.js';\n\nconst INSTRUMENTATION_KEY = Symbol.for('@astroscope/node/instrumentation');\n\nexport interface InstrumentationContext {\n dev: boolean;\n}\n\ninterface InstrumentationSeam {\n register?: ((ctx: InstrumentationContext) => void | Promise<void>) | undefined;\n}\n\ninterface LogSeam {\n default?: LoggerOptionsFactory | undefined;\n}\n\nexport interface PlatformSeams {\n /** `src/config.ts` — validation runs at import; a throw fails the startup */\n config?: (() => Promise<unknown>) | undefined;\n /** `src/instrumentation.ts` — extra instrumentation, once per process */\n instrumentation?: (() => Promise<InstrumentationSeam>) | undefined;\n /** `src/log.ts` — pino logger options (or a factory), never an instance */\n log?: (() => Promise<LogSeam>) | undefined;\n}\n\nexport interface PreparePlatformOptions {\n dev: boolean;\n telemetry: TelemetrySdkOptions | false;\n seams: PlatformSeams;\n}\n\n/**\n * The platform sequence in front of the boot lifecycle:\n * env → config → instrumentation (platform SDK + `register`, once per\n * process) → logger construction (after instrumentation, so entries carry\n * trace correlation). Prod runs it once in `startServer()`; dev re-runs it\n * per generation with the once-per-process parts guarded.\n */\nexport async function preparePlatform(options: PreparePlatformOptions): Promise<void> {\n loadEnvFiles();\n\n await options.seams.config?.();\n\n const g = globalThis as Record<symbol, unknown>;\n\n if (!g[INSTRUMENTATION_KEY]) {\n g[INSTRUMENTATION_KEY] = true;\n\n if (options.telemetry) {\n await startTelemetry(options.telemetry);\n }\n\n const instrumentation = await options.seams.instrumentation?.();\n\n await instrumentation?.register?.({ dev: options.dev });\n }\n\n const logSeam = await options.seams.log?.();\n\n await constructRootLogger(logSeam?.default, { dev: options.dev });\n}\n"],"mappings":";;;;;;;;AAQA,eAAsB,WAAW,MAAkB,SAAqC;CACtF,MAAM,KAAK,mBAAmB,OAAO;CACrC,MAAM,KAAK,YAAY,OAAO;CAC9B,MAAM,KAAK,kBAAkB,OAAO;AACtC;AAEA,eAAsB,YAAY,MAAkB,SAAqC;CACvF,IAAI;EACF,MAAM,KAAK,oBAAoB,OAAO;EACtC,MAAM,KAAK,aAAa,OAAO;CACjC,UAAU;EACR,MAAM,KAAK,mBAAmB,OAAO;CACvC;AACF;;;ACnBA,MAAMA,aAAW;AAGjB,IAAI,sBAAwC;AAC5C,IAAI,qBAA2C;AAC/C,IAAI,iBAAmC;AAEvC,SAAS,yBAAoC;CAC3C,OAAQ,wBAAwB,QAAQ,SAASA,UAAQ,CAAC,CAAC,gBAAgB,gCAAgC;EACzG,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;AAEA,SAAS,wBAAuC;CAC9C,OAAQ,uBAAuB,QAAQ,SAASA,UAAQ,CAAC,CAAC,oBAAoB,+BAA+B;EAC3G,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;AAEA,SAAS,oBAA+B;CACtC,OAAQ,mBAAmB,QAAQ,SAASA,UAAQ,CAAC,CAAC,gBAAgB,yBAAyB;EAC7F,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;;;;;;AAOA,SAAgB,uBAAuB,QAA4B;CACjE,sBAAsB,CAAC,CAAC,IAAI,GAAG,EAAE,uBAAuB,OAAO,CAAC;CAEhE,aAAa;EACX,sBAAsB,CAAC,CAAC,IAAI,IAAI,EAAE,uBAAuB,OAAO,CAAC;CACnE;AACF;AAEA,SAAgB,0BACd,YACA,YACM;CACN,uBAAuB,CAAC,CAAC,OAAO,aAAa,KAAM;EACjD,uBAAuB,WAAW;EAClC,cAAc,WAAW,SAAS;EAClC,6BAA6B,WAAW;CAC1C,CAAC;AACH;AAEA,SAAgB,qBAAqB,YAA8C,YAA0B;CAC3G,kBAAkB,CAAC,CAAC,OAAO,aAAa,KAAM;EAC5C,qBAAqB,WAAW;EAChC,6BAA6B,WAAW;CAC1C,CAAC;AACH;;;ACrDA,MAAM,WAAW;AACjB,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAE3B,MAAM,aAAa,MAAc,KAAK,MAAM,IAAI,GAAG,IAAI;AAgBvD,SAAS,YAAY,KAA0C;CAC7D,MAAM,YAAY,IAAI,QAAQ;CAG9B,QAFc,MAAM,QAAQ,SAAS,IAAI,UAAU,KAAK,UAAA,EAG/C,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,KAC1B,IAAI,QAAQ,gBACZ,IAAI,QAAQ;AAEjB;AAEA,SAAS,aAAa,KAA8B;CAClD,MAAM,WAAW,IAAI,QAAQ;CAC7B,MAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;CAEtD,OAAO,SAAS,mBAAmB,KAAK,KAAK,IAAI,QAAQ,cAAc;AACzE;AAEA,SAAS,UAAU,OAAwB;CACzC,IAAI,SAAS,MAAM,OAAO;CAC1B,IAAI,YAAY,OAAO,KAAK,GAAG,OAAO,MAAM;CAC5C,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,WAAW,KAAK;CAE7D,OAAO;AACT;;;;;;;;AASA,SAAgB,6BAA6B,QAAsC;CACjF,MAAM,SAAS,MAAM,UAAU,QAAQ;CACvC,MAAM,QAAQ,YAAY;CAC1B,MAAM,kBAAkB,OAAO,UAAU,cAAc,OAAO,QAAQ,OAAO,UAAU;CACvF,MAAM,oBAAoB,OAAO,YAAY,cAAc,OAAO,UAAU,OAAO,UAAU;CAE7F,QAAQ,KAAsB,KAAqB,UAA4B;EAC7E,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;EAClE,MAAM,SAAS,IAAI,UAAU;EAE7B,MAAM,UAAU,OAAO,WAAW,CAAC,gBAAgB,QAAQ,IAAI,OAAO,UAAU;EAChF,MAAM,YAAY,OAAO,aAAa,CAAC,kBAAkB,QAAQ,IAAI,OAAO,YAAY;EAExF,IAAI,CAAC,WAAW,CAAC,WAAW;GAC1B,MAAM;GAEN;EACF;EAEA,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,WAAW,SAAS,WAAW,cAAc;EAEnD,IAAI;EAEJ,IAAI,WAAW,MAAM,MAAM;GACzB,MAAM,QAAQ,aAAa,GAAG;GAC9B,MAAM,UAAmC;IAAE;IAAQ,KAAK;GAAS;GAGjE,IAAI,QAAQ,UAAU;IACpB,QAAQ,WAAW,eAAe,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;IACpE,QAAQ,aAAa,IAAI;IACzB,QAAQ,mBAAmB,YAAY,GAAG,KAAK,IAAI,OAAO;GAC5D;GAEA,gBAAgB,MAAM,KAAK,MAAM;IAAE;IAAO,KAAK;GAAQ,CAAC;GAExD,IAAI,UAAU,gBAAgB,KAAK;EACrC;EAEA,MAAM,SAAwB;GAC5B,QAAQ;GACR;GACA,OAAO,KAAA;GACP,YAAY,WAAW,SAAS,MAAM,EAAqB,CAAC,CAAC,QAAQ,OAAO,EAAE,IAAI,KAAA;EACpF;EAEA,IAAI;EACJ,IAAI;EACJ,IAAI;EAEJ,IAAI,WAAW;GACb,MAAM,gBAAgB,YAAY,QAAQ,cAAc,IAAI,OAAO;GACnE,MAAM,gBAAgB,IAAI,QAAQ;GAClC,MAAM,WAAW,YAAY,GAAG;GAChC,MAAM,OAAO,IAAI,QAAQ;GAEzB,OAAO,OAAO,UACZ,WAAW,UAAU,OAAO,eAAe,QAC3C;IACE,MAAM,SAAS;IACf,YAAY;KACV,uBAAuB;KACvB,YAAY;KACZ,aAAa,eAAe,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;KAC9D,cAAc;KACd,uBAAuB,IAAI,QAAQ,iBAAiB;KACpD,GAAI,QAAQ,EAAE,kBAAkB,KAAK;KACrC,GAAI,iBAAiB,EAAE,0BAA0B,SAAS,aAAa,EAAE;KACzE,GAAI,YAAY,EAAE,kBAAkB,SAAS;IAC/C;GACF,GACA,aACF;GAEA,gBAAgB,OAAO,UAAU,uBAAuB,KAAA,GAAW,MAAM,QAAQ,eAAe,IAAI,CAAC;GAErG,mBAAmB,uBAAuB,MAAM;EAClD;EAEA,IAAI,eAAe;EACnB,IAAI;EAEJ,MAAM,gBAAgB,IAAI,MAAM,KAAK,GAAG;EACxC,MAAM,cAAc,IAAI,IAAI,KAAK,GAAG;EAEpC,MAAM,sBAA4B;GAChC,IAAI,kBAAkB,KAAA,GAAW;GAEjC,gBAAgB,YAAY,IAAI;GAEhC,IAAI,eAAe;IACjB,cAAc,aAAa,6BAA6B,IAAI,UAAU;IACtE,cAAc,IAAI;GACpB;EACF;EAEA,IAAI,UAAU,OAAgB,GAAG,SAAoB;GACnD,cAAc;GACd,gBAAgB,UAAU,KAAK;GAE/B,OAAQ,cAAkD,OAAO,GAAG,IAAI;EAC1E;EAEA,IAAI,QAAQ,OAAgB,GAAG,SAAoB;GACjD,cAAc;GACd,gBAAgB,UAAU,KAAK;GAE/B,OAAQ,YAAuD,OAAO,GAAG,IAAI;EAC/E;EAEA,IAAI,YAAY;EAEhB,MAAM,YAAY,YAA2B;GAC3C,IAAI,WAAW;GAEf,YAAY;GAEZ,MAAM,SAAS,IAAI;GACnB,MAAM,eAAe,YAAY,IAAI,IAAI;GACzC,MAAM,OAAO,WAAW,iBAAiB,YAAY,IAAI,KAAK,SAAS;GAEvE,IAAI,eAGF,cAFc,UAAU,MAAM,UAAU,UAAU,MAAM,SAAS,OAE7C,CAClB;IACE,KAAK,EAAE,YAAY,OAAO;IAC1B,cAAc,UAAU,YAAY;IACpC;IACA;IACA,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM;IAC1C,GAAI,WAAW,EAAE,SAAS,KAAK;GACjC,GACA,UAAU,oBAAoB,mBAChC;GAGF,IAAI,iBAAiB,kBAAkB,KAAA,GAAW;IAChD,cAAc,UAAU;KAAE,MAAM,eAAe;KAAO,SAAS;IAAkB,CAAC;IAClF,cAAc,IAAI;GACpB;GAEA,IAAI,MAAM;IACR,KAAK,aAAa,6BAA6B,MAAM;IACrD,KAAK,aAAa,2BAA2B,YAAY;IACzD,KAAK,aAAa,QAAQ,IAAI;IAE9B,IAAI,WAAW,UAAU,KACvB,KAAK,UAAU;KAAE,MAAM,eAAe;KAAO,SAAS,UAAU,oBAAoB,QAAQ;IAAS,CAAC;SAEtG,KAAK,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;IAG5C,KAAK,IAAI;GACX;GAEA,IAAI,WAAW;IACb,mBAAmB;IACnB,0BAA0B;KAAE;KAAQ,OAAO,OAAO;KAAO;IAAO,GAAG,YAAY;IAE/E,IAAI,OAAO,YACT,qBAAqB;KAAE,MAAM,OAAO;KAAY;IAAO,GAAG,YAAY;GAE1E;EACF;EAEA,IAAI,KAAK,gBAAgB,SAAS,KAAK,CAAC;EACxC,IAAI,KAAK,eAAe,SAAS,CAAC,IAAI,gBAAgB,CAAC;EAEvD,MAAM,YAAkB,MAAM,eAAe,IAAI,QAAQ,KAAK;EAE9D,IAAI,MACF,QAAQ,KAAK,MAAM,QAAQ,QAAQ,OAAO,GAAG,IAAI,GAAG,GAAG;OAEvD,IAAI;CAER;AACF;;;;;;;ACnOA,SAAS,aAAa,WAAwE;CAC5F,QAAQ,aAAa,OAAO,WAAW;EACrC,MAAM,OAAO,YAAY,UAAU,aAAa,OAAO,MAAM,IAAI,CAAC;EAClE,MAAM,cAAc,MAAM,cAAc,CAAC,EAAE,YAAY;EAEvD,IAAI,CAAC,eAAe,CAAC,mBAAmB,WAAW,GAAG,OAAO;EAE7D,OAAO;GACL,GAAG;GACH,UAAU,YAAY;GACtB,SAAS,YAAY;GACrB,aAAa,IAAI,YAAY,WAAW,SAAS,EAAE;EACrD;CACF;AACF;;;;;;AAOA,eAAsB,oBACpB,SACA,KACiB;CACjB,MAAM,QAAQ,YAAY;CAC1B,MAAM,WAAW,OAAO,YAAY,aAAa,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC;CACnF,MAAM,OAAO,KAAK;EAAE,OAAO;EAAQ,GAAG;EAAS,OAAO,aAAa,QAAQ,KAAK;CAAE,CAAC;CAEnF,MAAM,OAAO;CAEb,KAAK,MAAM,SAAS,MAAM,OAAO,OAAO,CAAC,GAAG;EAC1C,MAAM,WAAW,MAAM,SAAS,SAAU,OAAO,OAAO,CAAC,GAAG,GAAG,MAAM,QAAQ,IAAiB,CAAC;EAG/F,KAFoB,MAAM;GAAE,GAAG;GAAU,cAAc,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY;EAAE,CAEpF,CAAC,CAAC,MAAM,MAAM,CAAkC,GAAG,MAAM,IAAI;CACrE;CAEA,IAAI,MAAM,UAAU,GAAG;EACrB,KAAK,KAAK,EAAE,SAAS,MAAM,QAAQ,GAAG,8CAA8C;EACpF,MAAM,UAAU;CAClB;CAEA,OAAO;AACT;;;;;AAMA,SAAgB,gBAAsB;CACpC,MAAM,QAAQ,YAAY;CAE1B,IAAI,MAAM,MAAM;CAEhB,KAAK,MAAM,SAAS,MAAM,OAAO,OAAO,CAAC,GAAG;EAC1C,MAAM,WAAW,MAAM,SAAS,SAAS,OAAO,OAAO,CAAC,GAAG,GAAG,MAAM,QAAQ,IAAI,KAAA;EAEhF,QAAQ,MACN,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY,GACjC,MAAM,MAAM,YAAY,GACxB,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC,GAC7B,GAAG,MAAM,IACX;CACF;CAEA,IAAI,MAAM,UAAU,GAAG;EACrB,QAAQ,MAAM,IAAI,MAAM,QAAQ,4BAA4B;EAC5D,MAAM,UAAU;CAClB;AACF;;;;;;;;;;;;ACzEA,MAAM,gBAAgB,OAAO,IAAI,4BAA4B;AAU7D,SAAS,YAAyC;CAChD,OAAQ,WAAuC;AACjD;AAEA,SAAS,WAAW,KAAa,OAAqB;CACpD,IAAI,CAAC,QAAQ,IAAI,MAAM,QAAQ,IAAI,OAAO;AAC5C;AAEA,eAAsB,eAAe,SAA6C;CAChF,MAAM,IAAI;CAEV,IAAI,EAAE,gBAAgB;CAEtB,IAAI,QAAQ,IAAI,yBAAyB,QAAQ;EAC/C,IAAI,MAAM,0CAA0C;EAEpD;CACF;CAIA,IAAI,CAAC,QAAQ,IAAI,kCAAkC,CAAC,QAAQ,IAAI,uCAC9D,WAAW,wBAAwB,MAAM;CAG3C,WAAW,yBAAyB,MAAM;CAC1C,WAAW,sBAAsB,MAAM;CAEvC,MAAM,CACJ,EAAE,WACF,EAAE,yBACF,EAAE,8BACF,EAAE,sBACF,EAAE,iBACA,MAAM,QAAQ,IAAI;EACpB,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,MAAM,aAAa,QAAQ,aACvB;EACE,MAAM,QAAQ,IAAI,oCAAoC,QAAQ,WAAW,QAAQ;EACjF,MAAM,QAAQ,IAAI,mCACd,OAAO,QAAQ,IAAI,gCAAgC,IAClD,QAAQ,WAAW,QAAQ;CAClC,IACA;CAEJ,MAAM,MAAM,IAAI,QAAQ;EACtB,kBAAkB,CAAC,IAAI,sBAAsB,GAAG,IAAI,2BAA2B,CAAC;EAChF,GAAI,cAAc,EAAE,eAAe,CAAC,IAAI,mBAAmB,UAAU,CAAC,EAAE;CAC1E,CAAC;CAED,IAAI,MAAM;CAIV,IAFwB,YAEd,CAAC,CAAC,MAAM;CAElB,EAAE,iBAAiB,EACjB,gBAAgB,IAAI,SAAS,EAC/B;CAEA,IAAI,YACF,IAAI,MAAM;EAAE,MAAM,WAAW;EAAM,MAAM,WAAW;CAAK,GAAG,8BAA8B;AAE9F;;;;;AAMA,eAAsB,oBAAmC;CACvD,MAAM,SAAS,UAAU;CAEzB,IAAI,CAAC,QAAQ;CAEb,OAAQ,WAAuC;CAE/C,MAAM,OAAO,SAAS;AACxB;;;;;;;AClGA,SAAgB,eAAqB;CACnC,MAAM,aAAa,QAAQ,IAAI;CAE/B,IAAI,YAAY;EACd,QAAQ,YAAY,UAAU;EAE9B,IAAI,MAAM,EAAE,MAAM,WAAW,GAAG,kCAAkC;EAElE;CACF;CAEA,IAAI,GAAG,WAAW,MAAM,GAAG;EACzB,QAAQ,YAAY,MAAM;EAC1B,IAAI,MAAM,EAAE,MAAM,OAAO,GAAG,iBAAiB;EAE7C;CACF;CAEA,IAAI,MAAM,oBAAoB;AAChC;;;ACtBA,MAAM,sBAAsB,OAAO,IAAI,kCAAkC;;;;;;;;AAoCzE,eAAsB,gBAAgB,SAAgD;CACpF,aAAa;CAEb,MAAM,QAAQ,MAAM,SAAS;CAE7B,MAAM,IAAI;CAEV,IAAI,CAAC,EAAE,sBAAsB;EAC3B,EAAE,uBAAuB;EAEzB,IAAI,QAAQ,WACV,MAAM,eAAe,QAAQ,SAAS;EAKxC,OAAM,MAFwB,QAAQ,MAAM,kBAAkB,EAAA,EAEvC,WAAW,EAAE,KAAK,QAAQ,IAAI,CAAC;CACxD;CAIA,MAAM,qBAAoB,MAFJ,QAAQ,MAAM,MAAM,EAAA,EAEP,SAAS,EAAE,KAAK,QAAQ,IAAI,CAAC;AAClE"}
|
package/dist/store-BIUF4lqk.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
//#region src/observability/log/store.ts
|
|
3
|
-
/**
|
|
4
|
-
* Shared state between the public `log` proxy and the server runtime. Keyed on
|
|
5
|
-
* `globalThis` because the two sides may live in different module instances
|
|
6
|
-
* (bundled app vs vite module runner in dev).
|
|
7
|
-
*/
|
|
8
|
-
const STORE_KEY = Symbol.for("@astroscope/node/log");
|
|
9
|
-
function getLogStore() {
|
|
10
|
-
const g = globalThis;
|
|
11
|
-
let store = g[STORE_KEY];
|
|
12
|
-
if (!store) {
|
|
13
|
-
store = {
|
|
14
|
-
root: void 0,
|
|
15
|
-
requestStorage: new AsyncLocalStorage(),
|
|
16
|
-
buffer: [],
|
|
17
|
-
dropped: 0
|
|
18
|
-
};
|
|
19
|
-
g[STORE_KEY] = store;
|
|
20
|
-
}
|
|
21
|
-
return store;
|
|
22
|
-
}
|
|
23
|
-
function getRequestRecord() {
|
|
24
|
-
return getLogStore().requestStorage.getStore();
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
export { getRequestRecord as n, getLogStore as t };
|
|
28
|
-
|
|
29
|
-
//# sourceMappingURL=store-BIUF4lqk.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store-BIUF4lqk.js","names":[],"sources":["../src/observability/log/store.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Logger } from 'pino';\n\n/**\n * Shared state between the public `log` proxy and the server runtime. Keyed on\n * `globalThis` because the two sides may live in different module instances\n * (bundled app vs vite module runner in dev).\n */\n\nconst STORE_KEY = Symbol.for('@astroscope/node/log');\n\nexport const EARLY_LOG_BUFFER_CAP = 100;\n\nexport interface BufferedEntry {\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';\n bindings: Record<string, unknown>[];\n args: unknown[];\n time: number;\n}\n\nexport interface RequestRecord {\n logger: Logger | undefined;\n url: string;\n route: string | undefined;\n actionName: string | undefined;\n}\n\nexport interface LogStore {\n root: Logger | undefined;\n requestStorage: AsyncLocalStorage<RequestRecord>;\n buffer: BufferedEntry[];\n dropped: number;\n}\n\nexport function getLogStore(): LogStore {\n const g = globalThis as Record<symbol, unknown>;\n let store = g[STORE_KEY] as LogStore | undefined;\n\n if (!store) {\n store = { root: undefined, requestStorage: new AsyncLocalStorage(), buffer: [], dropped: 0 };\n g[STORE_KEY] = store;\n }\n\n return store;\n}\n\nexport function getRequestRecord(): RequestRecord | undefined {\n return getLogStore().requestStorage.getStore();\n}\n"],"mappings":";;;;;;;AASA,MAAM,YAAY,OAAO,IAAI,sBAAsB;AAyBnD,SAAgB,cAAwB;CACtC,MAAM,IAAI;CACV,IAAI,QAAQ,EAAE;CAEd,IAAI,CAAC,OAAO;EACV,QAAQ;GAAE,MAAM,KAAA;GAAW,gBAAgB,IAAI,kBAAkB;GAAG,QAAQ,CAAC;GAAG,SAAS;EAAE;EAC3F,EAAE,aAAa;CACjB;CAEA,OAAO;AACT;AAEA,SAAgB,mBAA8C;CAC5D,OAAO,YAAY,CAAC,CAAC,eAAe,SAAS;AAC/C"}
|