Skip to content

Commit f828a86

Browse files
authored
[IOPLT-945] Update redis client with appinsights tracing (#1182)
* Update redis client with appinsights tracing * Add redis trace wrapper tests Add appinsights mock
1 parent 8ca666c commit f828a86

File tree

4 files changed

+199
-22
lines changed

4 files changed

+199
-22
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as appInsights from "applicationinsights";
2+
3+
// TODO: Add types when appinsights lib will be added
4+
export const mockTrackEvent = jest.fn();
5+
export const mockTrackDependency = jest.fn();
6+
7+
export const mockedAppinsightsTelemetryClient = {
8+
trackEvent: mockTrackEvent,
9+
trackDependency: mockTrackDependency,
10+
} as any as appInsights.TelemetryClient;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createWrappedRedisClusterClient } from "../redis-trace-wrapper";
2+
import { mockedAppinsightsTelemetryClient, mockTrackDependency } from "../__mocks__/appinsights.mocks";
3+
4+
5+
const mockedAppInsightsClient = mockedAppinsightsTelemetryClient;
6+
7+
const mockGet = jest.fn(async () => "42");
8+
9+
jest.mock("redis", () => ({
10+
createCluster: () => ({
11+
get: mockGet,
12+
}),
13+
}));
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
describe("createWrappedRedisClusterClient |> trace enabled", () => {
20+
const wrappedClient = createWrappedRedisClusterClient(
21+
{} as any,
22+
"TEST",
23+
mockedAppInsightsClient,
24+
);
25+
26+
it("should call `trackDependency` when the request completed successfully", async () => {
27+
const result = await wrappedClient.get("TEST");
28+
29+
expect(result).toEqual("42");
30+
expect(mockTrackDependency).toHaveBeenCalledWith({
31+
target: "Redis Cluster - TEST",
32+
name: "get",
33+
data: "",
34+
resultCode: "",
35+
duration: expect.any(Number),
36+
success: true,
37+
dependencyTypeName: "REDIS",
38+
});
39+
});
40+
41+
test("should call `trackDependency` when the request failed", async () => {
42+
mockGet.mockRejectedValueOnce({});
43+
44+
try {
45+
await wrappedClient.get("TEST");
46+
} catch (e) {}
47+
48+
expect(mockTrackDependency).toHaveBeenCalledWith({
49+
target: "Redis Cluster - TEST",
50+
name: "get",
51+
data: "",
52+
resultCode: "ERROR",
53+
duration: expect.any(Number),
54+
success: false,
55+
dependencyTypeName: "REDIS",
56+
});
57+
});
58+
});
59+
60+
describe("createWrappedRedisClusterClient |> trace disabled", () => {
61+
const wrappedClient = createWrappedRedisClusterClient(
62+
{} as any,
63+
"TEST",
64+
);
65+
66+
test("should NOT call `trackDependency` when the request completed successfully", async () => {
67+
const result = await wrappedClient.get("TEST");
68+
69+
expect(result).toEqual("42");
70+
expect(mockTrackDependency).not.toHaveBeenCalled();
71+
});
72+
73+
test("should NOT call `trackDependency` when the request failed", async () => {
74+
mockGet.mockRejectedValueOnce({});
75+
76+
try {
77+
await wrappedClient.get("TEST");
78+
} catch (e) {}
79+
80+
expect(mockTrackDependency).not.toHaveBeenCalled();
81+
});
82+
});

