Show timestamps in the web UI log console (#1137)

Add a LogEntry type for log entries sent over links and returned by
log queries. It names the fields every entry has (level, message,
timestamp) and leaves the rest as unknown.

The console now prefixes each line with the entry's time in the
browser's local time zone, which makes it possible to tell when an
error happened after the fact.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BastiaanandClaude Opus 5.5 authored and GitHub committed 2026-10-25 19:57:16 +00:00
1 parent ba518faa05
commit b474573633
5 files changed
+47 -16

No files matched your search

+11 -8
View File
@@ -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<typeof this.jsonSchema>) {
@@ -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<typeof this.jsonSchema>) {
+12
View File
@@ -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
*/
+19 -7
View File
@@ -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 = <span className="log-time">{formatTime(info.timestamp)}</span>;
let level = <span className={`log-${info.level}`}>{info.level}</span>;
let message = info.level === "server" && info.parsed ? formatParsedOutput(info.parsed) : info.message;
return <span key={key}>[{level}] {message}<br/></span>;
return <span key={key}>{time} [{level}] {message}<br/></span>;
}
/**
@@ -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)]
));
}
}
+4
View File
@@ -195,6 +195,10 @@ body {
white-space: pre-wrap;
}
.instance-console .log-time {
color: #808080;
}
.instance-console .factorio-time {
color: #afaf1f;
}
+1 -1
View File
@@ -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