@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
145 lines (115 loc) • 5.03 kB
text/typescript
import { ILogger } from '../../logger/types';
import { Method, MultiConfigs, MultiMethodExceptions, MultiMethodLatencies } from '../../sync/submitters/types';
import { KeyBuilderSS } from '../KeyBuilderSS';
import { ITelemetryCacheAsync } from '../types';
import { findLatencyIndex } from '../findLatencyIndex';
import { getTelemetryConfigStats } from '../../sync/submitters/telemetrySubmitter';
import { CONSUMER_MODE, STORAGE_REDIS } from '../../utils/constants';
import { isNaNNumber, isString } from '../../utils/lang';
import { MAX_LATENCY_BUCKET_COUNT, newBuckets } from '../inMemory/TelemetryCacheInMemory';
import { parseLatencyField, parseExceptionField, parseMetadata } from '../utils';
import type { RedisAdapter } from './RedisAdapter';
export class TelemetryCacheInRedis implements ITelemetryCacheAsync {
/**
* Create a Telemetry cache that uses Redis as storage.
* @param log - Logger instance.
* @param keys - Key builder.
* @param redis - Redis client.
*/
constructor(private readonly log: ILogger, private readonly keys: KeyBuilderSS, private readonly redis: RedisAdapter) { }
recordLatency(method: Method, latencyMs: number) {
const [key, field] = this.keys.buildLatencyKey(method, findLatencyIndex(latencyMs)).split('::');
return this.redis.hincrby(key, field, 1)
.catch(() => { /* Handle rejections for telemetry */ });
}
recordException(method: Method) {
const [key, field] = this.keys.buildExceptionKey(method).split('::');
return this.redis.hincrby(key, field, 1)
.catch(() => { /* Handle rejections for telemetry */ });
}
recordConfig() {
const [key, field] = this.keys.buildInitKey().split('::');
const value = JSON.stringify(getTelemetryConfigStats(CONSUMER_MODE, STORAGE_REDIS));
return this.redis.hset(key, field, value).catch(() => { /* Handle rejections for telemetry */ });
}
/**
* Pop telemetry latencies.
* The returned promise rejects if redis operations fail.
*/
popLatencies(): Promise<MultiMethodLatencies> {
return this.redis.hgetall(this.keys.latencyPrefix).then(latencies => {
const result: MultiMethodLatencies = new Map();
Object.keys(latencies).forEach(field => {
const parsedField = parseLatencyField(field);
if (isString(parsedField)) {
this.log.error(`Ignoring invalid latency field: ${field}: ${parsedField}`);
return;
}
const count = parseInt(latencies[field]);
if (isNaNNumber(count)) {
this.log.error(`Ignoring latency with invalid count: ${latencies[field]}`);
return;
}
const [metadata, method, bucket] = parsedField;
if (bucket >= MAX_LATENCY_BUCKET_COUNT) {
this.log.error(`Ignoring latency with invalid bucket: ${bucket}`);
return;
}
const methodLatencies = result.get(metadata) || {};
methodLatencies[method] = methodLatencies[method] || newBuckets();
methodLatencies[method]![bucket] = count;
result.set(metadata, methodLatencies);
});
return this.redis.del(this.keys.latencyPrefix).then(() => result);
});
}
/**
* Pop telemetry exceptions.
* The returned promise rejects if redis operations fail.
*/
popExceptions(): Promise<MultiMethodExceptions> {
return this.redis.hgetall(this.keys.exceptionPrefix).then(exceptions => {
const result: MultiMethodExceptions = new Map();
Object.keys(exceptions).forEach(field => {
const parsedField = parseExceptionField(field);
if (isString(parsedField)) {
this.log.error(`Ignoring invalid exception field: ${field}: ${parsedField}`);
return;
}
const count = parseInt(exceptions[field]);
if (isNaNNumber(count)) {
this.log.error(`Ignoring exception with invalid count: ${exceptions[field]}`);
return;
}
const [metadata, method] = parsedField;
if (!result.has(metadata)) result.set(metadata, {});
result.get(metadata)![method] = count;
});
return this.redis.del(this.keys.exceptionPrefix).then(() => result);
});
}
/**
* Pop telemetry configs.
* The returned promise rejects if redis operations fail.
*/
popConfigs(): Promise<MultiConfigs> {
return this.redis.hgetall(this.keys.initPrefix).then(configs => {
const result: MultiConfigs = new Map();
Object.keys(configs).forEach(field => {
const parsedField = parseMetadata(field);
if (isString(parsedField)) {
this.log.error(`Ignoring invalid config field: ${field}: ${parsedField}`);
return;
}
const [metadata] = parsedField;
try {
const config = JSON.parse(configs[field]);
result.set(metadata, config);
} catch (e) {
this.log.error(`Ignoring invalid config: ${configs[field]}`);
}
});
return this.redis.del(this.keys.initPrefix).then(() => result);
});
}
}