UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

381 lines 16.1 kB
"use strict"; 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 __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var QueryAnalyzerService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryAnalyzerService = void 0; const common_1 = require("@nestjs/common"); const typeorm_1 = require("typeorm"); let QueryAnalyzerService = QueryAnalyzerService_1 = class QueryAnalyzerService { constructor(dataSource) { this.dataSource = dataSource; this.logger = new common_1.Logger(QueryAnalyzerService_1.name); this.queryPatterns = new Map(); this.indexSuggestions = new Map(); } async analyzeQuery(context) { const issues = []; const optimizationHints = []; let executionPlan; try { executionPlan = await this.getExecutionPlan(context.sql, context.parameters); const structuralIssues = this.analyzeQueryStructure(context.sql); issues.push(...structuralIssues); if (executionPlan) { const planIssues = this.analyzeExecutionPlan(executionPlan, context.tableName); issues.push(...planIssues); } const hints = this.generateOptimizationHints(context, issues); optimizationHints.push(...hints); this.updateQueryPatterns(context); return { queryId: context.id, sql: context.sql, duration: context.duration || 0, severity: this.determineSeverity(context.duration || 0), issues, optimizationHints, affectedRows: context.affectedRows || 0, executionPlan }; } catch (error) { this.logger.error('Failed to analyze query:', error); return { queryId: context.id, sql: context.sql, duration: context.duration || 0, severity: this.determineSeverity(context.duration || 0), issues, optimizationHints, affectedRows: context.affectedRows || 0 }; } } async getExecutionPlan(sql, parameters) { try { const driverType = this.dataSource.options.type; if (driverType === 'postgres') { return this.getPostgresExecutionPlan(sql, parameters); } else if (driverType === 'mysql') { return this.getMySQLExecutionPlan(sql, parameters); } return undefined; } catch (error) { this.logger.debug('Could not get execution plan:', error.message); return undefined; } } async getPostgresExecutionPlan(sql, parameters) { try { const explainSql = `EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS) ${sql}`; const result = await this.dataSource.query(explainSql, parameters); const plan = result[0]['QUERY PLAN'][0]; return { totalCost: plan.Plan['Total Cost'], rows: plan.Plan['Actual Rows'], operations: this.extractPostgresOperations(plan.Plan) }; } catch (error) { this.logger.debug('Failed to get PostgreSQL execution plan:', error.message); return undefined; } } async getMySQLExecutionPlan(sql, parameters) { try { const explainSql = `EXPLAIN FORMAT=JSON ${sql}`; const result = await this.dataSource.query(explainSql, parameters); const plan = JSON.parse(result[0]['EXPLAIN']); return { totalCost: plan.query_block.cost_info.query_cost, rows: plan.query_block.cost_info.estimated_rows, operations: this.extractMySQLOperations(plan.query_block) }; } catch (error) { this.logger.debug('Failed to get MySQL execution plan:', error.message); return undefined; } } extractPostgresOperations(plan) { const operations = []; const extractOperation = (node) => { operations.push({ operation: node['Node Type'], table: node['Relation Name'] || 'N/A', cost: node['Total Cost'], rows: node['Actual Rows'], filter: node['Filter'] }); if (node.Plans) { node.Plans.forEach(extractOperation); } }; extractOperation(plan); return operations; } extractMySQLOperations(queryBlock) { const operations = []; if (queryBlock.table) { operations.push({ operation: queryBlock.table.access_type || 'table_scan', table: queryBlock.table.table_name, cost: queryBlock.cost_info.read_cost, rows: queryBlock.cost_info.estimated_rows, filter: queryBlock.table.attached_condition }); } return operations; } analyzeQueryStructure(sql) { const issues = []; const normalizedSql = sql.toLowerCase().trim(); if (normalizedSql.includes('select *')) { issues.push({ type: 'full_table_scan', severity: 'medium', description: 'Query uses SELECT * which may fetch unnecessary columns', suggestion: 'Specify only the columns you need to reduce data transfer and improve performance', affectedTable: this.extractTableName(sql) }); } if (normalizedSql.includes('select') && !normalizedSql.includes('where') && !normalizedSql.includes('limit')) { issues.push({ type: 'full_table_scan', severity: 'high', description: 'Query lacks WHERE clause and may scan entire table', suggestion: 'Add appropriate WHERE conditions to filter results', affectedTable: this.extractTableName(sql) }); } if (normalizedSql.match(/where.*\b(upper|lower|substring|concat|date|year|month)\s*\(/)) { issues.push({ type: 'missing_index', severity: 'medium', description: 'Query uses functions in WHERE clause which prevents index usage', suggestion: 'Consider using function-based indexes or restructuring the query', affectedTable: this.extractTableName(sql) }); } if (normalizedSql.includes('where') && normalizedSql.includes(' or ')) { issues.push({ type: 'missing_index', severity: 'medium', description: 'Query uses OR conditions which may not use indexes efficiently', suggestion: 'Consider using UNION or separate queries with appropriate indexes', affectedTable: this.extractTableName(sql) }); } if (normalizedSql.match(/like\s+['"]%/)) { issues.push({ type: 'missing_index', severity: 'medium', description: 'Query uses LIKE with leading wildcard which prevents index usage', suggestion: 'Consider using full-text search or restructuring the search pattern', affectedTable: this.extractTableName(sql) }); } const joinCount = (normalizedSql.match(/\bjoin\b/g) || []).length; if (joinCount > 5) { issues.push({ type: 'excessive_joins', severity: 'high', description: `Query has ${joinCount} joins which may impact performance`, suggestion: 'Consider denormalizing data, using materialized views, or breaking into multiple queries', affectedTable: this.extractTableName(sql) }); } if (normalizedSql.includes('where') && normalizedSql.includes('in (select')) { issues.push({ type: 'subquery_performance', severity: 'medium', description: 'Query uses IN with subquery which may be inefficient', suggestion: 'Consider rewriting as JOIN or using EXISTS instead of IN', affectedTable: this.extractTableName(sql) }); } return issues; } analyzeExecutionPlan(plan, tableName) { const issues = []; if (plan.totalCost > 10000) { issues.push({ type: 'full_table_scan', severity: 'high', description: `Query has high execution cost: ${plan.totalCost}`, suggestion: 'Consider adding indexes or optimizing the query structure', affectedTable: tableName }); } const hasTableScan = plan.operations.some(op => op.operation.toLowerCase().includes('seq scan') || op.operation.toLowerCase().includes('table scan')); if (hasTableScan) { issues.push({ type: 'full_table_scan', severity: 'high', description: 'Query performs full table scan', suggestion: 'Add appropriate indexes on filtered columns', affectedTable: tableName }); } plan.operations.forEach(op => { if (op.rows > 10000) { issues.push({ type: 'full_table_scan', severity: 'medium', description: `Operation scans ${op.rows} rows`, suggestion: 'Consider adding more selective filters or indexes', affectedTable: op.table }); } }); return issues; } generateOptimizationHints(context, issues) { const hints = []; if (issues.some(issue => issue.type === 'missing_index' || issue.type === 'full_table_scan')) { hints.push({ type: 'index_suggestion', priority: 'high', description: 'Add database indexes to improve query performance', implementation: this.generateIndexSuggestion(context.sql, context.tableName), estimatedImpact: 'Could reduce query time by 50-95%' }); } if (issues.some(issue => issue.type === 'subquery_performance')) { hints.push({ type: 'query_rewrite', priority: 'medium', description: 'Rewrite subqueries as JOINs for better performance', implementation: 'Convert IN (SELECT ...) to JOIN or EXISTS clauses', estimatedImpact: 'Could improve performance by 20-50%' }); } if (this.isFrequentQuery(context.sql)) { hints.push({ type: 'caching_opportunity', priority: 'medium', description: 'Consider caching this frequently executed query', implementation: 'Implement Redis caching or query result caching', estimatedImpact: 'Could reduce database load by 70-90%' }); } return hints; } generateIndexSuggestion(sql, tableName) { const normalizedSql = sql.toLowerCase(); const whereMatch = normalizedSql.match(/where\s+(.+?)(?:\s+order\s+by|\s+group\s+by|\s+having|\s+limit|$)/); if (whereMatch && tableName) { const whereClause = whereMatch[1]; const columns = this.extractColumnsFromWhere(whereClause); if (columns.length > 0) { return `CREATE INDEX idx_${tableName}_${columns.join('_')} ON ${tableName} (${columns.join(', ')});`; } } return 'Analyze query execution plan and add indexes on frequently filtered columns'; } extractColumnsFromWhere(whereClause) { const columns = []; const columnMatches = whereClause.match(/\b\w+\b\s*[=<>!]/g); if (columnMatches) { columnMatches.forEach(match => { const column = match.replace(/\s*[=<>!].*$/, '').trim(); if (column && !columns.includes(column)) { columns.push(column); } }); } return columns.slice(0, 3); } extractTableName(sql) { const normalizedSql = sql.toLowerCase().trim(); let match = normalizedSql.match(/from\s+`?(\w+)`?/); if (match) return match[1]; match = normalizedSql.match(/insert\s+into\s+`?(\w+)`?/); if (match) return match[1]; match = normalizedSql.match(/update\s+`?(\w+)`?/); if (match) return match[1]; match = normalizedSql.match(/delete\s+from\s+`?(\w+)`?/); if (match) return match[1]; return undefined; } determineSeverity(duration) { if (duration > 10000) return 'critical'; if (duration > 5000) return 'very_slow'; return 'slow'; } updateQueryPatterns(context) { const normalizedSql = this.normalizeQueryForPattern(context.sql); const pattern = this.queryPatterns.get(normalizedSql); if (pattern) { pattern.count++; pattern.averageDuration = (pattern.averageDuration * (pattern.count - 1) + (context.duration || 0)) / pattern.count; pattern.lastSeen = new Date(); } else { this.queryPatterns.set(normalizedSql, { pattern: normalizedSql, count: 1, averageDuration: context.duration || 0, lastSeen: new Date() }); } } normalizeQueryForPattern(sql) { return sql .replace(/\$\d+/g, '?') .replace(/\?/g, '?') .replace(/\s+/g, ' ') .replace(/\d+/g, 'N') .replace(/'[^']*'/g, "'?'") .trim() .substring(0, 200); } isFrequentQuery(sql) { const normalizedSql = this.normalizeQueryForPattern(sql); const pattern = this.queryPatterns.get(normalizedSql); return pattern ? pattern.count > 10 : false; } getQueryPatterns() { return Array.from(this.queryPatterns.values()) .sort((a, b) => b.count - a.count); } getSlowQueryPatterns() { return Array.from(this.queryPatterns.values()) .filter(pattern => pattern.averageDuration > 1000) .sort((a, b) => b.averageDuration - a.averageDuration); } getOptimizationSuggestions() { return Array.from(this.indexSuggestions.values()) .sort((a, b) => { const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); } clearPatterns() { this.queryPatterns.clear(); this.indexSuggestions.clear(); } }; exports.QueryAnalyzerService = QueryAnalyzerService; exports.QueryAnalyzerService = QueryAnalyzerService = QueryAnalyzerService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [typeorm_1.DataSource]) ], QueryAnalyzerService); //# sourceMappingURL=query-analyzer.service.js.map