@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
JavaScript
;
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