logstream

package
v5.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

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

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.

func NewCursors

func NewCursors() *Cursors

NewCursors creates the Cursors of a session.

func (*Cursors) Enter

func (c *Cursors) Enter(id string) *Turn

Enter queues a stream for container id. Turns are granted in the order Enter is called: call it synchronously from the (ordered) event listener, not from the goroutine that will stream.

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

func NewMonitor(apiClient client.APIClient, project string) *Monitor

NewMonitor creates a Monitor of the containers of a project.

func (*Monitor) ReadDaemonTime

func (c *Monitor) ReadDaemonTime(ctx context.Context) error

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

func (c *Monitor) Start(ctx context.Context) error

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

func (c *Monitor) WithServices(services []string)

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 New

func New(backend Backend) *Service

New creates a Service streaming from the given Backend.

func (*Service) Attach

func (s *Service) Attach(ctx context.Context, projectName string, options api.AttachOptions) error

Attach attaches the terminal to the container of a service.

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.

func (*Service) Logs

func (s *Service) Logs(
	ctx context.Context,
	projectName string,
	consumer api.LogConsumer,
	options api.LogOptions,
) error

Logs streams the logs of the containers of a project to consumer, following them (and the containers that come up meanwhile) when options.Follow is set.

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.

func (*Turn) End

func (t *Turn) End()

End hands over to the next stream. Every turn must end, including those that never got to stream; it is safe to call more than once.

func (*Turn) Wait

func (t *Turn) Wait(ctx context.Context) error

Wait blocks until every earlier stream of the container has ended.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL