@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
344 lines • 15.3 kB
JavaScript
"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 QueryWatcherInterceptor_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryWatcherInterceptor = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("typeorm");
const query_watcher_service_1 = require("./query-watcher.service");
let QueryWatcherInterceptor = QueryWatcherInterceptor_1 = class QueryWatcherInterceptor {
constructor(queryWatcher, dataSource) {
this.queryWatcher = queryWatcher;
this.dataSource = dataSource;
this.logger = new common_1.Logger(QueryWatcherInterceptor_1.name);
this.originalMethods = new Map();
this.queryCounter = 0;
}
async setupInterception() {
try {
await this.interceptQueryRunner();
await this.interceptQueryBuilder();
await this.interceptRepository();
this.logger.log('Query interception setup completed');
}
catch (error) {
this.logger.error('Failed to setup query interception:', error);
}
}
async interceptQueryRunner() {
const queryRunner = this.dataSource.createQueryRunner();
const originalQuery = queryRunner.query.bind(queryRunner);
queryRunner.query = async (query, parameters) => {
const context = this.createQueryContext(query, parameters);
try {
const result = await originalQuery(query, parameters);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = Array.isArray(result) ? result.length : 1;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
};
}
async interceptQueryBuilder() {
const originalMethods = {
getMany: typeorm_1.SelectQueryBuilder.prototype.getMany,
getOne: typeorm_1.SelectQueryBuilder.prototype.getOne,
getRawMany: typeorm_1.SelectQueryBuilder.prototype.getRawMany,
getRawOne: typeorm_1.SelectQueryBuilder.prototype.getRawOne,
getCount: typeorm_1.SelectQueryBuilder.prototype.getCount,
execute: typeorm_1.SelectQueryBuilder.prototype.execute
};
typeorm_1.SelectQueryBuilder.prototype.getMany = async function () {
const context = this.createQueryBuilderContext(this, 'select');
try {
const result = await originalMethods.getMany.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = result.length;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
typeorm_1.SelectQueryBuilder.prototype.getOne = async function () {
const context = this.createQueryBuilderContext(this, 'select');
try {
const result = await originalMethods.getOne.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = result ? 1 : 0;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
typeorm_1.SelectQueryBuilder.prototype.getRawMany = async function () {
const context = this.createQueryBuilderContext(this, 'select');
try {
const result = await originalMethods.getRawMany.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = result.length;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
typeorm_1.SelectQueryBuilder.prototype.getRawOne = async function () {
const context = this.createQueryBuilderContext(this, 'select');
try {
const result = await originalMethods.getRawOne.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = result ? 1 : 0;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
typeorm_1.SelectQueryBuilder.prototype.getCount = async function () {
const context = this.createQueryBuilderContext(this, 'select');
try {
const result = await originalMethods.getCount.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.resultCount = 1;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
typeorm_1.SelectQueryBuilder.prototype.execute = async function () {
const context = this.createQueryBuilderContext(this, 'raw');
try {
const result = await originalMethods.execute.call(this);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.affectedRows = result.affected || 0;
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
}.bind(this);
}
async interceptRepository() {
const repositories = this.dataSource.entityMetadatas.map(metadata => this.dataSource.getRepository(metadata.target));
for (const repository of repositories) {
this.interceptRepositoryMethods(repository);
}
}
interceptRepositoryMethods(repository) {
const methodsToIntercept = [
'find', 'findOne', 'findOneBy', 'findBy', 'findAndCount', 'findAndCountBy',
'count', 'countBy', 'save', 'insert', 'update', 'upsert', 'delete', 'remove'
];
for (const methodName of methodsToIntercept) {
if (typeof repository[methodName] === 'function') {
const originalMethod = repository[methodName];
repository[methodName] = async (...args) => {
const context = this.createRepositoryContext(repository, methodName, args);
try {
const result = await originalMethod.apply(repository, args);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
if (Array.isArray(result)) {
context.resultCount = result.length;
}
else if (result && typeof result === 'object') {
context.resultCount = 1;
context.affectedRows = result.affected || result.generatedMaps?.length || 1;
}
this.queryWatcher.trackQuery(context);
return result;
}
catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
context.error = error;
this.queryWatcher.trackQuery(context);
throw error;
}
};
}
}
}
createQueryContext(sql, parameters) {
return {
id: this.generateQueryId(),
sql: sql.trim(),
parameters: parameters || [],
startTime: Date.now(),
operation: this.detectOperationType(sql),
tableName: this.extractTableName(sql),
stack: this.captureStack(),
traceId: this.generateTraceId()
};
}
createQueryBuilderContext(queryBuilder, operation) {
const sql = queryBuilder.getSql();
const parameters = queryBuilder.getParameters();
return {
id: this.generateQueryId(),
sql: sql.trim(),
parameters: Object.values(parameters),
startTime: Date.now(),
operation: operation,
entityName: queryBuilder.expressionMap?.mainAlias?.metadata?.name,
tableName: queryBuilder.expressionMap?.mainAlias?.metadata?.tableName,
stack: this.captureStack(),
traceId: this.generateTraceId()
};
}
createRepositoryContext(repository, method, args) {
const entityName = repository.metadata?.name;
const tableName = repository.metadata?.tableName;
return {
id: this.generateQueryId(),
sql: `Repository.${method}`,
parameters: args,
startTime: Date.now(),
operation: this.mapRepositoryMethodToOperation(method),
entityName,
tableName,
stack: this.captureStack(),
traceId: this.generateTraceId()
};
}
detectOperationType(sql) {
const normalizedSql = sql.toLowerCase().trim();
if (normalizedSql.startsWith('select'))
return 'select';
if (normalizedSql.startsWith('insert'))
return 'insert';
if (normalizedSql.startsWith('update'))
return 'update';
if (normalizedSql.startsWith('delete'))
return 'delete';
return 'raw';
}
mapRepositoryMethodToOperation(method) {
const selectMethods = ['find', 'findOne', 'findOneBy', 'findBy', 'findAndCount', 'findAndCountBy', 'count', 'countBy'];
const insertMethods = ['save', 'insert'];
const updateMethods = ['update', 'upsert'];
const deleteMethods = ['delete', 'remove'];
if (selectMethods.includes(method))
return 'select';
if (insertMethods.includes(method))
return 'insert';
if (updateMethods.includes(method))
return 'update';
if (deleteMethods.includes(method))
return 'delete';
return 'raw';
}
extractTableName(sql) {
const normalizedSql = sql.toLowerCase().trim();
let match = normalizedSql.match(/from\s+`?(\w+)`?/i);
if (match)
return match[1];
match = normalizedSql.match(/insert\s+into\s+`?(\w+)`?/i);
if (match)
return match[1];
match = normalizedSql.match(/update\s+`?(\w+)`?/i);
if (match)
return match[1];
match = normalizedSql.match(/delete\s+from\s+`?(\w+)`?/i);
if (match)
return match[1];
return undefined;
}
captureStack() {
const stack = new Error().stack;
if (!stack)
return '';
const lines = stack.split('\n');
const filteredLines = lines.filter(line => !line.includes('QueryWatcherInterceptor') &&
!line.includes('node_modules/typeorm') &&
!line.includes('node_modules/@nestjs'));
return filteredLines.slice(0, 10).join('\n');
}
generateQueryId() {
return `query_${Date.now()}_${++this.queryCounter}`;
}
generateTraceId() {
return `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async getConnectionInfo() {
const driver = this.dataSource.driver;
const pool = driver.pool;
return {
id: this.dataSource.name || 'default',
database: this.dataSource.options.database,
host: this.dataSource.options.host || 'localhost',
port: this.dataSource.options.port || 5432,
username: this.dataSource.options.username || 'unknown',
poolSize: pool?.config?.max || 10,
activeConnections: pool?.totalCount || 0,
idleConnections: pool?.idleCount || 0,
waitingConnections: pool?.waitingCount || 0,
acquiredConnections: pool?.acquiredCount || 0
};
}
async cleanup() {
this.logger.log('Query interception cleanup completed');
}
};
exports.QueryWatcherInterceptor = QueryWatcherInterceptor;
exports.QueryWatcherInterceptor = QueryWatcherInterceptor = QueryWatcherInterceptor_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [query_watcher_service_1.QueryWatcherService,
typeorm_1.DataSource])
], QueryWatcherInterceptor);
//# sourceMappingURL=query-watcher.interceptor.js.map