UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

317 lines 12.8 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 RedisStorageDriver_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisStorageDriver = void 0; const common_1 = require("@nestjs/common"); const ioredis_1 = require("ioredis"); let RedisStorageDriver = RedisStorageDriver_1 = class RedisStorageDriver { constructor(config) { this.logger = new common_1.Logger(RedisStorageDriver_1.name); this.keyPrefix = 'telescope'; const redisConfig = config?.redis || {}; this.redis = new ioredis_1.Redis({ host: redisConfig.host || 'localhost', port: redisConfig.port || 6379, password: redisConfig.password, db: redisConfig.db || 0, retryDelayOnFailover: 100, maxRetriesPerRequest: 3, connectTimeout: 5000, commandTimeout: 5000, ...redisConfig.options }); this.ttl = redisConfig.ttl || 86400; this.redis.on('connect', () => { this.logger.log('Redis connected'); }); this.redis.on('error', (error) => { this.logger.error('Redis error:', error); }); } async store(entry) { try { const key = this.getEntryKey(entry.id); const indexKey = this.getIndexKey(entry.type); const timestampKey = this.getTimestampKey(); await this.redis.setex(key, this.ttl, JSON.stringify(entry)); await this.redis.zadd(indexKey, entry.timestamp.getTime(), entry.id); await this.redis.zadd(timestampKey, entry.timestamp.getTime(), entry.id); const metaKey = this.getMetaKey(entry.id); await this.redis.hset(metaKey, { type: entry.type, familyHash: entry.familyHash, tags: JSON.stringify(entry.tags), timestamp: entry.timestamp.getTime(), sequence: entry.sequence }); await this.redis.expire(metaKey, this.ttl); this.logger.debug(`Stored entry: ${entry.id}`); } catch (error) { this.logger.error(`Failed to store entry ${entry.id}:`, error); throw error; } } async storeBatch(entries) { if (entries.length === 0) return; try { const pipeline = this.redis.pipeline(); for (const entry of entries) { const key = this.getEntryKey(entry.id); const indexKey = this.getIndexKey(entry.type); const timestampKey = this.getTimestampKey(); const metaKey = this.getMetaKey(entry.id); pipeline.setex(key, this.ttl, JSON.stringify(entry)); pipeline.zadd(indexKey, entry.timestamp.getTime(), entry.id); pipeline.zadd(timestampKey, entry.timestamp.getTime(), entry.id); pipeline.hset(metaKey, { type: entry.type, familyHash: entry.familyHash, tags: JSON.stringify(entry.tags), timestamp: entry.timestamp.getTime(), sequence: entry.sequence }); pipeline.expire(metaKey, this.ttl); } await pipeline.exec(); this.logger.debug(`Stored batch of ${entries.length} entries`); } catch (error) { this.logger.error('Failed to store batch:', error); throw error; } } async find(filter) { try { let candidateIds = []; if (filter?.type) { const indexKey = this.getIndexKey(filter.type); candidateIds = await this.redis.zrevrange(indexKey, 0, -1); } else { const timestampKey = this.getTimestampKey(); candidateIds = await this.redis.zrevrange(timestampKey, 0, -1); } if (filter?.dateFrom || filter?.dateTo) { const minScore = filter.dateFrom ? filter.dateFrom.getTime() : 0; const maxScore = filter.dateTo ? filter.dateTo.getTime() : '+inf'; if (filter?.type) { const indexKey = this.getIndexKey(filter.type); candidateIds = await this.redis.zrevrangebyscore(indexKey, maxScore, minScore); } else { const timestampKey = this.getTimestampKey(); candidateIds = await this.redis.zrevrangebyscore(timestampKey, maxScore, minScore); } } if (filter?.tags && filter.tags.length > 0) { const filteredIds = []; for (const id of candidateIds) { const metaKey = this.getMetaKey(id); const tagsJson = await this.redis.hget(metaKey, 'tags'); if (tagsJson) { const tags = JSON.parse(tagsJson); if (filter.tags.some(tag => tags.includes(tag))) { filteredIds.push(id); } } } candidateIds = filteredIds; } const total = candidateIds.length; const offset = filter?.offset || 0; const limit = filter?.limit || 100; const paginatedIds = candidateIds.slice(offset, offset + limit); const entries = []; if (paginatedIds.length > 0) { const pipeline = this.redis.pipeline(); for (const id of paginatedIds) { const key = this.getEntryKey(id); pipeline.get(key); } const results = await pipeline.exec(); for (const [error, result] of results || []) { if (!error && result) { try { const entry = JSON.parse(result); if (entry.timestamp) { entry.timestamp = new Date(entry.timestamp); } entries.push(entry); } catch (parseError) { this.logger.error('Failed to parse entry:', parseError); } } } } return { entries, total, hasMore: offset + limit < total }; } catch (error) { this.logger.error('Failed to find entries:', error); throw error; } } async findById(id) { try { const key = this.getEntryKey(id); const result = await this.redis.get(key); if (!result) return null; const entry = JSON.parse(result); if (entry.timestamp) { entry.timestamp = new Date(entry.timestamp); } return entry; } catch (error) { this.logger.error(`Failed to find entry ${id}:`, error); return null; } } async delete(id) { try { const pipeline = this.redis.pipeline(); const metaKey = this.getMetaKey(id); const meta = await this.redis.hgetall(metaKey); if (meta && meta.type) { const key = this.getEntryKey(id); const indexKey = this.getIndexKey(meta.type); const timestampKey = this.getTimestampKey(); pipeline.del(key); pipeline.zrem(indexKey, id); pipeline.zrem(timestampKey, id); pipeline.del(metaKey); const results = await pipeline.exec(); return results ? results[0][1] === 1 : false; } return false; } catch (error) { this.logger.error(`Failed to delete entry ${id}:`, error); return false; } } async clear() { try { const pattern = `${this.keyPrefix}:*`; const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } this.logger.log('Redis storage cleared'); } catch (error) { this.logger.error('Failed to clear Redis storage:', error); throw error; } } async prune(olderThan) { try { const timestampKey = this.getTimestampKey(); const maxScore = olderThan.getTime(); const oldIds = await this.redis.zrangebyscore(timestampKey, 0, maxScore); if (oldIds.length === 0) return 0; const pipeline = this.redis.pipeline(); for (const id of oldIds) { const metaKey = this.getMetaKey(id); const meta = await this.redis.hgetall(metaKey); if (meta && meta.type) { const key = this.getEntryKey(id); const indexKey = this.getIndexKey(meta.type); pipeline.del(key); pipeline.zrem(indexKey, id); pipeline.zrem(timestampKey, id); pipeline.del(metaKey); } } await pipeline.exec(); this.logger.log(`Pruned ${oldIds.length} entries from Redis`); return oldIds.length; } catch (error) { this.logger.error('Failed to prune Redis storage:', error); throw error; } } async getStats() { try { const timestampKey = this.getTimestampKey(); const totalEntries = await this.redis.zcard(timestampKey); const typeKeys = await this.redis.keys(`${this.keyPrefix}:index:*`); const entriesByType = {}; for (const key of typeKeys) { const type = key.split(':').pop(); if (type) { const count = await this.redis.zcard(key); entriesByType[type] = count; } } const oldestScore = await this.redis.zrange(timestampKey, 0, 0, 'WITHSCORES'); const newestScore = await this.redis.zrevrange(timestampKey, 0, 0, 'WITHSCORES'); const oldestEntry = oldestScore.length > 1 ? new Date(parseInt(oldestScore[1])) : undefined; const newestEntry = newestScore.length > 1 ? new Date(parseInt(newestScore[1])) : undefined; return { totalEntries, entriesByType, oldestEntry, newestEntry }; } catch (error) { this.logger.error('Failed to get Redis storage stats:', error); throw error; } } async healthCheck() { try { const result = await this.redis.ping(); return result === 'PONG'; } catch (error) { return false; } } async cleanup() { try { await this.redis.disconnect(); this.logger.log('Redis connection closed'); } catch (error) { this.logger.error('Failed to cleanup Redis connection:', error); } } getEntryKey(id) { return `${this.keyPrefix}:entry:${id}`; } getIndexKey(type) { return `${this.keyPrefix}:index:${type}`; } getTimestampKey() { return `${this.keyPrefix}:timestamp`; } getMetaKey(id) { return `${this.keyPrefix}:meta:${id}`; } }; exports.RedisStorageDriver = RedisStorageDriver; exports.RedisStorageDriver = RedisStorageDriver = RedisStorageDriver_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [Object]) ], RedisStorageDriver); //# sourceMappingURL=redis-storage.driver.js.map