feat(logs): 'uc machine logs' to view logs from systemd services on machines (#283)

* logging: client side

Create internal/logs and put abstracted way service and machine logging in there
This adds the bits to do machine logging of the services.

See #273 and discussion in #158

Signed-off-by: Miek Gieben <miek@miek.nl>

* go mod tidy

Signed-off-by: Miek Gieben <miek@miek.nl>

* fix typo

Signed-off-by: Miek Gieben <miek@miek.nl>

* Hook it up further

Signed-off-by: Miek Gieben <miek@miek.nl>

* Check unit validity server side

Signed-off-by: Miek Gieben <miek@miek.nl>

* Make cli-docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* Add newline to logs

Signed-off-by: Miek Gieben <miek@miek.nl>

* docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* Polish a bit

Signed-off-by: Miek Gieben <miek@miek.nl>

* comment fix

Signed-off-by: Miek Gieben <miek@miek.nl>

* There is no groupid

Signed-off-by: Miek Gieben <miek@miek.nl>

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben
2026-04-21 17:27:39 +10:00
committed by GitHub
parent 3a79aeffcc
commit f44ada06b9
11 changed files with 547 additions and 218 deletions
+71
View File
@@ -140,6 +140,77 @@ func (cli *Client) ContainerLogs(
return ch, nil
}
// MachineLogs streams journal logs from the unit on a specified machine.
func (cli *Client) MachineLogs(
ctx context.Context, machineNameOrID string, unit string, opts api.ServiceLogsOptions,
) (<-chan api.ServiceLogEntry, 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.LogsRequest{
Id: unit,
Follow: opts.Follow,
Tail: int32(opts.Tail),
Since: opts.Since,
Until: opts.Until,
}
if !opts.Follow && opts.Tail == 0 {
// If not following and tail is 0, set tail to -1 to return all logs.
// Otherwise, no logs will be returned at all.
req.Tail = -1
}
stream, err := cli.MachineClient.MachineLogs(proxyCtx, req)
if err != nil {
return nil, err
}
// Enrich log entries from the machine with metadata.
metadata := api.ServiceLogEntryMetadata{
ServiceID: "",
ServiceName: "",
ContainerID: unit,
MachineID: machineNameOrID,
MachineName: machineNameOrID,
}
ch := make(chan api.LogEntry)
go func() {
defer close(ch)
for {
pbEntry, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
ch <- api.LogEntry{
Err: err,
}
return
}
entry := api.LogEntry{
Stream: api.LogStreamTypeFromProto(pbEntry.Stream),
Message: pbEntry.Message,
Timestamp: pbEntry.Timestamp.AsTime(),
}
select {
case ch <- entry:
case <-ctx.Done():
return
}
}
}()
enrichedCh := logsStreamWithServiceMetadata(ch, metadata)
return enrichedCh, nil
}
// logsStreamWithServiceMetadata wraps a container logs stream and enriches each log entry with service metadata.
func logsStreamWithServiceMetadata(
stream <-chan api.LogEntry, metadata api.ServiceLogEntryMetadata,