Skip to content

Log tab displays no messages: refactor logging to use JSON formatter instead of parsing formatted text - #254

Merged
jbrinkman merged 2 commits into
mainfrom
spec/issue-252-28856
Jul 15, 2026
Merged

Log tab displays no messages: refactor logging to use JSON formatter instead of parsing formatted text#254
jbrinkman merged 2 commits into
mainfrom
spec/issue-252-28856

Conversation

@jbrinkman

@jbrinkman jbrinkman commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

This PR refactors the logging architecture to fix the issue where logs were not appearing in the log tab display. The root cause was a flawed architecture where the logger formatted logs as text, then loggingMultiWriter attempted to parse that formatted text back into structured data with incorrect field indexing.

Changes Made

Core Architecture Fix

  • Configure JSON Formatter: Set log.SetFormatter(log.JSONFormatter) in both Initialize() and Activate() functions
  • Replace Text Parsing: Updated loggingMultiWriter.Write() to parse JSON instead of formatted text
  • Add JSON Support: Added encoding/json import and mapStringToLogLevel() helper function
  • Structured Pipeline: Maintain structured data from log call → JSON format → JSON parsing → ring buffer

Key Files Modified

  • internal/logging/logger.go: Added JSON formatter configuration (2 lines)
  • internal/tui/tui.go: Replaced text parsing with JSON parsing (~70 lines changed)

Architecture Before (Flawed)

logging.Info("msg", "key", "value")
  → charmbracelet/log formats to text: "2026/07/15 07:20:42 INFO msg key=value"
  → loggingMultiWriter.Write([]byte) receives formatted string
  → attempts to parse fields[0] as level (actually date)
  → parsing fails, entries are malformed
  → ring buffer gets incorrect data
  → log tab displays nothing

Architecture After (Fixed)

logging.Info("msg", "key", "value")
  → charmbracelet/log formats to JSON: {"time":"2026-07-15T07:20:42-04:00","level":"info","message":"msg","key":"value"}
  → loggingMultiWriter.Write([]byte) receives JSON
  → json.Unmarshal() parses structured data
  → extract level="info", message="msg", metadata={"key":"value"}
  → ringBuffer.Add(log.InfoLevel, "msg", "key", "value")
  → log tab displays correctly formatted entry

Quality Assurance

All automated QA checks pass:

  • Formatting: task fmt:check (go fmt)
  • Template Sync: task sync:check (template synchronization)
  • Linting: task lint (go vet static analysis)
  • Tests: task test (22/22 logging tests, 52/52 tui tests)
  • Build: task build (binary compilation)

Breaking Changes

Log File Format: Log files now use JSON format instead of text format. This provides structured, machine-readable logs that can be easily parsed by tools like jq or log aggregators.

Example log file output:

{"time":"2026-07-15T09:30:00-04:00","level":"info","message":"Watcher started","repo":"owner/name"}
{"time":"2026-07-15T09:30:01-04:00","level":"debug","message":"Polling GitHub","interval":"5m"}

For human-readable viewing, users can pipe through jq:

tail -f .kiro-krew/kiro-krew.log | jq -r '[.time, .level, .message] | @tsv'

Validation

Automated Validation: All technical criteria validated successfully
Manual Validation Required: TUI display testing (log tab appearance, color-coded levels, metadata display)

Testing

To verify the fix:

  1. Build and run: task build && ./dist/kiro-krew
  2. Start logging activity: watch start
  3. Open log tab and verify entries appear with proper formatting
  4. Check log file format: cat .kiro-krew/kiro-krew.log

Closes #252

Summary by CodeRabbit

  • New Features

    • Improved structured logging across the application.
    • TUI log entries now preserve timestamps, levels, messages, and additional metadata.
    • Log files consistently use JSON formatting for easier parsing and integration.
  • Bug Fixes

    • Fixed issues caused by interpreting formatted log text as structured data.
    • Improved handling of log levels and malformed log entries without interrupting output.

…tted text

