Skip to content
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

Add logging support to Playground #1035

Merged
merged 37 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6b21516
Basic error logging
bgrgicak Feb 13, 2024
d804cec
Move logger to a new file
bgrgicak Feb 14, 2024
8480857
Update log message param
bgrgicak Feb 14, 2024
7f2c27a
Cleanup
bgrgicak Feb 14, 2024
46542e5
Return n lines
bgrgicak Feb 14, 2024
315a622
Move logger to website
bgrgicak Feb 16, 2024
c23dd92
Display debug.log for request
bgrgicak Feb 16, 2024
60aa62f
Rewrite compiler error to return all error data
bgrgicak Feb 16, 2024
0923c3e
Remove test log
bgrgicak Feb 16, 2024
6d6bdaf
Clean up
bgrgicak Feb 16, 2024
d13033d
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 16, 2024
194bcbd
Merge branch 'add/wp-logs' of https://github.com/WordPress/wordpress-…
bgrgicak Feb 16, 2024
b84bdc0
Collect PHP logs on compile error
bgrgicak Feb 19, 2024
46ca5ee
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 19, 2024
42e47d8
Move logger to blueprints
bgrgicak Feb 19, 2024
3ea93c5
Remove unused dependency
bgrgicak Feb 19, 2024
b1b1c71
Fix linter error
bgrgicak Feb 19, 2024
c3e2c77
Move logger length to Logger
bgrgicak Feb 19, 2024
9eed92f
Cleanup unused changes
bgrgicak Feb 19, 2024
cabbd9b
Address feedback
bgrgicak Feb 21, 2024
5ab98c7
Reset base-php
bgrgicak Feb 21, 2024
6ec3c4a
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 21, 2024
c9f0b5c
Move logger to package
bgrgicak Feb 22, 2024
f28c20a
Start logger in client and node
bgrgicak Feb 22, 2024
65dd662
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 22, 2024
be90919
Remove node logger
bgrgicak Feb 23, 2024
d0398a3
Set WP_DEBUG only if WP exists
bgrgicak Feb 23, 2024
0650878
Update packages/playground/logger/logger.ts
bgrgicak Feb 23, 2024
a192ceb
Update packages/playground/remote/src/lib/playground-mu-plugin/playgr…
bgrgicak Feb 23, 2024
06158e7
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 23, 2024
b14f4a5
Move logger to a buildable package
bgrgicak Feb 26, 2024
d0546aa
Fix linter
bgrgicak Feb 26, 2024
451ede1
Merge branch 'trunk' into add/wp-logs
bgrgicak Feb 26, 2024
1d840ed
Move logger code to main plugin file
bgrgicak Feb 27, 2024
a3feb32
Attach JS logger in Website only
bgrgicak Feb 28, 2024
ef4993b
Revert compile error
bgrgicak Feb 28, 2024
988a9a5
Move window log collection to main
bgrgicak Feb 29, 2024
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
300 changes: 166 additions & 134 deletions packages/php-wasm/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UniversalPHP } from "@php-wasm/universal/src/lib/universal-php";
import { UniversalPHP } from '@php-wasm/universal/src/lib/universal-php';
/**
* Log severity levels.
*/
Expand All @@ -8,149 +8,181 @@ export type LogSeverity = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
* A logger for Playground.
*/
export class Logger {
private readonly LOG_PREFIX = 'Playground';

/**
* The length of the last PHP log.
*/
private lastPHPLogLength = 0;

/**
* The path to the error log file.
*/
private errorLogPath = '/wordpress/wp-content/debug.log';

constructor(errorLogPath?: string) {
if (errorLogPath) {
this.errorLogPath = errorLogPath;
}
this.collectJavaScriptErrors();
}

/**
* Collect errors from JavaScript window events like error and log them.
*/
private collectJavaScriptErrors() {
if (typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
this.log(
`${event.message} in ${event.filename} on line ${event.lineno}:${event.colno}`,
'fatal'
);
});
window.addEventListener('unhandledrejection', (event) => {
this.log(
`${event.reason.stack}`,
'fatal'
);
});
window.addEventListener('rejectionhandled', (event) => {
this.log(
`${event.reason.stack}`,
'fatal'
);
});
}
}

/**
* Read the WordPress debug.log file and return its content.
*
* @param UniversalPHP playground instance
* @returns string The content of the debug.log file
*/
private readonly LOG_PREFIX = 'Playground';

