Skip to content

Commit 02cbe04

Browse files
committed
feat(backend): add jsonOutput option to logger
1 parent 4a77365 commit 02cbe04

File tree

10 files changed

+207
-208
lines changed

10 files changed

+207
-208
lines changed

backend/src/applications/comments/services/comments-manager.service.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe(CommentsManager.name, () => {
177177
expect(notificationsManager.create).not.toHaveBeenCalled()
178178
expect(res).toEqual({ id: 777, fileId: 555, content: 'hello' })
179179
// Verify that the catch of createComment logged the error
180-
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('createComment'))
180+
expect(loggerSpy).toHaveBeenCalledWith(expect.objectContaining({ tag: 'createComment' }))
181181
loggerSpy.mockRestore()
182182
})
183183

@@ -196,7 +196,7 @@ describe(CommentsManager.name, () => {
196196

197197
expect(notificationsManager.create).toHaveBeenCalledTimes(1)
198198
notificationsManager.create.mockClear()
199-
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('notify'))
199+
expect(loggerSpy).toHaveBeenCalledWith(expect.objectContaining({ tag: 'notify' }))
200200
loggerSpy.mockRestore()
201201

202202
const space = makeSpace()
@@ -235,7 +235,7 @@ describe(CommentsManager.name, () => {
235235
await new Promise((r) => setImmediate(r))
236236

237237
expect(notificationsManager.create).toHaveBeenCalledTimes(1)
238-
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('notify'))
238+
expect(loggerSpy).toHaveBeenCalledWith(expect.objectContaining({ tag: 'notify' }))
239239
loggerSpy.mockRestore()
240240
})
241241

backend/src/applications/notifications/services/notifications-manager.service.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe(NotificationsManager.name, () => {
126126
await service.create([1], { app: NOTIFICATION_APP.LINKS } as any)
127127
await flushPromises()
128128
expect(loggerSpy).toHaveBeenCalled()
129-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/create/i)
129+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'storeNotification' })
130130
})
131131

132132
it('logs error when storeNotification promise rejects (create catch)', async () => {
@@ -135,7 +135,7 @@ describe(NotificationsManager.name, () => {
135135
await service.create([1, 2], { app: NOTIFICATION_APP.SYNC } as any, { author: { id: 5, login: 'xx' } } as any)
136136
await flushPromises()
137137
expect(loggerSpy).toHaveBeenCalled()
138-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/create/i)
138+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'create' })
139139
})
140140

141141
it('logs error when sendEmailNotification rejects (create catch)', async () => {
@@ -145,7 +145,7 @@ describe(NotificationsManager.name, () => {
145145
await service.create([1], { app: NOTIFICATION_APP.COMMENTS } as any)
146146
await flushPromises()
147147
expect(loggerSpy).toHaveBeenCalled()
148-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/create/i)
148+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'create' })
149149
})
150150
})
151151

@@ -158,7 +158,7 @@ describe(NotificationsManager.name, () => {
158158
service.wasRead({ id: 8 } as any, undefined)
159159
await flushPromises()
160160
expect(loggerSpy).toHaveBeenCalled()
161-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/wasRead/i)
161+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'wasRead' })
162162
})
163163
})
164164

@@ -206,7 +206,7 @@ describe(NotificationsManager.name, () => {
206206
const loggerSpy = spyLogger()
207207
await service.sendEmailNotification([{ id: 1, email: 'a@test', language: 'en' }] as any, { app: NOTIFICATION_APP.SYNC } as any, {} as any)
208208
expect(loggerSpy).toHaveBeenCalled()
209-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/sendEmailNotification/i)
209+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'sendEmailNotification' })
210210
})
211211
})
212212

@@ -249,7 +249,7 @@ describe(NotificationsManager.name, () => {
249249
const result = (service as any).genMail('en', { app: 99999 } as any, {} as any)
250250
expect(result).toBeUndefined()
251251
expect(loggerSpy).toHaveBeenCalled()
252-
expect(loggerSpy.mock.calls[0]?.[0] as string).toMatch(/case not handled/i)
252+
expect(loggerSpy.mock.calls[0]?.[0]).toMatchObject({ tag: 'genMail', msg: expect.stringContaining('case not handled') })
253253
})
254254
})
255255
})

backend/src/applications/sync/services/sync-paths-manager.service.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ describe(SyncPathsManager.name, () => {
411411
await flush()
412412

413413
expect(loggerSpy).toHaveBeenCalled()
414-
expect(loggerSpy.mock.calls.some(([msg]) => String(msg).includes('updatePaths'))).toBe(true)
414+
expect(loggerSpy.mock.calls.some(([payload]: { tag: string }[]) => payload?.tag === 'updatePaths')).toBe(true)
415415
})
416416