- Configure JSON formatter in logger Initialize() and Activate() functions
- Replace text parsing with JSON parsing in loggingMultiWriter.Write()
- Add mapStringToLogLevel() helper to map string levels to constants
- Maintain structured data throughout logging pipeline
- Handle JSON parsing errors gracefully without failing writes
- Log files now use JSON format for machine-readable structured logs

Fixes log tab display issue where logs were not appearing due to
flawed text parsing architecture. The system now uses structured
JSON throughout the pipeline eliminating parsing errors.

Closes #252

Signed-off-by: Joseph Brinkman <joe.brinkman@improving.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jbrinkman, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 161b9404-d1dd-4174-ab1d-630dc2bf2d48

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee8f92 and e977ed5.

📒 Files selected for processing (1)
  • internal/tui/tui.go
📝 Walkthrough

Walkthrough

The logging pipeline now emits JSON from both logger initialization paths. The TUI writer parses JSON payloads, maps levels, preserves metadata, and adds structured entries to the ring buffer while continuing to write file output.

Changes

JSON logging pipeline

Layer / File(s) Summary
Configure JSON logger output
.kiro-krew/specs/issue-252-logging-json-formatter.md, internal/logging/logger.go
The logger specification documents the JSON pipeline, and both initialization paths configure log.JSONFormatter.
Parse JSON into ring-buffer entries
internal/tui/tui.go
loggingMultiWriter.Write unmarshals JSON, extracts time, level, and message, preserves remaining fields, maps levels to clog.Level, and adds structured ring-buffer entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main logging refactor and log-tab fix.
Linked Issues check ✅ Passed The code matches #252 by switching to JSON formatting and parsing while preserving level and metadata in the ring buffer.
Out of Scope Changes check ✅ Passed No clear out-of-scope code changes are present beyond a supporting design spec for the same logging refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spec/issue-252-28856

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/tui/tui.go (1)

1321-1361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused struct and simplify field extraction.

The entry struct and its JSON tags are unused because the code manually extracts fields from the rawEntry map. Furthermore, the Time field is extracted but never used since ringBuffer.Add generates its own timestamp. You can simplify this by extracting level and message directly into local string variables.

♻️ Proposed refactor to remove the unused struct
-	// Parse JSON structured log entry
-	var entry struct {
-		Time    string                 `json:"time"`
-		Level   string                 `json:"level"`
-		Message string                 `json:"message"`
-		Fields  map[string]interface{} `json:"-"` // Capture remaining fields
-	}
-
-	// Unmarshal into a map first to capture all fields
+	// Unmarshal into a map to capture all fields
 	var rawEntry map[string]interface{}
 	if err := json.Unmarshal(p, &rawEntry); err != nil {
 		// JSON parse failed, but file write succeeded - don't fail the write
 		// This could happen during transition or with malformed input
 		return n, nil
 	}
 
-	// Extract known fields
-	if t, ok := rawEntry["time"].(string); ok {
-		entry.Time = t
-	}
-	if l, ok := rawEntry["level"].(string); ok {
-		entry.Level = l
-	}
-	if m, ok := rawEntry["message"].(string); ok {
-		entry.Message = m
-	}
+	var levelStr, message string
+	if l, ok := rawEntry["level"].(string); ok {
+		levelStr = l
+	}
+	if m, ok := rawEntry["message"].(string); ok {
+		message = m
+	}
 
 	// Map string level to log.Level constant
-	level := mapStringToLogLevel(entry.Level)
+	level := mapStringToLogLevel(levelStr)
 
 	// Convert remaining fields to key-value pairs (exclude time, level, message)
 	var keyvals []interface{}
 	for k, v := range rawEntry {
 		if k != "time" && k != "level" && k != "message" {
 			keyvals = append(keyvals, k, v)
 		}
 	}
 
 	// Add structured entry to ring buffer
