github.com/containerd/nerdctl@v1.7.7/pkg/logging/json_logger.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package logging 18 19 import ( 20 "errors" 21 "fmt" 22 "io" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "strconv" 27 "time" 28 29 "github.com/containerd/containerd/runtime/v2/logging" 30 "github.com/containerd/log" 31 "github.com/containerd/nerdctl/pkg/logging/jsonfile" 32 "github.com/containerd/nerdctl/pkg/strutil" 33 "github.com/docker/go-units" 34 "github.com/fahedouch/go-logrotate" 35 ) 36 37 var JSONDriverLogOpts = []string{ 38 LogPath, 39 MaxSize, 40 MaxFile, 41 } 42 43 type JSONLogger struct { 44 Opts map[string]string 45 logger *logrotate.Logger 46 } 47 48 func JSONFileLogOptsValidate(logOptMap map[string]string) error { 49 for key := range logOptMap { 50 if !strutil.InStringSlice(JSONDriverLogOpts, key) { 51 log.L.Warnf("log-opt %s is ignored for json-file log driver", key) 52 } 53 } 54 return nil 55 } 56 57 func (jsonLogger *JSONLogger) Init(dataStore, ns, id string) error { 58 // Initialize the log file (https://github.com/containerd/nerdctl/issues/1071) 59 var jsonFilePath string 60 if logPath, ok := jsonLogger.Opts[LogPath]; ok { 61 jsonFilePath = logPath 62 } else { 63 jsonFilePath = jsonfile.Path(dataStore, ns, id) 64 } 65 if err := os.MkdirAll(filepath.Dir(jsonFilePath), 0700); err != nil { 66 return err 67 } 68 if _, err := os.Stat(jsonFilePath); errors.Is(err, os.ErrNotExist) { 69 if writeErr := os.WriteFile(jsonFilePath, []byte{}, 0600); writeErr != nil { 70 return writeErr 71 } 72 } 73 return nil 74 } 75 76 func (jsonLogger *JSONLogger) PreProcess(dataStore string, config *logging.Config) error { 77 var jsonFilePath string 78 if logPath, ok := jsonLogger.Opts[LogPath]; ok { 79 jsonFilePath = logPath 80 } else { 81 jsonFilePath = jsonfile.Path(dataStore, config.Namespace, config.ID) 82 } 83 l := &logrotate.Logger{ 84 Filename: jsonFilePath, 85 } 86 //maxSize Defaults to unlimited. 87 var capVal int64 88 capVal = -1 89 if capacity, ok := jsonLogger.Opts[MaxSize]; ok { 90 var err error 91 capVal, err = units.FromHumanSize(capacity) 92 if err != nil { 93 return err 94 } 95 if capVal <= 0 { 96 return fmt.Errorf("max-size must be a positive number") 97 } 98 } 99 l.MaxBytes = capVal 100 maxFile := 1 101 if maxFileString, ok := jsonLogger.Opts[MaxFile]; ok { 102 var err error 103 maxFile, err = strconv.Atoi(maxFileString) 104 if err != nil { 105 return err 106 } 107 if maxFile < 1 { 108 return fmt.Errorf("max-file cannot be less than 1") 109 } 110 } 111 // MaxBackups does not include file to write logs to 112 l.MaxBackups = maxFile - 1 113 jsonLogger.logger = l 114 return nil 115 } 116 117 func (jsonLogger *JSONLogger) Process(stdout <-chan string, stderr <-chan string) error { 118 return jsonfile.Encode(stdout, stderr, jsonLogger.logger) 119 } 120 121 func (jsonLogger *JSONLogger) PostProcess() error { 122 return nil 123 } 124 125 // Loads log entries from logfiles produced by the json-logger driver and forwards 126 // them to the provided io.Writers after applying the provided logging options. 127 func viewLogsJSONFile(lvopts LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 128 logFilePath := jsonfile.Path(lvopts.DatastoreRootPath, lvopts.Namespace, lvopts.ContainerID) 129 if _, err := os.Stat(logFilePath); err != nil { 130 return fmt.Errorf("failed to stat JSON log file ") 131 } 132 133 if checkExecutableAvailableInPath("tail") { 134 return viewLogsJSONFileThroughTailExec(lvopts, logFilePath, stdout, stderr, stopChannel) 135 } 136 return viewLogsJSONFileDirect(lvopts, logFilePath, stdout, stderr, stopChannel) 137 } 138 139 // Loads JSON log entries directly from the provided JSON log file. 140 // If `LogViewOptions.Follow` is provided, it will refresh and re-read the file until 141 // it receives something through the stopChannel. 142 func viewLogsJSONFileDirect(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 143 fin, err := os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400) 144 if err != nil { 145 return err 146 } 147 defer fin.Close() 148 err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, lvopts.Tail) 149 if err != nil { 150 return fmt.Errorf("error occurred while doing initial read of JSON logfile %q: %s", jsonLogFilePath, err) 151 } 152 153 if lvopts.Follow { 154 // Get the current file handler's seek. 155 lastPos, err := fin.Seek(0, io.SeekCurrent) 156 if err != nil { 157 return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err) 158 } 159 fin.Close() 160 for { 161 select { 162 case <-stopChannel: 163 log.L.Debugf("received stop signal while re-reading JSON logfile, returning") 164 return nil 165 default: 166 // Re-open the file and seek to the last-consumed offset. 167 fin, err = os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400) 168 if err != nil { 169 fin.Close() 170 return fmt.Errorf("error occurred while trying to re-open JSON logfile %q: %s", jsonLogFilePath, err) 171 } 172 _, err = fin.Seek(lastPos, 0) 173 if err != nil { 174 fin.Close() 175 return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err) 176 } 177 178 err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0) 179 if err != nil { 180 fin.Close() 181 return fmt.Errorf("error occurred while doing follow-up decoding of JSON logfile %q at starting position %d: %s", jsonLogFilePath, lastPos, err) 182 } 183 184 // Record current file seek position before looping again. 185 lastPos, err = fin.Seek(0, io.SeekCurrent) 186 if err != nil { 187 fin.Close() 188 return fmt.Errorf("error occurred while trying to seek JSON logfile %q at current position: %s", jsonLogFilePath, err) 189 } 190 fin.Close() 191 } 192 // Give the OS a second to breathe before re-opening the file: 193 time.Sleep(time.Second) 194 } 195 } 196 return nil 197 } 198 199 // Loads logs through the `tail` executable. 200 func viewLogsJSONFileThroughTailExec(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 201 var args []string 202 203 args = append(args, "-n") 204 if lvopts.Tail == 0 { 205 args = append(args, "+0") 206 } else { 207 args = append(args, strconv.FormatUint(uint64(lvopts.Tail), 10)) 208 } 209 210 if lvopts.Follow { 211 args = append(args, "-f") 212 } 213 args = append(args, jsonLogFilePath) 214 cmd := exec.Command("tail", args...) 215 cmd.Stderr = os.Stderr 216 r, err := cmd.StdoutPipe() 217 if err != nil { 218 return err 219 } 220 if err := cmd.Start(); err != nil { 221 return err 222 } 223 224 // Setup killing goroutine: 225 go func() { 226 <-stopChannel 227 log.L.Debugf("killing tail logs process with PID: %d", cmd.Process.Pid) 228 cmd.Process.Kill() 229 }() 230 231 return jsonfile.Decode(stdout, stderr, r, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0) 232 }