417417
it('should catch notify failure at updatePaths level when building notification fails', async () => {
@@ -427,7 +427,7 @@ describe(SyncPathsManager.name, () => {
427427

428428
expect(res.delete).toEqual([1])
429429
expect(loggerSpy).toHaveBeenCalled()
430-
expect(loggerSpy.mock.calls.some(([msg]) => String(msg).includes('updatePaths'))).toBe(true)
430+
expect(loggerSpy.mock.calls.some(([payload]: { tag: string }[]) => payload?.tag === 'updatePaths')).toBe(true)
431431
// create not called because we failed before reaching it
432432
expect(notificationsManager.create).not.toHaveBeenCalled()
433433
})
@@ -446,7 +446,7 @@ describe(SyncPathsManager.name, () => {
446446
expect(res.delete).toEqual([2])
447447
expect(loggerSpy).toHaveBeenCalled()
448448
// error comes from notify() catch
449-
expect(loggerSpy.mock.calls.some(([msg]) => String(msg).includes('notify'))).toBe(true)
449+
expect(loggerSpy.mock.calls.some(([payload]: { tag: string }[]) => payload?.tag === 'notify')).toBe(true)
450450
})
451451
})
452452

backend/src/applications/users/services/users-manager.service.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe(UsersManager.name, () => {
174174
const updSpy1 = jest.spyOn(usersManager, 'updateAccesses').mockRejectedValue(new Error('reject-locked'))
175175
await expect(usersManager.logUser(uLocked, 'pwd', 'ip')).rejects.toThrow('Account locked')
176176
await flush()
177-
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('reject-locked'))
177+
expect(errSpy.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes('reject-locked'))).toBe(true)
178178
expect(updSpy1).toHaveBeenCalledWith(uLocked, 'ip', false)
179179
;(comparePassword as jest.Mock).mockResolvedValue(false)
180180
const uBad = new UserModel({ ...generateUserTest(), isActive: true, passwordAttempts: 0 }, false)
@@ -183,7 +183,7 @@ describe(UsersManager.name, () => {
183183
const out = await usersManager.logUser(uBad, 'bad', '1.1.1.1')
184184
expect(out).toBeNull()
185185
await flush()
186-
expect(errSpy2).toHaveBeenCalledWith(expect.stringContaining('reject-auth'))
186+
expect(errSpy2.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes('reject-auth'))).toBe(true)
187187
expect(updSpy2).toHaveBeenCalledWith(uBad, '1.1.1.1', false)
188188
;(comparePassword as jest.Mock).mockResolvedValue(true)
189189
const uGood = new UserModel({ ...generateUserTest(), isActive: true, passwordAttempts: 0 }, false)
@@ -487,7 +487,7 @@ describe(UsersManager.name, () => {
487487
const lSpy = jest.spyOn((usersManager as any)['logger'], 'log').mockImplementation(() => undefined as any)
488488
usersQueriesService.updateGroupMembers = jest.fn().mockResolvedValue(undefined)
489489
await expect(usersManager.leavePersonalGroup(userTest, 1)).resolves.toBeUndefined()
490-
expect(lSpy).toHaveBeenCalledWith(expect.stringMatching(/has left group/))
490+
expect(lSpy).toHaveBeenCalledWith(expect.objectContaining({ msg: expect.stringMatching(/has left group/) }))
491491
usersQueriesService.updateGroupMembers = jest.fn().mockRejectedValue(new Error('DB'))
492492
await expect(usersManager.leavePersonalGroup(userTest, 1)).rejects.toThrow('DB')
493493
usersQueriesService.getGroupWithMembers = jest.fn().mockResolvedValue({
@@ -507,11 +507,11 @@ describe(UsersManager.name, () => {
507507
const wSpy = jest.spyOn((usersManager as any)['logger'], 'warn').mockImplementation(() => undefined as any)
508508
usersQueriesService.deletePersonalGroup = jest.fn().mockResolvedValue(false)
509509
await expect(usersManager.deletePersonalGroup(userTest, 7)).rejects.toThrow('Unable to delete group')
510-
expect(wSpy).toHaveBeenCalledWith(expect.stringMatching(/does not exist/))
510+
expect(wSpy).toHaveBeenCalledWith(expect.objectContaining({ msg: expect.stringMatching(/does not exist/) }))
511511
const lgSpy = jest.spyOn((usersManager as any)['logger'], 'log').mockImplementation(() => undefined as any)
512512
usersQueriesService.deletePersonalGroup = jest.fn().mockResolvedValue(true)
513513
await expect(usersManager.deletePersonalGroup(userTest, 7)).resolves.toBeUndefined()
514-
expect(lgSpy).toHaveBeenCalledWith(expect.stringMatching(/was deleted/))
514+
expect(lgSpy).toHaveBeenCalledWith(expect.objectContaining({ msg: expect.stringMatching(/was deleted/) }))
515515
})
516516

517517
it('guests + proxies', async () => {

backend/src/applications/webdav/services/webdav-methods.service.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,7 +1447,7 @@ describe('WebDAVMethods', () => {
14471447
// Expected to throw
14481448
}
14491449

1450-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(' -> /webdav/test/dest.txt'))
1450+
expect(logSpy.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes(' -> /webdav/test/dest.txt'))).toBe(true)
14511451
})
14521452
})
14531453
})
@@ -2037,8 +2037,8 @@ describe('WebDAVMethods', () => {
20372037
// Expected to throw
20382038
}
20392039

2040-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PUT'))
2041-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('/webdav/test.txt'))
2040+
expect(logSpy.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes('PUT'))).toBe(true)
2041+
expect(logSpy.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes('/webdav/test.txt'))).toBe(true)
20422042
})
20432043