-	lmw.ringBuffer.Add(level, entry.Message, keyvals...)
+	lmw.ringBuffer.Add(level, message, keyvals...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/tui.go` around lines 1321 - 1361, Remove the unused entry struct
and its JSON tags from the log parsing flow. In the code around rawEntry and
lmw.ringBuffer.Add, extract only the level and message values into local string
variables, omit time extraction entirely, and pass those locals through the
existing mapStringToLogLevel and ringBuffer.Add calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/tui/tui.go`:
- Around line 1321-1361: Remove the unused entry struct and its JSON tags from
the log parsing flow. In the code around rawEntry and lmw.ringBuffer.Add,
extract only the level and message values into local string variables, omit time
extraction entirely, and pass those locals through the existing
mapStringToLogLevel and ringBuffer.Add calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9acaa910-dbeb-475e-8b94-74eca363c4cc

📥 Commits

Reviewing files that changed from the base of the PR and between 863e3ae and 0ee8f92.

📒 Files selected for processing (3)
  • .kiro-krew/specs/issue-252-logging-json-formatter.md
  • internal/logging/logger.go
  • internal/tui/tui.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the “log tab displays no messages” issue by switching the logging pipeline to JSON output (via charmbracelet/log’s JSON formatter) and updating the TUI’s loggingMultiWriter to parse structured JSON instead of attempting to re-parse formatted text.

Changes:

  • Configure the global logger to emit JSON (log.JSONFormatter) during initialization and activation.
  • Refactor loggingMultiWriter.Write() to json.Unmarshal log entries and forward structured level/message/metadata into the ring buffer.
  • Add a design/spec document capturing the rationale and intended architecture for issue #252.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
internal/tui/tui.go Replace fragile text parsing with JSON parsing and map JSON log level/message/fields into the ring buffer.
internal/logging/logger.go Configure the global logger to use log.JSONFormatter so downstream sinks can parse reliably.
.kiro-krew/specs/issue-252-logging-json-formatter.md Document the design, acceptance criteria, and validation strategy for the JSON logging refactor.
Comments suppressed due to low confidence (1)

internal/logging/logger.go:114

  • Activate() only sets JSONFormatter when globalLogger is nil. If the logger already exists (e.g., created elsewhere with a different formatter), activating handlers could reintroduce non-JSON output and break the TUI’s JSON parsing. Setting the formatter in the existing-logger branch makes activation robust and keeps the log pipeline consistently structured.
	} else {
		// Add new handlers to existing logger
		globalLogger.SetOutput(output)
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/tui/tui.go Outdated
Comment on lines +1322 to +1346
var entry struct {
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
Fields map[string]interface{} `json:"-"` // Capture remaining fields
}

// Unmarshal into a map first to capture all fields
var rawEntry map[string]interface{}
if err := json.Unmarshal(p, &rawEntry); err != nil {
// JSON parse failed, but file write succeeded - don't fail the write
// This could happen during transition or with malformed input
return n, nil
}

// Extract known fields
if t, ok := rawEntry["time"].(string); ok {
entry.Time = t
}
if l, ok := rawEntry["level"].(string); ok {
entry.Level = l
}
if m, ok := rawEntry["message"].(string); ok {
entry.Message = m
}
Comment thread internal/tui/tui.go
Comment on lines +1352 to 1357
var keyvals []interface{}
for k, v := range rawEntry {
if k != "time" && k != "level" && k != "message" {
keyvals = append(keyvals, k, v)
}
}
- Remove unused entry struct and its Time/Fields members
- Extract only level and message into local variables
- Normalize float64 values that are integers back to int64

Addresses Copilot and CodeRabbit review feedback on PR #254:
- Eliminates dead code (unused struct fields)
- Preserves original metadata types for integer values
- Simplifies the parsing flow

Signed-off-by: Joseph Brinkman <joe.brinkman@improving.com>
@jbrinkman
jbrinkman merged commit a5610a5 into main Jul 15, 2026
2 checks passed
@jbrinkman
jbrinkman deleted the spec/issue-252-28856 branch July 15, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Log tab displays no messages: refactor logging to use JSON formatter instead of parsing formatted text

2 participants