/**
* Whether the window events are connected.
*/
private windowConnected = false;

/**
* The length of the last PHP log.
*/
private lastPHPLogLength = 0;

/**
* The path to the error log file.
*/
private errorLogPath = '/wordpress/wp-content/debug.log';

constructor(errorLogPath?: string) {
if (errorLogPath) {
this.errorLogPath = errorLogPath;
}
}

/**
* Read the WordPress debug.log file and return its content.
*
* @param UniversalPHP playground instance
* @returns string The content of the debug.log file
*/
private async getRequestPhpErrorLog(playground: UniversalPHP) {
if (!await playground.fileExists(this.errorLogPath)) {
if (!(await playground.fileExists(this.errorLogPath))) {
return '';
}
return await playground.readFileAsText(this.errorLogPath);
}
/**
* Register a listener for the request.end event and log the data.
* @param UniversalPHP playground instance
*/
public addPlaygroundRequestEndListener(playground: UniversalPHP) {
playground.addEventListener('request.end', async () => {
const log = await this.getRequestPhpErrorLog(playground);
if (log.length > this.lastPHPLogLength) {
this.logRaw(log.substring(this.lastPHPLogLength));
this.lastPHPLogLength = log.length;
}
} );
}


/**
* Get UTC date in the PHP log format https://github.com/php/php-src/blob/master/main/main.c#L849
*
* @param date
* @returns string
*/
private formatLogDate(date: Date): string {
const formattedDate = new Intl.DateTimeFormat('en-GB', {
year: 'numeric',
month: 'short',
day: '2-digit',
timeZone: 'UTC',
}).format(date).replace(/ /g, '-');

const formattedTime = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short'
}).format(date);
return formattedDate + ' ' + formattedTime;
}

/**
* Format log message and severity and log it.
* @param string message
* @param LogSeverity severity
*/
public formatMessage(message: string, severity: LogSeverity): string {
const now = this.formatLogDate(new Date());
return `[${now}] ${this.LOG_PREFIX} ${severity}: ${message}`;
}

/**
* Log message with severity and timestamp.
* @param string message
* @param LogSeverity severity
*/
public log(message: string, severity?: LogSeverity): void {
if (severity === undefined) {
severity = 'info';
}
const log = this.formatMessage(message, severity);
this.logRaw(log);
}

/**
* Log message without severity and timestamp.
* @param string log
*/
public logRaw(log: string): void {
console.debug(log);
}
};

/**
* Log Windows errors.
*
* @param ErrorEvent event
*/
private logWindowError(event: ErrorEvent) {
this.log(
`${event.message} in ${event.filename} on line ${event.lineno}:${event.colno}`,
'fatal'
);
}

/**
* Log unhandled promise rejections.
*
* @param PromiseRejectionEvent event
*/
private logUnhandledRejection(event: PromiseRejectionEvent) {
this.log(`${event.reason.stack}`, 'fatal');
}

/**
* Register a listener for the window error events and log the data.
*/
public addWindowErrorListener() {
// Ensure that the window events are connected only once.
if (this.windowConnected) {
return;
}
if (typeof window === 'undefined') {
return;
}

window.addEventListener('error', this.logWindowError.bind(this));
window.addEventListener(
'unhandledrejection',
this.logUnhandledRejection.bind(this)
);
window.addEventListener(
'rejectionhandled',
this.logUnhandledRejection.bind(this)
);
this.windowConnected = true;
}

/**
* Register a listener for the request.end event and log the data.
* @param UniversalPHP playground instance
*/
public addPlaygroundRequestEndListener(playground: UniversalPHP) {
playground.addEventListener('request.end', async () => {
const log = await this.getRequestPhpErrorLog(playground);
if (log.length > this.lastPHPLogLength) {
this.logRaw(log.substring(this.lastPHPLogLength));
this.lastPHPLogLength = log.length;
}
});
}

/**
* Get UTC date in the PHP log format https://github.com/php/php-src/blob/master/main/main.c#L849
*
* @param date
* @returns string
*/
private formatLogDate(date: Date): string {
const formattedDate = new Intl.DateTimeFormat('en-GB', {
year: 'numeric',
month: 'short',
day: '2-digit',
timeZone: 'UTC',
})
.format(date)
.replace(/ /g, '-');

const formattedTime = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short',
}).format(date);
return formattedDate + ' ' + formattedTime;
}

