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  }