@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/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
- const beamableFormatter = createBeamableLogFormatter();
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
- this.logger = createLogger(this.env, { name: 'beamable-node-microservice' });
62
- this.serviceManager = new BeamableServiceManager(this.env, this.logger);
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
- if ((process.env.IS_LOCAL === '1' || process.env.IS_LOCAL === 'true') && this.services.length > 0) {
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 first (most reliable indicator)
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; // Definitely in Docker
829
+ return true;
813
830
  }
814
831
  } catch {
815
- // fs might not be available, continue with other checks
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 (highest priority after Docker checks)
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
- // If we have Beamable env vars AND no routing key from process.env (not auto-generated)
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