11package commands
22
33import (
4- "bufio"
54 "context"
65 "errors"
76 "fmt"
8- "io"
97 "os"
108 "os/exec"
11- "os/signal"
129 "path/filepath"
13- "regexp"
1410 "runtime"
1511 "strings"
16- "time"
1712
1813 "github.com/docker/model-runner/cmd/cli/commands/completion"
1914 "github.com/docker/model-runner/cmd/cli/desktop"
2015 "github.com/docker/model-runner/cmd/cli/pkg/standalone"
2116 "github.com/docker/model-runner/cmd/cli/pkg/types"
17+ dmrlogs "github.com/docker/model-runner/pkg/logs"
2218 "github.com/moby/moby/api/pkg/stdcopy"
2319 "github.com/moby/moby/client"
24- "github.com/nxadm/tail"
2520 "github.com/spf13/cobra"
26- "golang.org/x/sync/errgroup"
2721)
2822
2923func newLogsCmd () * cobra.Command {
@@ -32,135 +26,98 @@ func newLogsCmd() *cobra.Command {
3226 Use : "logs [OPTIONS]" ,
3327 Short : "Fetch the Docker Model Runner logs" ,
3428 RunE : func (cmd * cobra.Command , args []string ) error {
35- homeDir , err := os .UserHomeDir ()
36- if err != nil {
37- return err
38- }
39-
40- // If we're running in standalone mode, then print the container
41- // logs.
29+ // Standalone mode: fetch container logs via Docker API.
4230 engineKind := modelRunner .EngineKind ()
4331 useStandaloneLogs := engineKind == types .ModelRunnerEngineKindMoby ||
4432 engineKind == types .ModelRunnerEngineKindCloud
4533 if useStandaloneLogs {
46- dockerClient , err := desktop .DockerClientForContext (dockerCLI , dockerCLI .CurrentContext ())
34+ dockerClient , err := desktop .DockerClientForContext (
35+ dockerCLI , dockerCLI .CurrentContext (),
36+ )
4737 if err != nil {
4838 return fmt .Errorf ("failed to create Docker client: %w" , err )
4939 }
50- ctrID , _ , _ , err := standalone .FindControllerContainer (cmd .Context (), dockerClient )
40+ ctrID , _ , _ , err := standalone .FindControllerContainer (
41+ cmd .Context (), dockerClient ,
42+ )
5143 if err != nil {
52- return fmt .Errorf ("unable to identify Model Runner container: %w" , err )
44+ return fmt .Errorf (
45+ "unable to identify Model Runner container: %w" , err ,
46+ )
5347 } else if ctrID == "" {
5448 return errors .New ("unable to identify Model Runner container" )
5549 }
56- log , err := dockerClient .ContainerLogs (cmd .Context (), ctrID , client.ContainerLogsOptions {
57- ShowStdout : true ,
58- ShowStderr : true ,
59- Follow : follow ,
60- })
50+ log , err := dockerClient .ContainerLogs (
51+ cmd .Context (), ctrID , client.ContainerLogsOptions {
52+ ShowStdout : true ,
53+ ShowStderr : true ,
54+ Follow : follow ,
55+ },
56+ )
6157 if err != nil {
62- return fmt .Errorf ("unable to query Model Runner container logs: %w" , err )
58+ return fmt .Errorf (
59+ "unable to query Model Runner container logs: %w" , err ,
60+ )
6361 }
6462 defer log .Close ()
6563 _ , err = stdcopy .StdCopy (os .Stdout , os .Stderr , log )
6664 return err
6765 }
6866
69- var serviceLogPath string
70- var runtimeLogPath string
71- switch runtime .GOOS {
72- case "darwin" :
73- serviceLogPath = filepath .Join (homeDir , "Library/Containers/com.docker.docker/Data/log/host/inference.log" )
74- runtimeLogPath = filepath .Join (homeDir , "Library/Containers/com.docker.docker/Data/log/host/inference-llama.cpp-server.log" )
75- case "windows" , "linux" :
76- baseDir := homeDir
77- if runtime .GOOS == "linux" {
78- if ! isWSL () {
79- return fmt .Errorf ("log viewing on native Linux is only supported in standalone mode" )
80- }
81- // When running inside WSL2 with Docker Desktop, the log files
82- // are on the Windows host filesystem mounted under /mnt/.
83- winHomeDir , wslErr := windowsHomeDirFromWSL (cmd .Context ())
84- if wslErr != nil {
85- return fmt .Errorf ("unable to determine Windows home directory from WSL2: %w" , wslErr )
86- }
87- baseDir = winHomeDir
67+ // Desktop mode: try local log files first.
68+ serviceLogPath , runtimeLogPath , localErr := resolveDesktopLogPaths (
69+ cmd .Context (),
70+ )
71+ if localErr == nil {
72+ // Verify we can actually open the service log.
73+ f , openErr := os .Open (serviceLogPath )
74+ if openErr != nil {
75+ localErr = openErr
76+ } else {
77+ f .Close ()
8878 }
89- serviceLogPath = filepath .Join (baseDir , "AppData/Local/Docker/log/host/inference.log" )
90- runtimeLogPath = filepath .Join (baseDir , "AppData/Local/Docker/log/host/inference-llama.cpp-server.log" )
91- default :
92- return fmt .Errorf ("unsupported OS: %s" , runtime .GOOS )
9379 }
9480
95- if noEngines {
96- err = printMergedLog (cmd .OutOrStdout (), serviceLogPath , "" )
97- if err != nil {
98- return err
99- }
100- } else {
101- err = printMergedLog (cmd .OutOrStdout (), serviceLogPath , runtimeLogPath )
102- if err != nil {
103- return err
81+ if localErr != nil {
82+ // Local files unavailable (e.g. running inside a container).
83+ // Fall back to the DMR /logs API.
84+ apiErr := desktopClient .Logs (
85+ cmd .Context (), follow , noEngines , cmd .OutOrStdout (),
86+ )
87+ if apiErr != nil {
88+ return fmt .Errorf (
89+ "local logs unavailable (%w); API fallback failed: %w" ,
90+ localErr , apiErr ,
91+ )
10492 }
105- }
106-
107- if ! follow {
10893 return nil
10994 }
11095
111- ctx , cancel := signal .NotifyContext (context .Background (), os .Interrupt , os .Kill )
112- defer cancel ()
113-
114- g , ctx := errgroup .WithContext (ctx )
115-
116- // Poll mode is needed when tailing files over a mounted filesystem
117- // (Windows or WSL2 accessing the Windows host via /mnt/).
118- pollMode := runtime .GOOS == "windows" || (runtime .GOOS == "linux" && isWSL ())
119-
120- g .Go (func () error {
121- t , err := tail .TailFile (
122- serviceLogPath , tail.Config {Location : & tail.SeekInfo {Offset : 0 , Whence : io .SeekEnd }, Follow : true , ReOpen : true , Poll : pollMode },
123- )
124- if err != nil {
125- return err
126- }
127- for {
128- select {
129- case line , ok := <- t .Lines :
130- if ! ok {
131- return nil
132- }
133- cmd .Println (line .Text )
134- case <- ctx .Done ():
135- return t .Stop ()
136- }
137- }
138- })
139-
96+ // Local files are accessible: use shared merge/follow logic.
97+ enginePath := ""
14098 if ! noEngines {
141- g .Go (func () error {
142- t , err := tail .TailFile (
143- runtimeLogPath , tail.Config {Location : & tail.SeekInfo {Offset : 0 , Whence : io .SeekEnd }, Follow : true , ReOpen : true , Poll : pollMode },
144- )
145- if err != nil {
146- return err
147- }
148-
149- for {
150- select {
151- case line , ok := <- t .Lines :
152- if ! ok {
153- return nil
154- }
155- cmd .Println (line .Text )
156- case <- ctx .Done ():
157- return t .Stop ()
158- }
159- }
160- })
99+ enginePath = runtimeLogPath
161100 }
162101
163- return g .Wait ()
102+ // Poll mode is needed when tailing files over a mounted
103+ // filesystem (Windows or WSL2 accessing the Windows host).
104+ pollMode := runtime .GOOS == "windows" ||
105+ (runtime .GOOS == "linux" && isWSL ())
106+
107+ result , err := dmrlogs .MergeLogs (
108+ cmd .OutOrStdout (), serviceLogPath , enginePath ,
109+ )
110+ if err != nil {
111+ return err
112+ }
113+ if ! follow {
114+ return nil
115+ }
116+ return dmrlogs .Follow (
117+ cmd .Context (), cmd .OutOrStdout (),
118+ serviceLogPath , enginePath ,
119+ result , pollMode ,
120+ )
164121 },
165122 ValidArgsFunction : completion .NoComplete ,
166123 }
@@ -169,15 +126,64 @@ func newLogsCmd() *cobra.Command {
169126 return c
170127}
171128
172- // isWSL reports whether the current process is running inside a WSL2 environment.
129+ // resolveDesktopLogPaths returns the service and engine log file paths
130+ // for Docker Desktop mode. It returns an error when the OS is not
131+ // supported or path discovery fails (e.g. native Linux, WSL failure).
132+ func resolveDesktopLogPaths (ctx context.Context ) (string , string , error ) {
133+ homeDir , err := os .UserHomeDir ()
134+ if err != nil {
135+ return "" , "" , fmt .Errorf ("home directory: %w" , err )
136+ }
137+
138+ switch runtime .GOOS {
139+ case "darwin" :
140+ base := filepath .Join (
141+ homeDir ,
142+ "Library/Containers/com.docker.docker/Data/log/host" ,
143+ )
144+ return filepath .Join (base , dmrlogs .ServiceLogName ),
145+ filepath .Join (base , dmrlogs .EngineLogName ),
146+ nil
147+ case "windows" :
148+ base := filepath .Join (homeDir , "AppData/Local/Docker/log/host" )
149+ return filepath .Join (base , dmrlogs .ServiceLogName ),
150+ filepath .Join (base , dmrlogs .EngineLogName ),
151+ nil
152+ case "linux" :
153+ if ! isWSL () {
154+ return "" , "" , fmt .Errorf (
155+ "log viewing on native Linux is only supported in standalone mode" ,
156+ )
157+ }
158+ // In WSL2 with Docker Desktop, log files are on the Windows
159+ // host filesystem mounted under /mnt/.
160+ winHomeDir , wslErr := windowsHomeDirFromWSL (ctx )
161+ if wslErr != nil {
162+ return "" , "" , fmt .Errorf (
163+ "unable to determine Windows home directory from WSL2: %w" ,
164+ wslErr ,
165+ )
166+ }
167+ base := filepath .Join (winHomeDir , "AppData/Local/Docker/log/host" )
168+ return filepath .Join (base , dmrlogs .ServiceLogName ),
169+ filepath .Join (base , dmrlogs .EngineLogName ),
170+ nil
171+ default :
172+ return "" , "" , fmt .Errorf ("unsupported OS: %s" , runtime .GOOS )
173+ }
174+ }
175+
176+ // isWSL reports whether the current process is running inside a WSL2
177+ // environment.
173178func isWSL () bool {
174179 _ , ok := os .LookupEnv ("WSL_DISTRO_NAME" )
175180 return ok
176181}
177182
178- // windowsHomeDirFromWSL resolves the Windows user's home directory from
179- // within a WSL2 environment by running "wslpath" on the USERPROFILE path
180- // obtained via "wslvar". This returns a Linux path like /mnt/c/Users/Name.
183+ // windowsHomeDirFromWSL resolves the Windows user's home directory
184+ // from within a WSL2 environment by running "wslpath" on the
185+ // USERPROFILE path obtained via "wslvar". Returns a Linux path such
186+ // as /mnt/c/Users/Name.
181187func windowsHomeDirFromWSL (ctx context.Context ) (string , error ) {
182188 out , err := exec .CommandContext (ctx , "wslvar" , "USERPROFILE" ).Output ()
183189 if err != nil {
@@ -197,82 +203,3 @@ func windowsHomeDirFromWSL(ctx context.Context) (string, error) {
197203 }
198204 return linuxPath , nil
199205}
200-
201- var timestampRe = regexp .MustCompile (`\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\].*` )
202-
203- const timeFmt = "2006-01-02T15:04:05.000000000Z"
204-
205- func advanceToNextTimestamp (w io.Writer , logScanner * bufio.Scanner ) (time.Time , string ) {
206- if logScanner == nil {
207- return time.Time {}, ""
208- }
209-
210- for logScanner .Scan () {
211- text := logScanner .Text ()
212- match := timestampRe .FindStringSubmatch (text )
213- if len (match ) == 2 {
214- timestamp , err := time .Parse (timeFmt , match [1 ])
215- if err != nil {
216- fmt .Fprintln (w , text )
217- continue
218- }
219- return timestamp , text
220- } else {
221- fmt .Fprintln (w , text )
222- }
223- }
224- return time.Time {}, ""
225- }
226-
227- func printMergedLog (w io.Writer , logPath1 , logPath2 string ) error {
228- var logScanner1 * bufio.Scanner
229- if logPath1 != "" {
230- logFile1 , err := os .Open (logPath1 )
231- if err == nil {
232- defer logFile1 .Close ()
233- logScanner1 = bufio .NewScanner (logFile1 )
234- }
235- }
236-
237- var logScanner2 * bufio.Scanner
238- if logPath2 != "" {
239- logFile2 , err := os .Open (logPath2 )
240- if err == nil {
241- defer logFile2 .Close ()
242- logScanner2 = bufio .NewScanner (logFile2 )
243- }
244- }
245-
246- var timestamp1 time.Time
247- var timestamp2 time.Time
248- var line1 string
249- var line2 string
250-
251- timestamp1 , line1 = advanceToNextTimestamp (w , logScanner1 )
252- timestamp2 , line2 = advanceToNextTimestamp (w , logScanner2 )
253-
254- for line1 != "" && line2 != "" {
255- if ! timestamp2 .Before (timestamp1 ) {
256- fmt .Fprintln (w , line1 )
257- timestamp1 , line1 = advanceToNextTimestamp (w , logScanner1 )
258- } else {
259- fmt .Fprintln (w , line2 )
260- timestamp2 , line2 = advanceToNextTimestamp (w , logScanner2 )
261- }
262- }
263-
264- if line1 != "" {
265- fmt .Fprintln (w , line1 )
266- for logScanner1 .Scan () {
267- fmt .Fprintln (w , logScanner1 .Text ())
268- }
269- }
270- if line2 != "" {
271- fmt .Fprintln (w , line2 )
272- for logScanner2 .Scan () {
273- fmt .Fprintln (w , logScanner2 .Text ())
274- }
275- }
276-
277- return nil
278- }
0 commit comments