@sschepis/magazine 0.1.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,657 @@
1
+ /**
2
+ * DebugUtils - Debugging utilities and health check endpoints
3
+ */
4
+ export default class DebugUtils {
5
+ constructor(magazine, logger) {
6
+ this.magazine = magazine;
7
+ this.logger = logger.child({ context: 'DebugUtils' });
8
+ this.logger.info('DebugUtils initialized');
9
+
10
+ // Debug mode state
11
+ this.debugMode = false;
12
+
13
+ // Performance metrics
14
+ this.metrics = {
15
+ syncOperations: [],
16
+ queryOperations: [],
17
+ storageOperations: [],
18
+ networkCalls: []
19
+ };
20
+
21
+ // Log buffer (circular, max 200 entries)
22
+ this._logBuffer = [];
23
+ this._logBufferMax = 200;
24
+
25
+ // Map to store original methods before instrumentation
26
+ this._originalMethods = new Map();
27
+
28
+ // Verbose error mode flag
29
+ this._verboseErrors = false;
30
+
31
+ // Health check thresholds
32
+ this.healthThresholds = {
33
+ syncDelay: 300000, // 5 minutes
34
+ errorRate: 0.1, // 10%
35
+ responseTime: 5000, // 5 seconds
36
+ storageUsage: 0.9 // 90%
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Perform comprehensive health check
42
+ * @returns {Promise<Object>} Health check results
43
+ */
44
+ async healthCheck() {
45
+ const startTime = Date.now();
46
+ const results = {
47
+ status: 'healthy',
48
+ timestamp: new Date().toISOString(),
49
+ checks: {},
50
+ issues: []
51
+ };
52
+
53
+ try {
54
+ // Check network connectivity
55
+ results.checks.network = await this._checkNetwork();
56
+
57
+ // Check Gun.js database
58
+ results.checks.database = await this._checkDatabase();
59
+
60
+ // Check sync status
61
+ results.checks.sync = await this._checkSyncStatus();
62
+
63
+ // Check storage
64
+ results.checks.storage = await this._checkStorage();
65
+
66
+ // Check performance metrics
67
+ results.checks.performance = this._checkPerformance();
68
+
69
+ // Determine overall status
70
+ const issues = [];
71
+ Object.entries(results.checks).forEach(([check, result]) => {
72
+ if (result.status !== 'healthy') {
73
+ issues.push({
74
+ check,
75
+ status: result.status,
76
+ message: result.message
77
+ });
78
+ }
79
+ });
80
+
81
+ if (issues.length > 0) {
82
+ results.status = issues.some(i => i.status === 'critical') ? 'critical' : 'degraded';
83
+ results.issues = issues;
84
+ }
85
+
86
+ results.duration = Date.now() - startTime;
87
+
88
+ this.logger.info('Health check completed', {
89
+ status: results.status,
90
+ duration: results.duration,
91
+ issueCount: issues.length
92
+ });
93
+
94
+ return results;
95
+
96
+ } catch (error) {
97
+ this.logger.error('Health check failed', { error: error.message });
98
+ return {
99
+ status: 'error',
100
+ timestamp: new Date().toISOString(),
101
+ error: error.message,
102
+ duration: Date.now() - startTime
103
+ };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get debug information about the current state
109
+ * @returns {Promise<Object>} Debug information
110
+ */
111
+ async getDebugInfo() {
112
+ const debugInfo = {
113
+ timestamp: new Date().toISOString(),
114
+ configuration: this._getConfigInfo(),
115
+ network: await this._getNetworkInfo(),
116
+ storage: await this._getStorageInfo(),
117
+ performance: this._getPerformanceInfo(),
118
+ logs: this._getRecentLogs()
119
+ };
120
+
121
+ this.logger.debug('Debug info generated');
122
+ return debugInfo;
123
+ }
124
+
125
+ /**
126
+ * Record a metric
127
+ * @param {string} type - Metric type
128
+ * @param {Object} data - Metric data
129
+ */
130
+ recordMetric(type, data) {
131
+ const metric = {
132
+ timestamp: Date.now(),
133
+ ...data
134
+ };
135
+
136
+ if (this.metrics[type]) {
137
+ this.metrics[type].push(metric);
138
+
139
+ // Keep only last 1000 metrics per type
140
+ if (this.metrics[type].length > 1000) {
141
+ this.metrics[type] = this.metrics[type].slice(-1000);
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get performance report
148
+ * @param {Object} options - Report options
149
+ * @returns {Object} Performance report
150
+ */
151
+ getPerformanceReport(options = {}) {
152
+ const {
153
+ timeRange = 3600000, // Last hour
154
+ aggregation = 'average'
155
+ } = options;
156
+
157
+ const now = Date.now();
158
+ const startTime = now - timeRange;
159
+
160
+ const report = {};
161
+
162
+ Object.entries(this.metrics).forEach(([type, metrics]) => {
163
+ const recentMetrics = metrics.filter(m => m.timestamp >= startTime);
164
+
165
+ if (recentMetrics.length === 0) {
166
+ report[type] = { count: 0, average: 0, min: 0, max: 0 };
167
+ return;
168
+ }
169
+
170
+ const durations = recentMetrics.map(m => m.duration || 0);
171
+ const errors = recentMetrics.filter(m => m.error).length;
172
+
173
+ report[type] = {
174
+ count: recentMetrics.length,
175
+ errors,
176
+ errorRate: (errors / recentMetrics.length * 100).toFixed(2) + '%',
177
+ average: Math.round(durations.reduce((a, b) => a + b, 0) / durations.length),
178
+ min: Math.min(...durations),
179
+ max: Math.max(...durations),
180
+ p95: this._calculatePercentile(durations, 95),
181
+ p99: this._calculatePercentile(durations, 99)
182
+ };
183
+ });
184
+
185
+ return {
186
+ timeRange: {
187
+ start: new Date(startTime).toISOString(),
188
+ end: new Date(now).toISOString(),
189
+ duration: timeRange
190
+ },
191
+ metrics: report
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Enable debug mode
197
+ * @param {Object} options - Debug options
198
+ */
199
+ enableDebugMode(options = {}) {
200
+ const {
201
+ logLevel = 'debug',
202
+ captureMetrics = true,
203
+ verboseErrors = true
204
+ } = options;
205
+
206
+ // Set log level
207
+ this.magazine.setLogLevel(logLevel);
208
+
209
+ // Enable metric capture
210
+ if (captureMetrics) {
211
+ this._instrumentMethods();
212
+ }
213
+
214
+ // Enable verbose errors
215
+ if (verboseErrors) {
216
+ this._enableVerboseErrors();
217
+ }
218
+
219
+ this.logger.info('Debug mode enabled', options);
220
+ }
221
+
222
+ /**
223
+ * Export debug data
224
+ * @returns {Promise<Object>} Exportable debug data
225
+ */
226
+ async exportDebugData() {
227
+ const data = {
228
+ version: '1.0.0',
229
+ exportedAt: new Date().toISOString(),
230
+ debugInfo: await this.getDebugInfo(),
231
+ healthCheck: await this.healthCheck(),
232
+ performanceReport: this.getPerformanceReport(),
233
+ recentMetrics: {
234
+ sync: this.metrics.syncOperations.slice(-100),
235
+ query: this.metrics.queryOperations.slice(-100),
236
+ storage: this.metrics.storageOperations.slice(-100),
237
+ network: this.metrics.networkCalls.slice(-100)
238
+ }
239
+ };
240
+
241
+ this.logger.info('Debug data exported');
242
+ return data;
243
+ }
244
+
245
+ /**
246
+ * Check network connectivity
247
+ * @private
248
+ */
249
+ async _checkNetwork() {
250
+ try {
251
+ const startTime = Date.now();
252
+ const blockNumber = await this.magazine.getLatestBlockNumber();
253
+ const duration = Date.now() - startTime;
254
+
255
+ return {
256
+ status: duration < this.healthThresholds.responseTime ? 'healthy' : 'degraded',
257
+ latestBlock: blockNumber,
258
+ responseTime: duration,
259
+ message: `Network response time: ${duration}ms`
260
+ };
261
+ } catch (error) {
262
+ return {
263
+ status: 'critical',
264
+ error: error.message,
265
+ message: 'Network connection failed'
266
+ };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Check database connectivity
272
+ * @private
273
+ */
274
+ async _checkDatabase() {
275
+ try {
276
+ const startTime = Date.now();
277
+ const gun = this.magazine.eventStore.getGunInstance();
278
+
279
+ // Try to read a test value
280
+ await new Promise((resolve, reject) => {
281
+ gun.get('_health_check').put({
282
+ timestamp: Date.now()
283
+ }, (ack) => {
284
+ if (ack.err) reject(new Error(ack.err));
285
+ else resolve(ack);
286
+ });
287
+ });
288
+
289
+ const duration = Date.now() - startTime;
290
+
291
+ return {
292
+ status: duration < 1000 ? 'healthy' : 'degraded',
293
+ responseTime: duration,
294
+ message: `Database response time: ${duration}ms`
295
+ };
296
+ } catch (error) {
297
+ return {
298
+ status: 'critical',
299
+ error: error.message,
300
+ message: 'Database connection failed'
301
+ };
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Check sync status
307
+ * @private
308
+ */
309
+ async _checkSyncStatus() {
310
+ try {
311
+ const lastSync = this.magazine.eventSyncer.lastSyncTime || 0;
312
+ const timeSinceSync = Date.now() - lastSync;
313
+
314
+ if (lastSync === 0) {
315
+ return {
316
+ status: 'warning',
317
+ message: 'No sync performed yet'
318
+ };
319
+ }
320
+
321
+ return {
322
+ status: timeSinceSync < this.healthThresholds.syncDelay ? 'healthy' : 'warning',
323
+ lastSync: new Date(lastSync).toISOString(),
324
+ timeSinceSync,
325
+ message: `Last sync: ${Math.round(timeSinceSync / 1000)}s ago`
326
+ };
327
+ } catch (error) {
328
+ return {
329
+ status: 'error',
330
+ error: error.message,
331
+ message: 'Could not check sync status'
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Check storage status
338
+ * @private
339
+ */
340
+ async _checkStorage() {
341
+ try {
342
+ const stats = await this.magazine.getCompressionStats();
343
+ const usage = stats.totalCompressedSize / (100 * 1024 * 1024); // Assume 100MB limit
344
+
345
+ return {
346
+ status: usage < this.healthThresholds.storageUsage ? 'healthy' : 'warning',
347
+ usage: `${(usage * 100).toFixed(2)}%`,
348
+ compressed: stats.totalCompressedSize,
349
+ saved: stats.spaceSaved,
350
+ message: `Storage usage: ${(usage * 100).toFixed(2)}%`
351
+ };
352
+ } catch (error) {
353
+ return {
354
+ status: 'error',
355
+ error: error.message,
356
+ message: 'Could not check storage status'
357
+ };
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Check performance metrics
363
+ * @private
364
+ */
365
+ _checkPerformance() {
366
+ const report = this.getPerformanceReport({ timeRange: 300000 }); // Last 5 minutes
367
+ let status = 'healthy';
368
+ const issues = [];
369
+
370
+ Object.entries(report.metrics).forEach(([type, metrics]) => {
371
+ if (metrics.errorRate && parseFloat(metrics.errorRate) > this.healthThresholds.errorRate * 100) {
372
+ status = 'degraded';
373
+ issues.push(`High error rate in ${type}: ${metrics.errorRate}`);
374
+ }
375
+
376
+ if (metrics.average > this.healthThresholds.responseTime) {
377
+ status = 'degraded';
378
+ issues.push(`Slow ${type} operations: ${metrics.average}ms average`);
379
+ }
380
+ });
381
+
382
+ return {
383
+ status,
384
+ metrics: report.metrics,
385
+ message: issues.length > 0 ? issues.join(', ') : 'Performance metrics within normal range'
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Get configuration info
391
+ * @private
392
+ */
393
+ _getConfigInfo() {
394
+ const config = this.magazine.configManager.getAll();
395
+ return {
396
+ name: config.name,
397
+ address: config.address,
398
+ networkId: config.networkId,
399
+ logLevel: this.magazine.logger.level,
400
+ compression: {
401
+ enabled: this.magazine.eventStore.compressionEnabled,
402
+ strategy: this.magazine.eventStore.compressionStrategy
403
+ }
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Get network info
409
+ * @private
410
+ */
411
+ async _getNetworkInfo() {
412
+ try {
413
+ const info = await this.magazine.getNetworkInfo();
414
+ return {
415
+ ...info,
416
+ connected: true
417
+ };
418
+ } catch (error) {
419
+ return {
420
+ connected: false,
421
+ error: error.message
422
+ };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Get storage info
428
+ * @private
429
+ */
430
+ async _getStorageInfo() {
431
+ try {
432
+ const compressionStats = await this.magazine.getCompressionStats();
433
+ const eventCount = (await this.magazine.getAllEvents({ pagination: { page: 1, pageSize: 1 } })).meta.totalItems;
434
+
435
+ return {
436
+ eventCount,
437
+ ...compressionStats
438
+ };
439
+ } catch (error) {
440
+ return {
441
+ error: error.message
442
+ };
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Get performance info
448
+ * @private
449
+ */
450
+ _getPerformanceInfo() {
451
+ return this.getPerformanceReport({ timeRange: 3600000 }); // Last hour
452
+ }
453
+
454
+ /**
455
+ * Capture a log entry into the circular buffer
456
+ * @param {Object} entry - Log entry to capture
457
+ */
458
+ captureLog(entry) {
459
+ this._logBuffer.push(entry);
460
+ if (this._logBuffer.length > this._logBufferMax) {
461
+ this._logBuffer.shift();
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Get recent logs from the circular buffer
467
+ * @param {number} [count=50] - Number of recent log entries to return
468
+ * @returns {Array} Recent log entries
469
+ * @private
470
+ */
471
+ _getRecentLogs(count = 50) {
472
+ const n = Math.min(count, this._logBuffer.length);
473
+ return this._logBuffer.slice(-n);
474
+ }
475
+
476
+ /**
477
+ * Calculate percentile
478
+ * @private
479
+ */
480
+ _calculatePercentile(values, percentile) {
481
+ if (values.length === 0) return 0;
482
+
483
+ const sorted = [...values].sort((a, b) => a - b);
484
+ const index = Math.ceil((percentile / 100) * sorted.length) - 1;
485
+ return sorted[index];
486
+ }
487
+
488
+ /**
489
+ * Instrument methods for metric collection
490
+ * @private
491
+ */
492
+ _instrumentMethods() {
493
+ const mag = this.magazine;
494
+ const methodsToInstrument = [
495
+ 'syncEvents',
496
+ 'getAllEvents',
497
+ 'getEvents',
498
+ 'query',
499
+ 'transformEvent'
500
+ ];
501
+
502
+ for (const methodName of methodsToInstrument) {
503
+ if (typeof mag[methodName] !== 'function') {
504
+ this.logger.debug(`Skipping instrumentation for missing method: ${methodName}`);
505
+ continue;
506
+ }
507
+
508
+ // Skip if already instrumented
509
+ if (this._originalMethods.has(methodName)) {
510
+ continue;
511
+ }
512
+
513
+ const original = mag[methodName].bind(mag);
514
+ this._originalMethods.set(methodName, mag[methodName]);
515
+
516
+ const self = this;
517
+ mag[methodName] = function (...args) {
518
+ const startTime = Date.now();
519
+ let metricType;
520
+ if (methodName === 'syncEvents') {
521
+ metricType = 'syncOperations';
522
+ } else if (methodName === 'getAllEvents' || methodName === 'getEvents' || methodName === 'query') {
523
+ metricType = 'queryOperations';
524
+ } else {
525
+ metricType = 'storageOperations';
526
+ }
527
+
528
+ try {
529
+ const result = original(...args);
530
+
531
+ // Handle both sync and async results
532
+ if (result && typeof result.then === 'function') {
533
+ return result.then((value) => {
534
+ const duration = Date.now() - startTime;
535
+ self.recordMetric(metricType, {
536
+ method: methodName,
537
+ duration,
538
+ success: true
539
+ });
540
+ self.logger.debug(`${methodName} completed in ${duration}ms`);
541
+ return value;
542
+ }).catch((error) => {
543
+ const duration = Date.now() - startTime;
544
+ self.recordMetric(metricType, {
545
+ method: methodName,
546
+ duration,
547
+ success: false,
548
+ error: error.message
549
+ });
550
+ self.logger.debug(`${methodName} failed after ${duration}ms: ${error.message}`);
551
+ throw error;
552
+ });
553
+ }
554
+
555
+ const duration = Date.now() - startTime;
556
+ self.recordMetric(metricType, {
557
+ method: methodName,
558
+ duration,
559
+ success: true
560
+ });
561
+ self.logger.debug(`${methodName} completed in ${duration}ms`);
562
+ return result;
563
+ } catch (error) {
564
+ const duration = Date.now() - startTime;
565
+ self.recordMetric(metricType, {
566
+ method: methodName,
567
+ duration,
568
+ success: false,
569
+ error: error.message
570
+ });
571
+ self.logger.debug(`${methodName} failed after ${duration}ms: ${error.message}`);
572
+ throw error;
573
+ }
574
+ };
575
+ }
576
+
577
+ this.logger.debug('Method instrumentation enabled');
578
+ }
579
+
580
+ /**
581
+ * Enable verbose error logging
582
+ * @private
583
+ */
584
+ _enableVerboseErrors() {
585
+ this._verboseErrors = true;
586
+
587
+ const mag = this.magazine;
588
+ if (typeof mag.retryOperation === 'function' && !this._originalRetryOperation) {
589
+ const originalRetry = mag.retryOperation.bind(mag);
590
+ this._originalRetryOperation = mag.retryOperation;
591
+ const self = this;
592
+
593
+ mag.retryOperation = async function (operation, context, ...rest) {
594
+ try {
595
+ return await originalRetry(operation, context, ...rest);
596
+ } catch (error) {
597
+ if (self._verboseErrors) {
598
+ self.logger.error('Verbose error context for retryOperation failure', {
599
+ operationContext: context,
600
+ errorMessage: error.message,
601
+ errorStack: error.stack,
602
+ errorName: error.name,
603
+ timestamp: new Date().toISOString()
604
+ });
605
+ }
606
+ throw error;
607
+ }
608
+ };
609
+ }
610
+
611
+ this.logger.debug('Verbose error logging enabled');
612
+ }
613
+
614
+ /**
615
+ * Set debug mode on/off
616
+ * @param {boolean} enabled - Whether to enable debug mode
617
+ */
618
+ setDebugMode(enabled) {
619
+ this.debugMode = !!enabled;
620
+ this.logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
621
+
622
+ if (enabled) {
623
+ this._instrumentMethods();
624
+ this._enableVerboseErrors();
625
+
626
+ // Monkey-patch the logger's _log method to capture entries
627
+ if (this.logger._log && !this._originalLoggerLog) {
628
+ this._originalLoggerLog = this.logger._log.bind(this.logger);
629
+ const self = this;
630
+ this.logger._log = function (level, message, data) {
631
+ const result = self._originalLoggerLog(level, message, data);
632
+ self.captureLog({
633
+ timestamp: Date.now(),
634
+ level,
635
+ message,
636
+ data
637
+ });
638
+ return result;
639
+ };
640
+ }
641
+ } else {
642
+ // Restore logger _log if it was patched
643
+ if (this._originalLoggerLog) {
644
+ this.logger._log = this._originalLoggerLog;
645
+ this._originalLoggerLog = null;
646
+ }
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Check if debug mode is enabled
652
+ * @returns {boolean} Debug mode status
653
+ */
654
+ isDebugMode() {
655
+ return !!this.debugMode;
656
+ }
657
+ }