feat: machines logs viewer
This commit is contained in:
parent
9cc3c6ffe3
commit
2690db12b5
@ -17,8 +17,11 @@ import uuid
|
|||||||
import asyncio
|
import asyncio
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
import threading
|
||||||
|
|
||||||
api = None
|
api = None
|
||||||
api_task = None
|
api_task = None
|
||||||
@ -124,6 +127,7 @@ async def websocket_handler(request):
|
|||||||
try:
|
try:
|
||||||
# Send initial state to the new client
|
# Send initial state to the new client
|
||||||
await send("status", { 'sid': sid }, sid)
|
await send("status", { 'sid': sid }, sid)
|
||||||
|
await send_first_time_log(sid)
|
||||||
|
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == aiohttp.WSMsgType.ERROR:
|
if msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
@ -136,7 +140,7 @@ async def send(event, data, sid=None):
|
|||||||
try:
|
try:
|
||||||
if sid:
|
if sid:
|
||||||
ws = sockets.get(sid)
|
ws = sockets.get(sid)
|
||||||
if ws and not ws.closed: # Check if the WebSocket connection is open and not closing
|
if ws != None and not ws.closed: # Check if the WebSocket connection is open and not closing
|
||||||
await ws.send_json({ 'event': event, 'data': data })
|
await ws.send_json({ 'event': event, 'data': data })
|
||||||
else:
|
else:
|
||||||
for ws in sockets.values():
|
for ws in sockets.values():
|
||||||
@ -291,4 +295,48 @@ async def update_run_with_output(prompt_id, data):
|
|||||||
})
|
})
|
||||||
|
|
||||||
prompt_server.send_json_original = prompt_server.send_json
|
prompt_server.send_json_original = prompt_server.send_json
|
||||||
prompt_server.send_json = send_json_override.__get__(prompt_server, server.PromptServer)
|
prompt_server.send_json = send_json_override.__get__(prompt_server, server.PromptServer)
|
||||||
|
|
||||||
|
root_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
||||||
|
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
||||||
|
|
||||||
|
last_read_line = 0
|
||||||
|
|
||||||
|
async def watch_file_changes(file_path, callback):
|
||||||
|
global last_read_line
|
||||||
|
last_modified_time = os.stat(file_path).st_mtime
|
||||||
|
while True:
|
||||||
|
time.sleep(1) # sleep for a while to reduce CPU usage
|
||||||
|
modified_time = os.stat(file_path).st_mtime
|
||||||
|
if modified_time != last_modified_time:
|
||||||
|
last_modified_time = modified_time
|
||||||
|
with open(file_path, 'r') as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
if last_read_line > len(lines):
|
||||||
|
last_read_line = 0 # Reset if log file has been rotated
|
||||||
|
new_lines = lines[last_read_line:]
|
||||||
|
last_read_line = len(lines)
|
||||||
|
if new_lines:
|
||||||
|
await callback(''.join(new_lines))
|
||||||
|
|
||||||
|
|
||||||
|
async def send_first_time_log(sid):
|
||||||
|
with open(log_file_path, 'r') as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
await send("LOGS", ''.join(lines), sid)
|
||||||
|
|
||||||
|
async def send_logs_to_websocket(logs):
|
||||||
|
await send("LOGS", logs)
|
||||||
|
|
||||||
|
def start_loop(loop):
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
def run_in_new_thread(coroutine):
|
||||||
|
new_loop = asyncio.new_event_loop()
|
||||||
|
t = threading.Thread(target=start_loop, args=(new_loop,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
||||||
|
|
||||||
|
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
||||||
|
39
prestartup_script.py
Normal file
39
prestartup_script.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import atexit
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
class StreamToLogger(object):
|
||||||
|
def __init__(self, original, logger, log_level):
|
||||||
|
self.original_stdout = original
|
||||||
|
self.logger = logger
|
||||||
|
self.log_level = log_level
|
||||||
|
|
||||||
|
def write(self, buf):
|
||||||
|
self.original_stdout.write(buf)
|
||||||
|
self.original_stdout.flush()
|
||||||
|
for line in buf.rstrip().splitlines():
|
||||||
|
self.logger.log(self.log_level, line.rstrip())
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.original_stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Create a handler that rotates log files every 1MB
|
||||||
|
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
# Store original streams
|
||||||
|
original_stdout = sys.stdout
|
||||||
|
original_stderr = sys.stderr
|
||||||
|
|
||||||
|
# Redirect stdout and stderr to the logger
|
||||||
|
sys.stdout = StreamToLogger(original_stdout, logger, logging.INFO)
|
||||||
|
sys.stderr = StreamToLogger(original_stderr, logger, logging.ERROR)
|
@ -1,9 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import type { getMachines } from "@/server/curdMachine";
|
import type { getMachines } from "@/server/curdMachine";
|
||||||
import { Check, CircleOff, SatelliteDish } from "lucide-react";
|
import { Check, CircleOff, SatelliteDish } from "lucide-react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
@ -16,6 +24,12 @@ type State = {
|
|||||||
data: any;
|
data: any;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
|
logs: {
|
||||||
|
machine_id: string;
|
||||||
|
logs: string;
|
||||||
|
timestamp: number;
|
||||||
|
}[];
|
||||||
|
addLogs: (id: string, logs: string) => void;
|
||||||
addData: (
|
addData: (
|
||||||
id: string,
|
id: string,
|
||||||
json: {
|
json: {
|
||||||
@ -27,6 +41,13 @@ type State = {
|
|||||||
|
|
||||||
export const useStore = create<State>((set) => ({
|
export const useStore = create<State>((set) => ({
|
||||||
data: [],
|
data: [],
|
||||||
|
logs: [],
|
||||||
|
addLogs(id, logs) {
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
logs: [...state.logs, { machine_id: id, logs, timestamp: Date.now() }],
|
||||||
|
}));
|
||||||
|
},
|
||||||
addData: (id, json) =>
|
addData: (id, json) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@ -54,7 +75,14 @@ function MachineWS({
|
|||||||
}: {
|
}: {
|
||||||
machine: Awaited<ReturnType<typeof getMachines>>[0];
|
machine: Awaited<ReturnType<typeof getMachines>>[0];
|
||||||
}) {
|
}) {
|
||||||
const { addData } = useStore();
|
const { addData, addLogs } = useStore();
|
||||||
|
const logs = useStore((x) =>
|
||||||
|
x.logs
|
||||||
|
.filter((p) => p.machine_id === machine.id)
|
||||||
|
.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
|
);
|
||||||
|
const [sid, setSid] = useState("");
|
||||||
|
|
||||||
const wsEndpoint = machine.endpoint.replace(/^http/, "ws");
|
const wsEndpoint = machine.endpoint.replace(/^http/, "ws");
|
||||||
const { lastMessage, readyState } = useWebSocket(
|
const { lastMessage, readyState } = useWebSocket(
|
||||||
`${wsEndpoint}/comfyui-deploy/ws`,
|
`${wsEndpoint}/comfyui-deploy/ws`,
|
||||||
@ -62,6 +90,9 @@ function MachineWS({
|
|||||||
shouldReconnect: () => true,
|
shouldReconnect: () => true,
|
||||||
reconnectAttempts: 20,
|
reconnectAttempts: 20,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
|
// queryParams: {
|
||||||
|
// clientId: sid,
|
||||||
|
// },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -75,20 +106,72 @@ function MachineWS({
|
|||||||
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
|
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
|
||||||
}[readyState];
|
}[readyState];
|
||||||
|
|
||||||
|
const container = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastMessage?.data) return;
|
if (!lastMessage?.data) return;
|
||||||
|
|
||||||
const message = JSON.parse(lastMessage.data);
|
const message = JSON.parse(lastMessage.data);
|
||||||
console.log(message.event, message);
|
console.log(message.event, message);
|
||||||
|
|
||||||
|
if (message.data.sid) {
|
||||||
|
setSid(message.data.sid);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.data?.prompt_id) {
|
if (message.data?.prompt_id) {
|
||||||
addData(message.data.prompt_id, message);
|
addData(message.data.prompt_id, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.event === "LOGS") {
|
||||||
|
addLogs(machine.id, message.data);
|
||||||
|
}
|
||||||
}, [lastMessage]);
|
}, [lastMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// console.log(logs.length, container.current);
|
||||||
|
if (container.current) {
|
||||||
|
const scrollHeight = container.current.scrollHeight;
|
||||||
|
|
||||||
|
container.current.scrollTo({
|
||||||
|
top: scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [logs.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className="text-sm flex gap-2 font-normal" variant="outline">
|
<Dialog>
|
||||||
{machine.name} {connectionStatus}
|
<DialogTrigger asChild className="">
|
||||||
</Badge>
|
<Badge className="text-sm flex gap-2 font-normal" variant="outline">
|
||||||
|
{machine.name} {connectionStatus}
|
||||||
|
</Badge>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-3xl max-h-full">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Machine Logs</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
You can view your run's outputs here
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div
|
||||||
|
ref={(ref) => {
|
||||||
|
if (!container.current && ref) {
|
||||||
|
const scrollHeight = ref.scrollHeight;
|
||||||
|
|
||||||
|
ref.scrollTo({
|
||||||
|
top: scrollHeight,
|
||||||
|
behavior: "instant",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
container.current = ref;
|
||||||
|
}}
|
||||||
|
className="flex flex-col text-xs p-2 overflow-y-scroll max-h-[400px] whitespace-break-spaces"
|
||||||
|
>
|
||||||
|
{logs.map((x, i) => (
|
||||||
|
<div key={i}>{x.logs}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import { RunInputs } from "@/components/RunInputs";
|
|
||||||
import { LiveStatus } from "./LiveStatus";
|
import { LiveStatus } from "./LiveStatus";
|
||||||
|
import { RunInputs } from "@/components/RunInputs";
|
||||||
import { RunOutputs } from "@/components/RunOutputs";
|
import { RunOutputs } from "@/components/RunOutputs";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -22,10 +21,7 @@ export async function RunDisplay({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger
|
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||||
asChild
|
|
||||||
className="appearance-none hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{run.number}</TableCell>
|
<TableCell>{run.number}</TableCell>
|
||||||
<TableCell className="font-medium">{run.machine?.name}</TableCell>
|
<TableCell className="font-medium">{run.machine?.name}</TableCell>
|
||||||
@ -42,7 +38,7 @@ export async function RunDisplay({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="max-h-96 overflow-y-scroll">
|
<div className="max-h-96 overflow-y-scroll">
|
||||||
<RunInputs run={run}/>
|
<RunInputs run={run} />
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RunOutputs run_id={run.id} />
|
<RunOutputs run_id={run.id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -39,7 +39,7 @@ export async function RunsTable(props: { workflow_id: string }) {
|
|||||||
export async function DeploymentsTable(props: { workflow_id: string }) {
|
export async function DeploymentsTable(props: { workflow_id: string }) {
|
||||||
const allRuns = await findAllDeployments(props.workflow_id);
|
const allRuns = await findAllDeployments(props.workflow_id);
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-fit lg:h-[400px] w-full">
|
<div className="overflow-auto h-fit w-full">
|
||||||
<Table className="">
|
<Table className="">
|
||||||
<TableCaption>A list of your deployments</TableCaption>
|
<TableCaption>A list of your deployments</TableCaption>
|
||||||
<TableHeader className="bg-background top-0 sticky">
|
<TableHeader className="bg-background top-0 sticky">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user