Skip to content

Commit f7f5d5e

Browse files
committed
feat(node): vendor ioredis, redis instrumentations (#20510)
Vendor in the Redis and IORedis instrumentation code and unit tests, and update everything in Sentry to use our vendored code instead of the external dependency. A subsequent commit will update the node-redis instrumentation to use its recently-added Diagnostics Channel support. See: https://github.com/redis/node-redis/blob/master/docs/diagnostics-channel.md
1 parent b045541 commit f7f5d5e

13 files changed

Lines changed: 1634 additions & 34 deletions

File tree

.oxlintrc.base.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@
130130
"no-param-reassign": "off"
131131
}
132132
},
133+
{
134+
"files": ["**/integrations/tracing/redis/vendored/**/*.ts"],
135+
"rules": {
136+
"typescript/no-explicit-any": "off",
137+
"typescript/no-unsafe-member-access": "off",
138+
"typescript/no-this-alias": "off",
139+
"max-lines": "off",
140+
"no-bitwise": "off"
141+
}
142+
},
133143
{
134144
"files": [
135145
"**/scenarios/**",

packages/node/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
"@opentelemetry/instrumentation-graphql": "0.62.0",
7777
"@opentelemetry/instrumentation-hapi": "0.60.0",
7878
"@opentelemetry/instrumentation-http": "0.214.0",
79-
"@opentelemetry/instrumentation-ioredis": "0.62.0",
8079
"@opentelemetry/instrumentation-kafkajs": "0.23.0",
8180
"@opentelemetry/instrumentation-knex": "0.58.0",
8281
"@opentelemetry/instrumentation-koa": "0.62.0",
@@ -86,7 +85,6 @@
8685
"@opentelemetry/instrumentation-mysql": "0.60.0",
8786
"@opentelemetry/instrumentation-mysql2": "0.60.0",
8887
"@opentelemetry/instrumentation-pg": "0.66.0",
89-
"@opentelemetry/instrumentation-redis": "0.62.0",
9088
"@opentelemetry/instrumentation-tedious": "0.33.0",
9189
"@opentelemetry/sdk-trace-base": "^2.6.1",
9290
"@opentelemetry/semantic-conventions": "^1.40.0",

packages/node/src/integrations/tracing/redis.ts renamed to packages/node/src/integrations/tracing/redis/index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import type { Span } from '@opentelemetry/api';
2-
import type { RedisResponseCustomAttributeFunction } from '@opentelemetry/instrumentation-ioredis';
3-
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
4-
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
52
import type { IntegrationFn } from '@sentry/core';
63
import {
74
defineIntegration,
@@ -14,14 +11,18 @@ import {
1411
truncate,
1512
} from '@sentry/core';
1613
import { generateInstrumentOnce } from '@sentry/node-core';
14+
import type { IORedisCommandArgs } from '../../../utils/redisCache';
1715
import {
1816
calculateCacheItemSize,
1917
GET_COMMANDS,
2018
getCacheKeySafely,
2119
getCacheOperation,
2220
isInCommands,
2321
shouldConsiderForCache,
24-
} from '../../utils/redisCache';
22+
} from '../../../utils/redisCache';
23+
import type { IORedisInstrumentationConfig } from './vendored/types';
24+
import { IORedisInstrumentation } from './vendored/ioredis-instrumentation';
25+
import { RedisInstrumentation } from './vendored/redis-instrumentation';
2526

2627
interface RedisOptions {
2728
/**
@@ -46,11 +47,11 @@ const INTEGRATION_NAME = 'Redis';
4647
export let _redisOptions: RedisOptions = {};
4748

4849
/* Only exported for testing purposes */
49-
export const cacheResponseHook: RedisResponseCustomAttributeFunction = (
50+
export const cacheResponseHook: IORedisInstrumentationConfig['responseHook'] = (
5051
span: Span,
51-
redisCommand,
52-
cmdArgs,
53-
response,
52+
redisCommand: string,
53+
cmdArgs: IORedisCommandArgs,
54+
response: unknown,
5455
) => {
5556
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');
5657

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* NOTICE from the Sentry authors:
17+
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-ioredis-v0.62.0/packages/instrumentation-ioredis
18+
* - Upstream version: @opentelemetry/instrumentation-ioredis@0.62.0
19+
* - Minor TypeScript adjustments for this repository's compiler settings
20+
*/
21+
/* eslint-disable -- vendored @opentelemetry/instrumentation-ioredis */
22+
23+
import { context, diag, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
24+
import type { Span } from '@opentelemetry/api';
25+
import {
26+
InstrumentationBase,
27+
InstrumentationNodeModuleDefinition,
28+
isWrapped,
29+
safeExecuteInTheMiddle,
30+
SemconvStability,
31+
semconvStabilityFromStr,
32+
} from '@opentelemetry/instrumentation';
33+
import {
34+
ATTR_DB_QUERY_TEXT,
35+
ATTR_DB_SYSTEM_NAME,
36+
ATTR_SERVER_ADDRESS,
37+
ATTR_SERVER_PORT,
38+
} from '@opentelemetry/semantic-conventions';
39+
40+
import { defaultDbStatementSerializer } from './redis-common';
41+
import {
42+
ATTR_DB_CONNECTION_STRING,
43+
ATTR_DB_STATEMENT,
44+
ATTR_DB_SYSTEM,
45+
ATTR_NET_PEER_NAME,
46+
ATTR_NET_PEER_PORT,
47+
DB_SYSTEM_NAME_VALUE_REDIS,
48+
DB_SYSTEM_VALUE_REDIS,
49+
} from './semconv';
50+
import type { IORedisInstrumentationConfig } from './types';
51+
52+
const PACKAGE_NAME = '@opentelemetry/instrumentation-ioredis';
53+
const PACKAGE_VERSION = '0.62.0';
54+
55+
// ---- utils ----
56+
57+
function endSpan(span: Span, err: Error | null | undefined): void {
58+
if (err) {
59+
span.recordException(err);
60+
span.setStatus({
61+
code: SpanStatusCode.ERROR,
62+
message: err.message,
63+
});
64+
}
65+
span.end();
66+
}
67+
68+
// ---- IORedisInstrumentation ----
69+
70+
const DEFAULT_CONFIG: IORedisInstrumentationConfig = {
71+
requireParentSpan: true,
72+
};
73+
74+
export class IORedisInstrumentation extends InstrumentationBase<IORedisInstrumentationConfig> {
75+
_netSemconvStability!: SemconvStability;
76+
_dbSemconvStability!: SemconvStability;
77+
78+
constructor(config: IORedisInstrumentationConfig = {}) {
79+
super(PACKAGE_NAME, PACKAGE_VERSION, { ...DEFAULT_CONFIG, ...config });
80+
this._setSemconvStabilityFromEnv();
81+
}
82+
83+
_setSemconvStabilityFromEnv(): void {
84+
this._netSemconvStability = semconvStabilityFromStr('http', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
85+
this._dbSemconvStability = semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
86+
}
87+
88+
override setConfig(config: IORedisInstrumentationConfig = {}): void {
89+
super.setConfig({ ...DEFAULT_CONFIG, ...config });
90+
}
91+
92+
init() {
93+
return [
94+
new InstrumentationNodeModuleDefinition(
95+
'ioredis',
96+
['>=2.0.0 <6'],
97+
(module: any, moduleVersion?: string) => {
98+
const moduleExports =
99+
module[Symbol.toStringTag] === 'Module'
100+
? module.default // ESM
101+
: module; // CommonJS
102+
if (isWrapped(moduleExports.prototype.sendCommand)) {
103+
this._unwrap(moduleExports.prototype, 'sendCommand');
104+
}
105+
this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand(moduleVersion));
106+
if (isWrapped(moduleExports.prototype.connect)) {
107+
this._unwrap(moduleExports.prototype, 'connect');
108+
}
109+
this._wrap(moduleExports.prototype, 'connect', this._patchConnection());
110+
return module;
111+
},
112+
(module: any) => {
113+
if (module === undefined) return;
114+
const moduleExports =
115+
module[Symbol.toStringTag] === 'Module'
116+
? module.default // ESM
117+
: module; // CommonJS
118+
this._unwrap(moduleExports.prototype, 'sendCommand');
119+
this._unwrap(moduleExports.prototype, 'connect');
120+
},
121+
),
122+
];
123+
}
124+
125+
private _patchSendCommand(moduleVersion?: string) {
126+
return (original: Function) => {
127+
return this._traceSendCommand(original, moduleVersion);
128+
};
129+
}
130+
131+
private _patchConnection() {
132+
return (original: Function) => {
133+
return this._traceConnection(original);
134+
};
135+
}
136+
137+
private _traceSendCommand(original: Function, moduleVersion?: string) {
138+
const instrumentation = this;
139+
return function (this: any, cmd: any) {
140+
if (arguments.length < 1 || typeof cmd !== 'object') {
141+
return original.apply(this, arguments);
142+
}
143+
const config = instrumentation.getConfig();
144+
const dbStatementSerializer = config.dbStatementSerializer || defaultDbStatementSerializer;
145+
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
146+
if (config.requireParentSpan === true && hasNoParentSpan) {
147+
return original.apply(this, arguments);
148+
}
149+
const attributes: Record<string, any> = {};
150+
const { host, port } = this.options;
151+
const dbQueryText = dbStatementSerializer(cmd.name, cmd.args);
152+
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {
153+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
154+
attributes[ATTR_DB_STATEMENT] = dbQueryText;
155+
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
156+
}
157+
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {
158+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
159+
attributes[ATTR_DB_QUERY_TEXT] = dbQueryText;
160+
}
161+
if (instrumentation._netSemconvStability & SemconvStability.OLD) {
162+
attributes[ATTR_NET_PEER_NAME] = host;
163+
attributes[ATTR_NET_PEER_PORT] = port;
164+
}
165+
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
166+
attributes[ATTR_SERVER_ADDRESS] = host;
167+
attributes[ATTR_SERVER_PORT] = port;
168+
}
169+
const span = instrumentation.tracer.startSpan(cmd.name, {
170+
kind: SpanKind.CLIENT,
171+
attributes,
172+
});
173+
const { requestHook } = config;
174+
if (requestHook) {
175+
safeExecuteInTheMiddle(
176+
() =>
177+
requestHook(span, {
178+
moduleVersion,
179+
cmdName: cmd.name,
180+
cmdArgs: cmd.args,
181+
}),
182+
(e: Error | undefined) => {
183+
if (e) {
184+
diag.error('ioredis instrumentation: request hook failed', e);
185+
}
186+
},
187+
true,
188+
);
189+
}
190+
try {
191+
const result = original.apply(this, arguments);
192+
const origResolve = cmd.resolve;
193+
cmd.resolve = function (result: unknown) {
194+
safeExecuteInTheMiddle(
195+
() => config.responseHook?.(span, cmd.name, cmd.args, result),
196+
(e: Error | undefined) => {
197+
if (e) {
198+
diag.error('ioredis instrumentation: response hook failed', e);
199+
}
200+
},
201+
true,
202+
);
203+
endSpan(span, null);
204+
origResolve(result);
205+
};
206+
const origReject = cmd.reject;
207+
cmd.reject = function (err: Error) {
208+
endSpan(span, err);
209+
origReject(err);
210+
};
211+
return result;
212+
} catch (error) {
213+
endSpan(span, error as Error);
214+
throw error;
215+
}
216+
};
217+
}
218+
219+
private _traceConnection(original: Function) {
220+
const instrumentation = this;
221+
return function (this: any) {
222+
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
223+
if (instrumentation.getConfig().requireParentSpan === true && hasNoParentSpan) {
224+
return original.apply(this, arguments);
225+
}
226+
const attributes: Record<string, any> = {};
227+
const { host, port } = this.options;
228+
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {
229+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
230+
attributes[ATTR_DB_STATEMENT] = 'connect';
231+
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
232+
}
233+
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {
234+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
235+
attributes[ATTR_DB_QUERY_TEXT] = 'connect';
236+
}
237+
if (instrumentation._netSemconvStability & SemconvStability.OLD) {
238+
attributes[ATTR_NET_PEER_NAME] = host;
239+
attributes[ATTR_NET_PEER_PORT] = port;
240+
}
241+
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
242+
attributes[ATTR_SERVER_ADDRESS] = host;
243+
attributes[ATTR_SERVER_PORT] = port;
244+
}
245+
const span = instrumentation.tracer.startSpan('connect', {
246+
kind: SpanKind.CLIENT,
247+
attributes,
248+
});
249+
try {
250+
const client = original.apply(this, arguments);
251+
endSpan(span, null);
252+
return client;
253+
} catch (error) {
254+
endSpan(span, error as Error);
255+
throw error;
256+
}
257+
};
258+
}
259+
}

0 commit comments

Comments
 (0)