@omen.foundation/node-microservice-runtime 0.1.55 → 0.1.57

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.
@@ -123,15 +123,22 @@ export async function fetchClickHouseCredentials(
123
123
  const uriPath = '/api/beamo/otel/auth/writer/config';
124
124
  const configUrl = new URL(uriPath, apiUrl).toString();
125
125
 
126
+ // Create a minimal logger for verbose output
127
+ const verboseLogger = pino({ name: 'fetch-credentials', level: 'info' }, process.stdout);
128
+ verboseLogger.info(`[Credentials] Starting credential fetch from ${configUrl}...`);
129
+
126
130
  // Get secret from environment (required for signed requests)
127
131
  const secret = process.env.SECRET;
128
132
  if (!secret) {
133
+ verboseLogger.error('[Credentials] SECRET environment variable is missing');
129
134
  throw new Error('SECRET environment variable is required to fetch ClickHouse credentials');
130
135
  }
136
+ verboseLogger.info('[Credentials] SECRET found, calculating signature...');
131
137
 
132
138
  // Calculate signature for signed request (matching C# HttpSignedRequester)
133
139
  const pathAndQuery = getPathAndQuery(uriPath);
134
140
  const signature = calculateSignature(env.pid, secret, pathAndQuery, null, '1');
141
+ verboseLogger.info('[Credentials] Signature calculated, making API request...');
135
142
 
136
143
  // Build headers with signed authentication (matching C# HttpSignedRequester)
137
144
  const headers: Record<string, string> = {
@@ -141,22 +148,29 @@ export async function fetchClickHouseCredentials(
141
148
  'X-BEAM-SIGNATURE': signature,
142
149
  };
143
150
 
151
+ const fetchStartTime = Date.now();
144
152
  const response = await fetch(configUrl, {
145
153
  method: 'GET',
146
154
  headers,
147
155
  });
156
+ const fetchElapsed = Date.now() - fetchStartTime;
157
+ verboseLogger.info(`[Credentials] API request completed in ${fetchElapsed}ms, status: ${response.status}`);
148
158
 
149
159
  if (!response.ok) {
150
160
  const errorText = await response.text().catch(() => 'Unknown error');
161
+ verboseLogger.error(`[Credentials] API request failed: ${response.status} ${response.statusText}`);
151
162
  throw new Error(`Failed to fetch ClickHouse credentials from ${configUrl}: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
152
163
  }
153
164
 
165
+ verboseLogger.info('[Credentials] Parsing response JSON...');
154
166
  const credentials = await response.json() as ClickHouseCredentials;
155
167
 
156
168
  if (!credentials.endpoint || !credentials.username || !credentials.password) {
169
+ verboseLogger.error('[Credentials] Invalid response: missing required fields');
157
170
  throw new Error('Invalid ClickHouse credentials response: missing required fields');
158
171
  }
159
172
 
173
+ verboseLogger.info(`[Credentials] Successfully fetched credentials (endpoint: ${credentials.endpoint}, username: ${credentials.username})`);
160
174
  return credentials;
161
175
  }
162
176
 
@@ -555,7 +569,7 @@ export function addAuthEnvironmentVars(endpoint: string, username: string, passw
555
569
  */
556
570
  export async function startCollectorAndWaitForReady(
557
571
  env: EnvironmentConfig,
558
- timeoutMs: number = 180000 // 3 minutes to allow for download + startup
572
+ _timeoutMs?: number // Not used - kept for API compatibility
559
573
  ): Promise<string | null> {
560
574
  // Match C# logic: (this.InDocker() || UseLocalOtel) && !BEAM_DISABLE_STANDARD_OTEL
561
575
  const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
@@ -575,65 +589,25 @@ export async function startCollectorAndWaitForReady(
575
589
  }, process.stdout);
576
590
 
577
591
  initLogger.info('[OTLP] Setting up collector (waiting for readiness before enabling Portal logs)...');
592
+ const setupStartTime = Date.now();
578
593
 
579
- // Wait for collector to be ready with timeout
580
- const startTime = Date.now();
581
- let endpoint: string | null = null;
582
- let completed = false;
583
-
584
- // Start collector setup - wrap in a promise that handles errors
585
- const setupPromise = discoverOrStartCollector(initLogger, standardOtelEnabled, env)
586
- .then((result) => {
587
- if (result) {
588
- return result;
589
- }
590
- return null;
591
- })
592
- .catch((error) => {
593
- initLogger.error(`[OTLP] Collector setup error: ${error instanceof Error ? error.message : String(error)}`);
594
- return null;
595
- });
596
-
597
- // Set timeout - only resolve if setup hasn't completed
598
- const timeoutPromise = new Promise<string | null>((resolve) => {
599
- setTimeout(() => {
600
- if (!completed) {
601
- initLogger.warn(`[OTLP] Collector setup timeout after ${timeoutMs}ms, continuing without Portal logs`);
602
- completed = true;
603
- resolve(null);
604
- }
605
- }, timeoutMs);
606
- });
607
-
608
- // Race between setup and timeout - wait for whichever completes first
609
- // But we need to check if setup actually succeeded (got a valid endpoint)
594
+ // Simple linear async/await - no timeouts, no Promise.race complexity
610
595
  try {
611
- const result = await Promise.race([setupPromise, timeoutPromise]);
596
+ initLogger.info('[OTLP] Step 1: Discovering or starting collector...');
597
+ const endpoint = await discoverOrStartCollector(initLogger, standardOtelEnabled, env);
598
+ const elapsed = Date.now() - setupStartTime;
612
599
 
613
- // Check if we got a valid endpoint (setup completed successfully)
614
- if (result && typeof result === 'string' && result.length > 0 && !completed) {
615
- endpoint = result;
616
- completed = true;
617
- const elapsed = Date.now() - startTime;
618
- initLogger.info(`[OTLP] Collector ready at ${endpoint} (took ${elapsed}ms). Portal logs now enabled.`);
600
+ if (endpoint) {
601
+ initLogger.info(`[OTLP] Collector ready at ${endpoint}. Portal logs now enabled. (took ${elapsed}ms)`);
619
602
  return endpoint;
603
+ } else {
604
+ initLogger.warn(`[OTLP] Collector setup failed, continuing without Portal logs. (took ${elapsed}ms)`);
605
+ return null;
620
606
  }
621
-
622
- // If we got here, either:
623
- // 1. Setup returned null (failed or timed out internally)
624
- // 2. Timeout promise resolved
625
- // 3. Result is not a valid endpoint string
626
- if (!completed) {
627
- completed = true;
628
- }
629
- return null;
630
607
  } catch (error) {
631
- // Setup promise rejected - collector startup failed
608
+ const elapsed = Date.now() - setupStartTime;
632
609
  const errorMsg = error instanceof Error ? error.message : String(error);
633
- initLogger.error(`[OTLP] Collector setup failed: ${errorMsg}`);
634
- if (!completed) {
635
- completed = true;
636
- }
610
+ initLogger.error(`[OTLP] Collector setup failed after ${elapsed}ms: ${errorMsg}`);
637
611
  return null;
638
612
  }
639
613
  }
@@ -796,7 +770,10 @@ export async function startCollector(
796
770
  if ((!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) && env) {
797
771
  try {
798
772
  logger.info('[Collector] Fetching ClickHouse credentials from Beamable API...');
773
+ const credStartTime = Date.now();
799
774
  const credentials = await fetchClickHouseCredentials(env);
775
+ const credElapsed = Date.now() - credStartTime;
776
+ logger.info(`[Collector] ClickHouse credentials fetch completed in ${credElapsed}ms`);
800
777
  clickhouseEndpoint = credentials.endpoint;
801
778
  clickhouseUsername = credentials.username;
802
779
  clickhousePassword = credentials.password;
@@ -833,8 +810,11 @@ export async function startCollector(
833
810
  }
834
811
 
835
812
  // Now resolve collector binary and config (after credentials are fetched and set)
836
- logger.info('[Collector] Resolving collector binary and config...');
813
+ logger.info('[Collector] Step 2: Resolving collector binary and config...');
814
+ const resolveStartTime = Date.now();
837
815
  const collectorInfo = await resolveCollector(true, logger);
816
+ const resolveElapsed = Date.now() - resolveStartTime;
817
+ logger.info(`[Collector] Collector binary/config resolution completed in ${resolveElapsed}ms`);
838
818
 
839
819
  if (!collectorInfo.binaryPath) {
840
820
  logger.error('[Collector] Binary not found and download failed');
@@ -1067,11 +1047,14 @@ export async function discoverOrStartCollector(
1067
1047
 
1068
1048
  // First, check if collector is already running (via UDP discovery)
1069
1049
  if (!globalCollectorProcess) {
1050
+ logger.info('[Collector] Checking for existing collector via UDP discovery...');
1070
1051
  const status = await isCollectorRunning();
1052
+ logger.info(`[Collector] UDP discovery result: isRunning=${status.isRunning}, isReady=${status.isReady}, endpoint=${status.otlpEndpoint || 'none'}`);
1071
1053
  if (status.isRunning && status.isReady && status.otlpEndpoint) {
1072
1054
  logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);
1073
1055
  return `http://${status.otlpEndpoint}`;
1074
1056
  }
1057
+ logger.info('[Collector] No existing collector found, will start new one...');
1075
1058
  }
1076
1059
 
1077
1060
  // Collector not running - start it (or wait for existing one to be ready)
@@ -1089,12 +1072,15 @@ export async function discoverOrStartCollector(
1089
1072
  logger.info(`[Collector] Waiting for existing collector to become ready at ${endpoint}...`);
1090
1073
  } else {
1091
1074
  // Start a new collector
1092
- logger.info('[Collector] Starting OpenTelemetry collector...');
1075
+ logger.info('[Collector] Starting new OpenTelemetry collector...');
1076
+ const startCollectorTime = Date.now();
1093
1077
  const startResult = await startCollector(logger, undefined, env);
1078
+ const startCollectorElapsed = Date.now() - startCollectorTime;
1079
+ logger.info(`[Collector] Collector process started in ${startCollectorElapsed}ms, endpoint: ${startResult.endpoint}`);
1094
1080
  endpoint = startResult.endpoint;
1095
1081
 
1096
1082
  // Check if collector process exited immediately (crashed)
1097
- // Wait a bit longer to see if it crashes right after starting
1083
+ logger.info('[Collector] Checking if collector process is still running...');
1098
1084
  await new Promise(resolve => setTimeout(resolve, 200));
1099
1085
 
1100
1086
  if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
@@ -1103,6 +1089,7 @@ export async function discoverOrStartCollector(
1103
1089
  logger.error(`[Collector] ${errorMsg}`);
1104
1090
  return null;
1105
1091
  }
1092
+ logger.info('[Collector] Collector process is still running, proceeding to readiness check...');
1106
1093
  }
1107
1094
 
1108
1095
  // CRITICAL: Wait for collector to be fully ready before returning
@@ -1112,28 +1099,33 @@ export async function discoverOrStartCollector(
1112
1099
  const checkInterval = 500; // Check every 500ms
1113
1100
  const maxChecks = Math.floor(maxWaitTime / checkInterval);
1114
1101
 
1115
- logger.info('[Collector] Waiting for collector to become ready...');
1102
+ logger.info(`[Collector] Waiting for collector to become ready (checking every ${checkInterval}ms, max ${maxWaitTime / 1000}s)...`);
1103
+ const readinessStartTime = Date.now();
1116
1104
 
1117
1105
  for (let i = 0; i < maxChecks; i++) {
1118
1106
  await new Promise(resolve => setTimeout(resolve, checkInterval));
1107
+ const elapsed = Date.now() - readinessStartTime;
1119
1108
 
1120
1109
  // Check if process exited during wait
1121
1110
  if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
1122
1111
  const errorMsg = `Collector process exited during startup with code ${globalCollectorExitCode}. ${globalCollectorStderr.length > 0 ? `Stderr: ${globalCollectorStderr.join('; ')}` : 'No stderr output.'}`;
1123
1112
  globalCollectorInitError = errorMsg;
1124
- logger.error(`[Collector] ${errorMsg}`);
1113
+ logger.error(`[Collector] ${errorMsg} (after ${elapsed}ms)`);
1125
1114
  return null;
1126
1115
  }
1127
1116
 
1117
+ logger.info(`[Collector] Checking collector readiness... (${(elapsed / 1000).toFixed(1)}s elapsed, attempt ${i + 1}/${maxChecks})`);
1128
1118
  const newStatus = await isCollectorRunning();
1119
+ logger.info(`[Collector] Collector status: isRunning=${newStatus.isRunning}, isReady=${newStatus.isReady}, pid=${newStatus.pid}, endpoint=${newStatus.otlpEndpoint || 'none'}`);
1120
+
1129
1121
  if (newStatus.isRunning && newStatus.isReady) {
1130
- logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);
1122
+ logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint} (ready after ${elapsed}ms)`);
1131
1123
  return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
1132
1124
  }
1133
1125
 
1134
- // Log progress every 2 seconds
1135
- if (i > 0 && i % 4 === 0) {
1136
- logger.info(`[Collector] Still waiting for collector to become ready... (${(i * checkInterval) / 1000}s elapsed)`);
1126
+ // Log progress every second (every 2 checks)
1127
+ if (i > 0 && i % 2 === 0) {
1128
+ logger.info(`[Collector] Still waiting for collector to become ready... (${(elapsed / 1000).toFixed(1)}s elapsed)`);
1137
1129
  }
1138
1130
  }
1139
1131
 
package/src/runtime.ts CHANGED
@@ -87,40 +87,28 @@ export class MicroserviceRuntime {
87
87
  // Use deasync to wait synchronously (constructor can't be async)
88
88
  startupLogger.info('Setting up OpenTelemetry collector (waiting for readiness before enabling Portal logs)...');
89
89
 
90
+ // Wait for collector to be ready - simple linear async/await
91
+ // Use deasync since constructor can't be async
90
92
  let otlpEndpoint: string | null = null;
91
93
  let collectorReady = false;
92
- let collectorError: string | null = null;
93
94
 
94
- // Start collector setup asynchronously
95
- startCollectorAndWaitForReady(this.env, 180000)
95
+ startCollectorAndWaitForReady(this.env)
96
96
  .then((endpoint) => {
97
97
  otlpEndpoint = endpoint;
98
98
  collectorReady = true;
99
- if (endpoint) {
100
- startupLogger.info(`Collector ready at ${endpoint}, creating structured logger for Portal logs...`);
101
- } else {
102
- startupLogger.warn('Collector setup did not complete in time, Portal logs will not be available');
103
- }
104
99
  })
105
100
  .catch((error) => {
106
- collectorError = error instanceof Error ? error.message : String(error);
101
+ const errorMsg = error instanceof Error ? error.message : String(error);
102
+ startupLogger.error(`Failed to setup collector: ${errorMsg}`);
103
+ otlpEndpoint = null;
107
104
  collectorReady = true;
108
- startupLogger.error(`Failed to setup collector: ${collectorError}`);
109
105
  });
110
106
 
111
- // Wait synchronously for collector to be ready (with timeout)
112
- const startTime = Date.now();
113
- const timeoutMs = 180000; // 3 minutes
114
- deasync.loopWhile(() => {
115
- if (Date.now() - startTime >= timeoutMs) {
116
- return false; // Timeout
117
- }
118
- return !collectorReady; // Continue waiting if not ready
119
- });
107
+ // Wait synchronously for collector to be ready (constructor can't be async)
108
+ deasync.loopWhile(() => !collectorReady);
120
109
 
121
110
  // STEP 4: Create main logger
122
111
  // CRITICAL: Only create structured logger (Portal logs) if collector is ready
123
- // If collector timed out or failed, continue with minimal console logger
124
112
  // This ensures Portal logs only appear AFTER collector is ready
125
113
  if (otlpEndpoint) {
126
114
  // Collector is ready - create structured logger with OTLP support