git.iko.soy/parseable-macos
on master help sign in
Add severity-based row background tinting to log table (#28)

Tint entire log rows based on their severity/level value to make errors and warnings visually stand out at a glance. Supports a wide range of column names (level, severity, log_level, severity_text, priority, p_level, etc.) and value formats (textual like error/warn/ fatal/critical/panic/emerg, and numeric RFC 5424 syslog codes 0-7). Fatal/critical rows get a stronger red tint, error rows a subtle red tint, and warning rows a subtle orange tint. Info, debug, trace, and unknown levels remain untinted. Selection highlight takes priority over severity tinting. Also broadens the text color detection for severity columns to use the same comprehensive column name set. https://claude.ai/code/session_01Q5gW3CZFsF7dFhEtBDeQfW Co-authored-by: Claude <noreply@anthropic.com>

commiteb5d360c0b946d2d184f9e4a8a6d87672f0d21e6 authoriko <mail@iko.soy> dateSun 15 Feb 2026, 19:26 (8 months ago) parent8bc3961
2 files changed+247−6
ParseableViewer/Views/LogTableView.swift+106−6 ParseableViewerTests/LogTableHelperTests.swift+141
ParseableViewer/Views/LogTableView.swift+106−6
@@ -48,6 +48,100 @@ func levelColor(for value: String) -> Color {
4848 }
4949}
5050
51+// MARK: - Severity Row Tinting
52+
53+/// Semantic severity levels for row background tinting.
54+enum SeverityLevel {
55+ case fatal
56+ case error
57+ case warning
58+ case info
59+ case debug
60+ case trace
61+ case unknown
62+}
63+
64+/// Column names (lowercased) that commonly hold a severity / log-level value.
65+private let severityColumnNames: Set<String> = [
66+ "level", "severity", "log_level", "loglevel", "log.level",
67+ "p_level", "severity_text", "levelname", "log_severity",
68+ "priority", "syslog_severity", "msg_severity", "verbosity",
69+ "otel.severity", "severity_number",
70+]
71+
72+/// Maps a raw severity string to a ``SeverityLevel``.
73+func parseSeverity(from value: String) -> SeverityLevel {
74+ let lower = value.trimmingCharacters(in: .whitespaces).lowercased()
75+
76+ // Try well-known textual levels first.
77+ switch lower {
78+ // Fatal / Critical
79+ case "fatal", "critical", "panic", "emerg", "emergency", "alert", "crit":
80+ return .fatal
81+ // Error
82+ case "error", "err", "failure", "fail", "severe":
83+ return .error
84+ // Warning
85+ case "warn", "warning", "caution":
86+ return .warning
87+ // Info / Notice
88+ case "info", "information", "informational", "notice":
89+ return .info
90+ // Debug
91+ case "debug", "dbg", "verbose":
92+ return .debug
93+ // Trace
94+ case "trace", "finest", "finer", "fine", "all":
95+ return .trace
96+ default:
97+ break
98+ }
99+
100+ // Try numeric syslog severity (RFC 5424): 0=Emergency … 7=Debug.
101+ if let num = Int(lower) {
102+ switch num {
103+ case 0...1: return .fatal // Emergency, Alert
104+ case 2: return .fatal // Critical
105+ case 3: return .error // Error
106+ case 4: return .warning // Warning
107+ case 5: return .info // Notice
108+ case 6: return .info // Informational
109+ case 7: return .debug // Debug
110+ default: break
111+ }
112+ }
113+
114+ return .unknown
115+}
116+
117+/// Extracts the severity level from a log record by checking common column names.
118+func extractSeverity(from record: LogRecord) -> SeverityLevel {
119+ for (key, value) in record {
120+ if severityColumnNames.contains(key.lowercased()) {
121+ let str: String
122+ switch value {
123+ case .string(let s): str = s
124+ case .int(let i): str = String(i)
125+ default: continue
126+ }
127+ let level = parseSeverity(from: str)
128+ if level != .unknown { return level }
129+ }
130+ }
131+ return .unknown
132+}
133+
134+/// Returns a subtle background tint color for a severity level.
135+/// Info, debug, trace, and unknown levels return `nil` (no tint).
136+func severityRowTint(for level: SeverityLevel) -> Color? {
137+ switch level {
138+ case .fatal: return Color.red.opacity(0.12)
139+ case .error: return Color.red.opacity(0.07)
140+ case .warning: return Color.orange.opacity(0.07)
141+ case .info, .debug, .trace, .unknown: return nil
142+ }
143+}
144+
51145// MARK: - LogTableView
52146
53147struct LogTableView: View {
@@ -470,16 +564,22 @@ struct LogRowView: View {
470564 }
471565 }
472566 }
473− .background(
474− isSelected
475− ? Color.accentColor.opacity(0.2)
476− : (isAlternate ? Color.primary.opacity(0.02) : Color.clear)
477− )
567+ .background {
568+ if isSelected {
569+ Color.accentColor.opacity(0.2)
570+ } else if let tint = severityRowTint(for: extractSeverity(from: record)) {
571+ tint
572+ } else if isAlternate {
573+ Color.primary.opacity(0.02)
574+ } else {
575+ Color.clear
576+ }
577+ }
478578 }
479579
480580 private func colorForValue(column: String, value: JSONValue?) -> Color {
481581 guard let value else { return .secondary }
482− if column == "level" || column == "severity" || column == "log_level" {
582+ if severityColumnNames.contains(column.lowercased()) {
483583 return levelColor(for: value.displayString)
484584 }
485585 return .primary
ParseableViewerTests/LogTableHelperTests.swift+141
@@ -82,4 +82,145 @@ final class LogTableHelperTests: XCTestCase {
8282 XCTAssertEqual(levelColor(for: "unknown"), .primary)
8383 XCTAssertEqual(levelColor(for: ""), .primary)
8484 }
85+
86+ // MARK: - parseSeverity
87+
88+ func testParseSeverityFatal() {
89+ XCTAssertEqual(parseSeverity(from: "fatal"), .fatal)
90+ XCTAssertEqual(parseSeverity(from: "CRITICAL"), .fatal)
91+ XCTAssertEqual(parseSeverity(from: "panic"), .fatal)
92+ XCTAssertEqual(parseSeverity(from: "emerg"), .fatal)
93+ XCTAssertEqual(parseSeverity(from: "EMERGENCY"), .fatal)
94+ XCTAssertEqual(parseSeverity(from: "alert"), .fatal)
95+ XCTAssertEqual(parseSeverity(from: "crit"), .fatal)
96+ }
97+
98+ func testParseSeverityError() {
99+ XCTAssertEqual(parseSeverity(from: "error"), .error)
100+ XCTAssertEqual(parseSeverity(from: "ERROR"), .error)
101+ XCTAssertEqual(parseSeverity(from: "err"), .error)
102+ XCTAssertEqual(parseSeverity(from: "failure"), .error)
103+ XCTAssertEqual(parseSeverity(from: "FAIL"), .error)
104+ XCTAssertEqual(parseSeverity(from: "severe"), .error)
105+ }
106+
107+ func testParseSeverityWarning() {
108+ XCTAssertEqual(parseSeverity(from: "warn"), .warning)
109+ XCTAssertEqual(parseSeverity(from: "WARNING"), .warning)
110+ XCTAssertEqual(parseSeverity(from: "caution"), .warning)
111+ }
112+
113+ func testParseSeverityInfo() {
114+ XCTAssertEqual(parseSeverity(from: "info"), .info)
115+ XCTAssertEqual(parseSeverity(from: "INFO"), .info)
116+ XCTAssertEqual(parseSeverity(from: "information"), .info)
117+ XCTAssertEqual(parseSeverity(from: "informational"), .info)
118+ XCTAssertEqual(parseSeverity(from: "notice"), .info)
119+ }
120+
121+ func testParseSeverityDebug() {
122+ XCTAssertEqual(parseSeverity(from: "debug"), .debug)
123+ XCTAssertEqual(parseSeverity(from: "DEBUG"), .debug)
124+ XCTAssertEqual(parseSeverity(from: "dbg"), .debug)
125+ XCTAssertEqual(parseSeverity(from: "verbose"), .debug)
126+ }
127+
128+ func testParseSeverityTrace() {
129+ XCTAssertEqual(parseSeverity(from: "trace"), .trace)
130+ XCTAssertEqual(parseSeverity(from: "TRACE"), .trace)
131+ XCTAssertEqual(parseSeverity(from: "finest"), .trace)
132+ XCTAssertEqual(parseSeverity(from: "finer"), .trace)
133+ XCTAssertEqual(parseSeverity(from: "fine"), .trace)
134+ XCTAssertEqual(parseSeverity(from: "all"), .trace)
135+ }
136+
137+ func testParseSeverityNumericSyslog() {
138+ // RFC 5424 severity codes
139+ XCTAssertEqual(parseSeverity(from: "0"), .fatal) // Emergency
140+ XCTAssertEqual(parseSeverity(from: "1"), .fatal) // Alert
141+ XCTAssertEqual(parseSeverity(from: "2"), .fatal) // Critical
142+ XCTAssertEqual(parseSeverity(from: "3"), .error) // Error
143+ XCTAssertEqual(parseSeverity(from: "4"), .warning) // Warning
144+ XCTAssertEqual(parseSeverity(from: "5"), .info) // Notice
145+ XCTAssertEqual(parseSeverity(from: "6"), .info) // Informational
146+ XCTAssertEqual(parseSeverity(from: "7"), .debug) // Debug
147+ }
148+
149+ func testParseSeverityUnknown() {
150+ XCTAssertEqual(parseSeverity(from: ""), .unknown)
151+ XCTAssertEqual(parseSeverity(from: "something"), .unknown)
152+ XCTAssertEqual(parseSeverity(from: "99"), .unknown)
153+ }
154+
155+ func testParseSeverityWhitespace() {
156+ XCTAssertEqual(parseSeverity(from: " error "), .error)
157+ XCTAssertEqual(parseSeverity(from: " WARN "), .warning)
158+ }
159+
160+ // MARK: - extractSeverity
161+
162+ func testExtractSeverityFromLevelColumn() {
163+ let record: LogRecord = ["level": .string("error"), "message": .string("boom")]
164+ XCTAssertEqual(extractSeverity(from: record), .error)
165+ }
166+
167+ func testExtractSeverityFromSeverityColumn() {
168+ let record: LogRecord = ["severity": .string("warning"), "msg": .string("watch out")]
169+ XCTAssertEqual(extractSeverity(from: record), .warning)
170+ }
171+
172+ func testExtractSeverityFromSeverityTextColumn() {
173+ let record: LogRecord = ["severity_text": .string("FATAL")]
174+ XCTAssertEqual(extractSeverity(from: record), .fatal)
175+ }
176+
177+ func testExtractSeverityFromLogLevelColumn() {
178+ let record: LogRecord = ["log_level": .string("info")]
179+ XCTAssertEqual(extractSeverity(from: record), .info)
180+ }
181+
182+ func testExtractSeverityFromNumericValue() {
183+ let record: LogRecord = ["priority": .int(3)]
184+ XCTAssertEqual(extractSeverity(from: record), .error)
185+ }
186+
187+ func testExtractSeverityNoSeverityColumn() {
188+ let record: LogRecord = ["message": .string("hello"), "timestamp": .string("now")]
189+ XCTAssertEqual(extractSeverity(from: record), .unknown)
190+ }
191+
192+ func testExtractSeverityUnrecognizedValue() {
193+ let record: LogRecord = ["level": .string("custom_level")]
194+ XCTAssertEqual(extractSeverity(from: record), .unknown)
195+ }
196+
197+ // MARK: - severityRowTint
198+
199+ func testSeverityRowTintFatalReturnsColor() {
200+ XCTAssertNotNil(severityRowTint(for: .fatal))
201+ }
202+
203+ func testSeverityRowTintErrorReturnsColor() {
204+ XCTAssertNotNil(severityRowTint(for: .error))
205+ }
206+
207+ func testSeverityRowTintWarningReturnsColor() {
208+ XCTAssertNotNil(severityRowTint(for: .warning))
209+ }
210+
211+ func testSeverityRowTintInfoReturnsNil() {
212+ XCTAssertNil(severityRowTint(for: .info))
213+ }
214+
215+ func testSeverityRowTintDebugReturnsNil() {
216+ XCTAssertNil(severityRowTint(for: .debug))
217+ }
218+
219+ func testSeverityRowTintTraceReturnsNil() {
220+ XCTAssertNil(severityRowTint(for: .trace))
221+ }
222+
223+ func testSeverityRowTintUnknownReturnsNil() {
224+ XCTAssertNil(severityRowTint(for: .unknown))
225+ }
85226}