Skip to content

Commit 322c33d

Browse files
RI-7187 fix tests + add logs data -> plain transformer
1 parent fd60b60 commit 322c33d

File tree

4 files changed

+195
-16
lines changed

4 files changed

+195
-16
lines changed

redisinsight/api/src/common/logger/app-logger.spec.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ const getSessionMetadata = () =>
2525
plainToInstance(SessionMetadata, {
2626
userId: '123',
2727
sessionId: 'test-session-id',
28-
});
28+
requestMetadata: {
29+
any: 'data',
30+
},
31+
}, { groups: ['security' ] });
2932

3033
const getClientMetadata = () =>
3134
plainToInstance(ClientMetadata, {
@@ -34,7 +37,7 @@ const getClientMetadata = () =>
3437
context: ClientContext.Browser,
3538
uniqueId: 'unique-id',
3639
db: 1,
37-
});
40+
}, { groups: ['security' ] });
3841

3942
describe('AppLogger', () => {
4043
let logger: AppLogger;
@@ -115,7 +118,10 @@ describe('AppLogger', () => {
115118
...clientMetadata,
116119
sessionMetadata: undefined,
117120
},
118-
sessionMetadata: clientMetadata.sessionMetadata,
121+
sessionMetadata: {
122+
...clientMetadata.sessionMetadata,
123+
requestMetadata: undefined,
124+
},
119125
data: [{ foo: 'bar' }],
120126
error: undefined,
121127
});
@@ -137,7 +143,10 @@ describe('AppLogger', () => {
137143
expect(mockWinstonLogger[level]).toHaveBeenCalledWith({
138144
message: 'Test message',
139145
context: 'Test context',
140-
sessionMetadata,
146+
sessionMetadata: {
147+
...sessionMetadata,
148+
requestMetadata: undefined,
149+
},
141150
data: [{ foo: 'bar' }],
142151
error: undefined,
143152
});
@@ -168,7 +177,10 @@ describe('AppLogger', () => {
168177
...clientMetadata,
169178
sessionMetadata: undefined,
170179
},
171-
sessionMetadata: clientMetadata.sessionMetadata,
180+
sessionMetadata: {
181+
...clientMetadata.sessionMetadata,
182+
requestMetadata: undefined,
183+
},
172184
data: [{ foo: 'bar' }],
173185
error,
174186
});

redisinsight/api/src/common/logger/app-logger.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { WinstonModule, WinstonModuleOptions } from 'nest-winston';
33
import { cloneDeep, isString } from 'lodash';
44
import { ClientMetadata, SessionMetadata } from 'src/common/models';
55
import { instanceToPlain } from 'class-transformer';
6+
import { logDataToPlain } from 'src/utils/logsFormatter';
67

78
type LogMeta = object;
89

@@ -108,7 +109,7 @@ export class AppLogger implements LoggerService {
108109
context,
109110
error,
110111
...instanceToPlain(userMetadata),
111-
data: optionalParamsCopy?.length ? instanceToPlain(optionalParamsCopy) : undefined,
112+
data: optionalParamsCopy?.length ? logDataToPlain(optionalParamsCopy) : undefined,
112113
};
113114
}
114115

redisinsight/api/src/utils/logsFormatter.spec.ts

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,30 @@ import { BadRequestException, NotFoundException } from '@nestjs/common';
22
import { CloudOauthMisconfigurationException } from 'src/modules/cloud/auth/exceptions';
33
import { AxiosError, AxiosHeaders } from 'axios';
44
import { mockSessionMetadata } from 'src/__mocks__';
5-
import { getOriginalErrorCause, sanitizeError, sanitizeErrors } from './logsFormatter';
5+
import {
6+
ClientContext,
7+
ClientMetadata,
8+
SessionMetadata,
9+
} from 'src/common/models';
10+
import {
11+
getOriginalErrorCause,
12+
logDataToPlain,
13+
sanitizeError,
14+
sanitizeErrors,
15+
} from './logsFormatter';
616

717
const simpleError = new Error('Original error');
818
simpleError['some'] = 'field';
9-
const errorWithCause = new NotFoundException('Not found', { cause: simpleError });
10-
const errorWithCauseDepth2 = new BadRequestException('Bad req', { cause: errorWithCause });
11-
const errorWithCauseDepth3 = new CloudOauthMisconfigurationException('Misconfigured', { cause: errorWithCauseDepth2 });
19+
const errorWithCause = new NotFoundException('Not found', {
20+
cause: simpleError,
21+
});
22+
const errorWithCauseDepth2 = new BadRequestException('Bad req', {
23+
cause: errorWithCause,
24+
});
25+
const errorWithCauseDepth3 = new CloudOauthMisconfigurationException(
26+
'Misconfigured',
27+
{ cause: errorWithCauseDepth2 },
28+
);
1229
const axiosError = new AxiosError(
1330
'Request failed with status code 404',
1431
'NOT_FOUND',
@@ -32,6 +49,30 @@ const axiosError = new AxiosError(
3249
},
3350
);
3451

52+
const mockExtendedClientMetadata = Object.assign(new ClientMetadata(), {
53+
databaseId: 'sdb-id',
54+
context: ClientContext.Browser,
55+
sessionMetadata: Object.assign(new SessionMetadata(), {
56+
...mockSessionMetadata,
57+
data: {
58+
some: 'data',
59+
},
60+
requestMetadata: {
61+
some: 'meta',
62+
},
63+
}),
64+
});
65+
66+
const mockExtendedSessionMetadata = Object.assign(new SessionMetadata(), {
67+
...mockSessionMetadata,
68+
data: {
69+
some: 'data 2',
70+
},
71+
requestMetadata: {
72+
some: 'meta 2',
73+
},
74+
});
75+
3576
const mockLogData: any = {
3677
sessionMetadata: mockSessionMetadata,
3778
error: errorWithCauseDepth3,
@@ -57,6 +98,34 @@ const mockLogData: any = {
5798
};
5899
mockLogData.data.push({ circular: mockLogData.data });
59100

101+
const mockUnsafeLog: any = {
102+
clientMetadata: mockExtendedClientMetadata,
103+
error: errorWithCauseDepth3,
104+
data: [
105+
errorWithCauseDepth2,
106+
{
107+
any: [
108+
'other',
109+
{
110+
possible: 'data',
111+
with: [
112+
'nested',
113+
'structure',
114+
errorWithCause,
115+
{
116+
error: simpleError,
117+
},
118+
],
119+
},
120+
mockExtendedSessionMetadata,
121+
],
122+
},
123+
],
124+
};
125+
mockUnsafeLog.data.push(mockExtendedSessionMetadata);
126+
mockUnsafeLog.data[1].any[1].circular = mockExtendedClientMetadata;
127+
mockUnsafeLog.data.push(mockUnsafeLog.data);
128+
60129
describe('logsFormatter', () => {
61130
describe('getOriginalErrorCause', () => {
62131
it('should return last cause in the chain', () => {
@@ -89,7 +158,9 @@ describe('logsFormatter', () => {
89158
});
90159

91160
it('should return sanitized object with a single original cause for nested errors', () => {
92-
expect(sanitizeError(errorWithCauseDepth3, { omitSensitiveData: true })).toEqual({
161+
expect(
162+
sanitizeError(errorWithCauseDepth3, { omitSensitiveData: true }),
163+
).toEqual({
93164
type: 'CloudOauthMisconfigurationException',
94165
message: errorWithCauseDepth3.message,
95166
cause: {
@@ -174,4 +245,54 @@ describe('logsFormatter', () => {
174245
});
175246
});
176247
});
248+
249+
describe('logDataToPlain', () => {
250+
it('should sanitize all errors and replace circular dependencies after safeTransform of the data', () => {
251+
const result: any = logDataToPlain(mockUnsafeLog);
252+
253+
// should return error instances untouched
254+
expect(result.error).toBeInstanceOf(CloudOauthMisconfigurationException);
255+
expect(result.data[0]).toBeInstanceOf(BadRequestException);
256+
expect(result.data[1].any[1].with[2]).toBeInstanceOf(NotFoundException);
257+
expect(result.data[1].any[1].with[3].error).toBeInstanceOf(Error);
258+
259+
// should sanitize sessionMetadata instances and convert them to plain objects
260+
expect(result).toEqual({
261+
clientMetadata: {
262+
...mockExtendedClientMetadata,
263+
sessionMetadata: {
264+
...mockExtendedClientMetadata.sessionMetadata,
265+
requestMetadata: undefined,
266+
},
267+
},
268+
error: errorWithCauseDepth3,
269+
data: [
270+
errorWithCauseDepth2,
271+
{
272+
any: [
273+
'other',
274+
{
275+
circular: '[Circular]',
276+
possible: 'data',
277+
with: [
278+
'nested',
279+
'structure',
280+
errorWithCause,
281+
{
282+
error: simpleError,
283+
},
284+
],
285+
},
286+
{
287+
...mockExtendedSessionMetadata,
288+
requestMetadata: undefined,
289+
},
290+
],
291+
},
292+
'[Circular]',
293+
'[Circular]',
294+
],
295+
});
296+
});
297+
});
177298
});

redisinsight/api/src/utils/logsFormatter.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { format } from 'winston';
2-
import { omit } from 'lodash';
2+
import { isArray, isObject, isPlainObject, omit } from 'lodash';
33
import { inspect } from 'util';
44
import config, { Config } from 'src/utils/config';
5+
import { instanceToPlain } from 'class-transformer';
56

67
const LOGGER_CONFIG = config.get('logger') as Config['logger'];
78

@@ -23,7 +24,10 @@ export const getOriginalErrorCause = (cause: unknown): Error | undefined => {
2324
return undefined;
2425
};
2526

26-
export const sanitizeError = (error?: Error, opts: SanitizeOptions = {} ): SanitizedError | undefined => {
27+
export const sanitizeError = (
28+
error?: Error,
29+
opts: SanitizeOptions = {},
30+
): SanitizedError | undefined => {
2731
if (!error) return undefined;
2832

2933
return {
@@ -34,7 +38,11 @@ export const sanitizeError = (error?: Error, opts: SanitizeOptions = {} ): Sanit
3438
};
3539
};
3640

37-
export const sanitizeErrors = <T>(obj: T, opts: SanitizeOptions = {}, seen = new WeakMap<any, any>()): T => {
41+
export const sanitizeErrors = <T>(
42+
obj: T,
43+
opts: SanitizeOptions = {},
44+
seen = new WeakMap<any, any>(),
45+
): T => {
3846
if (obj instanceof Error) {
3947
return sanitizeError(obj, opts) as unknown as T;
4048
}
@@ -48,7 +56,7 @@ export const sanitizeErrors = <T>(obj: T, opts: SanitizeOptions = {}, seen = new
4856
const clone: any = Array.isArray(obj) ? [] : {};
4957
seen.set(obj, clone);
5058

51-
Object.keys(obj).forEach(key => {
59+
Object.keys(obj).forEach((key) => {
5260
clone[key] = sanitizeErrors(obj[key], opts, seen);
5361
});
5462

@@ -69,8 +77,45 @@ export const prettyFileFormat = format.printf((info) => {
6977
`${level}`.toUpperCase(),
7078
context,
7179
message,
72-
inspect(omit(info, ['timestamp', 'level', 'context', 'message', 'stack']), { depth: LOGGER_CONFIG.logDepthLevel }),
80+
inspect(omit(info, ['timestamp', 'level', 'context', 'message', 'stack']), {
81+
depth: LOGGER_CONFIG.logDepthLevel,
82+
}),
7383
];
7484

7585
return logData.join(separator);
7686
});
87+
88+
const MAX_DEPTH = 10;
89+
export const logDataToPlain = (value: any, seen = new WeakSet(), depth = 0): any => {
90+
if (depth > MAX_DEPTH) return '[MaxDepthExceeded]';
91+
92+
if (value === null || typeof value !== 'object' || value instanceof Error) {
93+
return value;
94+
}
95+
96+
if (isArray(value)) {
97+
if (seen.has(value)) return '[Circular]';
98+
seen.add(value);
99+
return value.map((val) => logDataToPlain(val, seen, depth + 1));
100+
}
101+
102+
if (isObject(value)) {
103+
if (seen.has(value)) return '[Circular]';
104+
seen.add(value);
105+
106+
if (!isPlainObject(value)) {
107+
return instanceToPlain(value);
108+
}
109+
110+
const plain = {};
111+
Object.keys(value).forEach((key) => {
112+
if (Object.prototype.hasOwnProperty.call(value, key)) {
113+
plain[key] = logDataToPlain(value[key], seen, depth + 1);
114+
}
115+
});
116+
117+
return plain;
118+
}
119+
120+
return value;
121+
};

0 commit comments

Comments
 (0)