@omen.foundation/node-microservice-runtime 0.1.50 → 0.1.52

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,49 @@ 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
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
+ if (standardOtelEnabled) {
616
+ // Try to discover collector asynchronously (won't block service startup)
617
+ // If collector is already running, we'll connect immediately
618
+ // If not, we'll retry periodically in the background
619
+ (async () => {
620
+ // Give collector a moment to start (if it's just starting)
621
+ await new Promise(resolve => setTimeout(resolve, 2000));
622
+
623
+ // Try to discover collector (quick check, non-blocking)
624
+ try {
625
+ const discoveredEndpoint = await discoverOrStartCollector(
626
+ pino({ name: 'beamable-otlp-bg', level: 'info' }, process.stdout),
627
+ standardOtelEnabled,
628
+ env
629
+ );
630
+
631
+ if (discoveredEndpoint) {
632
+ // Collector is ready - initialize OTLP logging now
633
+ const newProvider = await initializeOtlpLogging(
634
+ options.serviceName,
635
+ options.qualifiedServiceName,
636
+ env,
637
+ pino({ name: 'beamable-otlp-bg', level: 'info' }, process.stdout)
638
+ );
639
+
640
+ if (newProvider) {
641
+ // Update the provider reference so future logs use OTLP
642
+ otlpProviderRef.provider = newProvider;
643
+ console.error(`[OTLP] Connected to collector at ${discoveredEndpoint} (background connection)`);
644
+ }
645
+ }
646
+ } catch (error) {
647
+ // Silently fail - collector will connect later if it starts
648
+ }
649
+ })(); // Fire and forget - don't await
650
+ }
719
651
  }
720
-
721
- // Shared reference for OTLP logger provider
722
- const otlpProviderRef: { provider: LoggerProvider | null } = { provider: otlpProvider };
723
652
 
724
653
  const pinoOptions: LoggerOptions = {
725
654
  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 { startCollectorAsync } 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,34 +80,20 @@ 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 5-10+ 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: ~70-80 seconds minimum, so use 120 seconds for safety
93
- const otlpEndpoint = setupCollectorBeforeLogging(
94
- this.env,
95
- 120000 // 120 second timeout to allow for download + startup + readiness
96
- );
97
-
98
- if (otlpEndpoint) {
99
- startupLogger.info(`Collector ready at ${otlpEndpoint}, creating main logger...`);
100
- } else {
101
- startupLogger.warn('Collector setup did not complete, continuing without OTLP logging');
102
- }
83
+ // STEP 3: Start collector asynchronously in background (NON-BLOCKING)
84
+ // This allows the service to start immediately while collector downloads/starts
85
+ // Logs will work immediately (stdout), OTLP will connect when collector is ready
86
+ startupLogger.info('Starting OpenTelemetry collector setup in background (non-blocking)...');
87
+ startCollectorAsync(this.env);
103
88
 
104
- // STEP 4: Create main logger (collector is now ready, so all logs will be captured)
105
- // Pass the OTLP endpoint to skip re-discovery/startup
89
+ // STEP 4: Create main logger immediately (service can start serving requests)
90
+ // Logger works immediately via stdout, OTLP will connect when collector is ready
91
+ // The logger will automatically start using OTLP once the collector is available
106
92
  this.logger = createLogger(this.env, {
107
93
  name: 'beamable-node-microservice',
108
94
  serviceName: primaryService.name,
109
95
  qualifiedServiceName: qualifiedServiceName,
110
- otlpEndpoint: otlpEndpoint || undefined, // Pass endpoint if collector was set up
96
+ // Don't pass otlpEndpoint - let it discover/connect when collector is ready
111
97
  });
112
98
  this.serviceManager = new BeamableServiceManager(this.env, this.logger);
113
99