UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

677 lines (672 loc) 26.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ExportReportingService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExportReportingService = exports.ReportFormat = exports.ExportFormat = void 0; const common_1 = require("@nestjs/common"); const telescope_service_1 = require("./telescope.service"); const analytics_service_1 = require("./analytics.service"); const performance_correlation_service_1 = require("./performance-correlation.service"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); var ExportFormat; (function (ExportFormat) { ExportFormat["JSON"] = "json"; ExportFormat["CSV"] = "csv"; ExportFormat["XLSX"] = "xlsx"; ExportFormat["PDF"] = "pdf"; })(ExportFormat || (exports.ExportFormat = ExportFormat = {})); var ReportFormat; (function (ReportFormat) { ReportFormat["HTML"] = "html"; ReportFormat["PDF"] = "pdf"; ReportFormat["MARKDOWN"] = "md"; })(ReportFormat || (exports.ReportFormat = ReportFormat = {})); let ExportReportingService = ExportReportingService_1 = class ExportReportingService { constructor(telescopeService, analyticsService, performanceCorrelationService) { this.telescopeService = telescopeService; this.analyticsService = analyticsService; this.performanceCorrelationService = performanceCorrelationService; this.logger = new common_1.Logger(ExportReportingService_1.name); this.exportDir = path.join(process.cwd(), 'exports'); this.ensureExportDirectory(); } ensureExportDirectory() { if (!fs.existsSync(this.exportDir)) { fs.mkdirSync(this.exportDir, { recursive: true }); } } async exportData(options) { try { this.logger.log(`Exporting data with format: ${options.format}, type: ${options.type}`); let data; let recordCount = 0; const timeRange = options.timeRange || { start: new Date(Date.now() - 24 * 60 * 60 * 1000), end: new Date(), }; switch (options.type) { case 'raw': data = await this.collectRawData(options); recordCount = Array.isArray(data) ? data.length : 1; break; case 'analytics': data = await this.collectAnalyticsData(options); recordCount = 1; break; case 'performance': data = await this.collectPerformanceData(options); recordCount = Array.isArray(data) ? data.length : 1; break; case 'custom': data = await this.collectCustomData(options); recordCount = Array.isArray(data) ? data.length : 1; break; default: throw new Error(`Unsupported export type: ${options.type}`); } if (options.filters) { data = this.applyFilters(data, options.filters); recordCount = Array.isArray(data) ? data.length : 1; } if (options.fields) { data = this.selectFields(data, options.fields); } if (options.limit && Array.isArray(data)) { data = data.slice(0, options.limit); recordCount = data.length; } const fileName = this.generateFileName(options); const filePath = path.join(this.exportDir, fileName); switch (options.format) { case 'json': await this.exportToJson(data, filePath, options); break; case 'csv': await this.exportToCsv(data, filePath, options); break; case 'xlsx': await this.exportToXlsx(data, filePath, options); break; case 'pdf': await this.exportToPdf(data, filePath, options); break; default: throw new Error(`Unsupported export format: ${options.format}`); } return { success: true, filePath, metadata: { recordCount, timeRange, exportedAt: new Date(), format: options.format, }, }; } catch (error) { this.logger.error('Export failed:', error); return { success: false, error: error.message, metadata: { recordCount: 0, timeRange: options.timeRange || { start: new Date(), end: new Date() }, exportedAt: new Date(), format: options.format, }, }; } } async generateReport(options) { try { this.logger.log(`Generating report: ${options.type} in ${options.format} format`); const reportData = await this.collectReportData(options); const content = await this.generateReportContent(reportData, options); const fileName = this.generateReportFileName(options); const filePath = path.join(this.exportDir, fileName); switch (options.format) { case 'html': await this.saveHtmlReport(content, filePath); break; case 'pdf': await this.savePdfReport(content, filePath); break; case 'md': await this.saveMarkdownReport(content, filePath); break; default: throw new Error(`Unsupported report format: ${options.format}`); } return { success: true, filePath, content, metadata: { title: options.title || `${options.type} Report`, generatedAt: new Date(), timeRange: options.timeRange, format: options.format, sections: options.sections?.length || 0, }, }; } catch (error) { this.logger.error('Report generation failed:', error); return { success: false, error: error.message, metadata: { title: options.title || `${options.type} Report`, generatedAt: new Date(), timeRange: options.timeRange, format: options.format, sections: 0, }, }; } } async collectRawData(options) { const entries = await this.telescopeService.getEntries(); let filteredEntries = entries; if (options.timeRange) { filteredEntries = entries.filter(entry => { const entryTime = new Date(entry.timestamp); return entryTime >= options.timeRange.start && entryTime <= options.timeRange.end; }); } return filteredEntries; } async collectAnalyticsData(options) { if (options.timeRange) { return await this.analyticsService.getAnalyticsForTimeRange(options.timeRange.start, options.timeRange.end); } return this.analyticsService.getAnalytics(); } async collectPerformanceData(options) { const correlations = this.performanceCorrelationService.getRecentCorrelations(1000); if (options.timeRange) { return correlations.filter(correlation => { const corrTime = new Date(correlation.timestamp); return corrTime >= options.timeRange.start && corrTime <= options.timeRange.end; }); } return correlations; } async collectCustomData(options) { const data = { entries: await this.collectRawData(options), analytics: await this.collectAnalyticsData(options), performance: await this.collectPerformanceData(options), }; return data; } applyFilters(data, filters) { if (!Array.isArray(data)) { return data; } return data.filter(item => { if (filters.watchers && filters.watchers.length > 0) { if (!filters.watchers.includes(item.type)) { return false; } } if (filters.components && filters.components.length > 0) { const itemComponent = item.content?.component || item.component; if (!filters.components.includes(itemComponent)) { return false; } } if (filters.severities && filters.severities.length > 0) { const itemSeverity = item.content?.severity || item.severity; if (!filters.severities.includes(itemSeverity)) { return false; } } if (filters.tags && filters.tags.length > 0) { const itemTags = item.tags || []; if (!filters.tags.some(tag => itemTags.includes(tag))) { return false; } } return true; }); } selectFields(data, fields) { if (!Array.isArray(data)) { return this.selectObjectFields(data, fields); } return data.map(item => this.selectObjectFields(item, fields)); } selectObjectFields(obj, fields) { const result = {}; fields.forEach(field => { const fieldParts = field.split('.'); let value = obj; for (const part of fieldParts) { if (value && typeof value === 'object' && part in value) { value = value[part]; } else { value = undefined; break; } } if (value !== undefined) { this.setNestedValue(result, field, value); } }); return result; } setNestedValue(obj, path, value) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } generateFileName(options) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const type = options.type; const format = options.format; return `telescope-${type}-${timestamp}.${format}`; } generateReportFileName(options) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const type = options.type; const format = options.format; return `telescope-report-${type}-${timestamp}.${format}`; } async exportToJson(data, filePath, options) { const exportData = { metadata: { exportedAt: new Date(), type: options.type, format: options.format, recordCount: Array.isArray(data) ? data.length : 1, timeRange: options.timeRange, filters: options.filters, }, data, }; await fs.promises.writeFile(filePath, JSON.stringify(exportData, null, 2)); } async exportToCsv(data, filePath, options) { if (!Array.isArray(data)) { throw new Error('CSV export requires array data'); } if (data.length === 0) { await fs.promises.writeFile(filePath, ''); return; } const headers = this.extractCsvHeaders(data[0]); const csvRows = [headers.join(',')]; data.forEach(item => { const values = headers.map(header => { const value = this.getNestedValue(item, header); return this.escapeCsvValue(value); }); csvRows.push(values.join(',')); }); await fs.promises.writeFile(filePath, csvRows.join('\n')); } extractCsvHeaders(obj, prefix = '') { const headers = []; Object.keys(obj).forEach(key => { const fullKey = prefix ? `${prefix}.${key}` : key; const value = obj[key]; if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) { headers.push(...this.extractCsvHeaders(value, fullKey)); } else { headers.push(fullKey); } }); return headers; } getNestedValue(obj, path) { const parts = path.split('.'); let value = obj; for (const part of parts) { if (value && typeof value === 'object' && part in value) { value = value[part]; } else { return ''; } } return value; } escapeCsvValue(value) { if (value === null || value === undefined) { return ''; } const stringValue = String(value); if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) { return `"${stringValue.replace(/"/g, '""')}"`; } return stringValue; } async exportToXlsx(data, filePath, options) { await this.exportToCsv(data, filePath, options); } async exportToPdf(data, filePath, options) { const jsonData = JSON.stringify(data, null, 2); await fs.promises.writeFile(filePath, jsonData); } async collectReportData(options) { const analytics = await this.analyticsService.getAnalyticsForTimeRange(options.timeRange.start, options.timeRange.end); const performance = this.performanceCorrelationService.getRecentCorrelations(1000) .filter(correlation => { const corrTime = new Date(correlation.timestamp); return corrTime >= options.timeRange.start && corrTime <= options.timeRange.end; }); return { analytics, performance, timeRange: options.timeRange, }; } async generateReportContent(data, options) { const sections = options.sections || this.getDefaultSections(options.type); switch (options.format) { case 'html': return this.generateHtmlReport(data, options, sections); case 'md': return this.generateMarkdownReport(data, options, sections); case 'pdf': return this.generatePdfReport(data, options, sections); default: throw new Error(`Unsupported report format: ${options.format}`); } } getDefaultSections(type) { switch (type) { case 'performance': return [ { title: 'Executive Summary', type: 'overview' }, { title: 'Performance Metrics', type: 'metrics' }, { title: 'Response Time Analysis', type: 'charts' }, { title: 'Bottleneck Analysis', type: 'analysis' }, { title: 'Recommendations', type: 'recommendations' }, ]; case 'error': return [ { title: 'Error Overview', type: 'overview' }, { title: 'Error Metrics', type: 'metrics' }, { title: 'Error Distribution', type: 'charts' }, { title: 'Top Errors', type: 'table' }, { title: 'Error Impact Analysis', type: 'analysis' }, { title: 'Recommendations', type: 'recommendations' }, ]; case 'system': return [ { title: 'System Overview', type: 'overview' }, { title: 'System Metrics', type: 'metrics' }, { title: 'Component Health', type: 'charts' }, { title: 'Resource Usage', type: 'analysis' }, { title: 'Recommendations', type: 'recommendations' }, ]; default: return [ { title: 'Overview', type: 'overview' }, { title: 'Metrics', type: 'metrics' }, { title: 'Analysis', type: 'analysis' }, { title: 'Recommendations', type: 'recommendations' }, ]; } } generateHtmlReport(data, options, sections) { const title = options.title || `${options.type} Report`; const description = options.description || `Generated report for ${options.type} analysis`; let html = ` <!DOCTYPE html> <html> <head> <title>${title}</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; } .section { margin-bottom: 30px; } .section h2 { color: #333; border-bottom: 1px solid #ccc; padding-bottom: 5px; } .metric { display: inline-block; margin: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } .metric-value { font-size: 24px; font-weight: bold; color: #007bff; } .metric-label { font-size: 14px; color: #666; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #f5f5f5; } .recommendation { background-color: #e7f3ff; padding: 15px; border-left: 4px solid #007bff; margin: 10px 0; } </style> </head> <body> <div class="header"> <h1>${title}</h1> <p>${description}</p> <p><strong>Time Range:</strong> ${options.timeRange.start.toISOString()} - ${options.timeRange.end.toISOString()}</p> <p><strong>Generated:</strong> ${new Date().toISOString()}</p> </div> `; sections.forEach(section => { html += this.generateHtmlSection(section, data); }); html += ` </body> </html> `; return html; } generateHtmlSection(section, data) { let sectionHtml = `<div class="section"><h2>${section.title}</h2>`; switch (section.type) { case 'overview': sectionHtml += this.generateHtmlOverview(data); break; case 'metrics': sectionHtml += this.generateHtmlMetrics(data); break; case 'charts': sectionHtml += this.generateHtmlCharts(data); break; case 'table': sectionHtml += this.generateHtmlTable(data); break; case 'analysis': sectionHtml += this.generateHtmlAnalysis(data); break; case 'recommendations': sectionHtml += this.generateHtmlRecommendations(data); break; } sectionHtml += '</div>'; return sectionHtml; } generateHtmlOverview(data) { const analytics = data.analytics; if (!analytics) return '<p>No overview data available</p>'; return ` <div class="overview"> <div class="metric"> <div class="metric-value">${analytics.overview.totalRequests}</div> <div class="metric-label">Total Requests</div> </div> <div class="metric"> <div class="metric-value">${analytics.overview.totalErrors}</div> <div class="metric-label">Total Errors</div> </div> <div class="metric"> <div class="metric-value">${analytics.overview.averageResponseTime.toFixed(2)}ms</div> <div class="metric-label">Avg Response Time</div> </div> <div class="metric"> <div class="metric-value">${analytics.overview.errorRate.toFixed(2)}%</div> <div class="metric-label">Error Rate</div> </div> </div> `; } generateHtmlMetrics(data) { return '<p>Detailed metrics would be displayed here</p>'; } generateHtmlCharts(data) { return '<p>Charts would be displayed here</p>'; } generateHtmlTable(data) { return '<p>Data table would be displayed here</p>'; } generateHtmlAnalysis(data) { return '<p>Analysis results would be displayed here</p>'; } generateHtmlRecommendations(data) { const recommendations = [ 'Review and optimize slow queries', 'Implement caching for frequently accessed data', 'Consider scaling resources during peak hours', 'Monitor and fix recurring errors', ]; let html = '<div class="recommendations">'; recommendations.forEach(rec => { html += `<div class="recommendation">${rec}</div>`; }); html += '</div>'; return html; } generateMarkdownReport(data, options, sections) { const title = options.title || `${options.type} Report`; const description = options.description || `Generated report for ${options.type} analysis`; let markdown = `# ${title} ${description} **Time Range:** ${options.timeRange.start.toISOString()} - ${options.timeRange.end.toISOString()} **Generated:** ${new Date().toISOString()} --- `; sections.forEach(section => { markdown += this.generateMarkdownSection(section, data); }); return markdown; } generateMarkdownSection(section, data) { let sectionMd = `## ${section.title}\n\n`; switch (section.type) { case 'overview': sectionMd += this.generateMarkdownOverview(data); break; case 'metrics': sectionMd += this.generateMarkdownMetrics(data); break; case 'recommendations': sectionMd += this.generateMarkdownRecommendations(data); break; default: sectionMd += 'Section content would be generated here.\n\n'; } return sectionMd; } generateMarkdownOverview(data) { const analytics = data.analytics; if (!analytics) return 'No overview data available\n\n'; return ` | Metric | Value | |--------|-------| | Total Requests | ${analytics.overview.totalRequests} | | Total Errors | ${analytics.overview.totalErrors} | | Average Response Time | ${analytics.overview.averageResponseTime.toFixed(2)}ms | | Error Rate | ${analytics.overview.errorRate.toFixed(2)}% | `; } generateMarkdownMetrics(data) { return 'Detailed metrics would be displayed here.\n\n'; } generateMarkdownRecommendations(data) { const recommendations = [ 'Review and optimize slow queries', 'Implement caching for frequently accessed data', 'Consider scaling resources during peak hours', 'Monitor and fix recurring errors', ]; let md = ''; recommendations.forEach(rec => { md += `- ${rec}\n`; }); md += '\n'; return md; } generatePdfReport(data, options, sections) { return this.generateMarkdownReport(data, options, sections); } async saveHtmlReport(content, filePath) { await fs.promises.writeFile(filePath, content); } async savePdfReport(content, filePath) { await fs.promises.writeFile(filePath, content); } async saveMarkdownReport(content, filePath) { await fs.promises.writeFile(filePath, content); } async scheduleReport(options, cronExpression) { this.logger.log(`Scheduling report with cron: ${cronExpression}`); return 'report-schedule-id'; } async getExportHistory() { return []; } async getReportTemplates() { return []; } async deleteExport(filePath) { try { await fs.promises.unlink(filePath); return true; } catch (error) { this.logger.error('Failed to delete export file:', error); return false; } } }; exports.ExportReportingService = ExportReportingService; exports.ExportReportingService = ExportReportingService = ExportReportingService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [telescope_service_1.TelescopeService, analytics_service_1.AnalyticsService, performance_correlation_service_1.PerformanceCorrelationService]) ], ExportReportingService); //# sourceMappingURL=export-reporting.service.js.map