Skip to content

[php-wasm-logger] Filter logs by severity in Logger and assign severity based on verbosity argument in CLIs #2436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The `server` command supports the following optional arguments:
- `--login`: Automatically log the user in as an administrator.
- `--skip-wordpress-setup`: Do not download or install WordPress. Useful if you are mounting a full WordPress directory.
- `--skip-sqlite-setup`: Do not set up the SQLite database integration.
- `--quiet`: Do not output logs and progress messages.
- `--verbosity`: Output logs and progress messages. Defaults to 'normal'.
- `--debug`: Print the PHP error log if an error occurs during boot.

## Need some help with the CLI?
Expand Down
18 changes: 9 additions & 9 deletions packages/php-wasm/logger/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@
"lintFilePatterns": ["packages/php-wasm/logger/**/*.ts"],
"maxWarnings": 0
}
}
},
"typecheck": {
"executor": "nx:run-commands",
"options": {
"commands": [
"tsc -p packages/php-wasm/logger/tsconfig.lib.json --noEmit",
"tsc -p packages/php-wasm/logger/tsconfig.spec.json --noEmit"
]
},
"typecheck": {
"executor": "nx:run-commands",
"options": {
"commands": [
"tsc -p packages/php-wasm/logger/tsconfig.lib.json --noEmit",
"tsc -p packages/php-wasm/logger/tsconfig.spec.json --noEmit"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UniversalPHP, PHPRequestErrorEvent } from '../types';
import type { Logger } from '../logger';
import { type Logger, LogPrefix, LogSeverity } from '../logger';

let lastPHPLogLength = 0;
export const errorLogPath = '/wordpress/wp-content/debug.log';
Expand Down Expand Up @@ -42,8 +42,9 @@ export const collectPhpLogs = (
if (event.error) {
loggerInstance.logMessage({
message: `${event.error.message} ${event.error.stack}`,
severity: 'Fatal',
prefix: event.source === 'request' ? 'PHP' : 'WASM Crash',
severity: LogSeverity.Fatal,
prefix:
event.source === 'request' ? LogPrefix.PHP : LogPrefix.WASM,
});
loggerInstance.dispatchEvent(
new CustomEvent(loggerInstance.fatalErrorEvent, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Logger } from '../logger';
import { type Logger, LogSeverity } from '../logger';

/**
* Log Windows errors.
Expand All @@ -9,7 +9,7 @@ import type { Logger } from '../logger';
const logWindowErrorEvent = (loggerInstance: Logger, event: ErrorEvent) => {
loggerInstance.logMessage({
message: `${event.message} in ${event.filename} on line ${event.lineno}:${event.colno}`,
severity: 'Error',
severity: LogSeverity.Error,
});
};

Expand All @@ -30,7 +30,7 @@ const logPromiseRejection = (
const message = event?.reason.stack ?? event.reason;
loggerInstance.logMessage({
message,
severity: 'Error',
severity: LogSeverity.Error,
});
};

Expand Down
3 changes: 1 addition & 2 deletions packages/php-wasm/logger/src/lib/handlers/log-event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { LogHandler } from '../log-handlers';
import type { Log } from '../logger';
import { logger } from '../logger';
import { type Log, logger } from '../logger';

export const logEventType = 'playground-log';

Expand Down
13 changes: 6 additions & 7 deletions packages/php-wasm/logger/src/lib/handlers/log-to-console.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { LogHandler } from '../log-handlers';
import type { Log } from '../logger';
import { prepareLogMessage } from '../logger';
import { type Log, LogSeverity, prepareLogMessage } from '../logger';

/**
* Log message to the console.
Expand All @@ -23,19 +22,19 @@ export const logToConsole: LogHandler = (log: Log, ...args: any[]): void => {
}
/* eslint-disable no-console */
switch (log.severity) {
case 'Debug':
case LogSeverity.Debug:
console.debug(log.message, ...args);
break;
case 'Info':
case LogSeverity.Info:
console.info(log.message, ...args);
break;
case 'Warn':
case LogSeverity.Warn:
console.warn(log.message, ...args);
break;
case 'Error':
case LogSeverity.Error:
console.error(log.message, ...args);
break;
case 'Fatal':
case LogSeverity.Fatal:
console.error(log.message, ...args);
break;
default:
Expand Down
7 changes: 3 additions & 4 deletions packages/php-wasm/logger/src/lib/handlers/log-to-memory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { LogHandler } from '../log-handlers';
import type { Log } from '../logger';
import { formatLogEntry } from '../logger';
import { formatLogEntry, type Log, LogPrefix, LogSeverity } from '../logger';

const prepareLogMessage = (logMessage: object): string => {
if (logMessage instanceof Error) {
Expand All @@ -26,8 +25,8 @@ export const logToMemory: LogHandler = (log: Log): void => {
typeof log.message === 'object'
? prepareLogMessage(log.message)
: log.message,
log.severity ?? 'Info',
log.prefix ?? 'JavaScript'
log.severity ?? LogSeverity.Info,
log.prefix ?? LogPrefix.JS
);
addToLogArray(message);
}
Expand Down
84 changes: 68 additions & 16 deletions packages/php-wasm/logger/src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,48 @@ export type Log = {
raw?: boolean;
};

/**
* Log verbosity levels
*/
export const LogVerbosity = {
Normal: 'normal',
Quiet: 'quiet',
Debug: 'debug',
} as const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not distinguish between verbosity and severity at the logger level. One concept is enough. We can decide what each verbosity means in the CLI args parser when we let the user decide. All internal methods could use severities for filtering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.


export type LogVerbosity = (typeof LogVerbosity)[keyof typeof LogVerbosity];

/**
* Log severity levels.
*/
export type LogSeverity = 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal';
export const LogSeverity = {
Debug: 'debug',
Info: 'info',
Warn: 'warn',
Error: 'error',
Fatal: 'fatal',
} as const;

export type LogSeverity = (typeof LogSeverity)[keyof typeof LogSeverity];

/**
* Log prefix.
*/
export type LogPrefix = 'WASM Crash' | 'PHP' | 'JavaScript';
export const LogPrefix = {
WASM: 'Wasm Crash',
PHP: 'PHP',
JS: 'JavaScript',
} as const;

export type LogPrefix = (typeof LogPrefix)[keyof typeof LogPrefix];

/**
* A logger for Playground.
*/
export class Logger extends EventTarget {
public readonly fatalErrorEvent = 'playground-fatal-error';
private readonly handlers: LogHandler[];
private handlers: LogHandler[];
private filters: LogSeverity[];

// constructor
constructor(
Expand All @@ -41,6 +67,7 @@ export class Logger extends EventTarget {
handlers: LogHandler[] = []
) {
super();
this.filters = Object.values(LogSeverity);
this.handlers = handlers;
}

Expand All @@ -62,14 +89,39 @@ export class Logger extends EventTarget {
/**
* Log message with severity.
*
* @param message any
* @param severity LogSeverity
* @param raw boolean
* @param log Log
* @param args any
*/
public logMessage(log: Log, ...args: any[]): void {
for (const handler of this.handlers) {
handler(log, ...args);
const isConsole = handler.name === 'logToConsole';
const isSeverityAllowed = log.severity
? this.filters.includes(log.severity)
: !!this.filters.length;

if (!isConsole || isSeverityAllowed) {
handler(log, ...args);
}
Copy link
Collaborator

@adamziel adamziel Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused – what's the reasoning here? Do we need special casing?

Copy link
Collaborator Author

@mho22 mho22 Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're asking for the isConsole line : in my last comment I wanted to know if your suggestion with --verbosity only concerned logToConsole. If not I can remove that line.

If you're asking for the isSeverityAllowed line, the severity of a log can be a LogSeverity type but also undefined. And if it is undefined, it takes the Info severity in log-to-memory. That is why I had to use a ternary operator.

I would suggest to replace the undefined severity by a Log LogServerity and in log-to-memory.ts replace:

- log.severity ?? LogSeverity.Info,
+ log.severity == LogSeverity.Log ? LogSeverity.Info : log.severity

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in my last comment I wanted to know if your suggestion with --verbosity only concerned logToConsole.

Ah, good question! Let's target all handlers. If we want to restrict the console output while still collecting all the logs somewhere, we can do that at the logToConsole handler level.

Copy link
Collaborator Author

@mho22 mho22 Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

/**
* Filter message based on verbosiy
* @param verbosity LogVerbosity
*/
public filterByVerbosity(verbosity: LogVerbosity): void {
if (verbosity === LogVerbosity.Quiet) {
this.filters = [];
}

if (verbosity === LogVerbosity.Normal) {
this.filters = Object.values(LogSeverity).filter(
(severity) => severity !== LogSeverity.Debug
);
}

if (verbosity === LogVerbosity.Debug) {
this.filters = Object.values(LogSeverity);
}
}

Expand All @@ -84,7 +136,7 @@ export class Logger extends EventTarget {
{
message,
severity: undefined,
prefix: 'JavaScript',
prefix: LogPrefix.JS,
raw: false,
},
...args
Expand All @@ -101,8 +153,8 @@ export class Logger extends EventTarget {
this.logMessage(
{
message,
severity: 'Debug',
prefix: 'JavaScript',
severity: LogSeverity.Debug,
prefix: LogPrefix.JS,
raw: false,
},
...args
Expand All @@ -119,8 +171,8 @@ export class Logger extends EventTarget {
this.logMessage(
{
message,
severity: 'Info',
prefix: 'JavaScript',
severity: LogSeverity.Info,
prefix: LogPrefix.JS,
raw: false,
},
...args
Expand All @@ -137,8 +189,8 @@ export class Logger extends EventTarget {
this.logMessage(
{
message,
severity: 'Warn',
prefix: 'JavaScript',
severity: LogSeverity.Warn,
prefix: LogPrefix.JS,
raw: false,
},
...args
Expand All @@ -155,8 +207,8 @@ export class Logger extends EventTarget {
this.logMessage(
{
message,
severity: 'Error',
prefix: 'JavaScript',
severity: LogSeverity.Error,
prefix: LogPrefix.JS,
raw: false,
},
...args
Expand Down
65 changes: 59 additions & 6 deletions packages/php-wasm/logger/src/test/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,77 @@
import { logger } from '../lib/logger';
import { clearMemoryLogs } from '../lib/log-handlers';
import { type Log, logger, LogVerbosity } from '../lib/logger';
import { clearMemoryLogs, type LogHandler } from '../lib/log-handlers';

describe('Logger', () => {
beforeEach(async () => {
let output: string[];
let handlers: LogHandler[];

function logToConsole(log: Log, arg?: string) {
output.push(`${log.message}${arg ? arg : ''}`);
}

beforeAll(() => {
// @ts-ignore
handlers = logger.handlers;
});

beforeEach(() => {
output = [];
// @ts-ignore
logger.handlers = [...handlers, logToConsole];

clearMemoryLogs();
});

it('Log message should be added', () => {
it('adds message in logs', () => {
logger.warn('test');
const logs = logger.getLogs();
expect(logs.length).toBe(1);
expect(logs[0]).toMatch(
/\[\d{2}-[A-Za-z]{3,4}-\d{4} \d{2}:\d{2}:\d{2} UTC\] JavaScript Warn: test/
/\[\d{2}-[A-Za-z]{3,4}-\d{4} \d{2}:\d{2}:\d{2} UTC\] JavaScript warn: test/
);
});

it('Log event should be dispatched', () => {
it('dispatches log event', () => {
const eventListener = vitest.fn();
logger.addEventListener('playground-log', eventListener);
logger.warn('test');
expect(eventListener).toHaveBeenCalled();
});

it('outputs all logs by default', () => {
logger.log('log');
logger.info('info');
logger.warn('warn');
logger.error('error');
logger.debug('debug');
const logs = logger.getLogs();
expect(logs.length).toBe(5);
expect(output).toEqual(['log', 'info', 'warn', 'error', 'debug']);
});

it('outputs main logs when verbosity is set to normal', () => {
logger.filterByVerbosity(LogVerbosity.Normal);
logger.log('log');
logger.debug('debug');
const logs = logger.getLogs();
expect(logs.length).toBe(2);
expect(output).toEqual(['log']);
});

it('outputs main and debug logs when verbosity is set to debug', () => {
logger.filterByVerbosity(LogVerbosity.Debug);
logger.log('log');
logger.debug('debug');
const logs = logger.getLogs();
expect(logs.length).toBe(2);
expect(output).toEqual(['log', 'debug']);
});

it('does not output logs when verbosity is set to quiet', () => {
logger.filterByVerbosity(LogVerbosity.Quiet);
logger.log('log');
const logs = logger.getLogs();
expect(logs.length).toBe(1);
expect(output).toEqual([]);
});
});
Loading
Loading