refactor(logs): update log formatting for systemd services and handle merging in the client

This commit is contained in:
Pasha Sviderski
2026-04-21 22:00:38 +10:00
parent 7e1b91c372
commit b852d34068
5 changed files with 140 additions and 88 deletions
+34 -20
View File
@@ -10,6 +10,7 @@ import (
"charm.land/lipgloss/v2"
"github.com/docker/docker/pkg/stringid"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
)
@@ -80,9 +81,9 @@ func (f *Formatter) formatMachine(name string) string {
return style.Render(name)
}
func (f *Formatter) formatServiceContainer(serviceName, containerID string) string {
styleService := lipgloss.NewStyle().Bold(true).PaddingRight(f.maxServiceWidth - len(serviceName))
styleContainer := lipgloss.NewStyle().Faint(true)
func (f *Formatter) formatService(serviceName, containerID string) string {
styleService := lipgloss.NewStyle().Bold(true)
padding := f.maxServiceWidth - len(serviceName)
if len(f.serviceNames) > 1 {
// Service name is coloured for multi-service logs.
@@ -95,10 +96,15 @@ func (f *Formatter) formatServiceContainer(serviceName, containerID string) stri
styleService = styleService.Foreground(Palette[i%len(Palette)])
}
return styleService.Render(serviceName) + styleContainer.Render("["+containerID[:5]+"]")
// Journal logs are unit-scoped and have no container ID.
if containerID == "" {
return styleService.PaddingRight(padding).Render(serviceName)
}
return styleService.Render(serviceName) + tui.Faint.PaddingRight(padding).Render("/"+containerID[:5])
}
// printEntry prints a single log entry with proper formatting.
// PrintEntry prints a single log entry with proper formatting.
func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
if entry.Stream != api.LogStreamStdout && entry.Stream != api.LogStreamStderr {
return
@@ -114,8 +120,8 @@ func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
output.WriteString(f.formatMachine(entry.Metadata.MachineName))
output.WriteString(" ")
// Service[container_id]
output.WriteString(f.formatServiceContainer(entry.Metadata.ServiceName, entry.Metadata.ContainerID))
// Service/container_id or service name for a systemd service.
output.WriteString(f.formatService(entry.Metadata.ServiceName, entry.Metadata.ContainerID))
output.WriteString(" ")
// Message
@@ -131,23 +137,31 @@ func (f *Formatter) PrintEntry(entry api.ServiceLogEntry) {
// PrintError prints an error entry (e.g., stalled stream warning).
func (f *Formatter) PrintError(entry api.ServiceLogEntry) {
if entry.Metadata.ServiceName == "" {
msg := fmt.Sprintf("ERROR: %v", entry.Err)
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.BrightRed) // Bold bright red.
fmt.Fprintln(os.Stderr, style.Render(msg))
return
}
var msg string
if entry.Metadata.ContainerID != "" {
msg := fmt.Sprintf("WARNING: log stream from %s[%s] on machine '%s'",
msg = fmt.Sprintf("WARNING: log stream from container '%s/%s' on machine '%s'",
entry.Metadata.ServiceName,
stringid.TruncateID(entry.Metadata.ContainerID),
entry.Metadata.MachineName)
if errors.Is(entry.Err, api.ErrLogStreamStalled) {
msg += " stopped responding"
} else {
msg += fmt.Sprintf(": %v", entry.Err)
}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11")) // Bold bright yellow
fmt.Fprintln(os.Stderr, style.Render(msg))
} else {
msg := fmt.Sprintf("ERROR: %v", entry.Err)
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")) // Bold bright red
fmt.Fprintln(os.Stderr, style.Render(msg))
msg = fmt.Sprintf("WARNING: log stream from systemd service '%s' on machine '%s'",
entry.Metadata.ServiceName,
entry.Metadata.MachineName)
}
if errors.Is(entry.Err, api.ErrLogStreamStalled) {
msg += " stopped responding"
} else {
msg += fmt.Sprintf(": %v", entry.Err)
}
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11")) // Bold bright yellow.
fmt.Fprintln(os.Stderr, style.Render(msg))
}
+35 -30
View File
@@ -16,35 +16,39 @@ func NewLogsCommand() *cobra.Command {
var options logs.Options
cmd := &cobra.Command{
Use: "logs [UNIT...]",
Use: "logs [SERVICE...]",
Aliases: []string{"log"},
Short: "View systemd service logs.",
Long: `View logs from all replicas of the specified units(s) (uncloud, docker or uncloud-corrosion) across all machines in the cluster.
Long: `View logs from the specified systemd service(s) across all machines in the cluster.
Use -m to restrict to specific machines.
If no units are specified, streams logs from the uncloud unit.`,
Example: ` # View recent logs for a system service.
uc logs uncloud
Supported services:
uncloud the Uncloud daemon
docker the Docker daemon
uncloud-corrosion the Corrosion distributed state store
If no services are specified, streams logs from the uncloud service.`,
Example: ` # View recent logs for the uncloud service.
uc machine logs
uc machine logs uncloud
# Stream logs in real-time (follow mode).
uc logs -f uncloud
uc machine logs -f uncloud
# View logs from multiple services.
uc logs web uncloud docker
uc machine logs uncloud docker uncloud-corrosion
# View logs from uncloud
uc logs
# Show last 20 lines per replica (default is 100).
uc logs -n 20 docker
# Show last 20 lines per machine (default is 100).
uc machine logs -n 20 docker
# Show all logs without line limit.
uc logs -n all docker
uc machine logs -n all docker
# View logs from a specific time range.
uc logs --since 3h --until 1h30m docker
uc machine logs --since 3h --until 1h30m docker
# View logs only from replicas running on specific machines.
uc logs -m machine1,machine2 docker corrosion`,
# View logs only from specific machines.
uc machine logs -m machine1,machine2 uncloud uncloud-corrosion`,
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return runLogs(cmd.Context(), uncli, args, options)
@@ -62,7 +66,7 @@ func runLogs(ctx context.Context, uncli *cli.CLI, units []string, opts logs.Opti
for _, unit := range units {
if !journal.ValidUnit(unit) {
return fmt.Errorf("invalid unit '%s'", unit)
return fmt.Errorf("invalid systemd service '%s'", unit)
}
}
@@ -85,8 +89,10 @@ func runLogs(ctx context.Context, uncli *cli.CLI, units []string, opts logs.Opti
Machines: cli.ExpandCommaSeparatedValues(opts.Machines),
}
// Fetch machine names for all machines we want the unit logs from.
machines, err := c.ListMachines(ctx, &api.MachineFilter{NamesOrIDs: opts.Machines})
// Resolve machine records for the formatter's column width computation.
machines, err := c.ListMachines(ctx, &api.MachineFilter{
NamesOrIDs: logsOpts.Machines,
})
if err != nil {
return fmt.Errorf("list machines: %w", err)
}
@@ -95,23 +101,22 @@ func runLogs(ctx context.Context, uncli *cli.CLI, units []string, opts logs.Opti
machineNames = append(machineNames, m.Machine.Name)
}
// Collect log streams from the units on the machine machines.
unitStreams := make([]<-chan api.ServiceLogEntry, 0, len(units)+len(machines))
for _, machine := range machineNames {
for _, unit := range units {
ch, err := c.MachineLogs(ctx, machine, unit, logsOpts)
if err != nil {
return fmt.Errorf("stream logs for systemd service '%s': %w", unit, err)
}
unitStreams = append(unitStreams, ch)
// Collect one log stream per unit. MachineLogs merges across machines internally.
unitStreams := make([]<-chan api.ServiceLogEntry, 0, len(units))
for _, unit := range units {
ch, err := c.MachineLogs(ctx, unit, logsOpts)
if err != nil {
return fmt.Errorf("stream logs for systemd service '%s': %w", unit, err)
}
unitStreams = append(unitStreams, ch)
}
var stream <-chan api.ServiceLogEntry
if len(units) == 1 {
if len(unitStreams) == 1 {
stream = unitStreams[0]
} else {
// Each MachineLogs stream already runs its own inner merger with stall detection,
// so the outer merger across units skips it to avoid duplicate warnings.
merger := client.NewLogMerger(unitStreams, client.LogMergerOptions{})
stream = merger.Stream()
}