@omen.foundation/node-microservice-runtime 0.1.51 → 0.1.53
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 +82 -13
- package/dist/collector-manager.d.ts +17 -4
- package/dist/collector-manager.d.ts.map +1 -1
- package/dist/collector-manager.js +115 -19
- package/dist/collector-manager.js.map +1 -1
- package/dist/index.cjs +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +35 -69
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +49 -100
- package/dist/logger.js.map +1 -1
- package/dist/runtime.cjs +29 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +39 -22
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/collector-manager.ts +132 -18
- package/src/index.ts +2 -0
- package/src/logger.ts +60 -121
- package/src/runtime.ts +40 -23
package/src/logger.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
|
9
9
|
import { resourceFromAttributes, defaultResource } from '@opentelemetry/resources';
|
|
10
10
|
import { discoverOrStartCollector } from './collector-manager.js';
|
|
11
11
|
import type { Logger as PinoLogger } from 'pino';
|
|
12
|
-
import deasync from 'deasync';
|
|
13
12
|
|
|
14
13
|
// Helper to get require function that works in both CJS and ESM
|
|
15
14
|
declare const require: any;
|
|
@@ -535,113 +534,6 @@ function createBeamableLogFormatter(
|
|
|
535
534
|
});
|
|
536
535
|
}
|
|
537
536
|
|
|
538
|
-
/**
|
|
539
|
-
* Synchronously initializes OTLP logging with a timeout.
|
|
540
|
-
* This ensures OTLP is ready before any logs are emitted.
|
|
541
|
-
* Similar to C# microservices which configure logging early in startup.
|
|
542
|
-
*
|
|
543
|
-
* Uses a blocking wait mechanism to ensure initialization completes before returning.
|
|
544
|
-
*/
|
|
545
|
-
function initializeOtlpSync(
|
|
546
|
-
serviceName?: string,
|
|
547
|
-
qualifiedServiceName?: string,
|
|
548
|
-
env?: EnvironmentConfig,
|
|
549
|
-
timeoutMs: number = 60000 // 60 seconds to allow collector download, startup, and readiness check
|
|
550
|
-
): LoggerProvider | null {
|
|
551
|
-
// Match C# logic: (this.InDocker() || UseLocalOtel) && !BEAM_DISABLE_STANDARD_OTEL
|
|
552
|
-
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
553
|
-
const useLocalOtel = !!process.env.BEAM_LOCAL_OTEL;
|
|
554
|
-
const standardOtelEnabled = (isInDocker || useLocalOtel) && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
555
|
-
const hasExplicitEndpoint = !!process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
556
|
-
|
|
557
|
-
// If OTLP is not needed, return immediately
|
|
558
|
-
if (!standardOtelEnabled && !hasExplicitEndpoint) {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Create a minimal console logger for initialization messages
|
|
563
|
-
const initLogger = pino({
|
|
564
|
-
name: 'beamable-otlp-init',
|
|
565
|
-
level: 'info',
|
|
566
|
-
}, process.stdout);
|
|
567
|
-
|
|
568
|
-
// If collector was already set up via setupCollectorBeforeLogging,
|
|
569
|
-
// discoverOrStartCollector will find it and use a shorter timeout.
|
|
570
|
-
// If not, it will start it (with full timeout).
|
|
571
|
-
|
|
572
|
-
// Use deasync to wait synchronously for the async initialization
|
|
573
|
-
// This allows the event loop to process while we wait, enabling async operations
|
|
574
|
-
// (like collector download) to complete
|
|
575
|
-
let provider: LoggerProvider | null = null;
|
|
576
|
-
let completed = false;
|
|
577
|
-
let initError: Error | null = null;
|
|
578
|
-
|
|
579
|
-
// Start initialization promise (callbacks set flags for deasync.loopWhile to check)
|
|
580
|
-
initializeOtlpLogging(
|
|
581
|
-
serviceName,
|
|
582
|
-
qualifiedServiceName,
|
|
583
|
-
env,
|
|
584
|
-
initLogger
|
|
585
|
-
).then((result) => {
|
|
586
|
-
provider = result;
|
|
587
|
-
completed = true;
|
|
588
|
-
if (result) {
|
|
589
|
-
initLogger.info('[OTLP] OpenTelemetry logging initialized successfully');
|
|
590
|
-
}
|
|
591
|
-
return result;
|
|
592
|
-
}).catch((error) => {
|
|
593
|
-
initError = error instanceof Error ? error : new Error(String(error));
|
|
594
|
-
completed = true;
|
|
595
|
-
initLogger.error(`[OTLP] Failed to initialize: ${initError.message}`);
|
|
596
|
-
return null;
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
// Set timeout to prevent infinite wait
|
|
600
|
-
const startTime = Date.now();
|
|
601
|
-
const timeoutId = setTimeout(() => {
|
|
602
|
-
if (!completed) {
|
|
603
|
-
initLogger.warn(`[OTLP] Initialization timeout after ${timeoutMs}ms, continuing without OTLP`);
|
|
604
|
-
completed = true;
|
|
605
|
-
}
|
|
606
|
-
}, timeoutMs);
|
|
607
|
-
|
|
608
|
-
// Wait synchronously for the promise to resolve using deasync
|
|
609
|
-
// deasync.loopWhile() allows event loop to process while we wait
|
|
610
|
-
// This enables async operations (like collector download) to run and complete
|
|
611
|
-
// CRITICAL: This must complete before the logger is created to capture all startup logs
|
|
612
|
-
try {
|
|
613
|
-
// Use deasync to wait for completion, with timeout check
|
|
614
|
-
// loopWhile returns false to stop waiting, true to continue
|
|
615
|
-
deasync.loopWhile(() => {
|
|
616
|
-
// Check if we've exceeded timeout
|
|
617
|
-
const elapsed = Date.now() - startTime;
|
|
618
|
-
if (elapsed >= timeoutMs) {
|
|
619
|
-
initLogger.warn(`[OTLP] Synchronous wait timeout after ${elapsed}ms`);
|
|
620
|
-
return false; // Stop waiting
|
|
621
|
-
}
|
|
622
|
-
// Continue waiting if not completed
|
|
623
|
-
// This allows the event loop to process async operations
|
|
624
|
-
return !completed;
|
|
625
|
-
});
|
|
626
|
-
} catch (error) {
|
|
627
|
-
// If deasync fails, log and continue
|
|
628
|
-
initLogger.error(`[OTLP] Error during synchronous wait: ${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
clearTimeout(timeoutId);
|
|
632
|
-
|
|
633
|
-
// Verify we got a provider (collector is ready)
|
|
634
|
-
if (completed && provider) {
|
|
635
|
-
initLogger.info('[OTLP] Initialization completed successfully, collector is ready');
|
|
636
|
-
} else if (!completed) {
|
|
637
|
-
initLogger.warn('[OTLP] Initialization did not complete in time, logs may not be sent via OTLP initially');
|
|
638
|
-
initLogger.warn('[OTLP] Service will continue without OTLP telemetry - requests will still be serviced');
|
|
639
|
-
} else {
|
|
640
|
-
initLogger.warn('[OTLP] Initialization completed but no provider returned, OTLP telemetry disabled');
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return provider;
|
|
644
|
-
}
|
|
645
537
|
|
|
646
538
|
export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptions = {}): Logger {
|
|
647
539
|
const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
|
|
@@ -650,7 +542,13 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
650
542
|
// Initialize OTLP synchronously BEFORE creating the logger
|
|
651
543
|
// If otlpEndpoint is provided (collector already set up), create provider directly
|
|
652
544
|
// Otherwise, try to discover/start collector (with timeout)
|
|
653
|
-
|
|
545
|
+
// Check if standard OTLP is enabled (needed for the else branch)
|
|
546
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
547
|
+
const useLocalOtel = !!process.env.BEAM_LOCAL_OTEL;
|
|
548
|
+
const standardOtelEnabled = (isInDocker || useLocalOtel) && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
549
|
+
|
|
550
|
+
// Shared reference for OTLP logger provider (create before async operations)
|
|
551
|
+
const otlpProviderRef: { provider: LoggerProvider | null } = { provider: null };
|
|
654
552
|
|
|
655
553
|
if (options.otlpEndpoint) {
|
|
656
554
|
// Collector is already set up, create OTLP provider directly without discovery/startup
|
|
@@ -699,7 +597,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
699
597
|
return !completed;
|
|
700
598
|
});
|
|
701
599
|
|
|
702
|
-
|
|
600
|
+
otlpProviderRef.provider = provider;
|
|
703
601
|
|
|
704
602
|
// Restore original endpoint if it existed
|
|
705
603
|
if (originalEndpoint !== undefined) {
|
|
@@ -708,18 +606,59 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
708
606
|
delete process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
709
607
|
}
|
|
710
608
|
} else {
|
|
711
|
-
// No endpoint provided -
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
)
|
|
609
|
+
// No endpoint provided - collector is starting asynchronously in background via startCollectorAsync()
|
|
610
|
+
// Don't block here - logger works immediately via stdout, OTLP will connect when collector is ready
|
|
611
|
+
otlpProviderRef.provider = null; // Start without OTLP - will connect when collector is ready
|
|
612
|
+
|
|
613
|
+
// Start async discovery in background (non-blocking)
|
|
614
|
+
// This allows the service to start immediately while collector is downloading/starting
|
|
615
|
+
// CRITICAL: Only DISCOVER the collector, don't try to START it (startCollectorAsync handles startup)
|
|
616
|
+
if (standardOtelEnabled) {
|
|
617
|
+
// Poll for collector to become ready (won't block service startup)
|
|
618
|
+
// startCollectorAsync() is already starting it, we just need to wait and connect when ready
|
|
619
|
+
(async () => {
|
|
620
|
+
const bgLogger = pino({ name: 'beamable-otlp-bg', level: 'info' }, process.stdout);
|
|
621
|
+
const maxAttempts = 30; // Check for up to 30 seconds (30 * 1000ms)
|
|
622
|
+
const checkInterval = 1000; // Check every 1 second
|
|
623
|
+
|
|
624
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
625
|
+
try {
|
|
626
|
+
// Import isCollectorRunning dynamically to avoid circular dependencies
|
|
627
|
+
const { isCollectorRunning } = await import('./collector-manager.js');
|
|
628
|
+
const collectorStatus = await isCollectorRunning();
|
|
629
|
+
|
|
630
|
+
if (collectorStatus.isRunning && collectorStatus.isReady && collectorStatus.otlpEndpoint) {
|
|
631
|
+
// Collector is ready - initialize OTLP logging now
|
|
632
|
+
const endpoint = collectorStatus.otlpEndpoint.startsWith('http')
|
|
633
|
+
? collectorStatus.otlpEndpoint
|
|
634
|
+
: `http://${collectorStatus.otlpEndpoint}`;
|
|
635
|
+
|
|
636
|
+
const newProvider = await initializeOtlpLogging(
|
|
637
|
+
options.serviceName,
|
|
638
|
+
options.qualifiedServiceName,
|
|
639
|
+
env,
|
|
640
|
+
bgLogger
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
if (newProvider) {
|
|
644
|
+
// Update the provider reference so future logs use OTLP
|
|
645
|
+
otlpProviderRef.provider = newProvider;
|
|
646
|
+
console.error(`[OTLP] Connected to collector at ${endpoint} (background connection)`);
|
|
647
|
+
break; // Success, stop polling
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
// Silently fail and continue polling
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Wait before next check (unless we're on the last attempt)
|
|
655
|
+
if (attempt < maxAttempts - 1) {
|
|
656
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
})(); // Fire and forget - don't await
|
|
660
|
+
}
|
|
719
661
|
}
|
|
720
|
-
|
|
721
|
-
// Shared reference for OTLP logger provider
|
|
722
|
-
const otlpProviderRef: { provider: LoggerProvider | null } = { provider: otlpProvider };
|
|
723
662
|
|
|
724
663
|
const pinoOptions: LoggerOptions = {
|
|
725
664
|
name: options.name ?? 'beamable-node-runtime',
|
package/src/runtime.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { GatewayRequester } from './requester.js';
|
|
|
4
4
|
import { AuthManager } from './auth.js';
|
|
5
5
|
import { createLogger } from './logger.js';
|
|
6
6
|
import { loadEnvironmentConfig } from './env.js';
|
|
7
|
-
import {
|
|
7
|
+
import { startCollectorAndWaitForReady } from './collector-manager.js';
|
|
8
8
|
import pino from 'pino';
|
|
9
9
|
import { listRegisteredServices, getServiceOptions, getConfigureServicesHandlers, getInitializeServicesHandlers } from './decorators.js';
|
|
10
10
|
import { generateOpenApiDocument } from './docs.js';
|
|
@@ -80,30 +80,47 @@ export class MicroserviceRuntime {
|
|
|
80
80
|
const primaryService = registered[0];
|
|
81
81
|
const qualifiedServiceName = `micro_${primaryService.qualifiedName}`;
|
|
82
82
|
|
|
83
|
-
// STEP 3:
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
// - Downloading collector binary (~12MB, can take 30-60+ seconds on slow networks)
|
|
89
|
-
// - Downloading collector config (~1 second)
|
|
90
|
-
// - Starting collector process (~1 second)
|
|
91
|
-
// - Waiting for collector to be ready (up to 60 seconds)
|
|
92
|
-
// Total: ~90-125 seconds minimum, so use 180 seconds (3 minutes) for safety
|
|
93
|
-
// Real-world testing shows downloads can take 2+ minutes on slow networks
|
|
94
|
-
const otlpEndpoint = setupCollectorBeforeLogging(
|
|
95
|
-
this.env,
|
|
96
|
-
180000 // 180 second timeout (3 minutes) to allow for slow downloads + startup + readiness
|
|
97
|
-
);
|
|
83
|
+
// STEP 3: Wait for collector to be ready BEFORE creating structured logger
|
|
84
|
+
// Portal logs (structured logs via OTLP) should only appear AFTER collector is ready
|
|
85
|
+
// This ensures all runtime startup logs are captured and appear in Portal
|
|
86
|
+
// Use deasync to wait synchronously (constructor can't be async)
|
|
87
|
+
startupLogger.info('Setting up OpenTelemetry collector (waiting for readiness before enabling Portal logs)...');
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
90
|
+
const deasync = require('deasync');
|
|
91
|
+
let otlpEndpoint: string | null = null;
|
|
92
|
+
let collectorReady = false;
|
|
93
|
+
let collectorError: string | null = null;
|
|
94
|
+
|
|
95
|
+
// Start collector setup asynchronously
|
|
96
|
+
startCollectorAndWaitForReady(this.env, 180000)
|
|
97
|
+
.then((endpoint) => {
|
|
98
|
+
otlpEndpoint = endpoint;
|
|
99
|
+
collectorReady = true;
|
|
100
|
+
if (endpoint) {
|
|
101
|
+
startupLogger.info(`Collector ready at ${endpoint}, creating structured logger for Portal logs...`);
|
|
102
|
+
} else {
|
|
103
|
+
startupLogger.warn('Collector setup did not complete in time, Portal logs will not be available');
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
collectorError = error instanceof Error ? error.message : String(error);
|
|
108
|
+
collectorReady = true;
|
|
109
|
+
startupLogger.error(`Failed to setup collector: ${collectorError}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Wait synchronously for collector to be ready (with timeout)
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
const timeoutMs = 180000; // 3 minutes
|
|
115
|
+
deasync.loopWhile(() => {
|
|
116
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
117
|
+
return false; // Timeout
|
|
118
|
+
}
|
|
119
|
+
return !collectorReady; // Continue waiting if not ready
|
|
120
|
+
});
|
|
104
121
|
|
|
105
|
-
// STEP 4: Create main logger (collector is now ready,
|
|
106
|
-
// Pass the OTLP endpoint to
|
|
122
|
+
// STEP 4: Create main structured logger (collector is now ready, Portal logs will work)
|
|
123
|
+
// Pass the OTLP endpoint to ensure it uses the ready collector
|
|
107
124
|
this.logger = createLogger(this.env, {
|
|
108
125
|
name: 'beamable-node-microservice',
|
|
109
126
|
serviceName: primaryService.name,
|