Skip to content

Commit 345c2eb

Browse files
committed
feat(node): vendor ioredis, redis instrumentaitons
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 345c2eb

12 files changed

Lines changed: 1622 additions & 34 deletions

File tree

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: 8 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,
@@ -21,7 +18,10 @@ import {
2118
getCacheOperation,
2219
isInCommands,
2320
shouldConsiderForCache,
24-
} from '../../utils/redisCache';
21+
} from '../../../utils/redisCache';
22+
import type { IORedisInstrumentationConfig } from './vendored/types';
23+
import { IORedisInstrumentation } from './vendored/ioredis-instrumentation';
24+
import { RedisInstrumentation } from './vendored/redis-instrumentation';
2525

2626
interface RedisOptions {
2727
/**
@@ -46,11 +46,11 @@ const INTEGRATION_NAME = 'Redis';
4646
export let _redisOptions: RedisOptions = {};
4747

4848
/* Only exported for testing purposes */
49-
export const cacheResponseHook: RedisResponseCustomAttributeFunction = (
49+
export const cacheResponseHook: IORedisInstrumentationConfig['responseHook'] = (
5050
span: Span,
51-
redisCommand,
52-
cmdArgs,
53-
response,
51+
redisCommand: string,
52+
cmdArgs: any[],
53+
response: unknown,
5454
) => {
5555
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');
5656

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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/[email protected]
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 { ATTR_DB_QUERY_TEXT, ATTR_DB_SYSTEM_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT } from '@opentelemetry/semantic-conventions';
34+
35+
import { defaultDbStatementSerializer } from './redis-common';
36+
import {
37+
ATTR_DB_CONNECTION_STRING,
38+
ATTR_DB_STATEMENT,
39+
ATTR_DB_SYSTEM,
40+
ATTR_NET_PEER_NAME,
41+
ATTR_NET_PEER_PORT,
42+
DB_SYSTEM_NAME_VALUE_REDIS,
43+
DB_SYSTEM_VALUE_REDIS,
44+
} from './semconv';
45+
import type { IORedisInstrumentationConfig } from './types';
46+
47+
const PACKAGE_NAME = '@opentelemetry/instrumentation-ioredis';
48+
const PACKAGE_VERSION = '0.62.0';
49+
50+
// ---- utils ----
51+
52+
function endSpan(span: Span, err: Error | null | undefined): void {
53+
if (err) {
54+
span.recordException(err);
55+
span.setStatus({
56+
code: SpanStatusCode.ERROR,
57+
message: err.message,
58+
});
59+
}
60+
span.end();
61+
}
62+
63+
// ---- IORedisInstrumentation ----
64+
65+
const DEFAULT_CONFIG: IORedisInstrumentationConfig = {
66+
requireParentSpan: true,
67+
};
68+
69+
export class IORedisInstrumentation extends InstrumentationBase<IORedisInstrumentationConfig> {
70+
_netSemconvStability!: SemconvStability;
71+
_dbSemconvStability!: SemconvStability;
72+
73+
constructor(config: IORedisInstrumentationConfig = {}) {
74+
super(PACKAGE_NAME, PACKAGE_VERSION, { ...DEFAULT_CONFIG, ...config });
75+
this._setSemconvStabilityFromEnv();
76+
}
77+
78+
_setSemconvStabilityFromEnv(): void {
79+
this._netSemconvStability = semconvStabilityFromStr('http', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
80+
this._dbSemconvStability = semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
81+
}
82+
83+
override setConfig(config: IORedisInstrumentationConfig = {}): void {
84+
super.setConfig({ ...DEFAULT_CONFIG, ...config });
85+
}
86+
87+
init() {
88+
return [
89+
new InstrumentationNodeModuleDefinition(
90+
'ioredis',
91+
['>=2.0.0 <6'],
92+
(module: any, moduleVersion?: string) => {
93+
const moduleExports = module[Symbol.toStringTag] === 'Module'
94+
? module.default // ESM
95+
: module; // CommonJS
96+
if (isWrapped(moduleExports.prototype.sendCommand)) {
97+
this._unwrap(moduleExports.prototype, 'sendCommand');
98+
}
99+
this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand(moduleVersion));
100+
if (isWrapped(moduleExports.prototype.connect)) {
101+
this._unwrap(moduleExports.prototype, 'connect');
102+
}
103+
this._wrap(moduleExports.prototype, 'connect', this._patchConnection());
104+
return module;
105+
},
106+
(module: any) => {
107+
if (module === undefined) return;
108+
const moduleExports = module[Symbol.toStringTag] === 'Module'
109+
? module.default // ESM
110+
: module; // CommonJS
111+
this._unwrap(moduleExports.prototype, 'sendCommand');
112+
this._unwrap(moduleExports.prototype, 'connect');
113+
},
114+
),
115+
];
116+
}
117+
118+
private _patchSendCommand(moduleVersion?: string) {
119+
return (original: Function) => {
120+
return this._traceSendCommand(original, moduleVersion);
121+
};
122+
}
123+
124+
private _patchConnection() {
125+
return (original: Function) => {
126+
return this._traceConnection(original);
127+
};
128+
}
129+
130+
private _traceSendCommand(original: Function, moduleVersion?: string) {
131+
const instrumentation = this;
132+
return function (this: any, cmd: any) {
133+
if (arguments.length < 1 || typeof cmd !== 'object') {
134+
return original.apply(this, arguments);
135+
}
136+
const config = instrumentation.getConfig();
137+
const dbStatementSerializer = config.dbStatementSerializer || defaultDbStatementSerializer;
138+
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
139+
if (config.requireParentSpan === true && hasNoParentSpan) {
140+
return original.apply(this, arguments);
141+
}
142+
const attributes: Record<string, any> = {};
143+
const { host, port } = this.options;
144+
const dbQueryText = dbStatementSerializer(cmd.name, cmd.args);
145+
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {
146+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
147+
attributes[ATTR_DB_STATEMENT] = dbQueryText;
148+
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
149+
}
150+
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {
151+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
152+
attributes[ATTR_DB_QUERY_TEXT] = dbQueryText;
153+
}
154+
if (instrumentation._netSemconvStability & SemconvStability.OLD) {
155+
attributes[ATTR_NET_PEER_NAME] = host;
156+
attributes[ATTR_NET_PEER_PORT] = port;
157+
}
158+
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
159+
attributes[ATTR_SERVER_ADDRESS] = host;
160+
attributes[ATTR_SERVER_PORT] = port;
161+
}
162+
const span = instrumentation.tracer.startSpan(cmd.name, {
163+
kind: SpanKind.CLIENT,
164+
attributes,
165+
});
166+
const { requestHook } = config;
167+
if (requestHook) {
168+
safeExecuteInTheMiddle(
169+
() =>
170+
requestHook(span, {
171+
moduleVersion,
172+
cmdName: cmd.name,
173+
cmdArgs: cmd.args,
174+
}),
175+
(e: Error | undefined) => {
176+
if (e) {
177+
diag.error('ioredis instrumentation: request hook failed', e);
178+
}
179+
},
180+
true,
181+
);
182+
}
183+
try {
184+
const result = original.apply(this, arguments);
185+
const origResolve = cmd.resolve;
186+
cmd.resolve = function (result: unknown) {
187+
safeExecuteInTheMiddle(
188+
() => config.responseHook?.(span, cmd.name, cmd.args, result),
189+
(e: Error | undefined) => {
190+
if (e) {
191+
diag.error('ioredis instrumentation: response hook failed', e);
192+
}
193+
},
194+
true,
195+
);
196+
endSpan(span, null);
197+
origResolve(result);
198+
};
199+
const origReject = cmd.reject;
200+
cmd.reject = function (err: Error) {
201+
endSpan(span, err);
202+
origReject(err);
203+
};
204+
return result;
205+
} catch (error) {
206+
endSpan(span, error as Error);
207+
throw error;
208+
}
209+
};
210+
}
211+
212+
private _traceConnection(original: Function) {
213+
const instrumentation = this;
214+
return function (this: any) {
215+
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
216+
if (instrumentation.getConfig().requireParentSpan === true && hasNoParentSpan) {
217+
return original.apply(this, arguments);
218+
}
219+
const attributes: Record<string, any> = {};
220+
const { host, port } = this.options;
221+
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {
222+
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
223+
attributes[ATTR_DB_STATEMENT] = 'connect';
224+
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
225+
}
226+
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {
227+
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
228+
attributes[ATTR_DB_QUERY_TEXT] = 'connect';
229+
}
230+
if (instrumentation._netSemconvStability & SemconvStability.OLD) {
231+
attributes[ATTR_NET_PEER_NAME] = host;
232+
attributes[ATTR_NET_PEER_PORT] = port;
233+
}
234+
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
235+
attributes[ATTR_SERVER_ADDRESS] = host;
236+
attributes[ATTR_SERVER_PORT] = port;
237+
}
238+
const span = instrumentation.tracer.startSpan('connect', {
239+
kind: SpanKind.CLIENT,
240+
attributes,
241+
});
242+
try {
243+
const client = original.apply(this, arguments);
244+
endSpan(span, null);
245+
return client;
246+
} catch (error) {
247+
endSpan(span, error as Error);
248+
throw error;
249+
}
250+
};
251+
}
252+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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-redis-v0.62.0/packages/redis-common
18+
* - Upstream version: @opentelemetry/[email protected]
19+
* - Minor TypeScript adjustments for this repository's compiler settings
20+
*/
21+
/* eslint-disable -- vendored @opentelemetry/redis-common */
22+
23+
/**
24+
* List of regexes and the number of arguments that should be serialized for matching commands.
25+
* For example, HSET should serialize which key and field it's operating on, but not its value.
26+
* Setting the subset to -1 will serialize all arguments.
27+
* Commands without a match will have their first argument serialized.
28+
*
29+
* Refer to https://redis.io/commands/ for the full list.
30+
*/
31+
const serializationSubsets = [
32+
{
33+
regex: /^ECHO/i,
34+
args: 0,
35+
},
36+
{
37+
regex: /^(LPUSH|MSET|PFA|PUBLISH|RPUSH|SADD|SET|SPUBLISH|XADD|ZADD)/i,
38+
args: 1,
39+
},
40+
{
41+
regex: /^(HSET|HMSET|LSET|LINSERT)/i,
42+
args: 2,
43+
},
44+
{
45+
regex:
46+
/^(ACL|BIT|B[LRZ]|CLIENT|CLUSTER|CONFIG|COMMAND|DECR|DEL|EVAL|EX|FUNCTION|GEO|GET|HINCR|HMGET|HSCAN|INCR|L[TRLM]|MEMORY|P[EFISTU]|RPOP|S[CDIMORSU]|XACK|X[CDGILPRT]|Z[CDILMPRS])/i,
47+
args: -1,
48+
},
49+
];
50+
51+
/**
52+
* Given the redis command name and arguments, return a combination of the
53+
* command name + the allowed arguments according to `serializationSubsets`.
54+
*/
55+
export const defaultDbStatementSerializer = (cmdName: string, cmdArgs: Array<string | Buffer | number | any[]>): string => {
56+
if (Array.isArray(cmdArgs) && cmdArgs.length) {
57+
const nArgsToSerialize = serializationSubsets.find(({ regex }) => regex.test(cmdName))?.args ?? 0;
58+
const argsToSerialize: Array<string | Buffer | number | any[]> =
59+
nArgsToSerialize >= 0 ? cmdArgs.slice(0, nArgsToSerialize) : cmdArgs.slice();
60+
if (cmdArgs.length > argsToSerialize.length) {
61+
argsToSerialize.push(`[${cmdArgs.length - nArgsToSerialize} other arguments]`);
62+
}
63+
return `${cmdName} ${argsToSerialize.join(' ')}`;
64+
}
65+
return cmdName;
66+
};

0 commit comments

Comments
 (0)