GadgetLogTransportSocket and the drone-to-IDE log

This commit is contained in:
Rob Colbert 2026-05-09 07:16:50 -04:00
parent 632caf11ed
commit d7924a9d6f
11 changed files with 444 additions and 140 deletions

View File

@ -1,32 +1,131 @@
# Gadget Code: GadgetLogTransportSocket # Gadget Code: GadgetLogTransportSocket
We recently finished a large refactor of GadgetLog into the @gadget/api package, and all components are now consumers of this one unified logging interface. [GadgetLog](../packages/api/src/lib/log.ts) makes use of [GadgetLogTransport](../packages/api/src/lib/log-transport.ts) to provide an abstract log transport. The logging system is working as intended. Console logging works as expected. File logging works as expected. GadgetLog now ships four transports out of the box: **Console**, **File**, **Socket**, and the abstract base. The socket transport (`GadgetLogTransportSocket`) transmits every log entry from `gadget-drone``gadget-code:backend``gadget-code:frontend` over Socket.IO, rendering live in the IDE's LogPanel.
We are now going to implement `GadgetLogTransportSocket`, which will transmit a Socket.IO message `log` from `gadget-drone` to `gadget-code:backend`, which will forward the log message directly to [ChatSessionView](../gadget-code/frontend/src/pages/ChatSessionView.tsx) for display in [LogPanel](../gadget-code/frontend/src/components/LogPanel.tsx). ## Message Flow
I have already added the `LogMessage` type to the drone message definitions, and added definitions to the appropriate interfaces we use for extending Socket.IO. It is a fire-n-forget message (no callback, no blocking). ```
gadget-drone
│ GadgetLogTransportSocket.writeLog()
│ socket.emit("log", timestamp, component, level, message, metadata?)
gadget-code:backend (DroneSession.onLog)
│ SocketService.getCodeSessionByChatSessionId(chatSessionId)
│ codeSession.onLog(...)
gadget-code:backend (CodeSession.onLog)
│ socket.emit("log", timestamp, component, level, message, metadata?)
gadget-code:frontend (SocketClient)
│ raw socket "log" → internal "log:entry" event
│ { level, component, message, metadata, timestamp }
ChatSessionView.handleLogEntry
│ setLogs(...)
LogPanel — terminal-styled live log viewer
```
## How It Works ## Transport: `GadgetLogTransportSocket`
You will simply pass the parameters passed to the GadgetLog API as a message over Socket.IO to the Log panel dislay in the IDE. **File:** `packages/api/src/lib/log-transport-socket.ts`
## Instructions Accepts a generic emit function (no Socket.IO dependency in `@gadget/api`):
In this session, you will: ```typescript
export type LogEmitFunction = (
event: string,
timestamp: Date,
component: GadgetComponent,
level: GadgetLogLevel,
message: string,
metadata?: unknown,
) => void;
```
1. Implement [GadgetLogTransportSocket](../packages/api/src/lib/log-transport-socket.ts) `writeLog()` calls `this.emit("log", ...)` with the same parameters `GadgetLog.writeLog()` receives.
2. Register it for use as a default transport on the gadget-drone logger instance
3. Implement session message routing in gadget-code:backend to forward the message to gadget-code:frontend (the IDE)
4. Deliver log messages to the Chat Session view's Log panel for display
Log messages should render as closely to the Console Transport's output as possible, matching the colors used, and style. ## Registration in gadget-drone
When you are done, you will re-write this document in the style Registered in `gadget-drone.ts` `connectSocket()`, after the Socket.IO connection succeeds:
## References ```typescript
const socketTransport = new GadgetLogTransportSocket((event, ...args) => {
(this.socket as any)?.emit(event, ...args);
});
GadgetLog.addDefaultTransport(socketTransport);
this.log.transports.push(socketTransport);
AgentService.log.transports.push(socketTransport);
AiService.log.transports.push(socketTransport);
PlatformService.log.transports.push(socketTransport);
```
Always search first in the project's `docs` directories for information and knowledge. Here are some starting points for this session: The transport is injected into all existing loggers (main process + 3 services) and set as a default for future `GadgetLog` instances.
- [UI Design and Style Guide](../gadget-code/docs/ui-design-guide.md)] ## Backend Routing
- [System Architecture](./architecture.md)
- [Socket Protocol](./socket-protocol.md) ### DroneSession (`gadget-code/src/lib/drone-session.ts`)
- Registers `this.socket.on("log", this.onLog.bind(this))`
- `onLog()` guards on `this.chatSessionId`, looks up the `CodeSession` via `SocketService.getCodeSessionByChatSessionId()`, forwards the call.
### CodeSession (`gadget-code/src/lib/code-session.ts`)
- `onLog()` calls `this.socket.emit("log", timestamp, component, level, message, metadata)` forwards to the IDE.
## Frontend Bridging
### SocketClient (`gadget-code/frontend/src/lib/socket.ts`)
The raw Socket.IO `log` event is forwarded to the internal `log:entry` event system:
```typescript
this.socket.on("log", (timestamp, component, level, message, metadata) => {
this.emit("log:entry", {
level,
component: component.name,
message,
metadata,
timestamp: new Date(timestamp).getTime(),
});
});
```
### ChatSessionView
`handleLogEntry` callback builds a `LogEntry` with all fields and appends to the `logs` state array.
## LogPanel Display
**File:** `gadget-code/frontend/src/components/LogPanel.tsx`
Styled to match the Console Transport:
| Element | Console Color | Tailwind Class |
|---------|---------------|----------------|
| Timestamp | `darkGray` | `text-gray-600` |
| Level: debug | `darkGray` | `text-gray-500` |
| Level: info | `green` | `text-green-400` |
| Level: warn | `yellow` | `text-yellow-400` |
| Level: alert | `red` | `text-red-400` |
| Level: error | `bgRed` + `white` | `bg-red-800 text-white` |
| Level: crit | `bgRed` + `yellow` | `bg-red-800 text-yellow-300` |
| Level: fatal | `bgRed` + `darkGray` | `bg-red-800 text-gray-400` |
| Component | `cyan` | `text-cyan-400` |
| Message | `darkGray` | `text-text-secondary` |
Metadata is displayed as expandable JSON below the log line (toggled with `⊞`/`⊟`).
## Files Changed
| File | Action |
|------|--------|
| `packages/api/src/lib/log-transport-socket.ts` | **NEW** — socket transport |
| `packages/api/src/index.ts` | **MODIFY** — export new transport |
| `gadget-drone/src/gadget-drone.ts` | **MODIFY** — register transport on connect |
| `gadget-drone/src/lib/service.ts` | **MODIFY**`log` public for transport injection |
| `gadget-code/src/lib/drone-session.ts` | **MODIFY**`onLog` handler |
| `gadget-code/src/lib/code-session.ts` | **MODIFY**`onLog` forwarding |
| `gadget-code/frontend/src/lib/socket.ts` | **MODIFY** — bridge `log``log:entry` |
| `gadget-code/frontend/src/pages/ChatSessionView.tsx` | **MODIFY** — enriched `LogEntry` |
| `gadget-code/frontend/src/components/LogPanel.tsx` | **REWRITE** — console-styled display |

View File

@ -1,22 +1,44 @@
import { useRef, useEffect } from 'react'; import { useRef, useEffect, useState } from 'react';
import { WorkspaceMode } from '../lib/types';
interface LogEntry { interface LogEntry {
id: string; id: string;
timestamp: Date; timestamp: Date;
level: string; level: string;
component: string;
message: string; message: string;
metadata?: unknown;
} }
interface LogPanelProps { interface LogPanelProps {
logs: LogEntry[]; logs: LogEntry[];
expanded: boolean; expanded: boolean;
workspaceMode: WorkspaceMode;
onToggleExpand: () => void; onToggleExpand: () => void;
} }
export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand }: LogPanelProps) { const levelColors: Record<string, string> = {
debug: 'text-gray-500',
info: 'text-green-400',
warn: 'text-yellow-400',
alert: 'text-red-400',
error: 'bg-red-800 text-white',
crit: 'bg-red-800 text-yellow-300',
fatal: 'bg-red-800 text-gray-400',
};
function formatTimestamp(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const hh = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0');
const ss = String(date.getSeconds()).padStart(2, '0');
const ms = String(date.getMilliseconds()).padStart(3, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}.${ms}`;
}
export default function LogPanel({ logs, expanded, onToggleExpand }: LogPanelProps) {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [expandedMetadata, setExpandedMetadata] = useState<Set<string>>(new Set());
useEffect(() => { useEffect(() => {
if (scrollRef.current) { if (scrollRef.current) {
@ -24,6 +46,18 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand
} }
}, [logs]); }, [logs]);
const toggleMetadata = (id: string) => {
setExpandedMetadata(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
return ( return (
<div <div
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${ className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
@ -44,34 +78,48 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand
</div> </div>
<div <div
ref={scrollRef} ref={scrollRef}
className="flex-1 overflow-y-auto p-2 font-mono text-xs" className="flex-1 overflow-y-auto p-2 font-mono text-xs leading-relaxed"
> >
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className="text-text-muted">No log entries</div> <div className="text-text-muted">No log entries</div>
) : ( ) : (
logs.map((entry) => ( logs.map((entry) => {
<div key={entry.id} className="flex gap-2 py-0.5"> const levelClass = levelColors[entry.level] || 'text-text-secondary';
<span className="text-text-muted shrink-0"> return (
{entry.timestamp.toLocaleTimeString()} <div key={entry.id} className="flex gap-2 py-0.5 hover:bg-bg-elevated/50">
</span> <span className="text-gray-600 shrink-0 select-none">
<span {formatTimestamp(entry.timestamp)}
className={`shrink-0 ${ </span>
entry.level === 'error' <span className={`shrink-0 font-bold ${levelClass}`}>
? 'text-red-500' {entry.level.toUpperCase().padEnd(5)}
: entry.level === 'warn' </span>
? 'text-yellow-500' <span className="text-cyan-400 shrink-0">
: entry.level === 'info' {entry.component}
? 'text-blue-400' </span>
: 'text-text-secondary' <span className="text-text-secondary break-all min-w-0">
}`} {entry.message}
> </span>
[{entry.level.toUpperCase()}] {entry.metadata && (
</span> <button
<span className="text-text-primary">{entry.message}</span> onClick={() => toggleMetadata(entry.id)}
</div> className="shrink-0 text-text-muted hover:text-text-secondary transition-colors"
)) title={expandedMetadata.has(entry.id) ? 'Hide metadata' : 'Show metadata'}
>
{expandedMetadata.has(entry.id) ? '⊟' : '⊞'}
</button>
)}
{entry.metadata && expandedMetadata.has(entry.id) && (
<div className="w-full text-text-muted pl-[1em] border-l border-border-subtle mt-0.5 mb-1">
<pre className="whitespace-pre-wrap break-all">
{JSON.stringify(entry.metadata, null, 2)}
</pre>
</div>
)}
</div>
);
})
)} )}
</div> </div>
</div> </div>
); );
} }

