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