Add structured logging and ensure CLI shuts down cleanly
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
||||
FileSystemEntry,
|
||||
InstanceData,
|
||||
ServerMeta,
|
||||
|
||||
|
||||
WorkspaceCreateRequest,
|
||||
WorkspaceDescriptor,
|
||||
WorkspaceFileResponse,
|
||||
@@ -21,6 +21,15 @@ const DEFAULT_BASE = typeof window !== "undefined" ? window.__CODENOMAD_API_BASE
|
||||
const DEFAULT_EVENTS_URL = typeof window !== "undefined" ? window.__CODENOMAD_EVENTS_URL__ ?? "/api/events" : "/api/events"
|
||||
const API_BASE = import.meta.env.VITE_CODENOMAD_API_BASE ?? DEFAULT_BASE
|
||||
const EVENTS_URL = API_BASE ? `${API_BASE}${DEFAULT_EVENTS_URL}` : DEFAULT_EVENTS_URL
|
||||
const HTTP_PREFIX = "[HTTP]"
|
||||
|
||||
function logHttp(message: string, context?: Record<string, unknown>) {
|
||||
if (context) {
|
||||
console.log(`${HTTP_PREFIX} ${message}`, context)
|
||||
return
|
||||
}
|
||||
console.log(`${HTTP_PREFIX} ${message}`)
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const url = API_BASE ? new URL(path, API_BASE).toString() : path
|
||||
@@ -29,17 +38,30 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
...(init?.headers ?? {}),
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...init, headers })
|
||||
if (!response.ok) {
|
||||
const message = await response.text()
|
||||
throw new Error(message || `Request failed with ${response.status}`)
|
||||
const method = (init?.method ?? "GET").toUpperCase()
|
||||
const startedAt = Date.now()
|
||||
logHttp(`${method} ${path}`)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...init, headers })
|
||||
if (!response.ok) {
|
||||
const message = await response.text()
|
||||
logHttp(`${method} ${path} -> ${response.status}`, { durationMs: Date.now() - startedAt, error: message })
|
||||
throw new Error(message || `Request failed with ${response.status}`)
|
||||
}
|
||||
const duration = Date.now() - startedAt
|
||||
logHttp(`${method} ${path} -> ${response.status}`, { durationMs: duration })
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
return (await response.json()) as T
|
||||
} catch (error) {
|
||||
logHttp(`${method} ${path} failed`, { durationMs: Date.now() - startedAt, error })
|
||||
throw error
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
|
||||
export const cliApi = {
|
||||
fetchWorkspaces(): Promise<WorkspaceDescriptor[]> {
|
||||
return request<WorkspaceDescriptor[]>("/api/workspaces")
|
||||
@@ -124,16 +146,18 @@ export const cliApi = {
|
||||
return request(`/api/storage/instances/${encodeURIComponent(id)}`, { method: "DELETE" })
|
||||
},
|
||||
connectEvents(onEvent: (event: WorkspaceEventPayload) => void, onError?: () => void) {
|
||||
console.log(`[SSE] Connecting to ${EVENTS_URL}`)
|
||||
const source = new EventSource(EVENTS_URL)
|
||||
source.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data) as WorkspaceEventPayload
|
||||
onEvent(payload)
|
||||
} catch (error) {
|
||||
console.error("Failed to parse SSE event", error)
|
||||
console.error("[SSE] Failed to parse event", error)
|
||||
}
|
||||
}
|
||||
source.onerror = () => {
|
||||
console.warn("[SSE] EventSource error, closing stream")
|
||||
onError?.()
|
||||
}
|
||||
return source
|
||||
|
||||
@@ -3,6 +3,15 @@ import { cliApi } from "./api-client"
|
||||
|
||||
const RETRY_BASE_DELAY = 1000
|
||||
const RETRY_MAX_DELAY = 10000
|
||||
const SSE_PREFIX = "[SSE]"
|
||||
|
||||
function logSse(message: string, context?: Record<string, unknown>) {
|
||||
if (context) {
|
||||
console.log(`${SSE_PREFIX} ${message}`, context)
|
||||
return
|
||||
}
|
||||
console.log(`${SSE_PREFIX} ${message}`)
|
||||
}
|
||||
|
||||
class CliEvents {
|
||||
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
|
||||
@@ -17,8 +26,10 @@ class CliEvents {
|
||||
if (this.source) {
|
||||
this.source.close()
|
||||
}
|
||||
logSse("Connecting to backend events stream")
|
||||
this.source = cliApi.connectEvents((event) => this.dispatch(event), () => this.scheduleReconnect())
|
||||
this.source.onopen = () => {
|
||||
logSse("Events stream connected")
|
||||
this.retryDelay = RETRY_BASE_DELAY
|
||||
}
|
||||
}
|
||||
@@ -28,6 +39,7 @@ class CliEvents {
|
||||
this.source.close()
|
||||
this.source = null
|
||||
}
|
||||
logSse("Events stream disconnected, scheduling reconnect", { delayMs: this.retryDelay })
|
||||
setTimeout(() => {
|
||||
this.retryDelay = Math.min(this.retryDelay * 2, RETRY_MAX_DELAY)
|
||||
this.connect()
|
||||
@@ -35,6 +47,7 @@ class CliEvents {
|
||||
}
|
||||
|
||||
private dispatch(event: WorkspaceEventPayload) {
|
||||
logSse(`event ${event.type}`)
|
||||
this.handlers.get("*")?.forEach((handler) => handler(event))
|
||||
this.handlers.get(event.type)?.forEach((handler) => handler(event))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user