@timber-js/app 0.2.0-alpha.38 → 0.2.0-alpha.39
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/dist/cache/index.js +10 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/index.js +108 -23
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cache/singleflight.ts +18 -10
- package/src/server/action-handler.ts +3 -2
- package/src/server/default-logger.ts +95 -0
- package/src/server/flush.ts +2 -1
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +2 -1
- package/src/server/route-handler.ts +2 -1
- package/src/server/ssr-render.ts +20 -12
package/dist/server/logger.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logger — structured logging with environment-aware formatting.
|
|
3
3
|
*
|
|
4
|
-
* timber.js
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* timber.js ships a DefaultLogger that writes human-readable lines to stderr
|
|
5
|
+
* in production. Users can export a custom logger from instrumentation.ts to
|
|
6
|
+
* replace it with pino, winston, or any TimberLogger-compatible object.
|
|
7
7
|
*
|
|
8
8
|
* See design/17-logging.md §"Production Logging"
|
|
9
9
|
*/
|
|
@@ -16,14 +16,15 @@ export interface TimberLogger {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Set the user-provided logger. Called by the instrumentation loader
|
|
19
|
-
* when it finds a `logger` export in instrumentation.ts.
|
|
19
|
+
* when it finds a `logger` export in instrumentation.ts. Replaces
|
|
20
|
+
* the DefaultLogger entirely.
|
|
20
21
|
*/
|
|
21
22
|
export declare function setLogger(logger: TimberLogger): void;
|
|
22
23
|
/**
|
|
23
|
-
* Get the current logger
|
|
24
|
-
*
|
|
24
|
+
* Get the current logger. Always non-null — returns DefaultLogger when
|
|
25
|
+
* no custom logger is configured.
|
|
25
26
|
*/
|
|
26
|
-
export declare function getLogger(): TimberLogger
|
|
27
|
+
export declare function getLogger(): TimberLogger;
|
|
27
28
|
/** Log a completed request. Level: info. */
|
|
28
29
|
export declare function logRequestCompleted(data: {
|
|
29
30
|
method: string;
|
|
@@ -69,6 +70,22 @@ export declare function logRenderError(data: {
|
|
|
69
70
|
export declare function logProxyError(data: {
|
|
70
71
|
error: unknown;
|
|
71
72
|
}): void;
|
|
73
|
+
/** Log unhandled error in server action. Level: error. */
|
|
74
|
+
export declare function logActionError(data: {
|
|
75
|
+
method: string;
|
|
76
|
+
path: string;
|
|
77
|
+
error: unknown;
|
|
78
|
+
}): void;
|
|
79
|
+
/** Log unhandled error in route handler. Level: error. */
|
|
80
|
+
export declare function logRouteError(data: {
|
|
81
|
+
method: string;
|
|
82
|
+
path: string;
|
|
83
|
+
error: unknown;
|
|
84
|
+
}): void;
|
|
85
|
+
/** Log SSR streaming error (post-shell). Level: error. */
|
|
86
|
+
export declare function logStreamingError(data: {
|
|
87
|
+
error: unknown;
|
|
88
|
+
}): void;
|
|
72
89
|
/** Log waitUntil() adapter missing (once at startup). Level: warn. */
|
|
73
90
|
export declare function logWaitUntilUnsupported(): void;
|
|
74
91
|
/** Log waitUntil() promise rejection. Level: warn. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"node-stream-transforms.d.ts","sourceRoot":"","sources":["../../src/server/node-stream-transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAkBxC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA+ClE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,EACjD,OAAO,CAAC,EAAE,yBAAyB,GAClC,SAAS,CAuJX;AAOD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAwBtE;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,OAAO,GACvB,SAAS,GAAG,IAAI,CA8BlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/server/route-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/server/route-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1F,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE/E,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG;KACvB,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,YAAY;CACjC,CAAC;AAOF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,EAAE,CAyBpE;AAID;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyC/F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsEvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr-render.d.ts","sourceRoot":"","sources":["../../src/server/ssr-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsEvC;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAErC;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAkB,CAAC;AAU9C;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IACR,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,OAAO,aAAa,EAAE,QAAQ,CAAC,CAsFzC;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,aAAa,EAAE,QAAQ,GACvC,cAAc,CAAC,UAAU,CAAC,CAE5B;AAoFD;;;;;;;;;;;;GAYG;AACH,2CAA2C;AAC3C,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,UAAU,CAAC,CA2B5B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,OAAO,GACvB,QAAQ,CASV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.39",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -50,16 +50,24 @@ export function createSingleflight(opts?: SingleflightOptions): Singleflight {
|
|
|
50
50
|
() => reject(new SingleflightTimeoutError(key, timeoutMs)),
|
|
51
51
|
timeoutMs
|
|
52
52
|
);
|
|
53
|
-
fn()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
// Wrap in try/catch so a synchronous throw from fn()
|
|
54
|
+
// (e.g. argument validation) still clears the timer.
|
|
55
|
+
// Without this, the timer leaks until expiry.
|
|
56
|
+
try {
|
|
57
|
+
fn().then(
|
|
58
|
+
(value) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
resolve(value);
|
|
61
|
+
},
|
|
62
|
+
(err) => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
reject(err);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
63
71
|
});
|
|
64
72
|
} else {
|
|
65
73
|
promise = fn();
|
|
@@ -32,6 +32,7 @@ import { enforceBodyLimits, enforceFieldLimit, type BodyLimitsConfig } from './b
|
|
|
32
32
|
import { parseFormData } from './form-data.js';
|
|
33
33
|
import type { FormFlashData } from './form-flash.js';
|
|
34
34
|
import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
|
|
35
|
+
import { logActionError } from './logger.js';
|
|
35
36
|
|
|
36
37
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
37
38
|
|
|
@@ -193,7 +194,7 @@ async function handleRscAction(
|
|
|
193
194
|
});
|
|
194
195
|
} catch (error) {
|
|
195
196
|
// Log full error server-side for debugging
|
|
196
|
-
|
|
197
|
+
logActionError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
197
198
|
|
|
198
199
|
// Return structured error response — ActionError gets its code/data,
|
|
199
200
|
// unexpected errors get sanitized { code: 'INTERNAL_ERROR' }
|
|
@@ -309,7 +310,7 @@ async function handleFormAction(
|
|
|
309
310
|
renderer: config.revalidateRenderer,
|
|
310
311
|
});
|
|
311
312
|
} catch (error) {
|
|
312
|
-
|
|
313
|
+
logActionError({ method: req.method, path: new URL(req.url).pathname, error });
|
|
313
314
|
|
|
314
315
|
// Return the error as flash data for re-render.
|
|
315
316
|
// handleActionError produces { serverError } for ActionErrors
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
3
|
+
*
|
|
4
|
+
* Ships as the fallback so production deployments always have error visibility,
|
|
5
|
+
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
6
|
+
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
7
|
+
*
|
|
8
|
+
* Format:
|
|
9
|
+
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
10
|
+
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
11
|
+
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
15
|
+
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
16
|
+
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
17
|
+
*
|
|
18
|
+
* See design/17-logging.md §"DefaultLogger"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { isDevMode, isDebug } from './debug.js';
|
|
22
|
+
import { formatSsrError } from './error-formatter.js';
|
|
23
|
+
import type { TimberLogger } from './logger.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format data fields as `key=value` pairs for human-readable output.
|
|
27
|
+
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
28
|
+
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
29
|
+
* - Other values are stringified inline
|
|
30
|
+
*/
|
|
31
|
+
function formatDataFields(data?: Record<string, unknown>): string {
|
|
32
|
+
if (!data) return '';
|
|
33
|
+
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
let traceId: string | undefined;
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of Object.entries(data)) {
|
|
38
|
+
if (key === 'trace_id') {
|
|
39
|
+
// Defer trace_id to the end
|
|
40
|
+
traceId = typeof value === 'string' ? value : String(value);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (key === 'error') {
|
|
44
|
+
// Serialize errors with formatSsrError for clean output
|
|
45
|
+
parts.push(`error=${formatSsrError(value)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (value === undefined || value === null) continue;
|
|
49
|
+
parts.push(`${key}=${value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// trace_id always last, truncated to 8 chars for readability
|
|
53
|
+
if (traceId) {
|
|
54
|
+
parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pad level string to fixed width for alignment. */
|
|
61
|
+
function padLevel(level: string): string {
|
|
62
|
+
return level.padEnd(5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDefaultLogger(): TimberLogger {
|
|
66
|
+
return {
|
|
67
|
+
error(msg: string, data?: Record<string, unknown>): void {
|
|
68
|
+
if (isDevMode()) return;
|
|
69
|
+
const fields = formatDataFields(data);
|
|
70
|
+
// Use process.stderr.write for consistent output (no extra newline handling)
|
|
71
|
+
process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\n`);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
warn(msg: string, data?: Record<string, unknown>): void {
|
|
75
|
+
if (isDevMode()) return;
|
|
76
|
+
const fields = formatDataFields(data);
|
|
77
|
+
process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\n`);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
info(msg: string, data?: Record<string, unknown>): void {
|
|
81
|
+
if (isDevMode()) return;
|
|
82
|
+
const fields = formatDataFields(data);
|
|
83
|
+
process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\n`);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
debug(msg: string, data?: Record<string, unknown>): void {
|
|
87
|
+
// debug is suppressed in dev (dev logger handles it) and in
|
|
88
|
+
// production unless TIMBER_DEBUG is explicitly set.
|
|
89
|
+
if (isDevMode()) return;
|
|
90
|
+
if (!isDebug()) return;
|
|
91
|
+
const fields = formatDataFields(data);
|
|
92
|
+
process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\n`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/server/flush.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { DenySignal, RedirectSignal, RenderError } from './primitives.js';
|
|
12
|
+
import { logRenderError } from './logger.js';
|
|
12
13
|
|
|
13
14
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -169,7 +170,7 @@ function handleSignal(error: unknown, responseHeaders: Headers): FlushResult {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// Unknown error → HTTP 500
|
|
172
|
-
|
|
173
|
+
logRenderError({ method: '', path: '', error });
|
|
173
174
|
return {
|
|
174
175
|
response: new Response(null, {
|
|
175
176
|
status: 500,
|
package/src/server/logger.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logger — structured logging with environment-aware formatting.
|
|
3
3
|
*
|
|
4
|
-
* timber.js
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* timber.js ships a DefaultLogger that writes human-readable lines to stderr
|
|
5
|
+
* in production. Users can export a custom logger from instrumentation.ts to
|
|
6
|
+
* replace it with pino, winston, or any TimberLogger-compatible object.
|
|
7
7
|
*
|
|
8
8
|
* See design/17-logging.md §"Production Logging"
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { getTraceStore } from './tracing.js';
|
|
12
|
-
import {
|
|
13
|
-
import { isDebug } from './debug.js';
|
|
12
|
+
import { createDefaultLogger } from './default-logger.js';
|
|
14
13
|
|
|
15
14
|
// ─── Logger Interface ─────────────────────────────────────────────────────
|
|
16
15
|
|
|
@@ -24,21 +23,24 @@ export interface TimberLogger {
|
|
|
24
23
|
|
|
25
24
|
// ─── Logger Registry ──────────────────────────────────────────────────────
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
// Initialize with DefaultLogger so production errors are never silent.
|
|
27
|
+
// Replaced when setLogger() is called from instrumentation.ts.
|
|
28
|
+
let _logger: TimberLogger = createDefaultLogger();
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Set the user-provided logger. Called by the instrumentation loader
|
|
31
|
-
* when it finds a `logger` export in instrumentation.ts.
|
|
32
|
+
* when it finds a `logger` export in instrumentation.ts. Replaces
|
|
33
|
+
* the DefaultLogger entirely.
|
|
32
34
|
*/
|
|
33
35
|
export function setLogger(logger: TimberLogger): void {
|
|
34
36
|
_logger = logger;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* Get the current logger
|
|
39
|
-
*
|
|
40
|
+
* Get the current logger. Always non-null — returns DefaultLogger when
|
|
41
|
+
* no custom logger is configured.
|
|
40
42
|
*/
|
|
41
|
-
export function getLogger(): TimberLogger
|
|
43
|
+
export function getLogger(): TimberLogger {
|
|
42
44
|
return _logger;
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -71,12 +73,12 @@ export function logRequestCompleted(data: {
|
|
|
71
73
|
/** Number of concurrent in-flight requests (including this one) at completion time. */
|
|
72
74
|
concurrency?: number;
|
|
73
75
|
}): void {
|
|
74
|
-
_logger
|
|
76
|
+
_logger.info('request completed', withTraceContext(data));
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** Log request received. Level: debug. */
|
|
78
80
|
export function logRequestReceived(data: { method: string; path: string }): void {
|
|
79
|
-
_logger
|
|
81
|
+
_logger.debug('request received', withTraceContext(data));
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/** Log a slow request warning. Level: warn. */
|
|
@@ -88,7 +90,7 @@ export function logSlowRequest(data: {
|
|
|
88
90
|
/** Number of concurrent in-flight requests at the time the slow request completed. */
|
|
89
91
|
concurrency?: number;
|
|
90
92
|
}): void {
|
|
91
|
-
_logger
|
|
93
|
+
_logger.warn('slow request exceeded threshold', withTraceContext(data));
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/** Log middleware short-circuit. Level: debug. */
|
|
@@ -97,54 +99,55 @@ export function logMiddlewareShortCircuit(data: {
|
|
|
97
99
|
path: string;
|
|
98
100
|
status: number;
|
|
99
101
|
}): void {
|
|
100
|
-
_logger
|
|
102
|
+
_logger.debug('middleware short-circuited', withTraceContext(data));
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** Log unhandled error in middleware phase. Level: error. */
|
|
104
106
|
export function logMiddlewareError(data: { method: string; path: string; error: unknown }): void {
|
|
105
|
-
|
|
106
|
-
_logger.error('unhandled error in middleware phase', withTraceContext(data));
|
|
107
|
-
} else if (isDebug()) {
|
|
108
|
-
console.error('[timber] middleware error', data.error);
|
|
109
|
-
}
|
|
107
|
+
_logger.error('unhandled error in middleware phase', withTraceContext(data));
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
/** Log unhandled render-phase error. Level: error. */
|
|
113
111
|
export function logRenderError(data: { method: string; path: string; error: unknown }): void {
|
|
114
|
-
|
|
115
|
-
_logger.error('unhandled render-phase error', withTraceContext(data));
|
|
116
|
-
} else if (isDebug()) {
|
|
117
|
-
// No logger configured — fall back to console.error in dev with
|
|
118
|
-
// cleaned-up error messages (vendor paths rewritten, hints added).
|
|
119
|
-
console.error('[timber] render error:', formatSsrError(data.error));
|
|
120
|
-
}
|
|
112
|
+
_logger.error('unhandled render-phase error', withTraceContext(data));
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
/** Log proxy.ts uncaught error. Level: error. */
|
|
124
116
|
export function logProxyError(data: { error: unknown }): void {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
_logger.error('proxy.ts threw uncaught error', withTraceContext(data));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Log unhandled error in server action. Level: error. */
|
|
121
|
+
export function logActionError(data: { method: string; path: string; error: unknown }): void {
|
|
122
|
+
_logger.error('unhandled server action error', withTraceContext(data));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Log unhandled error in route handler. Level: error. */
|
|
126
|
+
export function logRouteError(data: { method: string; path: string; error: unknown }): void {
|
|
127
|
+
_logger.error('unhandled route handler error', withTraceContext(data));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Log SSR streaming error (post-shell). Level: error. */
|
|
131
|
+
export function logStreamingError(data: { error: unknown }): void {
|
|
132
|
+
_logger.error('SSR streaming error (post-shell)', withTraceContext(data));
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
/** Log waitUntil() adapter missing (once at startup). Level: warn. */
|
|
133
136
|
export function logWaitUntilUnsupported(): void {
|
|
134
|
-
_logger
|
|
137
|
+
_logger.warn('adapter does not support waitUntil()');
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
/** Log waitUntil() promise rejection. Level: warn. */
|
|
138
141
|
export function logWaitUntilRejected(data: { error: unknown }): void {
|
|
139
|
-
_logger
|
|
142
|
+
_logger.warn('waitUntil() promise rejected', withTraceContext(data));
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
/** Log staleWhileRevalidate refetch failure. Level: warn. */
|
|
143
146
|
export function logSwrRefetchFailed(data: { cacheKey: string; error: unknown }): void {
|
|
144
|
-
_logger
|
|
147
|
+
_logger.warn('staleWhileRevalidate refetch failed', withTraceContext(data));
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/** Log cache miss. Level: debug. */
|
|
148
151
|
export function logCacheMiss(data: { cacheKey: string }): void {
|
|
149
|
-
_logger
|
|
152
|
+
_logger.debug('timber.cache MISS', withTraceContext(data));
|
|
150
153
|
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type FlightInjectionEvent,
|
|
32
32
|
} from './flight-injection-state.js';
|
|
33
33
|
import { withTimeout, RenderTimeoutError } from './render-timeout.js';
|
|
34
|
+
import { logStreamingError } from './logger.js';
|
|
34
35
|
|
|
35
36
|
// ─── Head Injection ──────────────────────────────────────────────────────────
|
|
36
37
|
|
|
@@ -308,7 +309,7 @@ export function createNodeErrorHandler(signal?: AbortSignal): Transform {
|
|
|
308
309
|
return;
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
|
|
312
|
+
logStreamingError({ error });
|
|
312
313
|
transform.push(Buffer.from(NOINDEX_SCRIPT, 'utf-8'));
|
|
313
314
|
transform.end();
|
|
314
315
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { RouteContext } from './types.js';
|
|
12
|
+
import { logRouteError } from './logger.js';
|
|
12
13
|
|
|
13
14
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -122,7 +123,7 @@ async function runHandler(handler: RouteHandler, ctx: RouteContext): Promise<Res
|
|
|
122
123
|
const res = await handler(ctx);
|
|
123
124
|
return mergeResponseHeaders(res, ctx.headers);
|
|
124
125
|
} catch (error) {
|
|
125
|
-
|
|
126
|
+
logRouteError({ method: ctx.req.method, path: new URL(ctx.req.url).pathname, error });
|
|
126
127
|
return new Response(null, { status: 500 });
|
|
127
128
|
}
|
|
128
129
|
}
|
package/src/server/ssr-render.ts
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
import type { ReactNode } from 'react';
|
|
24
24
|
import { renderToReadableStream } from 'react-dom/server';
|
|
25
25
|
|
|
26
|
-
import { formatSsrError } from './error-formatter.js';
|
|
27
26
|
import { createRenderTimeout, RenderTimeoutError } from './render-timeout.js';
|
|
27
|
+
import { logRenderError, logStreamingError } from './logger.js';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Inline script that injects <meta name="robots" content="noindex"> into <head>.
|
|
@@ -187,7 +187,7 @@ export async function renderSsrNodeStream(
|
|
|
187
187
|
|
|
188
188
|
onError(error: unknown) {
|
|
189
189
|
if (isAbortError(error) || signal?.aborted) return;
|
|
190
|
-
|
|
190
|
+
logRenderError({ method: '', path: '', error });
|
|
191
191
|
},
|
|
192
192
|
});
|
|
193
193
|
|
|
@@ -207,10 +207,14 @@ export async function renderSsrNodeStream(
|
|
|
207
207
|
renderTimeout.signal.addEventListener(
|
|
208
208
|
'abort',
|
|
209
209
|
() => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
logRenderError({
|
|
211
|
+
method: '',
|
|
212
|
+
path: '',
|
|
213
|
+
error: new Error(
|
|
214
|
+
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
215
|
+
'A Suspense component or downstream fetch may be hanging.'
|
|
216
|
+
),
|
|
217
|
+
});
|
|
214
218
|
abort(renderTimeout.signal.reason);
|
|
215
219
|
},
|
|
216
220
|
{ once: true }
|
|
@@ -269,13 +273,17 @@ async function renderViaReadableStream(
|
|
|
269
273
|
onError(error: unknown) {
|
|
270
274
|
if (isAbortError(error) || effectiveSignal?.aborted) return;
|
|
271
275
|
if (error instanceof RenderTimeoutError) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
logRenderError({
|
|
277
|
+
method: '',
|
|
278
|
+
path: '',
|
|
279
|
+
error: new Error(
|
|
280
|
+
`SSR render timed out after ${timeoutMs}ms — aborting. ` +
|
|
281
|
+
'A Suspense component or downstream fetch may be hanging.'
|
|
282
|
+
),
|
|
283
|
+
});
|
|
276
284
|
return;
|
|
277
285
|
}
|
|
278
|
-
|
|
286
|
+
logRenderError({ method: '', path: '', error });
|
|
279
287
|
},
|
|
280
288
|
});
|
|
281
289
|
} catch (error) {
|
|
@@ -346,7 +354,7 @@ export function wrapStreamWithErrorHandling(
|
|
|
346
354
|
controller.close();
|
|
347
355
|
return;
|
|
348
356
|
}
|
|
349
|
-
|
|
357
|
+
logStreamingError({ error });
|
|
350
358
|
controller.enqueue(encoder.encode(NOINDEX_SCRIPT));
|
|
351
359
|
controller.close();
|
|
352
360
|
}
|