@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
969 lines • 39.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 __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var CacheWatcherService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheWatcherService = exports.CacheOperation = void 0;
const common_1 = require("@nestjs/common");
const telescope_service_1 = require("../../core/services/telescope.service");
const cache_watcher_config_1 = require("./cache-watcher.config");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const ioredis_1 = __importDefault(require("ioredis"));
var CacheOperation;
(function (CacheOperation) {
CacheOperation["GET"] = "get";
CacheOperation["SET"] = "set";
CacheOperation["DEL"] = "del";
CacheOperation["EXISTS"] = "exists";
CacheOperation["EXPIRE"] = "expire";
CacheOperation["TTL"] = "ttl";
CacheOperation["INCR"] = "incr";
CacheOperation["DECR"] = "decr";
CacheOperation["PUSH"] = "push";
CacheOperation["POP"] = "pop";
CacheOperation["FLUSH"] = "flush";
CacheOperation["FLUSHALL"] = "flushall";
CacheOperation["KEYS"] = "keys";
CacheOperation["SCAN"] = "scan";
CacheOperation["MGET"] = "mget";
CacheOperation["MSET"] = "mset";
CacheOperation["HGET"] = "hget";
CacheOperation["HSET"] = "hset";
CacheOperation["HDEL"] = "hdel";
CacheOperation["SADD"] = "sadd";
CacheOperation["SREM"] = "srem";
CacheOperation["ZADD"] = "zadd";
CacheOperation["ZREM"] = "zrem";
CacheOperation["PUBLISH"] = "publish";
CacheOperation["SUBSCRIBE"] = "subscribe";
CacheOperation["UNSUBSCRIBE"] = "unsubscribe";
CacheOperation["OTHER"] = "other";
})(CacheOperation || (exports.CacheOperation = CacheOperation = {}));
let CacheWatcherService = CacheWatcherService_1 = class CacheWatcherService {
constructor(telescopeService, cacheWatcherConfig, redisClientInstances) {
this.telescopeService = telescopeService;
this.redisClientInstances = redisClientInstances;
this.logger = new common_1.Logger(CacheWatcherService_1.name);
this.destroy$ = new rxjs_1.Subject();
this.metricsSubject = new rxjs_1.BehaviorSubject(this.initializeMetrics());
this.alertsSubject = new rxjs_1.Subject();
this.cacheContextSubject = new rxjs_1.Subject();
this.cacheHistory = [];
this.currentMetrics = this.initializeMetrics();
this.alertHistory = [];
this.keyPatterns = new Map();
this.responseTimes = [];
this.activeOperations = new Map();
this.redisClients = new Map();
this.redisInfo = new Map();
this.connectionPools = new Map();
this.config = { ...cache_watcher_config_1.defaultCacheWatcherConfig, ...cacheWatcherConfig };
}
async onModuleInit() {
if (!this.config.enabled) {
return;
}
await this.setupRedisIntegration();
this.startPeriodicProcessing();
this.startRedisMonitoring();
this.logger.log('Cache watcher initialized');
}
onModuleDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cleanupRedisConnections();
}
initializeMetrics() {
return {
totalOperations: 0,
hitCount: 0,
missCount: 0,
errorCount: 0,
evictionCount: 0,
averageResponseTime: 0,
slowOperations: 0,
operationsPerSecond: 0,
hitRate: 0,
missRate: 0,
errorRate: 0,
operationsByType: {},
operationsByResult: {},
topKeyPatterns: [],
slowOperationsByType: {},
totalMemoryUsage: 0,
averageValueSize: 0,
keyCount: 0,
activeConnections: 0,
connectionPool: {
total: 0,
active: 0,
idle: 0,
waiting: 0,
},
healthScore: 100,
healthStatus: 'healthy',
trends: {
lastHour: this.initializeTrendData(),
lastDay: this.initializeTrendData(),
lastWeek: this.initializeTrendData(),
},
};
}
initializeTrendData() {
return {
totalOperations: 0,
hitCount: 0,
missCount: 0,
errorCount: 0,
averageResponseTime: 0,
hitRate: 0,
change: 0,
changePercent: 0,
peak: 0,
};
}
trackCacheOperation(context) {
if (!this.config.enabled) {
return;
}
try {
if (Math.random() * 100 > this.config.sampleRate) {
return;
}
if (this.shouldExcludeOperation(context)) {
return;
}
this.sanitizeContext(context);
this.addToHistory(context);
this.updateMetrics(context);
this.trackKeyPattern(context);
const entry = this.createTelescopeEntry(context);
this.telescopeService.record(entry);
this.checkAlerts(context);
this.correlateCache(context);
this.cacheContextSubject.next(context);
this.logger.debug(`Cache operation tracked: ${context.operation} ${context.key}`);
}
catch (error) {
this.logger.error('Failed to track cache operation:', error);
}
}
shouldExcludeOperation(context) {
if (this.config.excludeOperations.includes(context.operation)) {
return true;
}
for (const pattern of this.config.excludeKeyPatterns) {
if (this.matchesPattern(context.key, pattern)) {
return true;
}
}
if (this.config.includeKeyPatterns.length > 0) {
const included = this.config.includeKeyPatterns.some(pattern => this.matchesPattern(context.key, pattern));
if (!included) {
return true;
}
}
return false;
}
matchesPattern(key, pattern) {
const regexPattern = pattern.replace(/\*/g, '.*');
return new RegExp(regexPattern).test(key);
}
sanitizeContext(context) {
if (this.config.sensitiveKeyPatterns.some(pattern => this.matchesPattern(context.key, pattern))) {
context.key = this.sanitizeKey(context.key);
}
if (this.config.sanitizeValues && context.value) {
context.value = this.sanitizeValue(context.value);
}
if (context.key.length > this.config.maxKeyLength) {
context.key = context.key.substring(0, this.config.maxKeyLength) + '...';
}
}
sanitizeKey(key) {
return key.replace(/([a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64})/gi, '[HASH]');
}
sanitizeValue(value) {
if (!value)
return value;
if (typeof value === 'string') {
const jsonString = JSON.stringify(value);
if (jsonString.length > this.config.maxValueSize) {
return { _truncated: true, _size: jsonString.length };
}
return value;
}
if (typeof value === 'object') {
const sanitized = { ...value };
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
const jsonString = JSON.stringify(sanitized);
if (jsonString.length > this.config.maxValueSize) {
return { _truncated: true, _size: jsonString.length };
}
return sanitized;
}
return value;
}
addToHistory(context) {
this.cacheHistory.push(context);
if (this.cacheHistory.length > this.config.maxHistorySize) {
this.cacheHistory.shift();
}
const retentionDate = new Date(Date.now() - this.config.retentionPeriod);
this.cacheHistory = this.cacheHistory.filter(cache => cache.timestamp > retentionDate);
}
updateMetrics(context) {
this.currentMetrics.totalOperations++;
if (context.error) {
this.currentMetrics.errorCount++;
}
else if (context.hit) {
this.currentMetrics.hitCount++;
}
else {
this.currentMetrics.missCount++;
}
this.currentMetrics.operationsByType[context.operation] =
(this.currentMetrics.operationsByType[context.operation] || 0) + 1;
const resultType = context.error ? 'error' : context.hit ? 'hit' : 'miss';
this.currentMetrics.operationsByResult[resultType] =
(this.currentMetrics.operationsByResult[resultType] || 0) + 1;
if (context.duration) {
this.responseTimes.push(context.duration);
this.currentMetrics.averageResponseTime = this.calculateAverage(this.responseTimes);
if (context.duration > this.config.slowOperationThreshold) {
this.currentMetrics.slowOperations++;
this.currentMetrics.slowOperationsByType[context.operation] =
(this.currentMetrics.slowOperationsByType[context.operation] || 0) + 1;
}
}
this.updateRates();
this.updateHealthScore();
}
trackKeyPattern(context) {
if (!this.config.captureKeyPatterns)
return;
const pattern = this.extractKeyPattern(context.key);
const existing = this.keyPatterns.get(pattern) || { count: 0, hits: 0, responseTimes: [] };
existing.count++;
if (context.hit) {
existing.hits++;
}
if (context.duration) {
existing.responseTimes.push(context.duration);
}
this.keyPatterns.set(pattern, existing);
}
extractKeyPattern(key) {
return key
.replace(/\d+/g, 'N')
.replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, 'UUID')
.replace(/[a-f0-9]{32}/gi, 'HASH32')
.replace(/[a-f0-9]{40}/gi, 'HASH40')
.replace(/[a-f0-9]{64}/gi, 'HASH64')
.replace(/\d{4}-\d{2}-\d{2}/g, 'DATE')
.replace(/\d{2}:\d{2}:\d{2}/g, 'TIME');
}
calculateAverage(values) {
if (values.length === 0)
return 0;
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
updateRates() {
const now = Date.now();
const oneMinuteAgo = now - 60 * 1000;
const recentOperations = this.cacheHistory.filter(cache => cache.timestamp.getTime() > oneMinuteAgo);
this.currentMetrics.operationsPerSecond = recentOperations.length / 60;
const total = this.currentMetrics.totalOperations || 1;
this.currentMetrics.hitRate = (this.currentMetrics.hitCount / total) * 100;
this.currentMetrics.missRate = (this.currentMetrics.missCount / total) * 100;
this.currentMetrics.errorRate = (this.currentMetrics.errorCount / total) * 100;
this.updateTopKeyPatterns();
}
updateTopKeyPatterns() {
this.currentMetrics.topKeyPatterns = Array.from(this.keyPatterns.entries())
.map(([pattern, data]) => ({
pattern,
count: data.count,
hitRate: data.count > 0 ? (data.hits / data.count) * 100 : 0,
avgResponseTime: this.calculateAverage(data.responseTimes),
}))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
}
updateHealthScore() {
let score = 100;
const issues = [];
if (this.currentMetrics.hitRate < 50) {
score -= 30;
issues.push('Low cache hit rate');
}
else if (this.currentMetrics.hitRate < 70) {
score -= 15;
issues.push('Moderate cache hit rate');
}
if (this.currentMetrics.errorRate > 10) {
score -= 25;
issues.push('High error rate');
}
else if (this.currentMetrics.errorRate > 5) {
score -= 10;
issues.push('Moderate error rate');
}
if (this.currentMetrics.slowOperations > this.currentMetrics.totalOperations * 0.1) {
score -= 20;
issues.push('Many slow operations');
}
if (this.currentMetrics.averageResponseTime > 100) {
score -= 15;
issues.push('High average response time');
}
this.currentMetrics.healthScore = Math.max(0, score);
if (score >= 80) {
this.currentMetrics.healthStatus = 'healthy';
}
else if (score >= 60) {
this.currentMetrics.healthStatus = 'warning';
}
else {
this.currentMetrics.healthStatus = 'critical';
}
}
createTelescopeEntry(context) {
const entryId = `cache_${context.id}`;
const familyHash = `${context.operation}:${context.keyPattern || context.key}`;
return {
id: entryId,
type: 'cache',
familyHash,
content: {
cache: {
operation: context.operation,
key: context.key,
keyPattern: context.keyPattern,
hit: context.hit,
ttl: context.ttl,
size: context.size,
cacheInstance: context.cacheInstance,
cacheType: context.cacheType,
},
execution: {
startTime: context.startTime,
endTime: context.endTime,
duration: context.duration,
result: context.result,
error: context.error,
},
value: this.config.captureValues ? context.value : undefined,
performance: context.performance,
connection: {
id: context.connectionId,
host: context.host,
port: context.port,
database: context.database,
},
redis: {
command: context.redisCommand,
args: context.redisArgs,
},
correlation: {
traceId: context.traceId,
requestId: context.requestId,
userId: context.userId,
sessionId: context.sessionId,
},
},
tags: this.generateTags(context),
timestamp: context.timestamp,
sequence: context.timestamp.getTime(),
};
}
generateTags(context) {
const tags = [
'cache',
`operation:${context.operation}`,
`result:${context.error ? 'error' : context.hit ? 'hit' : 'miss'}`,
];
if (context.cacheInstance) {
tags.push(`instance:${context.cacheInstance}`);
}
if (context.cacheType) {
tags.push(`type:${context.cacheType}`);
}
if (context.duration && context.duration > this.config.slowOperationThreshold) {
tags.push('slow');
}
if (context.keyPattern) {
tags.push(`pattern:${context.keyPattern}`);
}
return tags;
}
checkAlerts(context) {
if (!this.config.enableRealTimeAlerts) {
return;
}
if (this.currentMetrics.hitRate < this.config.alertThresholds.hitRate) {
this.createAlert({
type: 'hit_rate',
severity: 'medium',
message: `Cache hit rate below threshold: ${this.currentMetrics.hitRate.toFixed(1)}%`,
data: { hitRate: this.currentMetrics.hitRate, threshold: this.config.alertThresholds.hitRate },
cacheInstance: context.cacheInstance,
});
}
if (this.currentMetrics.missRate > this.config.alertThresholds.missRate) {
this.createAlert({
type: 'miss_rate',
severity: 'medium',
message: `Cache miss rate above threshold: ${this.currentMetrics.missRate.toFixed(1)}%`,
data: { missRate: this.currentMetrics.missRate, threshold: this.config.alertThresholds.missRate },
cacheInstance: context.cacheInstance,
});
}
if (this.currentMetrics.errorRate > this.config.alertThresholds.errorRate) {
this.createAlert({
type: 'error_rate',
severity: 'high',
message: `Cache error rate above threshold: ${this.currentMetrics.errorRate.toFixed(1)}%`,
data: { errorRate: this.currentMetrics.errorRate, threshold: this.config.alertThresholds.errorRate },
cacheInstance: context.cacheInstance,
});
}
if (context.duration && context.duration > this.config.alertThresholds.avgResponseTime) {
this.createAlert({
type: 'slow_operations',
severity: 'medium',
message: `Slow cache operation: ${context.operation} ${context.key} took ${context.duration}ms`,
data: { duration: context.duration, threshold: this.config.alertThresholds.avgResponseTime },
cacheInstance: context.cacheInstance,
});
}
}
createAlert(alert) {
const fullAlert = {
id: `cache_alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
acknowledged: false,
...alert,
};
this.alertHistory.push(fullAlert);
this.alertsSubject.next(fullAlert);
this.logger.warn(`Cache alert: ${fullAlert.message}`, fullAlert.data);
}
correlateCache(context) {
if (context.traceId) {
this.logger.debug(`Correlated cache operation ${context.operation} with trace ${context.traceId}`);
}
}
startPeriodicProcessing() {
(0, rxjs_1.interval)(30000)
.pipe((0, operators_1.takeUntil)(this.destroy$))
.subscribe(() => {
this.updateTrends();
this.cleanupOldData();
this.metricsSubject.next({ ...this.currentMetrics });
});
}
updateTrends() {
const now = Date.now();
const oneHourAgo = now - 60 * 60 * 1000;
const oneDayAgo = now - 24 * 60 * 60 * 1000;
const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
this.currentMetrics.trends.lastHour = this.calculateTrend(oneHourAgo);
this.currentMetrics.trends.lastDay = this.calculateTrend(oneDayAgo);
this.currentMetrics.trends.lastWeek = this.calculateTrend(oneWeekAgo);
}
calculateTrend(since) {
const operations = this.cacheHistory.filter(cache => cache.timestamp.getTime() > since);
const hits = operations.filter(op => op.hit && !op.error);
const misses = operations.filter(op => !op.hit && !op.error);
const errors = operations.filter(op => op.error);
const responseTimes = operations.filter(op => op.duration).map(op => op.duration);
const totalOps = operations.length;
const hitRate = totalOps > 0 ? (hits.length / totalOps) * 100 : 0;
return {
totalOperations: totalOps,
hitCount: hits.length,
missCount: misses.length,
errorCount: errors.length,
averageResponseTime: this.calculateAverage(responseTimes),
hitRate,
change: 0,
changePercent: 0,
peak: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
};
}
cleanupOldData() {
const now = Date.now();
const retentionDate = new Date(now - this.config.retentionPeriod);
this.cacheHistory = this.cacheHistory.filter(cache => cache.timestamp > retentionDate);
this.alertHistory = this.alertHistory.filter(alert => alert.timestamp > retentionDate);
if (this.responseTimes.length > 1000) {
this.responseTimes = this.responseTimes.slice(-1000);
}
if (this.keyPatterns.size > 100) {
const sorted = Array.from(this.keyPatterns.entries())
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 100);
this.keyPatterns.clear();
sorted.forEach(([pattern, data]) => {
this.keyPatterns.set(pattern, data);
});
}
}
getMetrics() {
return { ...this.currentMetrics };
}
getMetricsStream() {
return this.metricsSubject.asObservable().pipe((0, operators_1.shareReplay)(1));
}
getAlertsStream() {
return this.alertsSubject.asObservable();
}
getCacheStream() {
return this.cacheContextSubject.asObservable();
}
getRecentOperations(limit = 100) {
return this.cacheHistory.slice(-limit).reverse();
}
getOperationsByType(operation, limit = 100) {
return this.cacheHistory
.filter(cache => cache.operation === operation)
.slice(-limit)
.reverse();
}
getOperationsByKeyPattern(pattern, limit = 100) {
return this.cacheHistory
.filter(cache => cache.keyPattern === pattern)
.slice(-limit)
.reverse();
}
getCacheHealth(instance) {
if (instance) {
return this.calculateCacheHealth(instance);
}
const instances = new Set(this.cacheHistory
.map(cache => cache.cacheInstance)
.filter(Boolean));
return Array.from(instances).map(inst => this.calculateCacheHealth(inst));
}
calculateCacheHealth(instance) {
const instanceOperations = this.cacheHistory.filter(cache => cache.cacheInstance === instance);
const totalOps = instanceOperations.length;
const hits = instanceOperations.filter(op => op.hit && !op.error).length;
const errors = instanceOperations.filter(op => op.error).length;
const responseTimes = instanceOperations.filter(op => op.duration).map(op => op.duration);
const hitRate = totalOps > 0 ? (hits / totalOps) * 100 : 0;
const errorRate = totalOps > 0 ? (errors / totalOps) * 100 : 0;
const avgResponseTime = this.calculateAverage(responseTimes);
let score = 100;
const issues = [];
const recommendations = [];
if (hitRate < 70) {
score -= 25;
issues.push('Low hit rate');
recommendations.push('Review cache keys and TTL settings');
}
if (errorRate > 5) {
score -= 20;
issues.push('High error rate');
recommendations.push('Check cache server health and connectivity');
}
if (avgResponseTime > 50) {
score -= 15;
issues.push('High response time');
recommendations.push('Optimize cache server performance or add replicas');
}
let status = 'healthy';
if (score < 60) {
status = 'critical';
}
else if (score < 80) {
status = 'warning';
}
return {
instance,
status,
score,
issues,
recommendations,
metrics: {
hitRate,
errorRate,
avgResponseTime,
memoryUsage: 0,
connectionCount: 0,
},
};
}
acknowledgeAlert(alertId) {
const alert = this.alertHistory.find(a => a.id === alertId);
if (!alert)
return false;
alert.acknowledged = true;
return true;
}
getConfig() {
return { ...this.config };
}
async setupRedisIntegration() {
if (!this.config.redisIntegration?.enabled) {
return;
}
try {
if (this.redisClientInstances) {
for (const [name, client] of this.redisClientInstances) {
this.redisClients.set(name, client);
await this.setupRedisMonitoring(name, client);
}
}
else if (this.config.redisIntegration.autoDiscoverInstances) {
await this.discoverRedisInstances();
}
this.logger.log(`Redis integration setup complete: ${this.redisClients.size} instances`);
}
catch (error) {
this.logger.error('Failed to setup Redis integration:', error);
}
}
async discoverRedisInstances() {
const defaultHosts = [
{ host: 'localhost', port: 6379, name: 'default' },
{ host: '127.0.0.1', port: 6379, name: 'local' },
];
for (const config of defaultHosts) {
try {
const client = new ioredis_1.default(config.port, config.host, {
enableReadyCheck: false,
maxRetriesPerRequest: 1,
});
await client.ping();
this.redisClients.set(config.name, client);
await this.setupRedisMonitoring(config.name, client);
this.logger.log(`Discovered Redis instance: ${config.name} at ${config.host}:${config.port}`);
}
catch (error) {
this.logger.debug(`Failed to connect to Redis at ${config.host}:${config.port}`);
}
}
}
async setupRedisMonitoring(name, client) {
try {
const info = await client.info();
this.redisInfo.set(name, this.parseRedisInfo(info));
client.on('connect', () => {
this.logger.log(`Redis ${name} connected`);
});
client.on('error', (error) => {
this.logger.error(`Redis ${name} error:`, error);
this.createAlert({
type: 'connection_limit',
severity: 'high',
message: `Redis ${name} connection error: ${error.message}`,
data: { instance: name, error: error.message },
cacheInstance: name,
});
});
client.on('reconnecting', () => {
this.logger.warn(`Redis ${name} reconnecting`);
});
if (this.config.redisIntegration?.monitorCommands) {
await this.setupRedisCommandMonitoring(name, client);
}
}
catch (error) {
this.logger.error(`Failed to setup monitoring for Redis ${name}:`, error);
}
}
async setupRedisCommandMonitoring(name, client) {
try {
const originalSendCommand = client.sendCommand.bind(client);
client.sendCommand = (command, stream) => {
const startTime = Date.now();
const context = {
id: `redis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
operation: this.mapRedisCommandToOperation(command.name),
key: command.args?.[0] || 'unknown',
startTime: new Date(),
cacheInstance: name,
cacheType: 'redis',
redisCommand: command.name,
redisArgs: command.args,
};
const result = originalSendCommand(command, stream);
if (result && typeof result.then === 'function') {
result
.then((res) => {
context.endTime = new Date();
context.duration = Date.now() - startTime;
context.hit = res !== null && res !== undefined;
context.result = res;
this.trackCacheOperation(context);
})
.catch((error) => {
context.endTime = new Date();
context.duration = Date.now() - startTime;
context.hit = false;
context.error = error;
this.trackCacheOperation(context);
});
}
return result;
};
}
catch (error) {
this.logger.error(`Failed to setup command monitoring for Redis ${name}:`, error);
}
}
mapRedisCommandToOperation(command) {
const cmd = command.toLowerCase();
switch (cmd) {
case 'get': return CacheOperation.GET;
case 'set': return CacheOperation.SET;
case 'del': return CacheOperation.DEL;
case 'exists': return CacheOperation.EXISTS;
case 'expire': return CacheOperation.EXPIRE;
case 'ttl': return CacheOperation.TTL;
case 'incr': return CacheOperation.INCR;
case 'decr': return CacheOperation.DECR;
case 'lpush':
case 'rpush': return CacheOperation.PUSH;
case 'lpop':
case 'rpop': return CacheOperation.POP;
case 'flushdb': return CacheOperation.FLUSH;
case 'flushall': return CacheOperation.FLUSHALL;
case 'keys': return CacheOperation.KEYS;
case 'scan': return CacheOperation.SCAN;
case 'mget': return CacheOperation.MGET;
case 'mset': return CacheOperation.MSET;
case 'hget': return CacheOperation.HGET;
case 'hset': return CacheOperation.HSET;
case 'hdel': return CacheOperation.HDEL;
case 'sadd': return CacheOperation.SADD;
case 'srem': return CacheOperation.SREM;
case 'zadd': return CacheOperation.ZADD;
case 'zrem': return CacheOperation.ZREM;
case 'publish': return CacheOperation.PUBLISH;
case 'subscribe': return CacheOperation.SUBSCRIBE;
case 'unsubscribe': return CacheOperation.UNSUBSCRIBE;
default: return CacheOperation.OTHER;
}
}
parseRedisInfo(info) {
const parsed = {};
const sections = info.split('\r\n\r\n');
for (const section of sections) {
const lines = section.split('\r\n');
const sectionName = lines[0]?.replace('# ', '');
if (sectionName) {
parsed[sectionName] = {};
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line && line.includes(':')) {
const [key, value] = line.split(':');
parsed[sectionName][key] = isNaN(Number(value)) ? value : Number(value);
}
}
}
}
return parsed;
}
startRedisMonitoring() {
if (!this.config.redisIntegration?.enabled) {
return;
}
(0, rxjs_1.interval)(60000)
.pipe((0, operators_1.takeUntil)(this.destroy$))
.subscribe(async () => {
await this.updateRedisInfo();
});
}
async updateRedisInfo() {
for (const [name, client] of this.redisClients) {
try {
const info = await client.info();
const parsed = this.parseRedisInfo(info);
this.redisInfo.set(name, parsed);
this.updateRedisMetrics(name, parsed);
}
catch (error) {
this.logger.error(`Failed to get Redis info for ${name}:`, error);
}
}
}
updateRedisMetrics(instanceName, redisInfo) {
if (!this.currentMetrics.redisInfo) {
this.currentMetrics.redisInfo = {
version: '',
uptime: 0,
memory: { used: 0, peak: 0, fragmentation: 0 },
stats: { commands: 0, connections: 0, keyspaceHits: 0, keyspaceMisses: 0 },
clients: { connected: 0, blocked: 0, tracking: 0 },
};
}
if (redisInfo.Server) {
this.currentMetrics.redisInfo.version = redisInfo.Server.redis_version || '';
this.currentMetrics.redisInfo.uptime = redisInfo.Server.uptime_in_seconds || 0;
}
if (redisInfo.Memory) {
this.currentMetrics.redisInfo.memory.used = redisInfo.Memory.used_memory || 0;
this.currentMetrics.redisInfo.memory.peak = redisInfo.Memory.used_memory_peak || 0;
this.currentMetrics.redisInfo.memory.fragmentation = redisInfo.Memory.mem_fragmentation_ratio || 0;
}
if (redisInfo.Stats) {
this.currentMetrics.redisInfo.stats.commands = redisInfo.Stats.total_commands_processed || 0;
this.currentMetrics.redisInfo.stats.connections = redisInfo.Stats.total_connections_received || 0;
this.currentMetrics.redisInfo.stats.keyspaceHits = redisInfo.Stats.keyspace_hits || 0;
this.currentMetrics.redisInfo.stats.keyspaceMisses = redisInfo.Stats.keyspace_misses || 0;
}
if (redisInfo.Clients) {
this.currentMetrics.redisInfo.clients.connected = redisInfo.Clients.connected_clients || 0;
this.currentMetrics.redisInfo.clients.blocked = redisInfo.Clients.blocked_clients || 0;
this.currentMetrics.redisInfo.clients.tracking = redisInfo.Clients.tracking_clients || 0;
}
this.currentMetrics.connectionPool.active = this.currentMetrics.redisInfo.clients.connected;
this.currentMetrics.activeConnections = this.currentMetrics.redisInfo.clients.connected;
this.checkRedisAlerts(instanceName, redisInfo);
}
checkRedisAlerts(instanceName, redisInfo) {
if (redisInfo.Memory?.used_memory) {
const memoryUsageMB = redisInfo.Memory.used_memory / (1024 * 1024);
const memoryThreshold = this.config.alertThresholds.memoryUsage || 100;
if (memoryUsageMB > memoryThreshold) {
this.createAlert({
type: 'memory_usage',
severity: memoryUsageMB > memoryThreshold * 2 ? 'critical' : 'high',
message: `Redis ${instanceName} memory usage: ${memoryUsageMB.toFixed(1)}MB`,
data: { memoryUsage: memoryUsageMB, threshold: memoryThreshold },
cacheInstance: instanceName,
});
}
}
if (redisInfo.Clients?.connected_clients) {
const connectionThreshold = this.config.alertThresholds.connectionCount || 100;
if (redisInfo.Clients.connected_clients > connectionThreshold) {
this.createAlert({
type: 'connection_limit',
severity: 'medium',
message: `Redis ${instanceName} high connection count: ${redisInfo.Clients.connected_clients}`,
data: { connections: redisInfo.Clients.connected_clients, threshold: connectionThreshold },
cacheInstance: instanceName,
});
}
}
if (redisInfo.Memory?.mem_fragmentation_ratio) {
const fragmentation = redisInfo.Memory.mem_fragmentation_ratio;
if (fragmentation > 1.5) {
this.createAlert({
type: 'memory_usage',
severity: fragmentation > 2.0 ? 'high' : 'medium',
message: `Redis ${instanceName} high memory fragmentation: ${fragmentation.toFixed(2)}`,
data: { fragmentation, threshold: 1.5 },
cacheInstance: instanceName,
});
}
}
}
cleanupRedisConnections() {
for (const [name, client] of this.redisClients) {
try {
client.disconnect();
this.logger.log(`Disconnected from Redis ${name}`);
}
catch (error) {
this.logger.error(`Failed to disconnect from Redis ${name}:`, error);
}
}
this.redisClients.clear();
}
getRedisInfo(instanceName) {
if (instanceName) {
return this.redisInfo.get(instanceName);
}
return Object.fromEntries(this.redisInfo);
}
getRedisHealth() {
return Array.from(this.redisInfo.entries()).map(([instance, info]) => ({
instance,
status: this.calculateRedisHealth(info),
info,
}));
}
calculateRedisHealth(info) {
let score = 100;
if (info.Memory?.mem_fragmentation_ratio > 2.0) {
score -= 30;
}
else if (info.Memory?.mem_fragmentation_ratio > 1.5) {
score -= 15;
}
const connections = info.Clients?.connected_clients || 0;
if (connections > 80) {
score -= 20;
}
const hits = info.Stats?.keyspace_hits || 0;
const misses = info.Stats?.keyspace_misses || 0;
const hitRate = hits + misses > 0 ? (hits / (hits + misses)) * 100 : 100;
if (hitRate < 70) {
score -= 25;
}
if (score >= 80)
return 'healthy';
if (score >= 60)
return 'warning';
return 'critical';
}
async getKeyspaceInfo(instanceName) {
if (instanceName && this.redisClients.has(instanceName)) {
const client = this.redisClients.get(instanceName);
try {
const info = await client.info('keyspace');
return this.parseRedisInfo(info);
}
catch (error) {
this.logger.error(`Failed to get keyspace info for ${instanceName}:`, error);
return null;
}
}
const results = {};
for (const [name, client] of this.redisClients) {
try {
const info = await client.info('keyspace');
results[name] = this.parseRedisInfo(info);
}
catch (error) {
this.logger.error(`Failed to get keyspace info for ${name}:`, error);
}
}
return results;
}
};
exports.CacheWatcherService = CacheWatcherService;
exports.CacheWatcherService = CacheWatcherService = CacheWatcherService_1 = __decorate([
(0, common_1.Injectable)(),
__param(1, (0, common_1.Inject)('CACHE_WATCHER_CONFIG')),
__param(2, (0, common_1.Optional)()),
__param(2, (0, common_1.Inject)('REDIS_CLIENTS')),
__metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object, Map])
], CacheWatcherService);
//# sourceMappingURL=cache-watcher.service.js.map