@omen.foundation/node-microservice-runtime 0.1.10 → 0.1.12
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/env.cjs +22 -15
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +28 -31
- package/dist/env.js.map +1 -1
- package/dist/logger.cjs +59 -3
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +77 -4
- package/dist/logger.js.map +1 -1
- package/dist/runtime.cjs +9 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +25 -22
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/env.ts +33 -36
- package/src/logger.ts +83 -4
- package/src/runtime.ts +29 -25
package/dist/env.cjs
CHANGED
|
@@ -31,29 +31,36 @@ function resolveHealthPort() {
|
|
|
31
31
|
const candidate = base + (process.pid % span);
|
|
32
32
|
return candidate;
|
|
33
33
|
}
|
|
34
|
+
function isInContainer() {
|
|
35
|
+
try {
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
if (fs.existsSync('/.dockerenv')) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
const hostname = process.env.HOSTNAME || '';
|
|
44
|
+
if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||
|
|
48
|
+
process.env.CONTAINER === 'beamable' ||
|
|
49
|
+
!!process.env.ECS_CONTAINER_METADATA_URI ||
|
|
50
|
+
!!process.env.KUBERNETES_SERVICE_HOST) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
34
55
|
function resolveRoutingKey() {
|
|
35
56
|
var _a;
|
|
36
57
|
const raw = (_a = process.env.NAME_PREFIX) !== null && _a !== void 0 ? _a : process.env.ROUTING_KEY;
|
|
37
58
|
if (raw && raw.trim().length > 0) {
|
|
38
59
|
return raw.trim();
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
process.env.CONTAINER === 'beamable' ||
|
|
42
|
-
!!process.env.ECS_CONTAINER_METADATA_URI ||
|
|
43
|
-
!!process.env.KUBERNETES_SERVICE_HOST;
|
|
44
|
-
if (isExplicitlyInContainer) {
|
|
61
|
+
if (isInContainer()) {
|
|
45
62
|
return undefined;
|
|
46
63
|
}
|
|
47
|
-
const hasBeamableEnvVars = !!(process.env.CID && process.env.PID && process.env.HOST && process.env.SECRET);
|
|
48
|
-
const hasNoRoutingKey = !raw || raw.trim().length === 0;
|
|
49
|
-
if (hasBeamableEnvVars && hasNoRoutingKey) {
|
|
50
|
-
try {
|
|
51
|
-
return (0, routing_js_1.getDefaultRoutingKeyForMachine)();
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
throw new Error(`Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${error.message}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
64
|
try {
|
|
58
65
|
return (0, routing_js_1.getDefaultRoutingKeyForMachine)();
|
|
59
66
|
}
|
package/dist/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA4GpD,wBAAgB,qBAAqB,IAAI,iBAAiB,CA4BzD;AAED,wBAAgB,2BAA2B,IAAI,MAAM,CAMpD"}
|
package/dist/env.js
CHANGED
|
@@ -31,44 +31,41 @@ function resolveHealthPort() {
|
|
|
31
31
|
const candidate = base + (process.pid % span);
|
|
32
32
|
return candidate;
|
|
33
33
|
}
|
|
34
|
+
function isInContainer() {
|
|
35
|
+
// Check for Docker container
|
|
36
|
+
try {
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
if (fs.existsSync('/.dockerenv')) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// fs might not be available
|
|
44
|
+
}
|
|
45
|
+
// Check for Docker container hostname pattern (12 hex chars)
|
|
46
|
+
const hostname = process.env.HOSTNAME || '';
|
|
47
|
+
if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Explicit container indicators
|
|
51
|
+
if (process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||
|
|
52
|
+
process.env.CONTAINER === 'beamable' ||
|
|
53
|
+
!!process.env.ECS_CONTAINER_METADATA_URI ||
|
|
54
|
+
!!process.env.KUBERNETES_SERVICE_HOST) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
34
59
|
function resolveRoutingKey() {
|
|
35
60
|
const raw = process.env.NAME_PREFIX ?? process.env.ROUTING_KEY;
|
|
36
61
|
if (raw && raw.trim().length > 0) {
|
|
37
62
|
return raw.trim();
|
|
38
63
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
process.env.CONTAINER === 'beamable' ||
|
|
42
|
-
!!process.env.ECS_CONTAINER_METADATA_URI ||
|
|
43
|
-
!!process.env.KUBERNETES_SERVICE_HOST;
|
|
44
|
-
if (isExplicitlyInContainer) {
|
|
45
|
-
// We're definitely in a container - return undefined (becomes None in Scala)
|
|
64
|
+
// If we're actually in a container, return undefined (deployed service)
|
|
65
|
+
if (isInContainer()) {
|
|
46
66
|
return undefined;
|
|
47
67
|
}
|
|
48
|
-
//
|
|
49
|
-
// Beamable sets CID, PID, HOST, SECRET in containers but NOT routing key
|
|
50
|
-
// BUT: Local dev also has these env vars from .env file, so we need to be more careful
|
|
51
|
-
// If we have the required Beamable env vars but NO routing key AND no explicit container indicators,
|
|
52
|
-
// we might be in a deployed container OR local dev without routing key set
|
|
53
|
-
// For safety, if we're not explicitly in a container, try to generate a routing key for local dev
|
|
54
|
-
const hasBeamableEnvVars = !!(process.env.CID && process.env.PID && process.env.HOST && process.env.SECRET);
|
|
55
|
-
const hasNoRoutingKey = !raw || raw.trim().length === 0;
|
|
56
|
-
// If we have Beamable env vars but no routing key, and we're not explicitly in a container,
|
|
57
|
-
// we might be in local dev - try to generate a routing key
|
|
58
|
-
// Only return undefined if we're explicitly in a container
|
|
59
|
-
if (hasBeamableEnvVars && hasNoRoutingKey) {
|
|
60
|
-
// Try to generate a routing key for local dev
|
|
61
|
-
// If this fails, we'll throw an error (which is fine for local dev)
|
|
62
|
-
try {
|
|
63
|
-
return getDefaultRoutingKeyForMachine();
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
// If we can't generate a routing key, we might be in a container
|
|
67
|
-
// But since we don't have explicit container indicators, assume local dev and throw
|
|
68
|
-
throw new Error(`Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${error.message}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Local development - try to get a routing key
|
|
68
|
+
// Local development - always try to get a routing key
|
|
72
69
|
try {
|
|
73
70
|
return getDefaultRoutingKeyForMachine();
|
|
74
71
|
}
|
package/dist/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\r\nimport { tmpdir } from 'node:os';\r\nimport { join } from 'node:path';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport { getDefaultRoutingKeyForMachine } from './routing.js';\r\n\r\nfunction getBoolean(name: string, defaultValue = false): boolean {\r\n const raw = process.env[name];\r\n if (!raw) {\r\n return defaultValue;\r\n }\r\n return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());\r\n}\r\n\r\nfunction getNumber(name: string, defaultValue: number): number {\r\n const raw = process.env[name];\r\n if (!raw) {\r\n return defaultValue;\r\n }\r\n const parsed = Number(raw);\r\n return Number.isFinite(parsed) ? parsed : defaultValue;\r\n}\r\n\r\nfunction resolveHealthPort(): number {\r\n // Always default to 6565 if HEALTH_PORT is not explicitly set\r\n // This ensures the health check server starts in deployed environments\r\n // even if container detection fails or HEALTH_PORT env var is missing\r\n const preferred = getNumber('HEALTH_PORT', 6565);\r\n if (preferred > 0) {\r\n return preferred;\r\n }\r\n // Use PID-scoped pseudo random port to avoid collisions on dev machines.\r\n const base = 45000;\r\n const span = 2000;\r\n const candidate = base + (process.pid % span);\r\n return candidate;\r\n}\r\n\r\nfunction resolveRoutingKey(): string | undefined {\r\n const raw = process.env.NAME_PREFIX ?? process.env.ROUTING_KEY;\r\n if (raw && raw.trim().length > 0) {\r\n return raw.trim();\r\n }\r\n\r\n // Check for explicit container indicators first (highest priority)\r\n const isExplicitlyInContainer = \r\n process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||\r\n process.env.CONTAINER === 'beamable' ||\r\n !!process.env.ECS_CONTAINER_METADATA_URI ||\r\n !!process.env.KUBERNETES_SERVICE_HOST;\r\n \r\n if (isExplicitlyInContainer) {\r\n // We're definitely in a container - return undefined (becomes None in Scala)\r\n return undefined;\r\n }\r\n\r\n // For deployed services (in containers), routing key should be undefined (None in Scala)\r\n // Beamable sets CID, PID, HOST, SECRET in containers but NOT routing key\r\n // BUT: Local dev also has these env vars from .env file, so we need to be more careful\r\n // If we have the required Beamable env vars but NO routing key AND no explicit container indicators,\r\n // we might be in a deployed container OR local dev without routing key set\r\n // For safety, if we're not explicitly in a container, try to generate a routing key for local dev\r\n const hasBeamableEnvVars = !!(process.env.CID && process.env.PID && process.env.HOST && process.env.SECRET);\r\n const hasNoRoutingKey = !raw || raw.trim().length === 0;\r\n \r\n // If we have Beamable env vars but no routing key, and we're not explicitly in a container,\r\n // we might be in local dev - try to generate a routing key\r\n // Only return undefined if we're explicitly in a container\r\n if (hasBeamableEnvVars && hasNoRoutingKey) {\r\n // Try to generate a routing key for local dev\r\n // If this fails, we'll throw an error (which is fine for local dev)\r\n try {\r\n return getDefaultRoutingKeyForMachine();\r\n } catch (error) {\r\n // If we can't generate a routing key, we might be in a container\r\n // But since we don't have explicit container indicators, assume local dev and throw\r\n throw new Error(\r\n `Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${(error as Error).message}`,\r\n );\r\n }\r\n }\r\n\r\n // Local development - try to get a routing key\r\n try {\r\n return getDefaultRoutingKeyForMachine();\r\n } catch (error) {\r\n throw new Error(\r\n `Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${(error as Error).message}`,\r\n );\r\n }\r\n}\r\n\r\nfunction resolveSdkVersionExecution(): string {\r\n return process.env.BEAMABLE_SDK_VERSION_EXECUTION ?? '';\r\n}\r\n\r\nfunction resolveLogLevel(): string {\r\n const candidate = process.env.LOG_LEVEL ?? 'info';\r\n const allowed = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace']);\r\n return allowed.has(candidate.toLowerCase()) ? candidate.toLowerCase() : 'info';\r\n}\r\n\r\nfunction resolveWatchToken(): boolean {\r\n const value = process.env.WATCH_TOKEN;\r\n if (value === undefined) {\r\n return false;\r\n }\r\n return getBoolean('WATCH_TOKEN', false);\r\n}\r\n\r\nfunction resolveBeamInstanceCount(): number {\r\n return getNumber('BEAM_INSTANCE_COUNT', 1);\r\n}\r\n\r\nexport function loadEnvironmentConfig(): EnvironmentConfig {\r\n const cid = process.env.CID ?? '';\r\n const pid = process.env.PID ?? '';\r\n const host = process.env.HOST ?? '';\r\n\r\n if (!cid || !pid || !host) {\r\n throw new Error('Missing required Beamable environment variables (CID, PID, HOST).');\r\n }\r\n\r\n const config: EnvironmentConfig = {\r\n cid,\r\n pid,\r\n host,\r\n secret: process.env.SECRET ?? undefined,\r\n refreshToken: process.env.REFRESH_TOKEN ?? undefined,\r\n routingKey: resolveRoutingKey(),\r\n accountId: getNumber('USER_ACCOUNT_ID', 0) || undefined,\r\n accountEmail: process.env.USER_EMAIL ?? undefined,\r\n logLevel: resolveLogLevel(),\r\n healthPort: resolveHealthPort(),\r\n disableCustomInitializationHooks: getBoolean('DISABLE_CUSTOM_INITIALIZATION_HOOKS', false),\r\n watchToken: resolveWatchToken(),\r\n sdkVersionExecution: resolveSdkVersionExecution(),\r\n beamInstanceCount: resolveBeamInstanceCount(),\r\n logTruncateLimit: getNumber('LOG_TRUNCATE_LIMIT', 1000),\r\n };\r\n\r\n return config;\r\n}\r\n\r\nexport function ensureWritableTempDirectory(): string {\r\n const candidate = process.env.LOG_PATH ?? join(tmpdir(), 'beamable-node-runtime');\r\n if (!existsSync(candidate)) {\r\n return candidate;\r\n }\r\n return candidate;\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\r\nimport { tmpdir } from 'node:os';\r\nimport { join } from 'node:path';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport { getDefaultRoutingKeyForMachine } from './routing.js';\r\n\r\nfunction getBoolean(name: string, defaultValue = false): boolean {\r\n const raw = process.env[name];\r\n if (!raw) {\r\n return defaultValue;\r\n }\r\n return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());\r\n}\r\n\r\nfunction getNumber(name: string, defaultValue: number): number {\r\n const raw = process.env[name];\r\n if (!raw) {\r\n return defaultValue;\r\n }\r\n const parsed = Number(raw);\r\n return Number.isFinite(parsed) ? parsed : defaultValue;\r\n}\r\n\r\nfunction resolveHealthPort(): number {\r\n // Always default to 6565 if HEALTH_PORT is not explicitly set\r\n // This ensures the health check server starts in deployed environments\r\n // even if container detection fails or HEALTH_PORT env var is missing\r\n const preferred = getNumber('HEALTH_PORT', 6565);\r\n if (preferred > 0) {\r\n return preferred;\r\n }\r\n // Use PID-scoped pseudo random port to avoid collisions on dev machines.\r\n const base = 45000;\r\n const span = 2000;\r\n const candidate = base + (process.pid % span);\r\n return candidate;\r\n}\r\n\r\nfunction isInContainer(): boolean {\r\n // Check for Docker container\r\n try {\r\n const fs = require('fs');\r\n if (fs.existsSync('/.dockerenv')) {\r\n return true;\r\n }\r\n } catch {\r\n // fs might not be available\r\n }\r\n \r\n // Check for Docker container hostname pattern (12 hex chars)\r\n const hostname = process.env.HOSTNAME || '';\r\n if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {\r\n return true;\r\n }\r\n \r\n // Explicit container indicators\r\n if (\r\n process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||\r\n process.env.CONTAINER === 'beamable' ||\r\n !!process.env.ECS_CONTAINER_METADATA_URI ||\r\n !!process.env.KUBERNETES_SERVICE_HOST\r\n ) {\r\n return true;\r\n }\r\n \r\n return false;\r\n}\r\n\r\nfunction resolveRoutingKey(): string | undefined {\r\n const raw = process.env.NAME_PREFIX ?? process.env.ROUTING_KEY;\r\n if (raw && raw.trim().length > 0) {\r\n return raw.trim();\r\n }\r\n\r\n // If we're actually in a container, return undefined (deployed service)\r\n if (isInContainer()) {\r\n return undefined;\r\n }\r\n\r\n // Local development - always try to get a routing key\r\n try {\r\n return getDefaultRoutingKeyForMachine();\r\n } catch (error) {\r\n throw new Error(\r\n `Unable to determine routing key automatically. Set NAME_PREFIX environment variable. ${(error as Error).message}`,\r\n );\r\n }\r\n}\r\n\r\nfunction resolveSdkVersionExecution(): string {\r\n return process.env.BEAMABLE_SDK_VERSION_EXECUTION ?? '';\r\n}\r\n\r\nfunction resolveLogLevel(): string {\r\n const candidate = process.env.LOG_LEVEL ?? 'info';\r\n const allowed = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace']);\r\n return allowed.has(candidate.toLowerCase()) ? candidate.toLowerCase() : 'info';\r\n}\r\n\r\nfunction resolveWatchToken(): boolean {\r\n const value = process.env.WATCH_TOKEN;\r\n if (value === undefined) {\r\n return false;\r\n }\r\n return getBoolean('WATCH_TOKEN', false);\r\n}\r\n\r\nfunction resolveBeamInstanceCount(): number {\r\n return getNumber('BEAM_INSTANCE_COUNT', 1);\r\n}\r\n\r\nexport function loadEnvironmentConfig(): EnvironmentConfig {\r\n const cid = process.env.CID ?? '';\r\n const pid = process.env.PID ?? '';\r\n const host = process.env.HOST ?? '';\r\n\r\n if (!cid || !pid || !host) {\r\n throw new Error('Missing required Beamable environment variables (CID, PID, HOST).');\r\n }\r\n\r\n const config: EnvironmentConfig = {\r\n cid,\r\n pid,\r\n host,\r\n secret: process.env.SECRET ?? undefined,\r\n refreshToken: process.env.REFRESH_TOKEN ?? undefined,\r\n routingKey: resolveRoutingKey(),\r\n accountId: getNumber('USER_ACCOUNT_ID', 0) || undefined,\r\n accountEmail: process.env.USER_EMAIL ?? undefined,\r\n logLevel: resolveLogLevel(),\r\n healthPort: resolveHealthPort(),\r\n disableCustomInitializationHooks: getBoolean('DISABLE_CUSTOM_INITIALIZATION_HOOKS', false),\r\n watchToken: resolveWatchToken(),\r\n sdkVersionExecution: resolveSdkVersionExecution(),\r\n beamInstanceCount: resolveBeamInstanceCount(),\r\n logTruncateLimit: getNumber('LOG_TRUNCATE_LIMIT', 1000),\r\n };\r\n\r\n return config;\r\n}\r\n\r\nexport function ensureWritableTempDirectory(): string {\r\n const candidate = process.env.LOG_PATH ?? join(tmpdir(), 'beamable-node-runtime');\r\n if (!existsSync(candidate)) {\r\n return candidate;\r\n }\r\n return candidate;\r\n}\r\n"]}
|
package/dist/logger.cjs
CHANGED
|
@@ -65,7 +65,7 @@ function mapPinoLevelToBeamableLevel(level) {
|
|
|
65
65
|
return 'Info';
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
function createBeamableLogFormatter() {
|
|
68
|
+
function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
69
69
|
return new node_stream_1.Transform({
|
|
70
70
|
objectMode: false,
|
|
71
71
|
transform(chunk, _encoding, callback) {
|
|
@@ -113,6 +113,12 @@ function createBeamableLogFormatter() {
|
|
|
113
113
|
contextFields.service = pinoLog.service;
|
|
114
114
|
if (pinoLog.component)
|
|
115
115
|
contextFields.component = pinoLog.component;
|
|
116
|
+
if (serviceName) {
|
|
117
|
+
contextFields.serviceName = serviceName;
|
|
118
|
+
}
|
|
119
|
+
if (qualifiedServiceName) {
|
|
120
|
+
contextFields.qualifiedServiceName = qualifiedServiceName;
|
|
121
|
+
}
|
|
116
122
|
const standardPinoFields = ['level', 'time', 'pid', 'hostname', 'name', 'msg', 'err', 'v', 'cid', 'pid', 'routingKey', 'sdkVersionExecution', 'service', 'component'];
|
|
117
123
|
for (const [key, value] of Object.entries(pinoLog)) {
|
|
118
124
|
if (!standardPinoFields.includes(key) && value !== undefined && value !== null) {
|
|
@@ -122,6 +128,54 @@ function createBeamableLogFormatter() {
|
|
|
122
128
|
if (Object.keys(contextFields).length > 0) {
|
|
123
129
|
beamableLog.__c = contextFields;
|
|
124
130
|
}
|
|
131
|
+
const otelFields = {};
|
|
132
|
+
const resourceAttributes = {};
|
|
133
|
+
if (qualifiedServiceName) {
|
|
134
|
+
resourceAttributes['service.namespace'] = qualifiedServiceName;
|
|
135
|
+
}
|
|
136
|
+
if (serviceName) {
|
|
137
|
+
resourceAttributes['service.name'] = serviceName;
|
|
138
|
+
}
|
|
139
|
+
if (pinoLog.cid) {
|
|
140
|
+
resourceAttributes['beam.cid'] = String(pinoLog.cid);
|
|
141
|
+
}
|
|
142
|
+
if (pinoLog.pid) {
|
|
143
|
+
resourceAttributes['beam.pid'] = String(pinoLog.pid);
|
|
144
|
+
}
|
|
145
|
+
if (pinoLog.routingKey) {
|
|
146
|
+
resourceAttributes['beam.routing_key'] = String(pinoLog.routingKey);
|
|
147
|
+
}
|
|
148
|
+
const logAttributes = {};
|
|
149
|
+
if (pinoLog.component) {
|
|
150
|
+
logAttributes['component'] = String(pinoLog.component);
|
|
151
|
+
}
|
|
152
|
+
if (pinoLog.err) {
|
|
153
|
+
const err = pinoLog.err;
|
|
154
|
+
if (err.message)
|
|
155
|
+
logAttributes['exception.message'] = String(err.message);
|
|
156
|
+
if (err.stack)
|
|
157
|
+
logAttributes['exception.stacktrace'] = String(err.stack);
|
|
158
|
+
if (err.type)
|
|
159
|
+
logAttributes['exception.type'] = String(err.type);
|
|
160
|
+
}
|
|
161
|
+
const severityTextMap = {
|
|
162
|
+
'Debug': 'Debug',
|
|
163
|
+
'Info': 'Information',
|
|
164
|
+
'Warning': 'Warning',
|
|
165
|
+
'Error': 'Error',
|
|
166
|
+
'Fatal': 'Critical',
|
|
167
|
+
};
|
|
168
|
+
const severityText = severityTextMap[level] || 'Information';
|
|
169
|
+
otelFields['Timestamp'] = timestamp;
|
|
170
|
+
otelFields['SeverityText'] = severityText;
|
|
171
|
+
otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';
|
|
172
|
+
if (Object.keys(resourceAttributes).length > 0) {
|
|
173
|
+
otelFields['ResourceAttributes'] = resourceAttributes;
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(logAttributes).length > 0) {
|
|
176
|
+
otelFields['LogAttributes'] = logAttributes;
|
|
177
|
+
}
|
|
178
|
+
Object.assign(beamableLog, otelFields);
|
|
125
179
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
126
180
|
callback(null, Buffer.from(output, 'utf8'));
|
|
127
181
|
}
|
|
@@ -148,6 +202,8 @@ function createLogger(env, options = {}) {
|
|
|
148
202
|
pid: env.pid,
|
|
149
203
|
routingKey: (_c = env.routingKey) !== null && _c !== void 0 ? _c : null,
|
|
150
204
|
sdkVersionExecution: env.sdkVersionExecution,
|
|
205
|
+
serviceName: options.serviceName,
|
|
206
|
+
qualifiedServiceName: options.qualifiedServiceName,
|
|
151
207
|
},
|
|
152
208
|
redact: {
|
|
153
209
|
paths: ['secret', 'refreshToken'],
|
|
@@ -157,7 +213,7 @@ function createLogger(env, options = {}) {
|
|
|
157
213
|
};
|
|
158
214
|
if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {
|
|
159
215
|
if (!usePrettyLogs) {
|
|
160
|
-
const beamableFormatter = createBeamableLogFormatter();
|
|
216
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
161
217
|
beamableFormatter.pipe(process.stdout);
|
|
162
218
|
return (0, pino_1.default)(pinoOptions, beamableFormatter);
|
|
163
219
|
}
|
|
@@ -180,7 +236,7 @@ function createLogger(env, options = {}) {
|
|
|
180
236
|
}
|
|
181
237
|
const resolvedDestination = configuredDestination === 'temp' ? (0, env_js_1.ensureWritableTempDirectory)() : configuredDestination;
|
|
182
238
|
if (!usePrettyLogs) {
|
|
183
|
-
const beamableFormatter = createBeamableLogFormatter();
|
|
239
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
184
240
|
const fileStream = (0, pino_1.destination)({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
185
241
|
beamableFormatter.pipe(fileStream);
|
|
186
242
|
return (0, pino_1.default)(pinoOptions, beamableFormatter);
|
package/dist/logger.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { EnvironmentConfig } from './types.js';
|
|
|
3
3
|
interface LoggerFactoryOptions {
|
|
4
4
|
name?: string;
|
|
5
5
|
destinationPath?: string;
|
|
6
|
+
serviceName?: string;
|
|
7
|
+
qualifiedServiceName?: string;
|
|
6
8
|
}
|
|
7
9
|
export declare function createLogger(env: EnvironmentConfig, options?: LoggerFactoryOptions): Logger;
|
|
8
10
|
export {};
|
package/dist/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAa,EAAe,KAAK,MAAM,EAAsB,MAAM,MAAM,CAAC;AAI1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA2BpD,UAAU,oBAAoB;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAa,EAAe,KAAK,MAAM,EAAsB,MAAM,MAAM,CAAC;AAI1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA2BpD,UAAU,oBAAoB;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAiMD,wBAAgB,YAAY,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAsE/F"}
|
package/dist/logger.js
CHANGED
|
@@ -49,9 +49,10 @@ function mapPinoLevelToBeamableLevel(level) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Creates a transform stream that converts Pino JSON logs to Beamable's expected format.
|
|
51
51
|
* Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.
|
|
52
|
+
* Also includes OpenTelemetry-compatible fields for ClickHouse compatibility.
|
|
52
53
|
* Pino writes JSON strings (one per line) to the stream.
|
|
53
54
|
*/
|
|
54
|
-
function createBeamableLogFormatter() {
|
|
55
|
+
function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
55
56
|
return new Transform({
|
|
56
57
|
objectMode: false, // Pino writes strings, not objects
|
|
57
58
|
transform(chunk, _encoding, callback) {
|
|
@@ -91,7 +92,7 @@ function createBeamableLogFormatter() {
|
|
|
91
92
|
const errStack = err.stack ? `\n${err.stack}` : '';
|
|
92
93
|
messageParts.push(`${errMsg}${errStack}`);
|
|
93
94
|
}
|
|
94
|
-
// Build the Beamable log format
|
|
95
|
+
// Build the Beamable log format (for CloudWatch Logs Insights)
|
|
95
96
|
const beamableLog = {
|
|
96
97
|
__t: timestamp,
|
|
97
98
|
__l: level,
|
|
@@ -110,6 +111,13 @@ function createBeamableLogFormatter() {
|
|
|
110
111
|
contextFields.service = pinoLog.service;
|
|
111
112
|
if (pinoLog.component)
|
|
112
113
|
contextFields.component = pinoLog.component;
|
|
114
|
+
// Include service name in context for CloudWatch filtering
|
|
115
|
+
if (serviceName) {
|
|
116
|
+
contextFields.serviceName = serviceName;
|
|
117
|
+
}
|
|
118
|
+
if (qualifiedServiceName) {
|
|
119
|
+
contextFields.qualifiedServiceName = qualifiedServiceName;
|
|
120
|
+
}
|
|
113
121
|
// Include any other fields that aren't standard Pino fields
|
|
114
122
|
const standardPinoFields = ['level', 'time', 'pid', 'hostname', 'name', 'msg', 'err', 'v', 'cid', 'pid', 'routingKey', 'sdkVersionExecution', 'service', 'component'];
|
|
115
123
|
for (const [key, value] of Object.entries(pinoLog)) {
|
|
@@ -121,6 +129,67 @@ function createBeamableLogFormatter() {
|
|
|
121
129
|
if (Object.keys(contextFields).length > 0) {
|
|
122
130
|
beamableLog.__c = contextFields;
|
|
123
131
|
}
|
|
132
|
+
// Add OpenTelemetry-compatible fields for ClickHouse compatibility
|
|
133
|
+
// These fields allow an OpenTelemetry collector to parse and forward logs to ClickHouse
|
|
134
|
+
// The Portal's realm-level logs page queries ClickHouse's otel_logs table
|
|
135
|
+
const otelFields = {};
|
|
136
|
+
// ResourceAttributes - service identification (for ClickHouse filtering)
|
|
137
|
+
const resourceAttributes = {};
|
|
138
|
+
if (qualifiedServiceName) {
|
|
139
|
+
// ClickHouse expects service.namespace to match the service name format
|
|
140
|
+
resourceAttributes['service.namespace'] = qualifiedServiceName;
|
|
141
|
+
}
|
|
142
|
+
if (serviceName) {
|
|
143
|
+
resourceAttributes['service.name'] = serviceName;
|
|
144
|
+
}
|
|
145
|
+
if (pinoLog.cid) {
|
|
146
|
+
resourceAttributes['beam.cid'] = String(pinoLog.cid);
|
|
147
|
+
}
|
|
148
|
+
if (pinoLog.pid) {
|
|
149
|
+
resourceAttributes['beam.pid'] = String(pinoLog.pid);
|
|
150
|
+
}
|
|
151
|
+
if (pinoLog.routingKey) {
|
|
152
|
+
resourceAttributes['beam.routing_key'] = String(pinoLog.routingKey);
|
|
153
|
+
}
|
|
154
|
+
// LogAttributes - log-specific attributes
|
|
155
|
+
const logAttributes = {};
|
|
156
|
+
if (pinoLog.component) {
|
|
157
|
+
logAttributes['component'] = String(pinoLog.component);
|
|
158
|
+
}
|
|
159
|
+
if (pinoLog.err) {
|
|
160
|
+
const err = pinoLog.err;
|
|
161
|
+
if (err.message)
|
|
162
|
+
logAttributes['exception.message'] = String(err.message);
|
|
163
|
+
if (err.stack)
|
|
164
|
+
logAttributes['exception.stacktrace'] = String(err.stack);
|
|
165
|
+
if (err.type)
|
|
166
|
+
logAttributes['exception.type'] = String(err.type);
|
|
167
|
+
}
|
|
168
|
+
// Map Beamable log level to OpenTelemetry SeverityText
|
|
169
|
+
// Beamable: Debug, Info, Warning, Error, Fatal
|
|
170
|
+
// OpenTelemetry: Trace, Debug, Info, Warn, Error, Fatal, Unspecified
|
|
171
|
+
const severityTextMap = {
|
|
172
|
+
'Debug': 'Debug',
|
|
173
|
+
'Info': 'Information',
|
|
174
|
+
'Warning': 'Warning',
|
|
175
|
+
'Error': 'Error',
|
|
176
|
+
'Fatal': 'Critical',
|
|
177
|
+
};
|
|
178
|
+
const severityText = severityTextMap[level] || 'Information';
|
|
179
|
+
// Add OpenTelemetry fields to the log
|
|
180
|
+
// These are in addition to the Beamable format, so both systems can parse the logs
|
|
181
|
+
otelFields['Timestamp'] = timestamp; // OpenTelemetry timestamp format
|
|
182
|
+
otelFields['SeverityText'] = severityText;
|
|
183
|
+
otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';
|
|
184
|
+
if (Object.keys(resourceAttributes).length > 0) {
|
|
185
|
+
otelFields['ResourceAttributes'] = resourceAttributes;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(logAttributes).length > 0) {
|
|
188
|
+
otelFields['LogAttributes'] = logAttributes;
|
|
189
|
+
}
|
|
190
|
+
// Merge OpenTelemetry fields into the log (for ClickHouse compatibility)
|
|
191
|
+
// An OpenTelemetry collector can parse these fields and forward to ClickHouse
|
|
192
|
+
Object.assign(beamableLog, otelFields);
|
|
124
193
|
// Output as a single-line JSON string (required for CloudWatch)
|
|
125
194
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
126
195
|
callback(null, Buffer.from(output, 'utf8'));
|
|
@@ -148,6 +217,9 @@ export function createLogger(env, options = {}) {
|
|
|
148
217
|
pid: env.pid,
|
|
149
218
|
routingKey: env.routingKey ?? null,
|
|
150
219
|
sdkVersionExecution: env.sdkVersionExecution,
|
|
220
|
+
// Include service name in base fields for filtering
|
|
221
|
+
serviceName: options.serviceName,
|
|
222
|
+
qualifiedServiceName: options.qualifiedServiceName,
|
|
151
223
|
},
|
|
152
224
|
redact: {
|
|
153
225
|
paths: ['secret', 'refreshToken'],
|
|
@@ -161,7 +233,8 @@ export function createLogger(env, options = {}) {
|
|
|
161
233
|
if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {
|
|
162
234
|
if (!usePrettyLogs) {
|
|
163
235
|
// Deployed/remote: Use Beamable JSON format for log collection
|
|
164
|
-
|
|
236
|
+
// Include OpenTelemetry fields for ClickHouse compatibility
|
|
237
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
165
238
|
beamableFormatter.pipe(process.stdout);
|
|
166
239
|
return pino(pinoOptions, beamableFormatter);
|
|
167
240
|
}
|
|
@@ -194,7 +267,7 @@ export function createLogger(env, options = {}) {
|
|
|
194
267
|
// For file logging: Use Beamable format if not local, default Pino format if local
|
|
195
268
|
const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;
|
|
196
269
|
if (!usePrettyLogs) {
|
|
197
|
-
const beamableFormatter = createBeamableLogFormatter();
|
|
270
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
198
271
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
199
272
|
beamableFormatter.pipe(fileStream);
|
|
200
273
|
return pino(pinoOptions, beamableFormatter);
|
package/dist/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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","sourcesContent":["import pino, { destination, type Logger, type LoggerOptions } from 'pino';\r\nimport { Transform } from 'node:stream';\r\nimport { createRequire } from 'node:module';\r\nimport { ensureWritableTempDirectory } from './env.js';\r\nimport type { EnvironmentConfig } from './types.js';\r\n\r\n// Helper to get require function that works in both CJS and ESM\r\ndeclare const require: any;\r\nfunction getRequire(): any {\r\n // Check if we're in CJS context (require.main exists)\r\n if (typeof require !== 'undefined' && typeof require.main !== 'undefined') {\r\n // CJS context - use require directly\r\n return require;\r\n }\r\n // ESM context - use createRequire with import.meta.url\r\n // TypeScript will complain in CJS builds, but this code only runs in ESM\r\n // @ts-ignore - import.meta is ESM-only, TypeScript error in CJS is expected\r\n return createRequire(import.meta.url);\r\n}\r\n\r\n/**\r\n * Determines if we should use pretty logs (local dev) or raw JSON logs (deployed).\r\n * \r\n * Simple check: If IS_LOCAL=1 is set in environment, use pretty logs.\r\n * Otherwise, use raw Beamable JSON format for log collection.\r\n */\r\nfunction shouldUsePrettyLogs(): boolean {\r\n // Check for explicit IS_LOCAL flag\r\n return process.env.IS_LOCAL === '1' || process.env.IS_LOCAL === 'true';\r\n}\r\n\r\ninterface LoggerFactoryOptions {\r\n name?: string;\r\n destinationPath?: string;\r\n}\r\n\r\n/**\r\n * Maps Pino log levels to Beamable log levels\r\n * Pino levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal\r\n * Beamable levels: Debug, Info, Warning, Error, Fatal\r\n */\r\nfunction mapPinoLevelToBeamableLevel(level: number): string {\r\n switch (level) {\r\n case 10: // trace\r\n return 'Debug';\r\n case 20: // debug\r\n return 'Debug';\r\n case 30: // info\r\n return 'Info';\r\n case 40: // warn\r\n return 'Warning';\r\n case 50: // error\r\n return 'Error';\r\n case 60: // fatal\r\n return 'Fatal';\r\n default:\r\n return 'Info';\r\n }\r\n}\r\n\r\n/**\r\n * Creates a transform stream that converts Pino JSON logs to Beamable's expected format.\r\n * Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.\r\n * Pino writes JSON strings (one per line) to the stream.\r\n */\r\nfunction createBeamableLogFormatter(): Transform {\r\n return new Transform({\r\n objectMode: false, // Pino writes strings, not objects\r\n transform(chunk: Buffer, _encoding, callback) {\r\n try {\r\n const line = chunk.toString();\r\n // Skip empty lines\r\n if (!line.trim()) {\r\n callback();\r\n return;\r\n }\r\n \r\n // Parse Pino's JSON log line\r\n const pinoLog = JSON.parse(line);\r\n \r\n // Extract timestamp - Pino uses 'time' field (ISO 8601 string or milliseconds)\r\n // Convert to ISO 8601 string for Beamable\r\n let timestamp: string;\r\n if (typeof pinoLog.time === 'string') {\r\n timestamp = pinoLog.time;\r\n } else if (typeof pinoLog.time === 'number') {\r\n timestamp = new Date(pinoLog.time).toISOString();\r\n } else {\r\n timestamp = new Date().toISOString();\r\n }\r\n \r\n // Map Pino level to Beamable level\r\n const level = mapPinoLevelToBeamableLevel(pinoLog.level);\r\n \r\n // Build the message - combine msg with any additional fields\r\n // Pino's 'msg' field contains the log message\r\n const messageParts: string[] = [];\r\n if (pinoLog.msg) {\r\n messageParts.push(pinoLog.msg);\r\n }\r\n \r\n // Include error information if present\r\n if (pinoLog.err) {\r\n const err = pinoLog.err;\r\n const errMsg = err.message || err.msg || 'Error';\r\n const errStack = err.stack ? `\\n${err.stack}` : '';\r\n messageParts.push(`${errMsg}${errStack}`);\r\n }\r\n \r\n // Build the Beamable log format\r\n const beamableLog: Record<string, unknown> = {\r\n __t: timestamp,\r\n __l: level,\r\n __m: messageParts.length > 0 ? messageParts.join(' ') : 'No message',\r\n };\r\n \r\n // Include additional context fields that might be useful\r\n // These are included in the message object but not as top-level fields\r\n const contextFields: Record<string, unknown> = {};\r\n if (pinoLog.cid) contextFields.cid = pinoLog.cid;\r\n if (pinoLog.pid) contextFields.pid = pinoLog.pid;\r\n if (pinoLog.routingKey) contextFields.routingKey = pinoLog.routingKey;\r\n if (pinoLog.service) contextFields.service = pinoLog.service;\r\n if (pinoLog.component) contextFields.component = pinoLog.component;\r\n \r\n // Include any other fields that aren't standard Pino fields\r\n const standardPinoFields = ['level', 'time', 'pid', 'hostname', 'name', 'msg', 'err', 'v', 'cid', 'pid', 'routingKey', 'sdkVersionExecution', 'service', 'component'];\r\n for (const [key, value] of Object.entries(pinoLog)) {\r\n if (!standardPinoFields.includes(key) && value !== undefined && value !== null) {\r\n contextFields[key] = value;\r\n }\r\n }\r\n \r\n // If there are context fields, include them in the log\r\n if (Object.keys(contextFields).length > 0) {\r\n beamableLog.__c = contextFields;\r\n }\r\n \r\n // Output as a single-line JSON string (required for CloudWatch)\r\n const output = JSON.stringify(beamableLog) + '\\n';\r\n callback(null, Buffer.from(output, 'utf8'));\r\n } catch (error) {\r\n // If parsing fails, output a fallback log entry\r\n const fallbackLog = {\r\n __t: new Date().toISOString(),\r\n __l: 'Error',\r\n __m: `Failed to parse log entry: ${chunk.toString().substring(0, 200)}`,\r\n };\r\n callback(null, Buffer.from(JSON.stringify(fallbackLog) + '\\n', 'utf8'));\r\n }\r\n },\r\n });\r\n}\r\n\r\nexport function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptions = {}): Logger {\r\n const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;\r\n const usePrettyLogs = shouldUsePrettyLogs();\r\n\r\n const pinoOptions: LoggerOptions = {\r\n name: options.name ?? 'beamable-node-runtime',\r\n level: env.logLevel,\r\n base: {\r\n cid: env.cid,\r\n pid: env.pid,\r\n routingKey: env.routingKey ?? null,\r\n sdkVersionExecution: env.sdkVersionExecution,\r\n },\r\n redact: {\r\n paths: ['secret', 'refreshToken'],\r\n censor: '***',\r\n },\r\n // Use timestamp in milliseconds (Pino default) for accurate conversion\r\n timestamp: pino.stdTimeFunctions.isoTime,\r\n };\r\n\r\n // For deployed services, always log to stdout so container orchestrator can collect logs\r\n // For local development, log to stdout unless a specific file path is provided\r\n if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {\r\n if (!usePrettyLogs) {\r\n // Deployed/remote: Use Beamable JSON format for log collection\r\n const beamableFormatter = createBeamableLogFormatter();\r\n beamableFormatter.pipe(process.stdout);\r\n return pino(pinoOptions, beamableFormatter);\r\n } else {\r\n // Local development: Use Pino's pretty printing for human-readable logs\r\n // Try to use pino-pretty if available (optional dependency)\r\n // If not available, fall back to default Pino JSON output\r\n try {\r\n // Check if pino-pretty is available\r\n // Use getRequire() which handles both CJS and ESM contexts\r\n const requireFn = getRequire();\r\n const pinoPretty = requireFn('pino-pretty');\r\n // Create a pretty stream with formatting options\r\n const prettyStream = pinoPretty({\r\n colorize: true,\r\n translateTime: 'HH:MM:ss.l',\r\n ignore: 'pid,hostname',\r\n singleLine: false,\r\n });\r\n // Use pino with the pretty stream\r\n return pino(pinoOptions, prettyStream);\r\n } catch {\r\n // pino-pretty not available, use default Pino output (JSON but readable)\r\n // This is expected if pino-pretty isn't installed, so we silently fall back\r\n return pino(pinoOptions, process.stdout);\r\n }\r\n }\r\n }\r\n\r\n // For file logging: Use Beamable format if not local, default Pino format if local\r\n const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;\r\n if (!usePrettyLogs) {\r\n const beamableFormatter = createBeamableLogFormatter();\r\n const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });\r\n beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);\r\n return pino(pinoOptions, beamableFormatter);\r\n } else {\r\n const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });\r\n return pino(pinoOptions, fileStream);\r\n }\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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","sourcesContent":["import pino, { destination, type Logger, type LoggerOptions } from 'pino';\r\nimport { Transform } from 'node:stream';\r\nimport { createRequire } from 'node:module';\r\nimport { ensureWritableTempDirectory } from './env.js';\r\nimport type { EnvironmentConfig } from './types.js';\r\n\r\n// Helper to get require function that works in both CJS and ESM\r\ndeclare const require: any;\r\nfunction getRequire(): any {\r\n // Check if we're in CJS context (require.main exists)\r\n if (typeof require !== 'undefined' && typeof require.main !== 'undefined') {\r\n // CJS context - use require directly\r\n return require;\r\n }\r\n // ESM context - use createRequire with import.meta.url\r\n // TypeScript will complain in CJS builds, but this code only runs in ESM\r\n // @ts-ignore - import.meta is ESM-only, TypeScript error in CJS is expected\r\n return createRequire(import.meta.url);\r\n}\r\n\r\n/**\r\n * Determines if we should use pretty logs (local dev) or raw JSON logs (deployed).\r\n * \r\n * Simple check: If IS_LOCAL=1 is set in environment, use pretty logs.\r\n * Otherwise, use raw Beamable JSON format for log collection.\r\n */\r\nfunction shouldUsePrettyLogs(): boolean {\r\n // Check for explicit IS_LOCAL flag\r\n return process.env.IS_LOCAL === '1' || process.env.IS_LOCAL === 'true';\r\n}\r\n\r\ninterface LoggerFactoryOptions {\r\n name?: string;\r\n destinationPath?: string;\r\n serviceName?: string; // Service name for log filtering (e.g., \"ExampleNodeService\")\r\n qualifiedServiceName?: string; // Full qualified service name (e.g., \"micro_ExampleNodeService\")\r\n}\r\n\r\n/**\r\n * Maps Pino log levels to Beamable log levels\r\n * Pino levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal\r\n * Beamable levels: Debug, Info, Warning, Error, Fatal\r\n */\r\nfunction mapPinoLevelToBeamableLevel(level: number): string {\r\n switch (level) {\r\n case 10: // trace\r\n return 'Debug';\r\n case 20: // debug\r\n return 'Debug';\r\n case 30: // info\r\n return 'Info';\r\n case 40: // warn\r\n return 'Warning';\r\n case 50: // error\r\n return 'Error';\r\n case 60: // fatal\r\n return 'Fatal';\r\n default:\r\n return 'Info';\r\n }\r\n}\r\n\r\n/**\r\n * Creates a transform stream that converts Pino JSON logs to Beamable's expected format.\r\n * Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.\r\n * Also includes OpenTelemetry-compatible fields for ClickHouse compatibility.\r\n * Pino writes JSON strings (one per line) to the stream.\r\n */\r\nfunction createBeamableLogFormatter(serviceName?: string, qualifiedServiceName?: string): Transform {\r\n return new Transform({\r\n objectMode: false, // Pino writes strings, not objects\r\n transform(chunk: Buffer, _encoding, callback) {\r\n try {\r\n const line = chunk.toString();\r\n // Skip empty lines\r\n if (!line.trim()) {\r\n callback();\r\n return;\r\n }\r\n \r\n // Parse Pino's JSON log line\r\n const pinoLog = JSON.parse(line);\r\n \r\n // Extract timestamp - Pino uses 'time' field (ISO 8601 string or milliseconds)\r\n // Convert to ISO 8601 string for Beamable\r\n let timestamp: string;\r\n if (typeof pinoLog.time === 'string') {\r\n timestamp = pinoLog.time;\r\n } else if (typeof pinoLog.time === 'number') {\r\n timestamp = new Date(pinoLog.time).toISOString();\r\n } else {\r\n timestamp = new Date().toISOString();\r\n }\r\n \r\n // Map Pino level to Beamable level\r\n const level = mapPinoLevelToBeamableLevel(pinoLog.level);\r\n \r\n // Build the message - combine msg with any additional fields\r\n // Pino's 'msg' field contains the log message\r\n const messageParts: string[] = [];\r\n if (pinoLog.msg) {\r\n messageParts.push(pinoLog.msg);\r\n }\r\n \r\n // Include error information if present\r\n if (pinoLog.err) {\r\n const err = pinoLog.err;\r\n const errMsg = err.message || err.msg || 'Error';\r\n const errStack = err.stack ? `\\n${err.stack}` : '';\r\n messageParts.push(`${errMsg}${errStack}`);\r\n }\r\n \r\n // Build the Beamable log format (for CloudWatch Logs Insights)\r\n const beamableLog: Record<string, unknown> = {\r\n __t: timestamp,\r\n __l: level,\r\n __m: messageParts.length > 0 ? messageParts.join(' ') : 'No message',\r\n };\r\n \r\n // Include additional context fields that might be useful\r\n // These are included in the message object but not as top-level fields\r\n const contextFields: Record<string, unknown> = {};\r\n if (pinoLog.cid) contextFields.cid = pinoLog.cid;\r\n if (pinoLog.pid) contextFields.pid = pinoLog.pid;\r\n if (pinoLog.routingKey) contextFields.routingKey = pinoLog.routingKey;\r\n if (pinoLog.service) contextFields.service = pinoLog.service;\r\n if (pinoLog.component) contextFields.component = pinoLog.component;\r\n \r\n // Include service name in context for CloudWatch filtering\r\n if (serviceName) {\r\n contextFields.serviceName = serviceName;\r\n }\r\n if (qualifiedServiceName) {\r\n contextFields.qualifiedServiceName = qualifiedServiceName;\r\n }\r\n \r\n // Include any other fields that aren't standard Pino fields\r\n const standardPinoFields = ['level', 'time', 'pid', 'hostname', 'name', 'msg', 'err', 'v', 'cid', 'pid', 'routingKey', 'sdkVersionExecution', 'service', 'component'];\r\n for (const [key, value] of Object.entries(pinoLog)) {\r\n if (!standardPinoFields.includes(key) && value !== undefined && value !== null) {\r\n contextFields[key] = value;\r\n }\r\n }\r\n \r\n // If there are context fields, include them in the log\r\n if (Object.keys(contextFields).length > 0) {\r\n beamableLog.__c = contextFields;\r\n }\r\n \r\n // Add OpenTelemetry-compatible fields for ClickHouse compatibility\r\n // These fields allow an OpenTelemetry collector to parse and forward logs to ClickHouse\r\n // The Portal's realm-level logs page queries ClickHouse's otel_logs table\r\n const otelFields: Record<string, unknown> = {};\r\n \r\n // ResourceAttributes - service identification (for ClickHouse filtering)\r\n const resourceAttributes: Record<string, unknown> = {};\r\n if (qualifiedServiceName) {\r\n // ClickHouse expects service.namespace to match the service name format\r\n resourceAttributes['service.namespace'] = qualifiedServiceName;\r\n }\r\n if (serviceName) {\r\n resourceAttributes['service.name'] = serviceName;\r\n }\r\n if (pinoLog.cid) {\r\n resourceAttributes['beam.cid'] = String(pinoLog.cid);\r\n }\r\n if (pinoLog.pid) {\r\n resourceAttributes['beam.pid'] = String(pinoLog.pid);\r\n }\r\n if (pinoLog.routingKey) {\r\n resourceAttributes['beam.routing_key'] = String(pinoLog.routingKey);\r\n }\r\n \r\n // LogAttributes - log-specific attributes\r\n const logAttributes: Record<string, unknown> = {};\r\n if (pinoLog.component) {\r\n logAttributes['component'] = String(pinoLog.component);\r\n }\r\n if (pinoLog.err) {\r\n const err = pinoLog.err;\r\n if (err.message) logAttributes['exception.message'] = String(err.message);\r\n if (err.stack) logAttributes['exception.stacktrace'] = String(err.stack);\r\n if (err.type) logAttributes['exception.type'] = String(err.type);\r\n }\r\n \r\n // Map Beamable log level to OpenTelemetry SeverityText\r\n // Beamable: Debug, Info, Warning, Error, Fatal\r\n // OpenTelemetry: Trace, Debug, Info, Warn, Error, Fatal, Unspecified\r\n const severityTextMap: Record<string, string> = {\r\n 'Debug': 'Debug',\r\n 'Info': 'Information',\r\n 'Warning': 'Warning',\r\n 'Error': 'Error',\r\n 'Fatal': 'Critical',\r\n };\r\n const severityText = severityTextMap[level] || 'Information';\r\n \r\n // Add OpenTelemetry fields to the log\r\n // These are in addition to the Beamable format, so both systems can parse the logs\r\n otelFields['Timestamp'] = timestamp; // OpenTelemetry timestamp format\r\n otelFields['SeverityText'] = severityText;\r\n otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';\r\n if (Object.keys(resourceAttributes).length > 0) {\r\n otelFields['ResourceAttributes'] = resourceAttributes;\r\n }\r\n if (Object.keys(logAttributes).length > 0) {\r\n otelFields['LogAttributes'] = logAttributes;\r\n }\r\n \r\n // Merge OpenTelemetry fields into the log (for ClickHouse compatibility)\r\n // An OpenTelemetry collector can parse these fields and forward to ClickHouse\r\n Object.assign(beamableLog, otelFields);\r\n \r\n // Output as a single-line JSON string (required for CloudWatch)\r\n const output = JSON.stringify(beamableLog) + '\\n';\r\n callback(null, Buffer.from(output, 'utf8'));\r\n } catch (error) {\r\n // If parsing fails, output a fallback log entry\r\n const fallbackLog = {\r\n __t: new Date().toISOString(),\r\n __l: 'Error',\r\n __m: `Failed to parse log entry: ${chunk.toString().substring(0, 200)}`,\r\n };\r\n callback(null, Buffer.from(JSON.stringify(fallbackLog) + '\\n', 'utf8'));\r\n }\r\n },\r\n });\r\n}\r\n\r\nexport function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptions = {}): Logger {\r\n const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;\r\n const usePrettyLogs = shouldUsePrettyLogs();\r\n\r\n const pinoOptions: LoggerOptions = {\r\n name: options.name ?? 'beamable-node-runtime',\r\n level: env.logLevel,\r\n base: {\r\n cid: env.cid,\r\n pid: env.pid,\r\n routingKey: env.routingKey ?? null,\r\n sdkVersionExecution: env.sdkVersionExecution,\r\n // Include service name in base fields for filtering\r\n serviceName: options.serviceName,\r\n qualifiedServiceName: options.qualifiedServiceName,\r\n },\r\n redact: {\r\n paths: ['secret', 'refreshToken'],\r\n censor: '***',\r\n },\r\n // Use timestamp in milliseconds (Pino default) for accurate conversion\r\n timestamp: pino.stdTimeFunctions.isoTime,\r\n };\r\n\r\n // For deployed services, always log to stdout so container orchestrator can collect logs\r\n // For local development, log to stdout unless a specific file path is provided\r\n if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {\r\n if (!usePrettyLogs) {\r\n // Deployed/remote: Use Beamable JSON format for log collection\r\n // Include OpenTelemetry fields for ClickHouse compatibility\r\n const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);\r\n beamableFormatter.pipe(process.stdout);\r\n return pino(pinoOptions, beamableFormatter);\r\n } else {\r\n // Local development: Use Pino's pretty printing for human-readable logs\r\n // Try to use pino-pretty if available (optional dependency)\r\n // If not available, fall back to default Pino JSON output\r\n try {\r\n // Check if pino-pretty is available\r\n // Use getRequire() which handles both CJS and ESM contexts\r\n const requireFn = getRequire();\r\n const pinoPretty = requireFn('pino-pretty');\r\n // Create a pretty stream with formatting options\r\n const prettyStream = pinoPretty({\r\n colorize: true,\r\n translateTime: 'HH:MM:ss.l',\r\n ignore: 'pid,hostname',\r\n singleLine: false,\r\n });\r\n // Use pino with the pretty stream\r\n return pino(pinoOptions, prettyStream);\r\n } catch {\r\n // pino-pretty not available, use default Pino output (JSON but readable)\r\n // This is expected if pino-pretty isn't installed, so we silently fall back\r\n return pino(pinoOptions, process.stdout);\r\n }\r\n }\r\n }\r\n\r\n // For file logging: Use Beamable format if not local, default Pino format if local\r\n const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;\r\n if (!usePrettyLogs) {\r\n const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);\r\n const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });\r\n beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);\r\n return pino(pinoOptions, beamableFormatter);\r\n } else {\r\n const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });\r\n return pino(pinoOptions, fileStream);\r\n }\r\n}\r\n"]}
|
package/dist/runtime.cjs
CHANGED
|
@@ -23,12 +23,18 @@ class MicroserviceRuntime {
|
|
|
23
23
|
this.microServiceId = (0, node_crypto_1.randomUUID)();
|
|
24
24
|
this.isReady = false;
|
|
25
25
|
this.env = env !== null && env !== void 0 ? env : (0, env_js_1.loadEnvironmentConfig)();
|
|
26
|
-
this.logger = (0, logger_js_1.createLogger)(this.env, { name: 'beamable-node-microservice' });
|
|
27
|
-
this.serviceManager = new services_js_1.BeamableServiceManager(this.env, this.logger);
|
|
28
26
|
const registered = (0, decorators_js_1.listRegisteredServices)();
|
|
29
27
|
if (registered.length === 0) {
|
|
30
28
|
throw new Error('No microservices registered. Use the @Microservice decorator to register at least one class.');
|
|
31
29
|
}
|
|
30
|
+
const primaryService = registered[0];
|
|
31
|
+
const qualifiedServiceName = `micro_${primaryService.qualifiedName}`;
|
|
32
|
+
this.logger = (0, logger_js_1.createLogger)(this.env, {
|
|
33
|
+
name: 'beamable-node-microservice',
|
|
34
|
+
serviceName: primaryService.name,
|
|
35
|
+
qualifiedServiceName: qualifiedServiceName,
|
|
36
|
+
});
|
|
37
|
+
this.serviceManager = new services_js_1.BeamableServiceManager(this.env, this.logger);
|
|
32
38
|
this.services = registered.map((definition) => {
|
|
33
39
|
const instance = new definition.ctor();
|
|
34
40
|
const configureHandlers = (0, decorators_js_1.getConfigureServicesHandlers)(definition.ctor);
|
|
@@ -54,7 +60,7 @@ class MicroserviceRuntime {
|
|
|
54
60
|
this.requester = new requester_js_1.GatewayRequester(this.webSocket, this.logger);
|
|
55
61
|
this.authManager = new auth_js_1.AuthManager(this.env, this.requester);
|
|
56
62
|
this.requester.on('event', (envelope) => this.handleEvent(envelope));
|
|
57
|
-
if ((
|
|
63
|
+
if (!isRunningInContainer() && this.services.length > 0) {
|
|
58
64
|
this.discovery = new discovery_js_1.DiscoveryBroadcaster({
|
|
59
65
|
env: this.env,
|
|
60
66
|
serviceName: this.services[0].definition.name,
|
|
@@ -627,11 +633,6 @@ function isRunningInContainer() {
|
|
|
627
633
|
!!process.env.KUBERNETES_SERVICE_HOST) {
|
|
628
634
|
return true;
|
|
629
635
|
}
|
|
630
|
-
const hasBeamableEnvVars = !!(process.env.CID && process.env.PID && process.env.HOST && process.env.SECRET);
|
|
631
|
-
const hasExplicitRoutingKey = !!(process.env.NAME_PREFIX || process.env.ROUTING_KEY);
|
|
632
|
-
if (hasBeamableEnvVars && !hasExplicitRoutingKey) {
|
|
633
|
-
return true;
|
|
634
|
-
}
|
|
635
636
|
return false;
|
|
636
637
|
}
|
|
637
638
|
function normalizeScopes(scopes) {
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,YAAY,CAAC;AA2BpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,OAAO,CAAC,OAAO,CAAkB;gBAErB,GAAG,CAAC,EAAE,iBAAiB;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,iBAAiB,EAOlB,MAAM,YAAY,CAAC;AA2BpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,OAAO,CAAC,OAAO,CAAkB;gBAErB,GAAG,CAAC,EAAE,iBAAiB;IA2D7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YASjB,sBAAsB;YA2CtB,qBAAqB;YAarB,eAAe;YA+Gf,WAAW;IA8BzB,OAAO,CAAC,gBAAgB;IAkFxB,OAAO,CAAC,kBAAkB;YAUZ,QAAQ;IAsDtB,OAAO,CAAC,wBAAwB;YAmBlB,mBAAmB;YAiBnB,sBAAsB;YAStB,wBAAwB;YAoBxB,mBAAmB;IA0GjC,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,gBAAgB;YA+BV,6BAA6B;IA6B3C,OAAO,CAAC,kBAAkB;CAK3B;AA+GD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAoDrD;AAED,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,4CAA4C,CAC1D,SAAS,GAAE,OAAO,CAAC,iBAAiB,CAAM,EAC1C,OAAO,GAAE,sBAA2B,GACnC,OAAO,CA4BT"}
|