@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":
|
|
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.
|
|
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.
|
|
29
|
-
"@onlineapps/conn-infra-error-handler": "1.0.
|
|
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.
|
|
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.
|
|
37
|
-
"@onlineapps/service-common": "1.0.
|
|
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": {
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -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
|