20442044
it('should include destination URL in log when provided', () => {
@@ -2053,7 +2053,7 @@ describe('WebDAVMethods', () => {
20532053
// Expected to throw
20542054
}
20552055

2056-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(' -> /webdav/destination.txt'))
2056+
expect(logSpy.mock.calls.some(([payload]: { msg: string }[]) => payload?.msg?.includes(' -> /webdav/destination.txt'))).toBe(true)
20572057
})
20582058
})
20592059
})

backend/src/authentication/guards/auth-basic.guard.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe(AuthBasicGuard.name, () => {
126126
.mockImplementation(() => undefined)
127127
await expect(authBasicGuard.canActivate(context)).rejects.toThrow()
128128
expect(loggerSpy).toHaveBeenCalled()
129-
expect(loggerSpy.mock.calls[0][0]).toEqual(expect.stringContaining('cache failed'))
129+
expect(loggerSpy.mock.calls[0][0]).toEqual(expect.objectContaining({ tag: 'validate', msg: expect.stringContaining('cache failed') }))
130130
})
131131

132132
it('should not validate the user authentication', async () => {

backend/src/configuration/config.logger.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,23 @@ export const configLogger = (loggerConfig: LoggerConfig) =>
3939
return undefined
4040
}
4141
},
42-
transport: {
43-
target: 'pino-pretty',
44-
options: {
45-
ignore: 'hostname,context,reqId,req,res,user,userAgent,responseTime,tag',
46-
hideObject: false,
47-
singleLine: false,
48-
colorize: loggerConfig.colorize,
49-
colorizeObjects: false,
50-
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
51-
messageFormat: `[{context}]{if tag} [{tag}] {end}{if user} <{user}> {end} ${
52-
loggerConfig.colorize ? '\x1b[37m' : ''
53-
}{msg}{if res} ({res.contentLength} bytes in {responseTime}ms) {userAgent}{end}{if reqId} | {reqId}{end}`,
54-
destination: loggerConfig.stdout ? 1 : loggerConfig.filePath,
55-
mkdir: true,
56-
sync: false
57-
}
58-
}
42+
transport: loggerConfig.jsonOutput
43+
? null
44+
: {
45+
target: 'pino-pretty',
46+
options: {
47+
ignore: 'hostname,context,reqId,req,res,user,userAgent,responseTime,tag',
48+
hideObject: false,
49+
singleLine: false,
50+
colorize: loggerConfig.colorize,
51+
colorizeObjects: false,
52+
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
53+
messageFormat: `[{context}]{if tag} [{tag}] {end}{if user} <{user}> {end} ${
54+
loggerConfig.colorize ? '\x1b[37m' : ''
55+
}{msg}{if res} ({res.contentLength} bytes in {responseTime}ms) {userAgent}{end}{if reqId} | {reqId}{end}`,
56+
destination: loggerConfig.stdout ? 1 : loggerConfig.filePath,
57+
mkdir: true,
58+
sync: false
59+
}
60+
}
5961
}) satisfies Options

backend/src/configuration/config.validation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export class LoggerConfig {
5959
@IsString()
6060
@Transform(({ value }) => value || DEFAULT_LOG_FILE_PATH)
6161
filePath: string = DEFAULT_LOG_FILE_PATH
62+
63+
@IsOptional()
64+
@IsBoolean()
65+
jsonOutput: boolean = false
6266
}
6367

6468
export class GlobalConfig {

environment/environment.dist.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ logger:
2222
# Colorize output.
2323
# default: `true`
2424
colorize: true
25+
# JSON output. When enabled, `colorize` is ignored.
26+
# default: `false`
27+
jsonOutput: false
2528
# Path to the log file used when stdout is set to false
2629
filePath:
2730
mysql:

0 commit comments

Comments
 (0)