Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
"@opentelemetry/instrumentation-graphql": "0.62.0",
"@opentelemetry/instrumentation-hapi": "0.60.0",
"@opentelemetry/instrumentation-http": "0.214.0",
"@opentelemetry/instrumentation-ioredis": "0.62.0",
"@opentelemetry/instrumentation-kafkajs": "0.23.0",
"@opentelemetry/instrumentation-knex": "0.58.0",
"@opentelemetry/instrumentation-koa": "0.62.0",
Expand All @@ -86,7 +85,6 @@
"@opentelemetry/instrumentation-mysql": "0.60.0",
"@opentelemetry/instrumentation-mysql2": "0.60.0",
"@opentelemetry/instrumentation-pg": "0.66.0",
"@opentelemetry/instrumentation-redis": "0.62.0",
"@opentelemetry/instrumentation-tedious": "0.33.0",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type { Span } from '@opentelemetry/api';
import type { RedisResponseCustomAttributeFunction } from '@opentelemetry/instrumentation-ioredis';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
import type { IntegrationFn } from '@sentry/core';
import {
defineIntegration,
Expand All @@ -21,7 +18,10 @@ import {
getCacheOperation,
isInCommands,
shouldConsiderForCache,
} from '../../utils/redisCache';
} from '../../../utils/redisCache';
import type { IORedisInstrumentationConfig } from './vendored/types';
import { IORedisInstrumentation } from './vendored/ioredis-instrumentation';
import { RedisInstrumentation } from './vendored/redis-instrumentation';

interface RedisOptions {
/**
Expand All @@ -46,11 +46,11 @@ const INTEGRATION_NAME = 'Redis';
export let _redisOptions: RedisOptions = {};

/* Only exported for testing purposes */
export const cacheResponseHook: RedisResponseCustomAttributeFunction = (
export const cacheResponseHook: IORedisInstrumentationConfig['responseHook'] = (
span: Span,
redisCommand,
cmdArgs,
response,
redisCommand: string,
cmdArgs: any[],
response: unknown,
) => {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* NOTICE from the Sentry authors:
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-ioredis-v0.62.0/packages/instrumentation-ioredis
* - Upstream version: @opentelemetry/instrumentation-ioredis@0.62.0
* - Minor TypeScript adjustments for this repository's compiler settings
*/
/* eslint-disable -- vendored @opentelemetry/instrumentation-ioredis */

import { context, diag, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
import type { Span } from '@opentelemetry/api';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
isWrapped,
safeExecuteInTheMiddle,
SemconvStability,
semconvStabilityFromStr,
} from '@opentelemetry/instrumentation';
import { ATTR_DB_QUERY_TEXT, ATTR_DB_SYSTEM_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT } from '@opentelemetry/semantic-conventions';

import { defaultDbStatementSerializer } from './redis-common';
import {
ATTR_DB_CONNECTION_STRING,
ATTR_DB_STATEMENT,
ATTR_DB_SYSTEM,
ATTR_NET_PEER_NAME,
ATTR_NET_PEER_PORT,
DB_SYSTEM_NAME_VALUE_REDIS,
DB_SYSTEM_VALUE_REDIS,
} from './semconv';
import type { IORedisInstrumentationConfig } from './types';

const PACKAGE_NAME = '@opentelemetry/instrumentation-ioredis';
const PACKAGE_VERSION = '0.62.0';

// ---- utils ----

function endSpan(span: Span, err: Error | null | undefined): void {
if (err) {
span.recordException(err);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message,
});
}
span.end();
}

// ---- IORedisInstrumentation ----

const DEFAULT_CONFIG: IORedisInstrumentationConfig = {
requireParentSpan: true,
};

export class IORedisInstrumentation extends InstrumentationBase<IORedisInstrumentationConfig> {
_netSemconvStability!: SemconvStability;
_dbSemconvStability!: SemconvStability;

constructor(config: IORedisInstrumentationConfig = {}) {
super(PACKAGE_NAME, PACKAGE_VERSION, { ...DEFAULT_CONFIG, ...config });
this._setSemconvStabilityFromEnv();
}

_setSemconvStabilityFromEnv(): void {
this._netSemconvStability = semconvStabilityFromStr('http', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
this._dbSemconvStability = semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']);
}

override setConfig(config: IORedisInstrumentationConfig = {}): void {
super.setConfig({ ...DEFAULT_CONFIG, ...config });
}

init() {
return [
new InstrumentationNodeModuleDefinition(
'ioredis',
['>=2.0.0 <6'],
(module: any, moduleVersion?: string) => {

Check failure on line 92 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
const moduleExports = module[Symbol.toStringTag] === 'Module'
? module.default // ESM
: module; // CommonJS
if (isWrapped(moduleExports.prototype.sendCommand)) {
this._unwrap(moduleExports.prototype, 'sendCommand');
}
this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand(moduleVersion));
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, 'connect');
}
this._wrap(moduleExports.prototype, 'connect', this._patchConnection());
return module;
},
(module: any) => {

Check failure on line 106 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
if (module === undefined) return;
const moduleExports = module[Symbol.toStringTag] === 'Module'
? module.default // ESM
: module; // CommonJS
this._unwrap(moduleExports.prototype, 'sendCommand');
this._unwrap(moduleExports.prototype, 'connect');
},
),
];
}

