@omen.foundation/node-microservice-runtime 0.1.25 → 0.1.27
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/collector-manager.cjs +78 -18
- package/dist/collector-manager.d.ts +27 -3
- package/dist/collector-manager.d.ts.map +1 -1
- package/dist/collector-manager.js +89 -18
- package/dist/collector-manager.js.map +1 -1
- package/dist/index.cjs +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +34 -5
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +46 -7
- package/dist/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/collector-manager.ts +125 -21
- package/src/index.ts +5 -0
- package/src/logger.ts +44 -7
package/src/collector-manager.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { pipeline } from 'stream/promises';
|
|
|
5
5
|
import { createGunzip } from 'zlib';
|
|
6
6
|
import type { Logger } from 'pino';
|
|
7
7
|
import dgram from 'dgram';
|
|
8
|
+
import type { EnvironmentConfig } from './types.js';
|
|
9
|
+
import { hostToHttpUrl } from './utils/urls.js';
|
|
8
10
|
|
|
9
11
|
// Protocol is httpprotobuf
|
|
10
12
|
|
|
@@ -15,7 +17,7 @@ interface CollectorDiscoveryEntry {
|
|
|
15
17
|
otlpEndpoint: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
interface CollectorStatus {
|
|
20
|
+
export interface CollectorStatus {
|
|
19
21
|
isRunning: boolean;
|
|
20
22
|
isReady: boolean;
|
|
21
23
|
pid: number;
|
|
@@ -29,6 +31,62 @@ const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '86
|
|
|
29
31
|
const DISCOVERY_DELAY = 100; // milliseconds
|
|
30
32
|
const DISCOVERY_ATTEMPTS = 10;
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* ClickHouse credentials response from Beamable API
|
|
36
|
+
*/
|
|
37
|
+
interface ClickHouseCredentials {
|
|
38
|
+
endpoint: string;
|
|
39
|
+
expiresAt?: string;
|
|
40
|
+
password: string;
|
|
41
|
+
username: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetches ClickHouse credentials from Beamable API
|
|
46
|
+
* GET /api/beamo/otel/auth/writer/config
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchClickHouseCredentials(
|
|
49
|
+
env: EnvironmentConfig,
|
|
50
|
+
logger: Logger
|
|
51
|
+
): Promise<ClickHouseCredentials> {
|
|
52
|
+
const apiUrl = hostToHttpUrl(env.host);
|
|
53
|
+
const configUrl = new URL('/api/beamo/otel/auth/writer/config', apiUrl).toString();
|
|
54
|
+
|
|
55
|
+
logger.info('[Collector] Fetching ClickHouse credentials from Beamable API...');
|
|
56
|
+
|
|
57
|
+
// Build headers with authentication
|
|
58
|
+
const headers: Record<string, string> = {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
Accept: 'application/json',
|
|
61
|
+
'beam-scope': `${env.cid}.${env.pid}`,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// If we have a secret, we might need to sign the request
|
|
65
|
+
// For now, try without signature (the API might use the beam-scope header for auth)
|
|
66
|
+
const response = await fetch(configUrl, {
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
73
|
+
throw new Error(`Failed to fetch ClickHouse credentials: ${response.status} ${response.statusText} - ${errorText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const credentials = await response.json() as ClickHouseCredentials;
|
|
77
|
+
|
|
78
|
+
if (!credentials.endpoint || !credentials.username || !credentials.password) {
|
|
79
|
+
throw new Error('Invalid ClickHouse credentials response: missing required fields');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logger.info('[Collector] Successfully fetched ClickHouse credentials');
|
|
83
|
+
if (credentials.expiresAt) {
|
|
84
|
+
logger.debug(`[Collector] Credentials expire at: ${credentials.expiresAt}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return credentials;
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
/**
|
|
33
91
|
* Gets the collector storage directory (similar to C# LocalApplicationData/beam/collectors/version)
|
|
34
92
|
*/
|
|
@@ -170,6 +228,36 @@ function discoverCollectorViaUDP(timeoutMs: number = 5000): Promise<CollectorDis
|
|
|
170
228
|
});
|
|
171
229
|
}
|
|
172
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Gets the current status of ClickHouse credentials (from env or API)
|
|
233
|
+
*/
|
|
234
|
+
export function getClickHouseCredentialsStatus(): {
|
|
235
|
+
hasEndpoint: boolean;
|
|
236
|
+
hasUsername: boolean;
|
|
237
|
+
hasPassword: boolean;
|
|
238
|
+
source: 'environment' | 'api' | 'missing';
|
|
239
|
+
} {
|
|
240
|
+
const hasEndpoint = !!process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
241
|
+
const hasUsername = !!process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
242
|
+
const hasPassword = !!process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
243
|
+
|
|
244
|
+
if (hasEndpoint && hasUsername && hasPassword) {
|
|
245
|
+
return {
|
|
246
|
+
hasEndpoint: true,
|
|
247
|
+
hasUsername: true,
|
|
248
|
+
hasPassword: true,
|
|
249
|
+
source: 'environment',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
hasEndpoint,
|
|
255
|
+
hasUsername,
|
|
256
|
+
hasPassword,
|
|
257
|
+
source: 'missing',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
173
261
|
/**
|
|
174
262
|
* Checks if collector is already running via UDP discovery
|
|
175
263
|
*/
|
|
@@ -201,7 +289,8 @@ export async function isCollectorRunning(): Promise<CollectorStatus> {
|
|
|
201
289
|
*/
|
|
202
290
|
export async function startCollector(
|
|
203
291
|
logger: Logger,
|
|
204
|
-
otlpEndpoint?: string
|
|
292
|
+
otlpEndpoint?: string,
|
|
293
|
+
env?: EnvironmentConfig
|
|
205
294
|
): Promise<{ process: ChildProcess; endpoint: string }> {
|
|
206
295
|
logger.info('[Collector] Resolving collector binary and config...');
|
|
207
296
|
const collectorInfo = await resolveCollector(true, logger);
|
|
@@ -219,20 +308,34 @@ export async function startCollector(
|
|
|
219
308
|
logger.info(`[Collector] Using binary: ${collectorInfo.binaryPath}`);
|
|
220
309
|
logger.info(`[Collector] Using config: ${collectorInfo.configPath}`);
|
|
221
310
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
BEAM_CLICKHOUSE_PASSWORD: process.env.BEAM_CLICKHOUSE_PASSWORD,
|
|
228
|
-
};
|
|
311
|
+
// Fetch ClickHouse credentials from Beamable API if not in environment variables
|
|
312
|
+
// Per Gabriel: "these OTEL related ones you need to do it yourself at the beginning of the microservice startup"
|
|
313
|
+
let clickhouseEndpoint = process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
314
|
+
let clickhouseUsername = process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
315
|
+
let clickhousePassword = process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
229
316
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
317
|
+
if ((!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) && env) {
|
|
318
|
+
try {
|
|
319
|
+
const credentials = await fetchClickHouseCredentials(env, logger);
|
|
320
|
+
clickhouseEndpoint = credentials.endpoint;
|
|
321
|
+
clickhouseUsername = credentials.username;
|
|
322
|
+
clickhousePassword = credentials.password;
|
|
323
|
+
|
|
324
|
+
// Set them in process.env so the collector can access them
|
|
325
|
+
process.env.BEAM_CLICKHOUSE_ENDPOINT = clickhouseEndpoint;
|
|
326
|
+
process.env.BEAM_CLICKHOUSE_USERNAME = clickhouseUsername;
|
|
327
|
+
process.env.BEAM_CLICKHOUSE_PASSWORD = clickhousePassword;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const errorMsg = `[Collector] Failed to fetch ClickHouse credentials from API: ${error instanceof Error ? error.message : String(error)}`;
|
|
330
|
+
logger.error(errorMsg);
|
|
331
|
+
throw new Error(errorMsg);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
233
334
|
|
|
234
|
-
|
|
235
|
-
|
|
335
|
+
// Validate required environment variables (matching C# behavior)
|
|
336
|
+
// These must be set before starting the collector, otherwise it will fail
|
|
337
|
+
if (!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) {
|
|
338
|
+
const errorMsg = `[Collector] Required ClickHouse credentials are missing. Set BEAM_CLICKHOUSE_ENDPOINT, BEAM_CLICKHOUSE_USERNAME, and BEAM_CLICKHOUSE_PASSWORD, or ensure the API endpoint is accessible.`;
|
|
236
339
|
logger.error(errorMsg);
|
|
237
340
|
throw new Error(errorMsg);
|
|
238
341
|
}
|
|
@@ -250,12 +353,12 @@ export async function startCollector(
|
|
|
250
353
|
// Set environment variables for collector
|
|
251
354
|
// Note: BEAM_CLICKHOUSE_ENDPOINT is for collector → ClickHouse communication
|
|
252
355
|
// This is different from the OTLP endpoint (microservice → collector)
|
|
253
|
-
const
|
|
356
|
+
const collectorEnv = {
|
|
254
357
|
...process.env,
|
|
255
358
|
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
256
|
-
BEAM_CLICKHOUSE_ENDPOINT:
|
|
257
|
-
BEAM_CLICKHOUSE_USERNAME:
|
|
258
|
-
BEAM_CLICKHOUSE_PASSWORD:
|
|
359
|
+
BEAM_CLICKHOUSE_ENDPOINT: clickhouseEndpoint!,
|
|
360
|
+
BEAM_CLICKHOUSE_USERNAME: clickhouseUsername!,
|
|
361
|
+
BEAM_CLICKHOUSE_PASSWORD: clickhousePassword!,
|
|
259
362
|
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
260
363
|
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: process.env.BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT || '5s',
|
|
261
364
|
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: process.env.BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE || '5000',
|
|
@@ -269,7 +372,7 @@ export async function startCollector(
|
|
|
269
372
|
|
|
270
373
|
// Start collector process
|
|
271
374
|
const collectorProcess = spawn(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
272
|
-
env,
|
|
375
|
+
env: collectorEnv,
|
|
273
376
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
274
377
|
detached: false,
|
|
275
378
|
});
|
|
@@ -303,7 +406,8 @@ export async function startCollector(
|
|
|
303
406
|
*/
|
|
304
407
|
export async function discoverOrStartCollector(
|
|
305
408
|
logger: Logger,
|
|
306
|
-
standardOtelEnabled: boolean
|
|
409
|
+
standardOtelEnabled: boolean,
|
|
410
|
+
env?: EnvironmentConfig
|
|
307
411
|
): Promise<string | null> {
|
|
308
412
|
if (!standardOtelEnabled) {
|
|
309
413
|
return null;
|
|
@@ -319,7 +423,7 @@ export async function discoverOrStartCollector(
|
|
|
319
423
|
// Collector not running - start it
|
|
320
424
|
try {
|
|
321
425
|
logger.info('[Collector] Starting OpenTelemetry collector...');
|
|
322
|
-
const { endpoint } = await startCollector(logger);
|
|
426
|
+
const { endpoint } = await startCollector(logger, undefined, env);
|
|
323
427
|
|
|
324
428
|
// Wait a bit for collector to start and become ready
|
|
325
429
|
// Try to discover it
|
package/src/index.ts
CHANGED
package/src/logger.ts
CHANGED
|
@@ -135,7 +135,7 @@ async function initializeOtlpLogging(
|
|
|
135
135
|
if (!endpointUrl && standardOtelEnabled && logger) {
|
|
136
136
|
// Try to discover existing collector or start a new one (like C# does)
|
|
137
137
|
try {
|
|
138
|
-
const discoveredEndpoint = await discoverOrStartCollector(logger, standardOtelEnabled);
|
|
138
|
+
const discoveredEndpoint = await discoverOrStartCollector(logger, standardOtelEnabled, env);
|
|
139
139
|
if (discoveredEndpoint) {
|
|
140
140
|
endpointUrl = discoveredEndpoint;
|
|
141
141
|
} else {
|
|
@@ -252,6 +252,31 @@ async function initializeOtlpLogging(
|
|
|
252
252
|
// This helps diagnose if OTLP is working in deployed environments
|
|
253
253
|
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
254
254
|
|
|
255
|
+
// CRITICAL: Verify collector is actually ready before returning
|
|
256
|
+
// If we started a collector, wait for it to be ready (up to 5 seconds)
|
|
257
|
+
if (standardOtelEnabled && logger) {
|
|
258
|
+
try {
|
|
259
|
+
const { isCollectorRunning } = await import('./collector-manager.js');
|
|
260
|
+
// Wait up to 5 seconds for collector to be ready
|
|
261
|
+
for (let i = 0; i < 50; i++) {
|
|
262
|
+
const status = await isCollectorRunning();
|
|
263
|
+
if (status.isRunning && status.isReady) {
|
|
264
|
+
if (logger) {
|
|
265
|
+
logger.info(`[OTLP] Collector confirmed ready at ${status.otlpEndpoint || finalEndpointUrl}`);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
// Wait 100ms between checks
|
|
270
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// If we can't verify, log but continue
|
|
274
|
+
if (logger) {
|
|
275
|
+
logger.warn(`[OTLP] Could not verify collector readiness: ${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
255
280
|
return Promise.resolve(loggerProvider);
|
|
256
281
|
} catch (error) {
|
|
257
282
|
// If OTLP initialization fails, log error but continue without OTLP
|
|
@@ -517,7 +542,7 @@ function initializeOtlpSync(
|
|
|
517
542
|
serviceName?: string,
|
|
518
543
|
qualifiedServiceName?: string,
|
|
519
544
|
env?: EnvironmentConfig,
|
|
520
|
-
timeoutMs: number =
|
|
545
|
+
timeoutMs: number = 60000 // 60 seconds to allow collector download, startup, and readiness check
|
|
521
546
|
): LoggerProvider | null {
|
|
522
547
|
// Match C# logic: (this.InDocker() || UseLocalOtel) && !BEAM_DISABLE_STANDARD_OTEL
|
|
523
548
|
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
@@ -575,14 +600,20 @@ function initializeOtlpSync(
|
|
|
575
600
|
// Wait synchronously for the promise to resolve using deasync
|
|
576
601
|
// deasync.loopWhile() allows event loop to process while we wait
|
|
577
602
|
// This enables async operations (like collector download) to run and complete
|
|
603
|
+
// CRITICAL: This must complete before the logger is created to capture all startup logs
|
|
578
604
|
try {
|
|
579
605
|
// Use deasync to wait for completion, with timeout check
|
|
606
|
+
// loopWhile returns false to stop waiting, true to continue
|
|
580
607
|
deasync.loopWhile(() => {
|
|
581
608
|
// Check if we've exceeded timeout
|
|
582
|
-
|
|
609
|
+
const elapsed = Date.now() - startTime;
|
|
610
|
+
if (elapsed >= timeoutMs) {
|
|
611
|
+
initLogger.warn(`[OTLP] Synchronous wait timeout after ${elapsed}ms`);
|
|
583
612
|
return false; // Stop waiting
|
|
584
613
|
}
|
|
585
|
-
|
|
614
|
+
// Continue waiting if not completed
|
|
615
|
+
// This allows the event loop to process async operations
|
|
616
|
+
return !completed;
|
|
586
617
|
});
|
|
587
618
|
} catch (error) {
|
|
588
619
|
// If deasync fails, log and continue
|
|
@@ -591,9 +622,14 @@ function initializeOtlpSync(
|
|
|
591
622
|
|
|
592
623
|
clearTimeout(timeoutId);
|
|
593
624
|
|
|
594
|
-
//
|
|
595
|
-
if (
|
|
625
|
+
// Verify we got a provider (collector is ready)
|
|
626
|
+
if (completed && provider) {
|
|
627
|
+
initLogger.info('[OTLP] Initialization completed successfully, collector is ready');
|
|
628
|
+
} else if (!completed) {
|
|
596
629
|
initLogger.warn('[OTLP] Initialization did not complete in time, logs may not be sent via OTLP initially');
|
|
630
|
+
initLogger.warn('[OTLP] Service will continue without OTLP telemetry - requests will still be serviced');
|
|
631
|
+
} else {
|
|
632
|
+
initLogger.warn('[OTLP] Initialization completed but no provider returned, OTLP telemetry disabled');
|
|
597
633
|
}
|
|
598
634
|
|
|
599
635
|
return provider;
|
|
@@ -606,11 +642,12 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
606
642
|
// Initialize OTLP synchronously BEFORE creating the logger
|
|
607
643
|
// This ensures all logs from this point forward are captured via OTLP
|
|
608
644
|
// Similar to C# microservices which configure logging early in startup
|
|
645
|
+
// CRITICAL: We wait for collector to be fully initialized and ready before proceeding
|
|
609
646
|
const otlpProvider = initializeOtlpSync(
|
|
610
647
|
options.serviceName,
|
|
611
648
|
options.qualifiedServiceName,
|
|
612
649
|
env,
|
|
613
|
-
|
|
650
|
+
60000 // 60 second timeout to allow collector download, startup, and readiness verification
|
|
614
651
|
);
|
|
615
652
|
|
|
616
653
|
// Shared reference for OTLP logger provider
|