Anonymous - 2026-03-18

Originally posted by: eldara-cruncher

Analysis

Root cause

Docker's ServiceLogs API aggregates logs from multiple tasks/nodes. When requesting historical lines (Tail: "200"), Docker returns them in undefined interleaving order — this is documented Docker behavior for multi-replica services.

The current code has two problems:

  1. No timestamps requestedstream.go:51 sets Timestamps: false, so there's no sortable field at all
  2. Append-only insertionupdate.go:46 blindly appends each line as it arrives, preserving Docker's arbitrary order

Current data flow

ServiceLogs(Tail:"200", Follow:true, Timestamps:false)
  → stdcopy demux → scanner → formatLogLineWithNode()
    → channel: "nodename\x00formatted_line"
      → LineMsg → append to m.lines (no sorting)

m.lines []string and m.lineNodes []string have no timestamp field. Lines are formatted display strings with no sortable data.

Fix options

Both require setting Timestamps: true in the Docker API call.

Option A — Two-phase fetch (recommended)

Split the single ServiceLogs call into two:

  1. History phase: Follow: false, Timestamps: true, Tail: "200" — read all lines to EOF, sort by timestamp, bulk-insert into model
  2. Stream phase: Follow: true, Timestamps: true, Since: <latest_ts_from_phase1> — append as they arrive (mostly ordered already)

Changes:

  • stream.go: set Timestamps: true, split goroutine into two sequential API calls
  • messages.go: add InitialBatchMsg carrying pre-sorted batch
  • model.go: add lineTimestamps []time.Time parallel slice
  • update.go: handle InitialBatchMsg by bulk-setting sorted lines, then start streaming
  • formatLogLineWithNode(): parse and strip/keep the RFC3339Nano timestamp

Pros: clean separation, no timing heuristics, streaming path unchanged.
Cons: two API calls, brief gap mitigated by Since overlap + dedup.

Option B — Buffered initial sort (minimal change)

Keep a single ServiceLogs call but buffer the initial burst:

  1. Set Timestamps: true
  2. Detect the initial batch (read until channel drains or count reaches tail)
  3. Sort the batch by parsed timestamp, send as InitialBatchMsg
  4. Remaining lines go through existing LineMsg path

Pros: single API call, no gap risk.
Cons: relies on timing heuristic (~50ms drain timer) to detect "initial batch done".

Timestamp format

With Timestamps: true + Details: true, Docker returns:

com.docker.swarm.node.id=abc,com.docker.swarm.task.id=xyz 2025-03-18T10:15:45.123456789Z actual log message

formatLogLineWithNode() splits on first space to get details vs message. With timestamps enabled, the timestamp becomes the first token of message — needs to be extracted via time.Parse(time.RFC3339Nano, ...).

Timestamp display

Separate decision — should timestamps be visible?

  • Always show — most informative, matches docker service logs -t
  • Never show — cleaner, current behavior
  • Toggle — add a keybinding (e.g. t) to show/hide