= ({ log, displayFields = [], isContextView, hide
>
{formattedTime || "timestamp missing"}
+ {logLevel && (
+
+
+ {logLevel.label}
+
+
+ )}
): Logs => fields as Logs;
+const logLevels = Object.keys(LOG_LEVEL_COLORS) as LogLevel[];
+
+describe("logLevel utils", () => {
+ describe("getLogLevel", () => {
+ it("returns unknown for empty log", () => {
+ expect(getLogLevel(log({}))).toBe(LOG_LEVEL_UNKNOWN);
+ });
+
+ it("returns unknown for unrecognized field and value", () => {
+ expect(getLogLevel(log({ foo: "bar" }))).toBe(LOG_LEVEL_UNKNOWN);
+ });
+
+ it("returns unknown for numeric value", () => {
+ expect(getLogLevel(log({ level: 30 }))).toBe(LOG_LEVEL_UNKNOWN);
+ });
+
+ it("returns unknown for null value", () => {
+ expect(getLogLevel(log({ level: null }))).toBe(LOG_LEVEL_UNKNOWN);
+ });
+
+ it.each(LOG_LEVEL_FIELDS)("detects level from field \"%s\"", (field) => {
+ expect(getLogLevel(log({ [field]: "error" }))).toBe("error");
+ });
+
+ it("prefers 'level' over 'severity'", () => {
+ expect(getLogLevel(log({ level: "debug", severity: "error" }))).toBe("debug");
+ });
+
+ it("prefers 'severity' over 'status'", () => {
+ expect(getLogLevel(log({ severity: "warn", status: "error" }))).toBe("warn");
+ });
+
+ it("skips unknown value and continues checking next fields", () => {
+ expect(getLogLevel(log({ level: "custom", severity: "error" }))).toBe("error");
+ });
+
+ it("normalizes aliases", () => {
+ expect(getLogLevel(log({ level: "verbose" }))).toBe("debug");
+ expect(getLogLevel(log({ level: "information" }))).toBe("info");
+ expect(getLogLevel(log({ level: "informational" }))).toBe("info");
+ expect(getLogLevel(log({ level: "warning" }))).toBe("warn");
+ expect(getLogLevel(log({ level: "err" }))).toBe("error");
+ expect(getLogLevel(log({ level: "severe" }))).toBe("error");
+ expect(getLogLevel(log({ level: "critical" }))).toBe("fatal");
+ expect(getLogLevel(log({ level: "crit" }))).toBe("fatal");
+ expect(getLogLevel(log({ level: "alert" }))).toBe("fatal");
+ expect(getLogLevel(log({ level: "emergency" }))).toBe("fatal");
+ expect(getLogLevel(log({ level: "emerg" }))).toBe("fatal");
+ expect(getLogLevel(log({ level: "panic" }))).toBe("fatal");
+ });
+
+ it("handles case-insensitive values", () => {
+ expect(getLogLevel(log({ level: "INFO" }))).toBe("info");
+ expect(getLogLevel(log({ level: "Error" }))).toBe("error");
+ expect(getLogLevel(log({ level: "Warn" }))).toBe("warn");
+ });
+
+ it("trims whitespace from value", () => {
+ expect(getLogLevel(log({ level: " info " }))).toBe("info");
+ });
+ });
+
+ describe("getLogLevelColor", () => {
+ it("returns neutral light color for unknown level", () => {
+ expect(getLogLevelColor(log({}))).toBe(LOG_LEVEL_COLORS[LOG_LEVEL_UNKNOWN]);
+ });
+
+ it("returns neutral dark color for unknown level", () => {
+ expect(getLogLevelColor(log({}))).toBe(LOG_LEVEL_COLORS[LOG_LEVEL_UNKNOWN]);
+ });
+
+ it.each(logLevels.filter((level) => level !== LOG_LEVEL_UNKNOWN))(
+ "returns correct light color for level \"%s\"",
+ (level) => {
+ expect(getLogLevelColor(log({ level }))).toBe(LOG_LEVEL_COLORS[level]);
+ },
+ );
+
+ it.each(logLevels.filter((level) => level !== LOG_LEVEL_UNKNOWN))(
+ "returns correct dark color for level \"%s\"",
+ (level) => {
+ expect(getLogLevelColor(log({ level }))).toBe(LOG_LEVEL_COLORS[level]);
+ },
+ );
+
+ it("returns normalized level color", () => {
+ expect(getLogLevelColor(log({ level: "warning" }))).toBe(
+ LOG_LEVEL_COLORS.warn,
+ );
+ expect(getLogLevelColor(log({ level: "critical" }))).toBe(
+ LOG_LEVEL_COLORS.fatal,
+ );
+ expect(getLogLevelColor(log({ level: "information" }))).toBe(
+ LOG_LEVEL_COLORS.info,
+ );
+ });
+ });
+});
diff --git a/app/vmui/packages/vmui/src/utils/logLevel.ts b/app/vmui/packages/vmui/src/utils/logLevel.ts
new file mode 100644
index 0000000000..8fabc92aeb
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/logLevel.ts
@@ -0,0 +1,56 @@
+import { LOG_LEVEL_COLORS, LOG_LEVEL_FIELDS, LOG_LEVEL_UNKNOWN } from "../constants/logLevel";
+import type { Logs } from "../api/types";
+
+type LogLevel = keyof typeof LOG_LEVEL_COLORS;
+type LogLevelColor = (typeof LOG_LEVEL_COLORS)[LogLevel];
+
+const LEVEL_NORMALIZE: Record = {
+ trace: "trace",
+
+ debug: "debug",
+ verbose: "debug",
+
+ info: "info",
+ information: "info",
+ informational: "info",
+ notice: "info",
+
+ warn: "warn",
+ warning: "warn",
+
+ error: "error",
+ err: "error",
+ severe: "error",
+
+ fatal: "fatal",
+ critical: "fatal",
+ crit: "fatal",
+ alert: "fatal",
+ emergency: "fatal",
+ emerg: "fatal",
+ panic: "fatal",
+};
+
+const normalizeLogLevel = (value: unknown): LogLevel | null => {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ return LEVEL_NORMALIZE[value.trim().toLowerCase()] ?? null;
+};
+
+export const getLogLevel = (log: Logs): LogLevel => {
+ for (const field of LOG_LEVEL_FIELDS) {
+ const level = normalizeLogLevel(log[field]);
+
+ if (level) {
+ return level;
+ }
+ }
+
+ return LOG_LEVEL_UNKNOWN;
+};
+
+export const getLogLevelColor = (log: Logs): LogLevelColor => {
+ return LOG_LEVEL_COLORS[getLogLevel(log)];
+};
diff --git a/app/vmui/packages/vmui/src/utils/storage/constants.ts b/app/vmui/packages/vmui/src/utils/storage/constants.ts
index 7afa4d65d0..4da00fd95d 100644
--- a/app/vmui/packages/vmui/src/utils/storage/constants.ts
+++ b/app/vmui/packages/vmui/src/utils/storage/constants.ts
@@ -16,6 +16,7 @@ export const ALL_STORAGE_KEYS = [
"LOGS_TABLE_DRAWER",
"LOGS_FILTER_SIDEBAR_WIDTH",
"LOGS_FILTER_SIDEBAR_HIDDEN",
+ "LOGS_DISABLED_LEVEL_DETECTION"
] as const;
export const TABLE_KEYS = {
diff --git a/docs/victorialogs/CHANGELOG.md b/docs/victorialogs/CHANGELOG.md
index aad272b68d..1c548c53f6 100644
--- a/docs/victorialogs/CHANGELOG.md
+++ b/docs/victorialogs/CHANGELOG.md
@@ -26,6 +26,7 @@ according to the following docs:
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaLogs/blob/master/deployment/docker/rules): add new alerting rules `PersistentQueueRunsOutOfSpaceIn12Hours` and `PersistentQueueRunsOutOfSpaceIn4Hours` for `vlagent` persistent queue capacity. These alerts help users to take proactive actions before `vlagent` starts dropping logs due to insufficient persistent queue space. See [#10193](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10193)
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): remove the `Date format` setting and always display timestamps with nanosecond precision. See [#1161](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1161).
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): rename field `Query time` to `Hits query` on the Hits chart panel. The change makes it clear duration of which query is displayed.
+* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add log level detection for displayed log entries. This feature can be toggled in the Group View settings. See [#1245](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1245).
* BUGFIX: [vlagent](https://docs.victoriametrics.com/victorialogs/vlagent/): hide sensitive values passed via `-remoteWrite.proxyURL` in `/metrics`, `/flags`, and startup logs. Previously these values could be exposed in plain text. See [#1320](https://github.com/VictoriaMetrics/VictoriaLogs/pull/1320).
* BUGFIX: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): sanitize markdown URLs in logs rendered with `markdown parsing` enabled, allowing only `http`, `https`, `mailto`, and `tel` schemes for active links and images. See [#1313](https://github.com/VictoriaMetrics/VictoriaLogs/pull/1313).