@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/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
- let otlpProvider: LoggerProvider | null = null;
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
- otlpProvider = provider;
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 - try to discover/start collector (full initialization)
712
- // This ensures all logs from this point forward are captured via OTLP
713
- otlpProvider = initializeOtlpSync(
714
- options.serviceName,
715
- options.qualifiedServiceName,
716
- env,
717
- 60000 // 60 second timeout to allow collector download, startup, and readiness verification
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 { setupCollectorBeforeLogging } from './collector-manager.js';
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: Setup collector BEFORE creating the main logger
84
- // This ensures all logs from the main logger are captured via OTLP
85
- startupLogger.info('Setting up OpenTelemetry collector...');
86
- // Timeout needs to account for:
87
- // - API call for credentials (~1-2 seconds)
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
- if (otlpEndpoint) {
100
- startupLogger.info(`Collector ready at ${otlpEndpoint}, creating main logger...`);
101
- } else {
102
- startupLogger.warn('Collector setup did not complete, continuing without OTLP logging');
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, so all logs will be captured)
106
- // Pass the OTLP endpoint to skip re-discovery/startup
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,