wilfredwake 1.0.0
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/.env.example +52 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +400 -0
- package/README.md +831 -0
- package/bin/cli.js +133 -0
- package/index.js +222 -0
- package/package.json +41 -0
- package/src/cli/commands/health.js +238 -0
- package/src/cli/commands/init.js +192 -0
- package/src/cli/commands/status.js +180 -0
- package/src/cli/commands/wake.js +182 -0
- package/src/cli/config.js +263 -0
- package/src/config/services.yaml +49 -0
- package/src/orchestrator/orchestrator.js +397 -0
- package/src/orchestrator/registry.js +283 -0
- package/src/orchestrator/server.js +402 -0
- package/src/shared/colors.js +154 -0
- package/src/shared/logger.js +224 -0
- package/tests/cli.test.js +328 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ CLI CONFIG MANAGEMENT ║
|
|
4
|
+
* ║ Handles ~/.wilfredwake/config.json read/write ║
|
|
5
|
+
* ║ Stores user preferences, tokens, and settings ║
|
|
6
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration manager for wilfredwake CLI
|
|
15
|
+
* Handles persistent storage in user's home directory
|
|
16
|
+
*/
|
|
17
|
+
export class ConfigManager {
|
|
18
|
+
constructor() {
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════
|
|
20
|
+
// CONFIG PATHS
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════
|
|
22
|
+
this.configDir = path.join(os.homedir(), '.wilfredwake');
|
|
23
|
+
this.configFile = path.join(this.configDir, 'config.json');
|
|
24
|
+
this.cacheDir = path.join(this.configDir, 'cache');
|
|
25
|
+
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════
|
|
27
|
+
// DEFAULT CONFIGURATION
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════
|
|
29
|
+
this.defaultConfig = {
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
orchestratorUrl: 'http://localhost:3000',
|
|
32
|
+
token: null,
|
|
33
|
+
environment: 'dev',
|
|
34
|
+
userId: this._generateUserId(),
|
|
35
|
+
preferences: {
|
|
36
|
+
outputFormat: 'table', // table or json
|
|
37
|
+
verbose: false, // Enable debug output
|
|
38
|
+
autoWait: true, // Wait for services to be ready
|
|
39
|
+
timeout: 300, // Default timeout in seconds
|
|
40
|
+
colorOutput: true, // Enable colored output
|
|
41
|
+
notifyOnComplete: false, // Notify when wake completes
|
|
42
|
+
},
|
|
43
|
+
environments: {
|
|
44
|
+
dev: {
|
|
45
|
+
name: 'Development',
|
|
46
|
+
orchestratorUrl: 'http://localhost:3000',
|
|
47
|
+
},
|
|
48
|
+
staging: {
|
|
49
|
+
name: 'Staging',
|
|
50
|
+
orchestratorUrl: 'http://staging-orchestrator:3000',
|
|
51
|
+
},
|
|
52
|
+
prod: {
|
|
53
|
+
name: 'Production',
|
|
54
|
+
orchestratorUrl: 'http://prod-orchestrator:3000',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
lastSyncTime: null,
|
|
58
|
+
cacheExpiry: 3600000, // 1 hour in milliseconds
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a unique user ID for multi-developer support
|
|
64
|
+
* @private
|
|
65
|
+
* @returns {string} Unique user identifier
|
|
66
|
+
*/
|
|
67
|
+
_generateUserId() {
|
|
68
|
+
return `user_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize configuration directory and files
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async init() {
|
|
76
|
+
try {
|
|
77
|
+
// Create .wilfredwake directory if it doesn't exist
|
|
78
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
// Create cache directory
|
|
81
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
// Create config file with defaults if it doesn't exist
|
|
84
|
+
const exists = await this._fileExists(this.configFile);
|
|
85
|
+
if (!exists) {
|
|
86
|
+
await this.saveConfig(this.defaultConfig);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Failed to initialize configuration: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load configuration from disk
|
|
95
|
+
* @returns {Promise<Object>} Configuration object
|
|
96
|
+
*/
|
|
97
|
+
async loadConfig() {
|
|
98
|
+
try {
|
|
99
|
+
const exists = await this._fileExists(this.configFile);
|
|
100
|
+
if (!exists) {
|
|
101
|
+
await this.init();
|
|
102
|
+
return this.defaultConfig;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const content = await fs.readFile(this.configFile, 'utf-8');
|
|
106
|
+
const config = JSON.parse(content);
|
|
107
|
+
|
|
108
|
+
// Merge with defaults for any missing properties
|
|
109
|
+
return { ...this.defaultConfig, ...config };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Failed to load configuration: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Save configuration to disk
|
|
117
|
+
* @param {Object} config - Configuration object
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
120
|
+
async saveConfig(config) {
|
|
121
|
+
try {
|
|
122
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
123
|
+
const content = JSON.stringify(config, null, 2);
|
|
124
|
+
await fs.writeFile(this.configFile, content, 'utf-8');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to save configuration: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Update specific configuration value
|
|
132
|
+
* @param {string} key - Configuration key (supports dot notation: "preferences.verbose")
|
|
133
|
+
* @param {any} value - New value
|
|
134
|
+
* @returns {Promise<Object>} Updated configuration
|
|
135
|
+
*/
|
|
136
|
+
async updateConfig(key, value) {
|
|
137
|
+
const config = await this.loadConfig();
|
|
138
|
+
const keys = key.split('.');
|
|
139
|
+
let current = config;
|
|
140
|
+
|
|
141
|
+
// Navigate to the target object
|
|
142
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
143
|
+
if (!current[keys[i]]) {
|
|
144
|
+
current[keys[i]] = {};
|
|
145
|
+
}
|
|
146
|
+
current = current[keys[i]];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Set the value
|
|
150
|
+
current[keys[keys.length - 1]] = value;
|
|
151
|
+
|
|
152
|
+
await this.saveConfig(config);
|
|
153
|
+
return config;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get specific configuration value
|
|
158
|
+
* @param {string} key - Configuration key (supports dot notation)
|
|
159
|
+
* @returns {Promise<any>} Configuration value
|
|
160
|
+
*/
|
|
161
|
+
async getConfig(key) {
|
|
162
|
+
const config = await this.loadConfig();
|
|
163
|
+
const keys = key.split('.');
|
|
164
|
+
let current = config;
|
|
165
|
+
|
|
166
|
+
for (const k of keys) {
|
|
167
|
+
current = current?.[k];
|
|
168
|
+
if (current === undefined) return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Cache data for quick retrieval
|
|
176
|
+
* @param {string} key - Cache key
|
|
177
|
+
* @param {any} data - Data to cache
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
async setCacheData(key, data) {
|
|
181
|
+
try {
|
|
182
|
+
const cacheFile = path.join(this.cacheDir, `${key}.json`);
|
|
183
|
+
const cacheEntry = {
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
data,
|
|
186
|
+
};
|
|
187
|
+
await fs.writeFile(cacheFile, JSON.stringify(cacheEntry), 'utf-8');
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// Silently fail on cache errors
|
|
190
|
+
console.debug(`Cache write failed for ${key}: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve cached data if not expired
|
|
196
|
+
* @param {string} key - Cache key
|
|
197
|
+
* @returns {Promise<any|null>} Cached data or null if expired/not found
|
|
198
|
+
*/
|
|
199
|
+
async getCacheData(key) {
|
|
200
|
+
try {
|
|
201
|
+
const cacheFile = path.join(this.cacheDir, `${key}.json`);
|
|
202
|
+
const exists = await this._fileExists(cacheFile);
|
|
203
|
+
|
|
204
|
+
if (!exists) return null;
|
|
205
|
+
|
|
206
|
+
const content = await fs.readFile(cacheFile, 'utf-8');
|
|
207
|
+
const cacheEntry = JSON.parse(content);
|
|
208
|
+
|
|
209
|
+
const config = await this.loadConfig();
|
|
210
|
+
const isExpired =
|
|
211
|
+
Date.now() - cacheEntry.timestamp > config.cacheExpiry;
|
|
212
|
+
|
|
213
|
+
if (isExpired) {
|
|
214
|
+
await fs.unlink(cacheFile).catch(() => {});
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return cacheEntry.data;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear all cache
|
|
226
|
+
* @returns {Promise<void>}
|
|
227
|
+
*/
|
|
228
|
+
async clearCache() {
|
|
229
|
+
try {
|
|
230
|
+
const files = await fs.readdir(this.cacheDir);
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
await fs.unlink(path.join(this.cacheDir, file));
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.debug(`Cache clear failed: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if a file exists
|
|
241
|
+
* @private
|
|
242
|
+
* @param {string} filePath - Path to check
|
|
243
|
+
* @returns {Promise<boolean>} True if file exists
|
|
244
|
+
*/
|
|
245
|
+
async _fileExists(filePath) {
|
|
246
|
+
try {
|
|
247
|
+
await fs.access(filePath);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Reset configuration to defaults
|
|
256
|
+
* @returns {Promise<void>}
|
|
257
|
+
*/
|
|
258
|
+
async reset() {
|
|
259
|
+
await this.saveConfig(this.defaultConfig);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default ConfigManager;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ╔═══════════════════════════════════════════════════════════════╗
|
|
2
|
+
# ║ WILFREDWAKE SERVICE REGISTRY ║
|
|
3
|
+
# ║ Define all services, environments, and dependencies ║
|
|
4
|
+
# ║ This is the single source of truth for service mgmt ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════╝
|
|
6
|
+
|
|
7
|
+
# WILFREDWAKE SERVICE REGISTRY (SIMPLIFIED)
|
|
8
|
+
|
|
9
|
+
# Environment and version
|
|
10
|
+
env: dev
|
|
11
|
+
version: "1.0"
|
|
12
|
+
|
|
13
|
+
# SERVICES DEFINITION (DEV ONLY)
|
|
14
|
+
services:
|
|
15
|
+
dev:
|
|
16
|
+
# Core backend service
|
|
17
|
+
backend:
|
|
18
|
+
url: https://pension-backend-rs4h.onrender.com
|
|
19
|
+
health: /api/health
|
|
20
|
+
dependsOn: []
|
|
21
|
+
description: "pension backend Service"
|
|
22
|
+
|
|
23
|
+
# Frontend service
|
|
24
|
+
frontend:
|
|
25
|
+
url: https://transactions-k6gk.onrender.com
|
|
26
|
+
health: /health
|
|
27
|
+
dependsOn: []
|
|
28
|
+
description: "frontend Service"
|
|
29
|
+
|
|
30
|
+
# Payment gateway (producer)
|
|
31
|
+
payment-gateway:
|
|
32
|
+
url: https://payment-gateway-7eta.onrender.com
|
|
33
|
+
health: /health
|
|
34
|
+
dependsOn: []
|
|
35
|
+
description: "Payment Event Producer"
|
|
36
|
+
|
|
37
|
+
# Notification consumer (event processor)
|
|
38
|
+
notification-consumer:
|
|
39
|
+
url: https://notification-service-consumer.onrender.com
|
|
40
|
+
health: /
|
|
41
|
+
dependsOn: []
|
|
42
|
+
description: "Notification Event Consumer"
|
|
43
|
+
|
|
44
|
+
# Notification producer
|
|
45
|
+
notification-producer:
|
|
46
|
+
url: https://notification-service-producer.onrender.com
|
|
47
|
+
health: /health
|
|
48
|
+
dependsOn: []
|
|
49
|
+
description: "Notification Producer Service"
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ SERVICE ORCHESTRATOR ENGINE ║
|
|
4
|
+
* ║ Simplified wake logic using /health endpoint ║
|
|
5
|
+
* ║ Tracks last wake time and service readiness ║
|
|
6
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service state enumeration
|
|
13
|
+
*/
|
|
14
|
+
export const ServiceState = {
|
|
15
|
+
DEAD: 'dead', // Service is not responding (initial state)
|
|
16
|
+
WAKING: 'waking', // Health check response slow (> 5s threshold)
|
|
17
|
+
LIVE: 'live', // Health check responds with 200 OK quickly
|
|
18
|
+
FAILED: 'failed', // Health check failed/error
|
|
19
|
+
UNKNOWN: 'unknown', // State cannot be determined
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Orchestrator class - Manages service wake operations
|
|
24
|
+
* Uses simplified /health endpoint logic with timestamp tracking
|
|
25
|
+
*/
|
|
26
|
+
export class Orchestrator {
|
|
27
|
+
constructor(registry) {
|
|
28
|
+
this.registry = registry;
|
|
29
|
+
this.serviceStates = new Map(); // Track service states
|
|
30
|
+
this.lastWakeTime = new Map(); // Track when each service was last woken
|
|
31
|
+
this.requestTimestamps = new Map(); // Track request/response times
|
|
32
|
+
this.HEALTH_CHECK_TIMEOUT = 5000; // 5 second threshold before marking as "waking"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wake services with dependency ordering
|
|
37
|
+
* NEW LOGIC: Assume all services are dead initially
|
|
38
|
+
* Just call /health and track timestamps
|
|
39
|
+
*
|
|
40
|
+
* @param {string|Array<string>} target - Wake target (all, <service>, <group>)
|
|
41
|
+
* @param {string} environment - Environment name
|
|
42
|
+
* @param {Object} options - Wake options
|
|
43
|
+
* @param {boolean} options.wait - Wait for services to respond
|
|
44
|
+
* @param {number} options.timeout - Overall timeout in seconds
|
|
45
|
+
* @returns {Promise<Object>} Wake operation results
|
|
46
|
+
*/
|
|
47
|
+
async wake(target, environment = 'dev', options = {}) {
|
|
48
|
+
const { wait = true, timeout = 300 } = options;
|
|
49
|
+
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════
|
|
51
|
+
// RESOLVE WAKE ORDER
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════
|
|
53
|
+
let services;
|
|
54
|
+
try {
|
|
55
|
+
services = this.registry.resolveWakeOrder(target, environment);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: error.message,
|
|
60
|
+
services: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (services.length === 0) {
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
error: null,
|
|
68
|
+
services: [],
|
|
69
|
+
totalDuration: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════
|
|
74
|
+
// ASSUME ALL SERVICES ARE DEAD INITIALLY
|
|
75
|
+
// NEW: Instead of /wake endpoint, just check /health repeatedly
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════
|
|
77
|
+
const results = [];
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
|
|
80
|
+
for (const service of services) {
|
|
81
|
+
const serviceStartTime = Date.now();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// NEW: Mark service as being woken and initiate health check sequence
|
|
85
|
+
this._logTimestamp(service.name, 'Wake initiated');
|
|
86
|
+
|
|
87
|
+
// NEW: Set state to DEAD initially (always assume dead)
|
|
88
|
+
this._setServiceState(service.name, ServiceState.DEAD);
|
|
89
|
+
this._recordLastWakeTime(service.name);
|
|
90
|
+
|
|
91
|
+
// Check health status (handles timeout internally)
|
|
92
|
+
const status = await this._checkHealthWithTimeout(
|
|
93
|
+
service,
|
|
94
|
+
timeout
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const duration = Date.now() - serviceStartTime;
|
|
98
|
+
|
|
99
|
+
results.push({
|
|
100
|
+
name: service.name,
|
|
101
|
+
status,
|
|
102
|
+
url: service.url,
|
|
103
|
+
duration,
|
|
104
|
+
error: null,
|
|
105
|
+
lastWakeTime: this.lastWakeTime.get(service.name),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this._setServiceState(service.name, status);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const duration = Date.now() - serviceStartTime;
|
|
111
|
+
|
|
112
|
+
results.push({
|
|
113
|
+
name: service.name,
|
|
114
|
+
status: ServiceState.FAILED,
|
|
115
|
+
url: service.url,
|
|
116
|
+
duration,
|
|
117
|
+
error: error.message,
|
|
118
|
+
lastWakeTime: this.lastWakeTime.get(service.name),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this._setServiceState(service.name, ServiceState.FAILED);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const totalDuration = Date.now() - startTime;
|
|
126
|
+
const allLive = results.every(r => r.status === ServiceState.LIVE);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
success: allLive,
|
|
130
|
+
error: null,
|
|
131
|
+
services: results,
|
|
132
|
+
totalDuration,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get status of services
|
|
138
|
+
* Returns current state of services (always assumes dead initially)
|
|
139
|
+
*
|
|
140
|
+
* @param {string} serviceName - Optional specific service
|
|
141
|
+
* @param {string} environment - Environment name
|
|
142
|
+
* @returns {Promise<Object>} Status information
|
|
143
|
+
*/
|
|
144
|
+
async getStatus(serviceName, environment = 'dev') {
|
|
145
|
+
const services = serviceName
|
|
146
|
+
? [this.registry.getService(serviceName, environment)].filter(Boolean)
|
|
147
|
+
: this.registry.getServices(environment);
|
|
148
|
+
|
|
149
|
+
const statusResults = [];
|
|
150
|
+
|
|
151
|
+
for (const service of services) {
|
|
152
|
+
const status = await this._checkHealthWithTimeout(service, 5); // Quick check
|
|
153
|
+
|
|
154
|
+
statusResults.push({
|
|
155
|
+
name: service.name,
|
|
156
|
+
status,
|
|
157
|
+
url: service.url,
|
|
158
|
+
lastWakeTime: this.lastWakeTime.get(service.name) || null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
services: statusResults,
|
|
164
|
+
timestamp: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check health of services
|
|
170
|
+
* Performs health check without waking
|
|
171
|
+
*
|
|
172
|
+
* @param {string} serviceName - Optional specific service
|
|
173
|
+
* @param {string} environment - Environment name
|
|
174
|
+
* @returns {Promise<Object>} Health information
|
|
175
|
+
*/
|
|
176
|
+
async getHealth(serviceName, environment = 'dev') {
|
|
177
|
+
const services = serviceName
|
|
178
|
+
? [this.registry.getService(serviceName, environment)].filter(Boolean)
|
|
179
|
+
: this.registry.getServices(environment);
|
|
180
|
+
|
|
181
|
+
const healthResults = [];
|
|
182
|
+
|
|
183
|
+
for (const service of services) {
|
|
184
|
+
const health = await this._performHealthCheck(service);
|
|
185
|
+
healthResults.push({
|
|
186
|
+
name: service.name,
|
|
187
|
+
status: health.state,
|
|
188
|
+
url: service.url,
|
|
189
|
+
statusCode: health.statusCode,
|
|
190
|
+
responseTime: health.responseTime,
|
|
191
|
+
lastChecked: new Date().toISOString(),
|
|
192
|
+
lastWakeTime: this.lastWakeTime.get(service.name) || null,
|
|
193
|
+
error: health.error,
|
|
194
|
+
dependencies: service.dependsOn || [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
services: healthResults,
|
|
200
|
+
timestamp: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* REMOVED: _wakeService - No longer calls /wake endpoint
|
|
206
|
+
* NEW: Health-based checking with timeout threshold
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check health with timeout threshold
|
|
211
|
+
* NEW: If response takes > 5 seconds, mark as WAKING
|
|
212
|
+
* If responds quickly with 200, mark as LIVE
|
|
213
|
+
*
|
|
214
|
+
* @private
|
|
215
|
+
* @param {Object} service - Service definition
|
|
216
|
+
* @param {number} timeoutSeconds - Overall timeout
|
|
217
|
+
* @returns {Promise<string>} Service state
|
|
218
|
+
*/
|
|
219
|
+
async _checkHealthWithTimeout(service, timeoutSeconds = 5) {
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
const overallTimeoutMs = timeoutSeconds * 1000;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const health = await this._performHealthCheck(service);
|
|
225
|
+
const responseTime = Date.now() - startTime;
|
|
226
|
+
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════
|
|
228
|
+
// NEW LOGIC: Simple threshold-based state determination
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════
|
|
230
|
+
if (health.state === ServiceState.LIVE) {
|
|
231
|
+
this._logTimestamp(
|
|
232
|
+
service.name,
|
|
233
|
+
`Health check OK in ${responseTime}ms`
|
|
234
|
+
);
|
|
235
|
+
return ServiceState.LIVE;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If response is slow (> 5 seconds threshold), mark as waking
|
|
239
|
+
if (responseTime > this.HEALTH_CHECK_TIMEOUT) {
|
|
240
|
+
this._logTimestamp(
|
|
241
|
+
service.name,
|
|
242
|
+
`Health check slow (${responseTime}ms) - marking as waking`
|
|
243
|
+
);
|
|
244
|
+
return ServiceState.WAKING;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If it's dead but response came back (non-200), still mark as waking
|
|
248
|
+
if (health.statusCode === 503 || health.statusCode >= 500) {
|
|
249
|
+
this._logTimestamp(service.name, `Health returned ${health.statusCode} - marking as waking`);
|
|
250
|
+
return ServiceState.WAKING;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return health.state || ServiceState.UNKNOWN;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this._logTimestamp(service.name, `Health check error: ${error.message}`);
|
|
256
|
+
return ServiceState.FAILED;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Perform health check on service
|
|
262
|
+
* NEW: Simple /health endpoint call with timeout tracking
|
|
263
|
+
*
|
|
264
|
+
* @private
|
|
265
|
+
* @param {Object} service - Service definition
|
|
266
|
+
* @returns {Promise<Object>} Health check result
|
|
267
|
+
*/
|
|
268
|
+
async _performHealthCheck(service) {
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const healthUrl = new URL(service.health, service.url).toString();
|
|
273
|
+
|
|
274
|
+
this._logTimestamp(service.name, `Requesting ${service.health}`);
|
|
275
|
+
|
|
276
|
+
const response = await axios.get(healthUrl, {
|
|
277
|
+
timeout: 10000,
|
|
278
|
+
validateStatus: () => true,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const responseTime = Date.now() - startTime;
|
|
282
|
+
|
|
283
|
+
// ═══════════════════════════════════════════════════════════════
|
|
284
|
+
// DETERMINE STATE FROM STATUS CODE
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════
|
|
286
|
+
let state = ServiceState.UNKNOWN;
|
|
287
|
+
|
|
288
|
+
if (response.status < 300) {
|
|
289
|
+
state = ServiceState.LIVE; // Changed from READY
|
|
290
|
+
} else if (response.status === 503) {
|
|
291
|
+
state = ServiceState.DEAD; // Changed from SLEEPING
|
|
292
|
+
} else if (response.status >= 500) {
|
|
293
|
+
state = ServiceState.FAILED;
|
|
294
|
+
} else if (response.status >= 400) {
|
|
295
|
+
state = ServiceState.UNKNOWN;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._logTimestamp(
|
|
299
|
+
service.name,
|
|
300
|
+
`Responded ${response.status} in ${responseTime}ms`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
state,
|
|
305
|
+
statusCode: response.status,
|
|
306
|
+
responseTime,
|
|
307
|
+
error: null,
|
|
308
|
+
uptime: response.data?.uptime || null,
|
|
309
|
+
};
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const responseTime = Date.now() - startTime;
|
|
312
|
+
|
|
313
|
+
this._logTimestamp(
|
|
314
|
+
service.name,
|
|
315
|
+
`Health check failed: ${error.message}`
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
state: ServiceState.DEAD, // Changed: assume dead on error
|
|
320
|
+
statusCode: null,
|
|
321
|
+
responseTime,
|
|
322
|
+
error: error.message,
|
|
323
|
+
uptime: null,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Set service state
|
|
330
|
+
* @private
|
|
331
|
+
* @param {string} serviceName - Service name
|
|
332
|
+
* @param {string} state - New state
|
|
333
|
+
*/
|
|
334
|
+
_setServiceState(serviceName, state) {
|
|
335
|
+
this.serviceStates.set(serviceName, state);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Record the time a service was last woken
|
|
340
|
+
* NEW: Track when each service wake was initiated
|
|
341
|
+
*
|
|
342
|
+
* @private
|
|
343
|
+
* @param {string} serviceName - Service name
|
|
344
|
+
*/
|
|
345
|
+
_recordLastWakeTime(serviceName) {
|
|
346
|
+
const timestamp = new Date().toISOString();
|
|
347
|
+
this.lastWakeTime.set(serviceName, timestamp);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Log a timestamp for service event
|
|
352
|
+
* NEW: Detailed logging of request/response times
|
|
353
|
+
*
|
|
354
|
+
* @private
|
|
355
|
+
* @param {string} serviceName - Service name
|
|
356
|
+
* @param {string} message - Event message
|
|
357
|
+
*/
|
|
358
|
+
_logTimestamp(serviceName, message) {
|
|
359
|
+
const timestamp = new Date().toLocaleString('en-US', {
|
|
360
|
+
year: 'numeric',
|
|
361
|
+
month: '2-digit',
|
|
362
|
+
day: '2-digit',
|
|
363
|
+
hour: '2-digit',
|
|
364
|
+
minute: '2-digit',
|
|
365
|
+
second: '2-digit',
|
|
366
|
+
hour12: false,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const key = `${serviceName}:${Date.now()}`;
|
|
370
|
+
this.requestTimestamps.set(key, {
|
|
371
|
+
service: serviceName,
|
|
372
|
+
timestamp,
|
|
373
|
+
message,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
console.log(`[${timestamp}] ${serviceName}: ${message}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Wait for specified milliseconds
|
|
381
|
+
* @private
|
|
382
|
+
* @param {number} ms - Milliseconds to wait
|
|
383
|
+
*/
|
|
384
|
+
async _wait(ms) {
|
|
385
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clear service states and wake times
|
|
390
|
+
* @returns {void}
|
|
391
|
+
*/
|
|
392
|
+
clearStates() {
|
|
393
|
+
this.serviceStates.clear();
|
|
394
|
+
this.lastWakeTime.clear();
|
|
395
|
+
this.requestTimestamps.clear();
|
|
396
|
+
}
|
|
397
|
+
}
|