Skip to content

Commit e79fc17

Browse files
committed
feat: allow runtime debugging specific app(s)
Allows setting a window global variable (`__NC_LOGGER_DEBUG__`) to an array of app ids for which the logger will enforce the debug logging level. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 64f252b commit e79fc17

File tree

8 files changed

+164
-72
lines changed

8 files changed

+164
-72
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,30 @@ Right now the package uses `window.console` as log appender and produces the fol
3333
[WARN] mail: it's just a warning, carry on { app: 'mail', uid: 'christoph' }
3434
```
3535

36+
### Log level
3637
The logger tries to detect the server configured logging level by default,
3738
which can be configured using the `loglevel_frontend` option in the `config.php`.
38-
In case no logging level was configured or detection failed, the logger will fallback to the *warning* level.
3939

40-
If the server is set to the debug mode the configured logging level will be set to the *debug* level.
40+
In case no logging level was configured or detection failed, the logger will fallback to the *warning* level.
41+
If the server is set to the debug mode the fallback will be the *debug* instead of the *warning* level.
4142

4243
Any message with a lower level than the configured will not be printed on the console.
4344

44-
You can override the logging level in both cases by setting it manually using the `setLogLevel` function
45-
when building the logger.
45+
#### Override the log level
46+
You can override the logging level in both cases by setting it manually
47+
using the `setLogLevel` function when building the logger.
48+
49+
It is also possible to debug an app without the need of manually recompile it to change the `setLogLevel`.
50+
To do so the runtime debugging configuration can be changed by running this in the browser console:
51+
52+
```js
53+
// debug a single app
54+
window.__NC_LOGGER_DEBUG__=['YOUR_APP_ID']
55+
// debug multiple apps
56+
window.__NC_LOGGER_DEBUG__=['files', 'viewer']
57+
```
58+
59+
This will enforce the *debug* logging level for the specified apps.
4660

4761
## Contributing
4862

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
66
SPDX-PackageDownloadLocation = "https://github.com/nextcloud-libraries/nextcloud-logger"
77

88
[[annotations]]
9-
path = ["package-lock.json", "package.json", "tsconfig.json"]
9+
path = ["package-lock.json", "package.json", "tsconfig.json", "tests/tsconfig.json"]
1010
precedence = "aggregate"
1111
SPDX-FileCopyrightText = "2019-2024 Nextcloud GmbH and Nextcloud contributors"
1212
SPDX-License-Identifier = "GPL-3.0-or-later"

lib/ALogger.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
import type { IContext, ILogger } from './contracts.ts'
7+
8+
import { LogLevel } from './contracts.ts'
9+
10+
/**
11+
* Abstract base logger implementing common functionality
12+
*/
13+
export abstract class ALogger implements ILogger {
14+
/**
15+
* The initial logging context set by the constructor (LoggerBuilder factory)
16+
*/
17+
protected abstract context: IContext
18+
19+
/**
20+
* Log a message with the specified level and context
21+
*
22+
* @param level - The log level requested
23+
* @param message - The log message
24+
* @param context - The logging context
25+
*/
26+
protected abstract log(level: LogLevel, message: string, context: IContext): void
27+
28+
public debug(message: string | Error, context?: IContext): void {
29+
this.logIfNeeded(LogLevel.Debug, message, { ...context, ...this.context })
30+
}
31+
32+
public info(message: string | Error, context?: IContext): void {
33+
this.logIfNeeded(LogLevel.Info, message, { ...context, ...this.context })
34+
}
35+
36+
public warn(message: string | Error, context?: IContext): void {
37+
this.logIfNeeded(LogLevel.Warn, message, { ...context, ...this.context })
38+
}
39+
40+
public error(message: string | Error, context?: IContext): void {
41+
this.logIfNeeded(LogLevel.Error, message, { ...context, ...this.context })
42+
}
43+
44+
public fatal(message: string | Error, context?: IContext): void {
45+
this.logIfNeeded(LogLevel.Fatal, message, { ...context, ...this.context })
46+
}
47+
48+
/**
49+
* Check if the message should be logged and prepare the context
50+
*
51+
* @param level - The logging level requested
52+
* @param message - The log message or Error object
53+
* @param context - The logging context
54+
*/
55+
private logIfNeeded(level: LogLevel, message: string | Error, context: IContext): void {
56+
// Skip if level is configured and this is below the level
57+
if (typeof this.context?.level === 'number' && level < this.context?.level && !this.debuggingEnabled()) {
58+
return
59+
}
60+
61+
// Handle logging when only an error was passed as log message
62+
if (typeof message === 'object') {
63+
if (context.error) {
64+
context.error = [context.error, message]
65+
} else {
66+
context.error = message
67+
}
68+
if (level === LogLevel.Debug || this.debuggingEnabled()) {
69+
context.stacktrace = message.stack
70+
}
71+
return this.log(level, `${message.name}: ${message.message}`, context)
72+
}
73+
74+
this.log(level, message, context)
75+
}
76+
77+
/**
78+
* Check if debugging is enabled for the current app
79+
*/
80+
private debuggingEnabled(): boolean {
81+
const debugContexts = window.__NC_LOGGER_DEBUG__
82+
return typeof this.context.app === 'string'
83+
&& Array.isArray(debugContexts)
84+
&& debugContexts.includes(this.context.app)
85+
}
86+
}

lib/ConsoleLogger.ts

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,19 @@
55

66
import type { IContext, ILogger } from './contracts.ts'
77

8+
import { ALogger } from './ALogger.ts'
89
import { LogLevel } from './contracts.ts'
910

1011
/* eslint-disable no-console -- This class is a console logger so it needs to write to the console. */
11-
export class ConsoleLogger implements ILogger {
12-
private context: IContext
12+
export class ConsoleLogger extends ALogger implements ILogger {
13+
protected context: IContext
1314

1415
constructor(context?: IContext) {
16+
super()
1517
this.context = context || {}
1618
}
1719

18-
private formatMessage(message: string | Error, level: LogLevel, context?: IContext): string {
19-
let msg = '[' + LogLevel[level].toUpperCase() + '] '
20-
21-
if (context && context.app) {
22-
msg += context.app + ': '
23-
}
24-
25-
if (typeof message === 'string') {
26-
return msg + message
27-
}
28-
29-
// basic error formatting
30-
msg += `Unexpected ${message.name}`
31-
if (message.message) {
32-
msg += ` "${message.message}"`
33-
}
34-
// only add stack trace when debugging
35-
if (level === LogLevel.Debug && message.stack) {
36-
msg += `\n\nStack trace:\n${message.stack}`
37-
}
38-
39-
return msg
40-
}
41-
42-
log(level: LogLevel, message: string | Error, context: IContext) {
43-
// Skip if level is configured and this is below the level
44-
if (typeof this.context?.level === 'number' && level < this.context?.level) {
45-
return
46-
}
47-
48-
// Add error object to context
49-
if (typeof message === 'object' && context?.error === undefined) {
50-
context.error = message
51-
}
52-
20+
protected log(level: LogLevel, message: string | Error, context: IContext) {
5321
switch (level) {
5422
case LogLevel.Debug:
5523
console.debug(this.formatMessage(message, LogLevel.Debug, context), context)
@@ -70,24 +38,28 @@ export class ConsoleLogger implements ILogger {
7038
}
7139
}
7240

73-
debug(message: string | Error, context?: IContext): void {
74-
this.log(LogLevel.Debug, message, { ...this.context, ...context })
75-
}
41+
private formatMessage(message: string | Error, level: LogLevel, context?: IContext): string {
42+
let msg = '[' + LogLevel[level].toUpperCase() + '] '
7643

77-
info(message: string | Error, context?: IContext): void {
78-
this.log(LogLevel.Info, message, { ...this.context, ...context })
79-
}
44+
if (context && context.app) {
45+
msg += context.app + ': '
46+
}
8047

81-
warn(message: string | Error, context?: IContext): void {
82-
this.log(LogLevel.Warn, message, { ...this.context, ...context })
83-
}
48+
if (typeof message === 'string') {
49+
return msg + message
50+
}
8451

85-
error(message: string | Error, context?: IContext): void {
86-
this.log(LogLevel.Error, message, { ...this.context, ...context })
87-
}
52+
// basic error formatting
53+
msg += `Unexpected ${message.name}`
54+
if (message.message) {
55+
msg += ` "${message.message}"`
56+
}
57+
// only add stack trace when debugging
58+
if (level === LogLevel.Debug && message.stack) {
59+
msg += `\n\nStack trace:\n${message.stack}`
60+
}
8861

89-
fatal(message: string | Error, context?: IContext): void {
90-
this.log(LogLevel.Fatal, message, { ...this.context, ...context })
62+
return msg
9163
}
9264
}
9365

lib/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
5+
56
export enum LogLevel {
67
Debug = 0,
78
Info = 1,

lib/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { LogLevel } from './contracts.ts'
77

88
declare global {
99
interface Window {
10+
__NC_LOGGER_DEBUG__?: string[]
11+
1012
_oc_config: {
1113
loglevel: LogLevel
1214
}

tests/ConsoleLogger.spec.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
* SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: GPL-3.0-or-later
44
*/
5-
import { afterEach, describe, expect, it, test, vi } from 'vitest'
6-
import { ConsoleLogger, buildConsoleLogger } from '../lib/ConsoleLogger'
5+
6+
import { beforeEach, describe, expect, it, test, vi } from 'vitest'
7+
import { buildConsoleLogger, ConsoleLogger } from '../lib/ConsoleLogger.ts'
78

89
// Dummy Error
910
class MyError extends Error {
10-
1111
constructor(msg: string) {
1212
super(msg)
1313
this.name = 'MyError'
1414
}
15-
1615
}
1716

18-
afterEach(() => {
17+
beforeEach(() => {
18+
delete window.__NC_LOGGER_DEBUG__
1919
vi.resetAllMocks()
2020
})
2121

@@ -33,8 +33,6 @@ test('building the console logger', () => {
3333
})
3434

3535
describe('ConsoleLogger', () => {
36-
afterEach(() => { vi.resetAllMocks() })
37-
3836
it('logs debug messages', () => {
3937
const logger = new ConsoleLogger()
4038
const debug = vi.spyOn(window.console, 'debug').mockImplementation(() => {})
@@ -94,10 +92,10 @@ describe('ConsoleLogger', () => {
9492
const logger = new ConsoleLogger({ one: 1, two: 2 })
9593
const debug = vi.spyOn(window.console, 'debug').mockImplementation(() => {})
9694

97-
logger.debug('Should be logged', { two: 3 })
95+
logger.debug('Should be logged', { three: 3 })
9896
expect(debug).toHaveBeenCalledTimes(1)
9997
expect(debug.mock.calls[0][0]).toBe('[DEBUG] Should be logged')
100-
expect(debug.mock.calls[0][1]).toEqual({ one: 1, two: 3 })
98+
expect(debug.mock.calls[0][1]).toEqual({ one: 1, two: 2, three: 3 })
10199
})
102100

103101
it('allows extending empty global context', () => {
@@ -119,6 +117,20 @@ describe('ConsoleLogger', () => {
119117
expect(debug).toHaveBeenCalledTimes(0)
120118
})
121119

120+
it('respects the runtime debug configuration', () => {
121+
const logger = new ConsoleLogger({ app: 'test', level: 2 })
122+
123+
const debug = vi.spyOn(window.console, 'debug')
124+
debug.mockImplementationOnce(() => {})
125+
126+
logger.debug('Should not be logged')
127+
expect(debug).toHaveBeenCalledTimes(0)
128+
129+
window.__NC_LOGGER_DEBUG__ = ['files', 'test']
130+
logger.debug('Should be logged now')
131+
expect(debug).toHaveBeenCalledTimes(1)
132+
})
133+
122134
it('logs Error objects', () => {
123135
const error = new MyError('some message')
124136
const logger = new ConsoleLogger({})
@@ -128,10 +140,9 @@ describe('ConsoleLogger', () => {
128140

129141
logger.warn(error)
130142
expect(warn).toHaveBeenCalledTimes(1)
131-
expect(console[0][0]).toContain('MyError')
132-
expect(console[0][0]).toContain('some message')
133-
expect(console[0][0]).not.toContain('Stack trace')
143+
expect(console[0][0]).toMatch('MyError: some message')
134144
expect(console[0][1]).toHaveProperty('error', error)
145+
expect(console[0][1]).not.toHaveProperty('stacktrace', error.stack)
135146
})
136147

137148
it('logs Error objects and stack trace on debug', () => {
@@ -143,9 +154,9 @@ describe('ConsoleLogger', () => {
143154

144155
logger.debug(error)
145156
expect(debug).toHaveBeenCalledTimes(1)
146-
expect(console[0][0]).toContain('MyError')
147-
expect(console[0][0]).toContain('some message')
148-
expect(console[0][0]).toContain('Stack trace:')
157+
expect(console[0][0]).toContain('MyError: some message')
158+
expect(console[0][1]).toHaveProperty('error', error)
159+
expect(console[0][1]).toHaveProperty('stacktrace', error.stack)
149160
})
150161

151162
it('logs Error objects and does not override context', () => {
@@ -159,6 +170,8 @@ describe('ConsoleLogger', () => {
159170
expect(warn).toHaveBeenCalledTimes(1)
160171
expect(console[0][0]).toContain('MyError')
161172
expect(console[0][0]).toContain('some message')
162-
expect(console[0][1]).toHaveProperty('error', 'none')
173+
expect(console[0][1]).toHaveProperty('error')
174+
// @ts-expect-error - We know error is an array here
175+
expect(console[0][1]!.error).toEqual(['none', error])
163176
})
164177
})

tests/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"include": ["../lib", "."]
4+
}

0 commit comments

Comments
 (0)