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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsbURBS2lCO0FBRWpCLGdEQUF3RDtBQUN4RCw2Q0FBMkQ7QUFDM0QsMkNBQXlDO0FBRXpDLFNBQVMsYUFBYSxDQUFDLEtBQWE7SUFDbEMsSUFBSSxLQUFLLEdBQUcsSUFBSSxFQUFFO1FBQ2hCLHNDQUFzQztRQUN0QyxPQUFPLENBQUMsS0FBSyxDQUFDLG1EQUFtRCxDQUFDLENBQUM7UUFDbkUsT0FBTyxJQUFJLENBQUM7S0FDYjtJQUNELE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsS0FBSyxHQUFHLEdBQUcsRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDLDRFQUE0RTtJQUN2SCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFFRCxTQUFTLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUk7SUFDbEMsSUFBSSxJQUFJLENBQUMsR0FBRyxLQUFLLElBQUksRUFBRTtRQUNyQixNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRTtZQUNsQixjQUFjLEVBQUUsTUFBTTtTQUN2QixDQUFDLENBQUM7UUFDSCxPQUFPLENBQUMsUUFBUSxHQUFHLElBQUksQ0FBQyxRQUFRLENBQUM7S0FDbEM7U0FBTTtRQUNMLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFO1lBQ2xCLGNBQWMsRUFBRSxPQUFPO1NBQ3hCLENBQUMsQ0FBQztLQUNKO0FBQ0gsQ0FBQztBQWdDRDs7R0FFRztBQUNILE1BQU0sS0FBSztJQUNULElBQUksQ0FBUztJQUNiLE9BQU8sQ0FBZTtJQUN0QixNQUFNLENBQWM7SUFDcEIsTUFBTSxDQUFtQjtJQUN6QixjQUFjLENBQVU7SUFDeEIsT0FBTyxDQUdMO0lBQ0YsUUFBUSxDQUFtRTtJQUUzRTs7Ozs7T0FLRztJQUNILFlBQ0UsSUFBWSxFQUNaLE9BQXFCLEVBQ3JCLE1BQW1CLEVBQ25CLE9BR0M7UUFFRCxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQztRQUNqQixJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztRQUN2QixJQUFJLENBQUMsY0FBYyxHQUFHLE1BQU0sQ0FBQyxjQUFjLENBQUM7UUFDNUMsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFFdkIsSUFBSSxJQUFJLENBQUMsT0FBTyxFQUFFO1lBQ2hCLG9CQUFvQjtZQUNwQixJQUFJLENBQUMsUUFBUSxHQUFHLEVBQUUsQ0FBQztZQUVuQiw0RUFBNEU7WUFDNUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLEdBQUcsSUFBSSxxQkFBTyxDQUFDO2dCQUNuQyxJQUFJLEVBQUUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsR0FBRyxDQUFDLFdBQVc7Z0JBQ2xELElBQUksRUFBRSxrQ0FBa0M7Z0JBQ3hDLFVBQVUsRUFBRSxDQUFDLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLFNBQVMsQ0FBQztnQkFDNUQsU0FBUyxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7YUFDbkMsQ0FBQyxDQUFDO1lBRUgsNkVBQTZFO1lBQzdFLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLElBQUkscUJBQU8sQ0FBQztnQkFDakMsSUFBSSxFQUFFLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxTQUFTO2dCQUNoRCxJQUFJLEVBQUUsd0NBQXdDO2dCQUM5QyxVQUFVLEVBQUU7b0JBQ1YsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDO29CQUNuQyxTQUFTO29CQUNULGNBQWM7aUJBQ2Y7Z0JBQ0QsU0FBUyxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7YUFDbkMsQ0FBQyxDQUFDO1lBRUgsNERBQTREO1lBQzVELElBQUksQ0FBQyxRQUFRLENBQUMsU0FBUyxHQUFHLElBQUksdUJBQVMsQ0FBQztnQkFDdEMsSUFBSSxFQUFFLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxZQUFZO2dCQUNuRCxJQUFJLEVBQUUsdUNBQXVDO2dCQUM3QyxVQUFVLEVBQUUsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsRUFBRSxTQUFTLENBQUM7Z0JBQzVELFNBQVMsRUFBRSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO2FBQ25DLENBQUMsQ0FBQztTQUNKO1FBRUQsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsTUFBTSxDQUN6QjtZQUNFLElBQUksRUFBRSxXQUFXO1lBQ2pCLElBQUksRUFBRSxJQUFJO1lBQ1YsRUFBRSxFQUFFLENBQUM7U0FDTixFQUNELE1BQU0sRUFDTjtZQUNFLElBQUksRUFBRSxNQUFNLENBQUMsTUFBTSxDQUNqQjtnQkFDRSxHQUFHLEVBQUUsS0FBSzthQUNYLEVBQ0QsTUFBTSxDQUFDLElBQUksQ0FDWjtZQUNELE9BQU8sRUFBRSxNQUFNLENBQUMsTUFBTSxDQUNwQjtnQkFDRSxHQUFHLEVBQUUsS0FBSzthQUNYLEVBQ0QsTUFBTSxDQUFDLE9BQU8sQ0FDZjtZQUNELFFBQVEsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUNyQjtnQkFDRSxHQUFHLEVBQUUsS0FBSzthQUNYLEVBQ0QsTUFBTSxDQUFDLFFBQVEsQ0FDaEI7U0FDRixDQUNGLENBQUM7UUFDRixJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQztJQUNyQixDQUFDO0lBRUQsR0FBRyxDQUFDLE9BQWUsRUFBRSxJQUFhO1FBQ2hDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRTtZQUN2QixPQUFPLEVBQUUsSUFBSSxDQUFDLElBQUk7WUFDbEIsT0FBTztZQUNQLElBQUk7U0FDTCxDQUFDLENBQUM7SUFDTCxDQUFDO0lBRUQsT0FBTyxDQUFDLE9BQWUsRUFBRSxJQUFhO1FBQ3BDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLFNBQVMsRUFBRTtZQUMzQixPQUFPLEVBQUUsSUFBSSxDQUFDLElBQUk7WUFDbEIsT0FBTztZQUNQLElBQUk7U0FDTCxDQUFDLENBQUM7SUFDTCxDQUFDO0lBRUQsS0FBSyxDQUFDLEdBQVUsRUFBRSxJQUFhO1FBQzdCLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRTtZQUN6QixPQUFPLEVBQUUsSUFBSSxDQUFDLElBQUk7WUFDbEIsSUFBSTtZQUNKLEdBQUc7U0FDSixDQUFDLENBQUM7SUFDTCxDQUFDO0lBRUQsU0FBUyxDQUFDLE9BQWUsRUFBRSxJQUFhO1FBQ3RDLE1BQU0sS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2pDLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ3hCLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUVELFlBQVksQ0FBQyxPQUFlO1FBQzFCLElBQUksSUFBSSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUU7WUFDM0IsSUFBSSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUN4QjtnQkFDRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTTtnQkFDdEIsT0FBTzthQUNSLEVBQ0QsQ0FBQyxDQUNGLENBQUM7U0FDSDtJQUNILENBQUM7SUFFRCxXQUFXLENBQUMsT0FBZSxFQUFFLFlBQW9CO1FBQy9DLElBQUksSUFBSSxDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUU7WUFDekIsSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUN0QjtnQkFDRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTTtnQkFDdEIsT0FBTztnQkFDUCxZQUFZO2FBQ2IsRUFDRCxDQUFDLENBQ0YsQ0FBQztTQUNIO0lBQ0gsQ0FBQztJQUVELGNBQWMsQ0FBQyxPQUFlLEVBQUUsU0FBaUI7UUFDL0MsSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFLFNBQVMsRUFBRTtZQUM1QixNQUFNLE9BQU8sR0FBRyx3QkFBVyxDQUFDLEdBQUcsRUFBRSxDQUFDO1lBQ2xDLElBQUksQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FDN0I7Z0JBQ0UsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU07Z0JBQ3RCLE9BQU87YUFDUixFQUNELE9BQU8sR0FBRyxTQUFTLENBQ3BCLENBQUM7U0FDSDtJQUNILENBQUM7SUFFRCxvQkFBb0IsQ0FDbEIsRUFBVSxFQUNWLE9BQWU7UUFLZixJQUFJLFNBQXlCLENBQUM7UUFDOUIsTUFBTSxjQUFjLEdBQUcsSUFBSSxPQUFPLENBQUMsQ0FBQyxDQUFDLEVBQUUsTUFBTSxFQUFFLEVBQUU7WUFDL0MsU0FBUyxHQUFHLFVBQVUsQ0FBQyxHQUFHLEVBQUU7Z0JBQzFCLE1BQU0sQ0FDSixJQUFJLENBQUMsU0FBUyxDQUFDLHVCQUF1QixFQUFFO29CQUN0QyxPQUFPO29CQUNQLE9BQU8sRUFBRSxFQUFFO2lCQUNaLENBQUMsQ0FDSCxDQUFDO1lBQ0osQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO1FBQ1QsQ0FBQyxDQUFDLENBQUM7UUFDSCxPQUFPO1lBQ0wsY0FBYztZQUNkLEtBQUssRUFBRSxHQUFHLEVBQUU7Z0JBQ1YsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQzFCLENBQUM7U0FDRixDQUFDO0lBQ0osQ0FBQztJQUVELEtBQUssQ0FBQyxjQUFjLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxJQUFJO1FBQ3JDLE1BQU0sU0FBUyxHQUFHLHdCQUFXLENBQUMsR0FBRyxFQUFFLENBQUM7UUFDcEMsSUFBSTtZQUNGLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7WUFDaEMsTUFBTSxNQUFNLEdBQUcsTUFBTSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsQ0FBQztZQUMzQyxJQUFJLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUM3QyxPQUFPLE1BQU0sQ0FBQztTQUNmO1FBQUMsT0FBTyxHQUFHLEVBQUU7WUFDWixJQUFJLENBQUMsY0FBYyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUM3QyxJQUFJLENBQUMsV0FBVyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUM7WUFDNUMsTUFBTSxJQUFJLENBQUMsU0FBUyxDQUFDLHFCQUFxQixFQUFFO2dCQUMxQyxPQUFPLEVBQUUsSUFBSTtnQkFDYixJQUFJO2dCQUNKLEtBQUssRUFBRSxHQUFHO2FBQ1gsQ0FBQyxDQUFDO1NBQ0o7SUFDSCxDQUFDO0lBRUQsU0FBUyxDQUFDLE1BQXdCO1FBQ2hDLE9BQU8sSUFBSSxLQUFLLENBQUMsTUFBTSxFQUFFO1lBQ3ZCLEdBQUcsRUFBRSxDQUFDLE1BQU0sRUFBRSxJQUFJLEVBQUUsRUFBRTtnQkFDcEIsNEJBQTRCO2dCQUM1QixJQUFJLENBQUMsSUFBQSxpQkFBUyxFQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFO29CQUM1QixPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztpQkFDckI7Z0JBRUQsaUNBQWlDO2dCQUNqQyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxLQUFLLE9BQU8sRUFBRTtvQkFDbEMsTUFBTSxJQUFJLENBQUMsU0FBUyxDQUFDLGlCQUFpQixFQUFFO3dCQUN0QyxPQUFPLEVBQUUsSUFBSTtxQkFDZCxDQUFDLENBQUM7aUJBQ0o7Z0JBRUQsT0FBTyxDQUFDLEdBQUcsSUFBZSxFQUFvQixFQUFFO29CQUM5Qyx3Q0FBd0M7b0JBQ3hDLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLElBQUksSUFBSSxDQUFDLGNBQWMsRUFBRTt3QkFDaEQsTUFBTSxFQUFFLGNBQWMsRUFBRSxLQUFLLEVBQUUsR0FBRyxJQUFJLENBQUMsb0JBQW9CLENBQ3pELElBQUksQ0FBQyxjQUFjLEVBQ25CLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FDYixDQUFDO3dCQUNGLE9BQU8sT0FBTyxDQUFDLElBQUksQ0FBQzs0QkFDbEIsSUFBSSxDQUFDLGNBQWMsQ0FBQyxNQUFNLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQzs0QkFDdkMsY0FBYzt5QkFDZixDQUFDLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO3FCQUNuQjt5QkFBTTt3QkFDTCxPQUFPLElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsQ0FBQztxQkFDaEQ7Z0JBQ0gsQ0FBQyxDQUFDO1lBQ0osQ0FBQztTQUNGLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNILElBQUk7UUFDRixJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUU7WUFDZixPQUFPLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDOUI7UUFFRCw2QkFBNkI7UUFDN0IsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1lBQzdCLElBQUksTUFBTSxHQUFHLElBQUksQ0FBQztZQUNsQixNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsSUFBSSxDQUFDO1lBQ3hCLE1BQU0sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxjQUFjLEVBQUUsR0FDL0QsTUFBTSxDQUFDO1lBQ1QsTUFBTSxPQUFPLEdBQUc7Z0JBQ2QsSUFBSSxFQUFFLElBQUk7YUFDWCxDQUFDO1lBRUYsSUFBSSxPQUFPLENBQUMsR0FBRyxLQUFLLElBQUksRUFBRTtnQkFDeEIsTUFBTSxDQUFDLE1BQU0sQ0FBQyxPQUFPLEVBQUU7b0JBQ3JCLElBQUksRUFBRSxTQUFTO29CQUNmLEtBQUssRUFBRSxPQUFPLENBQUMsS0FBSztpQkFDckIsQ0FBQyxDQUFDO2dCQUNILE1BQU0sY0FBYyxHQUFtQjtvQkFDckMsb0JBQW9CLEVBQUUsT0FBTyxDQUFDLGNBQWMsSUFBSSxLQUFLO29CQUNyRCxvQkFBb0IsRUFBRSxhQUFhO2lCQUNwQyxDQUFDO2dCQUVGLE9BQU8sQ0FBQyxJQUFJLEVBQUUsY0FBYyxFQUFFLE9BQU8sQ0FBQyxDQUFDO2dCQUN2QyxNQUFNLEdBQUcsSUFBSSxpQkFBTyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsS0FBSyxFQUFFLGNBQWMsQ0FBQyxDQUFDO2dCQUUzRCwwQkFBMEI7Z0JBQzFCLE1BQU0sQ0FBQyxFQUFFLENBQUMsWUFBWSxFQUFFLENBQUMsR0FBRyxFQUFFLEVBQUU7b0JBQzlCLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFO3dCQUNkLElBQUksRUFBRSxZQUFZO3FCQUNuQixDQUFDLENBQUM7Z0JBQ0wsQ0FBQyxDQUFDLENBQUM7Z0JBQ0gsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxJQUFJLEVBQUUsRUFBRTtvQkFDMUIsTUFBTSxPQUFPLEdBQUcsY0FBYyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO29CQUNqRCxJQUFJLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRTt3QkFDaEIsR0FBRyxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRztxQkFDdEIsQ0FBQyxDQUFDO2dCQUNMLENBQUMsQ0FBQyxDQUFDO2dCQUNILE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUU7b0JBQzFCLE1BQU0sS0FBSyxHQUFHLElBQUksS0FBSyxDQUFDLGdCQUFnQixJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUM7b0JBQzVELElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFO3dCQUNoQixHQUFHLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHO3FCQUN0QixDQUFDLENBQUM7Z0JBQ0wsQ0FBQyxDQUFDLENBQUM7Z0JBRUgsaUJBQWlCO2FBQ2xCO2lCQUFNLElBQUksUUFBUSxDQUFDLEdBQUcsS0FBSyxJQUFJLEVBQUU7Z0JBQ2hDLGdCQUFnQjtnQkFDaEIsTUFBTSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxRQUFRLENBQUM7Z0JBQ2pDLE1BQU0sQ0FBQyxNQUFNLENBQUMsT0FBTyxFQUFFO29CQUNyQixJQUFJLEVBQUUsVUFBVTtvQkFDaEIsS0FBSztvQkFDTCxJQUFJO2lCQUNMLENBQUMsQ0FBQztnQkFDSCxNQUFNLE9BQU8sR0FBaUI7b0JBQzVCLFNBQVMsRUFBRSxLQUFLO29CQUNoQixJQUFJO29CQUNKLEVBQUU7b0JBQ0YsYUFBYTtvQkFDYixnQkFBZ0IsRUFBRSxHQUFHLEVBQUU7d0JBQ3JCLE9BQU8sSUFBSSxDQUFDO29CQUNkLENBQUM7b0JBQ0Qsb0JBQW9CLEVBQUUsUUFBUSxDQUFDLGNBQWMsSUFBSSxLQUFLO2lCQUN2RCxDQUFDO2dCQUNGLElBQUksY0FBYyxFQUFFO29CQUNsQixPQUFPLENBQUMsY0FBYyxHQUFHLGNBQWMsQ0FBQztpQkFDekM7Z0JBQ0QsT0FBTyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7Z0JBQ2hDLE1BQU0sR0FBRyxJQUFJLGlCQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7YUFDL0I7aUJBQU07Z0JBQ0wsY0FBYztnQkFDZCxNQUFNLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRTtvQkFDckIsSUFBSSxFQUFFLFFBQVE7b0JBQ2QsSUFBSTtvQkFDSixJQUFJO29CQUNKLEVBQUU7aUJBQ0gsQ0FBQyxDQUFDO2dCQUNILE1BQU0sT0FBTyxHQUFpQjtvQkFDNUIsSUFBSTtvQkFDSixJQUFJO29CQUNKLEVBQUU7b0JBQ0YsYUFBYTtvQkFDYixnQkFBZ0IsRUFBRSxHQUFHLEVBQUU7d0JBQ3JCLE9BQU8sSUFBSSxDQUFDO29CQUNkLENBQUM7b0JBQ0Qsb0JBQW9CLEVBQUUsTUFBTSxDQUFDLGNBQWMsSUFBSSxLQUFLO2lCQUNyRCxDQUFDO2dCQUNGLElBQUksY0FBYyxFQUFFO29CQUNsQixPQUFPLENBQUMsY0FBYyxHQUFHLGNBQWMsQ0FBQztpQkFDekM7Z0JBQ0QsT0FBTyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7Z0JBQ2hDLE1BQU0sR0FBRyxJQUFJLGlCQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7Z0JBQzlCLHFCQUFxQjthQUN0QjtZQUVELElBQUksQ0FBQyxHQUFHLENBQUMsaUJBQWlCLE9BQU8sQ0FBQyxJQUFJLE9BQU8sRUFBRSxPQUFPLENBQUMsQ0FBQztZQUV4RCxNQUFNLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBQztZQUVoQyxnQkFBZ0I7WUFDaEIsTUFBTSxDQUFDLEVBQUUsQ0FBQyxTQUFTLEVBQUUsR0FBRyxFQUFFO2dCQUN4QixJQUFJLENBQUMsT0FBTyxDQUFDLDZCQUE2QixPQUFPLENBQUMsSUFBSSxPQUFPLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDdkUsQ0FBQyxDQUFDLENBQUM7WUFDSCxNQUFNLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFO2dCQUN6QixJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztZQUN0QixDQUFDLENBQUMsQ0FBQztZQUNILE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRTtnQkFDdEIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7Z0JBQ3JCLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNoQixDQUFDLENBQUMsQ0FBQztZQUNILE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRTtnQkFDdEIsTUFBTSxLQUFLLEdBQUcsSUFBSSxLQUFLLENBQUMseUJBQXlCLENBQUMsQ0FBQztnQkFDbkQsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDMUIsQ0FBQyxDQUFDLENBQUM7WUFDSCxNQUFNLENBQUMsRUFBRSxDQUFDLGNBQWMsRUFBRSxDQUFDLElBQUksRUFBRSxFQUFFO2dCQUNqQyxJQUFJLENBQUMsR0FBRyxDQUNOLG1CQUFtQixPQUFPLENBQUMsSUFBSSxlQUFlLElBQUksS0FBSyxFQUN2RCxPQUFPLENBQ1IsQ0FBQztZQUNKLENBQUMsQ0FBQyxDQUFDO1lBQ0gsTUFBTSxDQUFDLEVBQUUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFO2dCQUNwQixJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxDQUFDLGtCQUFrQixDQUFDLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFDbEQsQ0FBQyxDQUFDLENBQUM7UUFDTCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRDs7O09BR0c7SUFDSCxLQUFLLENBQUMsTUFBTTtRQUNWLHNCQUFzQjtRQUN0QixNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7WUFDckIsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUU7Z0JBQ1YsTUFBTSxJQUFJLEtBQUssQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCLENBQUMsQ0FBQzthQUM1QztRQUNILENBQUMsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNyQyxDQUFDO0lBRUQsR0FBRyxDQUFDLEdBQVU7UUFDWixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3JDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRTtZQUNsQixLQUFLLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEdBQUcsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2xDLENBQUMsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxLQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUU7WUFDcEMsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUNwQyxPQUFPLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsS0FBSyxFQUFFLEVBQUU7Z0JBQy9CLE1BQU0sTUFBTSxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0JBQy9DLE9BQU8sTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3JCLENBQUMsQ0FBQyxDQUFDO1FBQ0wsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0NBQ0Y7QUFFRCxpQkFBUyxLQUFLLENBQUMifQ==
\ 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);
});