@@ -20,11 +20,14 @@ func NewLogsCommand(groupID string) *cobra.Command {
2020 var options logs.Options
2121
2222 cmd := & cobra.Command {
23- Use : "logs [SERVICE...]" ,
23+ Use : "logs [SERVICE[/CONTAINER] ...]" ,
2424 Aliases : []string {"log" },
2525 Short : "View service logs." ,
2626 Long : `View logs from all replicas of the specified service(s) across all machines in the cluster.
2727
28+ To view logs from specific replicas (containers) within a service, use the SERVICE/CONTAINER form,
29+ where CONTAINER is a container name, full ID, or unique ID prefix.
30+
2831If no services are specified, streams logs from all services defined in the Compose file
2932(compose.yaml by default or the file(s) specified with --file).` ,
3033 Example : ` # View recent logs for a service.
@@ -48,6 +51,9 @@ If no services are specified, streams logs from all services defined in the Comp
4851 # View logs from a specific time range.
4952 uc logs --since 3h --until 1h30m web
5053
54+ # View logs only from specific replicas (containers).
55+ uc logs web/61d57fd3428f api/2f60
56+
5157 # View logs only from replicas running on specific machines.
5258 uc logs -m machine1,machine2 web api` ,
5359 RunE : func (cmd * cobra.Command , args []string ) error {
@@ -64,10 +70,15 @@ If no services are specified, streams logs from all services defined in the Comp
6470 return cmd
6571}
6672
67- func runLogs (ctx context.Context , uncli * cli.CLI , serviceNames []string , opts logs.Options ) error {
73+ func runLogs (ctx context.Context , uncli * cli.CLI , args []string , opts logs.Options ) error {
74+ serviceArgs , err := logs .ParseServiceArgs (args )
75+ if err != nil {
76+ return err
77+ }
78+
6879 // If no services specified, try to load them from the Compose file(s).
6980 fromCompose := false
70- if len (serviceNames ) == 0 {
81+ if len (serviceArgs ) == 0 {
7182 fromCompose = true
7283 project , err := compose .LoadProject (ctx , opts .Files )
7384 if err != nil {
@@ -77,10 +88,15 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
7788 uncli .SetClusterContextIfUnset (compose .ClusterContext (project ))
7889
7990 // View logs for all services, including disabled by inactive profiles.
80- serviceNames = append (project .ServiceNames (), project .DisabledServiceNames ()... )
81- if len (serviceNames ) == 0 {
91+ composeServices : = append (project .ServiceNames (), project .DisabledServiceNames ()... )
92+ if len (composeServices ) == 0 {
8293 return errors .New ("no services found in Compose file(s)" )
8394 }
95+
96+ serviceArgs = make ([]logs.ServiceArg , len (composeServices ))
97+ for i , name := range composeServices {
98+ serviceArgs [i ] = logs.ServiceArg {Service : name }
99+ }
84100 }
85101
86102 // Parse tail option.
@@ -95,7 +111,7 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
95111 }
96112 defer c .Close ()
97113
98- logsOpts := api.ServiceLogsOptions {
114+ baseOpts := api.ServiceLogsOptions {
99115 Follow : opts .Follow ,
100116 Tail : tail ,
101117 Since : opts .Since ,
@@ -106,20 +122,23 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
106122 // Collect log streams from all services. When service names come from a Compose file,
107123 // skip the ones that are not found in the cluster (they may have been removed or not deployed yet).
108124 machineIDsSet := mapset .NewSet [string ]()
109- svcStreams := make ([]<- chan api.ServiceLogEntry , 0 , len (serviceNames ))
125+ svcStreams := make ([]<- chan api.ServiceLogEntry , 0 , len (serviceArgs ))
110126 var foundServices , notFoundServices []string
111127
112- for _ , serviceName := range serviceNames {
113- svc , ch , err := c .ServiceLogs (ctx , serviceName , logsOpts )
128+ for _ , sa := range serviceArgs {
129+ svcOpts := baseOpts
130+ svcOpts .Containers = sa .Containers
131+
132+ svc , ch , err := c .ServiceLogs (ctx , sa .Service , svcOpts )
114133 if err != nil {
115134 if errors .Is (err , api .ErrNotFound ) && fromCompose {
116- notFoundServices = append (notFoundServices , serviceName )
135+ notFoundServices = append (notFoundServices , sa . Service )
117136 continue
118137 }
119- return fmt .Errorf ("stream logs for service '%s': %w" , serviceName , err )
138+ return fmt .Errorf ("stream logs for service '%s': %w" , sa . Service , err )
120139 }
121140 svcStreams = append (svcStreams , ch )
122- foundServices = append (foundServices , serviceName )
141+ foundServices = append (foundServices , sa . Service )
123142
124143 machineIDs := svc .MachineIDs ()
125144 machineIDsSet .Append (machineIDs ... )
@@ -130,15 +149,14 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
130149 return fmt .Errorf ("stream logs for services defined in %s: no services found in the cluster" ,
131150 strings .Join (opts .Files , ", " ))
132151 }
133- serviceNames = foundServices
134152
135153 for _ , name := range notFoundServices {
136154 tui .PrintWarning (fmt .Sprintf ("service '%s' not found in the cluster, skipping" , name ))
137155 }
138156 }
139157
140158 var stream <- chan api.ServiceLogEntry
141- if len (serviceNames ) == 1 {
159+ if len (svcStreams ) == 1 {
142160 stream = svcStreams [0 ]
143161 } else {
144162 // Merge all service streams into a single sorted stream without stall detection as its handled per-service.
@@ -147,6 +165,8 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
147165 }
148166
149167 // Fetch machine names for all machines (machineIDsSet) service containers are running on.
168+ // Note: this is the full set per service, not narrowed by --machine or per-container filters,
169+ // so the formatter may pad columns wider than strictly needed when filters are active.
150170 machines , err := c .ListMachines (ctx , & api.MachineFilter {NamesOrIDs : machineIDsSet .ToSlice ()})
151171 if err != nil {
152172 return fmt .Errorf ("list machines: %w" , err )
@@ -156,7 +176,7 @@ func runLogs(ctx context.Context, uncli *cli.CLI, serviceNames []string, opts lo
156176 machineNames = append (machineNames , m .Machine .Name )
157177 }
158178
159- formatter := logs .NewFormatter (machineNames , serviceNames , opts .UTC )
179+ formatter := logs .NewFormatter (machineNames , foundServices , opts .UTC )
160180
161181 // Print merged logs.
162182 for entry := range stream {
0 commit comments