/**
* Format log message and severity and log it.
* @param string message
* @param LogSeverity severity
*/
public formatMessage(message: string, severity: LogSeverity): string {
const now = this.formatLogDate(new Date());
return `[${now}] ${this.LOG_PREFIX} ${severity}: ${message}`;
}

/**
* Log message with severity and timestamp.
* @param string message
* @param LogSeverity severity
*/
public log(message: string, severity?: LogSeverity): void {
if (severity === undefined) {
severity = 'info';
}
const log = this.formatMessage(message, severity);
this.logRaw(log);
}

/**
* Log message without severity and timestamp.
* @param string log
*/
public logRaw(log: string): void {
console.debug(log);
}
}

/**
* The logger instance.
*/
let logger: Logger | undefined = undefined;
export const logger: Logger = new Logger();

/**
* Collect errors from JavaScript window events like error and log them.
* @param loggerInstance The logger instance
*/
export function collectWindowErrors(loggerInstance: Logger) {
loggerInstance.addWindowErrorListener();
Copy link
Collaborator

@adamziel adamziel Feb 28, 2024

Choose a reason for hiding this comment

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

Perhaps eventually this call could be inlined, as in:

export function collectWindowErrors(loggerInstance: Logger) {
		window.addEventListener('error', e => logWindowError(loggerInstance, e));
		window.addEventListener(
			'unhandledrejection',
			e => logUnhandledRejection(loggerInstance, e.reason)
		);
		window.addEventListener(
			'rejectionhandled',
			e => logUnhandledRejection(loggerInstance, e.reason)
		);
}

But for now, let's leave it the way it is and see how adding support for other runtimes shapes the development of the API.

}

/**
* Get the logger instance.
*
* @param errorLogPath The path to the error log file.
* @returns Logger
* Collect PHP logs from the error_log file and log them.
* @param UniversalPHP playground instance
* @param loggerInstance The logger instance
*/
export function getLogger(errorLogPath?: string): Logger {
if (!logger) {
logger = new Logger(errorLogPath);
}
return logger;
export function collectPhpLogs(
loggerInstance: Logger,
playground: UniversalPHP
) {
loggerInstance.addPlaygroundRequestEndListener(playground);
}
4 changes: 2 additions & 2 deletions packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import { consumeAPI } from '@php-wasm/web';
import { ProgressTracker } from '@php-wasm/progress';
import { PlaygroundClient } from '@wp-playground/remote';
import { getLogger } from '@php-wasm/logger';
import { collectPhpLogs, logger } from '@php-wasm/logger';
export interface StartPlaygroundOptions {
iframe: HTMLIFrameElement;
remoteUrl: string;
Expand Down Expand Up @@ -95,7 +95,7 @@ export async function startPlaygroundWeb({
}),
progressTracker
);
getLogger().addPlaygroundRequestEndListener(playground);
collectPhpLogs(logger, playground);
await runBlueprintSteps(compiled, playground);
progressTracker.finish();

Expand Down
4 changes: 2 additions & 2 deletions packages/playground/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@wp-playground/blueprints';
import { NodePHP } from '@php-wasm/node';
import { UniversalPHP } from '@php-wasm/universal';
import { getLogger } from '@php-wasm/logger';
import { collectPhpLogs, logger } from '@php-wasm/logger';

export interface NodePlaygroundOptions {
blueprint?: Blueprint;
Expand Down Expand Up @@ -44,7 +44,7 @@ export async function startPlaygroundNode(
},
});

getLogger().addPlaygroundRequestEndListener(playground);
collectPhpLogs(logger, playground);

await defineSiteUrl(playground, {
siteUrl: options.serverUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { usePlayground } from '../../lib/hooks';
import { StorageType } from '../../types';
import PlaygroundContext from './context';
import { logTrackingEvent } from '../../lib/tracking';
import { collectWindowErrors, logger } from '@php-wasm/logger';

export const supportedDisplayModes = [
'browser',
Expand Down Expand Up @@ -49,6 +50,10 @@ export default function PlaygroundViewport({
}
}, [blueprint?.steps]);

useEffect(() => {
collectWindowErrors(logger);
bgrgicak marked this conversation as resolved.
Show resolved Hide resolved
}, []);

return (
<PlaygroundContext.Provider
value={{
Expand Down