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