@onlineapps/service-wrapper 2.1.86 → 2.1.88

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.
@@ -12,13 +12,18 @@
12
12
  "heartbeatInterval": 30000
13
13
  },
14
14
  "infrastructureGate": {
15
- "maxWaitMs": 5000,
15
+ "maxWaitMs": 30000,
16
16
  "checkIntervalMs": 2000
17
17
  },
18
18
  "monitoring": {
19
19
  "enabled": true,
20
20
  "metrics": ["requests", "errors", "duration"]
21
21
  },
22
+ "startupAlerts": {
23
+ "enabled": true,
24
+ "cooldownMs": 600000,
25
+ "stateFile": "/tmp/oa_startup_failure_state.json"
26
+ },
22
27
  "logging": {
23
28
  "enabled": true,
24
29
  "level": "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.1.86",
3
+ "version": "2.1.88",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -25,16 +25,16 @@
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@onlineapps/conn-base-cache": "1.0.8",
28
- "@onlineapps/conn-base-monitoring": "1.0.8",
29
- "@onlineapps/conn-infra-error-handler": "1.0.7",
28
+ "@onlineapps/conn-base-monitoring": "1.0.9",
29
+ "@onlineapps/conn-infra-error-handler": "1.0.8",
30
30
  "@onlineapps/conn-infra-mq": "1.1.66",
31
31
  "@onlineapps/conn-orch-api-mapper": "1.0.26",
32
32
  "@onlineapps/conn-orch-cookbook": "2.0.27",
33
- "@onlineapps/conn-orch-orchestrator": "1.0.90",
33
+ "@onlineapps/conn-orch-orchestrator": "1.0.91",
34
34
  "@onlineapps/conn-orch-registry": "1.1.43",
35
35
  "@onlineapps/conn-orch-validator": "2.0.25",
36
- "@onlineapps/monitoring-core": "1.0.18",
37
- "@onlineapps/service-common": "1.0.13",
36
+ "@onlineapps/monitoring-core": "1.0.20",
37
+ "@onlineapps/service-common": "1.0.15",
38
38
  "@onlineapps/runtime-config": "1.0.2"
39
39
  },
40
40
  "devDependencies": {
@@ -423,6 +423,14 @@ class ServiceWrapper {
423
423
 
424
424
  // Cleanup before restart
425
425
  await this._cleanupBeforeRestart();
426
+
427
+ // Startup failure alert (works without MQ/monitoring)
428
+ try {
429
+ await this._maybeSendStartupFailureAlert({ phase, phaseName, error, isTransient });
430
+ } catch (alertErr) {
431
+ // Alerts must never block restart; log and continue.
432
+ console.warn('[StartupAlerts] Failed to send startup failure alert:', alertErr.message);
433
+ }
426
434
 
427
435
  if (isTransient) {
428
436
  // Přechodná chyba → restart může pomoci
@@ -431,11 +439,85 @@ class ServiceWrapper {
431
439
  } else {
432
440
  // Trvalá chyba → restart nepomůže
433
441
  console.error(`[FÁZE ${phase}] Permanent error - fix required, no restart`);
434
- // TODO: Send alert (email, Slack, etc.)
435
442
  process.exit(1);
436
443
  }
437
444
  }
438
445
 
446
+ /**
447
+ * Send startup-failure alert via SMTP (independent of MQ).
448
+ * Uses service-common sendMonitoringFailFallbackEmail (SMTP config via INFRA_REPORT_* env).
449
+ * Throttled by a persistent state file (survives container restarts).
450
+ *
451
+ * @private
452
+ */
453
+ async _maybeSendStartupFailureAlert({ phase, phaseName, error, isTransient }) {
454
+ const cfg = this.config.wrapper?.startupAlerts;
455
+ if (!cfg || cfg.enabled !== true) {
456
+ return;
457
+ }
458
+
459
+ const cooldownMs = cfg.cooldownMs;
460
+ const stateFile = cfg.stateFile;
461
+ if (typeof cooldownMs !== 'number' || Number.isNaN(cooldownMs) || cooldownMs <= 0) {
462
+ throw new Error(`[StartupAlerts] Invalid configuration - wrapper.startupAlerts.cooldownMs must be a positive number, got: ${cooldownMs}`);
463
+ }
464
+ if (typeof stateFile !== 'string' || stateFile.trim() === '') {
465
+ throw new Error(`[StartupAlerts] Invalid configuration - wrapper.startupAlerts.stateFile must be a non-empty string, got: ${stateFile}`);
466
+ }
467
+
468
+ const fs = require('fs');
469
+ const path = require('path');
470
+ const serviceName = this.config.service?.name || 'unnamed-service';
471
+ const now = Date.now();
472
+
473
+ let state = { lastAlertAt: 0, failureCount: 0, lastFailureAt: 0 };
474
+ try {
475
+ if (fs.existsSync(stateFile)) {
476
+ const raw = fs.readFileSync(stateFile, 'utf8');
477
+ state = { ...state, ...(JSON.parse(raw) || {}) };
478
+ } else {
479
+ // Ensure parent dir exists (best-effort)
480
+ const dir = path.dirname(stateFile);
481
+ try { fs.mkdirSync(dir, { recursive: true }); } catch (_) { /* ignore */ }
482
+ }
483
+ } catch (e) {
484
+ // If state cannot be read, continue with defaults (alerting must not crash init)
485
+ }
486
+
487
+ state.failureCount = (state.failureCount || 0) + 1;
488
+ state.lastFailureAt = now;
489
+
490
+ const shouldSend = !state.lastAlertAt || (now - state.lastAlertAt) >= cooldownMs;
491
+ if (!shouldSend) {
492
+ // Persist updated counters anyway
493
+ try { fs.writeFileSync(stateFile, JSON.stringify(state), 'utf8'); } catch (_) { /* ignore */ }
494
+ return;
495
+ }
496
+
497
+ const { sendMonitoringFailFallbackEmail } = require('@onlineapps/service-common');
498
+ const transientLabel = isTransient ? 'TRANSIENT' : 'PERMANENT';
499
+ const subject = `[StartupFailure] ${serviceName} phase ${phase} (${transientLabel})`;
500
+ const text = [
501
+ `Service: ${serviceName}`,
502
+ `Phase: ${phase} (${phaseName})`,
503
+ `Type: ${transientLabel}`,
504
+ `Failure count (container): ${state.failureCount}`,
505
+ `Timestamp: ${new Date(now).toISOString()}`,
506
+ `Error: ${error?.message || 'unknown error'}`
507
+ ].join('\n');
508
+ const html = `<pre>${text}</pre>`;
509
+
510
+ const sent = await sendMonitoringFailFallbackEmail(subject, text, html);
511
+ state.lastAlertAt = now;
512
+ try { fs.writeFileSync(stateFile, JSON.stringify(state), 'utf8'); } catch (_) { /* ignore */ }
513
+
514
+ if (sent) {
515
+ this.logger?.info('[StartupAlerts] ✓ Startup failure alert sent', { service: serviceName, phase, transient: isTransient });
516
+ } else {
517
+ this.logger?.warn('[StartupAlerts] Startup failure alert not sent (SMTP config missing or send failed)', { service: serviceName, phase });
518
+ }
519
+ }
520
+
439
521
  async initialize() {
440
522
  if (this.isInitialized) {
441
523
  // Logger might not be initialized yet, use console as fallback