Repository: vercel/next.js
Pull request: Switch development log item format as JSON

Change the dev next.js log file to a more structural format, each logging item is a JSON so that we can easily parse or let agents search through the file.

Before:
```
[00:00:12.345] Browser LOG     Test message
[00:00:13.456] Server  ERROR   Server error
```
After:
```
{"timestamp":"00:00:12.345","source":"Browser","level":"LOG","message":"Test message"}
{"timestamp":"00:00:13.456","source":"Server","level":"ERROR","message":"Server error"}
```


Changed files:
filename: .prettierignore
patch: @@ -40,6 +40,7 @@ packages/next-codemod/**/*.js
 packages/next-codemod/**/*.d.ts
 
 packages/next-env/**/*.d.ts
+**/next-env.d.ts
 
 test/**/out/**
 test/development/basic/hmr/components/parse-error.js

filename: packages/next/src/server/dev/browser-logs/file-logger.test.ts
patch: @@ -54,13 +54,22 @@ describe('FileLogger', () => {
 
     expect(lines).toHaveLength(2)
 
-    // Check format: [timestamp] source level message
-    expect(lines[0]).toMatch(
-      /^\[\d{2}:\d{2}:\d{2}\.\d{3}\] Browser LOG {5}Test message$/
-    )
-    expect(lines[1]).toMatch(
-      /^\[\d{2}:\d{2}:\d{2}\.\d{3}\] Server {2}ERROR {3}Server error$/
-    )
+    // Check format: JSON objects with timestamp, source, level, message
+    const log1 = JSON.parse(lines[0])
+    expect(log1).toMatchObject({
+      timestamp: expect.stringMatching(/^\d{2}:\d{2}:\d{2}\.\d{3}$/),
+      source: 'Browser',
+      level: 'LOG',
+      message: 'Test message',
+    })
+
+    const log2 = JSON.parse(lines[1])
+    expect(log2).toMatchObject({
+      timestamp: expect.stringMatching(/^\d{2}:\d{2}:\d{2}\.\d{3}$/),
+      source: 'Server',
+      level: 'ERROR',
+      message: 'Server error',
+    })
   })
 
   it('should append multiple log entries', () => {
@@ -95,11 +104,11 @@ describe('FileLogger', () => {
     const logFilePath = path.join(logsDir, 'next-development.log')
 
     const logContent = fs.readFileSync(logFilePath, 'utf-8')
-    expect(logContent).toContain('Message with "quotes" and')
-    expect(logContent).toContain('newlines')
+    const log = JSON.parse(logContent.trim())
+    expect(log.message).toBe('Message with "quotes" and \n newlines')
   })
 
-  it('should pad log levels correctly', () => {
+  it('should format different log levels correctly', () => {
     fileLogger.logBrowser('LOG', 'Short level')
     fileLogger.logBrowser('WARN', 'Medium level')
     fileLogger.logBrowser('ERROR', 'Long level')
@@ -114,10 +123,24 @@ describe('FileLogger', () => {
     const logContent = fs.readFileSync(logFilePath, 'utf-8')
     const lines = logContent.trim().split('\n')
 
-    // All levels should be padded to 7 characters
-    expect(lines[0]).toMatch(/Browser LOG {5}Short level/)
-    expect(lines[1]).toMatch(/Browser WARN {4}Medium level/)
-    expect(lines[2]).toMatch(/Browser ERROR {3}Long level/)
+    // All levels should be in JSON format
+    const log1 = JSON.parse(lines[0])
+    expect(log1).toMatchObject({
+      level: 'LOG',
+      message: 'Short level',
+    })
+
+    const log2 = JSON.parse(lines[1])
+    expect(log2).toMatchObject({
+      level: 'WARN',
+      message: 'Medium level',
+    })
+
+    const log3 = JSON.parse(lines[2])
+    expect(log3).toMatchObject({
+      level: 'ERROR',
+      message: 'Long level',
+    })
   })
 
   it('should not create log file when mcpServer is disabled', () => {

filename: packages/next/src/server/dev/browser-logs/file-logger.ts
patch: @@ -60,9 +60,7 @@ export class FileLogger {
 
   private formatLogEntry(entry: LogEntry): string {
     const { timestamp, source, level, message } = entry
-    const levelPadded = level.toUpperCase().padEnd(7, ' ') // Pad level to 7 characters for alignment
-    const sourcePadded = source === 'Browser' ? source : 'Server '
-    return `[${timestamp}] ${sourcePadded} ${levelPadded} ${message}\n`
+    return JSON.stringify({ timestamp, source, level, message }) + '\n'
   }
 
   private scheduleFlush(): void {

filename: test/e2e/app-dir/log-file/log-file.test.ts
patch: @@ -45,19 +45,33 @@ describe('log-file', () => {
         // Strip lines containing "Download the React DevTools"
         .split('\n')
         .filter((line) => {
-          // filter out the noise logs
-          if (
-            /Download the React DevTools|connected to ws at|received ws message|Next.js page already hydrated|Next.js hydrate callback fired|Compiling|Compiled|Ready in/.test(
-              line
-            )
-          ) {
+          if (!line.trim()) return false
+          // Parse JSON and filter out the noise logs
+          try {
+            const log = JSON.parse(line)
+            if (
+              /Download the React DevTools|connected to ws at|received ws message|Next.js page already hydrated|Next.js hydrate callback fired|Compiling|Compiled|Ready in/.test(
+                log.message
+              )
+            ) {
+              return false
+            }
+            return true
+          } catch {
             return false
           }
-          return true
+        })
+        .map((line) => {
+          // Normalize timestamps in JSON to consistent format
+          try {
+            const log = JSON.parse(line)
+            log.timestamp = 'xx:xx:xx.xxx'
+            return JSON.stringify(log)
+          } catch {
+            return line
+          }
         })
         .join('\n')
-        // Normalize timestamps to consistent format
-        .replace(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/g, '[xx:xx:xx.xxx]')
     )
   }
 
@@ -85,10 +99,9 @@ describe('log-file', () => {
         // The server logs will be replayed in the browser, but they'll not be forwarded to terminal,
         // as terminal already has them in the first place.
         expect(newLogContent).toMatchInlineSnapshot(`
-         "[xx:xx:xx.xxx] Server  LOG     RSC: This is a log message from server component
-         [xx:xx:xx.xxx] Server  ERROR   RSC: This is an error message from server component
-         [xx:xx:xx.xxx] Server  WARN    RSC: This is a warning message from server component
-         "
+         "{"timestamp":"xx:xx:xx.xxx","source":"Server","level":"LOG","message":"RSC: This is a log message from server component"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"ERROR","message":"RSC: This is an error message from server component"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"WARN","message":"RSC: This is a warning message from server component"}"
         `)
       })
     } else {
@@ -123,14 +136,13 @@ describe('log-file', () => {
         const newLogContent = getNewLogContent()
         // Only browser only logs are being forwarded to terminal
         expect(newLogContent).toMatchInlineSnapshot(`
-         "[xx:xx:xx.xxx] Browser LOG     Client: Complex circular object: {"data":{"nested":{"items":[1,2,3],"value":42},"parent":"[Circular]"},"metadata":{"name":"safe stringify","version":"1.0.0"},"name":"test"}
-         [xx:xx:xx.xxx] Browser ERROR   Client: This is an error message from client component
-         [xx:xx:xx.xxx] Browser WARN    Client: This is a warning message from client component
-         [xx:xx:xx.xxx] Server  ERROR   [browser] "Client: This is an error message from client component" "\\n    at ClientPage.useEffect (app/client/page.tsx:25:13)\\n  23 |     circularObj.data.parent = circularObj\\n  24 |     console.log('Client: Complex circular object:', circularObj)\\n> 25 |     console.error('Client: This is an error message from client component')\\n     |             ^\\n  26 |     console.warn('Client: This is a warning message from client component')\\n  27 |   }, [])\\n  28 |" "(app/client/page.tsx:25:13)"
-         [xx:xx:xx.xxx] Browser ERROR   Client: This is an error message from client component "\\n    at ClientPage.useEffect (app/client/page.tsx:25:13)\\n  23 |     circularObj.data.parent = circularObj\\n  24 |     console.log('Client: Complex circular object:', circularObj)\\n> 25 |     console.error('Client: This is an error message from client component')\\n     |             ^\\n  26 |     console.warn('Client: This is a warning message from client component')\\n  27 |   }, [])\\n  28 |" "(app/client/page.tsx:25:13)"
-         [xx:xx:xx.xxx] Server  WARN    [browser] "Client: This is a warning message from client component" "(app/client/page.tsx:26:13)"
-         [xx:xx:xx.xxx] Browser WARN    Client: This is a warning message from client component
-         "
+         "{"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"LOG","message":"Client: Complex circular object: {\\"data\\":{\\"nested\\":{\\"items\\":[1,2,3],\\"value\\":42},\\"parent\\":\\"[Circular]\\"},\\"metadata\\":{\\"name\\":\\"safe stringify\\",\\"version\\":\\"1.0.0\\"},\\"name\\":\\"test\\"}"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"ERROR","message":"Client: This is an error message from client component"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"WARN","message":"Client: This is a warning message from client component"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"ERROR","message":"[browser] \\"Client: This is an error message from client component\\" \\"\\\\n    at ClientPage.useEffect (app/client/page.tsx:25:13)\\\\n  23 |     circularObj.data.parent = circularObj\\\\n  24 |     console.log('Client: Complex circular object:', circularObj)\\\\n> 25 |     console.error('Client: This is an error message from client component')\\\\n     |             ^\\\\n  26 |     console.warn('Client: This is a warning message from client component')\\\\n  27 |   }, [])\\\\n  28 |\\" \\"(app/client/page.tsx:25:13)\\""}
+         {"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"ERROR","message":"Client: This is an error message from client component \\"\\\\n    at ClientPage.useEffect (app/client/page.tsx:25:13)\\\\n  23 |     circularObj.data.parent = circularObj\\\\n  24 |     console.log('Client: Complex circular object:', circularObj)\\\\n> 25 |     console.error('Client: This is an error message from client component')\\\\n     |             ^\\\\n  26 |     console.warn('Client: This is a warning message from client component')\\\\n  27 |   }, [])\\\\n  28 |\\" \\"(app/client/page.tsx:25:13)\\""}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"WARN","message":"[browser] \\"Client: This is a warning message from client component\\" \\"(app/client/page.tsx:26:13)\\""}
+         {"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"WARN","message":"Client: This is a warning message from client component"}"
         `)
       })
     } else {
@@ -148,12 +160,11 @@ describe('log-file', () => {
       await retry(async () => {
         const newLogContent = getNewLogContent()
         expect(newLogContent).toMatchInlineSnapshot(`
-         "[xx:xx:xx.xxx] Server  LOG     Pages Router SSR: This is a log message from getServerSideProps
-         [xx:xx:xx.xxx] Server  ERROR   Pages Router SSR: This is an error message from getServerSideProps
-         [xx:xx:xx.xxx] Server  WARN    Pages Router SSR: This is a warning message from getServerSideProps
-         [xx:xx:xx.xxx] Server  LOG     Pages Router isomorphic: This is a log message from render
-         [xx:xx:xx.xxx] Browser LOG     Pages Router isomorphic: This is a log message from render
-         "
+         "{"timestamp":"xx:xx:xx.xxx","source":"Server","level":"LOG","message":"Pages Router SSR: This is a log message from getServerSideProps"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"ERROR","message":"Pages Router SSR: This is an error message from getServerSideProps"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"WARN","message":"Pages Router SSR: This is a warning message from getServerSideProps"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Server","level":"LOG","message":"Pages Router isomorphic: This is a log message from render"}
+         {"timestamp":"xx:xx:xx.xxx","source":"Browser","level":"LOG","message":"Pages Router isomorphic: This is a log message from render"}"
         `)
       })
     } else {