mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
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:
1 parent
ba518faa05
commit
b474573633
5 files changed
+47
-16
No files matched your search
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,10 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.instance-console .log-time {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.instance-console .factorio-time {
|
||||
color: #afaf1f;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user