@webpieces/bunyan 0.4.395 → 0.4.397
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 +12 -8
- package/package.json +3 -3
- package/src/BunyanConsoleFactory.d.ts +1 -1
- package/src/BunyanConsoleFactory.js +1 -1
- package/src/BunyanConsoleFactory.js.map +1 -1
- package/src/BunyanFactoryBase.d.ts +8 -1
- package/src/BunyanFactoryBase.js +13 -2
- package/src/BunyanFactoryBase.js.map +1 -1
- package/src/BunyanGcpFactory.d.ts +1 -1
- package/src/BunyanGcpFactory.js +1 -1
- package/src/BunyanGcpFactory.js.map +1 -1
- package/src/BunyanLogger.js +11 -14
- package/src/BunyanLogger.js.map +1 -1
- package/src/ChunkingRawStream.d.ts +52 -0
- package/src/ChunkingRawStream.js +104 -0
- package/src/ChunkingRawStream.js.map +1 -0
- package/src/index.d.ts +9 -4
- package/src/index.js +11 -5
- package/src/index.js.map +1 -1
- package/src/streams.d.ts +5 -0
- package/src/streams.js +21 -1
- package/src/streams.js.map +1 -1
package/README.md
CHANGED
|
@@ -20,9 +20,10 @@ import { LogManager, HeaderRegistry } from '@webpieces/core-util';
|
|
|
20
20
|
import { ServiceInfo } from '@webpieces/core-util';
|
|
21
21
|
import { BunyanGcpFactory, BunyanConsoleFactory } from '@webpieces/bunyan';
|
|
22
22
|
|
|
23
|
-
// FIRST:
|
|
24
|
-
// before you build one — a forgotten call throws at startup rather than shipping
|
|
25
|
-
|
|
23
|
+
// FIRST: identify this service. Both factories read name+version in their CONSTRUCTOR, so this
|
|
24
|
+
// must come before you build one — a forgotten call throws at startup rather than shipping logs
|
|
25
|
+
// that cannot say which build emitted them.
|
|
26
|
+
ServiceInfo.setInfo('my-service', '2.1.0');
|
|
26
27
|
|
|
27
28
|
const loggerFactory = process.env.K_SERVICE
|
|
28
29
|
? new BunyanGcpFactory()
|
|
@@ -42,10 +43,13 @@ Credentials on the instance (automatic on Cloud Run), exactly as the source serv
|
|
|
42
43
|
|
|
43
44
|
There are none — both factories take no arguments.
|
|
44
45
|
|
|
45
|
-
- **Service name** — from `ServiceInfo.
|
|
46
|
-
becomes bunyan's mandatory root-logger `name` and surfaces as `name` in the payload
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
- **Service name + version** — from `ServiceInfo.setInfo(...)` (see above), NOT factory options.
|
|
47
|
+
The name becomes bunyan's mandatory root-logger `name` and surfaces as `name` in the payload;
|
|
48
|
+
the version rides as a bunyan base field and surfaces as `version`. They live in
|
|
49
|
+
`@webpieces/core-util` because they are facts about the SERVICE, not about bunyan: the winston
|
|
50
|
+
backend reads the same values, and `requestIdSource` reads the name (it records which service
|
|
51
|
+
minted a request-id).
|
|
52
|
+
- **`version` is opaque** — a git SHA, a semver tag, a CI build number, whatever identifies your
|
|
53
|
+
build. webpieces neither parses nor derives it; your app decides where it comes from.
|
|
50
54
|
- **Level** — there is deliberately no knob. webpieces does not filter by level; bunyan
|
|
51
55
|
filters at its own default.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/bunyan",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.397",
|
|
4
4
|
"description": "Node-only bunyan LoggerFactory backends for webpieces: Console (local pretty) + GCP (@google-cloud/logging-bunyan), auto-enriched with HeaderRegistry context keys",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
+
"@webpieces/core-util": "0.4.397",
|
|
27
|
+
"@webpieces/core-context": "0.4.397",
|
|
28
28
|
"bunyan": "1.8.15",
|
|
29
29
|
"@google-cloud/logging-bunyan": "5.1.0",
|
|
30
30
|
"@types/bunyan": "1.8.11"
|
|
@@ -5,7 +5,7 @@ import { BunyanFactoryBase } from './BunyanFactoryBase';
|
|
|
5
5
|
* keys as tags — same enrichment as the GCP backend, different rendering. Mirrors
|
|
6
6
|
* the tested trytami local console stream.
|
|
7
7
|
*
|
|
8
|
-
* The service name
|
|
8
|
+
* The service name + version come from {@link ServiceInfo}, which startup must have populated
|
|
9
9
|
* BEFORE constructing this.
|
|
10
10
|
*/
|
|
11
11
|
export declare class BunyanConsoleFactory extends BunyanFactoryBase {
|
|
@@ -9,7 +9,7 @@ const streams_1 = require("./streams");
|
|
|
9
9
|
* keys as tags — same enrichment as the GCP backend, different rendering. Mirrors
|
|
10
10
|
* the tested trytami local console stream.
|
|
11
11
|
*
|
|
12
|
-
* The service name
|
|
12
|
+
* The service name + version come from {@link ServiceInfo}, which startup must have populated
|
|
13
13
|
* BEFORE constructing this.
|
|
14
14
|
*/
|
|
15
15
|
class BunyanConsoleFactory extends BunyanFactoryBase_1.BunyanFactoryBase {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BunyanConsoleFactory.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanConsoleFactory.ts"],"names":[],"mappings":";;;AAAA,2DAAwD;AACxD,uCAAgD;AAEhD;;;;;;;;GAQG;AACH,MAAa,oBAAqB,SAAQ,qCAAiB;IACvD;QACI,KAAK,CAAC,CAAC,IAAA,6BAAmB,GAAE,CAAC,CAAC,CAAC;IACnC,CAAC;CACJ;AAJD,oDAIC","sourcesContent":["import { BunyanFactoryBase } from './BunyanFactoryBase';\nimport { createConsoleStream } from './streams';\n\n/**\n * BunyanConsoleFactory - the LOCAL developer backend. Human-readable, greppable\n * text to stdout (`[LEVEL][time][ctx tags]: message`) with the registered context\n * keys as tags — same enrichment as the GCP backend, different rendering. Mirrors\n * the tested trytami local console stream.\n *\n * The service name
|
|
1
|
+
{"version":3,"file":"BunyanConsoleFactory.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanConsoleFactory.ts"],"names":[],"mappings":";;;AAAA,2DAAwD;AACxD,uCAAgD;AAEhD;;;;;;;;GAQG;AACH,MAAa,oBAAqB,SAAQ,qCAAiB;IACvD;QACI,KAAK,CAAC,CAAC,IAAA,6BAAmB,GAAE,CAAC,CAAC,CAAC;IACnC,CAAC;CACJ;AAJD,oDAIC","sourcesContent":["import { BunyanFactoryBase } from './BunyanFactoryBase';\nimport { createConsoleStream } from './streams';\n\n/**\n * BunyanConsoleFactory - the LOCAL developer backend. Human-readable, greppable\n * text to stdout (`[LEVEL][time][ctx tags]: message`) with the registered context\n * keys as tags — same enrichment as the GCP backend, different rendering. Mirrors\n * the tested trytami local console stream.\n *\n * The service name + version come from {@link ServiceInfo}, which startup must have populated\n * BEFORE constructing this.\n */\nexport class BunyanConsoleFactory extends BunyanFactoryBase {\n constructor() {\n super([createConsoleStream()]);\n }\n}\n"]}
|
|
@@ -11,9 +11,16 @@ import type { Logger as WpLogger, LoggerFactory } from '@webpieces/core-util';
|
|
|
11
11
|
* The root logger's `name` is bunyan's ONE mandatory option (its constructor throws
|
|
12
12
|
* `options.name (string) is required`), and it comes from {@link ServiceInfo} — so an
|
|
13
13
|
* app names itself ONCE, in one place, for logging AND for requestIdSource. Reading it
|
|
14
|
-
* here means a forgotten `ServiceInfo.
|
|
14
|
+
* here means a forgotten `ServiceInfo.setInfo(...)` throws OUR actionable error while
|
|
15
15
|
* the process boots, rather than bunyan's opaque TypeError.
|
|
16
16
|
*
|
|
17
|
+
* `version` rides alongside as a bunyan BASE FIELD (bunyan passes unknown top-level options straight
|
|
18
|
+
* onto every record — it strips only stream/streams/level/serializers/src, and its own format
|
|
19
|
+
* version is `v`, so there is no collision). This backend previously could not stamp a version at
|
|
20
|
+
* all: it lived on winston's factory options, which made "do my logs say which build emitted them?"
|
|
21
|
+
* depend on the logging library rather than on the service. Both backends now read the ONE
|
|
22
|
+
* {@link ServiceInfo}.
|
|
23
|
+
*
|
|
17
24
|
* Per-logger names ride as `loggerName` on each child rather than `name`, because
|
|
18
25
|
* bunyan REFUSES to let a child override `name` (`invalid options.name: child cannot
|
|
19
26
|
* set logger name`).
|
package/src/BunyanFactoryBase.js
CHANGED
|
@@ -16,9 +16,16 @@ const BunyanLogger_1 = require("./BunyanLogger");
|
|
|
16
16
|
* The root logger's `name` is bunyan's ONE mandatory option (its constructor throws
|
|
17
17
|
* `options.name (string) is required`), and it comes from {@link ServiceInfo} — so an
|
|
18
18
|
* app names itself ONCE, in one place, for logging AND for requestIdSource. Reading it
|
|
19
|
-
* here means a forgotten `ServiceInfo.
|
|
19
|
+
* here means a forgotten `ServiceInfo.setInfo(...)` throws OUR actionable error while
|
|
20
20
|
* the process boots, rather than bunyan's opaque TypeError.
|
|
21
21
|
*
|
|
22
|
+
* `version` rides alongside as a bunyan BASE FIELD (bunyan passes unknown top-level options straight
|
|
23
|
+
* onto every record — it strips only stream/streams/level/serializers/src, and its own format
|
|
24
|
+
* version is `v`, so there is no collision). This backend previously could not stamp a version at
|
|
25
|
+
* all: it lived on winston's factory options, which made "do my logs say which build emitted them?"
|
|
26
|
+
* depend on the logging library rather than on the service. Both backends now read the ONE
|
|
27
|
+
* {@link ServiceInfo}.
|
|
28
|
+
*
|
|
22
29
|
* Per-logger names ride as `loggerName` on each child rather than `name`, because
|
|
23
30
|
* bunyan REFUSES to let a child override `name` (`invalid options.name: child cannot
|
|
24
31
|
* set logger name`).
|
|
@@ -27,7 +34,11 @@ class BunyanFactoryBase {
|
|
|
27
34
|
base;
|
|
28
35
|
loggers = new Map();
|
|
29
36
|
constructor(streams) {
|
|
30
|
-
this.base = bunyan_1.default.createLogger({
|
|
37
|
+
this.base = bunyan_1.default.createLogger({
|
|
38
|
+
name: core_util_1.ServiceInfo.getName(),
|
|
39
|
+
version: core_util_1.ServiceInfo.getVersion(),
|
|
40
|
+
streams,
|
|
41
|
+
});
|
|
31
42
|
}
|
|
32
43
|
getLogger(name) {
|
|
33
44
|
let logger = this.loggers.get(name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BunyanFactoryBase.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanFactoryBase.ts"],"names":[],"mappings":";;;;AAAA,4DAA4B;AAE5B,oDAAmD;AACnD,iDAA8C;AAE9C
|
|
1
|
+
{"version":3,"file":"BunyanFactoryBase.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanFactoryBase.ts"],"names":[],"mappings":";;;;AAAA,4DAA4B;AAE5B,oDAAmD;AACnD,iDAA8C;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAsB,iBAAiB;IAClB,IAAI,CAAS;IACb,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;IAEvD,YAAsB,OAAwB;QAC1C,IAAI,CAAC,IAAI,GAAG,gBAAM,CAAC,YAAY,CAAC;YAC5B,IAAI,EAAE,uBAAW,CAAC,OAAO,EAAE;YAC3B,OAAO,EAAE,uBAAW,CAAC,UAAU,EAAE;YACjC,OAAO;SACV,CAAC,CAAC;IACP,CAAC;IAED,SAAS,CAAC,IAAY;QAClB,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,GAAG,IAAI,2BAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AApBD,8CAoBC","sourcesContent":["import Logger from 'bunyan';\nimport type { Logger as WpLogger, LoggerFactory } from '@webpieces/core-util';\nimport { ServiceInfo } from '@webpieces/core-util';\nimport { BunyanLogger } from './BunyanLogger';\n\n/**\n * BunyanFactoryBase - shared plumbing for the bunyan {@link LoggerFactory}\n * backends. Builds ONE underlying bunyan logger with the caller-chosen stream,\n * then hands out a cached {@link BunyanLogger} per name (each a bunyan child\n * carrying `loggerName`). Every logger reads the magic context DIRECTLY from\n * RequestContext, so nothing is threaded through here. Subclasses differ only in\n * the stream they pass up (GCP vs local console).\n *\n * The root logger's `name` is bunyan's ONE mandatory option (its constructor throws\n * `options.name (string) is required`), and it comes from {@link ServiceInfo} — so an\n * app names itself ONCE, in one place, for logging AND for requestIdSource. Reading it\n * here means a forgotten `ServiceInfo.setInfo(...)` throws OUR actionable error while\n * the process boots, rather than bunyan's opaque TypeError.\n *\n * `version` rides alongside as a bunyan BASE FIELD (bunyan passes unknown top-level options straight\n * onto every record — it strips only stream/streams/level/serializers/src, and its own format\n * version is `v`, so there is no collision). This backend previously could not stamp a version at\n * all: it lived on winston's factory options, which made \"do my logs say which build emitted them?\"\n * depend on the logging library rather than on the service. Both backends now read the ONE\n * {@link ServiceInfo}.\n *\n * Per-logger names ride as `loggerName` on each child rather than `name`, because\n * bunyan REFUSES to let a child override `name` (`invalid options.name: child cannot\n * set logger name`).\n */\nexport abstract class BunyanFactoryBase implements LoggerFactory {\n private readonly base: Logger;\n private readonly loggers = new Map<string, WpLogger>();\n\n protected constructor(streams: Logger.Stream[]) {\n this.base = Logger.createLogger({\n name: ServiceInfo.getName(),\n version: ServiceInfo.getVersion(),\n streams,\n });\n }\n\n getLogger(name: string): WpLogger {\n let logger = this.loggers.get(name);\n if (!logger) {\n logger = new BunyanLogger(this.base.child({ loggerName: name }));\n this.loggers.set(name, logger);\n }\n return logger;\n }\n}\n"]}
|
|
@@ -7,7 +7,7 @@ import { BunyanFactoryBase } from './BunyanFactoryBase';
|
|
|
7
7
|
* matches the tested-in-GCP trytami service exactly. Requires GCP Application
|
|
8
8
|
* Default Credentials on the instance.
|
|
9
9
|
*
|
|
10
|
-
* The service name
|
|
10
|
+
* The service name + version come from {@link ServiceInfo}, which startup must have populated
|
|
11
11
|
* BEFORE constructing this.
|
|
12
12
|
*/
|
|
13
13
|
export declare class BunyanGcpFactory extends BunyanFactoryBase {
|
package/src/BunyanGcpFactory.js
CHANGED
|
@@ -11,7 +11,7 @@ const streams_1 = require("./streams");
|
|
|
11
11
|
* matches the tested-in-GCP trytami service exactly. Requires GCP Application
|
|
12
12
|
* Default Credentials on the instance.
|
|
13
13
|
*
|
|
14
|
-
* The service name
|
|
14
|
+
* The service name + version come from {@link ServiceInfo}, which startup must have populated
|
|
15
15
|
* BEFORE constructing this.
|
|
16
16
|
*/
|
|
17
17
|
class BunyanGcpFactory extends BunyanFactoryBase_1.BunyanFactoryBase {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BunyanGcpFactory.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanGcpFactory.ts"],"names":[],"mappings":";;;AAAA,2DAAwD;AACxD,uCAAoD;AAEpD;;;;;;;;;;GAUG;AACH,MAAa,gBAAiB,SAAQ,qCAAiB;IACnD;QACI,KAAK,CAAC,CAAC,IAAA,iCAAuB,GAAE,CAAC,CAAC,CAAC;IACvC,CAAC;CACJ;AAJD,4CAIC","sourcesContent":["import { BunyanFactoryBase } from './BunyanFactoryBase';\nimport { createGoogleCloudStream } from './streams';\n\n/**\n * BunyanGcpFactory - the GCP backend. Streams to Cloud Logging via\n * @google-cloud/logging-bunyan (which owns the numeric-level→severity mapping,\n * msg→message, trace/httpRequest fields). The logged context keys ride along as\n * structured payload fields, read straight from RequestContext on each line. This\n * matches the tested-in-GCP trytami service exactly. Requires GCP Application\n * Default Credentials on the instance.\n *\n * The service name
|
|
1
|
+
{"version":3,"file":"BunyanGcpFactory.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanGcpFactory.ts"],"names":[],"mappings":";;;AAAA,2DAAwD;AACxD,uCAAoD;AAEpD;;;;;;;;;;GAUG;AACH,MAAa,gBAAiB,SAAQ,qCAAiB;IACnD;QACI,KAAK,CAAC,CAAC,IAAA,iCAAuB,GAAE,CAAC,CAAC,CAAC;IACvC,CAAC;CACJ;AAJD,4CAIC","sourcesContent":["import { BunyanFactoryBase } from './BunyanFactoryBase';\nimport { createGoogleCloudStream } from './streams';\n\n/**\n * BunyanGcpFactory - the GCP backend. Streams to Cloud Logging via\n * @google-cloud/logging-bunyan (which owns the numeric-level→severity mapping,\n * msg→message, trace/httpRequest fields). The logged context keys ride along as\n * structured payload fields, read straight from RequestContext on each line. This\n * matches the tested-in-GCP trytami service exactly. Requires GCP Application\n * Default Credentials on the instance.\n *\n * The service name + version come from {@link ServiceInfo}, which startup must have populated\n * BEFORE constructing this.\n */\nexport class BunyanGcpFactory extends BunyanFactoryBase {\n constructor() {\n super([createGoogleCloudStream()]);\n }\n}\n"]}
|
package/src/BunyanLogger.js
CHANGED
|
@@ -8,21 +8,18 @@ const LoggedError_1 = require("./LoggedError");
|
|
|
8
8
|
const MISSING_CONTEXT_MESSAGE = 'Log emitted OUTSIDE RequestContext.run(...) — every request must be wrapped in ' +
|
|
9
9
|
'RequestContext.run() by a server filter. That filter appears to be missing: correlation ' +
|
|
10
10
|
'fields (requestId, tenant, ...) will be absent from logs until it is added. Reported once.';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The error, whole. There is deliberately NO size guard here any more.
|
|
13
|
+
*
|
|
14
|
+
* This used to truncate an error over 100KB down to 5 stack frames and a 100-character message, to
|
|
15
|
+
* keep one giant stack from blowing past Cloud Logging's per-entry limit. That traded the wrong
|
|
16
|
+
* thing away: a stack trace big enough to trip the limit is precisely the one worth reading, and it
|
|
17
|
+
* arrived pre-shredded. The GCP stream now SPLITS an oversized record across several complete
|
|
18
|
+
* records instead (see ChunkingRawStream), so the whole stack survives and nothing is lost — which
|
|
19
|
+
* makes truncating here strictly harmful.
|
|
20
|
+
*/
|
|
20
21
|
function normalizeError(err) {
|
|
21
|
-
|
|
22
|
-
if (JSON.stringify({ err: full }).length <= MAX_ERR_SERIALIZED) {
|
|
23
|
-
return full;
|
|
24
|
-
}
|
|
25
|
-
return new LoggedError_1.LoggedError(`error too long: ${err.name}`, err.message.substring(0, 100), truncateStack(err.stack, 5));
|
|
22
|
+
return new LoggedError_1.LoggedError(err.name, err.message, err.stack);
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
25
|
* BunyanLogger - a webpieces {@link WpLogger} backed by a bunyan logger (one per
|
package/src/BunyanLogger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BunyanLogger.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanLogger.ts"],"names":[],"mappings":";;;AAEA,0DAAyD;AACzD,+CAA4C;AAE5C,+EAA+E;AAC/E,4DAA4D;AAC5D,MAAM,uBAAuB,GACzB,iFAAiF;IACjF,0FAA0F;IAC1F,4FAA4F,CAAC;AAEjG
|
|
1
|
+
{"version":3,"file":"BunyanLogger.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/BunyanLogger.ts"],"names":[],"mappings":";;;AAEA,0DAAyD;AACzD,+CAA4C;AAE5C,+EAA+E;AAC/E,4DAA4D;AAC5D,MAAM,uBAAuB,GACzB,iFAAiF;IACjF,0FAA0F;IAC1F,4FAA4F,CAAC;AAEjG;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,GAAU;IAC9B,OAAO,IAAI,yBAAW,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAa,YAAY;IAKQ;IAJ7B,4EAA4E;IAC5E,kFAAkF;IAC1E,sBAAsB,GAAG,KAAK,CAAC;IAEvC,YAA6B,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAE/C,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAW;QAC7B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAW;QAC7B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAEO,WAAW,CAAC,GAAW;QAC3B,MAAM,MAAM,GAAkD,EAAE,CAAC;QACjE,IAAI,CAAC,6BAAc,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAChC,OAAO,MAAM,CAAC;QAClB,CAAC;QAED,6FAA6F;QAC7F,iGAAiG;QACjG,8FAA8F;QAC9F,6CAA6C;QAC7C,6BAAc,CAAC,wBAAwB,EAAE,CAAC,OAAO,CAAC,CAAC,KAAsB,EAAE,IAAY,EAAE,EAAE;YACvF,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,IAAI,GAAG,EAAE,CAAC;YACN,MAAM,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,8EAA8E;IAC9E,8EAA8E;IACtE,wBAAwB;QAC5B,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAC9B,OAAO;QACX,CAAC;QACD,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC/C,CAAC;CACJ;AAxDD,oCAwDC","sourcesContent":["import type Logger from 'bunyan';\nimport type { Logger as WpLogger } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { LoggedError } from './LoggedError';\n\n// A log line with no active RequestContext = a missing request-wrapping server\n// filter. We report that once (below), never on every line.\nconst MISSING_CONTEXT_MESSAGE =\n 'Log emitted OUTSIDE RequestContext.run(...) — every request must be wrapped in ' +\n 'RequestContext.run() by a server filter. That filter appears to be missing: correlation ' +\n 'fields (requestId, tenant, ...) will be absent from logs until it is added. Reported once.';\n\n/**\n * The error, whole. There is deliberately NO size guard here any more.\n *\n * This used to truncate an error over 100KB down to 5 stack frames and a 100-character message, to\n * keep one giant stack from blowing past Cloud Logging's per-entry limit. That traded the wrong\n * thing away: a stack trace big enough to trip the limit is precisely the one worth reading, and it\n * arrived pre-shredded. The GCP stream now SPLITS an oversized record across several complete\n * records instead (see ChunkingRawStream), so the whole stack survives and nothing is lost — which\n * makes truncating here strictly harmful.\n */\nfunction normalizeError(err: Error): LoggedError {\n return new LoggedError(err.name, err.message, err.stack);\n}\n\n/**\n * BunyanLogger - a webpieces {@link WpLogger} backed by a bunyan logger (one per\n * name, created as a bunyan child carrying `loggerName`). On every call it reads\n * the logged HeaderRegistry keys DIRECTLY from the active {@link RequestContext}\n * (secured values masked) into the bunyan fields object, and normalizes an optional\n * Error into `err: { name, message, stack }` — matching the tested trytami AppLogger.\n * The GCP stream then serializes those fields into the structured log payload.\n *\n * This context-reading is INLINED here (and duplicated in the winston logger) on\n * purpose: it must run ONLY when a bunyan/winston backend is installed, never for\n * the plain ConsoleLogger — so it does not belong on RequestContext.\n */\nexport class BunyanLogger implements WpLogger {\n // One-shot latch (see MISSING_CONTEXT_MESSAGE) — flips true after the first\n // out-of-context line so we report the missing filter exactly once, not per line.\n private reportedMissingContext = false;\n\n constructor(private readonly bunyan: Logger) {}\n\n trace(message: string, err?: Error): void {\n this.bunyan.trace(this.buildFields(err), message);\n }\n\n debug(message: string, err?: Error): void {\n this.bunyan.debug(this.buildFields(err), message);\n }\n\n info(message: string, err?: Error): void {\n this.bunyan.info(this.buildFields(err), message);\n }\n\n warn(message: string, err?: Error): void {\n this.bunyan.warn(this.buildFields(err), message);\n }\n\n error(message: string, err?: Error): void {\n this.bunyan.error(this.buildFields(err), message);\n }\n\n private buildFields(err?: Error): Record<string, string | object | LoggedError> {\n const fields: Record<string, string | object | LoggedError> = {};\n if (!RequestContext.isActive()) {\n this.reportMissingContextOnce();\n return fields;\n }\n\n // ONE loop, in HeaderRegistry.buildStructuredLogFields — the registry owns the keys and each\n // ContextKey masks its own value. Values may be OBJECTS (the `api` tag), so an object-valued key\n // nests into the structured payload (bunyan's GCP stream serializes fields) rather than being\n // dropped by the string-only buildLogFields.\n RequestContext.buildStructuredLogFields().forEach((value: string | object, name: string) => {\n fields[name] = value;\n });\n if (err) {\n fields['err'] = normalizeError(err);\n }\n return fields;\n }\n\n // Report the missing request-wrapping filter once. Uses the RAW bunyan logger\n // (not buildFields()), so there is no re-entrancy — just a single extra line.\n private reportMissingContextOnce(): void {\n if (this.reportedMissingContext) {\n return;\n }\n this.reportedMissingContext = true;\n this.bunyan.error(MISSING_CONTEXT_MESSAGE);\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Writable } from 'stream';
|
|
2
|
+
type JsonValue = string | number | boolean | bigint | object | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* A bunyan record as it reaches a `type: 'raw'` stream: the standard fields (v, level, name, time,
|
|
5
|
+
* msg, err, ...) plus whatever context tags the logger injected. Values are whatever JSON holds.
|
|
6
|
+
*/
|
|
7
|
+
type BunyanRecord = Record<string, JsonValue>;
|
|
8
|
+
/**
|
|
9
|
+
* ChunkingRawStream - sits between bunyan and the Cloud Logging stream, SPLITTING an oversized
|
|
10
|
+
* record into several complete records instead of letting it be rejected.
|
|
11
|
+
*
|
|
12
|
+
* WHY: Cloud Logging caps a LogEntry at 256 KiB, and on the API path an oversized entry fails the
|
|
13
|
+
* whole `entries.write` call — `INVALID_ARGUMENT: Log entry with size X exceeds maximum size of
|
|
14
|
+
* 256.0K` — which can take good entries batched alongside it down too. A 300KB stack trace or
|
|
15
|
+
* response body is not exotic; it is exactly what you most want to read.
|
|
16
|
+
*
|
|
17
|
+
* WHY NOT TRUNCATE (what this replaces): the previous guard cut a big error down to 5 stack frames.
|
|
18
|
+
* That kept the line under the limit by destroying its only useful content. Splitting keeps all of
|
|
19
|
+
* it, addressable by `jsonPayload.logChunk.uid`.
|
|
20
|
+
*
|
|
21
|
+
* WHY A STREAM WRAPPER, not the BunyanLogger: bunyan's own machinery (and anything else writing to
|
|
22
|
+
* the logger) reaches the stream directly, and the stream is where the size limit actually lives.
|
|
23
|
+
* `loggingBunyan.stream()` is `type: 'raw'`, so we receive the record OBJECT — no parsing needed,
|
|
24
|
+
* and rebuilding a piece is just a field swap.
|
|
25
|
+
*
|
|
26
|
+
* GCP-ONLY: wired in by {@link createGoogleCloudStream}. The local console stream is untouched — a
|
|
27
|
+
* dev terminal has no size limit, and splitting there would only hurt readability.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ChunkingRawStream extends Writable {
|
|
30
|
+
private readonly target;
|
|
31
|
+
private readonly budgetBytes;
|
|
32
|
+
constructor(target: Writable, budgetBytes?: number);
|
|
33
|
+
_write(record: BunyanRecord, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
34
|
+
/** The record as-is when it fits; otherwise one complete record per chunk. */
|
|
35
|
+
private split;
|
|
36
|
+
/** One piece: every original field, with `msg`/`err.stack` replaced and a `logChunk` tag added. */
|
|
37
|
+
private buildRecord;
|
|
38
|
+
/**
|
|
39
|
+
* Serialized size of the record.
|
|
40
|
+
*
|
|
41
|
+
* Measured as JSON even though this path ships over gRPC/protobuf, where the true size differs.
|
|
42
|
+
* JSON over-counts (every key is spelled out, every string escaped), and over-counting is the
|
|
43
|
+
* safe direction: we chunk slightly sooner than strictly needed rather than one byte too late.
|
|
44
|
+
*
|
|
45
|
+
* The replacer is what makes this TOTAL, with no try/catch: a plain JSON.stringify throws on a
|
|
46
|
+
* circular value — real here, since request/response object cycles are exactly why the winston
|
|
47
|
+
* backend runs safe-stable-stringify — and on a bigint. A measurement that throws would take
|
|
48
|
+
* down the very log line this class exists to save.
|
|
49
|
+
*/
|
|
50
|
+
private serializedBytes;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChunkingRawStream = void 0;
|
|
4
|
+
const stream_1 = require("stream");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const LoggedError_1 = require("./LoggedError");
|
|
7
|
+
/**
|
|
8
|
+
* ChunkingRawStream - sits between bunyan and the Cloud Logging stream, SPLITTING an oversized
|
|
9
|
+
* record into several complete records instead of letting it be rejected.
|
|
10
|
+
*
|
|
11
|
+
* WHY: Cloud Logging caps a LogEntry at 256 KiB, and on the API path an oversized entry fails the
|
|
12
|
+
* whole `entries.write` call — `INVALID_ARGUMENT: Log entry with size X exceeds maximum size of
|
|
13
|
+
* 256.0K` — which can take good entries batched alongside it down too. A 300KB stack trace or
|
|
14
|
+
* response body is not exotic; it is exactly what you most want to read.
|
|
15
|
+
*
|
|
16
|
+
* WHY NOT TRUNCATE (what this replaces): the previous guard cut a big error down to 5 stack frames.
|
|
17
|
+
* That kept the line under the limit by destroying its only useful content. Splitting keeps all of
|
|
18
|
+
* it, addressable by `jsonPayload.logChunk.uid`.
|
|
19
|
+
*
|
|
20
|
+
* WHY A STREAM WRAPPER, not the BunyanLogger: bunyan's own machinery (and anything else writing to
|
|
21
|
+
* the logger) reaches the stream directly, and the stream is where the size limit actually lives.
|
|
22
|
+
* `loggingBunyan.stream()` is `type: 'raw'`, so we receive the record OBJECT — no parsing needed,
|
|
23
|
+
* and rebuilding a piece is just a field swap.
|
|
24
|
+
*
|
|
25
|
+
* GCP-ONLY: wired in by {@link createGoogleCloudStream}. The local console stream is untouched — a
|
|
26
|
+
* dev terminal has no size limit, and splitting there would only hurt readability.
|
|
27
|
+
*/
|
|
28
|
+
class ChunkingRawStream extends stream_1.Writable {
|
|
29
|
+
target;
|
|
30
|
+
budgetBytes;
|
|
31
|
+
constructor(target, budgetBytes = core_util_1.GCP_LOG_BUDGET_BYTES) {
|
|
32
|
+
// objectMode: bunyan hands a raw stream the record OBJECT, not a serialized line.
|
|
33
|
+
super({ objectMode: true });
|
|
34
|
+
this.target = target;
|
|
35
|
+
this.budgetBytes = budgetBytes;
|
|
36
|
+
}
|
|
37
|
+
_write(record, _encoding, callback) {
|
|
38
|
+
for (const piece of this.split(record)) {
|
|
39
|
+
this.target.write(piece);
|
|
40
|
+
}
|
|
41
|
+
callback();
|
|
42
|
+
}
|
|
43
|
+
/** The record as-is when it fits; otherwise one complete record per chunk. */
|
|
44
|
+
split(record) {
|
|
45
|
+
const renderedBytes = this.serializedBytes(record);
|
|
46
|
+
if (renderedBytes <= this.budgetBytes) {
|
|
47
|
+
// The overwhelmingly common path — through untouched, no tag added.
|
|
48
|
+
return [record];
|
|
49
|
+
}
|
|
50
|
+
const message = typeof record['msg'] === 'string' ? record['msg'] : String(record['msg'] ?? '');
|
|
51
|
+
const err = record['err'];
|
|
52
|
+
const stack = err?.stack;
|
|
53
|
+
const budgets = core_util_1.LogChunker.chunkBudgets(renderedBytes, this.budgetBytes, message, stack ?? '');
|
|
54
|
+
const messageChunks = core_util_1.LogChunker.chunk(message, budgets.firstBudget);
|
|
55
|
+
const stackChunks = core_util_1.LogChunker.chunk(stack ?? '', budgets.secondBudget);
|
|
56
|
+
const uid = core_util_1.LogChunker.newUid();
|
|
57
|
+
const total = Math.max(messageChunks.length, stackChunks.length);
|
|
58
|
+
const pieces = [];
|
|
59
|
+
for (let index = 0; index < total; index++) {
|
|
60
|
+
pieces.push(this.buildRecord(record, messageChunks[index] ?? '', err === undefined ? undefined : new LoggedError_1.LoggedError(err.name, err.message, stackChunks[index] ?? ''), new core_util_1.LogChunkInfo(uid, index, total)));
|
|
61
|
+
}
|
|
62
|
+
return pieces;
|
|
63
|
+
}
|
|
64
|
+
/** One piece: every original field, with `msg`/`err.stack` replaced and a `logChunk` tag added. */
|
|
65
|
+
buildRecord(record, messageChunk, err, chunkInfo) {
|
|
66
|
+
const piece = Object.assign({}, record);
|
|
67
|
+
piece['msg'] = messageChunk;
|
|
68
|
+
piece['logChunk'] = chunkInfo;
|
|
69
|
+
if (err !== undefined) {
|
|
70
|
+
piece['err'] = err;
|
|
71
|
+
}
|
|
72
|
+
return piece;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Serialized size of the record.
|
|
76
|
+
*
|
|
77
|
+
* Measured as JSON even though this path ships over gRPC/protobuf, where the true size differs.
|
|
78
|
+
* JSON over-counts (every key is spelled out, every string escaped), and over-counting is the
|
|
79
|
+
* safe direction: we chunk slightly sooner than strictly needed rather than one byte too late.
|
|
80
|
+
*
|
|
81
|
+
* The replacer is what makes this TOTAL, with no try/catch: a plain JSON.stringify throws on a
|
|
82
|
+
* circular value — real here, since request/response object cycles are exactly why the winston
|
|
83
|
+
* backend runs safe-stable-stringify — and on a bigint. A measurement that throws would take
|
|
84
|
+
* down the very log line this class exists to save.
|
|
85
|
+
*/
|
|
86
|
+
serializedBytes(record) {
|
|
87
|
+
const seen = new WeakSet();
|
|
88
|
+
const json = JSON.stringify(record, (_key, value) => {
|
|
89
|
+
if (typeof value === 'bigint') {
|
|
90
|
+
return value.toString();
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'object' && value !== null) {
|
|
93
|
+
if (seen.has(value)) {
|
|
94
|
+
return '[Circular]';
|
|
95
|
+
}
|
|
96
|
+
seen.add(value);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
});
|
|
100
|
+
return core_util_1.LogChunker.byteLength(json);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.ChunkingRawStream = ChunkingRawStream;
|
|
104
|
+
//# sourceMappingURL=ChunkingRawStream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChunkingRawStream.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/ChunkingRawStream.ts"],"names":[],"mappings":";;;AAAA,mCAAkC;AAClC,oDAAsF;AACtF,+CAA4C;AAa5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,iBAAkB,SAAQ,iBAAQ;IAC1B,MAAM,CAAW;IACjB,WAAW,CAAS;IAErC,YAAY,MAAgB,EAAE,cAAsB,gCAAoB;QACpE,kFAAkF;QAClF,KAAK,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACnC,CAAC;IAEQ,MAAM,CACX,MAAoB,EACpB,SAAyB,EACzB,QAAwC;QAExC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,QAAQ,EAAE,CAAC;IACf,CAAC;IAED,8EAA8E;IACtE,KAAK,CAAC,MAAoB;QAC9B,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QACnD,IAAI,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,oEAAoE;YACpE,OAAO,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAChG,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAA4B,CAAC;QACrD,MAAM,KAAK,GAAG,GAAG,EAAE,KAAK,CAAC;QAEzB,MAAM,OAAO,GAAG,sBAAU,CAAC,YAAY,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC/F,MAAM,aAAa,GAAG,sBAAU,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;QACrE,MAAM,WAAW,GAAG,sBAAU,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAExE,MAAM,GAAG,GAAG,sBAAU,CAAC,MAAM,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QACjE,MAAM,MAAM,GAAmB,EAAE,CAAC;QAClC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;YACzC,MAAM,CAAC,IAAI,CACP,IAAI,CAAC,WAAW,CACZ,MAAM,EACN,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,EAC1B,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,yBAAW,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAChG,IAAI,wBAAY,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CACtC,CACJ,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,mGAAmG;IAC3F,WAAW,CACf,MAAoB,EACpB,YAAoB,EACpB,GAA4B,EAC5B,SAAuB;QAEvB,MAAM,KAAK,GAAiB,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACtD,KAAK,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC;QAC5B,KAAK,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;QAC9B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACpB,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC;QACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;;;;;OAWG;IACK,eAAe,CAAC,MAAoB;QACxC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,KAAgB,EAAa,EAAE;YAC9E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC;YACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC9C,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;oBAClB,OAAO,YAAY,CAAC;gBACxB,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;YACD,OAAO,KAAK,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,OAAO,sBAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;CACJ;AAlGD,8CAkGC","sourcesContent":["import { Writable } from 'stream';\nimport { GCP_LOG_BUDGET_BYTES, LogChunker, LogChunkInfo } from '@webpieces/core-util';\nimport { LoggedError } from './LoggedError';\n\n// JSON-shaped value (the widest thing a record field can hold), used instead of `any`/`unknown`\n// which the code rules disallow. Mirrors the JsonValue in streams.ts, plus bigint — which\n// JSON.stringify refuses outright, so serializedBytes below must be able to see and rewrite it.\ntype JsonValue = string | number | boolean | bigint | object | null | undefined;\n\n/**\n * A bunyan record as it reaches a `type: 'raw'` stream: the standard fields (v, level, name, time,\n * msg, err, ...) plus whatever context tags the logger injected. Values are whatever JSON holds.\n */\ntype BunyanRecord = Record<string, JsonValue>;\n\n/**\n * ChunkingRawStream - sits between bunyan and the Cloud Logging stream, SPLITTING an oversized\n * record into several complete records instead of letting it be rejected.\n *\n * WHY: Cloud Logging caps a LogEntry at 256 KiB, and on the API path an oversized entry fails the\n * whole `entries.write` call — `INVALID_ARGUMENT: Log entry with size X exceeds maximum size of\n * 256.0K` — which can take good entries batched alongside it down too. A 300KB stack trace or\n * response body is not exotic; it is exactly what you most want to read.\n *\n * WHY NOT TRUNCATE (what this replaces): the previous guard cut a big error down to 5 stack frames.\n * That kept the line under the limit by destroying its only useful content. Splitting keeps all of\n * it, addressable by `jsonPayload.logChunk.uid`.\n *\n * WHY A STREAM WRAPPER, not the BunyanLogger: bunyan's own machinery (and anything else writing to\n * the logger) reaches the stream directly, and the stream is where the size limit actually lives.\n * `loggingBunyan.stream()` is `type: 'raw'`, so we receive the record OBJECT — no parsing needed,\n * and rebuilding a piece is just a field swap.\n *\n * GCP-ONLY: wired in by {@link createGoogleCloudStream}. The local console stream is untouched — a\n * dev terminal has no size limit, and splitting there would only hurt readability.\n */\nexport class ChunkingRawStream extends Writable {\n private readonly target: Writable;\n private readonly budgetBytes: number;\n\n constructor(target: Writable, budgetBytes: number = GCP_LOG_BUDGET_BYTES) {\n // objectMode: bunyan hands a raw stream the record OBJECT, not a serialized line.\n super({ objectMode: true });\n this.target = target;\n this.budgetBytes = budgetBytes;\n }\n\n override _write(\n record: BunyanRecord,\n _encoding: BufferEncoding,\n callback: (error?: Error | null) => void,\n ): void {\n for (const piece of this.split(record)) {\n this.target.write(piece);\n }\n callback();\n }\n\n /** The record as-is when it fits; otherwise one complete record per chunk. */\n private split(record: BunyanRecord): BunyanRecord[] {\n const renderedBytes = this.serializedBytes(record);\n if (renderedBytes <= this.budgetBytes) {\n // The overwhelmingly common path — through untouched, no tag added.\n return [record];\n }\n\n const message = typeof record['msg'] === 'string' ? record['msg'] : String(record['msg'] ?? '');\n const err = record['err'] as LoggedError | undefined;\n const stack = err?.stack;\n\n const budgets = LogChunker.chunkBudgets(renderedBytes, this.budgetBytes, message, stack ?? '');\n const messageChunks = LogChunker.chunk(message, budgets.firstBudget);\n const stackChunks = LogChunker.chunk(stack ?? '', budgets.secondBudget);\n\n const uid = LogChunker.newUid();\n const total = Math.max(messageChunks.length, stackChunks.length);\n const pieces: BunyanRecord[] = [];\n for (let index = 0; index < total; index++) {\n pieces.push(\n this.buildRecord(\n record,\n messageChunks[index] ?? '',\n err === undefined ? undefined : new LoggedError(err.name, err.message, stackChunks[index] ?? ''),\n new LogChunkInfo(uid, index, total),\n ),\n );\n }\n return pieces;\n }\n\n /** One piece: every original field, with `msg`/`err.stack` replaced and a `logChunk` tag added. */\n private buildRecord(\n record: BunyanRecord,\n messageChunk: string,\n err: LoggedError | undefined,\n chunkInfo: LogChunkInfo,\n ): BunyanRecord {\n const piece: BunyanRecord = Object.assign({}, record);\n piece['msg'] = messageChunk;\n piece['logChunk'] = chunkInfo;\n if (err !== undefined) {\n piece['err'] = err;\n }\n return piece;\n }\n\n /**\n * Serialized size of the record.\n *\n * Measured as JSON even though this path ships over gRPC/protobuf, where the true size differs.\n * JSON over-counts (every key is spelled out, every string escaped), and over-counting is the\n * safe direction: we chunk slightly sooner than strictly needed rather than one byte too late.\n *\n * The replacer is what makes this TOTAL, with no try/catch: a plain JSON.stringify throws on a\n * circular value — real here, since request/response object cycles are exactly why the winston\n * backend runs safe-stable-stringify — and on a bigint. A measurement that throws would take\n * down the very log line this class exists to save.\n */\n private serializedBytes(record: BunyanRecord): number {\n const seen = new WeakSet<object>();\n const json = JSON.stringify(record, (_key: string, value: JsonValue): JsonValue => {\n if (typeof value === 'bigint') {\n return value.toString();\n }\n if (typeof value === 'object' && value !== null) {\n if (seen.has(value)) {\n return '[Circular]';\n }\n seen.add(value);\n }\n return value;\n });\n return LogChunker.byteLength(json);\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { ServiceInfo } from '@webpieces/core-util';
|
|
9
9
|
* import { BunyanGcpFactory, BunyanConsoleFactory } from '@webpieces/bunyan';
|
|
10
10
|
*
|
|
11
|
-
* ServiceInfo.
|
|
11
|
+
* ServiceInfo.setInfo('my-service', '2.1.0'); // FIRST — the factories read it in their constructor
|
|
12
12
|
* const loggerFactory = process.env.K_SERVICE
|
|
13
13
|
* ? new BunyanGcpFactory() // Cloud Logging via @google-cloud/logging-bunyan
|
|
14
14
|
* : new BunyanConsoleFactory(); // local → pretty console
|
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
18
|
* BREAKING (was `new BunyanFactoryOptions('my-service')` passed to each factory): the service name
|
|
19
|
-
* moved to `ServiceInfo.
|
|
19
|
+
* moved to `ServiceInfo.setInfo(...)` in @webpieces/core-util, because it is a fact about the
|
|
20
20
|
* SERVICE, not about bunyan — winston needs the same name, and so does `requestIdSource`. Migration:
|
|
21
|
-
* delete the `BunyanFactoryOptions` import, call `ServiceInfo.
|
|
22
|
-
* building the factory, and drop the ctor argument. A forgotten call throws at startup.
|
|
21
|
+
* delete the `BunyanFactoryOptions` import, call `ServiceInfo.setInfo(<the same string>, <version>)`
|
|
22
|
+
* before building the factory, and drop the ctor argument. A forgotten call throws at startup.
|
|
23
|
+
*
|
|
24
|
+
* NEW: every line now also carries `version` (the second setInfo arg) — this backend could not stamp
|
|
25
|
+
* a build version before. It is opaque: a git SHA, a semver tag, a CI build number, whatever
|
|
26
|
+
* identifies your build.
|
|
23
27
|
*
|
|
24
28
|
* Both backends auto-enrich every line with the logged context keys, read
|
|
25
29
|
* DIRECTLY from the active RequestContext (@webpieces/core-context) on each line —
|
|
@@ -32,4 +36,5 @@ export { BunyanGcpFactory } from './BunyanGcpFactory';
|
|
|
32
36
|
export { BunyanConsoleFactory } from './BunyanConsoleFactory';
|
|
33
37
|
export { BunyanLogger } from './BunyanLogger';
|
|
34
38
|
export { createGoogleCloudStream, createConsoleStream } from './streams';
|
|
39
|
+
export { ChunkingRawStream } from './ChunkingRawStream';
|
|
35
40
|
export { LEVEL_TO_BUNYAN, logLevelToBunyanLevel } from './levels';
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logLevelToBunyanLevel = exports.LEVEL_TO_BUNYAN = exports.createConsoleStream = exports.createGoogleCloudStream = exports.BunyanLogger = exports.BunyanConsoleFactory = exports.BunyanGcpFactory = void 0;
|
|
3
|
+
exports.logLevelToBunyanLevel = exports.LEVEL_TO_BUNYAN = exports.ChunkingRawStream = exports.createConsoleStream = exports.createGoogleCloudStream = exports.BunyanLogger = exports.BunyanConsoleFactory = exports.BunyanGcpFactory = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @webpieces/bunyan
|
|
6
6
|
*
|
|
@@ -11,7 +11,7 @@ exports.logLevelToBunyanLevel = exports.LEVEL_TO_BUNYAN = exports.createConsoleS
|
|
|
11
11
|
* import { ServiceInfo } from '@webpieces/core-util';
|
|
12
12
|
* import { BunyanGcpFactory, BunyanConsoleFactory } from '@webpieces/bunyan';
|
|
13
13
|
*
|
|
14
|
-
* ServiceInfo.
|
|
14
|
+
* ServiceInfo.setInfo('my-service', '2.1.0'); // FIRST — the factories read it in their constructor
|
|
15
15
|
* const loggerFactory = process.env.K_SERVICE
|
|
16
16
|
* ? new BunyanGcpFactory() // Cloud Logging via @google-cloud/logging-bunyan
|
|
17
17
|
* : new BunyanConsoleFactory(); // local → pretty console
|
|
@@ -19,10 +19,14 @@ exports.logLevelToBunyanLevel = exports.LEVEL_TO_BUNYAN = exports.createConsoleS
|
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
21
|
* BREAKING (was `new BunyanFactoryOptions('my-service')` passed to each factory): the service name
|
|
22
|
-
* moved to `ServiceInfo.
|
|
22
|
+
* moved to `ServiceInfo.setInfo(...)` in @webpieces/core-util, because it is a fact about the
|
|
23
23
|
* SERVICE, not about bunyan — winston needs the same name, and so does `requestIdSource`. Migration:
|
|
24
|
-
* delete the `BunyanFactoryOptions` import, call `ServiceInfo.
|
|
25
|
-
* building the factory, and drop the ctor argument. A forgotten call throws at startup.
|
|
24
|
+
* delete the `BunyanFactoryOptions` import, call `ServiceInfo.setInfo(<the same string>, <version>)`
|
|
25
|
+
* before building the factory, and drop the ctor argument. A forgotten call throws at startup.
|
|
26
|
+
*
|
|
27
|
+
* NEW: every line now also carries `version` (the second setInfo arg) — this backend could not stamp
|
|
28
|
+
* a build version before. It is opaque: a git SHA, a semver tag, a CI build number, whatever
|
|
29
|
+
* identifies your build.
|
|
26
30
|
*
|
|
27
31
|
* Both backends auto-enrich every line with the logged context keys, read
|
|
28
32
|
* DIRECTLY from the active RequestContext (@webpieces/core-context) on each line —
|
|
@@ -40,6 +44,8 @@ Object.defineProperty(exports, "BunyanLogger", { enumerable: true, get: function
|
|
|
40
44
|
var streams_1 = require("./streams");
|
|
41
45
|
Object.defineProperty(exports, "createGoogleCloudStream", { enumerable: true, get: function () { return streams_1.createGoogleCloudStream; } });
|
|
42
46
|
Object.defineProperty(exports, "createConsoleStream", { enumerable: true, get: function () { return streams_1.createConsoleStream; } });
|
|
47
|
+
var ChunkingRawStream_1 = require("./ChunkingRawStream");
|
|
48
|
+
Object.defineProperty(exports, "ChunkingRawStream", { enumerable: true, get: function () { return ChunkingRawStream_1.ChunkingRawStream; } });
|
|
43
49
|
var levels_1 = require("./levels");
|
|
44
50
|
Object.defineProperty(exports, "LEVEL_TO_BUNYAN", { enumerable: true, get: function () { return levels_1.LEVEL_TO_BUNYAN; } });
|
|
45
51
|
Object.defineProperty(exports, "logLevelToBunyanLevel", { enumerable: true, get: function () { return levels_1.logLevelToBunyanLevel; } });
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/index.ts"],"names":[],"mappings":";;;AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAC7B,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AACrB,qCAAyE;AAAhE,kHAAA,uBAAuB,OAAA;AAAE,8GAAA,mBAAmB,OAAA;AACrD,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,mCAAkE;AAAzD,yGAAA,eAAe,OAAA;AAAE,+GAAA,qBAAqB,OAAA","sourcesContent":["/**\n * @webpieces/bunyan\n *\n * Node-only bunyan {@link LoggerFactory} backends for webpieces. Install one at\n * startup via `LogManager.setFactory(...)`:\n *\n * ```ts\n * import { ServiceInfo } from '@webpieces/core-util';\n * import { BunyanGcpFactory, BunyanConsoleFactory } from '@webpieces/bunyan';\n *\n * ServiceInfo.setInfo('my-service', '2.1.0'); // FIRST — the factories read it in their constructor\n * const loggerFactory = process.env.K_SERVICE\n * ? new BunyanGcpFactory() // Cloud Logging via @google-cloud/logging-bunyan\n * : new BunyanConsoleFactory(); // local → pretty console\n * // hand to setupRuntime(new RuntimeSetupOptions(loggerFactory, ...))\n * ```\n *\n * BREAKING (was `new BunyanFactoryOptions('my-service')` passed to each factory): the service name\n * moved to `ServiceInfo.setInfo(...)` in @webpieces/core-util, because it is a fact about the\n * SERVICE, not about bunyan — winston needs the same name, and so does `requestIdSource`. Migration:\n * delete the `BunyanFactoryOptions` import, call `ServiceInfo.setInfo(<the same string>, <version>)`\n * before building the factory, and drop the ctor argument. A forgotten call throws at startup.\n *\n * NEW: every line now also carries `version` (the second setInfo arg) — this backend could not stamp\n * a build version before. It is opaque: a git SHA, a semver tag, a CI build number, whatever\n * identifies your build.\n *\n * Both backends auto-enrich every line with the logged context keys, read\n * DIRECTLY from the active RequestContext (@webpieces/core-context) on each line —\n * no ContextReader is threaded in. There is no level knob: bunyan filters at its\n * own default.\n *\n * @packageDocumentation\n */\nexport { BunyanGcpFactory } from './BunyanGcpFactory';\nexport { BunyanConsoleFactory } from './BunyanConsoleFactory';\nexport { BunyanLogger } from './BunyanLogger';\nexport { createGoogleCloudStream, createConsoleStream } from './streams';\nexport { ChunkingRawStream } from './ChunkingRawStream';\nexport { LEVEL_TO_BUNYAN, logLevelToBunyanLevel } from './levels';\n"]}
|
package/src/streams.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ import Logger from 'bunyan';
|
|
|
5
5
|
* to @google-cloud/logging-bunyan, exactly as the tested trytami service does.
|
|
6
6
|
* Sends to the Cloud Logging API (needs ADC on the instance).
|
|
7
7
|
*
|
|
8
|
+
* Records pass through a {@link ChunkingRawStream} first, because Cloud Logging caps a LogEntry at
|
|
9
|
+
* 256 KiB and rejects the whole `entries.write` call when one entry exceeds it — so a single fat
|
|
10
|
+
* response body or stack trace can take a batch of good entries down with it. Oversized records are
|
|
11
|
+
* SPLIT into several complete records sharing a `logChunk.uid` rather than lost or truncated.
|
|
12
|
+
*
|
|
8
13
|
* We do NOT filter by level — that is bunyan's job. The stream is created at
|
|
9
14
|
* bunyan's default level ('info'); there is no webpieces level knob.
|
|
10
15
|
*/
|
package/src/streams.js
CHANGED
|
@@ -6,12 +6,20 @@ const tslib_1 = require("tslib");
|
|
|
6
6
|
const stream_1 = require("stream");
|
|
7
7
|
const bunyan_1 = tslib_1.__importDefault(require("bunyan"));
|
|
8
8
|
const logging_bunyan_1 = require("@google-cloud/logging-bunyan");
|
|
9
|
+
const ChunkingRawStream_1 = require("./ChunkingRawStream");
|
|
9
10
|
// bunyan record fields that are structural / rendered specially, so they are not
|
|
10
11
|
// shown as context tags in the local console line.
|
|
12
|
+
//
|
|
13
|
+
// `name` (the service) and `version` (the build) are ServiceInfo base fields riding on EVERY
|
|
14
|
+
// record. They earn their keep in GCP, where you filter across many services and deploys, but
|
|
15
|
+
// locally each service logs to its own place and you can check git yourself — so as a tag on every
|
|
16
|
+
// single line they are pure noise. Listing them here is the bunyan twin of winston's
|
|
17
|
+
// LOCAL_STRUCTURAL_KEYS; GCP still gets both (that stream does its own formatting).
|
|
11
18
|
const BUNYAN_STD_FIELDS = new Set([
|
|
12
19
|
'v',
|
|
13
20
|
'level',
|
|
14
21
|
'name',
|
|
22
|
+
'version',
|
|
15
23
|
'hostname',
|
|
16
24
|
'pid',
|
|
17
25
|
'time',
|
|
@@ -68,13 +76,25 @@ function writeConsole(line) {
|
|
|
68
76
|
* to @google-cloud/logging-bunyan, exactly as the tested trytami service does.
|
|
69
77
|
* Sends to the Cloud Logging API (needs ADC on the instance).
|
|
70
78
|
*
|
|
79
|
+
* Records pass through a {@link ChunkingRawStream} first, because Cloud Logging caps a LogEntry at
|
|
80
|
+
* 256 KiB and rejects the whole `entries.write` call when one entry exceeds it — so a single fat
|
|
81
|
+
* response body or stack trace can take a batch of good entries down with it. Oversized records are
|
|
82
|
+
* SPLIT into several complete records sharing a `logChunk.uid` rather than lost or truncated.
|
|
83
|
+
*
|
|
71
84
|
* We do NOT filter by level — that is bunyan's job. The stream is created at
|
|
72
85
|
* bunyan's default level ('info'); there is no webpieces level knob.
|
|
73
86
|
*/
|
|
74
87
|
// webpieces-disable no-function-outside-class -- bunyan Stream factory; whole file is bunyan stream/render factories
|
|
75
88
|
function createGoogleCloudStream() {
|
|
76
89
|
const loggingBunyan = new logging_bunyan_1.LoggingBunyan();
|
|
77
|
-
|
|
90
|
+
// stream('info') returns { level, type: 'raw', stream: loggingBunyan } — we keep bunyan's raw
|
|
91
|
+
// contract and only interpose on the stream it writes records to.
|
|
92
|
+
const gcp = loggingBunyan.stream('info');
|
|
93
|
+
return {
|
|
94
|
+
level: gcp.level,
|
|
95
|
+
type: 'raw',
|
|
96
|
+
stream: new ChunkingRawStream_1.ChunkingRawStream(loggingBunyan),
|
|
97
|
+
};
|
|
78
98
|
}
|
|
79
99
|
/**
|
|
80
100
|
* The local dev stream: human-readable text to stdout via {@link writeConsole}.
|
package/src/streams.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/streams.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../../../../packages/logging/bunyan/src/streams.ts"],"names":[],"mappings":";;AAgGA,0DAUC;AAOD,kDAWC;;AA5HD,mCAAkC;AAClC,4DAA4B;AAC5B,iEAA6D;AAE7D,2DAAwD;AAOxD,iFAAiF;AACjF,mDAAmD;AACnD,EAAE;AACF,6FAA6F;AAC7F,8FAA8F;AAC9F,mGAAmG;AACnG,qFAAqF;AACrF,oFAAoF;AACpF,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS;IACtC,GAAG;IACH,OAAO;IACP,MAAM;IACN,SAAS;IACT,UAAU;IACV,KAAK;IACL,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,YAAY;CACf,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,GAAc;IAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACd,CAAC;IACD,uDAAuD;IACvD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACtD,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,IAAY;IAC9B,MAAM,GAAG,GAAiB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,CAAC,gBAAM,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAW,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACnG,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAErC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,SAAS;QACb,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IAEhE,IAAI,OAAO,GAAG,IAAI,SAAS,KAAK,IAAI,KAAK,MAAM,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;IAEhF,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAA4B,CAAC;IAClD,IAAI,GAAG,EAAE,CAAC;QACN,OAAO,IAAI,kBAAkB,CAAC;QAC9B,OAAO,IAAI,gBAAgB,GAAG,CAAC,OAAO,EAAE,CAAC;QACzC,OAAO,IAAI,aAAa,GAAG,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,IAAI,qBAAqB,GAAG,CAAC,KAAK,EAAE,CAAC;QAChD,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,qHAAqH;AACrH,SAAgB,uBAAuB;IACnC,MAAM,aAAa,GAAG,IAAI,8BAAa,EAAE,CAAC;IAC1C,8FAA8F;IAC9F,kEAAkE;IAClE,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,OAAO;QACH,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,IAAI,qCAAiB,CAAC,aAAa,CAAC;KAC/C,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,qHAAqH;AACrH,SAAgB,mBAAmB;IAC/B,MAAM,QAAQ,GAAG,IAAI,iBAAQ,CAAC;QAC1B,KAAK,CAAC,KAAsB,EAAE,SAAyB,EAAE,QAAwC;YAC7F,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC/B,QAAQ,EAAE,CAAC;QACf,CAAC;KACJ,CAAC,CAAC;IACH,OAAO;QACH,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,QAAQ;KACnB,CAAC;AACN,CAAC","sourcesContent":["import { Writable } from 'stream';\nimport Logger from 'bunyan';\nimport { LoggingBunyan } from '@google-cloud/logging-bunyan';\nimport { LoggedError } from './LoggedError';\nimport { ChunkingRawStream } from './ChunkingRawStream';\n\n// A parsed bunyan JSON record: standard fields plus arbitrary injected context\n// tags. Values are whatever JSON holds.\ntype JsonValue = string | number | boolean | object | null;\ntype BunyanRecord = Record<string, JsonValue>;\n\n// bunyan record fields that are structural / rendered specially, so they are not\n// shown as context tags in the local console line.\n//\n// `name` (the service) and `version` (the build) are ServiceInfo base fields riding on EVERY\n// record. They earn their keep in GCP, where you filter across many services and deploys, but\n// locally each service logs to its own place and you can check git yourself — so as a tag on every\n// single line they are pure noise. Listing them here is the bunyan twin of winston's\n// LOCAL_STRUCTURAL_KEYS; GCP still gets both (that stream does its own formatting).\nconst BUNYAN_STD_FIELDS = new Set<string>([\n 'v',\n 'level',\n 'name',\n 'version',\n 'hostname',\n 'pid',\n 'time',\n 'msg',\n 'src',\n 'err',\n 'loggerName',\n]);\n\nfunction formatTime(iso: JsonValue): string {\n if (typeof iso !== 'string') {\n return '';\n }\n // ISO 8601 \"2026-07-08T12:34:56.789Z\" → \"12:34:56.789\"\n const timePart = iso.split('T')[1];\n return timePart ? timePart.replace('Z', '') : iso;\n}\n\n/**\n * Render one bunyan JSON line as a human-readable, greppable console line:\n * `[LEVEL][time][ctx tags]: message` plus multi-line error details. Ported from\n * the tested trytami writeConsole, generalized: every non-structural field\n * (i.e. the injected context keys) becomes a `key:value` tag.\n */\nfunction writeConsole(line: string): void {\n const obj: BunyanRecord = JSON.parse(line);\n const levelName = (Logger.nameFromLevel[obj['level'] as number] ?? 'info').toUpperCase().padEnd(5);\n const time = formatTime(obj['time']);\n\n const tags: string[] = [];\n if (obj['loggerName']) {\n tags.push(`logger:${String(obj['loggerName'])}`);\n }\n for (const key of Object.keys(obj)) {\n if (BUNYAN_STD_FIELDS.has(key)) {\n continue;\n }\n tags.push(`${key}:${String(obj[key])}`);\n }\n const tagStr = tags.length > 0 ? tags.join(', ') : 'no-context';\n\n let message = `[${levelName}][${time}][${tagStr}]: ${String(obj['msg'] ?? '')}`;\n\n const err = obj['err'] as LoggedError | undefined;\n if (err) {\n message += `\\nError Details:`;\n message += `\\n Message: ${err.message}`;\n message += `\\n Name: ${err.name}`;\n if (err.stack) {\n message += `\\n Stack Trace:\\n${err.stack}`;\n }\n }\n\n // This IS a logging backend (the console sink); direct stdout is intentional.\n console.log(message);\n}\n\n/**\n * The GCP stream: delegates ALL structured-JSON formatting (numeric level → GCP\n * severity, msg→message, trace/httpRequest fields, stripping name/hostname/pid)\n * to @google-cloud/logging-bunyan, exactly as the tested trytami service does.\n * Sends to the Cloud Logging API (needs ADC on the instance).\n *\n * Records pass through a {@link ChunkingRawStream} first, because Cloud Logging caps a LogEntry at\n * 256 KiB and rejects the whole `entries.write` call when one entry exceeds it — so a single fat\n * response body or stack trace can take a batch of good entries down with it. Oversized records are\n * SPLIT into several complete records sharing a `logChunk.uid` rather than lost or truncated.\n *\n * We do NOT filter by level — that is bunyan's job. The stream is created at\n * bunyan's default level ('info'); there is no webpieces level knob.\n */\n// webpieces-disable no-function-outside-class -- bunyan Stream factory; whole file is bunyan stream/render factories\nexport function createGoogleCloudStream(): Logger.Stream {\n const loggingBunyan = new LoggingBunyan();\n // stream('info') returns { level, type: 'raw', stream: loggingBunyan } — we keep bunyan's raw\n // contract and only interpose on the stream it writes records to.\n const gcp = loggingBunyan.stream('info');\n return {\n level: gcp.level,\n type: 'raw',\n stream: new ChunkingRawStream(loggingBunyan),\n };\n}\n\n/**\n * The local dev stream: human-readable text to stdout via {@link writeConsole}.\n * No level is set — bunyan filters at its own default ('info').\n */\n// webpieces-disable no-function-outside-class -- bunyan Stream factory; whole file is bunyan stream/render factories\nexport function createConsoleStream(): Logger.Stream {\n const writable = new Writable({\n write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {\n writeConsole(chunk.toString());\n callback();\n },\n });\n return {\n name: 'console',\n stream: writable,\n };\n}\n"]}
|