diff --git a/lib/index.d.ts b/lib/index.d.ts index 28f176d..66e3c16 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,6 +1,7 @@ /// -import { Redis as _Redis, Cluster } from "ioredis"; +import IoRedis, { Redis as _Redis, Cluster } from "ioredis"; import EventEmitter from "events"; +import { Registry, Counter, Histogram } from "prom-client"; interface RedisConfig { /** provide host ip/url, default - localhost */ host?: string; @@ -45,15 +46,43 @@ declare class Redis { emitter: EventEmitter; config: RedisConfig; client: Cluster | _Redis; + commandTimeout?: number; + metrics?: { + register: Registry; + labels: { + [key: string]: string; + }; + }; + trackers?: { + commands?: Counter; + errors?: Counter; + latencies?: Histogram; + }; /** * @param {string} name - unique name to this service * @param {EventEmitter} emitter * @param {RedisConfig} config - configuration object of service + * @param {Registry} metrics - prometheus client */ - constructor(name: string, emitter: EventEmitter, config: RedisConfig); + constructor(name: string, emitter: EventEmitter, config: RedisConfig, metrics?: { + register: Registry; + labels: { + [key: string]: string; + }; + }); log(message: string, data: unknown): void; success(message: string, data: unknown): void; error(err: Error, data: unknown): void; + makeError(message: string, data: unknown): Error; + trackCommand(command: string): void; + trackErrors(command: string, errorMessage: string): void; + trackLatencies(command: string, startTime: number): void; + createTimeoutPromise(ms: number, command: string): { + timeoutPromise: Promise; + clear: () => void; + }; + executeCommand(target: any, prop: any, args: any): Promise; + makeProxy(client: Cluster | _Redis): Cluster | IoRedis; /** * Connect to redis server with the config * diff --git a/lib/index.js b/lib/index.js index b2ad13d..8eb50f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,6 +23,9 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; const ioredis_1 = __importStar(require("ioredis")); +const commands_1 = require("@ioredis/commands"); +const prom_client_1 = require("prom-client"); +const perf_hooks_1 = require("perf_hooks"); function retryStrategy(times) { if (times > 1000) { // eslint-disable-next-line no-console @@ -53,14 +56,49 @@ class Redis { emitter; config; client; + commandTimeout; + metrics; + trackers; /** * @param {string} name - unique name to this service * @param {EventEmitter} emitter * @param {RedisConfig} config - configuration object of service + * @param {Registry} metrics - prometheus client */ - constructor(name, emitter, config) { + constructor(name, emitter, config, metrics) { this.name = name; this.emitter = emitter; + this.commandTimeout = config.commandTimeout; + this.metrics = metrics; + if (this.metrics) { + // register counters + this.trackers = {}; + // create counter for tracking the number of times redis commands are called + this.trackers.commands = new prom_client_1.Counter({ + name: `${this.name.replaceAll("-", "_")}:commands`, + help: "keep track of all redis commands", + labelNames: [...Object.keys(this.metrics.labels), "command"], + registers: [this.metrics.register], + }); + // create counter for tracking the number of times redis commands have failed + this.trackers.errors = new prom_client_1.Counter({ + name: `${this.name.replaceAll("-", "_")}:errors`, + help: "keep track of all redis command errors", + labelNames: [ + ...Object.keys(this.metrics.labels), + "command", + "errorMessage", + ], + registers: [this.metrics.register], + }); + // create histogram for tracking latencies of redis commands + this.trackers.latencies = new prom_client_1.Histogram({ + name: `${this.name.replaceAll("-", "_")}:latencies`, + help: "keep track of redis command latencies", + labelNames: [...Object.keys(this.metrics.labels), "command"], + registers: [this.metrics.register], + }); + } this.config = Object.assign({ host: "localhost", port: 6379, @@ -99,6 +137,101 @@ class Redis { err, }); } + makeError(message, data) { + const error = new Error(message); + this.error(error, data); + return error; + } + trackCommand(command) { + if (this.trackers?.commands) { + this.trackers.commands.inc({ + ...this.metrics.labels, + command, + }, 1); + } + } + trackErrors(command, errorMessage) { + if (this.trackers?.errors) { + this.trackers.errors.inc({ + ...this.metrics.labels, + command, + errorMessage, + }, 1); + } + } + trackLatencies(command, startTime) { + if (this.trackers?.latencies) { + const endTime = perf_hooks_1.performance.now(); + this.trackers.latencies.observe({ + ...this.metrics.labels, + command, + }, endTime - startTime); + } + } + createTimeoutPromise(ms, command) { + let timeoutId; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(this.makeError("redis.COMMAND_TIMEOUT", { + command, + timeout: ms, + })); + }, ms); + }); + return { + timeoutPromise, + clear: () => { + clearTimeout(timeoutId); + }, + }; + } + async executeCommand(target, prop, args) { + const startTime = perf_hooks_1.performance.now(); + try { + this.trackCommand(String(prop)); + const result = await target[prop](...args); + this.trackLatencies(String(prop), startTime); + return result; + } + catch (err) { + this.trackLatencies(String(prop), startTime); + this.trackErrors(String(prop), err.message); + throw this.makeError("redis.COMMAND_ERROR", { + command: prop, + args, + error: err, + }); + } + } + makeProxy(client) { + return new Proxy(client, { + get: (target, prop) => { + // check if a command or not + if (!(0, commands_1.exists)(String(prop))) { + return target[prop]; + } + // check if client in ready state + if (this.client.status !== "ready") { + throw this.makeError("redis.NOT_READY", { + command: prop, + }); + } + return (...args) => { + // If timeout is set, apply Promise.race + if (this.client.isCluster && this.commandTimeout) { + const { timeoutPromise, clear } = this.createTimeoutPromise(this.commandTimeout, String(prop)); + return Promise.race([ + this.executeCommand(target, prop, args), + timeoutPromise, + ]).finally(clear); + } + else { + return this.executeCommand(target, prop, args); + } + }; + }, + }); + } /** * Connect to redis server with the config * @@ -198,6 +331,7 @@ class Redis { // single node finish } this.log(`Connecting in ${infoObj.mode} mode`, infoObj); + client = this.makeProxy(client); // common events client.on("connect", () => { this.success(`Successfully connected in ${infoObj.mode} mode`, null); @@ -249,4 +383,4 @@ class Redis { } } module.exports = Redis; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsbURBS2lCO0FBR2pCLFNBQVMsYUFBYSxDQUFDLEtBQWE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsSUFBSSxFQUFFO1FBQ2hCLHNDQUFzQztRQUN0QyxPQUFPLENBQUMsS0FBSyxDQUFDLG1EQUFtRCxDQUFDLENBQUM7UUFDbkUsT0FBTyxJQUFJLENBQUM7S0FDYjtJQUNELE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxHQUFHLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDLDRFQUE0RTtJQUN2SCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFFRCxTQUFTLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUk7SUFDbEMsSUFBSSxJQUFJLENBQUMsR0FBRyxLQUFLLElBQUksRUFBRTtRQUNyQixNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRTtZQUNsQixjQUFjLEVBQUUsTUFBTTtTQUN2QixDQUFDLENBQUM7UUFDSCxPQUFPLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUM7S0FDbEM7U0FBTTtRQUNMLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFO1lBQ2xCLGNBQWMsRUFBRSxPQUFPO1NBQ3hCLENBQUMsQ0FBQztLQUNKO0FBQ0gsQ0FBQztBQWdDRDs7R0FFRztBQUNILE1BQU0sS0FBSztJQUNULElBQUksQ0FBUztJQUNiLE9BQU8sQ0FBZTtJQUN0QixNQUFNLENBQWM7SUFDcEIsTUFBTSxDQUFtQjtJQUV6Qjs7OztPQUlHO0lBQ0gsWUFBWSxJQUFZLEVBQUUsT0FBcUIsRUFBRSxNQUFtQjtRQUNsRSxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLENBQUMsT0FBTyxHQUFHLE9B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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsbURBS2lCO0FBRWpCLGdEQUF3RDtBQUN4RCw2Q0FBMkQ7QUFDM0QsMkNBQXlDO0FBRXpDLFNBQVMsYUFBYSxDQUFDLEtBQWE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsSUFBSSxFQUFFO1FBQ2hCLHNDQUFzQztRQUN0QyxPQUFPLENBQUMsS0FBSyxDQUFDLG1EQUFtRCxDQUFDLENBQUM7UUFDbkUsT0FBTyxJQUFJLENBQUM7S0FDYjtJQUNELE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxHQUFHLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDLDRFQUE0RTtJQUN2SCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFFRCxTQUFTLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUk7SUFDbEMsSUFBSSxJQUFJLENBQUMsR0FBRyxLQUFLLElBQUksRUFBRTtRQUNyQixNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRTtZQUNsQixjQUFjLEVBQUUsTUFBTTtTQUN2QixDQUFDLENBQUM7UUFDSCxPQUFPLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUM7S0FDbEM7U0FBTTtRQUNMLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFO1lBQ2xCLGNBQWMsRUFBRSxPQUFPO1NBQ3hCLENBQUMsQ0FBQztLQUNKO0FBQ0gsQ0FBQztBQWdDRDs7R0FFRztBQUNILE1BQU0sS0FBSztJQUNULElBQUksQ0FBUztJQUNiLE9BQU8sQ0FBZTtJQUN0QixNQUFNLENBQWM7SUFDcEIsTUFBTSxDQUFtQjtJQUN6QixjQUFjLENBQVU7SUFDeEIsT0FBTyxDQUdMO0lBQ0YsUUFBUSxDQUFtRTtJQUUzRTs7Ozs7T0FLRztJQUNILFlBQ0UsSUFB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 \ No newline at end of file diff --git a/package.json b/package.json index 62c0135..2fb0fb0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ }, "homepage": "https://github.com/akshendra/redis-wrapper#readme", "dependencies": { - "ioredis": "5.3.2" + "@ioredis/commands": "^1.2.0", + "ioredis": "5.3.2", + "prom-client": "^15.0.0" }, "devDependencies": { "@types/node": "^16.11.7", @@ -36,4 +38,4 @@ "eslint-plugin-import": "^2.23.4", "typescript": "^4.4.4" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 2900adf..1db290e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,9 @@ import IoRedis, { RedisOptions, } from "ioredis"; import EventEmitter from "events"; +import { exists as isCommand } from "@ioredis/commands"; +import { Registry, Counter, Histogram } from "prom-client"; +import { performance } from "perf_hooks"; function retryStrategy(times: number): number { if (times > 1000) { @@ -67,15 +70,66 @@ class Redis { emitter: EventEmitter; config: RedisConfig; client: Cluster | _Redis; + commandTimeout?: number; + metrics?: { + register: Registry; + labels: { [key: string]: string }; + }; + trackers?: { commands?: Counter; errors?: Counter; latencies?: Histogram }; /** * @param {string} name - unique name to this service * @param {EventEmitter} emitter * @param {RedisConfig} config - configuration object of service + * @param {Registry} metrics - prometheus client */ - constructor(name: string, emitter: EventEmitter, config: RedisConfig) { + constructor( + name: string, + emitter: EventEmitter, + config: RedisConfig, + metrics?: { + register: Registry; + labels: { [key: string]: string }; + } + ) { this.name = name; this.emitter = emitter; + this.commandTimeout = config.commandTimeout; + this.metrics = metrics; + + if (this.metrics) { + // register counters + this.trackers = {}; + + // create counter for tracking the number of times redis commands are called + this.trackers.commands = new Counter({ + name: `${this.name.replaceAll("-", "_")}:commands`, + help: "keep track of all redis commands", + labelNames: [...Object.keys(this.metrics.labels), "command"], + registers: [this.metrics.register], + }); + + // create counter for tracking the number of times redis commands have failed + this.trackers.errors = new Counter({ + name: `${this.name.replaceAll("-", "_")}:errors`, + help: "keep track of all redis command errors", + labelNames: [ + ...Object.keys(this.metrics.labels), + "command", + "errorMessage", + ], + registers: [this.metrics.register], + }); + + // create histogram for tracking latencies of redis commands + this.trackers.latencies = new Histogram({ + name: `${this.name.replaceAll("-", "_")}:latencies`, + help: "keep track of redis command latencies", + labelNames: [...Object.keys(this.metrics.labels), "command"], + registers: [this.metrics.register], + }); + } + this.config = Object.assign( { host: "localhost", @@ -131,6 +185,128 @@ class Redis { }); } + makeError(message: string, data: unknown): Error { + const error = new Error(message); + this.error(error, data); + return error; + } + + trackCommand(command: string): void { + if (this.trackers?.commands) { + this.trackers.commands.inc( + { + ...this.metrics.labels, + command, + }, + 1 + ); + } + } + + trackErrors(command: string, errorMessage: string): void { + if (this.trackers?.errors) { + this.trackers.errors.inc( + { + ...this.metrics.labels, + command, + errorMessage, + }, + 1 + ); + } + } + + trackLatencies(command: string, startTime: number): void { + if (this.trackers?.latencies) { + const endTime = performance.now(); + this.trackers.latencies.observe( + { + ...this.metrics.labels, + command, + }, + endTime - startTime + ); + } + } + + createTimeoutPromise( + ms: number, + command: string + ): { + timeoutPromise: Promise; + clear: () => void; + } { + let timeoutId: NodeJS.Timeout; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject( + this.makeError("redis.COMMAND_TIMEOUT", { + command, + timeout: ms, + }) + ); + }, ms); + }); + return { + timeoutPromise, + clear: () => { + clearTimeout(timeoutId); + }, + }; + } + + async executeCommand(target, prop, args): Promise { + const startTime = performance.now(); + try { + this.trackCommand(String(prop)); + const result = await target[prop](...args); + this.trackLatencies(String(prop), startTime); + return result; + } catch (err) { + this.trackLatencies(String(prop), startTime); + this.trackErrors(String(prop), err.message); + throw this.makeError("redis.COMMAND_ERROR", { + command: prop, + args, + error: err, + }); + } + } + + makeProxy(client: Cluster | _Redis) { + return new Proxy(client, { + get: (target, prop) => { + // check if a command or not + if (!isCommand(String(prop))) { + return target[prop]; + } + + // check if client in ready state + if (this.client.status !== "ready") { + throw this.makeError("redis.NOT_READY", { + command: prop, + }); + } + + return (...args: unknown[]): Promise => { + // If timeout is set, apply Promise.race + if (this.client.isCluster && this.commandTimeout) { + const { timeoutPromise, clear } = this.createTimeoutPromise( + this.commandTimeout, + String(prop) + ); + return Promise.race([ + this.executeCommand(target, prop, args), + timeoutPromise, + ]).finally(clear); + } else { + return this.executeCommand(target, prop, args); + } + }; + }, + }); + } + /** * Connect to redis server with the config * @@ -236,6 +412,8 @@ class Redis { this.log(`Connecting in ${infoObj.mode} mode`, infoObj); + client = this.makeProxy(client); + // common events client.on("connect", () => { this.success(`Successfully connected in ${infoObj.mode} mode`, null); diff --git a/tests/single.js b/tests/single.js index aaee6be..45ab77a 100644 --- a/tests/single.js +++ b/tests/single.js @@ -1,65 +1,77 @@ - - -const Redis = require('../lib/index'); -const { EventEmitter } = require('events'); +const Redis = require("../lib/index"); +const { EventEmitter } = require("events"); +const promClient = require("prom-client"); const emitter = new EventEmitter(); -emitter.on('log', console.log.bind(console)); -emitter.on('success', console.log.bind(console)); -emitter.on('error', console.error.bind(console)); +emitter.on("log", console.log.bind(console)); +emitter.on("success", console.log.bind(console)); +emitter.on("error", console.error.bind(console)); async function doSome(client) { for (let i = 0; i < 1000; i += 1) { console.log(i); - await client.set(`Key:${i}`, i); + // add timeout of 1s + await new Promise((resolve) => setTimeout(resolve, 1000)); + try { + await client.set(`Key:${i}`, i); + const data = await client.get(`Key:${i}`); + console.log(`Fetched: ${data}`); + } catch (err) { + console.log(err); + } } } async function doPPL(redis) { - await redis.ppl([{ - command: 'hset', - args: [ - 'Map', - 'one', - '1', - ], - }, { - command: 'hmset', - args: [ - 'Map', - { 'two': 2, 'three': 3 }, - ], - }, { - command: 'set', - args: [ - 'count', - '3', - ], - }]); - const response = await redis.ppl([{ - command: 'hgetall', - args: [ - 'Map', - ], - }, { - command: 'get', - args: [ - 'count', - ], - action(val) { - return { - count: val, - }; + await redis.ppl([ + { + command: "hset", + args: ["Map", "one", "1"], + }, + { + command: "hmset", + args: ["Map", { two: 2, three: 3 }], + }, + { + command: "set", + args: ["count", "3"], }, - }]); + ]); + const response = await redis.ppl([ + { + command: "hgetall", + args: ["Map"], + }, + { + command: "get", + args: ["count"], + action(val) { + return { + count: val, + }; + }, + }, + ]); console.log(JSON.stringify(response, null, 2)); } -const redis = new Redis('redis', emitter, { - host: '127.0.0.1', - port: 6379, -}); -redis.init() +const redis = new Redis( + "redis", + emitter, + { + host: "127.0.0.1", + port: 6379, + commandTimeout: 100, + }, + { + register: promClient.register, + labels: { + service: "test-test_test9", + }, + } +); +redis + .init() .then(() => { const client = redis.client; return doSome(client); @@ -68,10 +80,10 @@ redis.init() return doPPL(redis); }) .then(() => { - console.log('Started'); + console.log("Started"); process.exit(0); }) - .catch(err => { + .catch((err) => { console.error(err); process.exit(1); });