github.com/containerd/nerdctl@v1.7.7/pkg/logging/journald_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 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "os" 25 "os/exec" 26 "strconv" 27 "sync" 28 "text/template" 29 "time" 30 31 "github.com/containerd/containerd/runtime/v2/logging" 32 "github.com/containerd/log" 33 "github.com/containerd/nerdctl/pkg/strutil" 34 "github.com/coreos/go-systemd/v22/journal" 35 "github.com/docker/cli/templates" 36 timetypes "github.com/docker/docker/api/types/time" 37 ) 38 39 var JournalDriverLogOpts = []string{ 40 Tag, 41 } 42 43 func JournalLogOptsValidate(logOptMap map[string]string) error { 44 for key := range logOptMap { 45 if !strutil.InStringSlice(JournalDriverLogOpts, key) { 46 log.L.Warnf("log-opt %s is ignored for journald log driver", key) 47 } 48 } 49 return nil 50 } 51 52 type JournaldLogger struct { 53 Opts map[string]string 54 vars map[string]string 55 } 56 57 type identifier struct { 58 ID string 59 FullID string 60 Namespace string 61 } 62 63 func (journaldLogger *JournaldLogger) Init(dataStore, ns, id string) error { 64 return nil 65 } 66 67 func (journaldLogger *JournaldLogger) PreProcess(dataStore string, config *logging.Config) error { 68 if !journal.Enabled() { 69 return errors.New("the local systemd journal is not available for logging") 70 } 71 shortID := config.ID[:12] 72 var syslogIdentifier string 73 if _, ok := journaldLogger.Opts[Tag]; !ok { 74 syslogIdentifier = shortID 75 } else { 76 var tmpl *template.Template 77 var err error 78 tmpl, err = templates.Parse(journaldLogger.Opts[Tag]) 79 if err != nil { 80 return err 81 } 82 83 if tmpl != nil { 84 idn := identifier{ 85 ID: shortID, 86 FullID: config.ID, 87 Namespace: config.Namespace, 88 } 89 var b bytes.Buffer 90 if err := tmpl.Execute(&b, idn); err != nil { 91 return err 92 } 93 syslogIdentifier = b.String() 94 } 95 } 96 // construct log metadata for the container 97 vars := map[string]string{ 98 "SYSLOG_IDENTIFIER": syslogIdentifier, 99 } 100 journaldLogger.vars = vars 101 return nil 102 } 103 104 func (journaldLogger *JournaldLogger) Process(stdout <-chan string, stderr <-chan string) error { 105 var wg sync.WaitGroup 106 wg.Add(2) 107 f := func(wg *sync.WaitGroup, dataChan <-chan string, pri journal.Priority, vars map[string]string) { 108 defer wg.Done() 109 for log := range dataChan { 110 journal.Send(log, pri, vars) 111 } 112 } 113 // forward both stdout and stderr to the journal 114 go f(&wg, stdout, journal.PriInfo, journaldLogger.vars) 115 go f(&wg, stderr, journal.PriErr, journaldLogger.vars) 116 117 wg.Wait() 118 return nil 119 } 120 121 func (journaldLogger *JournaldLogger) PostProcess() error { 122 return nil 123 } 124 125 // Exec's `journalctl` with the provided arguments and hooks it up 126 // to the given stdout/stderr streams. 127 func FetchLogs(stdout, stderr io.Writer, journalctlArgs []string, stopChannel chan os.Signal) error { 128 journalctl, err := exec.LookPath("journalctl") 129 if err != nil { 130 return fmt.Errorf("could not find `journalctl` executable in PATH: %s", err) 131 } 132 133 cmd := exec.Command(journalctl, journalctlArgs...) 134 cmd.Stdout = stdout 135 cmd.Stderr = stderr 136 137 if err := cmd.Start(); err != nil { 138 return fmt.Errorf("failed to start journalctl command with args %#v: %s", journalctlArgs, err) 139 } 140 141 // Setup killing goroutine: 142 go func() { 143 <-stopChannel 144 log.L.Debugf("killing journalctl logs process with PID: %#v", cmd.Process.Pid) 145 cmd.Process.Kill() 146 }() 147 148 return nil 149 } 150 151 // Formats command line arguments for `journalctl` with the provided log viewing options and 152 // exec's and redirects `journalctl`s outputs to the provided io.Writers. 153 func viewLogsJournald(lvopts LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error { 154 if !checkExecutableAvailableInPath("journalctl") { 155 return fmt.Errorf("`journalctl` executable could not be found in PATH, cannot use Journald to view logs") 156 } 157 shortID := lvopts.ContainerID[:12] 158 var journalctlArgs = []string{fmt.Sprintf("SYSLOG_IDENTIFIER=%s", shortID), "--output=cat"} 159 if lvopts.Follow { 160 journalctlArgs = append(journalctlArgs, "-f") 161 } 162 if lvopts.Since != "" { 163 // using GetTimestamp from moby to keep time format consistency 164 ts, err := timetypes.GetTimestamp(lvopts.Since, time.Now()) 165 if err != nil { 166 return fmt.Errorf("invalid value for \"since\": %w", err) 167 } 168 date, err := prepareJournalCtlDate(ts) 169 if err != nil { 170 return err 171 } 172 journalctlArgs = append(journalctlArgs, "--since", date) 173 } 174 if lvopts.Timestamps { 175 log.L.Warnf("unsupported Timestamps option for journald driver") 176 } 177 if lvopts.Until != "" { 178 // using GetTimestamp from moby to keep time format consistency 179 ts, err := timetypes.GetTimestamp(lvopts.Until, time.Now()) 180 if err != nil { 181 return fmt.Errorf("invalid value for \"until\": %w", err) 182 } 183 date, err := prepareJournalCtlDate(ts) 184 if err != nil { 185 return err 186 } 187 journalctlArgs = append(journalctlArgs, "--until", date) 188 } 189 return FetchLogs(stdout, stderr, journalctlArgs, stopChannel) 190 } 191 192 func prepareJournalCtlDate(t string) (string, error) { 193 i, err := strconv.ParseInt(t, 10, 64) 194 if err != nil { 195 return "", err 196 } 197 tm := time.Unix(i, 0) 198 s := tm.Format("2006-01-02 15:04:05") 199 return s, nil 200 }