Skip to content

Commit 278e20e

Browse files
committed
feat: tail JSON log
Signed-off-by: xyz-li <[email protected]>
1 parent b7a96ea commit 278e20e

File tree

3 files changed

+258
-143
lines changed

3 files changed

+258
-143
lines changed

pkg/logging/json_logger.go

Lines changed: 74 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,24 @@
1717
package logging
1818

1919
import (
20-
"bufio"
20+
"context"
2121
"errors"
2222
"fmt"
2323
"io"
2424
"os"
25-
"os/exec"
2625
"path/filepath"
2726
"strconv"
28-
"strings"
2927
"time"
3028

3129
"github.com/containerd/containerd/errdefs"
3230
"github.com/containerd/containerd/runtime/v2/logging"
3331
"github.com/containerd/log"
3432
"github.com/containerd/nerdctl/v2/pkg/logging/jsonfile"
33+
"github.com/containerd/nerdctl/v2/pkg/logging/tail"
3534
"github.com/containerd/nerdctl/v2/pkg/strutil"
3635
"github.com/docker/go-units"
3736
"github.com/fahedouch/go-logrotate"
37+
"github.com/fsnotify/fsnotify"
3838
)
3939

4040
var JSONDriverLogOpts = []string{
@@ -142,9 +142,6 @@ func viewLogsJSONFile(lvopts LogViewOptions, stdout, stderr io.Writer, stopChann
142142
}
143143
}
144144

145-
if checkExecutableAvailableInPath("tail") {
146-
return viewLogsJSONFileThroughTailExec(lvopts, logFilePath, stdout, stderr, stopChannel)
147-
}
148145
return viewLogsJSONFileDirect(lvopts, logFilePath, stdout, stderr, stopChannel)
149146
}
150147

@@ -156,118 +153,91 @@ func viewLogsJSONFileDirect(lvopts LogViewOptions, jsonLogFilePath string, stdou
156153
if err != nil {
157154
return err
158155
}
159-
defer fin.Close()
160-
err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, lvopts.Tail)
161-
if err != nil {
162-
return fmt.Errorf("error occurred while doing initial read of JSON logfile %q: %s", jsonLogFilePath, err)
163-
}
156+
defer func() { fin.Close() }()
164157

