@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/src/logger.ts
CHANGED
|
@@ -32,6 +32,8 @@ function shouldUsePrettyLogs(): boolean {
|
|
|
32
32
|
interface LoggerFactoryOptions {
|
|
33
33
|
name?: string;
|
|
34
34
|
destinationPath?: string;
|
|
35
|
+
serviceName?: string; // Service name for log filtering (e.g., "ExampleNodeService")
|
|
36
|
+
qualifiedServiceName?: string; // Full qualified service name (e.g., "micro_ExampleNodeService")
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -61,9 +63,10 @@ function mapPinoLevelToBeamableLevel(level: number): string {
|
|
|
61
63
|
/**
|
|
62
64
|
* Creates a transform stream that converts Pino JSON logs to Beamable's expected format.
|
|
63
65
|
* Beamable expects logs with __t (timestamp), __l (level), and __m (message) fields.
|
|
66
|
+
* Also includes OpenTelemetry-compatible fields for ClickHouse compatibility.
|
|
64
67
|
* Pino writes JSON strings (one per line) to the stream.
|
|
65
68
|
*/
|
|
66
|
-
function createBeamableLogFormatter(): Transform {
|
|
69
|
+
function createBeamableLogFormatter(serviceName?: string, qualifiedServiceName?: string): Transform {
|
|
67
70
|
return new Transform({
|
|
68
71
|
objectMode: false, // Pino writes strings, not objects
|
|
69
72
|
transform(chunk: Buffer, _encoding, callback) {
|
|
@@ -107,7 +110,7 @@ function createBeamableLogFormatter(): Transform {
|
|
|
107
110
|
messageParts.push(`${errMsg}${errStack}`);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
// Build the Beamable log format
|
|
113
|
+
// Build the Beamable log format (for CloudWatch Logs Insights)
|
|
111
114
|
const beamableLog: Record<string, unknown> = {
|
|
112
115
|
__t: timestamp,
|
|
113
116
|
__l: level,
|
|
@@ -123,6 +126,14 @@ function createBeamableLogFormatter(): Transform {
|
|
|
123
126
|
if (pinoLog.service) contextFields.service = pinoLog.service;
|
|
124
127
|
if (pinoLog.component) contextFields.component = pinoLog.component;
|
|
125
128
|
|
|
129
|
+
// Include service name in context for CloudWatch filtering
|
|
130
|
+
if (serviceName) {
|
|
131
|
+
contextFields.serviceName = serviceName;
|
|
132
|
+
}
|
|
133
|
+
if (qualifiedServiceName) {
|
|
134
|
+
contextFields.qualifiedServiceName = qualifiedServiceName;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
// Include any other fields that aren't standard Pino fields
|
|
127
138
|
const standardPinoFields = ['level', 'time', 'pid', 'hostname', 'name', 'msg', 'err', 'v', 'cid', 'pid', 'routingKey', 'sdkVersionExecution', 'service', 'component'];
|
|
128
139
|
for (const [key, value] of Object.entries(pinoLog)) {
|
|
@@ -136,6 +147,70 @@ function createBeamableLogFormatter(): Transform {
|
|
|
136
147
|
beamableLog.__c = contextFields;
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
// Add OpenTelemetry-compatible fields for ClickHouse compatibility
|
|
151
|
+
// These fields allow an OpenTelemetry collector to parse and forward logs to ClickHouse
|
|
152
|
+
// The Portal's realm-level logs page queries ClickHouse's otel_logs table
|
|
153
|
+
const otelFields: Record<string, unknown> = {};
|
|
154
|
+
|
|
155
|
+
// ResourceAttributes - service identification (for ClickHouse filtering)
|
|
156
|
+
const resourceAttributes: Record<string, unknown> = {};
|
|
157
|
+
if (qualifiedServiceName) {
|
|
158
|
+
// ClickHouse expects service.namespace to match the service name format
|
|
159
|
+
resourceAttributes['service.namespace'] = qualifiedServiceName;
|
|
160
|
+
}
|
|
161
|
+
if (serviceName) {
|
|
162
|
+
resourceAttributes['service.name'] = serviceName;
|
|
163
|
+
}
|
|
164
|
+
if (pinoLog.cid) {
|
|
165
|
+
resourceAttributes['beam.cid'] = String(pinoLog.cid);
|
|
166
|
+
}
|
|
167
|
+
if (pinoLog.pid) {
|
|
168
|
+
resourceAttributes['beam.pid'] = String(pinoLog.pid);
|
|
169
|
+
}
|
|
170
|
+
if (pinoLog.routingKey) {
|
|
171
|
+
resourceAttributes['beam.routing_key'] = String(pinoLog.routingKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// LogAttributes - log-specific attributes
|
|
175
|
+
const logAttributes: Record<string, unknown> = {};
|
|
176
|
+
if (pinoLog.component) {
|
|
177
|
+
logAttributes['component'] = String(pinoLog.component);
|
|
178
|
+
}
|
|
179
|
+
if (pinoLog.err) {
|
|
180
|
+
const err = pinoLog.err;
|
|
181
|
+
if (err.message) logAttributes['exception.message'] = String(err.message);
|
|
182
|
+
if (err.stack) logAttributes['exception.stacktrace'] = String(err.stack);
|
|
183
|
+
if (err.type) logAttributes['exception.type'] = String(err.type);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Map Beamable log level to OpenTelemetry SeverityText
|
|
187
|
+
// Beamable: Debug, Info, Warning, Error, Fatal
|
|
188
|
+
// OpenTelemetry: Trace, Debug, Info, Warn, Error, Fatal, Unspecified
|
|
189
|
+
const severityTextMap: Record<string, string> = {
|
|
190
|
+
'Debug': 'Debug',
|
|
191
|
+
'Info': 'Information',
|
|
192
|
+
'Warning': 'Warning',
|
|
193
|
+
'Error': 'Error',
|
|
194
|
+
'Fatal': 'Critical',
|
|
195
|
+
};
|
|
196
|
+
const severityText = severityTextMap[level] || 'Information';
|
|
197
|
+
|
|
198
|
+
// Add OpenTelemetry fields to the log
|
|
199
|
+
// These are in addition to the Beamable format, so both systems can parse the logs
|
|
200
|
+
otelFields['Timestamp'] = timestamp; // OpenTelemetry timestamp format
|
|
201
|
+
otelFields['SeverityText'] = severityText;
|
|
202
|
+
otelFields['Body'] = messageParts.length > 0 ? messageParts.join(' ') : 'No message';
|
|
203
|
+
if (Object.keys(resourceAttributes).length > 0) {
|
|
204
|
+
otelFields['ResourceAttributes'] = resourceAttributes;
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(logAttributes).length > 0) {
|
|
207
|
+
otelFields['LogAttributes'] = logAttributes;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Merge OpenTelemetry fields into the log (for ClickHouse compatibility)
|
|
211
|
+
// An OpenTelemetry collector can parse these fields and forward to ClickHouse
|
|
212
|
+
Object.assign(beamableLog, otelFields);
|
|
213
|
+
|
|
139
214
|
// Output as a single-line JSON string (required for CloudWatch)
|
|
140
215
|
const output = JSON.stringify(beamableLog) + '\n';
|
|
141
216
|
callback(null, Buffer.from(output, 'utf8'));
|
|
@@ -164,6 +239,9 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
164
239
|
pid: env.pid,
|
|
165
240
|
routingKey: env.routingKey ?? null,
|
|
166
241
|
sdkVersionExecution: env.sdkVersionExecution,
|
|
242
|
+
// Include service name in base fields for filtering
|
|
243
|
+
serviceName: options.serviceName,
|
|
244
|
+
qualifiedServiceName: options.qualifiedServiceName,
|
|
167
245
|
},
|
|
168
246
|
redact: {
|
|
169
247
|
paths: ['secret', 'refreshToken'],
|
|
@@ -178,7 +256,8 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
178
256
|
if (!configuredDestination || configuredDestination === '-' || configuredDestination === 'stdout' || configuredDestination === 'console') {
|
|
179
257
|
if (!usePrettyLogs) {
|
|
180
258
|
// Deployed/remote: Use Beamable JSON format for log collection
|
|
181
|
-
|
|
259
|
+
// Include OpenTelemetry fields for ClickHouse compatibility
|
|
260
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
182
261
|
beamableFormatter.pipe(process.stdout);
|
|
183
262
|
return pino(pinoOptions, beamableFormatter);
|
|
184
263
|
} else {
|
|
@@ -210,7 +289,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
210
289
|
// For file logging: Use Beamable format if not local, default Pino format if local
|
|
211
290
|
const resolvedDestination = configuredDestination === 'temp' ? ensureWritableTempDirectory() : configuredDestination;
|
|
212
291
|
if (!usePrettyLogs) {
|
|
213
|
-
const beamableFormatter = createBeamableLogFormatter();
|
|
292
|
+
const beamableFormatter = createBeamableLogFormatter(options.serviceName, options.qualifiedServiceName);
|
|
214
293
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
215
294
|
beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);
|
|
216
295
|
return pino(pinoOptions, beamableFormatter);
|
package/src/runtime.ts
CHANGED
|
@@ -58,13 +58,24 @@ export class MicroserviceRuntime {
|
|
|
58
58
|
|
|
59
59
|
constructor(env?: EnvironmentConfig) {
|
|
60
60
|
this.env = env ?? loadEnvironmentConfig();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
|
|
62
|
+
// Get registered services first to extract service name for logger
|
|
64
63
|
const registered = listRegisteredServices();
|
|
65
64
|
if (registered.length === 0) {
|
|
66
65
|
throw new Error('No microservices registered. Use the @Microservice decorator to register at least one class.');
|
|
67
66
|
}
|
|
67
|
+
|
|
68
|
+
// Use the first service's name for the main logger (for CloudWatch filtering and ClickHouse compatibility)
|
|
69
|
+
const primaryService = registered[0];
|
|
70
|
+
const qualifiedServiceName = `micro_${primaryService.qualifiedName}`;
|
|
71
|
+
|
|
72
|
+
this.logger = createLogger(this.env, {
|
|
73
|
+
name: 'beamable-node-microservice',
|
|
74
|
+
serviceName: primaryService.name,
|
|
75
|
+
qualifiedServiceName: qualifiedServiceName,
|
|
76
|
+
});
|
|
77
|
+
this.serviceManager = new BeamableServiceManager(this.env, this.logger);
|
|
78
|
+
|
|
68
79
|
this.services = registered.map((definition) => {
|
|
69
80
|
const instance = new definition.ctor() as Record<string, unknown>;
|
|
70
81
|
const configureHandlers = getConfigureServicesHandlers(definition.ctor);
|
|
@@ -92,8 +103,9 @@ export class MicroserviceRuntime {
|
|
|
92
103
|
this.authManager = new AuthManager(this.env, this.requester);
|
|
93
104
|
this.requester.on('event', (envelope) => this.handleEvent(envelope));
|
|
94
105
|
|
|
95
|
-
// Discovery broadcaster only runs in local development
|
|
96
|
-
|
|
106
|
+
// Discovery broadcaster only runs in local development (not in containers)
|
|
107
|
+
// This allows the portal to detect that the service is running locally
|
|
108
|
+
if (!isRunningInContainer() && this.services.length > 0) {
|
|
97
109
|
this.discovery = new DiscoveryBroadcaster({
|
|
98
110
|
env: this.env,
|
|
99
111
|
serviceName: this.services[0].definition.name,
|
|
@@ -803,27 +815,30 @@ function debugLog(...args: unknown[]): void {
|
|
|
803
815
|
}
|
|
804
816
|
}
|
|
805
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Determines if we're running in a deployed container.
|
|
820
|
+
* Used for service registration logic (routing key handling, discovery broadcaster, etc.)
|
|
821
|
+
*
|
|
822
|
+
* Note: For log formatting, use IS_LOCAL env var instead.
|
|
823
|
+
*/
|
|
806
824
|
function isRunningInContainer(): boolean {
|
|
807
|
-
// Check for Docker container
|
|
808
|
-
// Docker creates /.dockerenv file in containers
|
|
825
|
+
// Check for Docker container
|
|
809
826
|
try {
|
|
810
827
|
const fs = require('fs');
|
|
811
828
|
if (fs.existsSync('/.dockerenv')) {
|
|
812
|
-
return true;
|
|
829
|
+
return true;
|
|
813
830
|
}
|
|
814
831
|
} catch {
|
|
815
|
-
// fs might not be available
|
|
832
|
+
// fs might not be available
|
|
816
833
|
}
|
|
817
834
|
|
|
818
|
-
// Check for Docker container hostname pattern
|
|
819
|
-
// Docker containers often have hostnames that are just hex strings (12 chars)
|
|
835
|
+
// Check for Docker container hostname pattern (12 hex chars)
|
|
820
836
|
const hostname = process.env.HOSTNAME || '';
|
|
821
837
|
if (hostname && /^[a-f0-9]{12}$/i.test(hostname)) {
|
|
822
|
-
// This looks like a Docker container ID as hostname
|
|
823
838
|
return true;
|
|
824
839
|
}
|
|
825
840
|
|
|
826
|
-
// Explicit container indicators
|
|
841
|
+
// Explicit container indicators
|
|
827
842
|
if (
|
|
828
843
|
process.env.DOTNET_RUNNING_IN_CONTAINER === 'true' ||
|
|
829
844
|
process.env.CONTAINER === 'beamable' ||
|
|
@@ -833,18 +848,7 @@ function isRunningInContainer(): boolean {
|
|
|
833
848
|
return true;
|
|
834
849
|
}
|
|
835
850
|
|
|
836
|
-
//
|
|
837
|
-
// This indicates a deployed container (Beamable sets these but not routing key)
|
|
838
|
-
const hasBeamableEnvVars = !!(process.env.CID && process.env.PID && process.env.HOST && process.env.SECRET);
|
|
839
|
-
const hasExplicitRoutingKey = !!(process.env.NAME_PREFIX || process.env.ROUTING_KEY);
|
|
840
|
-
|
|
841
|
-
if (hasBeamableEnvVars && !hasExplicitRoutingKey) {
|
|
842
|
-
// We have Beamable env vars but no explicit routing key from env
|
|
843
|
-
// This means we're in a deployed container (routing key would be auto-generated, but we're in container)
|
|
844
|
-
return true;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Default: assume local development (npm run dev)
|
|
851
|
+
// Default: assume local development
|
|
848
852
|
return false;
|
|
849
853
|
}
|
|
850
854
|
|