private _patchSendCommand(moduleVersion?: string) {
return (original: Function) => {
return this._traceSendCommand(original, moduleVersion);
};
}

private _patchConnection() {
return (original: Function) => {
return this._traceConnection(original);
};
}

private _traceSendCommand(original: Function, moduleVersion?: string) {
const instrumentation = this;

Check warning on line 131 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-this-alias)

Unexpected aliasing of 'this' to local variable.
return function (this: any, cmd: any) {

Check failure on line 132 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.

Check failure on line 132 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
if (arguments.length < 1 || typeof cmd !== 'object') {
return original.apply(this, arguments);
}
const config = instrumentation.getConfig();
const dbStatementSerializer = config.dbStatementSerializer || defaultDbStatementSerializer;
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
if (config.requireParentSpan === true && hasNoParentSpan) {
return original.apply(this, arguments);
}
const attributes: Record<string, any> = {};

Check failure on line 142 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
const { host, port } = this.options;
const dbQueryText = dbStatementSerializer(cmd.name, cmd.args);
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {

Check failure on line 145 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

eslint(no-bitwise)

Unexpected use of `"&"`.
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
attributes[ATTR_DB_STATEMENT] = dbQueryText;
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
}
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {

Check failure on line 150 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

eslint(no-bitwise)

Unexpected use of `"&"`.
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
attributes[ATTR_DB_QUERY_TEXT] = dbQueryText;
}
if (instrumentation._netSemconvStability & SemconvStability.OLD) {

Check failure on line 154 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

eslint(no-bitwise)

Unexpected use of `"&"`.
attributes[ATTR_NET_PEER_NAME] = host;
attributes[ATTR_NET_PEER_PORT] = port;
}
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
attributes[ATTR_SERVER_ADDRESS] = host;
attributes[ATTR_SERVER_PORT] = port;
}
const span = instrumentation.tracer.startSpan(cmd.name, {
kind: SpanKind.CLIENT,
attributes,
});
const { requestHook } = config;
if (requestHook) {
safeExecuteInTheMiddle(
() =>
requestHook(span, {
moduleVersion,
cmdName: cmd.name,
cmdArgs: cmd.args,
}),
(e: Error | undefined) => {
if (e) {
diag.error('ioredis instrumentation: request hook failed', e);
}
},
true,
);
}
try {
const result = original.apply(this, arguments);
const origResolve = cmd.resolve;
cmd.resolve = function (result: unknown) {
safeExecuteInTheMiddle(
() => config.responseHook?.(span, cmd.name, cmd.args, result),
(e: Error | undefined) => {
if (e) {
diag.error('ioredis instrumentation: response hook failed', e);
}
},
true,
);
endSpan(span, null);
origResolve(result);
};
const origReject = cmd.reject;
cmd.reject = function (err: Error) {
endSpan(span, err);
origReject(err);
};
return result;
} catch (error) {
endSpan(span, error as Error);
throw error;
}
};
}