View File

@ -17,6 +17,13 @@ export interface ServerToClientEvents {
success: boolean, success: boolean,
message?: string, message?: string,
) => void; ) => void;
log: (
timestamp: string,
component: { name: string; slug: string },
level: string,
message: string,
metadata?: unknown,
) => void;
} }
export interface ClientToServerEvents { export interface ClientToServerEvents {
@ -72,7 +79,9 @@ export interface SocketEvents {
"agent:complete": (data: { agentId: string }) => void; "agent:complete": (data: { agentId: string }) => void;
"log:entry": (data: { "log:entry": (data: {
level: string; level: string;
component: string;
message: string; message: string;
metadata?: unknown;
timestamp: number; timestamp: number;
}) => void; }) => void;
"chat:message": (data: { "chat:message": (data: {
@ -152,6 +161,25 @@ class SocketClient {
this.emit("workspaceModeChanged", mode); this.emit("workspaceModeChanged", mode);
}); });
this.socket.on(
"log",
(
timestamp: string,
component: { name: string; slug: string },
level: string,
message: string,
metadata?: unknown,
) => {
this.emit("log:entry", {
level,
component: component.name,
message,
metadata,
timestamp: new Date(timestamp).getTime(),
});
},
);
this._socket.on("connect", () => { this._socket.on("connect", () => {
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.emit("connect"); this.emit("connect");
@ -297,7 +325,7 @@ class SocketClient {
startSessionHeartbeat(): void { startSessionHeartbeat(): void {
if (this.heartbeatInterval) return; if (this.heartbeatInterval) return;
this.heartbeatInterval = setInterval(() => { this.heartbeatInterval = setInterval(() => {
if (this._socket?.connected) { if (this._socket) {
this._socket.emit("sessionHeartbeat", (ack: boolean) => { this._socket.emit("sessionHeartbeat", (ack: boolean) => {
if (!ack) { if (!ack) {
console.warn("sessionHeartbeat: drone did not acknowledge"); console.warn("sessionHeartbeat: drone did not acknowledge");

View File

@ -13,7 +13,9 @@ interface LogEntry {
id: string; id: string;
timestamp: Date; timestamp: Date;
level: string; level: string;
component: string;
message: string; message: string;
metadata?: unknown;
} }
interface StreamingState { interface StreamingState {
@ -107,6 +109,33 @@ export default function ChatSessionView() {
}; };
}, [session, project]); }, [session, project]);
// Re-lock on socket reconnect to restore lock on a new CodeSession
const handleSocketReconnect = useCallback(async () => {
if (!sessionRef.current || !projectRef.current) return;
const droneJson = localStorage.getItem('dtp_drone_registration');
if (!droneJson) return;
try {
const registration = JSON.parse(droneJson);
const success = await socketClient.requestSessionLock(
registration,
projectRef.current,
sessionRef.current,
);
if (!success) {
console.warn('Failed to re-lock drone after socket reconnect');
}
} catch (err) {
console.error('Failed to re-lock drone after socket reconnect', err);
}
}, []);
useEffect(() => {
socketClient.on('connect', handleSocketReconnect);
return () => {
socketClient.off('connect', handleSocketReconnect);
};
}, [handleSocketReconnect]);
// Release session lock on unmount only // Release session lock on unmount only
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -409,14 +438,16 @@ export default function ChatSessionView() {
setWorkspaceMode(mode as WorkspaceMode); setWorkspaceMode(mode as WorkspaceMode);
}, []); }, []);
const handleLogEntry = useCallback((data: { level: string; message: string; timestamp: number }) => { const handleLogEntry = useCallback((data: { level: string; component: string; message: string; metadata?: unknown; timestamp: number }) => {
setLogs(prev => [ setLogs(prev => [
...prev, ...prev,
{ {
id: `log-${Date.now()}-${Math.random()}`, id: `log-${Date.now()}-${Math.random()}`,
timestamp: new Date(data.timestamp), timestamp: new Date(data.timestamp),
level: data.level, level: data.level,
component: data.component,
message: data.message, message: data.message,
metadata: data.metadata,
}, },
]); ]);
}, []); }, []);
@ -756,7 +787,6 @@ export default function ChatSessionView() {
<LogPanel <LogPanel
logs={logs} logs={logs}
expanded={logExpanded} expanded={logExpanded}
workspaceMode={workspaceMode}
onToggleExpand={() => setLogExpanded(!logExpanded)} onToggleExpand={() => setLogExpanded(!logExpanded)}
/> />
</div> </div>

View File

@ -8,6 +8,8 @@ import {
SocketSessionType, SocketSessionType,
} from "./socket-session.js"; } from "./socket-session.js";
import { import {
GadgetComponent,
GadgetLogLevel,
IChatSession, IChatSession,
IDroneRegistration, IDroneRegistration,
IProject, IProject,
@ -43,10 +45,7 @@ export class CodeSession extends SocketSession {
this.onRequestWorkspaceMode.bind(this), this.onRequestWorkspaceMode.bind(this),
); );
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this)); this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
this.socket.on( this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
"releaseSessionLock",
this.onReleaseSessionLock.bind(this),
);
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this)); this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
} }
@ -96,23 +95,27 @@ export class CodeSession extends SocketSession {
chatSession: IChatSession, chatSession: IChatSession,
cb: (success: boolean, chatSessionId: string) => void, cb: (success: boolean, chatSessionId: string) => void,
) { ) {
const droneSession = SocketService.getDroneSession(registration); try {
droneSession.socket.emit( const droneSession = SocketService.getDroneSession(registration);
"requestSessionLock", droneSession.socket.emit(
registration, "requestSessionLock",
project, registration,
chatSession, project,
(success: boolean, chatSessionId: string): void => { chatSession,
if (success) { (success: boolean, chatSessionId: string): void => {
this.selectedDrone = registration; if (success) {
this.chatSession = chatSession; this.selectedDrone = registration;
this.project = project; this.chatSession = chatSession;
SocketService.registerChatSession(chatSession._id, this); this.project = project;
droneSession.setChatSessionId(chatSession._id); SocketService.registerChatSession(chatSession._id, this);
} droneSession.setChatSessionId(chatSession._id);
cb(success, chatSessionId); }
}, cb(success, chatSessionId);
); },
);
} catch (error) {
cb(false, "");
}
} }
/** /**
@ -136,20 +139,24 @@ export class CodeSession extends SocketSession {
return cb(false, this.workspaceMode); return cb(false, this.workspaceMode);
} }
const droneSession = SocketService.getDroneSession(this.selectedDrone); try {
droneSession.socket.emit( const droneSession = SocketService.getDroneSession(this.selectedDrone);
"requestWorkspaceMode", droneSession.socket.emit(
registration, "requestWorkspaceMode",
project, registration,
chatSession, project,
mode, chatSession,
(success: boolean, currentMode: WorkspaceMode) => { mode,
if (success) { (success: boolean, currentMode: WorkspaceMode) => {
this.workspaceMode = currentMode; if (success) {
} this.workspaceMode = currentMode;
cb(success, currentMode); }
}, cb(success, currentMode);
); },
);
} catch (error) {
cb(false, this.workspaceMode);
}
} }
/** /**
@ -173,43 +180,47 @@ export class CodeSession extends SocketSession {
return cb(false, { message: "No project selected" }); return cb(false, { message: "No project selected" });
} }
const droneSession = SocketService.getDroneSession(this.selectedDrone); try {
const droneSession = SocketService.getDroneSession(this.selectedDrone);
let turn: ChatTurnDocument = await ChatSessionService.createTurn( let turn: ChatTurnDocument = await ChatSessionService.createTurn(
this.chatSession, this.chatSession,
content, content,
); );
this.currentTurnId = turn._id; this.currentTurnId = turn._id;
this.log.info("ChatTurn created", { this.log.info("ChatTurn created", {
turnId: turn._id, turnId: turn._id,
chatSessionId: this.chatSession._id, chatSessionId: this.chatSession._id,
}); });
droneSession.setCurrentTurnId(turn._id); droneSession.setCurrentTurnId(turn._id);
cb(true, { turnId: turn._id, message: "turn created successfully" }); cb(true, { turnId: turn._id, message: "turn created successfully" });
droneSession.socket.emit( droneSession.socket.emit(
"processWorkOrder", "processWorkOrder",
this.selectedDrone, this.selectedDrone,
turn, turn,
async (success: boolean, message?: string) => { async (success: boolean, message?: string) => {
if (success) { if (success) {
this.log.info("work order accepted by drone", { this.log.info("work order accepted by drone", {
turnId: turn._id, turnId: turn._id,
message, message,
}); });
} else { } else {
this.log.error("work order rejected by drone", { this.log.error("work order rejected by drone", {
turnId: turn._id, turnId: turn._id,
message, message,
}); });
turn.status = ChatTurnStatus.Error; turn.status = ChatTurnStatus.Error;
turn.errorMessage = message || "Drone rejected work order"; turn.errorMessage = message || "Drone rejected work order";
await turn.save(); await turn.save();
} }
}, },
); );
} catch (error) {
cb(false, {});
}
} }
/** /**
@ -222,23 +233,27 @@ export class CodeSession extends SocketSession {
chatSession: IChatSession, chatSession: IChatSession,
cb: (success: boolean) => void, cb: (success: boolean) => void,
) { ) {
const droneSession = SocketService.getDroneSession(registration); try {
droneSession.socket.emit( const droneSession = SocketService.getDroneSession(registration);
"releaseSessionLock", droneSession.socket.emit(
registration, "releaseSessionLock",
project, registration,
chatSession, project,
(success: boolean) => { chatSession,
if (success) { (success: boolean) => {
SocketService.unregisterChatSession(chatSession._id); if (success) {
droneSession.chatSessionId = undefined; SocketService.unregisterChatSession(chatSession._id);
this.selectedDrone = undefined; droneSession.chatSessionId = undefined;
this.chatSession = undefined; this.selectedDrone = undefined;
this.project = undefined; this.chatSession = undefined;
} this.project = undefined;
cb(success); }
}, cb(success);
); },
);
} catch (error) {
cb(false);
}
} }
/** /**
@ -249,10 +264,17 @@ export class CodeSession extends SocketSession {
if (!this.selectedDrone) { if (!this.selectedDrone) {
return cb(false); return cb(false);
} }
const droneSession = SocketService.getDroneSession(this.selectedDrone); try {
droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => { const droneSession = SocketService.getDroneSession(this.selectedDrone);
cb(ack); droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => {
}); cb(ack);
});
} catch (error) {
this.log.error("failed to forward session heartbeat to drone", {
error,
});
cb(false);
}
} }
/** /**
@ -268,6 +290,16 @@ export class CodeSession extends SocketSession {
this.socket.emit("status", content); this.socket.emit("status", content);
} }
onLog(
timestamp: Date,
component: GadgetComponent,
level: GadgetLogLevel,
message: string,
metadata?: unknown,
): void {
this.socket.emit("log", timestamp, component, level, message, metadata);
}
onThinking(content: string): void { onThinking(content: string): void {
this.socket.emit("thinking", content); this.socket.emit("thinking", content);
} }

View File

@ -3,6 +3,8 @@
// All Rights Reserved // All Rights Reserved
import { import {
GadgetComponent,
GadgetLogLevel,
IUser, IUser,
IDroneRegistration, IDroneRegistration,
ChatTurnStatus, ChatTurnStatus,
@ -58,6 +60,28 @@ export class DroneSession extends SocketSession {
); );
this.socket.on("requestTermination", this.onRequestTermination.bind(this)); this.socket.on("requestTermination", this.onRequestTermination.bind(this));
this.socket.on("log", this.onLog.bind(this));
}
async onLog(
timestamp: Date,
component: GadgetComponent,
level: GadgetLogLevel,
message: string,
metadata?: unknown,
): Promise<void> {
if (!this.chatSessionId) {
return;
}
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(
this.chatSessionId,
);
codeSession.onLog(timestamp, component, level, message, metadata);
} catch (error) {
this.log.error("failed to route log message", { error });
}
} }
async onStatus(message: string): Promise<void> { async onStatus(message: string): Promise<void> {

View File

@ -12,7 +12,7 @@ import AgentService, { IAgentWorkOrder } from "./services/agent.ts";
import AiService from "./services/ai.ts"; import AiService from "./services/ai.ts";
import PlatformService from "./services/platform.ts"; import PlatformService from "./services/platform.ts";
import WorkspaceService from "./services/workspace.ts"; import WorkspaceService from "./services/workspace.ts";
import { DroneStatus, IUser } from "@gadget/api"; import { DroneStatus, GadgetLog, GadgetLogTransportSocket, IUser } from "@gadget/api";
import { GadgetProcess } from "./lib/process.ts"; import { GadgetProcess } from "./lib/process.ts";
import { import {
@ -207,6 +207,18 @@ class GadgetDrone extends GadgetProcess {
}); });
this.socket.on("connect", () => { this.socket.on("connect", () => {
this.log.info("connected to Gadget Code platform."); this.log.info("connected to Gadget Code platform.");
const socketTransport = new GadgetLogTransportSocket(
(event, timestamp, component, level, message, metadata): void => {
(this.socket as any)?.emit(event, timestamp, component, level, message, metadata);
},
);
GadgetLog.addDefaultTransport(socketTransport);
this.log.transports.push(socketTransport);
AgentService.log.transports.push(socketTransport);
AiService.log.transports.push(socketTransport);
PlatformService.log.transports.push(socketTransport);
resolve(); resolve();
}); });

View File

@ -7,7 +7,7 @@
import { GadgetComponent, GadgetLog } from "@gadget/api"; import { GadgetComponent, GadgetLog } from "@gadget/api";
export abstract class GadgetService implements GadgetComponent { export abstract class GadgetService implements GadgetComponent {
protected log: GadgetLog; public log: GadgetLog;
constructor() { constructor() {
this.log = new GadgetLog(this); this.log = new GadgetLog(this);

View File

@ -90,7 +90,7 @@ class AgentService extends GadgetService {
}; };
const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => { const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => {
this.log.debug("stream chunk received", { chunk }); // this.log.debug("stream chunk received", { chunk });
switch (chunk.type) { switch (chunk.type) {
case "thinking": case "thinking":

View File

@ -8,6 +8,7 @@ export * from "./lib/log.ts";
export * from "./lib/log-transport.ts"; export * from "./lib/log-transport.ts";
export * from "./lib/log-transport-console.ts"; export * from "./lib/log-transport-console.ts";
export * from "./lib/log-transport-file.ts"; export * from "./lib/log-transport-file.ts";
export * from "./lib/log-transport-socket.ts";
export * from "./lib/log-file.ts"; export * from "./lib/log-file.ts";
/* /*

View File

@ -0,0 +1,30 @@
import { GadgetComponent } from "./component.ts";
import { GadgetLogLevel } from "./log.ts";
import { GadgetLogTransport } from "./log-transport.ts";
export type LogEmitFunction = (
event: string,
timestamp: Date,
component: GadgetComponent,
level: GadgetLogLevel,
message: string,
metadata?: unknown,
) => void;
export class GadgetLogTransportSocket implements GadgetLogTransport {
private emit: LogEmitFunction;
constructor(emit: LogEmitFunction) {
this.emit = emit;
}
async writeLog(
timestamp: Date,
component: GadgetComponent,
level: GadgetLogLevel,
message: string,
metadata?: unknown,
): Promise<void> {
this.emit("log", timestamp, component, level, message, metadata);
}
}