navis.js 1.0.0 → 3.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.
@@ -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
+
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Distributed Tracer
3
+ * v3: Basic distributed tracing support
4
+ */
5
+
6
+ class Tracer {
7
+ constructor(options = {}) {
8
+ this.serviceName = options.serviceName || 'navis-service';
9
+ this.enabled = options.enabled !== false;
10
+ this.spans = new Map();
11
+ this.traceId = null;
12
+ }
13
+
14
+ /**
15
+ * Start a new trace
16
+ * @param {string} operationName - Operation name
17
+ * @param {Object} context - Trace context
18
+ * @returns {string} - Trace ID
19
+ */
20
+ startTrace(operationName, context = {}) {
21
+ if (!this.enabled) return null;
22
+
23
+ const traceId = this._generateId();
24
+ const spanId = this._generateId();
25
+
26
+ this.traceId = traceId;
27
+
28
+ const span = {
29
+ traceId,
30
+ spanId,
31
+ operationName,
32
+ startTime: Date.now(),
33
+ tags: context.tags || {},
34
+ logs: [],
35
+ childSpans: [],
36
+ };
37
+
38
+ this.spans.set(spanId, span);
39
+ return traceId;
40
+ }
41
+
42
+ /**
43
+ * Start a new span
44
+ * @param {string} operationName - Operation name
45
+ * @param {Object} options - Span options
46
+ * @returns {string} - Span ID
47
+ */
48
+ startSpan(operationName, options = {}) {
49
+ if (!this.enabled) return null;
50
+
51
+ const spanId = this._generateId();
52
+ const parentSpanId = options.parentSpanId || this._getCurrentSpanId();
53
+ const traceId = options.traceId || this.traceId || this.startTrace(operationName);
54
+
55
+ const span = {
56
+ traceId,
57
+ spanId,
58
+ parentSpanId,
59
+ operationName,
60
+ startTime: Date.now(),
61
+ tags: options.tags || {},
62
+ logs: [],
63
+ childSpans: [],
64
+ };
65
+
66
+ this.spans.set(spanId, span);
67
+
68
+ // Add as child span if parent exists
69
+ if (parentSpanId) {
70
+ const parentSpan = this.spans.get(parentSpanId);
71
+ if (parentSpan) {
72
+ parentSpan.childSpans.push(spanId);
73
+ }
74
+ }
75
+
76
+ return spanId;
77
+ }
78
+
79
+ /**
80
+ * Finish a span
81
+ * @param {string} spanId - Span ID
82
+ * @param {Object} options - Finish options
83
+ */
84
+ finishSpan(spanId, options = {}) {
85
+ if (!this.enabled || !spanId) return;
86
+
87
+ const span = this.spans.get(spanId);
88
+ if (!span) return;
89
+
90
+ span.endTime = Date.now();
91
+ span.duration = span.endTime - span.startTime;
92
+ span.status = options.status || 'ok';
93
+ span.error = options.error || null;
94
+
95
+ if (options.tags) {
96
+ span.tags = { ...span.tags, ...options.tags };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Add tag to span
102
+ * @param {string} spanId - Span ID
103
+ * @param {string} key - Tag key
104
+ * @param {*} value - Tag value
105
+ */
106
+ addTag(spanId, key, value) {
107
+ if (!this.enabled || !spanId) return;
108
+
109
+ const span = this.spans.get(spanId);
110
+ if (span) {
111
+ span.tags[key] = value;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Add log to span
117
+ * @param {string} spanId - Span ID
118
+ * @param {string} message - Log message
119
+ * @param {Object} fields - Log fields
120
+ */
121
+ addLog(spanId, message, fields = {}) {
122
+ if (!this.enabled || !spanId) return;
123
+
124
+ const span = this.spans.get(spanId);
125
+ if (span) {
126
+ span.logs.push({
127
+ timestamp: Date.now(),
128
+ message,
129
+ fields,
130
+ });
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get trace data
136
+ * @param {string} traceId - Trace ID
137
+ * @returns {Object} - Trace data
138
+ */
139
+ getTrace(traceId) {
140
+ const traceSpans = Array.from(this.spans.values()).filter(
141
+ span => span.traceId === traceId
142
+ );
143
+
144
+ return {
145
+ traceId,
146
+ spans: traceSpans,
147
+ duration: traceSpans.length > 0
148
+ ? Math.max(...traceSpans.map(s => s.endTime || Date.now())) -
149
+ Math.min(...traceSpans.map(s => s.startTime))
150
+ : 0,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Get all traces
156
+ * @returns {Array} - All traces
157
+ */
158
+ getAllTraces() {
159
+ const traceIds = new Set();
160
+ for (const span of this.spans.values()) {
161
+ traceIds.add(span.traceId);
162
+ }
163
+
164
+ return Array.from(traceIds).map(traceId => this.getTrace(traceId));
165
+ }
166
+
167
+ /**
168
+ * Clear old spans (keep last N)
169
+ * @param {number} keepCount - Number of spans to keep
170
+ */
171
+ clearOldSpans(keepCount = 1000) {
172
+ const spans = Array.from(this.spans.entries());
173
+ if (spans.length > keepCount) {
174
+ // Sort by start time and keep most recent
175
+ spans.sort((a, b) => b[1].startTime - a[1].startTime);
176
+ const toKeep = spans.slice(0, keepCount);
177
+ this.spans.clear();
178
+ for (const [id, span] of toKeep) {
179
+ this.spans.set(id, span);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get current span ID
186
+ * @private
187
+ */
188
+ _getCurrentSpanId() {
189
+ // Return the most recently created span
190
+ const spans = Array.from(this.spans.values());
191
+ if (spans.length === 0) return null;
192
+ return spans[spans.length - 1].spanId;
193
+ }
194
+
195
+ /**
196
+ * Generate unique ID
197
+ * @private
198
+ */
199
+ _generateId() {
200
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
201
+ }
202
+ }
203
+
204
+ module.exports = Tracer;
205
+