@omen.foundation/node-microservice-runtime 0.1.16 → 0.1.18
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/logger.cjs +98 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +147 -3
- package/dist/logger.js.map +1 -1
- package/package.json +6 -1
- package/src/logger.ts +186 -3
package/dist/logger.cjs
CHANGED
|
@@ -38,6 +38,10 @@ const pino_1 = __importStar(require("pino"));
|
|
|
38
38
|
const node_stream_1 = require("node:stream");
|
|
39
39
|
const node_module_1 = require("node:module");
|
|
40
40
|
const env_js_1 = require("./env.js");
|
|
41
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
42
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
43
|
+
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
44
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
41
45
|
function getRequire() {
|
|
42
46
|
if (typeof require !== 'undefined' && typeof require.main !== 'undefined') {
|
|
43
47
|
return require;
|
|
@@ -65,7 +69,70 @@ function mapPinoLevelToBeamableLevel(level) {
|
|
|
65
69
|
return 'Info';
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
function
|
|
72
|
+
function initializeOtlpLogging(serviceName, qualifiedServiceName, env) {
|
|
73
|
+
const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
74
|
+
|| process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
75
|
+
|| (process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS : null);
|
|
76
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
77
|
+
const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
78
|
+
if (!otlpEndpoint && !standardOtelEnabled) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const resourceAttributes = {};
|
|
83
|
+
if (serviceName) {
|
|
84
|
+
resourceAttributes['service.namespace'] = serviceName;
|
|
85
|
+
resourceAttributes['service.name'] = serviceName;
|
|
86
|
+
}
|
|
87
|
+
if (qualifiedServiceName) {
|
|
88
|
+
resourceAttributes['service.instance.id'] = qualifiedServiceName;
|
|
89
|
+
}
|
|
90
|
+
if (env === null || env === void 0 ? void 0 : env.cid) {
|
|
91
|
+
resourceAttributes['beam.cid'] = String(env.cid);
|
|
92
|
+
}
|
|
93
|
+
if (env === null || env === void 0 ? void 0 : env.pid) {
|
|
94
|
+
resourceAttributes['beam.pid'] = String(env.pid);
|
|
95
|
+
}
|
|
96
|
+
if (env === null || env === void 0 ? void 0 : env.routingKey) {
|
|
97
|
+
resourceAttributes['beam.routing_key'] = String(env.routingKey);
|
|
98
|
+
}
|
|
99
|
+
let endpointUrl = otlpEndpoint;
|
|
100
|
+
if (!endpointUrl) {
|
|
101
|
+
if (standardOtelEnabled) {
|
|
102
|
+
console.error('[OTLP] Standard OTLP is enabled but no endpoint configured. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const finalEndpointUrl = endpointUrl.includes('/v1/logs')
|
|
107
|
+
? endpointUrl
|
|
108
|
+
: `${endpointUrl.replace(/\/$/, '')}/v1/logs`;
|
|
109
|
+
const exporter = new exporter_logs_otlp_http_1.OTLPLogExporter({
|
|
110
|
+
url: finalEndpointUrl,
|
|
111
|
+
headers: process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS
|
|
112
|
+
? JSON.parse(process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS)
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
115
|
+
const baseResource = (0, resources_1.defaultResource)();
|
|
116
|
+
const customResource = (0, resources_1.resourceFromAttributes)(resourceAttributes);
|
|
117
|
+
const resource = baseResource.merge(customResource);
|
|
118
|
+
const processor = new sdk_logs_1.SimpleLogRecordProcessor(exporter);
|
|
119
|
+
const loggerProvider = new sdk_logs_1.LoggerProvider({
|
|
120
|
+
resource: resource,
|
|
121
|
+
processors: [processor],
|
|
122
|
+
});
|
|
123
|
+
api_logs_1.logs.setGlobalLoggerProvider(loggerProvider);
|
|
124
|
+
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
125
|
+
return loggerProvider;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error('[OTLP] Failed to initialize OTLP logging:', error instanceof Error ? error.message : String(error));
|
|
129
|
+
if (error instanceof Error && error.stack) {
|
|
130
|
+
console.error('[OTLP] Stack trace:', error.stack);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function createBeamableLogFormatter(serviceName, qualifiedServiceName, otlpLoggerProvider) {
|
|
69
136
|
return new node_stream_1.Transform({
|
|
70
137
|
objectMode: false,
|
|
71
138
|
transform(chunk, _encoding, callback) {
|
|
@@ -178,6 +245,33 @@ function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
|
178
245
|
otelFields['ResourceAttributes'] = resourceAttributes;
|
|
179
246
|
otelFields['LogAttributes'] = logAttributes;
|
|
180
247
|
Object.assign(beamableLog, otelFields);
|
|
248
|
+
if (otlpLoggerProvider) {
|
|
249
|
+
try {
|
|
250
|
+
const otlpLogger = otlpLoggerProvider.getLogger(serviceName || 'beamable-node-runtime', undefined, {
|
|
251
|
+
schemaUrl: undefined,
|
|
252
|
+
});
|
|
253
|
+
const severityNumberMap = {
|
|
254
|
+
'Debug': 5,
|
|
255
|
+
'Info': 9,
|
|
256
|
+
'Warning': 13,
|
|
257
|
+
'Error': 17,
|
|
258
|
+
'Fatal': 21,
|
|
259
|
+
};
|
|
260
|
+
otlpLogger.emit({
|
|
261
|
+
severityNumber: severityNumberMap[level] || 9,
|
|
262
|
+
severityText: severityText,
|
|
263
|
+
body: messageParts.length > 0 ? messageParts.join(' ') : 'No message',
|
|
264
|
+
attributes: {
|
|
265
|
+
...logAttributes,
|
|
266
|
+
...(pinoLog.component ? { component: String(pinoLog.component) } : {}),
|
|
267
|
+
},
|
|
268
|
+
timestamp: new Date(timestamp).getTime() * 1000000,
|
|
269
|
+
observedTimestamp: Date.now() * 1000000,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (otlpError) {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
181
275
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
182
276
|
callback(null, Buffer.from(output, 'utf8'));
|
|
183
277
|
}
|
|
@@ -196,6 +290,7 @@ function createLogger(env, options = {}) {
|
|
|
196
290
|
var _a, _b, _c;
|
|
197
291
|
const configuredDestination = (_a = options.destinationPath) !== null && _a !== void 0 ? _a : process.env.LOG_PATH;
|
|
198
292
|
const usePrettyLogs = shouldUsePrettyLogs();
|
|
293
|
+
const otlpLoggerProvider = initializeOtlpLogging(options.serviceName, options.qualifiedServiceName, env);
|
|
199
294
|
const pinoOptions = {
|
|
200
295
|
name: (_b = options.name) !== null && _b !== void 0 ? _b : 'beamable-node-runtime',
|
|
201
296
|
level: env.logLevel,
|
|
@@ -215,7 +310,7 @@ function createLogger(env, options = {}) {
|
|
|
215
310
|
};
|
|
216
311
|
if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {
|
|
217
312
|
if (!usePrettyLogs) {
|
|
218
|
-
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
313
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName, otlpLoggerProvider);
|
|
219
314
|
beamableFormatter.pipe(process.stdout);
|
|
220
315
|
return (0, pino_1.default)(pinoOptions, beamableFormatter);
|
|
221
316
|
}
|
|
@@ -238,7 +333,7 @@ function createLogger(env, options = {}) {
|
|
|
238
333
|
}
|
|
239
334
|
const resolvedDestination = configuredDestination === 'temp' ? (0, env_js_1.ensureWritableTempDirectory)() : configuredDestination;
|
|
240
335
|
if (!usePrettyLogs) {
|
|
241
|
-
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
336
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName, otlpLoggerProvider);
|
|
242
337
|
const fileStream = (0, pino_1.destination)({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
243
338
|
beamableFormatter.pipe(fileStream);
|
|
244
339
|
return (0, pino_1.default)(pinoOptions, beamableFormatter);
|
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;
|
|
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;AA+BpD,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;AA4XD,wBAAgB,YAAY,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAuF/F"}
|
package/dist/logger.js
CHANGED
|
@@ -2,6 +2,10 @@ import pino, { destination } from 'pino';
|
|
|
2
2
|
import { Transform } from 'node:stream';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { ensureWritableTempDirectory } from './env.js';
|
|
5
|
+
import { logs } from '@opentelemetry/api-logs';
|
|
6
|
+
import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
7
|
+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
8
|
+
import { resourceFromAttributes, defaultResource } from '@opentelemetry/resources';
|
|
5
9
|
function getRequire() {
|
|
6
10
|
// Check if we're in CJS context (require.main exists)
|
|
7
11
|
if (typeof require !== 'undefined' && typeof require.main !== 'undefined') {
|
|
@@ -46,13 +50,114 @@ function mapPinoLevelToBeamableLevel(level) {
|
|
|
46
50
|
return 'Info';
|
|
47
51
|
}
|
|
48
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Initializes OpenTelemetry OTLP log exporter if configured.
|
|
55
|
+
* Similar to C# microservices, checks for BEAM_OTEL_EXPORTER_OTLP_ENDPOINT or uses standard enabled flag.
|
|
56
|
+
*
|
|
57
|
+
* @param serviceName - Service name for resource attributes
|
|
58
|
+
* @param qualifiedServiceName - Qualified service name for resource attributes
|
|
59
|
+
* @param env - Environment configuration
|
|
60
|
+
* @returns OTLP logger provider if configured, null otherwise
|
|
61
|
+
*/
|
|
62
|
+
function initializeOtlpLogging(serviceName, qualifiedServiceName, env) {
|
|
63
|
+
// Check for explicit OTLP endpoint (same as C#: BEAM_OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
64
|
+
// Also check standard OTEL environment variables
|
|
65
|
+
const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
66
|
+
|| process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
67
|
+
|| (process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS : null);
|
|
68
|
+
// Check if standard OTLP is enabled (similar to C# OtelExporterStandardEnabled)
|
|
69
|
+
// In Docker containers, OTLP should be enabled unless explicitly disabled
|
|
70
|
+
// Simple check: if IS_LOCAL is not set, we're likely in a container
|
|
71
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
72
|
+
const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
73
|
+
// If no explicit endpoint and standard OTLP not enabled, skip OTLP
|
|
74
|
+
if (!otlpEndpoint && !standardOtelEnabled) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// Build resource attributes (similar to C# resourceProvider)
|
|
79
|
+
const resourceAttributes = {};
|
|
80
|
+
if (serviceName) {
|
|
81
|
+
resourceAttributes['service.namespace'] = serviceName;
|
|
82
|
+
resourceAttributes['service.name'] = serviceName;
|
|
83
|
+
}
|
|
84
|
+
if (qualifiedServiceName) {
|
|
85
|
+
resourceAttributes['service.instance.id'] = qualifiedServiceName;
|
|
86
|
+
}
|
|
87
|
+
if (env?.cid) {
|
|
88
|
+
resourceAttributes['beam.cid'] = String(env.cid);
|
|
89
|
+
}
|
|
90
|
+
if (env?.pid) {
|
|
91
|
+
resourceAttributes['beam.pid'] = String(env.pid);
|
|
92
|
+
}
|
|
93
|
+
if (env?.routingKey) {
|
|
94
|
+
resourceAttributes['beam.routing_key'] = String(env.routingKey);
|
|
95
|
+
}
|
|
96
|
+
// Determine endpoint URL
|
|
97
|
+
// If explicit endpoint provided, use it (append /v1/logs if needed for HTTP)
|
|
98
|
+
// Note: C# microservices discover the collector endpoint via socket communication or start a collector
|
|
99
|
+
// For Node.js, we require an explicit endpoint to be configured - we don't assume localhost
|
|
100
|
+
// The OTLP endpoint should be provided via BEAM_OTEL_EXPORTER_OTLP_ENDPOINT environment variable
|
|
101
|
+
let endpointUrl = otlpEndpoint;
|
|
102
|
+
// If no explicit endpoint is provided, we cannot use OTLP
|
|
103
|
+
// C# microservices can discover/start collectors, but Node.js requires explicit configuration
|
|
104
|
+
if (!endpointUrl) {
|
|
105
|
+
if (standardOtelEnabled) {
|
|
106
|
+
// Log that OTLP is expected but endpoint not configured
|
|
107
|
+
console.error('[OTLP] Standard OTLP is enabled but no endpoint configured. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Ensure endpoint has /v1/logs suffix for HTTP exporter (like C# does)
|
|
112
|
+
const finalEndpointUrl = endpointUrl.includes('/v1/logs')
|
|
113
|
+
? endpointUrl
|
|
114
|
+
: `${endpointUrl.replace(/\/$/, '')}/v1/logs`;
|
|
115
|
+
// Create OTLP HTTP exporter (C# uses HttpProtobuf by default)
|
|
116
|
+
const exporter = new OTLPLogExporter({
|
|
117
|
+
url: finalEndpointUrl,
|
|
118
|
+
// Headers if provided (similar to C# OtelExporterOtlpHeaders)
|
|
119
|
+
headers: process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS
|
|
120
|
+
? JSON.parse(process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS)
|
|
121
|
+
: undefined,
|
|
122
|
+
});
|
|
123
|
+
// Create resource with attributes (merge with default resource)
|
|
124
|
+
const baseResource = defaultResource();
|
|
125
|
+
const customResource = resourceFromAttributes(resourceAttributes);
|
|
126
|
+
const resource = baseResource.merge(customResource);
|
|
127
|
+
// Create log record processor
|
|
128
|
+
const processor = new SimpleLogRecordProcessor(exporter);
|
|
129
|
+
// Create logger provider with resource and processor
|
|
130
|
+
const loggerProvider = new LoggerProvider({
|
|
131
|
+
resource: resource,
|
|
132
|
+
processors: [processor],
|
|
133
|
+
});
|
|
134
|
+
// Set as global logger provider
|
|
135
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
136
|
+
// Log successful initialization (to stdout for debugging)
|
|
137
|
+
// This helps diagnose if OTLP is working in deployed environments
|
|
138
|
+
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
139
|
+
return loggerProvider;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// If OTLP initialization fails, log error but continue without OTLP
|
|
143
|
+
// Don't throw - we still want stdout logging to work
|
|
144
|
+
// Log to stderr so it's visible in container logs
|
|
145
|
+
console.error('[OTLP] Failed to initialize OTLP logging:', error instanceof Error ? error.message : String(error));
|
|
146
|
+
if (error instanceof Error && error.stack) {
|
|
147
|
+
console.error('[OTLP] Stack trace:', error.stack);
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
49
152
|
/**
|
|
50
153
|
* Creates a transform stream that converts Pino JSON logs to Beamable's expected format.
|
|
51
154
|
* Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.
|
|
52
155
|
* Also includes OpenTelemetry-compatible fields for ClickHouse compatibility.
|
|
53
156
|
* Pino writes JSON strings (one per line) to the stream.
|
|
157
|
+
*
|
|
158
|
+
* Also sends logs via OTLP if OTLP logger provider is configured.
|
|
54
159
|
*/
|
|
55
|
-
function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
160
|
+
function createBeamableLogFormatter(serviceName, qualifiedServiceName, otlpLoggerProvider) {
|
|
56
161
|
return new Transform({
|
|
57
162
|
objectMode: false, // Pino writes strings/Buffers, not objects
|
|
58
163
|
transform(chunk, _encoding, callback) {
|
|
@@ -211,6 +316,41 @@ function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
|
211
316
|
// An OpenTelemetry collector can parse these fields and forward to ClickHouse
|
|
212
317
|
// Note: These extra fields won't break CloudWatch - it will just store them in @message
|
|
213
318
|
Object.assign(beamableLog, otelFields);
|
|
319
|
+
// Send log via OTLP if configured (similar to C# MicroserviceOtelLogRecordExporter)
|
|
320
|
+
if (otlpLoggerProvider) {
|
|
321
|
+
try {
|
|
322
|
+
const otlpLogger = otlpLoggerProvider.getLogger(serviceName || 'beamable-node-runtime', undefined, // version
|
|
323
|
+
{
|
|
324
|
+
schemaUrl: undefined, // optional schema URL
|
|
325
|
+
});
|
|
326
|
+
// Map Beamable level to OpenTelemetry SeverityNumber
|
|
327
|
+
const severityNumberMap = {
|
|
328
|
+
'Debug': 5, // SEVERITY_NUMBER_DEBUG
|
|
329
|
+
'Info': 9, // SEVERITY_NUMBER_INFO
|
|
330
|
+
'Warning': 13, // SEVERITY_NUMBER_WARN
|
|
331
|
+
'Error': 17, // SEVERITY_NUMBER_ERROR
|
|
332
|
+
'Fatal': 21, // SEVERITY_NUMBER_FATAL
|
|
333
|
+
};
|
|
334
|
+
// Create log record for OTLP
|
|
335
|
+
otlpLogger.emit({
|
|
336
|
+
severityNumber: severityNumberMap[level] || 9,
|
|
337
|
+
severityText: severityText,
|
|
338
|
+
body: messageParts.length > 0 ? messageParts.join(' ') : 'No message',
|
|
339
|
+
attributes: {
|
|
340
|
+
...logAttributes,
|
|
341
|
+
// Include additional context
|
|
342
|
+
...(pinoLog.component ? { component: String(pinoLog.component) } : {}),
|
|
343
|
+
},
|
|
344
|
+
timestamp: new Date(timestamp).getTime() * 1_000_000, // nanoseconds
|
|
345
|
+
observedTimestamp: Date.now() * 1_000_000, // nanoseconds
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (otlpError) {
|
|
349
|
+
// If OTLP send fails, continue with stdout logging
|
|
350
|
+
// Don't block the log output
|
|
351
|
+
// Could add retry logic here similar to C# if needed
|
|
352
|
+
}
|
|
353
|
+
}
|
|
214
354
|
// Output as a single-line JSON string (required for CloudWatch)
|
|
215
355
|
// CloudWatch Logs Insights will store this entire JSON string in the @message field
|
|
216
356
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
@@ -231,6 +371,9 @@ function createBeamableLogFormatter(serviceName, qualifiedServiceName) {
|
|
|
231
371
|
export function createLogger(env, options = {}) {
|
|
232
372
|
const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
|
|
233
373
|
const usePrettyLogs = shouldUsePrettyLogs();
|
|
374
|
+
// Initialize OTLP logging if configured (similar to C# microservices)
|
|
375
|
+
// This sends logs via OpenTelemetry Protocol in addition to stdout JSON
|
|
376
|
+
const otlpLoggerProvider = initializeOtlpLogging(options.serviceName, options.qualifiedServiceName, env);
|
|
234
377
|
const pinoOptions = {
|
|
235
378
|
name: options.name ?? 'beamable-node-runtime',
|
|
236
379
|
level: env.logLevel,
|
|
@@ -256,7 +399,8 @@ export function createLogger(env, options = {}) {
|
|
|
256
399
|
if (!usePrettyLogs) {
|
|
257
400
|
// Deployed/remote: Use Beamable JSON format for log collection
|
|
258
401
|
// Include OpenTelemetry fields for ClickHouse compatibility
|
|
259
|
-
|
|
402
|
+
// Also send logs via OTLP if configured
|
|
403
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName, otlpLoggerProvider);
|
|
260
404
|
beamableFormatter.pipe(process.stdout);
|
|
261
405
|
return pino(pinoOptions, beamableFormatter);
|
|
262
406
|
}
|
|
@@ -289,7 +433,7 @@ export function createLogger(env, options = {}) {
|
|
|
289
433
|
// For file logging: Use Beamable format if not local, default Pino format if local
|
|
290
434
|
const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;
|
|
291
435
|
if (!usePrettyLogs) {
|
|
292
|
-
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
436
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName, otlpLoggerProvider);
|
|
293
437
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
294
438
|
beamableFormatter.pipe(fileStream);
|
|
295
439
|
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":"AAAA,OAAO,IAAI,EAAE,EAAE,WAAW,EAAmC,MAAM,MAAM,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAKvD,SAAS,UAAU;IACjB,sDAAsD;IACtD,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC1E,qCAAqC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,uDAAuD;IACvD,yEAAyE;IACzE,4EAA4E;IAC5E,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,SAAS,mBAAmB;IAC1B,mCAAmC;IACnC,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC;AACzE,CAAC;AASD;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,KAAa;IAChD,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,EAAE,EAAE,QAAQ;YACf,OAAO,OAAO,CAAC;QACjB,KAAK,EAAE,EAAE,QAAQ;YACf,OAAO,OAAO,CAAC;QACjB,KAAK,EAAE,EAAE,OAAO;YACd,OAAO,MAAM,CAAC;QAChB,KAAK,EAAE,EAAE,OAAO;YACd,OAAO,SAAS,CAAC;QACnB,KAAK,EAAE,EAAE,QAAQ;YACf,OAAO,OAAO,CAAC;QACjB,KAAK,EAAE,EAAE,QAAQ;YACf,OAAO,OAAO,CAAC;QACjB;YACE,OAAO,MAAM,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,0BAA0B,CAAC,WAAoB,EAAE,oBAA6B;IACrF,OAAO,IAAI,SAAS,CAAC;QACnB,UAAU,EAAE,KAAK,EAAE,2CAA2C;QAC9D,SAAS,CAAC,KAAa,EAAE,SAAS,EAAE,QAAQ;YAC1C,8DAA8D;YAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnE,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACrC,mBAAmB;gBACnB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;oBACjB,QAAQ,EAAE,CAAC;oBACX,OAAO;gBACT,CAAC;gBAED,6BAA6B;gBAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAEjC,+EAA+E;gBAC/E,0CAA0C;gBAC1C,IAAI,SAAiB,CAAC;gBACtB,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACrC,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;gBAC3B,CAAC;qBAAM,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC5C,SAAS,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBACnD,CAAC;qBAAM,CAAC;oBACN,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;gBACvC,CAAC;gBAED,mCAAmC;gBACnC,MAAM,KAAK,GAAG,2BAA2B,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAEzD,6DAA6D;gBAC7D,8CAA8C;gBAC9C,MAAM,YAAY,GAAa,EAAE,CAAC;gBAClC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;oBAChB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACjC,CAAC;gBAED,uCAAuC;gBACvC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;oBAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;oBACxB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,GAAG,IAAI,OAAO,CAAC;oBACjD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACnD,YAAY,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC;gBAC5C,CAAC;gBAED,+DAA+D;gBAC/D,MAAM,WAAW,GAA4B;oBAC3C,GAAG,EAAE,SAAS;oBACd,GAAG,EAAE,KAAK;oBACV,GAAG,EAAE,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY;iBACrE,CAAC;gBAEF,yDAAyD;gBACzD,uEAAuE;gBACvE,MAAM,aAAa,GAA4B,EAAE,CAAC;gBAClD,IAAI,OAAO,CAAC,GAAG;oBAAE,aAAa,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;gBACjD,IAAI,OAAO,CAAC,GAAG;oBAAE,aAAa,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;gBACjD,IAAI,OAAO,CAAC,UAAU;oBAAE,aAAa,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;gBACtE,IAAI,OAAO,CAAC,OAAO;oBAAE,aAAa,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;gBAC7D,IAAI,OAAO,CAAC,SAAS;oBAAE,aAAa,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;gBAEnE,2DAA2D;gBAC3D,IAAI,WAAW,EAAE,CAAC;oBAChB,aAAa,CAAC,WAAW,GAAG,WAAW,CAAC;gBAC1C,CAAC;gBACD,IAAI,oBAAoB,EAAE,CAAC;oBACzB,aAAa,CAAC,oBAAoB,GAAG,oBAAoB,CAAC;gBAC5D,CAAC;gBAED,4DAA4D;gBAC5D,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;gBACtK,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;oBACnD,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;wBAC/E,aAAa,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;oBAC7B,CAAC;gBACH,CAAC;gBAED,uDAAuD;gBACvD,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1C,WAAW,CAAC,GAAG,GAAG,aAAa,CAAC;gBAClC,CAAC;gBAED,mEAAmE;gBACnE,wFAAwF;gBACxF,0EAA0E;gBAC1E,MAAM,UAAU,GAA4B,EAAE,CAAC;gBAE/C,yEAAyE;gBACzE,MAAM,kBAAkB,GAA4B,EAAE,CAAC;gBACvD,wFAAwF;gBACxF,kFAAkF;gBAClF,4FAA4F;gBAC5F,0DAA0D;gBAC1D,IAAI,WAAW,EAAE,CAAC;oBAChB,kBAAkB,CAAC,mBAAmB,CAAC,GAAG,WAAW,CAAC;oBACtD,kBAAkB,CAAC,cAAc,CAAC,GAAG,WAAW,CAAC;gBACnD,CAAC;gBACD,4CAA4C;gBAC5C,IAAI,oBAAoB,EAAE,CAAC;oBACzB,kBAAkB,CAAC,uBAAuB,CAAC,GAAG,oBAAoB,CAAC;gBACrE,CAAC;gBACD,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;oBAChB,kBAAkB,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACvD,CAAC;gBACD,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;oBAChB,kBAAkB,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBACvD,CAAC;gBACD,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;oBACvB,kBAAkB,CAAC,kBAAkB,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;gBACtE,CAAC;gBAED,0CAA0C;gBAC1C,MAAM,aAAa,GAA4B,EAAE,CAAC;gBAClD,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;oBACtB,aAAa,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACzD,CAAC;gBACD,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;oBAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;oBACxB,IAAI,GAAG,CAAC,OAAO;wBAAE,aAAa,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBAC1E,IAAI,GAAG,CAAC,KAAK;wBAAE,aAAa,CAAC,sBAAsB,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBACzE,IAAI,GAAG,CAAC,IAAI;wBAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnE,CAAC;gBAED,uDAAuD;gBACvD,+CAA+C;gBAC/C,qEAAqE;gBACrE,MAAM,eAAe,GAA2B;oBAC9C,OAAO,EAAE,OAAO;oBAChB,MAAM,EAAE,aAAa;oBACrB,SAAS,EAAE,SAAS;oBACpB,OAAO,EAAE,OAAO;oBAChB,OAAO,EAAE,UAAU;iBACpB,CAAC;gBACF,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC;gBAE7D,wFAAwF;gBACxF,sEAAsE;gBACtE,8GAA8G;gBAE9G,sFAAsF;gBACtF,iEAAiE;gBACjE,IAAI,WAAW,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,EAAE,CAAC;oBAC5D,kBAAkB,CAAC,mBAAmB,CAAC,GAAG,WAAW,CAAC;oBACtD,kBAAkB,CAAC,cAAc,CAAC,GAAG,WAAW,CAAC;gBACnD,CAAC;gBAED,sCAAsC;gBACtC,mFAAmF;gBACnF,0FAA0F;gBAC1F,UAAU,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,CAAC,4CAA4C;gBACjF,UAAU,CAAC,cAAc,CAAC,GAAG,YAAY,CAAC;gBAC1C,UAAU,CAAC,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;gBACrF,qFAAqF;gBACrF,UAAU,CAAC,oBAAoB,CAAC,GAAG,kBAAkB,CAAC;gBACtD,gFAAgF;gBAChF,UAAU,CAAC,eAAe,CAAC,GAAG,aAAa,CAAC;gBAE5C,0FAA0F;gBAC1F,8FAA8F;gBAC9F,wFAAwF;gBACxF,uFAAuF;gBACvF,8CAA8C;gBAE9C,yEAAyE;gBACzE,8EAA8E;gBAC9E,wFAAwF;gBACxF,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;gBAEvC,gEAAgE;gBAChE,oFAAoF;gBACpF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;gBAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;YAC9C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,gDAAgD;gBAChD,MAAM,WAAW,GAAG;oBAClB,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;oBAC7B,GAAG,EAAE,OAAO;oBACZ,GAAG,EAAE,8BAA8B,KAAK,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;iBACxE,CAAC;gBACF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAsB,EAAE,UAAgC,EAAE;IACrF,MAAM,qBAAqB,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9E,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAC;IAE5C,MAAM,WAAW,GAAkB;QACjC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,uBAAuB;QAC7C,KAAK,EAAE,GAAG,CAAC,QAAQ;QACnB,IAAI,EAAE;YACJ,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,IAAI;YAClC,mBAAmB,EAAE,GAAG,CAAC,mBAAmB;YAC5C,oDAAoD;YACpD,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;SACnD;QACD,MAAM,EAAE;YACN,KAAK,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC;YACjC,MAAM,EAAE,KAAK;SACd;QACD,uEAAuE;QACvE,SAAS,EAAE,IAAI,CAAC,gBAAgB,CAAC,OAAO;KACzC,CAAC;IAEF,yFAAyF;IACzF,+EAA+E;IAC/E,IAAI,CAAC,qBAAqB,IAAI,qBAAqB,KAAK,GAAG,IAAI,qBAAqB,KAAK,QAAQ,IAAI,qBAAqB,KAAK,SAAS,EAAE,CAAC;QACzI,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,+DAA+D;YAC/D,4DAA4D;YAC5D,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;YACxG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACvC,OAAO,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,wEAAwE;YACxE,4DAA4D;YAC5D,0DAA0D;YAC1D,IAAI,CAAC;gBACH,oCAAoC;gBACpC,2DAA2D;gBAC3D,MAAM,SAAS,GAAG,UAAU,EAAE,CAAC;gBAC/B,MAAM,UAAU,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC;gBAC5C,iDAAiD;gBACjD,MAAM,YAAY,GAAG,UAAU,CAAC;oBAC9B,QAAQ,EAAE,IAAI;oBACd,aAAa,EAAE,YAAY;oBAC3B,MAAM,EAAE,cAAc;oBACtB,UAAU,EAAE,KAAK;iBAClB,CAAC,CAAC;gBACH,kCAAkC;gBAClC,OAAO,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,yEAAyE;gBACzE,4EAA4E;gBAC5E,OAAO,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,mFAAmF;IACnF,MAAM,mBAAmB,GAAG,qBAAqB,KAAK,MAAM,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACrH,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACxG,MAAM,UAAU,GAAG,WAAW,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtG,iBAAiB,CAAC,IAAI,CAAC,UAA8C,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAC9C,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,WAAW,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtG,OAAO,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACvC,CAAC;AACH,CAAC","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/Buffers, not objects\r\n transform(chunk: Buffer, _encoding, callback) {\r\n // Ensure we have a Buffer - Pino may write strings or Buffers\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\r\n try {\r\n const line = buffer.toString('utf8');\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 // IMPORTANT: The Portal searches for service:ExampleNodeService (just the service name)\r\n // So we need to set service.namespace to the service name, NOT the qualified name\r\n // The qualified name (micro_ExampleNodeService) is used for CloudWatch log stream filtering\r\n // But ClickHouse uses just the service name for filtering\r\n if (serviceName) {\r\n resourceAttributes['service.namespace'] = serviceName;\r\n resourceAttributes['service.name'] = serviceName;\r\n }\r\n // Also include qualified name for reference\r\n if (qualifiedServiceName) {\r\n resourceAttributes['service.qualifiedName'] = qualifiedServiceName;\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 // CRITICAL: ResourceAttributes and LogAttributes MUST always be present (even if empty)\r\n // ClickHouse schema expects these fields to exist for proper querying\r\n // The Portal queries ResourceAttributes['service.namespace'] - if ResourceAttributes is missing, queries fail\r\n \r\n // Ensure ResourceAttributes always has service.namespace when serviceName is provided\r\n // This is the primary field used by Portal for service filtering\r\n if (serviceName && !resourceAttributes['service.namespace']) {\r\n resourceAttributes['service.namespace'] = serviceName;\r\n resourceAttributes['service.name'] = serviceName;\r\n }\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 // CRITICAL: Always include all required OpenTelemetry fields for ClickHouse compatibility\r\n otelFields['Timestamp'] = timestamp; // OpenTelemetry timestamp format (ISO 8601)\r\n otelFields['SeverityText'] = severityText;\r\n otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';\r\n // ALWAYS include ResourceAttributes (even if empty) - required for ClickHouse schema\r\n otelFields['ResourceAttributes'] = resourceAttributes;\r\n // ALWAYS include LogAttributes (even if empty) - required for ClickHouse schema\r\n otelFields['LogAttributes'] = logAttributes;\r\n \r\n // IMPORTANT: For CloudWatch Logs Insights, we need to ensure @message contains valid JSON\r\n // The Portal expects @message to be a JSON string that can be parsed to extract __t, __l, __m\r\n // We output the Beamable format as the primary format, and include OpenTelemetry fields\r\n // CloudWatch will store this as @message, and an OpenTelemetry collector will parse it\r\n // and forward to ClickHouse's otel_logs table\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 // Note: These extra fields won't break CloudWatch - it will just store them in @message\r\n Object.assign(beamableLog, otelFields);\r\n \r\n // Output as a single-line JSON string (required for CloudWatch)\r\n // CloudWatch Logs Insights will store this entire JSON string in the @message field\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"]}
|
|
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\nimport { logs } from '@opentelemetry/api-logs';\r\nimport { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';\r\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\r\nimport { resourceFromAttributes, defaultResource } from '@opentelemetry/resources';\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 * Initializes OpenTelemetry OTLP log exporter if configured.\r\n * Similar to C# microservices, checks for BEAM_OTEL_EXPORTER_OTLP_ENDPOINT or uses standard enabled flag.\r\n * \r\n * @param serviceName - Service name for resource attributes\r\n * @param qualifiedServiceName - Qualified service name for resource attributes\r\n * @param env - Environment configuration\r\n * @returns OTLP logger provider if configured, null otherwise\r\n */\r\nfunction initializeOtlpLogging(\r\n serviceName?: string,\r\n qualifiedServiceName?: string,\r\n env?: EnvironmentConfig\r\n): LoggerProvider | null {\r\n // Check for explicit OTLP endpoint (same as C#: BEAM_OTEL_EXPORTER_OTLP_ENDPOINT)\r\n // Also check standard OTEL environment variables\r\n const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT \r\n || process.env.OTEL_EXPORTER_OTLP_ENDPOINT\r\n || (process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS : null);\r\n \r\n // Check if standard OTLP is enabled (similar to C# OtelExporterStandardEnabled)\r\n // In Docker containers, OTLP should be enabled unless explicitly disabled\r\n // Simple check: if IS_LOCAL is not set, we're likely in a container\r\n const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';\r\n const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;\r\n \r\n // If no explicit endpoint and standard OTLP not enabled, skip OTLP\r\n if (!otlpEndpoint && !standardOtelEnabled) {\r\n return null;\r\n }\r\n \r\n try {\r\n // Build resource attributes (similar to C# resourceProvider)\r\n const resourceAttributes: Record<string, string> = {};\r\n if (serviceName) {\r\n resourceAttributes['service.namespace'] = serviceName;\r\n resourceAttributes['service.name'] = serviceName;\r\n }\r\n if (qualifiedServiceName) {\r\n resourceAttributes['service.instance.id'] = qualifiedServiceName;\r\n }\r\n if (env?.cid) {\r\n resourceAttributes['beam.cid'] = String(env.cid);\r\n }\r\n if (env?.pid) {\r\n resourceAttributes['beam.pid'] = String(env.pid);\r\n }\r\n if (env?.routingKey) {\r\n resourceAttributes['beam.routing_key'] = String(env.routingKey);\r\n }\r\n \r\n // Determine endpoint URL\r\n // If explicit endpoint provided, use it (append /v1/logs if needed for HTTP)\r\n // Note: C# microservices discover the collector endpoint via socket communication or start a collector\r\n // For Node.js, we require an explicit endpoint to be configured - we don't assume localhost\r\n // The OTLP endpoint should be provided via BEAM_OTEL_EXPORTER_OTLP_ENDPOINT environment variable\r\n let endpointUrl = otlpEndpoint;\r\n \r\n // If no explicit endpoint is provided, we cannot use OTLP\r\n // C# microservices can discover/start collectors, but Node.js requires explicit configuration\r\n if (!endpointUrl) {\r\n if (standardOtelEnabled) {\r\n // Log that OTLP is expected but endpoint not configured\r\n console.error('[OTLP] Standard OTLP is enabled but no endpoint configured. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');\r\n }\r\n return null;\r\n }\r\n \r\n // Ensure endpoint has /v1/logs suffix for HTTP exporter (like C# does)\r\n const finalEndpointUrl = endpointUrl.includes('/v1/logs') \r\n ? endpointUrl \r\n : `${endpointUrl.replace(/\\/$/, '')}/v1/logs`;\r\n \r\n // Create OTLP HTTP exporter (C# uses HttpProtobuf by default)\r\n const exporter = new OTLPLogExporter({\r\n url: finalEndpointUrl,\r\n // Headers if provided (similar to C# OtelExporterOtlpHeaders)\r\n headers: process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS \r\n ? JSON.parse(process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS)\r\n : undefined,\r\n });\r\n \r\n // Create resource with attributes (merge with default resource)\r\n const baseResource = defaultResource();\r\n const customResource = resourceFromAttributes(resourceAttributes);\r\n const resource = baseResource.merge(customResource);\r\n \r\n // Create log record processor\r\n const processor = new SimpleLogRecordProcessor(exporter);\r\n \r\n // Create logger provider with resource and processor\r\n const loggerProvider = new LoggerProvider({\r\n resource: resource,\r\n processors: [processor],\r\n });\r\n \r\n // Set as global logger provider\r\n logs.setGlobalLoggerProvider(loggerProvider);\r\n \r\n // Log successful initialization (to stdout for debugging)\r\n // This helps diagnose if OTLP is working in deployed environments\r\n console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);\r\n \r\n return loggerProvider;\r\n } catch (error) {\r\n // If OTLP initialization fails, log error but continue without OTLP\r\n // Don't throw - we still want stdout logging to work\r\n // Log to stderr so it's visible in container logs\r\n console.error('[OTLP] Failed to initialize OTLP logging:', error instanceof Error ? error.message : String(error));\r\n if (error instanceof Error && error.stack) {\r\n console.error('[OTLP] Stack trace:', error.stack);\r\n }\r\n return null;\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\n * Also sends logs via OTLP if OTLP logger provider is configured.\r\n */\r\nfunction createBeamableLogFormatter(\r\n serviceName?: string,\r\n qualifiedServiceName?: string,\r\n otlpLoggerProvider?: LoggerProvider | null\r\n): Transform {\r\n return new Transform({\r\n objectMode: false, // Pino writes strings/Buffers, not objects\r\n transform(chunk: Buffer, _encoding, callback) {\r\n // Ensure we have a Buffer - Pino may write strings or Buffers\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\r\n try {\r\n const line = buffer.toString('utf8');\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 // IMPORTANT: The Portal searches for service:ExampleNodeService (just the service name)\r\n // So we need to set service.namespace to the service name, NOT the qualified name\r\n // The qualified name (micro_ExampleNodeService) is used for CloudWatch log stream filtering\r\n // But ClickHouse uses just the service name for filtering\r\n if (serviceName) {\r\n resourceAttributes['service.namespace'] = serviceName;\r\n resourceAttributes['service.name'] = serviceName;\r\n }\r\n // Also include qualified name for reference\r\n if (qualifiedServiceName) {\r\n resourceAttributes['service.qualifiedName'] = qualifiedServiceName;\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 // CRITICAL: ResourceAttributes and LogAttributes MUST always be present (even if empty)\r\n // ClickHouse schema expects these fields to exist for proper querying\r\n // The Portal queries ResourceAttributes['service.namespace'] - if ResourceAttributes is missing, queries fail\r\n \r\n // Ensure ResourceAttributes always has service.namespace when serviceName is provided\r\n // This is the primary field used by Portal for service filtering\r\n if (serviceName && !resourceAttributes['service.namespace']) {\r\n resourceAttributes['service.namespace'] = serviceName;\r\n resourceAttributes['service.name'] = serviceName;\r\n }\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 // CRITICAL: Always include all required OpenTelemetry fields for ClickHouse compatibility\r\n otelFields['Timestamp'] = timestamp; // OpenTelemetry timestamp format (ISO 8601)\r\n otelFields['SeverityText'] = severityText;\r\n otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';\r\n // ALWAYS include ResourceAttributes (even if empty) - required for ClickHouse schema\r\n otelFields['ResourceAttributes'] = resourceAttributes;\r\n // ALWAYS include LogAttributes (even if empty) - required for ClickHouse schema\r\n otelFields['LogAttributes'] = logAttributes;\r\n \r\n // IMPORTANT: For CloudWatch Logs Insights, we need to ensure @message contains valid JSON\r\n // The Portal expects @message to be a JSON string that can be parsed to extract __t, __l, __m\r\n // We output the Beamable format as the primary format, and include OpenTelemetry fields\r\n // CloudWatch will store this as @message, and an OpenTelemetry collector will parse it\r\n // and forward to ClickHouse's otel_logs table\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 // Note: These extra fields won't break CloudWatch - it will just store them in @message\r\n Object.assign(beamableLog, otelFields);\r\n \r\n // Send log via OTLP if configured (similar to C# MicroserviceOtelLogRecordExporter)\r\n if (otlpLoggerProvider) {\r\n try {\r\n const otlpLogger = otlpLoggerProvider.getLogger(\r\n serviceName || 'beamable-node-runtime',\r\n undefined, // version\r\n {\r\n schemaUrl: undefined, // optional schema URL\r\n }\r\n );\r\n \r\n // Map Beamable level to OpenTelemetry SeverityNumber\r\n const severityNumberMap: Record<string, number> = {\r\n 'Debug': 5, // SEVERITY_NUMBER_DEBUG\r\n 'Info': 9, // SEVERITY_NUMBER_INFO\r\n 'Warning': 13, // SEVERITY_NUMBER_WARN\r\n 'Error': 17, // SEVERITY_NUMBER_ERROR\r\n 'Fatal': 21, // SEVERITY_NUMBER_FATAL\r\n };\r\n \r\n // Create log record for OTLP\r\n otlpLogger.emit({\r\n severityNumber: severityNumberMap[level] || 9,\r\n severityText: severityText,\r\n body: messageParts.length > 0 ? messageParts.join(' ') : 'No message',\r\n attributes: {\r\n ...logAttributes,\r\n // Include additional context\r\n ...(pinoLog.component ? { component: String(pinoLog.component) } : {}),\r\n },\r\n timestamp: new Date(timestamp).getTime() * 1_000_000, // nanoseconds\r\n observedTimestamp: Date.now() * 1_000_000, // nanoseconds\r\n });\r\n } catch (otlpError) {\r\n // If OTLP send fails, continue with stdout logging\r\n // Don't block the log output\r\n // Could add retry logic here similar to C# if needed\r\n }\r\n }\r\n \r\n // Output as a single-line JSON string (required for CloudWatch)\r\n // CloudWatch Logs Insights will store this entire JSON string in the @message field\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 // Initialize OTLP logging if configured (similar to C# microservices)\r\n // This sends logs via OpenTelemetry Protocol in addition to stdout JSON\r\n const otlpLoggerProvider = initializeOtlpLogging(\r\n options.serviceName,\r\n options.qualifiedServiceName,\r\n env\r\n );\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 // Also send logs via OTLP if configured\r\n const beamableFormatter = createBeamableLogFormatter(\r\n options.serviceName,\r\n options.qualifiedServiceName,\r\n otlpLoggerProvider\r\n );\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 options.serviceName,\r\n options.qualifiedServiceName,\r\n otlpLoggerProvider\r\n );\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omen.foundation/node-microservice-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Beamable microservice runtime for Node.js/TypeScript services.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
},
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@opentelemetry/api": "^1.9.0",
|
|
31
|
+
"@opentelemetry/api-logs": "^0.208.0",
|
|
32
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
|
33
|
+
"@opentelemetry/resources": "^2.2.0",
|
|
34
|
+
"@opentelemetry/sdk-logs": "^0.208.0",
|
|
30
35
|
"beamable-sdk": "^0.6.0",
|
|
31
36
|
"dotenv": "^16.4.7",
|
|
32
37
|
"eventemitter3": "^5.0.1",
|
package/src/logger.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { Transform } from 'node:stream';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { ensureWritableTempDirectory } from './env.js';
|
|
5
5
|
import type { EnvironmentConfig } from './types.js';
|
|
6
|
+
import { logs } from '@opentelemetry/api-logs';
|
|
7
|
+
import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
8
|
+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
9
|
+
import { resourceFromAttributes, defaultResource } from '@opentelemetry/resources';
|
|
6
10
|
|
|
7
11
|
// Helper to get require function that works in both CJS and ESM
|
|
8
12
|
declare const require: any;
|
|
@@ -60,13 +64,135 @@ function mapPinoLevelToBeamableLevel(level: number): string {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Initializes OpenTelemetry OTLP log exporter if configured.
|
|
69
|
+
* Similar to C# microservices, checks for BEAM_OTEL_EXPORTER_OTLP_ENDPOINT or uses standard enabled flag.
|
|
70
|
+
*
|
|
71
|
+
* @param serviceName - Service name for resource attributes
|
|
72
|
+
* @param qualifiedServiceName - Qualified service name for resource attributes
|
|
73
|
+
* @param env - Environment configuration
|
|
74
|
+
* @returns OTLP logger provider if configured, null otherwise
|
|
75
|
+
*/
|
|
76
|
+
function initializeOtlpLogging(
|
|
77
|
+
serviceName?: string,
|
|
78
|
+
qualifiedServiceName?: string,
|
|
79
|
+
env?: EnvironmentConfig
|
|
80
|
+
): LoggerProvider | null {
|
|
81
|
+
// Check for explicit OTLP endpoint (same as C#: BEAM_OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
82
|
+
// Also check standard OTEL environment variables
|
|
83
|
+
const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
84
|
+
|| process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
85
|
+
|| (process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS : null);
|
|
86
|
+
|
|
87
|
+
// Check if standard OTLP is enabled (similar to C# OtelExporterStandardEnabled)
|
|
88
|
+
// In Docker containers, OTLP should be enabled unless explicitly disabled
|
|
89
|
+
// Simple check: if IS_LOCAL is not set, we're likely in a container
|
|
90
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
91
|
+
const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
92
|
+
|
|
93
|
+
// If no explicit endpoint and standard OTLP not enabled, skip OTLP
|
|
94
|
+
if (!otlpEndpoint && !standardOtelEnabled) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Build resource attributes (similar to C# resourceProvider)
|
|
100
|
+
const resourceAttributes: Record<string, string> = {};
|
|
101
|
+
if (serviceName) {
|
|
102
|
+
resourceAttributes['service.namespace'] = serviceName;
|
|
103
|
+
resourceAttributes['service.name'] = serviceName;
|
|
104
|
+
}
|
|
105
|
+
if (qualifiedServiceName) {
|
|
106
|
+
resourceAttributes['service.instance.id'] = qualifiedServiceName;
|
|
107
|
+
}
|
|
108
|
+
if (env?.cid) {
|
|
109
|
+
resourceAttributes['beam.cid'] = String(env.cid);
|
|
110
|
+
}
|
|
111
|
+
if (env?.pid) {
|
|
112
|
+
resourceAttributes['beam.pid'] = String(env.pid);
|
|
113
|
+
}
|
|
114
|
+
if (env?.routingKey) {
|
|
115
|
+
resourceAttributes['beam.routing_key'] = String(env.routingKey);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Determine endpoint URL
|
|
119
|
+
// If explicit endpoint provided, use it (append /v1/logs if needed for HTTP)
|
|
120
|
+
// Note: C# microservices discover the collector endpoint via socket communication or start a collector
|
|
121
|
+
// For Node.js, we require an explicit endpoint to be configured - we don't assume localhost
|
|
122
|
+
// The OTLP endpoint should be provided via BEAM_OTEL_EXPORTER_OTLP_ENDPOINT environment variable
|
|
123
|
+
let endpointUrl = otlpEndpoint;
|
|
124
|
+
|
|
125
|
+
// If no explicit endpoint is provided, we cannot use OTLP
|
|
126
|
+
// C# microservices can discover/start collectors, but Node.js requires explicit configuration
|
|
127
|
+
if (!endpointUrl) {
|
|
128
|
+
if (standardOtelEnabled) {
|
|
129
|
+
// Log that OTLP is expected but endpoint not configured
|
|
130
|
+
console.error('[OTLP] Standard OTLP is enabled but no endpoint configured. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ensure endpoint has /v1/logs suffix for HTTP exporter (like C# does)
|
|
136
|
+
const finalEndpointUrl = endpointUrl.includes('/v1/logs')
|
|
137
|
+
? endpointUrl
|
|
138
|
+
: `${endpointUrl.replace(/\/$/, '')}/v1/logs`;
|
|
139
|
+
|
|
140
|
+
// Create OTLP HTTP exporter (C# uses HttpProtobuf by default)
|
|
141
|
+
const exporter = new OTLPLogExporter({
|
|
142
|
+
url: finalEndpointUrl,
|
|
143
|
+
// Headers if provided (similar to C# OtelExporterOtlpHeaders)
|
|
144
|
+
headers: process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS
|
|
145
|
+
? JSON.parse(process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS)
|
|
146
|
+
: undefined,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Create resource with attributes (merge with default resource)
|
|
150
|
+
const baseResource = defaultResource();
|
|
151
|
+
const customResource = resourceFromAttributes(resourceAttributes);
|
|
152
|
+
const resource = baseResource.merge(customResource);
|
|
153
|
+
|
|
154
|
+
// Create log record processor
|
|
155
|
+
const processor = new SimpleLogRecordProcessor(exporter);
|
|
156
|
+
|
|
157
|
+
// Create logger provider with resource and processor
|
|
158
|
+
const loggerProvider = new LoggerProvider({
|
|
159
|
+
resource: resource,
|
|
160
|
+
processors: [processor],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Set as global logger provider
|
|
164
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
165
|
+
|
|
166
|
+
// Log successful initialization (to stdout for debugging)
|
|
167
|
+
// This helps diagnose if OTLP is working in deployed environments
|
|
168
|
+
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
169
|
+
|
|
170
|
+
return loggerProvider;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// If OTLP initialization fails, log error but continue without OTLP
|
|
173
|
+
// Don't throw - we still want stdout logging to work
|
|
174
|
+
// Log to stderr so it's visible in container logs
|
|
175
|
+
console.error('[OTLP] Failed to initialize OTLP logging:', error instanceof Error ? error.message : String(error));
|
|
176
|
+
if (error instanceof Error && error.stack) {
|
|
177
|
+
console.error('[OTLP] Stack trace:', error.stack);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
63
183
|
/**
|
|
64
184
|
* Creates a transform stream that converts Pino JSON logs to Beamable's expected format.
|
|
65
185
|
* Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.
|
|
66
186
|
* Also includes OpenTelemetry-compatible fields for ClickHouse compatibility.
|
|
67
187
|
* Pino writes JSON strings (one per line) to the stream.
|
|
188
|
+
*
|
|
189
|
+
* Also sends logs via OTLP if OTLP logger provider is configured.
|
|
68
190
|
*/
|
|
69
|
-
function createBeamableLogFormatter(
|
|
191
|
+
function createBeamableLogFormatter(
|
|
192
|
+
serviceName?: string,
|
|
193
|
+
qualifiedServiceName?: string,
|
|
194
|
+
otlpLoggerProvider?: LoggerProvider | null
|
|
195
|
+
): Transform {
|
|
70
196
|
return new Transform({
|
|
71
197
|
objectMode: false, // Pino writes strings/Buffers, not objects
|
|
72
198
|
transform(chunk: Buffer, _encoding, callback) {
|
|
@@ -235,6 +361,46 @@ function createBeamableLogFormatter(serviceName?: string, qualifiedServiceName?:
|
|
|
235
361
|
// Note: These extra fields won't break CloudWatch - it will just store them in @message
|
|
236
362
|
Object.assign(beamableLog, otelFields);
|
|
237
363
|
|
|
364
|
+
// Send log via OTLP if configured (similar to C# MicroserviceOtelLogRecordExporter)
|
|
365
|
+
if (otlpLoggerProvider) {
|
|
366
|
+
try {
|
|
367
|
+
const otlpLogger = otlpLoggerProvider.getLogger(
|
|
368
|
+
serviceName || 'beamable-node-runtime',
|
|
369
|
+
undefined, // version
|
|
370
|
+
{
|
|
371
|
+
schemaUrl: undefined, // optional schema URL
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Map Beamable level to OpenTelemetry SeverityNumber
|
|
376
|
+
const severityNumberMap: Record<string, number> = {
|
|
377
|
+
'Debug': 5, // SEVERITY_NUMBER_DEBUG
|
|
378
|
+
'Info': 9, // SEVERITY_NUMBER_INFO
|
|
379
|
+
'Warning': 13, // SEVERITY_NUMBER_WARN
|
|
380
|
+
'Error': 17, // SEVERITY_NUMBER_ERROR
|
|
381
|
+
'Fatal': 21, // SEVERITY_NUMBER_FATAL
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Create log record for OTLP
|
|
385
|
+
otlpLogger.emit({
|
|
386
|
+
severityNumber: severityNumberMap[level] || 9,
|
|
387
|
+
severityText: severityText,
|
|
388
|
+
body: messageParts.length > 0 ? messageParts.join(' ') : 'No message',
|
|
389
|
+
attributes: {
|
|
390
|
+
...logAttributes,
|
|
391
|
+
// Include additional context
|
|
392
|
+
...(pinoLog.component ? { component: String(pinoLog.component) } : {}),
|
|
393
|
+
},
|
|
394
|
+
timestamp: new Date(timestamp).getTime() * 1_000_000, // nanoseconds
|
|
395
|
+
observedTimestamp: Date.now() * 1_000_000, // nanoseconds
|
|
396
|
+
});
|
|
397
|
+
} catch (otlpError) {
|
|
398
|
+
// If OTLP send fails, continue with stdout logging
|
|
399
|
+
// Don't block the log output
|
|
400
|
+
// Could add retry logic here similar to C# if needed
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
238
404
|
// Output as a single-line JSON string (required for CloudWatch)
|
|
239
405
|
// CloudWatch Logs Insights will store this entire JSON string in the @message field
|
|
240
406
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
@@ -255,6 +421,14 @@ function createBeamableLogFormatter(serviceName?: string, qualifiedServiceName?:
|
|
|
255
421
|
export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptions = {}): Logger {
|
|
256
422
|
const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
|
|
257
423
|
const usePrettyLogs = shouldUsePrettyLogs();
|
|
424
|
+
|
|
425
|
+
// Initialize OTLP logging if configured (similar to C# microservices)
|
|
426
|
+
// This sends logs via OpenTelemetry Protocol in addition to stdout JSON
|
|
427
|
+
const otlpLoggerProvider = initializeOtlpLogging(
|
|
428
|
+
options.serviceName,
|
|
429
|
+
options.qualifiedServiceName,
|
|
430
|
+
env
|
|
431
|
+
);
|
|
258
432
|
|
|
259
433
|
const pinoOptions: LoggerOptions = {
|
|
260
434
|
name: options.name ?? 'beamable-node-runtime',
|
|
@@ -282,7 +456,12 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
282
456
|
if (!usePrettyLogs) {
|
|
283
457
|
// Deployed/remote: Use Beamable JSON format for log collection
|
|
284
458
|
// Include OpenTelemetry fields for ClickHouse compatibility
|
|
285
|
-
|
|
459
|
+
// Also send logs via OTLP if configured
|
|
460
|
+
const beamableFormatter = createBeamableLogFormatter(
|
|
461
|
+
options.serviceName,
|
|
462
|
+
options.qualifiedServiceName,
|
|
463
|
+
otlpLoggerProvider
|
|
464
|
+
);
|
|
286
465
|
beamableFormatter.pipe(process.stdout);
|
|
287
466
|
return pino(pinoOptions, beamableFormatter);
|
|
288
467
|
} else {
|
|
@@ -314,7 +493,11 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
314
493
|
// For file logging: Use Beamable format if not local, default Pino format if local
|
|
315
494
|
const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;
|
|
316
495
|
if (!usePrettyLogs) {
|
|
317
|
-
const beamableFormatter = createBeamableLogFormatter(
|
|
496
|
+
const beamableFormatter = createBeamableLogFormatter(
|
|
497
|
+
options.serviceName,
|
|
498
|
+
options.qualifiedServiceName,
|
|
499
|
+
otlpLoggerProvider
|
|
500
|
+
);
|
|
318
501
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
319
502
|
beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);
|
|
320
503
|
return pino(pinoOptions, beamableFormatter);
|