|
| 1 | +/* |
| 2 | + * Copyright 2026 New Relic Corporation. All rights reserved. |
| 3 | + * SPDX-License-Identifier: Apache-2.0 |
| 4 | + */ |
| 5 | + |
| 6 | +'use strict' |
| 7 | +const PropagationSubscriber = require('../propagation') |
| 8 | +const { redisClientOpts } = require('../../symbols') |
| 9 | + |
| 10 | +/** |
| 11 | + * Stores `RedisClient.options` on the Redis client via the symbol `RedisClient[redisClientOpts]`, |
| 12 | + * and then to the coordinating context via `ctx[redisClientOpts]`. |
| 13 | + * |
| 14 | + * We have to store the `redisClientOpts` symbol on the client directly because |
| 15 | + * the client could set up its options (e.g. selecting a database) outside of a |
| 16 | + * transaction, so we cannot rely on `ctx[redisClientOpts]` being sent. |
| 17 | + * |
| 18 | + * When `ctx` is valid (we're now in a transaction), we copy over |
| 19 | + * `RedisClient[redisClientOpts]` to `ctx[redisClientOpts]`. |
| 20 | + * |
| 21 | + * This is required because the subscriber responsible |
| 22 | + * for segment creation (`./add-command`) is listening to events on the |
| 23 | + * `RedisCommandQueue` class, and it does NOT have access to `RedisClient`. |
| 24 | + * It will read from `ctx[redisClientOpts]` to retrieve the datastore parameters |
| 25 | + * it needs. |
| 26 | + * |
| 27 | + * Any `RedisClient` method that calls `RedisClient.#queue.addCommand()` MUST |
| 28 | + * have a subscriber that extends from this class, so the `addCommand` subscriber |
| 29 | + * knows what the datastore parameters are. |
| 30 | + */ |
| 31 | +module.exports = class ClientPropagationSubscriber extends PropagationSubscriber { |
| 32 | + constructor({ agent, logger, channelName }) { |
| 33 | + super({ agent, logger, packageName: '@redis/client', channelName }) |
| 34 | + this.events = ['asyncStart'] |
| 35 | + } |
| 36 | + |
| 37 | + asyncStart(data) { |
| 38 | + // asyncStart always fires (with or without context) |
| 39 | + // Initialize client options here so they're available for |
| 40 | + // both in-transaction and out-of-transaction commands |
| 41 | + const { self: client } = data |
| 42 | + if (!client[redisClientOpts]) { |
| 43 | + client[redisClientOpts] = this.getRedisParams(client.options) |
| 44 | + } |
| 45 | + return super.asyncStart(data) |
| 46 | + } |
| 47 | + |
| 48 | + handler(data, ctx) { |
| 49 | + // handler only fires when there's a transaction context |
| 50 | + // Transfer a COPY of the client opts to context, so that |
| 51 | + // updates to client opts don't affect the current context. |
| 52 | + // This ensures SELECT reports the old database, but subsequent |
| 53 | + // commands report the new database (feature parity with v3) |
| 54 | + const { self: client } = data |
| 55 | + ctx[redisClientOpts] = Object.assign({}, client[redisClientOpts]) |
| 56 | + return super.handler(data, ctx) |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * Extracts the datastore parameters from the client options |
| 61 | + * |
| 62 | + * @param {object} clientOpts client.options |
| 63 | + * @returns {object} { host, port_path_or_id, database_name } |
| 64 | + */ |
| 65 | + getRedisParams(clientOpts) { |
| 66 | + // need to replicate logic done in RedisClient |
| 67 | + // to parse the url to assign to socket.host/port |
| 68 | + // see: https://github.com/redis/node-redis/blob/5576a0db492cda2cd88e09881bc330aa956dd0f5/packages/client/lib/client/index.ts#L160 |
| 69 | + if (clientOpts?.url) { |
| 70 | + const parsedURL = new URL(clientOpts.url) |
| 71 | + clientOpts.socket = Object.assign({}, clientOpts.socket, { host: parsedURL.hostname }) |
| 72 | + if (parsedURL.port) { |
| 73 | + clientOpts.socket.port = parsedURL.port |
| 74 | + } |
| 75 | + |
| 76 | + if (parsedURL.pathname) { |
| 77 | + clientOpts.database = parsedURL.pathname.substring(1) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + return { |
| 82 | + host: clientOpts?.host || clientOpts?.socket?.host || 'localhost', |
| 83 | + port_path_or_id: |
| 84 | + clientOpts?.port || clientOpts?.socket?.path || clientOpts?.socket?.port || '6379', |
| 85 | + database_name: clientOpts?.database || 0 |
| 86 | + } |
| 87 | + } |
| 88 | +} |
0 commit comments