navis.js 2.0.0 → 3.0.1
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/LICENSE +1 -1
- package/README.md +69 -10
- package/bin/navis.js +36 -0
- package/examples/v3-features-demo.js +226 -0
- package/package.json +2 -2
- package/src/index.js +20 -0
- package/src/messaging/base-messaging.js +82 -0
- package/src/messaging/kafka-adapter.js +152 -0
- package/src/messaging/nats-adapter.js +141 -0
- package/src/messaging/sqs-adapter.js +175 -0
- package/src/observability/logger.js +141 -0
- package/src/observability/metrics.js +193 -0
- package/src/observability/tracer.js +205 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS Messaging Adapter
|
|
3
|
+
* v3: NATS integration for async messaging
|
|
4
|
+
*
|
|
5
|
+
* Note: Requires nats to be installed
|
|
6
|
+
* npm install nats
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BaseMessaging = require('./base-messaging');
|
|
10
|
+
|
|
11
|
+
class NATSMessaging extends BaseMessaging {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.servers = options.servers || ['nats://localhost:4222'];
|
|
15
|
+
this.nc = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect to NATS
|
|
20
|
+
*/
|
|
21
|
+
async connect() {
|
|
22
|
+
try {
|
|
23
|
+
const { connect } = require('nats');
|
|
24
|
+
this.connectNATS = connect;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error('nats is required. Install it with: npm install nats');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.nc = await this.connectNATS({
|
|
30
|
+
servers: this.servers,
|
|
31
|
+
...this.options.natsConfig,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.isConnected = true;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Disconnect from NATS
|
|
40
|
+
*/
|
|
41
|
+
async disconnect() {
|
|
42
|
+
this.isConnected = false;
|
|
43
|
+
if (this.nc) {
|
|
44
|
+
await this.nc.close();
|
|
45
|
+
this.nc = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Publish message to NATS subject
|
|
51
|
+
* @param {string} subject - NATS subject
|
|
52
|
+
* @param {Object} message - Message payload
|
|
53
|
+
* @param {Object} options - Publishing options
|
|
54
|
+
*/
|
|
55
|
+
async publish(subject, message, options = {}) {
|
|
56
|
+
if (!this.isConnected) {
|
|
57
|
+
await this.connect();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = Buffer.from(JSON.stringify(message));
|
|
61
|
+
this.nc.publish(subject, data);
|
|
62
|
+
|
|
63
|
+
return { subject, published: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subscribe to NATS subject
|
|
68
|
+
* @param {string} subject - NATS subject
|
|
69
|
+
* @param {Function} handler - Message handler function
|
|
70
|
+
* @param {Object} options - Subscription options
|
|
71
|
+
*/
|
|
72
|
+
async subscribe(subject, handler, options = {}) {
|
|
73
|
+
if (!this.isConnected) {
|
|
74
|
+
await this.connect();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._registerHandler(subject, handler);
|
|
78
|
+
|
|
79
|
+
const subscription = this.nc.subscribe(subject, {
|
|
80
|
+
queue: options.queue || undefined,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
(async () => {
|
|
84
|
+
for await (const msg of subscription) {
|
|
85
|
+
try {
|
|
86
|
+
const body = JSON.parse(msg.data.toString());
|
|
87
|
+
const handlers = this._getHandlers(subject);
|
|
88
|
+
|
|
89
|
+
for (const h of handlers) {
|
|
90
|
+
await h(body, {
|
|
91
|
+
subject: msg.subject,
|
|
92
|
+
reply: msg.reply,
|
|
93
|
+
sid: msg.sid,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error processing NATS message:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})().catch(console.error);
|
|
101
|
+
|
|
102
|
+
return { subject, handler, subscription };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Unsubscribe from subject
|
|
107
|
+
*/
|
|
108
|
+
async unsubscribe(subject, handler = null) {
|
|
109
|
+
if (handler) {
|
|
110
|
+
const handlers = this._getHandlers(subject);
|
|
111
|
+
const index = handlers.indexOf(handler);
|
|
112
|
+
if (index > -1) {
|
|
113
|
+
handlers.splice(index, 1);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.subscribers.delete(subject);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Request-reply pattern
|
|
122
|
+
* @param {string} subject - NATS subject
|
|
123
|
+
* @param {Object} message - Request message
|
|
124
|
+
* @param {Object} options - Request options
|
|
125
|
+
*/
|
|
126
|
+
async request(subject, message, options = {}) {
|
|
127
|
+
if (!this.isConnected) {
|
|
128
|
+
await this.connect();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = Buffer.from(JSON.stringify(message));
|
|
132
|
+
const response = await this.nc.request(subject, data, {
|
|
133
|
+
timeout: options.timeout || 5000,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return JSON.parse(response.data.toString());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = NATSMessaging;
|
|
141
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS SQS Messaging Adapter
|
|
3
|
+
* v3: SQS integration for async messaging
|
|
4
|
+
*
|
|
5
|
+
* Note: Requires @aws-sdk/client-sqs to be installed
|
|
6
|
+
* npm install @aws-sdk/client-sqs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BaseMessaging = require('./base-messaging');
|
|
10
|
+
|
|
11
|
+
class SQSMessaging extends BaseMessaging {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.region = options.region || process.env.AWS_REGION || 'us-east-1';
|
|
15
|
+
this.queueUrl = options.queueUrl;
|
|
16
|
+
this.sqsClient = null;
|
|
17
|
+
|
|
18
|
+
// Try to load AWS SDK (optional dependency)
|
|
19
|
+
try {
|
|
20
|
+
const { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } = require('@aws-sdk/client-sqs');
|
|
21
|
+
this.SQSClient = SQSClient;
|
|
22
|
+
this.SendMessageCommand = SendMessageCommand;
|
|
23
|
+
this.ReceiveMessageCommand = ReceiveMessageCommand;
|
|
24
|
+
this.DeleteMessageCommand = DeleteMessageCommand;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// AWS SDK not installed - will throw error on connect
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connect to SQS
|
|
32
|
+
*/
|
|
33
|
+
async connect() {
|
|
34
|
+
if (!this.SQSClient) {
|
|
35
|
+
throw new Error('@aws-sdk/client-sqs is required. Install it with: npm install @aws-sdk/client-sqs');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.sqsClient = new this.SQSClient({
|
|
39
|
+
region: this.region,
|
|
40
|
+
...this.options.awsConfig,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.isConnected = true;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Disconnect from SQS
|
|
49
|
+
*/
|
|
50
|
+
async disconnect() {
|
|
51
|
+
this.isConnected = false;
|
|
52
|
+
if (this.sqsClient) {
|
|
53
|
+
// SQS client doesn't need explicit disconnect
|
|
54
|
+
this.sqsClient = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Publish message to SQS queue
|
|
60
|
+
* @param {string} queueUrl - SQS queue URL
|
|
61
|
+
* @param {Object} message - Message payload
|
|
62
|
+
* @param {Object} options - Publishing options
|
|
63
|
+
*/
|
|
64
|
+
async publish(queueUrl, message, options = {}) {
|
|
65
|
+
if (!this.isConnected) {
|
|
66
|
+
await this.connect();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const command = new this.SendMessageCommand({
|
|
70
|
+
QueueUrl: queueUrl || this.queueUrl,
|
|
71
|
+
MessageBody: JSON.stringify(message),
|
|
72
|
+
MessageAttributes: options.attributes || {},
|
|
73
|
+
DelaySeconds: options.delaySeconds || 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const response = await this.sqsClient.send(command);
|
|
77
|
+
return {
|
|
78
|
+
messageId: response.MessageId,
|
|
79
|
+
md5OfBody: response.MD5OfMessageBody,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe to SQS queue (long polling)
|
|
85
|
+
* @param {string} queueUrl - SQS queue URL
|
|
86
|
+
* @param {Function} handler - Message handler function
|
|
87
|
+
* @param {Object} options - Subscription options
|
|
88
|
+
*/
|
|
89
|
+
async subscribe(queueUrl, handler, options = {}) {
|
|
90
|
+
if (!this.isConnected) {
|
|
91
|
+
await this.connect();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const targetQueueUrl = queueUrl || this.queueUrl;
|
|
95
|
+
this._registerHandler(targetQueueUrl, handler);
|
|
96
|
+
|
|
97
|
+
// Start polling
|
|
98
|
+
const poll = async () => {
|
|
99
|
+
if (!this.isConnected) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const command = new this.ReceiveMessageCommand({
|
|
105
|
+
QueueUrl: targetQueueUrl,
|
|
106
|
+
MaxNumberOfMessages: options.maxMessages || 1,
|
|
107
|
+
WaitTimeSeconds: options.waitTimeSeconds || 20, // Long polling
|
|
108
|
+
VisibilityTimeout: options.visibilityTimeout || 30,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const response = await this.sqsClient.send(command);
|
|
112
|
+
|
|
113
|
+
if (response.Messages && response.Messages.length > 0) {
|
|
114
|
+
for (const message of response.Messages) {
|
|
115
|
+
try {
|
|
116
|
+
const body = JSON.parse(message.Body);
|
|
117
|
+
const handlers = this._getHandlers(targetQueueUrl);
|
|
118
|
+
|
|
119
|
+
for (const h of handlers) {
|
|
120
|
+
await h(body, {
|
|
121
|
+
messageId: message.MessageId,
|
|
122
|
+
receiptHandle: message.ReceiptHandle,
|
|
123
|
+
attributes: message.Attributes,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Delete message after successful processing
|
|
128
|
+
if (options.autoDelete !== false) {
|
|
129
|
+
await this.sqsClient.send(new this.DeleteMessageCommand({
|
|
130
|
+
QueueUrl: targetQueueUrl,
|
|
131
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('Error processing SQS message:', error);
|
|
136
|
+
// Message will become visible again after visibility timeout
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error receiving SQS messages:', error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Continue polling
|
|
145
|
+
if (this.isConnected) {
|
|
146
|
+
setTimeout(poll, options.pollInterval || 0);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Start polling
|
|
151
|
+
poll();
|
|
152
|
+
|
|
153
|
+
return { queueUrl: targetQueueUrl, handler };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Unsubscribe from queue
|
|
158
|
+
*/
|
|
159
|
+
async unsubscribe(queueUrl, handler = null) {
|
|
160
|
+
const targetQueueUrl = queueUrl || this.queueUrl;
|
|
161
|
+
|
|
162
|
+
if (handler) {
|
|
163
|
+
const handlers = this._getHandlers(targetQueueUrl);
|
|
164
|
+
const index = handlers.indexOf(handler);
|
|
165
|
+
if (index > -1) {
|
|
166
|
+
handlers.splice(index, 1);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
this.subscribers.delete(targetQueueUrl);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = SQSMessaging;
|
|
175
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger
|
|
3
|
+
* v3: Logging with levels and structured output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LOG_LEVELS = {
|
|
7
|
+
DEBUG: 0,
|
|
8
|
+
INFO: 1,
|
|
9
|
+
WARN: 2,
|
|
10
|
+
ERROR: 3,
|
|
11
|
+
FATAL: 4,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class Logger {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
const level = options.level || process.env.LOG_LEVEL || 'INFO';
|
|
17
|
+
this.level = level;
|
|
18
|
+
this.levelValue = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
|
|
19
|
+
this.format = options.format || 'json'; // 'json' or 'text'
|
|
20
|
+
this.enableColors = options.enableColors !== false;
|
|
21
|
+
this.context = options.context || {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log a message
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
_log(level, message, data = {}) {
|
|
29
|
+
const levelValue = LOG_LEVELS[level];
|
|
30
|
+
if (levelValue < this.levelValue) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const logEntry = {
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
level,
|
|
37
|
+
message,
|
|
38
|
+
...this.context,
|
|
39
|
+
...data,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (this.format === 'json') {
|
|
43
|
+
console.log(JSON.stringify(logEntry));
|
|
44
|
+
} else {
|
|
45
|
+
const color = this._getColor(level);
|
|
46
|
+
const reset = this.enableColors ? '\x1b[0m' : '';
|
|
47
|
+
const coloredLevel = this.enableColors ? `${color}${level}${reset}` : level;
|
|
48
|
+
console.log(`[${logEntry.timestamp}] ${coloredLevel}: ${message}`, data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get color for log level
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
_getColor(level) {
|
|
57
|
+
if (!this.enableColors) return '';
|
|
58
|
+
|
|
59
|
+
const colors = {
|
|
60
|
+
DEBUG: '\x1b[36m', // Cyan
|
|
61
|
+
INFO: '\x1b[32m', // Green
|
|
62
|
+
WARN: '\x1b[33m', // Yellow
|
|
63
|
+
ERROR: '\x1b[31m', // Red
|
|
64
|
+
FATAL: '\x1b[35m', // Magenta
|
|
65
|
+
};
|
|
66
|
+
return colors[level] || '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Debug log
|
|
71
|
+
*/
|
|
72
|
+
debug(message, data = {}) {
|
|
73
|
+
this._log('DEBUG', message, data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Info log
|
|
78
|
+
*/
|
|
79
|
+
info(message, data = {}) {
|
|
80
|
+
this._log('INFO', message, data);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Warning log
|
|
85
|
+
*/
|
|
86
|
+
warn(message, data = {}) {
|
|
87
|
+
this._log('WARN', message, data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Error log
|
|
92
|
+
*/
|
|
93
|
+
error(message, error = null, data = {}) {
|
|
94
|
+
const errorData = error ? {
|
|
95
|
+
error: {
|
|
96
|
+
message: error.message,
|
|
97
|
+
stack: error.stack,
|
|
98
|
+
name: error.name,
|
|
99
|
+
},
|
|
100
|
+
} : {};
|
|
101
|
+
this._log('ERROR', message, { ...errorData, ...data });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fatal log
|
|
106
|
+
*/
|
|
107
|
+
fatal(message, error = null, data = {}) {
|
|
108
|
+
const errorData = error ? {
|
|
109
|
+
error: {
|
|
110
|
+
message: error.message,
|
|
111
|
+
stack: error.stack,
|
|
112
|
+
name: error.name,
|
|
113
|
+
},
|
|
114
|
+
} : {};
|
|
115
|
+
this._log('FATAL', message, { ...errorData, ...data });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create child logger with additional context
|
|
120
|
+
*/
|
|
121
|
+
child(context) {
|
|
122
|
+
return new Logger({
|
|
123
|
+
level: this.level,
|
|
124
|
+
format: this.format,
|
|
125
|
+
enableColors: this.enableColors,
|
|
126
|
+
context: { ...this.context, ...context },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set log level
|
|
132
|
+
*/
|
|
133
|
+
setLevel(level) {
|
|
134
|
+
const upperLevel = level.toUpperCase();
|
|
135
|
+
this.level = upperLevel;
|
|
136
|
+
this.levelValue = LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = Logger;
|
|
141
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector
|
|
3
|
+
* v3: Collect and expose application metrics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Metrics {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.metrics = {
|
|
9
|
+
counters: new Map(),
|
|
10
|
+
gauges: new Map(),
|
|
11
|
+
histograms: new Map(),
|
|
12
|
+
};
|
|
13
|
+
this.enabled = options.enabled !== false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Increment a counter
|
|
18
|
+
* @param {string} name - Metric name
|
|
19
|
+
* @param {number} value - Increment value (default: 1)
|
|
20
|
+
* @param {Object} labels - Metric labels
|
|
21
|
+
*/
|
|
22
|
+
increment(name, value = 1, labels = {}) {
|
|
23
|
+
if (!this.enabled) return;
|
|
24
|
+
|
|
25
|
+
const key = this._getKey(name, labels);
|
|
26
|
+
const current = this.metrics.counters.get(key) || 0;
|
|
27
|
+
this.metrics.counters.set(key, current + value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Decrement a counter
|
|
32
|
+
* @param {string} name - Metric name
|
|
33
|
+
* @param {number} value - Decrement value (default: 1)
|
|
34
|
+
* @param {Object} labels - Metric labels
|
|
35
|
+
*/
|
|
36
|
+
decrement(name, value = 1, labels = {}) {
|
|
37
|
+
this.increment(name, -value, labels);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set a gauge value
|
|
42
|
+
* @param {string} name - Metric name
|
|
43
|
+
* @param {number} value - Gauge value
|
|
44
|
+
* @param {Object} labels - Metric labels
|
|
45
|
+
*/
|
|
46
|
+
gauge(name, value, labels = {}) {
|
|
47
|
+
if (!this.enabled) return;
|
|
48
|
+
|
|
49
|
+
const key = this._getKey(name, labels);
|
|
50
|
+
this.metrics.gauges.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Record a histogram value
|
|
55
|
+
* @param {string} name - Metric name
|
|
56
|
+
* @param {number} value - Value to record
|
|
57
|
+
* @param {Object} labels - Metric labels
|
|
58
|
+
*/
|
|
59
|
+
histogram(name, value, labels = {}) {
|
|
60
|
+
if (!this.enabled) return;
|
|
61
|
+
|
|
62
|
+
const key = this._getKey(name, labels);
|
|
63
|
+
if (!this.metrics.histograms.has(key)) {
|
|
64
|
+
this.metrics.histograms.set(key, []);
|
|
65
|
+
}
|
|
66
|
+
this.metrics.histograms.get(key).push({
|
|
67
|
+
value,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Keep only last 1000 values per histogram
|
|
72
|
+
const values = this.metrics.histograms.get(key);
|
|
73
|
+
if (values.length > 1000) {
|
|
74
|
+
values.shift();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Record request duration
|
|
80
|
+
* @param {string} method - HTTP method
|
|
81
|
+
* @param {string} path - Request path
|
|
82
|
+
* @param {number} duration - Duration in milliseconds
|
|
83
|
+
* @param {number} statusCode - HTTP status code
|
|
84
|
+
*/
|
|
85
|
+
recordRequest(method, path, duration, statusCode) {
|
|
86
|
+
this.histogram('http_request_duration_ms', duration, {
|
|
87
|
+
method,
|
|
88
|
+
path,
|
|
89
|
+
status: statusCode,
|
|
90
|
+
});
|
|
91
|
+
this.increment('http_requests_total', 1, {
|
|
92
|
+
method,
|
|
93
|
+
path,
|
|
94
|
+
status: statusCode,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get all metrics
|
|
100
|
+
* @returns {Object} - All collected metrics
|
|
101
|
+
*/
|
|
102
|
+
getAll() {
|
|
103
|
+
return {
|
|
104
|
+
counters: Object.fromEntries(this.metrics.counters),
|
|
105
|
+
gauges: Object.fromEntries(this.metrics.gauges),
|
|
106
|
+
histograms: Object.fromEntries(
|
|
107
|
+
Array.from(this.metrics.histograms.entries()).map(([key, values]) => [
|
|
108
|
+
key,
|
|
109
|
+
{
|
|
110
|
+
count: values.length,
|
|
111
|
+
sum: values.reduce((sum, v) => sum + v.value, 0),
|
|
112
|
+
avg: values.length > 0 ? values.reduce((sum, v) => sum + v.value, 0) / values.length : 0,
|
|
113
|
+
min: values.length > 0 ? Math.min(...values.map(v => v.value)) : 0,
|
|
114
|
+
max: values.length > 0 ? Math.max(...values.map(v => v.value)) : 0,
|
|
115
|
+
values: values.slice(-100), // Last 100 values
|
|
116
|
+
},
|
|
117
|
+
])
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get metrics in Prometheus format
|
|
124
|
+
* @returns {string} - Prometheus metrics format
|
|
125
|
+
*/
|
|
126
|
+
toPrometheus() {
|
|
127
|
+
const lines = [];
|
|
128
|
+
|
|
129
|
+
// Counters
|
|
130
|
+
for (const [key, value] of this.metrics.counters.entries()) {
|
|
131
|
+
const { name, labels } = this._parseKey(key);
|
|
132
|
+
const labelStr = Object.entries(labels)
|
|
133
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
134
|
+
.join(',');
|
|
135
|
+
lines.push(`${name}{${labelStr}} ${value}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Gauges
|
|
139
|
+
for (const [key, value] of this.metrics.gauges.entries()) {
|
|
140
|
+
const { name, labels } = this._parseKey(key);
|
|
141
|
+
const labelStr = Object.entries(labels)
|
|
142
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
143
|
+
.join(',');
|
|
144
|
+
lines.push(`${name}{${labelStr}} ${value}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Histograms
|
|
148
|
+
for (const [key, histogram] of this.metrics.histograms.entries()) {
|
|
149
|
+
const { name, labels } = this._parseKey(key);
|
|
150
|
+
const labelStr = Object.entries(labels)
|
|
151
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
152
|
+
.join(',');
|
|
153
|
+
lines.push(`${name}_count{${labelStr}} ${histogram.length}`);
|
|
154
|
+
lines.push(`${name}_sum{${labelStr}} ${histogram.reduce((sum, v) => sum + v.value, 0)}`);
|
|
155
|
+
lines.push(`${name}_avg{${labelStr}} ${histogram.length > 0 ? histogram.reduce((sum, v) => sum + v.value, 0) / histogram.length : 0}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Reset all metrics
|
|
163
|
+
*/
|
|
164
|
+
reset() {
|
|
165
|
+
this.metrics.counters.clear();
|
|
166
|
+
this.metrics.gauges.clear();
|
|
167
|
+
this.metrics.histograms.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get key for metric storage
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
_getKey(name, labels) {
|
|
175
|
+
const labelStr = JSON.stringify(labels);
|
|
176
|
+
return `${name}::${labelStr}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse key back to name and labels
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_parseKey(key) {
|
|
184
|
+
const [name, labelStr] = key.split('::');
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
labels: JSON.parse(labelStr || '{}'),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = Metrics;
|
|
193
|
+
|