mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
feat: service logs command to stream service logs (#196)
* chore: fix landing logo shadow * feat: implement service logs command with colored output and strict ordering * revert Makefile * fix after rebase * move logs command under services with root shortcut * simplify server options for streaming logs and CLI flags * refactor ContainerLogs grpc server * update --tail flag * minor docker.proto * refactor client ServiceLogs and ContainerLogs * move ProxyMachinesContext from api to client pkg * minor refactor proto Stream * add api/logs * implement LogMerger * fix LogMerger to correctly use semaphore * increate inflish entries to 100 per stream * send heartbeats * refactor ContainerLogs to synchronise Send of entries and heartbeats to the steram * minor logmerege * remove ContainerName form ServiceLogEntryMetadata * refactor ContainerLogs into docker.Service * detect stalled container logs streams * update logmerger tests * fix comment in test * refactor LogMerger with options * uc logs: format one or multiple services * make LogMerger emit heartbeats, emit entries <= watermark, rewrite tests * update uc logs with new LogMerger * go mod tidy * fix after merge --------- Co-authored-by: Evgenii Orlov <evgenii.orlov@semrush.com>
This commit is contained in:
co-authored by
Evgenii Orlov
parent
234985b57d
commit
79dc05cb66
@@ -0,0 +1,141 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
)
|
||||
|
||||
// ServiceLogs streams log entries from all service containers in chronological order based on timestamps.
|
||||
// Keep in mind that perfect ordering of log events across multiple machines can't be guaranteed due to the
|
||||
// imperfection of physical clocks or potential clock skew between machines.
|
||||
// It uses a low watermark algorithm to ensure proper ordering across multiple machines.
|
||||
// Heartbeat entries from the server advance the watermark to enable timely emission of buffered logs.
|
||||
func (cli *Client) ServiceLogs(
|
||||
ctx context.Context, serviceNameOrID string, opts api.ServiceLogsOptions,
|
||||
) (api.Service, <-chan api.ServiceLogEntry, error) {
|
||||
svc, err := cli.InspectService(ctx, serviceNameOrID)
|
||||
if err != nil {
|
||||
return svc, nil, fmt.Errorf("inspect service: %w", err)
|
||||
}
|
||||
|
||||
if len(svc.Containers) == 0 {
|
||||
return svc, nil, fmt.Errorf("no containers found for service: %s", serviceNameOrID)
|
||||
}
|
||||
|
||||
machines, err := cli.ListMachines(ctx, nil)
|
||||
if err != nil {
|
||||
return svc, nil, fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
|
||||
ctrStreams := make([]<-chan api.ServiceLogEntry, 0, len(svc.Containers))
|
||||
for _, ctr := range svc.Containers {
|
||||
// Try to get machine name for ServiceLogEntry metadata and friendlier error message.
|
||||
machineName := ctr.MachineID
|
||||
m := machines.FindByNameOrID(ctr.MachineID)
|
||||
if m != nil {
|
||||
machineName = m.Machine.Name
|
||||
}
|
||||
|
||||
stream, err := cli.ContainerLogs(ctx, ctr.MachineID, ctr.Container.ID, opts)
|
||||
if err != nil {
|
||||
return svc, nil, fmt.Errorf("stream logs from service container '%s' on machine '%s': %w",
|
||||
stringid.TruncateID(ctr.Container.ID), machineName, err)
|
||||
}
|
||||
|
||||
// Enrich log entries from the container with service metadata.
|
||||
metadata := api.ServiceLogEntryMetadata{
|
||||
ServiceID: svc.ID,
|
||||
ServiceName: svc.Name,
|
||||
ContainerID: ctr.Container.ID,
|
||||
MachineID: ctr.MachineID,
|
||||
MachineName: machineName,
|
||||
}
|
||||
enrichedStream := logsStreamWithServiceMetadata(stream, metadata)
|
||||
ctrStreams = append(ctrStreams, enrichedStream)
|
||||
}
|
||||
|
||||
// Use the log merger to combine streams from all containers in chronological order.
|
||||
merger := NewLogMerger(ctrStreams, DefaultLogMergerOptions)
|
||||
mergedStream := merger.Stream()
|
||||
|
||||
return svc, mergedStream, nil
|
||||
}
|
||||
|
||||
// ContainerLogs streams log entries from a single container on a specified machine.
|
||||
func (cli *Client) ContainerLogs(
|
||||
ctx context.Context, machineNameOrID string, containerID string, opts api.ServiceLogsOptions,
|
||||
) (<-chan api.ContainerLogEntry, error) {
|
||||
proxyCtx, _, err := cli.ProxyMachinesContext(ctx, []string{machineNameOrID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request context to proxy to machine '%s': %w", machineNameOrID, err)
|
||||
}
|
||||
|
||||
req := &pb.ContainerLogsRequest{
|
||||
ContainerId: containerID,
|
||||
Follow: opts.Follow,
|
||||
Tail: int32(opts.Tail),
|
||||
Since: opts.Since,
|
||||
Until: opts.Until,
|
||||
}
|
||||
stream, err := cli.Docker.GRPCClient.ContainerLogs(proxyCtx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan api.ContainerLogEntry)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
for {
|
||||
pbEntry, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ch <- api.ContainerLogEntry{
|
||||
Err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
entry := api.ContainerLogEntry{
|
||||
Stream: api.LogStreamTypeFromProto(pbEntry.Stream),
|
||||
Message: pbEntry.Message,
|
||||
Timestamp: pbEntry.Timestamp.AsTime(),
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- entry:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// logsStreamWithServiceMetadata wraps a container logs stream and enriches each log entry with service metadata.
|
||||
func logsStreamWithServiceMetadata(
|
||||
stream <-chan api.ContainerLogEntry, metadata api.ServiceLogEntryMetadata,
|
||||
) <-chan api.ServiceLogEntry {
|
||||
out := make(chan api.ServiceLogEntry)
|
||||
|
||||
go func() {
|
||||
for entry := range stream {
|
||||
out <- api.ServiceLogEntry{
|
||||
Metadata: metadata,
|
||||
ContainerLogEntry: entry,
|
||||
}
|
||||
}
|
||||
close(out)
|
||||
}()
|
||||
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user