forked from tldraw/tldraw
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR begins to address some pains we've felt with regard to logging on cloudflare workers: 1. a console.log invocation is only flushed to the console (or cf dashboard) once a request completes. so if a request hangs for some reason you never see the log output. 2. logs are very eagerly sampled, which makes it hard to rely on them for debugging because you never know whether a log statement wasn't reached or whether it was just dropped. so this PR - adds a Logger durable object that you can connect to via a websocket to get an instant stream of every log statement, no waiting for requests to finish. - wraps the Logger durable object with a Logger class to consolidate code. The logger durable object is on in dev and preview envs, but is not available in staging or production yet because that would require auth and filtering user DO events by user. You can try it on this PR by going to https://pr-5219-preview-deploy.tldraw.com/__debug-tail and then opening the app in another tab also tackles https://linear.app/tldraw/issue/INT-648/investigate-flaky-preview-deploys somewhat ### Change type - [x] `other`
- Loading branch information
Showing
13 changed files
with
277 additions
and
79 deletions.
There are no files selected for viewing
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
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,40 @@ | ||
import { useEffect, useRef } from 'react' | ||
import { MULTIPLAYER_SERVER } from '../../utils/config' | ||
|
||
export function Component() { | ||
const ref = useRef<HTMLDivElement>(null) | ||
const isAutoScroll = useRef(true) | ||
useEffect(() => { | ||
const elem = ref.current | ||
if (!elem) return | ||
const socket = new WebSocket(MULTIPLAYER_SERVER + '/app/__debug-tail') | ||
socket.onmessage = (msg) => { | ||
const div = document.createElement('pre') | ||
div.textContent = msg.data | ||
elem.appendChild(div) | ||
if (isAutoScroll.current) { | ||
elem.scrollTo({ top: elem.scrollHeight }) | ||
} | ||
} | ||
socket.onerror = (err) => { | ||
console.error(err) | ||
} | ||
socket.onclose = () => { | ||
setTimeout(() => { | ||
window.location.reload() | ||
}, 500) | ||
} | ||
|
||
const onScroll = () => { | ||
isAutoScroll.current = elem.scrollTop + elem.clientHeight > elem.scrollHeight - 100 | ||
} | ||
elem.addEventListener('scroll', onScroll) | ||
return () => { | ||
socket.close() | ||
elem.removeEventListener('scroll', onScroll) | ||
} | ||
}, []) | ||
return ( | ||
<div ref={ref} style={{ fontFamily: 'monospace', overflow: 'scroll', height: '100vh' }}></div> | ||
) | ||
} |
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,44 @@ | ||
import { createSentry } from '@tldraw/worker-shared' | ||
import { Environment, isDebugLogging } from './types' | ||
import { getLogger } from './utils/durableObjects' | ||
|
||
export class Logger { | ||
readonly logger | ||
constructor( | ||
env: Environment, | ||
private prefix: string, | ||
private sentry?: ReturnType<typeof createSentry> | ||
) { | ||
if (isDebugLogging(env)) { | ||
this.logger = getLogger(env) | ||
} | ||
} | ||
private outgoing: string[] = [] | ||
|
||
private isRunning = false | ||
|
||
debug(...args: any[]) { | ||
if (!this.logger && !this.sentry) return | ||
const msg = `[${this.prefix} ${new Date().toISOString()}]: ${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' ')}` | ||
this.outgoing.push(msg) | ||
this.processQueue() | ||
} | ||
|
||
private async processQueue() { | ||
if (this.isRunning) return | ||
this.isRunning = true | ||
try { | ||
while (this.outgoing.length) { | ||
const batch = this.outgoing | ||
this.outgoing = [] | ||
await this.logger?.debug(batch) | ||
for (const message of batch) { | ||
// eslint-disable-next-line @typescript-eslint/no-deprecated | ||
this.sentry?.addBreadcrumb({ message }) | ||
} | ||
} | ||
} finally { | ||
this.isRunning = false | ||
} | ||
} | ||
} |
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,65 @@ | ||
import { DurableObject } from 'cloudflare:workers' | ||
import { IRequest } from 'itty-router' | ||
import { Environment, isDebugLogging } from './types' | ||
|
||
export class TLLoggerDurableObject extends DurableObject<Environment> { | ||
private readonly isDebugEnv | ||
private readonly db | ||
constructor(ctx: DurableObjectState, env: Environment) { | ||
super(ctx, env) | ||
this.isDebugEnv = isDebugLogging(env) | ||
this.db = this.ctx.storage.sql | ||
this.db.exec( | ||
`CREATE TABLE IF NOT EXISTS logs ( | ||
message TEXT NOT NULL | ||
); | ||
CREATE TRIGGER IF NOT EXISTS limit_logs | ||
AFTER INSERT ON logs | ||
BEGIN | ||
DELETE FROM logs WHERE rowid NOT IN ( | ||
SELECT rowid FROM logs ORDER BY rowid DESC LIMIT 20000 | ||
); | ||
END;` | ||
) | ||
} | ||
|
||
private sockets = new Set<WebSocket>() | ||
|
||
async debug(messages: string[]) { | ||
if (!this.isDebugEnv) return | ||
for (const message of messages) { | ||
this.db.exec(`INSERT INTO logs (message) VALUES (?)`, message) | ||
} | ||
|
||
const sockets = Array.from(this.sockets) | ||
if (this.sockets.size === 0) return | ||
for (const message of messages) { | ||
sockets.forEach((socket) => { | ||
socket.send(message + '\n') | ||
}) | ||
} | ||
} | ||
|
||
getFullHistory() { | ||
return this.db | ||
.exec('SELECT message FROM logs ORDER BY rowid ASC') | ||
.toArray() | ||
.map((row) => row.message) | ||
} | ||
|
||
override async fetch(_req: IRequest) { | ||
if (!this.isDebugEnv) return new Response('Not Found', { status: 404 }) | ||
const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair() | ||
serverWebSocket.accept() | ||
|
||
this.sockets.add(serverWebSocket) | ||
const cleanup = () => { | ||
this.sockets.delete(serverWebSocket) | ||
serverWebSocket.close() | ||
} | ||
serverWebSocket.addEventListener('close', cleanup) | ||
serverWebSocket.addEventListener('error', cleanup) | ||
serverWebSocket.send('Connected to logger\n' + this.getFullHistory().join('\n')) | ||
return new Response(null, { status: 101, webSocket: clientWebSocket }) | ||
} | ||
} |
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
Oops, something went wrong.