Documentation
¶
Overview ¶
Package logstream streams the output of the containers of a project to the user: logs, attach, and the monitor of the containers' events they follow.
Index ¶
- func SinceLastRun(ctr container.InspectResponse) string
- type Backend
- type Cursor
- type Cursors
- type Monitor
- type Printer
- type RunEndTracker
- type Service
- func (s *Service) Attach(ctx context.Context, projectName string, options api.AttachOptions) error
- func (s *Service) AttachContainer(ctx context.Context, service, id, name string, ...) error
- func (s *Service) AttachContainers(ctx context.Context, project *types.Project, ...) ([]container.Summary, error)
- func (s *Service) DoLogContainer(ctx context.Context, limiter *semaphore.Weighted, consumer api.LogConsumer, ...) error
- func (s *Service) FollowStartedContainersLogs(ctx context.Context, eg *errgroup.Group, limiter *semaphore.Weighted, ...) api.ContainerEventListener
- func (s *Service) InspectWithSlot(ctx context.Context, limiter *semaphore.Weighted, id string) (container.InspectResponse, error)
- func (s *Service) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, ...) error
- type Turn
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SinceLastRun ¶
func SinceLastRun(ctr container.InspectResponse) string
SinceLastRun returns the FALLBACK log window anchor for a container (re)started while we follow the project, used when the session has not observed a previous exit (RunEndTracker): the previous run's FinishedAt. The new run's StartedAt looks like the natural anchor but loses output — the daemon starts copying stdout before it records StartedAt, so a fast process can get its first lines timestamped just before it, and `since=StartedAt` then drops them forever. Nothing can be logged between the previous run's end and the new run's start, so FinishedAt captures the entire new run without replaying the previous one — UNLESS the new run already finished by inspection time (its own FinishedAt shadows the previous run's), which is exactly what the tracker protects against. A container with no previous run has a zero FinishedAt, which means "no lower bound" — equally exact for a fresh container.
Types ¶
type Backend ¶
type Backend interface {
engine.Backend
// CLI is the Docker CLI Compose runs in, and the owner of the terminal streams.
CLI() command.Cli
}
Backend is what the operations need from the engine they stream from.
The terminal reaches them through CLI: the stream attach prints its announcement to, and the Docker CLI's own attach implementation.
type Cursor ¶
type Cursor struct {
// contains filtered or unexported fields
}
Cursor is the position of a container's log relay.
type Cursors ¶
type Cursors struct {
// contains filtered or unexported fields
}
Cursors hands the successive log streams of a container over to each other, so that a restarted container's lines are relayed exactly once.
The log journal is shared by all the runs of a container, and a followed stream has no upper bound: it ends with the run in progress when it opened. Opened late, after the container restarted again (the restart delay is 100ms, a loaded daemon or monitor easily takes longer), it returns the next run's lines as well, which that run's own stream then returns a second time. So the streams of a container run one at a time, in start-event order, each starting right after the last line the previous ones relayed. Line timestamps are the daemon's own, no clock of ours is involved.
type Monitor ¶
type Monitor struct {
// contains filtered or unexported fields
}
Monitor follows the events of the containers of a project and reports them to its listeners, see Start.
func NewMonitor ¶
NewMonitor creates a Monitor of the containers of a project.
func (*Monitor) ReadDaemonTime ¶
ReadDaemonTime takes, as the point from which to subscribe to events, the daemon's current time, in the "<seconds>.<nanoseconds>" form the events API takes as since. It does so once. A daemon that can't be asked (the request is refused or fails) leaves since empty, so that events are subscribed live as they were before the replay existed, with a warning: this is a missing capability, not a reason to fail. A daemon that answers with no time or one that can't be parsed is an error: the replay would have no sound starting point, and nothing explains the answer. An interruption is an error too, the client's, which carries the context's: callers decide how to report it (Start ends quietly, an interactive up returns it).
func (*Monitor) Start ¶
Start runs monitor to detect application events and return after termination
The monitor consumes the engine's event stream, filtered on this project's containers, and has to interpret event sequences that depend on how a container terminates or comes back. The sequences below were mapped from moby's daemon sources and verified against a live engine (v29.x); meaningful event attributes are in parentheses:
container process exits on its own, whatever the exit code:
die (exitCode) — and nothing else: no stop event is emitted
restart policy in action, the daemon restarts the container after exit:
die (exitCode) → start → die → start → ...
No restart event is ever emitted for policy-driven restarts, and no
event at all while the container waits in restart backoff (status
"restarting", no process running); ContainerInspect reports
State.Restarting=true in that window.
docker stop (or compose stop) on a running container:
kill (signal=stop signal, SIGTERM by default)
[→ kill (signal=9, i.e. SIGKILL) when the grace period expires]
→ stop + die (exitCode)
The relative order of stop and die is not guaranteed: stop is emitted
by the ContainerStop call completing, die by the asynchronous exit
notification from containerd. A manual stop cancels the restart policy.
docker stop on a container in restart backoff (see #13985):
stop — and nothing else: with no process to signal, the daemon skips
the kill event, no die is ever produced, and the pending restart is
cancelled.
docker restart, and the ContainerRestart API in general (also used by
compose watch sync+restart, see #13161):
kill → stop + die → start → restart
A transient stop/die pair is emitted even though the container is
coming back. State.Restarting is never set on this path — the only
daemon code setting it is the restart-policy one — so the container is
briefly reported as "exited" between die and start, then "running"
again (https://github.com/moby/moby/issues/45538); an inspection
triggered by the die event usually, but not reliably, lands after the
new start. The restart action fires last, after start.
docker kill:
kill (signal=9, i.e. SIGKILL) → die (exitCode) — no stop event
A non-fatal signal (docker kill -s HUP) emits kill alone, with no die
unless the process exits.
OOM-killed container:
oom → die (exitCode, typically 137)
The die event carries no OOM-specific attribute, and a restart policy
applies as for any other exit (start follows). An oom can also occur
with no die at all, when the kernel kills a child process and the
container's main process survives.
docker rm -f on a running container:
kill → die → destroy
docker rm on a stopped container, docker rm -f on a restarting one:
destroy — and nothing else
Other actions (pause, unpause, attach, exec_*, health_status: *, ...) may show up in the stream but don't affect container tracking. Note that exec_create, exec_start and health_status actions embed variable data in the action name itself (e.g. "health_status: healthy"), which would break exact matching. On daemon shutdown containers are stopped (kill/die/stop) and the event stream usually delivers those events before it breaks, so a daemon restart is typically observed as a per-container kill/die/stop sequence followed by Events returning an error. A container that dies while the daemon itself is down never gets a die event at all: on daemon restore it is just recorded as exited, and containers the restore restarts per policy only emit start.
Events are requested since a point taken BEFORE the initial listing. Both the listing and the subscription are round trips, and a container may start and terminate in between (the first container of an `up` that starts right as the monitor attaches, running something like `echo hi`): its die would be emitted to nobody, and the monitor would track the container forever. Replaying the events from before the listing closes that window. Events already reflected in the listing are harmless: the listing notifies nothing, so each replayed event is reported once, and the tracked set only ever gets an ID added again or removed again.
The point is the DAEMON's clock, read from its system info (one more round trip before the listing): events are stamped by the daemon, and a client clock that differs from it (a remote daemon) would either miss the start of the window or replay events from before the monitor was started. There is no falling back on the client's clock. A daemon that refuses or can't answer the request (a read-only socket proxy that doesn't expose the info endpoint) doesn't fail the command, which worked without it: the monitor subscribes to live events, as it did before the replay, and warns that the window can't be closed. A daemon that answers with a time that can't be used is another matter and makes Start fail. A caller needing to know that before it goes on (before it starts the containers the monitor is to follow) calls ReadDaemonTime itself first; Start then doesn't ask again.
func (*Monitor) WithListener ¶
func (c *Monitor) WithListener(listener api.ContainerEventListener)
WithListener registers a listener of the events of the monitored containers.
func (*Monitor) WithServices ¶
WithServices restricts the monitor to the given services, all of them when none is given.
type Printer ¶
type Printer interface {
HandleEvent(event api.ContainerEvent)
}
Printer watches application containers and collects their logs.
func NewPrinter ¶
func NewPrinter(consumer api.LogConsumer) Printer
NewPrinter builds a Printer passing containers logs to LogConsumer
type RunEndTracker ¶
type RunEndTracker struct {
// contains filtered or unexported fields
}
RunEndTracker remembers, per container, when the session last saw it exit — the re-attach anchor that stays correct even when the NEW run is already over: the event stream is ordered, so at start-event time the recorded value is necessarily the PREVIOUS run's end. The inspected FinishedAt (SinceLastRun) cannot give that guarantee — by the time we inspect, a fast run's own FinishedAt has overwritten it and the window would exclude everything the run printed.
func NewRunEndTracker ¶
func NewRunEndTracker() *RunEndTracker
NewRunEndTracker creates an empty RunEndTracker.
func (*RunEndTracker) Observe ¶
func (t *RunEndTracker) Observe(e api.ContainerEvent)
Observe records exit events (other event types are ignored). An exit carrying no timestamp is deliberately dropped rather than patched with the local clock: the anchor is compared by the DAEMON against its own container-log timestamps, so substituting our clock would trade a hypothetical daemon quirk for real clock-skew mis-anchoring. Dropping it merely degrades that container to the SinceLastRun fallback — the exact pre-tracker behavior, imperfect only for a run fast enough to have finished again by inspection time.
func (*RunEndTracker) Since ¶
func (t *RunEndTracker) Since(containerID string) string
Since returns the log-window anchor for a container being re-attached: the recorded end of its previous run in RFC3339Nano — the same format the FinishedAt fallback feeds the logs API — or "" when the session never saw it exit (first start).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service streams the containers' output on a Backend.
func (*Service) AttachContainer ¶
func (s *Service) AttachContainer(ctx context.Context, service, id, name string, listener api.ContainerEventListener) error
AttachContainer attaches to one container, reporting its output to listener.
func (*Service) AttachContainers ¶
func (s *Service) AttachContainers(ctx context.Context, project *types.Project, listener api.ContainerEventListener, selectedServices []string) ([]container.Summary, error)
AttachContainers attaches to the containers of the selected services of the project, reporting their output to listener, and returns them.
func (*Service) DoLogContainer ¶
func (s *Service) DoLogContainer( ctx context.Context, limiter *semaphore.Weighted, consumer api.LogConsumer, name string, ctr container.InspectResponse, options api.LogOptions, cur *Cursor, ) error
DoLogContainer opens the container's log stream and copies it to consumer until it ends. The caller must have already acquired limiter's slot (see concurrency.Acquire); it is released here right after ContainerLogs returns, so a long-lived --follow stream never keeps blocking new connections or the monitor.
cur, when not nil, is the container's Cursor: the stream then starts after the last line it relayed, and records the lines it relays. It is ignored for TTY containers, whose unframed stream cannot carry the timestamps it needs.
func (*Service) FollowStartedContainersLogs ¶
func (s *Service) FollowStartedContainersLogs( ctx context.Context, eg *errgroup.Group, limiter *semaphore.Weighted, consumer api.LogConsumer, options api.LogOptions, ) api.ContainerEventListener
FollowStartedContainersLogs streams the logs of containers (re)started while following, ignoring those whose logging driver doesn't support reading logs
func (*Service) InspectWithSlot ¶
func (s *Service) InspectWithSlot(ctx context.Context, limiter *semaphore.Weighted, id string) (container.InspectResponse, error)
InspectWithSlot acquires limiter's slot, then inspects the container, releasing the slot on error since no caller reaches doLogContainer (which owns the slot from here on) in that case.
type Turn ¶
type Turn struct {
Cursor *Cursor
// contains filtered or unexported fields
}
Turn is one stream's place in line for its container's relay.