-
Notifications
You must be signed in to change notification settings - Fork 271
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add logging support to Playground (#1035)
## 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
Showing
18 changed files
with
437 additions
and
20 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './logger'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
Oops, something went wrong.