Skip to content

Commit

Permalink
Add logging support to Playground (#1035)
Browse files Browse the repository at this point in the history
## What is this PR doing?

This PR adds support for collecting PHP logs and WordPress debug logs in
Playground.
After each request logs are printed with `console.debug` in the browser.

Support for Node, WP-now, and VSCode will be added in separate PRs.
We also plan to add more Playground logs in the future to help us
improve the stability of WordPress Playground.

## What problem is it solving?

When Playground crashes it's hard to know why it happened. By adding
access to PHP logs and users will be able to debug their sites.
Playground developers will benefit from having Playground and PHP logs
in a single place and a shared format.

## How is the problem addressed?

On `request.end` we collect logs from the current PHP request and send
them back with the event.
These logs are caught by the Logger and printed.
Additionally, the logger catches some of the Playground errors using
regular `window` events.

## Testing Instructions

- Checkout this branch
- Start Playground `npm run dev`
- [Open Playground in your
browser](http://localhost:5400/website-server/?url=/wp-admin/post-new.php&networking=yes)
- Confirm that the debug log output is recorded in the browser console
after each request
- Add `throw new Exception('This is a test exception');` after this file
of
https://github.com/WordPress/wordpress-playground/blob/c23dd927a92501fa94fd38d6eebc0d0ff39699eb/packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/playground_logger.php#L9
- Refresh Playground and confirm that the PHP log is printed. This
confirms that we can collect logs even if Playground fails to run
- Replace the Exception from /playground_logger.php with this code
```
add_action('init', function () {
    if (isset($_GET['post'])) {
        throw new Exception('This is a test exception');
    }
});
```
- Refresh Playground
- Confirm that it loads
- Go to a post edit page using wp-admin
- Confirm that the post edit page crashes and the debug log is printed
  • Loading branch information
bgrgicak authored Feb 29, 2024
1 parent 6d86b8b commit 050a2dc
Show file tree
Hide file tree
Showing 18 changed files with 437 additions and 20 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/php-wasm/logger/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
Empty file.
28 changes: 28 additions & 0 deletions packages/php-wasm/logger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@php-wasm/logger",
"version": "0.0.1",
"description": "A logger for PHP-wasm clients like Playground and WP-now.",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/wordpress-playground"
},
"homepage": "https://developer.wordpress.org/playground",
"author": "The WordPress contributors",
"typedoc": {
"entryPoint": "./src/index.ts",
"readmeFile": "./README.md",
"displayName": "@php-wasm/logger",
"tsconfig": "./tsconfig.lib.json"
},
"publishConfig": {
"access": "public",
"directory": "../../../dist/packages/php-wasm/logger"
},
"license": "GPL-2.0-or-later",
"main": "index.cjs",
"types": "index.d.ts",
"engines": {
"node": ">=18.18.2",
"npm": ">=8.11.0"
}
}
50 changes: 50 additions & 0 deletions packages/php-wasm/logger/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "php-wasm-logger",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/php-wasm/logger/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/php-wasm/logger"
}
},
"test": {
"executor": "nx:noop",
"dependsOn": ["test:vite"]
},
"test:esmcjs": {
"executor": "@wp-playground/nx-extensions:assert-built-esm-and-cjs",
"options": {
"outputPath": "dist/packages/php-wasm/logger"
},
"dependsOn": ["build"]
},
"test:vite": {
"executor": "@nx/vite:test",
"outputs": ["{workspaceRoot}/coverage/packages/php-wasm/logger"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../../coverage/packages/php-wasm/logger"
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/php-wasm/logger/**/*.ts"]
}
}
},
"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"
]
}
}
}
1 change: 1 addition & 0 deletions packages/php-wasm/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './logger';
188 changes: 188 additions & 0 deletions packages/php-wasm/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { UniversalPHP } from '@php-wasm/universal/src/lib/universal-php';
/**
* Log severity levels.
*/
export type LogSeverity = 'debug' | 'info' | 'warn' | 'error' | 'fatal';

/**
* A logger for Playground.
*/
export class Logger {
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))) {
return '';
}
return await playground.readFileAsText(this.errorLogPath);
}

/**
* 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.
*/
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();
}

/**
* Collect PHP logs from the error_log file and log them.
* @param UniversalPHP playground instance
* @param loggerInstance The logger instance
*/
export function collectPhpLogs(
loggerInstance: Logger,
playground: UniversalPHP
) {
loggerInstance.addPlaygroundRequestEndListener(playground);
}
23 changes: 23 additions & 0 deletions packages/php-wasm/logger/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"allowSyntheticDefaultImports": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"types": ["vitest"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
10 changes: 10 additions & 0 deletions packages/php-wasm/logger/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
19 changes: 19 additions & 0 deletions packages/php-wasm/logger/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
},
"include": [
"vite.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
Loading

0 comments on commit 050a2dc

Please sign in to comment.