rabbitmq-sdk 0.0.1-security → 1.2.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.
Potentially problematic release.
This version of rabbitmq-sdk might be problematic. Click here for more details.
- package/.eslintrc.js +23 -0
- package/.kiro/specs/sdk-rabbitmq/design.md +369 -0
- package/.kiro/specs/sdk-rabbitmq/requirements.md +97 -0
- package/.kiro/specs/sdk-rabbitmq/tasks.md +248 -0
- package/README.md +273 -5
- package/bun.lock +790 -0
- package/config.example.json +13 -0
- package/dist/components/ConfigurationManager.d.ts +35 -0
- package/dist/components/ConfigurationManager.d.ts.map +1 -0
- package/dist/components/ConfigurationManager.js +118 -0
- package/dist/components/ConfigurationManager.js.map +1 -0
- package/dist/components/ConnectionManager.d.ts +93 -0
- package/dist/components/ConnectionManager.d.ts.map +1 -0
- package/dist/components/ConnectionManager.js +349 -0
- package/dist/components/ConnectionManager.js.map +1 -0
- package/dist/components/DLQHandler.d.ts +81 -0
- package/dist/components/DLQHandler.d.ts.map +1 -0
- package/dist/components/DLQHandler.js +228 -0
- package/dist/components/DLQHandler.js.map +1 -0
- package/dist/components/Logger.d.ts +77 -0
- package/dist/components/Logger.d.ts.map +1 -0
- package/dist/components/Logger.js +193 -0
- package/dist/components/Logger.js.map +1 -0
- package/dist/components/MessagePublisher.d.ts +49 -0
- package/dist/components/MessagePublisher.d.ts.map +1 -0
- package/dist/components/MessagePublisher.js +158 -0
- package/dist/components/MessagePublisher.js.map +1 -0
- package/dist/components/MessageSubscriber.d.ts +108 -0
- package/dist/components/MessageSubscriber.d.ts.map +1 -0
- package/dist/components/MessageSubscriber.js +503 -0
- package/dist/components/MessageSubscriber.js.map +1 -0
- package/dist/components/ResourceCreator.d.ts +89 -0
- package/dist/components/ResourceCreator.d.ts.map +1 -0
- package/dist/components/ResourceCreator.js +352 -0
- package/dist/components/ResourceCreator.js.map +1 -0
- package/dist/components/SdkRabbitmq.d.ts +103 -0
- package/dist/components/SdkRabbitmq.d.ts.map +1 -0
- package/dist/components/SdkRabbitmq.js +364 -0
- package/dist/components/SdkRabbitmq.js.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +20 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/IConfiguration.d.ts +35 -0
- package/dist/interfaces/IConfiguration.d.ts.map +1 -0
- package/dist/interfaces/IConfiguration.js +3 -0
- package/dist/interfaces/IConfiguration.js.map +1 -0
- package/dist/interfaces/IConnection.d.ts +21 -0
- package/dist/interfaces/IConnection.d.ts.map +1 -0
- package/dist/interfaces/IConnection.js +3 -0
- package/dist/interfaces/IConnection.js.map +1 -0
- package/dist/interfaces/IDLQ.d.ts +12 -0
- package/dist/interfaces/IDLQ.d.ts.map +1 -0
- package/dist/interfaces/IDLQ.js +3 -0
- package/dist/interfaces/IDLQ.js.map +1 -0
- package/dist/interfaces/IErrors.d.ts +33 -0
- package/dist/interfaces/IErrors.d.ts.map +1 -0
- package/dist/interfaces/IErrors.js +56 -0
- package/dist/interfaces/IErrors.js.map +1 -0
- package/dist/interfaces/ILogger.d.ts +14 -0
- package/dist/interfaces/ILogger.d.ts.map +1 -0
- package/dist/interfaces/ILogger.js +3 -0
- package/dist/interfaces/ILogger.js.map +1 -0
- package/dist/interfaces/IMessage.d.ts +52 -0
- package/dist/interfaces/IMessage.d.ts.map +1 -0
- package/dist/interfaces/IMessage.js +3 -0
- package/dist/interfaces/IMessage.js.map +1 -0
- package/dist/interfaces/IResource.d.ts +31 -0
- package/dist/interfaces/IResource.d.ts.map +1 -0
- package/dist/interfaces/IResource.js +3 -0
- package/dist/interfaces/IResource.js.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts +17 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.js +3 -0
- package/dist/interfaces/ISdkRabbitmq.js.map +1 -0
- package/dist/interfaces/index.d.ts +9 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +33 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/utils/configSchema.d.ts +8 -0
- package/dist/utils/configSchema.d.ts.map +1 -0
- package/dist/utils/configSchema.js +51 -0
- package/dist/utils/configSchema.js.map +1 -0
- package/docker-compose.yml +24 -0
- package/example.ts +65 -0
- package/examples/README-dynamic-routing.md +155 -0
- package/examples/bind-unbind-example.js +56 -0
- package/examples/test-chatbot-exchange.ts +83 -0
- package/examples/test-dynamic-routing-flow.js +299 -0
- package/examples/test-dynamic-routing-flow.ts +355 -0
- package/examples/test-no-disconnect.ts +0 -0
- package/examples/test-raw-rabbitmq.js +68 -0
- package/examples/test-same-channel.ts +81 -0
- package/examples/test-schedule-flow.ts +713 -0
- package/examples/test-simple-greeting.ts +66 -0
- package/examples/test-simple-schedule.ts +76 -0
- package/examples/test-wildcard.ts +364 -0
- package/jest.config.js +17 -0
- package/package.json +42 -4
- package/preinstall.js +1 -0
- package/prompts/test-dynamic-routing-flow.md +46 -0
- package/run.js +4 -0
- package/scripts/run-dynamic-routing-test.ts +31 -0
- package/src/.gitkeep +1 -0
- package/src/components/.gitkeep +1 -0
- package/src/components/ConfigurationManager.ts +104 -0
- package/src/components/ConnectionManager.ts +357 -0
- package/src/components/DLQHandler.ts +271 -0
- package/src/components/Logger.ts +224 -0
- package/src/components/MessagePublisher.ts +180 -0
- package/src/components/MessageSubscriber.ts +597 -0
- package/src/components/ResourceCreator.ts +411 -0
- package/src/components/SdkRabbitmq.ts +443 -0
- package/src/components/__tests__/ConfigurationManager.test.ts +357 -0
- package/src/components/__tests__/ConnectionManager.test.ts +387 -0
- package/src/components/__tests__/DLQHandler.test.ts +399 -0
- package/src/components/__tests__/Logger.test.ts +354 -0
- package/src/components/__tests__/MessagePublisher.test.ts +337 -0
- package/src/components/__tests__/MessageSubscriber.test.ts +542 -0
- package/src/components/__tests__/ResourceCreator.test.ts +465 -0
- package/src/components/__tests__/SdkRabbitmq.integration.test.ts +433 -0
- package/src/components/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/interfaces/.gitkeep +1 -0
- package/src/interfaces/IConfiguration.ts +38 -0
- package/src/interfaces/IConnection.ts +27 -0
- package/src/interfaces/IDLQ.ts +13 -0
- package/src/interfaces/IErrors.ts +53 -0
- package/src/interfaces/ILogger.ts +16 -0
- package/src/interfaces/IMessage.ts +65 -0
- package/src/interfaces/IResource.ts +35 -0
- package/src/interfaces/ISdkRabbitmq.ts +26 -0
- package/src/interfaces/index.ts +23 -0
- package/src/utils/.gitkeep +1 -0
- package/src/utils/configSchema.ts +58 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { IDLQHandler } from '../interfaces/IDLQ';
|
|
2
|
+
import { IConfiguration } from '../interfaces/IConfiguration';
|
|
3
|
+
import { ConnectionManager } from './ConnectionManager';
|
|
4
|
+
import { ResourceCreator } from './ResourceCreator';
|
|
5
|
+
import { Logger } from './Logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DLQHandler manages Dead Letter Queue functionality
|
|
9
|
+
* Handles DLQ setup, configuration, and failed message routing
|
|
10
|
+
*/
|
|
11
|
+
export class DLQHandler implements IDLQHandler {
|
|
12
|
+
private config: IConfiguration;
|
|
13
|
+
private connectionManager: ConnectionManager;
|
|
14
|
+
private resourceCreator: ResourceCreator;
|
|
15
|
+
private dlqSetupCache = new Set<string>();
|
|
16
|
+
private logger: Logger;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
config: IConfiguration,
|
|
20
|
+
connectionManager: ConnectionManager,
|
|
21
|
+
resourceCreator: ResourceCreator
|
|
22
|
+
) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.connectionManager = connectionManager;
|
|
25
|
+
this.resourceCreator = resourceCreator;
|
|
26
|
+
this.logger = Logger.createComponentLogger('DLQHandler', config.logging);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if DLQ is enabled in configuration
|
|
31
|
+
* @returns boolean indicating if DLQ is active
|
|
32
|
+
*/
|
|
33
|
+
public isEnabled(): boolean {
|
|
34
|
+
return this.config.dlq.active;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set up DLQ for a given original queue
|
|
39
|
+
* Creates DLQ exchange and queue following naming conventions
|
|
40
|
+
* @param originalQueue Original queue name
|
|
41
|
+
* @returns Promise<string> DLQ queue name
|
|
42
|
+
*/
|
|
43
|
+
public async setupDLQ(originalQueue: string): Promise<string> {
|
|
44
|
+
if (!originalQueue) {
|
|
45
|
+
throw new Error('Original queue name is required for DLQ setup');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!this.isEnabled()) {
|
|
49
|
+
throw new Error('DLQ is not enabled in configuration');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check cache to avoid duplicate setup
|
|
53
|
+
if (this.dlqSetupCache.has(originalQueue)) {
|
|
54
|
+
return this.getDLQQueueName(originalQueue);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dlqExchangeName = this.getDLQExchangeName(originalQueue);
|
|
58
|
+
const dlqQueueName = this.getDLQQueueName(originalQueue);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Create DLQ exchange
|
|
62
|
+
await this.resourceCreator.ensureExchange(dlqExchangeName, 'direct', {
|
|
63
|
+
durable: true,
|
|
64
|
+
autoDelete: false
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Create DLQ queue with TTL if configured
|
|
68
|
+
const queueOptions: any = {
|
|
69
|
+
durable: true,
|
|
70
|
+
exclusive: false,
|
|
71
|
+
autoDelete: false
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Add TTL if configured
|
|
75
|
+
if (this.config.dlq.ttl && this.config.dlq.ttl > 0) {
|
|
76
|
+
queueOptions.arguments = {
|
|
77
|
+
'x-message-ttl': this.config.dlq.ttl
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await this.resourceCreator.ensureQueue(dlqQueueName, queueOptions);
|
|
82
|
+
|
|
83
|
+
// Bind DLQ queue to DLQ exchange
|
|
84
|
+
await this.resourceCreator.bindQueue(dlqQueueName, dlqExchangeName, originalQueue);
|
|
85
|
+
|
|
86
|
+
// Cache successful setup
|
|
87
|
+
this.dlqSetupCache.add(originalQueue);
|
|
88
|
+
|
|
89
|
+
this.logger.logOperation('setupDLQ', `DLQ setup completed`, {
|
|
90
|
+
originalQueue,
|
|
91
|
+
dlqExchange: dlqExchangeName,
|
|
92
|
+
dlqQueue: dlqQueueName,
|
|
93
|
+
ttl: this.config.dlq.ttl
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return dlqQueueName;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.logger.logError(`Failed to setup DLQ`, error as Error, { originalQueue });
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handle a failed message by routing to DLQ
|
|
105
|
+
* @param message Failed message object
|
|
106
|
+
* @param originalQueue Original queue name where message failed
|
|
107
|
+
*/
|
|
108
|
+
public async handleFailedMessage(message: any, originalQueue: string): Promise<void> {
|
|
109
|
+
if (!originalQueue) {
|
|
110
|
+
throw new Error('Original queue name is required for failed message handling');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!message) {
|
|
114
|
+
throw new Error('Message is required for failed message handling');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!this.isEnabled()) {
|
|
118
|
+
this.logger.info(`DLQ is disabled. Acknowledging failed message without reprocessing`, {
|
|
119
|
+
originalQueue
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Ensure DLQ is set up for this queue
|
|
126
|
+
await this.setupDLQ(originalQueue);
|
|
127
|
+
|
|
128
|
+
const dlqExchangeName = this.getDLQExchangeName(originalQueue);
|
|
129
|
+
|
|
130
|
+
// Prepare message for DLQ with original metadata
|
|
131
|
+
const dlqMessage = this.prepareDLQMessage(message, originalQueue);
|
|
132
|
+
|
|
133
|
+
// Route message to DLQ
|
|
134
|
+
await this.routeMessageToDLQ(dlqExchangeName, originalQueue, dlqMessage);
|
|
135
|
+
|
|
136
|
+
this.logger.logOperation('routeToDLQ', `Failed message routed to DLQ`, {
|
|
137
|
+
originalQueue,
|
|
138
|
+
dlqExchange: dlqExchangeName
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.logger.logError(`Failed to handle failed message`, error as Error, { originalQueue });
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get DLQ exchange name following naming convention
|
|
148
|
+
* @param originalQueue Original queue name
|
|
149
|
+
* @returns DLQ exchange name (original-queue.dlq)
|
|
150
|
+
*/
|
|
151
|
+
private getDLQExchangeName(originalQueue: string): string {
|
|
152
|
+
return `${originalQueue}.dlq`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get DLQ queue name following naming convention
|
|
157
|
+
* @param originalQueue Original queue name
|
|
158
|
+
* @returns DLQ queue name (original-queue.dlq)
|
|
159
|
+
*/
|
|
160
|
+
private getDLQQueueName(originalQueue: string): string {
|
|
161
|
+
return `${originalQueue}.dlq`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Prepare message for DLQ with original metadata
|
|
166
|
+
* @param message Original message
|
|
167
|
+
* @param originalQueue Original queue name
|
|
168
|
+
* @returns Enhanced message with DLQ metadata
|
|
169
|
+
*/
|
|
170
|
+
private prepareDLQMessage(message: any, originalQueue: string): any {
|
|
171
|
+
const now = new Date();
|
|
172
|
+
|
|
173
|
+
// Extract original message content and metadata
|
|
174
|
+
const originalContent = message.content ? message.content : message;
|
|
175
|
+
const originalProperties = message.properties || {};
|
|
176
|
+
const originalFields = message.fields || {};
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
originalMessage: originalContent,
|
|
180
|
+
dlqMetadata: {
|
|
181
|
+
originalQueue: originalQueue,
|
|
182
|
+
originalExchange: originalFields.exchange || '',
|
|
183
|
+
originalRoutingKey: originalFields.routingKey || '',
|
|
184
|
+
failedAt: now.toISOString(),
|
|
185
|
+
retryCount: this.getRetryCount(originalProperties) + 1,
|
|
186
|
+
maxRetries: this.config.dlq.maxRetries || 3,
|
|
187
|
+
retryDelay: this.config.dlq.retryDelay || 5000
|
|
188
|
+
},
|
|
189
|
+
originalProperties: originalProperties,
|
|
190
|
+
originalFields: originalFields
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Route message to DLQ exchange
|
|
196
|
+
* @param dlqExchange DLQ exchange name
|
|
197
|
+
* @param routingKey Routing key (original queue name)
|
|
198
|
+
* @param message Message to route
|
|
199
|
+
*/
|
|
200
|
+
private async routeMessageToDLQ(dlqExchange: string, routingKey: string, message: any): Promise<void> {
|
|
201
|
+
return this.connectionManager.executeOperation(async () => {
|
|
202
|
+
const connection = this.connectionManager.getConnection();
|
|
203
|
+
if (!connection) {
|
|
204
|
+
throw new Error('No active connection to RabbitMQ');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const channel = await (connection as any).createChannel();
|
|
209
|
+
|
|
210
|
+
// Publish message to DLQ exchange
|
|
211
|
+
const messageBuffer = Buffer.from(JSON.stringify(message));
|
|
212
|
+
const publishOptions = {
|
|
213
|
+
persistent: true,
|
|
214
|
+
timestamp: Date.now(),
|
|
215
|
+
headers: {
|
|
216
|
+
'x-dlq-routed': true,
|
|
217
|
+
'x-original-queue': routingKey
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const published = channel.publish(dlqExchange, routingKey, messageBuffer, publishOptions);
|
|
222
|
+
|
|
223
|
+
if (!published) {
|
|
224
|
+
throw new Error('Failed to publish message to DLQ - channel buffer full');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await channel.close();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const errorMessage = `Failed to route message to DLQ exchange '${dlqExchange}': ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
230
|
+
throw new Error(errorMessage);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get retry count from message properties
|
|
237
|
+
* @param properties Message properties
|
|
238
|
+
* @returns Current retry count
|
|
239
|
+
*/
|
|
240
|
+
private getRetryCount(properties: any): number {
|
|
241
|
+
if (!properties || !properties.headers) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return properties.headers['x-retry-count'] || 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Clear DLQ setup cache (useful for testing)
|
|
250
|
+
*/
|
|
251
|
+
public clearCache(): void {
|
|
252
|
+
this.dlqSetupCache.clear();
|
|
253
|
+
this.logger.info('DLQHandler cache cleared');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get cache statistics for monitoring
|
|
258
|
+
*/
|
|
259
|
+
public getCacheStats(): { setupCache: number } {
|
|
260
|
+
return {
|
|
261
|
+
setupCache: this.dlqSetupCache.size
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get DLQ configuration
|
|
267
|
+
*/
|
|
268
|
+
public getDLQConfig(): IConfiguration['dlq'] {
|
|
269
|
+
return this.config.dlq;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ILogger } from '../interfaces/ILogger';
|
|
2
|
+
import { IConfiguration } from '../interfaces/IConfiguration';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Log levels with numeric values for comparison
|
|
6
|
+
*/
|
|
7
|
+
export enum LogLevel {
|
|
8
|
+
ERROR = 0,
|
|
9
|
+
WARN = 1,
|
|
10
|
+
INFO = 2,
|
|
11
|
+
DEBUG = 3
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Log entry structure for JSON formatting
|
|
16
|
+
*/
|
|
17
|
+
interface LogEntry {
|
|
18
|
+
timestamp: string;
|
|
19
|
+
level: string;
|
|
20
|
+
message: string;
|
|
21
|
+
context?: any;
|
|
22
|
+
component?: string;
|
|
23
|
+
operation?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Logger class that provides structured logging with configurable levels and formats
|
|
28
|
+
*/
|
|
29
|
+
export class Logger implements ILogger {
|
|
30
|
+
private static instance: Logger | null = null;
|
|
31
|
+
private logLevel: LogLevel;
|
|
32
|
+
private format: 'json' | 'text';
|
|
33
|
+
private component?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Private constructor to enforce singleton pattern
|
|
37
|
+
*/
|
|
38
|
+
private constructor(config?: IConfiguration['logging'], component?: string) {
|
|
39
|
+
this.logLevel = this.parseLogLevel(config?.level || 'info');
|
|
40
|
+
this.format = config?.format || 'text';
|
|
41
|
+
if (component) {
|
|
42
|
+
this.component = component;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get singleton instance of Logger
|
|
48
|
+
*/
|
|
49
|
+
public static getInstance(config?: IConfiguration['logging'], component?: string): Logger {
|
|
50
|
+
if (!Logger.instance) {
|
|
51
|
+
Logger.instance = new Logger(config, component);
|
|
52
|
+
}
|
|
53
|
+
return Logger.instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new logger instance for a specific component
|
|
58
|
+
*/
|
|
59
|
+
public static createComponentLogger(component: string, config?: IConfiguration['logging']): Logger {
|
|
60
|
+
return new Logger(config, component);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log error message with optional context
|
|
65
|
+
*/
|
|
66
|
+
public error(message: string, context?: any): void {
|
|
67
|
+
this.log(LogLevel.ERROR, 'ERROR', message, context);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Log warning message with optional context
|
|
72
|
+
*/
|
|
73
|
+
public warn(message: string, context?: any): void {
|
|
74
|
+
this.log(LogLevel.WARN, 'WARN', message, context);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Log info message with optional context
|
|
79
|
+
*/
|
|
80
|
+
public info(message: string, context?: any): void {
|
|
81
|
+
this.log(LogLevel.INFO, 'INFO', message, context);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Log debug message with optional context
|
|
86
|
+
*/
|
|
87
|
+
public debug(message: string, context?: any): void {
|
|
88
|
+
this.log(LogLevel.DEBUG, 'DEBUG', message, context);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Log operation with context (for operation tracking)
|
|
93
|
+
*/
|
|
94
|
+
public logOperation(operation: string, message: string, context?: any): void {
|
|
95
|
+
const operationContext = {
|
|
96
|
+
operation,
|
|
97
|
+
...context
|
|
98
|
+
};
|
|
99
|
+
this.info(message, operationContext);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Log error with stack trace
|
|
104
|
+
*/
|
|
105
|
+
public logError(message: string, error: Error, context?: any): void {
|
|
106
|
+
const errorContext = {
|
|
107
|
+
error: {
|
|
108
|
+
name: error.name,
|
|
109
|
+
message: error.message,
|
|
110
|
+
stack: error.stack
|
|
111
|
+
},
|
|
112
|
+
...context
|
|
113
|
+
};
|
|
114
|
+
this.error(message, errorContext);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Log connection state changes
|
|
119
|
+
*/
|
|
120
|
+
public logConnectionState(state: string, context?: any): void {
|
|
121
|
+
const connectionContext = {
|
|
122
|
+
connectionState: state,
|
|
123
|
+
...context
|
|
124
|
+
};
|
|
125
|
+
this.info(`Connection state changed to: ${state}`, connectionContext);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Internal logging method
|
|
130
|
+
*/
|
|
131
|
+
private log(level: LogLevel, levelName: string, message: string, context?: any): void {
|
|
132
|
+
// Check if this log level should be output
|
|
133
|
+
if (level > this.logLevel) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const timestamp = new Date().toISOString();
|
|
138
|
+
|
|
139
|
+
if (this.format === 'json') {
|
|
140
|
+
const logEntry: LogEntry = {
|
|
141
|
+
timestamp,
|
|
142
|
+
level: levelName,
|
|
143
|
+
message
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (context) {
|
|
147
|
+
try {
|
|
148
|
+
// Test if context can be serialized
|
|
149
|
+
JSON.stringify(context);
|
|
150
|
+
logEntry.context = context;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logEntry.context = '[Circular Reference]';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.component) {
|
|
157
|
+
logEntry.component = this.component;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(JSON.stringify(logEntry));
|
|
161
|
+
} else {
|
|
162
|
+
// Text format
|
|
163
|
+
const componentPrefix = this.component ? `[${this.component}] ` : '';
|
|
164
|
+
let contextSuffix = '';
|
|
165
|
+
if (context) {
|
|
166
|
+
try {
|
|
167
|
+
contextSuffix = ` | Context: ${JSON.stringify(context)}`;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
contextSuffix = ` | Context: [Circular Reference]`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const logMessage = `${timestamp} [${levelName}] ${componentPrefix}${message}${contextSuffix}`;
|
|
174
|
+
|
|
175
|
+
// Use appropriate console method based on level
|
|
176
|
+
switch (level) {
|
|
177
|
+
case LogLevel.ERROR:
|
|
178
|
+
console.error(logMessage);
|
|
179
|
+
break;
|
|
180
|
+
case LogLevel.WARN:
|
|
181
|
+
console.warn(logMessage);
|
|
182
|
+
break;
|
|
183
|
+
case LogLevel.DEBUG:
|
|
184
|
+
console.debug(logMessage);
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
console.log(logMessage);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse log level string to enum value
|
|
194
|
+
*/
|
|
195
|
+
private parseLogLevel(level: string): LogLevel {
|
|
196
|
+
switch (level.toLowerCase()) {
|
|
197
|
+
case 'error':
|
|
198
|
+
return LogLevel.ERROR;
|
|
199
|
+
case 'warn':
|
|
200
|
+
return LogLevel.WARN;
|
|
201
|
+
case 'info':
|
|
202
|
+
return LogLevel.INFO;
|
|
203
|
+
case 'debug':
|
|
204
|
+
return LogLevel.DEBUG;
|
|
205
|
+
default:
|
|
206
|
+
return LogLevel.INFO;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update logger configuration
|
|
212
|
+
*/
|
|
213
|
+
public updateConfig(config: IConfiguration['logging']): void {
|
|
214
|
+
this.logLevel = this.parseLogLevel(config?.level || 'info');
|
|
215
|
+
this.format = config?.format || 'text';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Reset singleton instance (for testing purposes)
|
|
220
|
+
*/
|
|
221
|
+
public static resetInstance(): void {
|
|
222
|
+
Logger.instance = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// amqplib types are used via ConnectionManager
|
|
2
|
+
import { IMessagePublisher } from '../interfaces/IMessage';
|
|
3
|
+
import { PublishError } from '../interfaces/IErrors';
|
|
4
|
+
import { ConnectionManager } from './ConnectionManager';
|
|
5
|
+
import { ResourceCreator } from './ResourceCreator';
|
|
6
|
+
import { Logger } from './Logger';
|
|
7
|
+
import { IConfiguration } from '../interfaces/IConfiguration';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MessagePublisher handles message publishing to RabbitMQ exchanges
|
|
11
|
+
* Implements parameter validation, JSON serialization, and automatic exchange creation
|
|
12
|
+
*/
|
|
13
|
+
export class MessagePublisher implements IMessagePublisher {
|
|
14
|
+
private connectionManager: ConnectionManager;
|
|
15
|
+
private resourceCreator: ResourceCreator;
|
|
16
|
+
private logger: Logger;
|
|
17
|
+
|
|
18
|
+
constructor(connectionManager: ConnectionManager, resourceCreator: ResourceCreator, config?: IConfiguration['logging']) {
|
|
19
|
+
this.connectionManager = connectionManager;
|
|
20
|
+
this.resourceCreator = resourceCreator;
|
|
21
|
+
this.logger = Logger.createComponentLogger('MessagePublisher', config);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Publish a message to an exchange
|
|
26
|
+
* @param exchange Exchange name (required)
|
|
27
|
+
* @param routingKey Routing key for message routing (required)
|
|
28
|
+
* @param payload Message payload to be JSON serialized (required)
|
|
29
|
+
* @returns Promise<boolean> - true for success, false for failure
|
|
30
|
+
*/
|
|
31
|
+
public async publish(exchange: string, routingKey: string, payload: any): Promise<boolean> {
|
|
32
|
+
// Parameter validation - all parameters are required
|
|
33
|
+
this.validatePublishParameters(exchange, routingKey, payload);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// JSON serialization for all payloads
|
|
37
|
+
const serializedPayload = this.serializePayload(payload);
|
|
38
|
+
|
|
39
|
+
// Automatic exchange creation before publishing
|
|
40
|
+
await this.ensureExchangeExists(exchange);
|
|
41
|
+
|
|
42
|
+
return await this.connectionManager.executeOperation(async () => {
|
|
43
|
+
const connection = this.connectionManager.getConnection();
|
|
44
|
+
if (!connection) {
|
|
45
|
+
throw new PublishError('No active connection to RabbitMQ', {
|
|
46
|
+
exchange,
|
|
47
|
+
routingKey,
|
|
48
|
+
payload: typeof payload
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const channel = await (connection as any).createChannel();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Publish message with JSON content type
|
|
56
|
+
const published = channel.publish(
|
|
57
|
+
exchange,
|
|
58
|
+
routingKey,
|
|
59
|
+
Buffer.from(serializedPayload),
|
|
60
|
+
{
|
|
61
|
+
contentType: 'application/json',
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
persistent: true // Make messages persistent by default
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await channel.close();
|
|
68
|
+
|
|
69
|
+
// Log successful publish
|
|
70
|
+
this.logger.logOperation('publish', `Message published successfully`, {
|
|
71
|
+
exchange,
|
|
72
|
+
routingKey,
|
|
73
|
+
payloadType: typeof payload,
|
|
74
|
+
payloadSize: serializedPayload.length
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return published;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
await channel.close();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Detailed error logging with context
|
|
85
|
+
this.logPublishError(error, exchange, routingKey, payload);
|
|
86
|
+
|
|
87
|
+
// Return false for failure as per requirements
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate publish method parameters
|
|
94
|
+
* @param exchange Exchange name
|
|
95
|
+
* @param routingKey Routing key
|
|
96
|
+
* @param payload Message payload
|
|
97
|
+
*/
|
|
98
|
+
private validatePublishParameters(exchange: string, routingKey: string, payload: any): void {
|
|
99
|
+
if (!exchange || typeof exchange !== 'string') {
|
|
100
|
+
throw new PublishError('Exchange parameter is required and must be a non-empty string', {
|
|
101
|
+
exchange,
|
|
102
|
+
routingKey,
|
|
103
|
+
payload: typeof payload
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!routingKey || typeof routingKey !== 'string') {
|
|
108
|
+
throw new PublishError('RoutingKey parameter is required and must be a non-empty string', {
|
|
109
|
+
exchange,
|
|
110
|
+
routingKey,
|
|
111
|
+
payload: typeof payload
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (payload === undefined || payload === null) {
|
|
116
|
+
throw new PublishError('Payload parameter is required and cannot be null or undefined', {
|
|
117
|
+
exchange,
|
|
118
|
+
routingKey,
|
|
119
|
+
payload
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Serialize payload to JSON string
|
|
126
|
+
* @param payload Payload to serialize
|
|
127
|
+
* @returns JSON string representation
|
|
128
|
+
*/
|
|
129
|
+
private serializePayload(payload: any): string {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.stringify(payload);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new PublishError('Failed to serialize payload to JSON', {
|
|
134
|
+
payload: typeof payload,
|
|
135
|
+
error: error instanceof Error ? error.message : 'Unknown serialization error'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Ensure exchange exists before publishing
|
|
142
|
+
* @param exchange Exchange name
|
|
143
|
+
*/
|
|
144
|
+
private async ensureExchangeExists(exchange: string): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
// Use ResourceCreator to automatically create exchange if it doesn't exist
|
|
147
|
+
// Always use topic type by default
|
|
148
|
+
await this.resourceCreator.ensureExchange(exchange, 'topic');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new PublishError(`Failed to ensure exchange '${exchange}' exists`, {
|
|
151
|
+
exchange,
|
|
152
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Log publish errors with detailed context
|
|
159
|
+
* @param error The error that occurred
|
|
160
|
+
* @param exchange Exchange name
|
|
161
|
+
* @param routingKey Routing key
|
|
162
|
+
* @param payload Original payload
|
|
163
|
+
*/
|
|
164
|
+
private logPublishError(error: unknown, exchange: string, routingKey: string, payload: any): void {
|
|
165
|
+
let payloadSize = 0;
|
|
166
|
+
try {
|
|
167
|
+
payloadSize = JSON.stringify(payload).length;
|
|
168
|
+
} catch {
|
|
169
|
+
payloadSize = -1; // Indicate serialization failed
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.logger.logError('Message publish failed', error as Error, {
|
|
173
|
+
exchange,
|
|
174
|
+
routingKey,
|
|
175
|
+
payloadType: typeof payload,
|
|
176
|
+
payloadSize,
|
|
177
|
+
operation: 'publish'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|