syntropylog
Version:
An instance manager with observability for Node.js applications
367 lines • 14.3 kB
JavaScript
/**
* @file src/redis/BeaconRedis.ts
* @description Implementation of IBeaconRedis that wraps a native `redis` client.
* It centralizes command execution to add instrumentation (logging, metrics, etc.).
*/
import { errorToJsonValue, } from '../types';
/**
* The primary implementation of the `IBeaconRedis` interface.
* This class wraps a native `redis` client and uses a central logger
* to provide instrumentation for all commands. It delegates connection
* management and command execution to specialized classes.
* @implements {IBeaconRedis}
*/
export class BeaconRedis {
config;
/** @private The logger instance for this specific Redis client. */
logger;
/** @private Manages the connection state and lifecycle of the native client. */
connectionManager;
/** @private Executes the actual commands against the native client. */
commandExecutor;
/**
* Constructs a new BeaconRedis instance.
* @param {RedisInstanceConfig} config - The configuration specific to this Redis instance.
* @param {RedisConnectionManager} connectionManager - The manager for the client's connection lifecycle.
* @param {RedisCommandExecutor} commandExecutor - The executor for sending commands to Redis.
* @param {ILogger} logger - The pre-configured logger instance for this client.
*/
constructor(config, connectionManager, commandExecutor, logger) {
this.config = config;
this.logger = logger;
this.connectionManager = connectionManager;
this.commandExecutor = commandExecutor;
}
// --- Lifecycle and Management Methods ---
/**
* @inheritdoc
*/
getInstanceName() {
return this.config.instanceName;
}
/**
* @inheritdoc
*/
async connect() {
return this.connectionManager.ensureReady();
}
/**
* @inheritdoc
*/
async quit() {
return this.connectionManager.disconnect();
}
/**
* @inheritdoc
*/
updateConfig(newConfig) {
this.logger.info({ newConfig }, 'Dynamically updating Redis instance configuration...');
Object.assign(this.config, newConfig);
}
/**
* @inheritdoc
* @throws {Error} This method is not yet implemented.
*/
multi() {
// TODO: Implement a fully instrumented transaction class.
// This would need a more complex implementation to queue commands and log them on exec().
throw new Error('The multi() method is not yet implemented.');
}
/**
* A centralized method for executing and instrumenting any Redis command.
* It ensures the client is ready, executes the command, logs the outcome
* (success or failure) with timing information, and handles errors.
* @private
* @template T The expected return type of the command.
* @param {string} commandName - The name of the Redis command (e.g., 'GET', 'HSET').
* @param {() => Promise<T>} commandFn - A function that, when called, executes the native Redis command.
* @param {...RedisValue[]} params - The parameters passed to the original command, used for logging.
* @returns {Promise<T>} A promise that resolves with the result of the command.
* @throws The error from the native command is re-thrown after being logged.
*/
async _executeCommand(commandName, commandFn, ...params) {
const startTime = Date.now();
// Use a base logger with the source pre-set for this command.
const commandLogger = this.logger.withSource('redis');
try {
// 1. Ensure the client is connected and ready before executing.
await this.connectionManager.ensureReady();
// 2. Execute the command by calling the provided function.
const result = await commandFn();
const durationMs = Date.now() - startTime;
// 3. On success, log the execution details.
// Determine the log level from the instance's specific configuration.
const logLevel = this.config.logging?.onSuccess ?? 'debug';
const logPayload = {
command: commandName,
instance: this.getInstanceName(),
durationMs,
};
// Conditionally add command parameters and return value to the log payload.
if (this.config.logging?.logCommandValues) {
logPayload.params = params;
}
if (this.config.logging?.logReturnValue) {
logPayload.result = result;
}
// The log is sent to the central pipeline where serialization and masking occur.
commandLogger[logLevel](logPayload, `Redis command [${commandName}] executed successfully.`);
return result;
}
catch (error) {
const durationMs = Date.now() - startTime;
const errorLogLevel = this.config.logging?.onError ?? 'error';
// The error object will be serialized by the central SerializerRegistry.
commandLogger[errorLogLevel]({
command: commandName,
instance: this.getInstanceName(),
durationMs,
err: errorToJsonValue(error),
params: this.config.logging?.logCommandValues ? params : undefined,
}, `Redis command [${commandName}] failed.`);
throw error;
}
}
// --- Public Command Methods ---
// Each command now simply calls _executeCommand. The structure remains the same.
/**
* @inheritdoc
*/
async get(key) {
return this._executeCommand('GET', () => this.commandExecutor.get(key), key);
}
/**
* @inheritdoc
*/
async set(key, value, ttlSeconds) {
const options = ttlSeconds ? { EX: ttlSeconds } : undefined;
return this._executeCommand('SET', () => this.commandExecutor.set(key, value, options), key, value, ttlSeconds);
}
/**
* @inheritdoc
*/
async del(keys) {
return this._executeCommand('DEL', () => this.commandExecutor.del(keys), keys);
}
/**
* @inheritdoc
*/
async exists(keys) {
return this._executeCommand('EXISTS', () => this.commandExecutor.exists(keys), keys);
}
/**
* @inheritdoc
*/
async expire(key, seconds) {
return this._executeCommand('EXPIRE', () => this.commandExecutor.expire(key, seconds), key, seconds);
}
/**
* @inheritdoc
*/
async ttl(key) {
return this._executeCommand('TTL', () => this.commandExecutor.ttl(key), key);
}
/**
* @inheritdoc
*/
async incr(key) {
return this._executeCommand('INCR', () => this.commandExecutor.incr(key), key);
}
/**
* @inheritdoc
*/
async decr(key) {
return this._executeCommand('DECR', () => this.commandExecutor.decr(key), key);
}
/**
* @inheritdoc
*/
async incrBy(key, increment) {
return this._executeCommand('INCRBY', () => this.commandExecutor.incrBy(key, increment), key, increment);
}
/**
* @inheritdoc
*/
async decrBy(key, decrement) {
return this._executeCommand('DECRBY', () => this.commandExecutor.decrBy(key, decrement), key, decrement);
}
/**
* @inheritdoc
*/
async hGet(key, field) {
return this._executeCommand('HGET', async () => (await this.commandExecutor.hGet(key, field)) ?? null, key, field);
}
async hSet(key, fieldOrFields, value) {
if (typeof fieldOrFields === 'string') {
// Handle single field-value pair.
return this._executeCommand('HSET', () => this.commandExecutor.hSet(key, fieldOrFields, value), key, fieldOrFields, value);
}
// Handle object of field-value pairs.
return this._executeCommand('HSET', () => this.commandExecutor.hSet(key, fieldOrFields), key, fieldOrFields);
}
/**
* @inheritdoc
*/
async hGetAll(key) {
return this._executeCommand('HGETALL', () => this.commandExecutor.hGetAll(key), key);
}
/**
* @inheritdoc
*/
async hDel(key, fields) {
return this._executeCommand('HDEL', () => this.commandExecutor.hDel(key, fields), key, fields);
}
/**
* @inheritdoc
*/
async hExists(key, field) {
return this._executeCommand('HEXISTS', () => this.commandExecutor.hExists(key, field), key, field);
}
/**
* @inheritdoc
*/
async hIncrBy(key, field, increment) {
return this._executeCommand('HINCRBY', () => this.commandExecutor.hIncrBy(key, field, increment), key, field, increment);
}
async lPush(key, elementOrElements) {
return this._executeCommand('LPUSH', () => this.commandExecutor.lPush(key, elementOrElements), key, elementOrElements);
}
async rPush(key, elementOrElements) {
return this._executeCommand('RPUSH', () => this.commandExecutor.rPush(key, elementOrElements), key, elementOrElements);
}
/**
* @inheritdoc
*/
async lPop(key) {
return this._executeCommand('LPOP', () => this.commandExecutor.lPop(key), key);
}
/**
* @inheritdoc
*/
async rPop(key) {
return this._executeCommand('RPOP', () => this.commandExecutor.rPop(key), key);
}
/**
* @inheritdoc
*/
async lRange(key, start, stop) {
return this._executeCommand('LRANGE', () => this.commandExecutor.lRange(key, start, stop), key, start, stop);
}
/**
* @inheritdoc
*/
async lLen(key) {
return this._executeCommand('LLEN', () => this.commandExecutor.lLen(key), key);
}
/**
* @inheritdoc
*/
async lTrim(key, start, stop) {
return this._executeCommand('LTRIM', () => this.commandExecutor.lTrim(key, start, stop), key, start, stop);
}
async sAdd(key, memberOrMembers) {
return this._executeCommand('SADD', () => this.commandExecutor.sAdd(key, memberOrMembers), key, memberOrMembers);
}
/**
* @inheritdoc
*/
async sMembers(key) {
return this._executeCommand('SMEMBERS', () => this.commandExecutor.sMembers(key), key);
}
/**
* @inheritdoc
*/
async sIsMember(key, member) {
return this._executeCommand('SISMEMBER', () => this.commandExecutor.sIsMember(key, member), key, member);
}
async sRem(key, memberOrMembers) {
return this._executeCommand('SREM', () => this.commandExecutor.sRem(key, memberOrMembers), key, memberOrMembers);
}
/**
* @inheritdoc
*/
async sCard(key) {
return this._executeCommand('SCARD', () => this.commandExecutor.sCard(key), key);
}
async zAdd(key, scoreOrMembers, member) {
// Check if we are using the array overload for multiple members.
if (Array.isArray(scoreOrMembers)) {
return this._executeCommand('ZADD', () => this.commandExecutor.zAdd(key, scoreOrMembers), key, scoreOrMembers);
}
// Handle single score-member pair.
return this._executeCommand('ZADD', () => this.commandExecutor.zAdd(key, scoreOrMembers, member), key, scoreOrMembers, member);
}
/**
* @inheritdoc
*/
async zRange(key, min, max, options) {
return this._executeCommand('ZRANGE', () => this.commandExecutor.zRange(key, min, max, options), key, min, max, options);
}
/**
* @inheritdoc
*/
async zRangeWithScores(key, min, max, options) {
return this._executeCommand('ZRANGE_WITHSCORES', () => this.commandExecutor.zRangeWithScores(key, min, max, options), key, min, max, options);
}
/**
* @inheritdoc
*/
async zRem(key, members) {
return this._executeCommand('ZREM', () => this.commandExecutor.zRem(key, members), key, members);
}
/**
* @inheritdoc
*/
async zCard(key) {
return this._executeCommand('ZCARD', () => this.commandExecutor.zCard(key), key);
}
/**
* @inheritdoc
*/
async zScore(key, member) {
return this._executeCommand('ZSCORE', () => this.commandExecutor.zScore(key, member), key, member);
}
/**
* Subscribes the client to a channel to listen for messages.
* Note: This is a long-lived command. The initial subscription action is logged,
* but individual messages received by the listener are not logged by this wrapper.
* The listener itself should handle any required logging for received messages.
* @param {string} channel - The channel to subscribe to.
* @param {(message: string, channel: string) => void} listener - The function to call when a message is received.
* @returns {Promise<void>} A promise that resolves when the subscription is successful.
*/
async subscribe(channel, listener) {
return this._executeCommand('SUBSCRIBE', () => this.commandExecutor.subscribe(channel, listener), channel);
}
/**
* Unsubscribes the client from a channel, or all channels if none is specified.
* @param {string} [channel] - The optional channel to unsubscribe from.
* @returns {Promise<void>} A promise that resolves when the unsubscription is successful.
*/
async unsubscribe(channel) {
return this._executeCommand('UNSUBSCRIBE', () => this.commandExecutor.unsubscribe(channel), channel);
}
/**
* @inheritdoc
*/
async ping(message) {
return this._executeCommand('PING', () => this.connectionManager.ping(message), message);
}
/**
* @inheritdoc
*/
async info(section) {
return this._executeCommand('INFO', () => this.connectionManager.info(section), section);
}
/**
* Executes a Lua script on the server.
* @param {string} script - The Lua script to execute.
* @param {string[]} keys - An array of key names used by the script, accessible via the `KEYS` table in Lua.
* @param {string[]} args - An array of argument values for the script, accessible via the `ARGV` table in Lua.
* @returns {Promise<any>} A promise that resolves with the result of the script execution.
*/
async eval(script, keys, args) {
return this._executeCommand('EVAL', () => this.commandExecutor.eval(script, keys, args), script, keys, args);
}
}
//# sourceMappingURL=BeaconRedis.js.map