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:
No timestamps requested — stream.go:51 sets Timestamps: false, so there's no sortable field at all
Append-only insertion — update.go:46 blindly appends each line as it arrives, preserving Docker's arbitrary order
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:
Set Timestamps: true
Detect the initial batch (read until channel drains or count reaches tail)
Sort the batch by parsed timestamp, send as InitialBatchMsg
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
If you would like to refer to this comment somewhere else in this project, copy and paste the following link:
Originally posted by: eldara-cruncher
Analysis
Root cause
Docker's
ServiceLogsAPI 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:
stream.go:51setsTimestamps: false, so there's no sortable field at allupdate.go:46blindly appends each line as it arrives, preserving Docker's arbitrary orderCurrent data flow
m.lines []stringandm.lineNodes []stringhave no timestamp field. Lines are formatted display strings with no sortable data.Fix options
Both require setting
Timestamps: truein the Docker API call.Option A — Two-phase fetch (recommended)
Split the single
ServiceLogscall into two:Follow: false, Timestamps: true, Tail: "200"— read all lines to EOF, sort by timestamp, bulk-insert into modelFollow: true, Timestamps: true, Since: <latest_ts_from_phase1>— append as they arrive (mostly ordered already)Changes:
stream.go: setTimestamps: true, split goroutine into two sequential API callsmessages.go: addInitialBatchMsgcarrying pre-sorted batchmodel.go: addlineTimestamps []time.Timeparallel sliceupdate.go: handleInitialBatchMsgby bulk-setting sorted lines, then start streamingformatLogLineWithNode(): parse and strip/keep the RFC3339Nano timestampPros: clean separation, no timing heuristics, streaming path unchanged.
Cons: two API calls, brief gap mitigated by
Sinceoverlap + dedup.Option B — Buffered initial sort (minimal change)
Keep a single
ServiceLogscall but buffer the initial burst:Timestamps: truetail)InitialBatchMsgLineMsgpathPros: 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:formatLogLineWithNode()splits on first space to get details vs message. With timestamps enabled, the timestamp becomes the first token ofmessage— needs to be extracted viatime.Parse(time.RFC3339Nano, ...).Timestamp display
Separate decision — should timestamps be visible?
docker service logs -tt) to show/hide