@onlineapps/service-wrapper 2.0.7 → 2.0.9
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/README.md +76 -38
- package/docs/API_STRUCTURE_STANDARD.md +132 -0
- package/docs/ARCHITECTURE_DECISION.md +200 -0
- package/docs/CONFIGURATION_GUIDE.md +261 -0
- package/docs/FINAL_ARCHITECTURE.md +271 -0
- package/docs/HANDLER_VS_HTTP_COMPARISON.md +269 -0
- package/docs/INSTALLATION_GUIDE.md +353 -0
- package/docs/OPERATIONS_SCHEMA.md +405 -0
- package/docs/SERVICE_TESTING_STANDARD.md +389 -0
- package/docs/WRAPPER_ARCHITECTURE.md +218 -0
- package/onlineapps-service-wrapper-2.0.8.tgz +0 -0
- package/package.json +4 -3
- package/src/ServiceWrapper.js +433 -270
- package/test/monitoring-integration.test.js +150 -0
package/src/ServiceWrapper.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @description Thin orchestration layer for microservices that handles all infrastructure concerns.
|
|
6
|
-
* Delegates all actual work to specialized connectors.
|
|
4
|
+
* Service Wrapper - Collection of connectors for business services
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Provides infrastructure components in single-process architecture:
|
|
7
|
+
* - MQ integration (RabbitMQ)
|
|
8
|
+
* - Service discovery (Registry)
|
|
9
|
+
* - Monitoring and metrics
|
|
10
|
+
* - Health checks
|
|
11
|
+
*
|
|
12
|
+
* Uses HTTP pattern exclusively - no direct handler calls
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
// Import connectors
|
|
14
16
|
const MQConnector = require('@onlineapps/conn-infra-mq');
|
|
15
17
|
const RegistryConnector = require('@onlineapps/conn-orch-registry');
|
|
16
|
-
const
|
|
18
|
+
const MonitoringConnector = require('@onlineapps/conn-base-monitoring');
|
|
17
19
|
const OrchestratorConnector = require('@onlineapps/conn-orch-orchestrator');
|
|
18
20
|
const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
|
|
19
21
|
const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
|
|
@@ -21,373 +23,534 @@ const CacheConnector = require('@onlineapps/conn-base-cache');
|
|
|
21
23
|
const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* ALL actual functionality is delegated to specialized connectors.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* const wrapper = new ServiceWrapper({
|
|
30
|
-
* service: expressApp,
|
|
31
|
-
* serviceName: 'hello-service',
|
|
32
|
-
* openApiSpec: require('./openapi.json'),
|
|
33
|
-
* config: {
|
|
34
|
-
* rabbitmq: process.env.RABBITMQ_URL,
|
|
35
|
-
* redis: process.env.REDIS_HOST,
|
|
36
|
-
* registry: process.env.REGISTRY_URL
|
|
37
|
-
* }
|
|
38
|
-
* });
|
|
39
|
-
*
|
|
40
|
-
* await wrapper.start();
|
|
26
|
+
* ServiceWrapper class
|
|
27
|
+
* Collection of connectors that enhance business service with infrastructure
|
|
41
28
|
*/
|
|
42
29
|
class ServiceWrapper {
|
|
43
30
|
/**
|
|
44
31
|
* Create a new ServiceWrapper instance
|
|
45
|
-
* @constructor
|
|
46
32
|
* @param {Object} options - Configuration options
|
|
47
|
-
* @param {Object}
|
|
48
|
-
* @param {
|
|
49
|
-
* @param {
|
|
50
|
-
* @param {Object} options.
|
|
51
|
-
* @param {Object} [options.config={}] - Infrastructure configuration
|
|
52
|
-
* @param {string} [options.config.rabbitmq] - RabbitMQ connection URL
|
|
53
|
-
* @param {string} [options.config.redis] - Redis connection string
|
|
54
|
-
* @param {string} [options.config.registry] - Registry service URL
|
|
55
|
-
* @param {number} [options.config.port=3000] - Service port (deprecated, use serviceUrl)
|
|
56
|
-
* @param {number} [options.config.prefetch=10] - MQ prefetch count
|
|
57
|
-
* @param {boolean} [options.config.directCall] - Use direct Express calls (default: auto-detect)
|
|
58
|
-
*
|
|
59
|
-
* @throws {Error} If required options are missing
|
|
33
|
+
* @param {Object} options.app - Express application instance
|
|
34
|
+
* @param {Object} options.server - HTTP server instance
|
|
35
|
+
* @param {Object} options.config - Service and wrapper configuration
|
|
36
|
+
* @param {Object} options.operations - Operations schema
|
|
60
37
|
*/
|
|
61
38
|
constructor(options = {}) {
|
|
62
39
|
this._validateOptions(options);
|
|
63
40
|
|
|
64
41
|
// Store configuration
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Initialize connectors
|
|
72
|
-
this.logger = new LoggerConnector({ serviceName: this.serviceName });
|
|
42
|
+
this.app = options.app;
|
|
43
|
+
this.server = options.server;
|
|
44
|
+
this.config = this._processConfig(options.config);
|
|
45
|
+
this.operations = options.operations;
|
|
46
|
+
|
|
47
|
+
// Initialize connector placeholders
|
|
73
48
|
this.mqClient = null;
|
|
74
49
|
this.registryClient = null;
|
|
75
50
|
this.orchestrator = null;
|
|
76
51
|
this.cacheConnector = null;
|
|
52
|
+
this.monitoring = null;
|
|
53
|
+
this.logger = null;
|
|
77
54
|
|
|
78
55
|
// State
|
|
79
|
-
this.
|
|
56
|
+
this.isInitialized = false;
|
|
80
57
|
}
|
|
81
58
|
|
|
82
59
|
/**
|
|
83
60
|
* Validate constructor options
|
|
84
61
|
* @private
|
|
85
|
-
* @param {Object} options - Options to validate
|
|
86
|
-
* @throws {Error} If required options are missing
|
|
87
62
|
*/
|
|
88
63
|
_validateOptions(options) {
|
|
89
|
-
if (!options.
|
|
90
|
-
throw new Error('
|
|
64
|
+
if (!options.app) {
|
|
65
|
+
throw new Error('Express app instance is required');
|
|
66
|
+
}
|
|
67
|
+
if (!options.server) {
|
|
68
|
+
throw new Error('HTTP server instance is required');
|
|
91
69
|
}
|
|
92
|
-
if (!options.
|
|
93
|
-
throw new Error('
|
|
70
|
+
if (!options.config) {
|
|
71
|
+
throw new Error('Configuration is required');
|
|
94
72
|
}
|
|
95
|
-
if (!options.
|
|
96
|
-
throw new Error('
|
|
73
|
+
if (!options.operations) {
|
|
74
|
+
throw new Error('Operations schema is required');
|
|
97
75
|
}
|
|
98
76
|
}
|
|
99
77
|
|
|
100
78
|
/**
|
|
101
|
-
*
|
|
102
|
-
* @
|
|
103
|
-
* @method start
|
|
104
|
-
* @returns {Promise<void>}
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* await wrapper.start();
|
|
108
|
-
* console.log('Service wrapper started');
|
|
79
|
+
* Process and validate configuration
|
|
80
|
+
* @private
|
|
109
81
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.logger.info(`Starting service wrapper for ${this.serviceName}`);
|
|
118
|
-
|
|
119
|
-
// 1. Initialize infrastructure connectors
|
|
120
|
-
await this._initializeConnectors();
|
|
121
|
-
|
|
122
|
-
// 2. Initialize cache connector if Redis is configured
|
|
123
|
-
if (this.config.redis) {
|
|
124
|
-
this.cacheConnector = new CacheConnector({
|
|
125
|
-
host: this.config.redis,
|
|
126
|
-
namespace: this.serviceName,
|
|
127
|
-
defaultTTL: this.config.cacheTTL || 300
|
|
128
|
-
});
|
|
129
|
-
await this.cacheConnector.connect();
|
|
130
|
-
this.logger.info('Cache connector initialized');
|
|
82
|
+
_processConfig(config) {
|
|
83
|
+
// Replace environment variables in config
|
|
84
|
+
const processValue = (value) => {
|
|
85
|
+
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
|
|
86
|
+
const envVar = value.slice(2, -1);
|
|
87
|
+
const [varName, defaultValue] = envVar.split(':');
|
|
88
|
+
return process.env[varName] || defaultValue || '';
|
|
131
89
|
}
|
|
90
|
+
return value;
|
|
91
|
+
};
|
|
132
92
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const serviceUrl = this.serviceUrl || `http://localhost:${this.config.port || 3000}`;
|
|
147
|
-
|
|
148
|
-
this.orchestrator = OrchestratorConnector.create({
|
|
149
|
-
mqClient: this.mqClient,
|
|
150
|
-
registryClient: this.registryClient,
|
|
151
|
-
apiMapper: ApiMapperConnector.create({
|
|
152
|
-
openApiSpec: this.openApiSpec,
|
|
153
|
-
serviceUrl: serviceUrl,
|
|
154
|
-
service: this.service,
|
|
155
|
-
directCall: useDirectCall,
|
|
156
|
-
logger: this.logger
|
|
157
|
-
}),
|
|
158
|
-
cookbook: CookbookConnector,
|
|
159
|
-
cache: this.cacheConnector,
|
|
160
|
-
errorHandler: errorHandler,
|
|
161
|
-
logger: this.logger,
|
|
162
|
-
defaultTimeout: this.config.defaultTimeout || 30000
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// 5. Register service with registry
|
|
166
|
-
await this._registerService();
|
|
167
|
-
|
|
168
|
-
// 6. Subscribe to workflow messages
|
|
169
|
-
await this._subscribeToQueues();
|
|
170
|
-
|
|
171
|
-
// 7. Start heartbeat
|
|
172
|
-
this._startHeartbeat();
|
|
173
|
-
|
|
174
|
-
this.isRunning = true;
|
|
175
|
-
this.logger.info(`Service wrapper started successfully for ${this.serviceName}`);
|
|
93
|
+
// Recursively process config object
|
|
94
|
+
const processObject = (obj) => {
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
97
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
98
|
+
result[key] = processObject(value);
|
|
99
|
+
} else {
|
|
100
|
+
result[key] = processValue(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
this.logger.error('Failed to start service wrapper', { error: error.message });
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
106
|
+
return processObject(config);
|
|
181
107
|
}
|
|
182
108
|
|
|
183
109
|
/**
|
|
184
|
-
*
|
|
110
|
+
* Initialize all wrapper components
|
|
185
111
|
* @async
|
|
186
|
-
* @method stop
|
|
187
|
-
* @returns {Promise<void>}
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* await wrapper.stop();
|
|
191
|
-
* console.log('Service wrapper stopped');
|
|
192
112
|
*/
|
|
193
|
-
async
|
|
194
|
-
if (
|
|
195
|
-
|
|
113
|
+
async initialize() {
|
|
114
|
+
if (this.isInitialized) {
|
|
115
|
+
console.warn('ServiceWrapper already initialized');
|
|
196
116
|
return;
|
|
197
117
|
}
|
|
198
118
|
|
|
199
119
|
try {
|
|
200
|
-
this.
|
|
120
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
121
|
+
console.log(`Initializing ServiceWrapper for ${serviceName}`);
|
|
201
122
|
|
|
202
|
-
//
|
|
203
|
-
if (this.
|
|
204
|
-
|
|
123
|
+
// 1. Initialize monitoring first (needed for logging)
|
|
124
|
+
if (this.config.wrapper?.monitoring?.enabled !== false) {
|
|
125
|
+
await this._initializeMonitoring();
|
|
205
126
|
}
|
|
206
127
|
|
|
207
|
-
//
|
|
208
|
-
if (this.
|
|
209
|
-
await this.
|
|
128
|
+
// 2. Initialize MQ connection
|
|
129
|
+
if (this.config.wrapper?.mq?.enabled !== false) {
|
|
130
|
+
await this._initializeMQ();
|
|
210
131
|
}
|
|
211
132
|
|
|
212
|
-
//
|
|
213
|
-
if (this.
|
|
214
|
-
await this.
|
|
133
|
+
// 3. Initialize service registry
|
|
134
|
+
if (this.config.wrapper?.registry?.enabled !== false) {
|
|
135
|
+
await this._initializeRegistry();
|
|
215
136
|
}
|
|
216
137
|
|
|
217
|
-
//
|
|
138
|
+
// 4. Initialize cache if configured
|
|
139
|
+
if (this.config.wrapper?.cache?.enabled === true) {
|
|
140
|
+
await this._initializeCache();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 5. Setup health checks
|
|
144
|
+
if (this.config.wrapper?.health?.enabled !== false) {
|
|
145
|
+
this._setupHealthChecks();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 6. Initialize orchestrator for workflow processing
|
|
218
149
|
if (this.mqClient) {
|
|
219
|
-
await this.
|
|
150
|
+
await this._initializeOrchestrator();
|
|
151
|
+
await this._subscribeToQueues();
|
|
220
152
|
}
|
|
221
153
|
|
|
222
|
-
this.
|
|
223
|
-
|
|
154
|
+
this.isInitialized = true;
|
|
155
|
+
console.log(`ServiceWrapper initialized successfully for ${serviceName}`);
|
|
224
156
|
|
|
225
157
|
} catch (error) {
|
|
226
|
-
|
|
158
|
+
console.error('Failed to initialize ServiceWrapper:', error);
|
|
227
159
|
throw error;
|
|
228
160
|
}
|
|
229
161
|
}
|
|
230
162
|
|
|
231
163
|
/**
|
|
232
|
-
* Initialize
|
|
164
|
+
* Initialize monitoring
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
async _initializeMonitoring() {
|
|
168
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
169
|
+
|
|
170
|
+
this.monitoring = MonitoringConnector;
|
|
171
|
+
await this.monitoring.init({
|
|
172
|
+
serviceName,
|
|
173
|
+
...this.config.wrapper?.monitoring
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.logger = this.monitoring;
|
|
177
|
+
console.log('Monitoring connector initialized');
|
|
178
|
+
|
|
179
|
+
// Add monitoring middleware to Express app
|
|
180
|
+
if (this.app) {
|
|
181
|
+
this.app.use((req, res, next) => {
|
|
182
|
+
const start = Date.now();
|
|
183
|
+
|
|
184
|
+
res.on('finish', () => {
|
|
185
|
+
const duration = Date.now() - start;
|
|
186
|
+
this.monitoring.info('Request processed', {
|
|
187
|
+
method: req.method,
|
|
188
|
+
path: req.path,
|
|
189
|
+
statusCode: res.statusCode,
|
|
190
|
+
duration
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
next();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Initialize MQ connection
|
|
233
201
|
* @private
|
|
234
|
-
* @async
|
|
235
|
-
* @returns {Promise<void>}
|
|
236
202
|
*/
|
|
237
|
-
async
|
|
238
|
-
|
|
239
|
-
|
|
203
|
+
async _initializeMQ() {
|
|
204
|
+
const mqUrl = this.config.wrapper?.mq?.url;
|
|
205
|
+
if (!mqUrl) {
|
|
206
|
+
throw new Error('MQ URL is required when MQ is enabled. Set wrapper.mq.url in config');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
240
210
|
|
|
241
211
|
this.mqClient = new MQConnector({
|
|
242
212
|
type: 'rabbitmq',
|
|
243
|
-
host:
|
|
244
|
-
queue: `${
|
|
245
|
-
prefetch: this.config.prefetch || 10,
|
|
213
|
+
host: mqUrl,
|
|
214
|
+
queue: `${serviceName}.workflow`,
|
|
215
|
+
prefetch: this.config.wrapper?.mq?.prefetch || 10,
|
|
246
216
|
durable: true,
|
|
247
217
|
noAck: false,
|
|
248
|
-
logger:
|
|
249
|
-
info: (...args) => this.logger.info(...args),
|
|
250
|
-
warn: (...args) => this.logger.warn(...args),
|
|
251
|
-
error: (...args) => this.logger.error(...args),
|
|
252
|
-
debug: (...args) => this.logger.debug(...args)
|
|
253
|
-
}
|
|
218
|
+
logger: this.logger || console
|
|
254
219
|
});
|
|
255
|
-
await this.mqClient.connect();
|
|
256
220
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
amqpUrl: rabbitUrl,
|
|
260
|
-
serviceName: this.serviceName,
|
|
261
|
-
version: this.openApiSpec?.info?.version || '1.0.0',
|
|
262
|
-
registryUrl: this.config.registry || process.env.REGISTRY_URL || 'http://localhost:4000',
|
|
263
|
-
logger: this.logger
|
|
264
|
-
});
|
|
221
|
+
await this.mqClient.connect();
|
|
222
|
+
console.log('MQ connector initialized');
|
|
265
223
|
}
|
|
266
224
|
|
|
267
225
|
/**
|
|
268
|
-
*
|
|
226
|
+
* Initialize service registry
|
|
269
227
|
* @private
|
|
270
|
-
* @async
|
|
271
|
-
* @returns {Promise<void>}
|
|
272
228
|
*/
|
|
273
|
-
async
|
|
274
|
-
const
|
|
229
|
+
async _initializeRegistry() {
|
|
230
|
+
const registryUrl = this.config.wrapper?.registry?.url;
|
|
231
|
+
if (!registryUrl) {
|
|
232
|
+
throw new Error('Registry URL is required when registry is enabled. Set wrapper.registry.url in config');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
236
|
+
const servicePort = this.config.service?.port || process.env.PORT || 3000;
|
|
237
|
+
const mqUrl = this.config.wrapper?.mq?.url || '';
|
|
238
|
+
|
|
239
|
+
this.registryClient = new RegistryConnector.ServiceRegistryClient({
|
|
240
|
+
amqpUrl: mqUrl,
|
|
241
|
+
serviceName: serviceName,
|
|
242
|
+
version: this.config.service?.version || '1.0.0',
|
|
243
|
+
registryUrl: registryUrl,
|
|
244
|
+
logger: this.logger || console
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Register service
|
|
275
248
|
const serviceInfo = {
|
|
276
|
-
name:
|
|
277
|
-
url:
|
|
278
|
-
|
|
249
|
+
name: serviceName,
|
|
250
|
+
url: `http://localhost:${servicePort}`,
|
|
251
|
+
operations: this.operations?.operations || {},
|
|
279
252
|
metadata: {
|
|
280
|
-
version: this.
|
|
281
|
-
description: this.
|
|
253
|
+
version: this.config.service?.version || '1.0.0',
|
|
254
|
+
description: this.config.service?.description || ''
|
|
282
255
|
}
|
|
283
256
|
};
|
|
284
257
|
|
|
285
258
|
await this.registryClient.register(serviceInfo);
|
|
286
|
-
|
|
259
|
+
console.log(`Service registered: ${serviceName}`);
|
|
260
|
+
|
|
261
|
+
// Start heartbeat
|
|
262
|
+
const heartbeatInterval = this.config.wrapper?.registry?.heartbeatInterval || 30000;
|
|
263
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
264
|
+
try {
|
|
265
|
+
await this.registryClient.sendHeartbeat(serviceName);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('Heartbeat failed:', error.message);
|
|
268
|
+
}
|
|
269
|
+
}, heartbeatInterval);
|
|
287
270
|
}
|
|
288
271
|
|
|
289
272
|
/**
|
|
290
|
-
*
|
|
273
|
+
* Initialize cache connector
|
|
291
274
|
* @private
|
|
292
|
-
* @async
|
|
293
|
-
* @returns {Promise<void>}
|
|
294
275
|
*/
|
|
295
|
-
async
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
async (rawMessage) => {
|
|
302
|
-
try {
|
|
303
|
-
// Parse message content if it's a buffer (AMQP message)
|
|
304
|
-
let message;
|
|
305
|
-
if (rawMessage && rawMessage.content) {
|
|
306
|
-
// This is an AMQP message with content buffer
|
|
307
|
-
const messageContent = rawMessage.content.toString();
|
|
308
|
-
try {
|
|
309
|
-
message = JSON.parse(messageContent);
|
|
310
|
-
} catch (parseError) {
|
|
311
|
-
this.logger.error('Failed to parse message content', {
|
|
312
|
-
error: parseError.message,
|
|
313
|
-
content: messageContent
|
|
314
|
-
});
|
|
315
|
-
throw parseError;
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
// Already parsed or direct message
|
|
319
|
-
message = rawMessage;
|
|
320
|
-
}
|
|
276
|
+
async _initializeCache() {
|
|
277
|
+
const cacheUrl = this.config.wrapper?.cache?.url;
|
|
278
|
+
if (!cacheUrl) {
|
|
279
|
+
console.warn('Cache enabled but no URL provided. Skipping cache initialization');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
321
282
|
|
|
322
|
-
|
|
323
|
-
this.logger.info('DEBUG: Received message structure', {
|
|
324
|
-
workflow_id: message.workflow_id,
|
|
325
|
-
has_cookbook: !!message.cookbook,
|
|
326
|
-
cookbook_name: message.cookbook?.name,
|
|
327
|
-
cookbook_steps: message.cookbook?.steps?.length,
|
|
328
|
-
first_step: message.cookbook?.steps?.[0],
|
|
329
|
-
current_step: message.current_step
|
|
330
|
-
});
|
|
283
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
331
284
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
285
|
+
this.cacheConnector = new CacheConnector({
|
|
286
|
+
host: cacheUrl,
|
|
287
|
+
namespace: serviceName,
|
|
288
|
+
defaultTTL: this.config.wrapper?.cache?.ttl || 300
|
|
289
|
+
});
|
|
337
290
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
result
|
|
342
|
-
});
|
|
291
|
+
await this.cacheConnector.connect();
|
|
292
|
+
console.log('Cache connector initialized');
|
|
293
|
+
}
|
|
343
294
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Setup health check endpoint
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
_setupHealthChecks() {
|
|
300
|
+
const healthEndpoint = this.config.wrapper?.health?.endpoint || '/health';
|
|
301
|
+
|
|
302
|
+
this.app.get(healthEndpoint, (req, res) => {
|
|
303
|
+
const health = {
|
|
304
|
+
status: 'healthy',
|
|
305
|
+
service: this.config.service?.name || 'unnamed-service',
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
components: {
|
|
308
|
+
http: 'healthy',
|
|
309
|
+
mq: this.mqClient?.isConnected() ? 'healthy' : 'unhealthy',
|
|
310
|
+
registry: this.registryClient ? 'healthy' : 'disabled',
|
|
311
|
+
cache: this.cacheConnector ? 'healthy' : 'disabled'
|
|
350
312
|
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Determine overall status
|
|
316
|
+
const statuses = Object.values(health.components);
|
|
317
|
+
if (statuses.includes('unhealthy')) {
|
|
318
|
+
health.status = 'unhealthy';
|
|
319
|
+
res.status(503);
|
|
320
|
+
} else {
|
|
321
|
+
res.status(200);
|
|
351
322
|
}
|
|
352
|
-
);
|
|
353
323
|
|
|
354
|
-
|
|
324
|
+
res.json(health);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log(`Health check endpoint registered at ${healthEndpoint}`);
|
|
355
328
|
}
|
|
356
329
|
|
|
357
330
|
/**
|
|
358
|
-
*
|
|
331
|
+
* Initialize orchestrator for workflow processing
|
|
359
332
|
* @private
|
|
360
333
|
*/
|
|
361
|
-
|
|
362
|
-
this.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
334
|
+
async _initializeOrchestrator() {
|
|
335
|
+
const servicePort = this.config.service?.port || process.env.PORT || 3000;
|
|
336
|
+
const serviceUrl = `http://localhost:${servicePort}`;
|
|
337
|
+
|
|
338
|
+
// Create error handler
|
|
339
|
+
const errorHandler = new ErrorHandlerConnector({
|
|
340
|
+
maxRetries: this.config.wrapper?.errorHandling?.maxRetries || 3,
|
|
341
|
+
retryDelay: this.config.wrapper?.errorHandling?.retryDelay || 1000,
|
|
342
|
+
logger: this.logger || console
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Create API mapper for HTTP calls (never direct calls)
|
|
346
|
+
const apiMapper = ApiMapperConnector.create({
|
|
347
|
+
openApiSpec: this.operations, // Using operations.json instead of OpenAPI
|
|
348
|
+
serviceUrl: serviceUrl,
|
|
349
|
+
directCall: false, // ALWAYS use HTTP pattern
|
|
350
|
+
logger: this.logger || console
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Create orchestrator
|
|
354
|
+
this.orchestrator = OrchestratorConnector.create({
|
|
355
|
+
mqClient: this.mqClient,
|
|
356
|
+
registryClient: this.registryClient,
|
|
357
|
+
apiMapper: apiMapper,
|
|
358
|
+
cookbook: CookbookConnector,
|
|
359
|
+
cache: this.cacheConnector,
|
|
360
|
+
errorHandler: errorHandler,
|
|
361
|
+
logger: this.logger || console,
|
|
362
|
+
defaultTimeout: this.config.wrapper?.timeout || 30000
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
console.log('Orchestrator initialized');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Subscribe to workflow queues
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
async _subscribeToQueues() {
|
|
373
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
374
|
+
|
|
375
|
+
// Subscribe to workflow.init queue (for all services)
|
|
376
|
+
await this.mqClient.consume('workflow.init', async (message) => {
|
|
377
|
+
await this._processWorkflowMessage(message, 'workflow.init');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Subscribe to service-specific queue
|
|
381
|
+
const serviceQueue = `${serviceName}.workflow`;
|
|
382
|
+
await this.mqClient.consume(serviceQueue, async (message) => {
|
|
383
|
+
await this._processWorkflowMessage(message, serviceQueue);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
console.log(`Subscribed to queues: workflow.init, ${serviceQueue}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Process workflow message
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
async _processWorkflowMessage(rawMessage, queueName) {
|
|
394
|
+
try {
|
|
395
|
+
// Parse message
|
|
396
|
+
let message;
|
|
397
|
+
if (rawMessage && rawMessage.content) {
|
|
398
|
+
// AMQP message with content buffer
|
|
399
|
+
const messageContent = rawMessage.content.toString();
|
|
400
|
+
message = JSON.parse(messageContent);
|
|
401
|
+
} else {
|
|
402
|
+
// Already parsed message
|
|
403
|
+
message = rawMessage;
|
|
368
404
|
}
|
|
369
|
-
|
|
405
|
+
|
|
406
|
+
console.log(`Processing message from ${queueName}:`, {
|
|
407
|
+
workflow_id: message.workflow_id,
|
|
408
|
+
step: message.step?.operation || message.operation
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Check if this message is for our service
|
|
412
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
413
|
+
if (message.step?.service && message.step.service !== serviceName) {
|
|
414
|
+
console.log(`Message not for this service (target: ${message.step.service})`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Process based on message type
|
|
419
|
+
if (message.operation && this.operations?.operations?.[message.operation]) {
|
|
420
|
+
// Direct operation call
|
|
421
|
+
const result = await this._executeOperation(message.operation, message.input || {});
|
|
422
|
+
return result;
|
|
423
|
+
} else if (message.step?.operation && this.operations?.operations?.[message.step.operation]) {
|
|
424
|
+
// Workflow step
|
|
425
|
+
const result = await this._executeOperation(message.step.operation, message.input || {});
|
|
426
|
+
return result;
|
|
427
|
+
} else if (this.orchestrator) {
|
|
428
|
+
// Delegate to orchestrator for complex workflow processing
|
|
429
|
+
const result = await this.orchestrator.processWorkflowMessage(message, serviceName);
|
|
430
|
+
return result;
|
|
431
|
+
} else {
|
|
432
|
+
throw new Error(`Unknown message format or operation: ${JSON.stringify(message)}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(`Error processing message from ${queueName}:`, error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Execute operation by calling HTTP endpoint
|
|
443
|
+
* @private
|
|
444
|
+
*/
|
|
445
|
+
async _executeOperation(operationName, input) {
|
|
446
|
+
const operation = this.operations?.operations?.[operationName];
|
|
447
|
+
if (!operation) {
|
|
448
|
+
throw new Error(`Unknown operation: ${operationName}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const servicePort = this.config.service?.port || process.env.PORT || 3000;
|
|
452
|
+
const url = `http://localhost:${servicePort}${operation.endpoint}`;
|
|
453
|
+
const method = operation.method || 'POST';
|
|
454
|
+
|
|
455
|
+
console.log(`Executing operation ${operationName} via ${method} ${url}`);
|
|
456
|
+
|
|
457
|
+
// Make HTTP request using fetch (Node.js 18+) or http module
|
|
458
|
+
const http = require('http');
|
|
459
|
+
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
const postData = JSON.stringify(input);
|
|
462
|
+
|
|
463
|
+
const options = {
|
|
464
|
+
hostname: 'localhost',
|
|
465
|
+
port: servicePort,
|
|
466
|
+
path: operation.endpoint,
|
|
467
|
+
method: method,
|
|
468
|
+
headers: {
|
|
469
|
+
'Content-Type': 'application/json',
|
|
470
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const req = http.request(options, (res) => {
|
|
475
|
+
let data = '';
|
|
476
|
+
|
|
477
|
+
res.on('data', (chunk) => {
|
|
478
|
+
data += chunk;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
res.on('end', () => {
|
|
482
|
+
try {
|
|
483
|
+
const result = JSON.parse(data);
|
|
484
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
485
|
+
resolve(result);
|
|
486
|
+
} else {
|
|
487
|
+
reject(new Error(`Operation failed: ${result.error || data}`));
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
reject(new Error(`Invalid response: ${data}`));
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
req.on('error', (error) => {
|
|
496
|
+
reject(error);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
req.write(postData);
|
|
500
|
+
req.end();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Shutdown wrapper and cleanup resources
|
|
506
|
+
* @async
|
|
507
|
+
*/
|
|
508
|
+
async shutdown() {
|
|
509
|
+
const serviceName = this.config.service?.name || 'unnamed-service';
|
|
510
|
+
console.log(`Shutting down ServiceWrapper for ${serviceName}`);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// Stop heartbeat
|
|
514
|
+
if (this.heartbeatTimer) {
|
|
515
|
+
clearInterval(this.heartbeatTimer);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Unregister from registry
|
|
519
|
+
if (this.registryClient) {
|
|
520
|
+
await this.registryClient.unregister(serviceName);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Disconnect from cache
|
|
524
|
+
if (this.cacheConnector) {
|
|
525
|
+
await this.cacheConnector.disconnect();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Disconnect from MQ
|
|
529
|
+
if (this.mqClient) {
|
|
530
|
+
await this.mqClient.disconnect();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.isInitialized = false;
|
|
534
|
+
console.log('ServiceWrapper shutdown complete');
|
|
535
|
+
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error('Error during shutdown:', error);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
370
540
|
}
|
|
371
541
|
|
|
372
542
|
/**
|
|
373
543
|
* Get wrapper status
|
|
374
|
-
* @method getStatus
|
|
375
|
-
* @returns {Object} Status information
|
|
376
|
-
*
|
|
377
|
-
* @example
|
|
378
|
-
* const status = wrapper.getStatus();
|
|
379
|
-
* console.log('Wrapper status:', status);
|
|
380
544
|
*/
|
|
381
545
|
getStatus() {
|
|
382
546
|
return {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
heartbeatInterval: this.config.heartbeatInterval || 30000
|
|
547
|
+
initialized: this.isInitialized,
|
|
548
|
+
service: this.config.service?.name || 'unnamed-service',
|
|
549
|
+
components: {
|
|
550
|
+
mq: this.mqClient?.isConnected() || false,
|
|
551
|
+
registry: !!this.registryClient,
|
|
552
|
+
cache: !!this.cacheConnector,
|
|
553
|
+
monitoring: !!this.monitoring
|
|
391
554
|
}
|
|
392
555
|
};
|
|
393
556
|
}
|