diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 732ff875..3c0cdddd 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -1,9 +1,15 @@ import { Type, Static } from "@sinclair/typebox"; import { JsonString, StringEnum, StringKey, plainJson } from "./composites.js"; -import { levels } from "../logging.js"; +import { type LogEntry, levels } from "../logging.js"; import { ControllerConfig, HostConfig } from "../config/index.js"; import { ExternalFactorioVersionSchema, LatestReleasesSchema } from "../external/index.js"; +const LogEntrySchema = Type.Object({ + "level": StringKey(levels), + "message": Type.String(), + "timestamp": Type.String(), +}); + export class ControllerStopRequest { declare ["constructor"]: typeof ControllerStopRequest; static type = "request" as const; @@ -236,11 +242,11 @@ export class LogQueryRequest { static Response = class Response { constructor( - public log: object[], + public log: LogEntry[], ) { } static jsonSchema = Type.Object({ - "log": Type.Array(Type.Object({})), + "log": Type.Array(LogEntrySchema), }); static fromJSON(json: Static) { @@ -256,14 +262,11 @@ export class LogMessageEvent { static dst = ["controller", "control"] as const; constructor( - public info: { level: keyof typeof levels, message: string }, + public info: LogEntry, ) { } static jsonSchema = Type.Object({ - "info": Type.Object({ - "level": StringKey(levels), - "message": Type.String(), - }), + "info": LogEntrySchema, }); static fromJSON(json: Static) { diff --git a/packages/lib/src/logging.ts b/packages/lib/src/logging.ts index 275693ec..473888fd 100644 --- a/packages/lib/src/logging.ts +++ b/packages/lib/src/logging.ts @@ -93,6 +93,18 @@ export class WebConsoleFormat { } } +/** + * Log entry as produced by the logger and sent over links + */ +export interface LogEntry { + level: keyof typeof levels; + message: string; + /** ISO 8601 time the entry was logged at. */ + timestamp: string; + /** Other fields like host_id, instance_id, plugin and parsed vary by source. */ + [key: string]: unknown; +} + /** * Filter object for logs */ diff --git a/packages/web_ui/src/components/LogConsole.tsx b/packages/web_ui/src/components/LogConsole.tsx index 0a201f0c..8b12247f 100644 --- a/packages/web_ui/src/components/LogConsole.tsx +++ b/packages/web_ui/src/components/LogConsole.tsx @@ -54,16 +54,28 @@ function formatParsedOutput(parsed: Parsed) { return <>{time}{info}{parsed.message}; } -type Info = { - level: keyof typeof lib.levels; - message: string; +type Info = lib.LogEntry & { parsed?: Parsed; }; +/** + * Format an ISO 8601 timestamp as local YYYY-MM-DD HH:MM:SS + */ +function formatTime(timestamp: string) { + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) { + return timestamp; + } + const pad = (value: number) => String(value).padStart(2, "0"); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + function formatLog(info: Info, key: number): ReactElement { + let time = {formatTime(info.timestamp)}; let level = {info.level}; let message = info.level === "server" && info.parsed ? formatParsedOutput(info.parsed) : info.message; - return [{level}] {message}
; + return {time} [{level}] {message}
; } /** @@ -186,10 +198,10 @@ export default function LogConsole(props: LogConsoleProps) { setPastLines([]); } - function logHandler(info: Info) { - if (!props.actionsOnly || passesActionsOnly(info)) { + function logHandler(info: lib.LogEntry) { + if (!props.actionsOnly || passesActionsOnly(info as Info)) { setLines(currentLines => currentLines.concat( - [formatLog(info, currentLines.length)] + [formatLog(info as Info, currentLines.length)] )); } } diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index 5553ceff..b478b8f7 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -195,6 +195,10 @@ body { white-space: pre-wrap; } +.instance-console .log-time { + color: #808080; +} + .instance-console .factorio-time { color: #afaf1f; } diff --git a/packages/web_ui/src/util/websocket.ts b/packages/web_ui/src/util/websocket.ts index b98f0a72..3e51511c 100644 --- a/packages/web_ui/src/util/websocket.ts +++ b/packages/web_ui/src/util/websocket.ts @@ -11,7 +11,7 @@ type saveListHandler = (saveListEvent: lib.SaveDetails) => void; type modPackHandler = (modPack: lib.ModPack) => void; type modInfoHandler = (modInfo: lib.ModInfo) => void; type userHandler = (rawUser: lib.UserDetails) => void; -type logHandler = (info: { level: keyof typeof lib.levels, message: string }) => void; +type logHandler = (info: lib.LogEntry) => void; /** * Connector for control connection to controller