src/utils/redis-trace-wrapper.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import commands from "@redis/client/dist/lib/cluster/commands";
2+
import * as appInsights from "applicationinsights";
3+
import * as redis from "redis";
4+
5+
function wrapAsyncFunctionWithAppInsights<
6+
K extends keyof redis.RedisClusterType,
7+
T extends redis.RedisClusterType[K]
8+
>(
9+
redisClient: redis.RedisClusterType,
10+
originalFunction: T,
11+
functionName: string,
12+
clientName: string,
13+
appInsightsClient: appInsights.TelemetryClient
14+
): T {
15+
return async function (...args: Array<unknown>) {
16+
const startTime = Date.now();
17+
18+
try {
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
const result = await (originalFunction as any).apply(redisClient, args);
21+
const duration = Date.now() - startTime;
22+
23+
// Do not log any argument or result,
24+
// as they can contain personal information
25+
appInsightsClient.trackDependency({
26+
target: `Redis Cluster - ${clientName}`,
27+
name: functionName,
28+
data: "",
29+
resultCode: "",
30+
duration,
31+
success: true,
32+
dependencyTypeName: "REDIS"
33+
});
34+
35+
return result;
36+
} catch (error) {
37+
const duration = Date.now() - startTime;
38+
appInsightsClient.trackDependency({
39+
target: `Redis Cluster - ${clientName}`,
40+
name: functionName,
41+
data: "",
42+
resultCode: "ERROR",
43+
duration,
44+
success: false,
45+
dependencyTypeName: "REDIS"
46+
});
47+
throw error;
48+
}
49+
} as T;
50+
}
51+
52+
function wrapRedisClusterClient(
53+
client: redis.RedisClusterType,
54+
clientName: string,
55+
appInsightsClient: appInsights.TelemetryClient
56+
) {
57+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
58+
const clientAsObject = client as Record<any, any>;
59+
for (const functionName of Object.keys(commands)) {
60+
if (typeof clientAsObject[functionName] === "function") {
61+
clientAsObject[functionName] = wrapAsyncFunctionWithAppInsights(
62+
client,
63+
clientAsObject[functionName],
64+
functionName,
65+
clientName,
66+
appInsightsClient
67+
);
68+
}
69+
}
70+
71+
return client;
72+
}
73+
74+
export function createWrappedRedisClusterClient(
75+
options: redis.RedisClusterOptions,
76+
clientName: string,
77+
appInsightsClient?: appInsights.TelemetryClient
78+
) {
79+
const cluster = redis.createCluster(options);
80+
return appInsightsClient
81+
? wrapRedisClusterClient(cluster, clientName, appInsightsClient)
82+
: cluster;
83+
}

src/utils/redis.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as redis from "redis";
66

77
import { keyPrefixes } from "../services/redisSessionStorage";
88
import { log } from "./logger";
9+
import { createWrappedRedisClusterClient } from "./redis-trace-wrapper";
910

1011
export const obfuscateTokensInfo = (message: string) =>
1112
pipe(
@@ -36,29 +37,30 @@ export const createClusterRedisClient =
3637
const redisPort: number = parseInt(port || DEFAULT_REDIS_PORT, 10);
3738
log.info("Creating CLUSTER redis client", { url: completeRedisUrl });
3839

39-
const redisClient = redis.createCluster<
40-
Record<string, never>,
41-
Record<string, never>,
42-
Record<string, never>
43-
>({
44-
defaults: {
45-
legacyMode: false,
46-
password,
47-
socket: {
48-
// TODO: We can add a whitelist with all the IP addresses of the redis clsuter
49-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
50-
checkServerIdentity: (_hostname, _cert) => undefined,
51-
keepAlive: 2000,
52-
tls: enableTls
53-
}
40+
const redisClient = createWrappedRedisClusterClient(
41+
{
42+
defaults: {
43+
legacyMode: false,
44+
password,
45+
socket: {
46+
// TODO: We can add a whitelist with all the IP addresses of the redis clsuter
47+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
48+
checkServerIdentity: (_hostname, _cert) => undefined,
49+
keepAlive: 2000,
50+
tls: enableTls
51+
}
52+
},
53+
rootNodes: [
54+
{
55+
url: `${completeRedisUrl}:${redisPort}`
56+
}
57+
],
58+
useReplicas
5459
},
55-
rootNodes: [
56-
{
57-
url: `${completeRedisUrl}:${redisPort}`
58-
}
59-
],
60-
useReplicas
61-
});
60+
useReplicas ? "FAST" : "SAFE",
61+
appInsightsClient
62+
);
63+
6264
redisClient.on("error", (err) => {
6365
log.error("[REDIS Error] an error occurs on redis client: %s", err);
6466
appInsightsClient?.trackEvent({

0 commit comments

Comments
 (0)