165-
if lvopts.Follow {
166-
// Get the current file handler's seek.
167-
lastPos, err := fin.Seek(0, io.SeekCurrent)
168-
if err != nil {
169-
return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
170-
}
171-
fin.Close()
172-
for {
173-
select {
174-
case <-stopChannel:
175-
log.L.Debugf("received stop signal while re-reading JSON logfile, returning")
158+
// Search start point based on tail line.
159+
start, err := tail.FindTailLineStartIndex(fin, lvopts.Tail)
160+
if err != nil {
161+
return fmt.Errorf("failed to tail %d lines of JSON logfile %q: %v", lvopts.Tail, jsonLogFilePath, err)
162+
}
163+
164+
if _, err := fin.Seek(start, io.SeekStart); err != nil {
165+
return fmt.Errorf("failed to seek in log file %q: %v", jsonLogFilePath, err)
166+
}
167+
168+
limitedMode := (lvopts.Tail > 0) && (!lvopts.Follow)
169+
limitedNum := lvopts.Tail
170+
var stop bool
171+
var watcher *fsnotify.Watcher
172+
baseName := filepath.Base(jsonLogFilePath)
173+
dir := filepath.Dir(jsonLogFilePath)
174+
retryTimes := 2
175+
backBytes := 0
176+
177+
for {
178+
select {
179+
case <-stopChannel:
180+
log.L.Debugf("received stop signal while re-reading JSON logfile, returning")
181+
return nil
182+
default:
183+
if stop || (limitedMode && limitedNum == 0) {
184+
log.L.Debugf("finished parsing log JSON filefile, path: %s", jsonLogFilePath)
176185
return nil
177-
default:
178-
// Re-open the file and seek to the last-consumed offset.
179-
fin, err = os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400)
180-
if err != nil {
181-
fin.Close()
182-
return fmt.Errorf("error occurred while trying to re-open JSON logfile %q: %s", jsonLogFilePath, err)
186+
}
187+
188+
if line, err := jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until); err != nil {
189+
if len(line) > 0 {
190+
time.Sleep(5 * time.Millisecond)
191+
if retryTimes == 0 {
192+
log.L.Infof("finished parsing log JSON filefile, path: %s, line: %s", jsonLogFilePath, string(line))
193+
return fmt.Errorf("error occurred while doing read of JSON logfile %q: %s, retryTimes: %d", jsonLogFilePath, err, retryTimes)
194+
}
195+
retryTimes--
196+
backBytes = len(line)
197+
} else {
198+
return fmt.Errorf("error occurred while doing read of JSON logfile %q: %s", jsonLogFilePath, err)
183199
}
184-
_, err = fin.Seek(lastPos, 0)
200+
} else {
201+
retryTimes = 2
202+
backBytes = 0
203+
}
204+
205+
if lvopts.Follow {
206+
// Get the current file handler's seek.
207+
lastPos, err := fin.Seek(int64(-backBytes), io.SeekCurrent)
185208
if err != nil {
186-
fin.Close()
187209
return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
188210
}
189211

190-
err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
191-
if err != nil {
192-
fin.Close()
193-
return fmt.Errorf("error occurred while doing follow-up decoding of JSON logfile %q at starting position %d: %s", jsonLogFilePath, lastPos, err)
212+
if watcher == nil {
213+
// Initialize the watcher if it has not been initialized yet.
214+
if watcher, err = NewLogFileWatcher(dir); err != nil {
215+
return err
216+
}
217+
defer watcher.Close()
218+
// If we just created the watcher, try again to read as we might have missed
219+
// the event.
220+
continue
194221
}
195222

196-
// Record current file seek position before looping again.
197-
lastPos, err = fin.Seek(0, io.SeekCurrent)
223+
var recreated bool
224+
// Wait until the next log change.
225+
recreated, err = startTail(context.Background(), baseName, watcher)
198226
if err != nil {
227+
return err
228+
}
229+
if recreated {
230+
newF, err := openFileShareDelete(jsonLogFilePath)
231+
if err != nil {
232+
return fmt.Errorf("failed to open JSON logfile %q: %v", jsonLogFilePath, err)
233+
}
199234
fin.Close()
200-
return fmt.Errorf("error occurred while trying to seek JSON logfile %q at current position: %s", jsonLogFilePath, err)
235+
fin = newF
201236
}
202-
fin.Close()
237+
continue
203238
}
239+
stop = true
204240
// Give the OS a second to breathe before re-opening the file:
205-
time.Sleep(time.Second)
206-
}
207-
}
208-
return nil
209-
}
210-
211-
// Loads logs through the `tail` executable.
212-
func viewLogsJSONFileThroughTailExec(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
213-
var args []string
214-
215-
args = append(args, "-n")
216-
if lvopts.Tail == 0 {
217-
args = append(args, "+0")
218-
} else {
219-
args = append(args, strconv.FormatUint(uint64(lvopts.Tail), 10))
220-
}
221-
222-
if lvopts.Follow {
223-
// using the `-F` to follow the file name instead of descriptor and retry if inaccessible
224-
args = append(args, "-F")
225-
}
226-
args = append(args, jsonLogFilePath)
227-
cmd := exec.Command("tail", args...)
228-
229-
cmdStdout, err := cmd.StdoutPipe()
230-
if err != nil {
231-
return err
232-
}
233-
234-
cmdStderr, err := cmd.StderrPipe()
235-
if err != nil {
236-
return err
237-
}
238-
239-
if err := cmd.Start(); err != nil {
240-
return err
241-
}
242-
243-
// filter the unwanted error message of the tail
244-
go filterTailStderr(cmdStderr)
245-
246-
// Setup killing goroutine:
247-
go func() {
248-
<-stopChannel
249-
log.L.Debugf("killing tail logs process with PID: %d", cmd.Process.Pid)
250-
cmd.Process.Kill()
251-
}()
252-
253-
return jsonfile.Decode(stdout, stderr, cmdStdout, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
254-
}
255-
256-
func filterTailStderr(reader io.Reader) error {
257-
scanner := bufio.NewScanner(reader)
258-
for scanner.Scan() {
259-
line := scanner.Text()
260-
if strings.HasSuffix(line, "has appeared; following new file") ||
261-
strings.HasSuffix(line, "has become inaccessible: No such file or directory") ||
262-
strings.HasSuffix(line, "has been replaced; following new file") ||
263-
strings.HasSuffix(line, ": No such file or directory") {
264-
continue
265241
}
266-
fmt.Fprintln(os.Stderr, line)
267242
}
268-
269-
if err := scanner.Err(); err != nil {
270-
return err
271-
}
272-
return nil
273243
}

0 commit comments

Comments
 (0)