private _traceConnection(original: Function) {
const instrumentation = this;

Check warning on line 213 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-this-alias)

Unexpected aliasing of 'this' to local variable.
return function (this: any) {

Check failure on line 214 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
const hasNoParentSpan = trace.getSpan(context.active()) === undefined;
if (instrumentation.getConfig().requireParentSpan === true && hasNoParentSpan) {
return original.apply(this, arguments);
}
const attributes: Record<string, any> = {};

Check failure on line 219 in packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts

View workflow job for this annotation

GitHub Actions / Lint

typescript-eslint(no-explicit-any)

Unexpected `any`. Specify a different type.
const { host, port } = this.options;
if (instrumentation._dbSemconvStability & SemconvStability.OLD) {
attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS;
attributes[ATTR_DB_STATEMENT] = 'connect';
attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`;
}
if (instrumentation._dbSemconvStability & SemconvStability.STABLE) {
attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS;
attributes[ATTR_DB_QUERY_TEXT] = 'connect';
}
if (instrumentation._netSemconvStability & SemconvStability.OLD) {
attributes[ATTR_NET_PEER_NAME] = host;
attributes[ATTR_NET_PEER_PORT] = port;
}
if (instrumentation._netSemconvStability & SemconvStability.STABLE) {
attributes[ATTR_SERVER_ADDRESS] = host;
attributes[ATTR_SERVER_PORT] = port;
}
const span = instrumentation.tracer.startSpan('connect', {
kind: SpanKind.CLIENT,
attributes,
});
try {
const client = original.apply(this, arguments);
endSpan(span, null);
return client;
} catch (error) {
endSpan(span, error as Error);
throw error;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* NOTICE from the Sentry authors:
* - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-redis-v0.62.0/packages/redis-common
* - Upstream version: @opentelemetry/redis-common@0.38.2
* - Minor TypeScript adjustments for this repository's compiler settings
*/
/* eslint-disable -- vendored @opentelemetry/redis-common */

/**
* List of regexes and the number of arguments that should be serialized for matching commands.
* For example, HSET should serialize which key and field it's operating on, but not its value.
* Setting the subset to -1 will serialize all arguments.
* Commands without a match will have their first argument serialized.
*
* Refer to https://redis.io/commands/ for the full list.
*/
const serializationSubsets = [
{
regex: /^ECHO/i,
args: 0,
},
{
regex: /^(LPUSH|MSET|PFA|PUBLISH|RPUSH|SADD|SET|SPUBLISH|XADD|ZADD)/i,
args: 1,
},
{
regex: /^(HSET|HMSET|LSET|LINSERT)/i,
args: 2,
},
{
regex:
/^(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,
args: -1,
},
];

/**
* Given the redis command name and arguments, return a combination of the
* command name + the allowed arguments according to `serializationSubsets`.
*/
export const defaultDbStatementSerializer = (cmdName: string, cmdArgs: Array<string | Buffer | number | any[]>): string => {
if (Array.isArray(cmdArgs) && cmdArgs.length) {
const nArgsToSerialize = serializationSubsets.find(({ regex }) => regex.test(cmdName))?.args ?? 0;
const argsToSerialize: Array<string | Buffer | number | any[]> =
nArgsToSerialize >= 0 ? cmdArgs.slice(0, nArgsToSerialize) : cmdArgs.slice();
if (cmdArgs.length > argsToSerialize.length) {
argsToSerialize.push(`[${cmdArgs.length - nArgsToSerialize} other arguments]`);
}
return `${cmdName} ${argsToSerialize.join(' ')}`;
}
return cmdName;
};
Loading
Loading