github.com/hernad/nomad@v1.6.112/drivers/docker/docklog/docker_logger.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package docklog
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	docker "github.com/fsouza/go-dockerclient"
    16  	"github.com/hashicorp/go-hclog"
    17  	"github.com/hashicorp/go-multierror"
    18  
    19  	"github.com/hernad/nomad/client/lib/fifo"
    20  )
    21  
    22  // DockerLogger is a small utility to forward logs from a docker container to a target
    23  // destination
    24  type DockerLogger interface {
    25  	Start(*StartOpts) error
    26  	Stop() error
    27  }
    28  
    29  // StartOpts are the options needed to start docker log monitoring
    30  type StartOpts struct {
    31  	// Endpoint sets the docker client endpoint, defaults to environment if not set
    32  	Endpoint string
    33  
    34  	// ContainerID of the container to monitor logs for
    35  	ContainerID string
    36  	TTY         bool
    37  
    38  	// Stdout path to fifo
    39  	Stdout string
    40  	//Stderr path to fifo
    41  	Stderr string
    42  
    43  	// StartTime is the Unix time that the docker logger should fetch logs beginning
    44  	// from
    45  	StartTime int64
    46  
    47  	// TLS settings for docker client
    48  	TLSCert string
    49  	TLSKey  string
    50  	TLSCA   string
    51  }
    52  
    53  // NewDockerLogger returns an implementation of the DockerLogger interface
    54  func NewDockerLogger(logger hclog.Logger) DockerLogger {
    55  	return &dockerLogger{
    56  		logger: logger,
    57  		doneCh: make(chan interface{}),
    58  	}
    59  }
    60  
    61  // dockerLogger implements the DockerLogger interface
    62  type dockerLogger struct {
    63  	logger hclog.Logger
    64  
    65  	stdout  io.WriteCloser
    66  	stderr  io.WriteCloser
    67  	stdLock sync.Mutex
    68  
    69  	cancelCtx context.CancelFunc
    70  	doneCh    chan interface{}
    71  }
    72  
    73  // Start log monitoring
    74  func (d *dockerLogger) Start(opts *StartOpts) error {
    75  	client, err := d.getDockerClient(opts)
    76  	if err != nil {
    77  		return fmt.Errorf("failed to open docker client: %v", err)
    78  	}
    79  
    80  	ctx, cancel := context.WithCancel(context.Background())
    81  	d.cancelCtx = cancel
    82  
    83  	go func() {
    84  		defer close(d.doneCh)
    85  
    86  		stdout, stderr, err := d.openStreams(ctx, opts)
    87  		if err != nil {
    88  			d.logger.Error("log streaming ended with terminal error", "error", err)
    89  			return
    90  		}
    91  
    92  		sinceTime := time.Unix(opts.StartTime, 0)
    93  		backoff := 0.0
    94  
    95  		for {
    96  			logOpts := docker.LogsOptions{
    97  				Context:      ctx,
    98  				Container:    opts.ContainerID,
    99  				OutputStream: stdout,
   100  				ErrorStream:  stderr,
   101  				Since:        sinceTime.Unix(),
   102  				Follow:       true,
   103  				Stdout:       true,
   104  				Stderr:       true,
   105  
   106  				// When running in TTY, we must use a raw terminal.
   107  				// If not, we set RawTerminal to false to allow docker client
   108  				// to interpret special stdout/stderr messages
   109  				RawTerminal: opts.TTY,
   110  			}
   111  
   112  			err := client.Logs(logOpts)
   113  			if ctx.Err() != nil {
   114  				// If context is terminated then we can safely break the loop
   115  				return
   116  			} else if err == nil {
   117  				backoff = 0.0
   118  			} else if isLoggingTerminalError(err) {
   119  				d.logger.Error("log streaming ended with terminal error", "error", err)
   120  				return
   121  			} else if err != nil {
   122  				backoff = nextBackoff(backoff)
   123  				d.logger.Error("log streaming ended with error", "error", err, "retry_in", backoff)
   124  
   125  				time.Sleep(time.Duration(backoff) * time.Second)
   126  			}
   127  
   128  			sinceTime = time.Now()
   129  
   130  			container, err := client.InspectContainerWithOptions(docker.InspectContainerOptions{
   131  				ID: opts.ContainerID,
   132  			})
   133  			if err != nil {
   134  				_, notFoundOk := err.(*docker.NoSuchContainer)
   135  				if !notFoundOk {
   136  					return
   137  				}
   138  			} else if !container.State.Running {
   139  				return
   140  			}
   141  		}
   142  	}()
   143  	return nil
   144  
   145  }
   146  
   147  // openStreams open logger stdout/stderr; should be called in a background goroutine to avoid locking up
   148  // process to avoid locking goroutine process
   149  func (d *dockerLogger) openStreams(ctx context.Context, opts *StartOpts) (stdout, stderr io.WriteCloser, err error) {
   150  	d.stdLock.Lock()
   151  	stdoutF, stderrF := d.stdout, d.stderr
   152  	d.stdLock.Unlock()
   153  
   154  	if stdoutF != nil && stderrF != nil {
   155  		return stdoutF, stderrF, nil
   156  	}
   157  
   158  	// opening a fifo may block indefinitely until a reader end opens, so
   159  	// we preform open() without holding the stdLock, so Stop and interleave.
   160  	// This a defensive measure - logmon (the reader end) should be up and
   161  	// started before dockerLogger is started
   162  	if stdoutF == nil {
   163  		stdoutF, err = fifo.OpenWriter(opts.Stdout)
   164  		if err != nil {
   165  			return nil, nil, err
   166  		}
   167  	}
   168  
   169  	if stderrF == nil {
   170  		stderrF, err = fifo.OpenWriter(opts.Stderr)
   171  		if err != nil {
   172  			return nil, nil, err
   173  		}
   174  	}
   175  
   176  	if ctx.Err() != nil {
   177  		// Stop was called and don't need files anymore
   178  		stdoutF.Close()
   179  		stderrF.Close()
   180  		return nil, nil, ctx.Err()
   181  	}
   182  
   183  	d.stdLock.Lock()
   184  	d.stdout, d.stderr = stdoutF, stderrF
   185  	d.stdLock.Unlock()
   186  
   187  	return stdoutF, stderrF, nil
   188  }
   189  
   190  // Stop log monitoring
   191  func (d *dockerLogger) Stop() error {
   192  	if d.cancelCtx != nil {
   193  		d.cancelCtx()
   194  	}
   195  
   196  	d.stdLock.Lock()
   197  	stdout, stderr := d.stdout, d.stderr
   198  	d.stdLock.Unlock()
   199  
   200  	if stdout != nil {
   201  		stdout.Close()
   202  	}
   203  	if stderr != nil {
   204  		stderr.Close()
   205  	}
   206  	return nil
   207  }
   208  
   209  func (d *dockerLogger) getDockerClient(opts *StartOpts) (*docker.Client, error) {
   210  	var err error
   211  	var merr multierror.Error
   212  	var newClient *docker.Client
   213  
   214  	// Default to using whatever is configured in docker.endpoint. If this is
   215  	// not specified we'll fall back on NewClientFromEnv which reads config from
   216  	// the DOCKER_* environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and
   217  	// DOCKER_CERT_PATH. This allows us to lock down the config in production
   218  	// but also accept the standard ENV configs for dev and test.
   219  	if opts.Endpoint != "" {
   220  		if opts.TLSCert+opts.TLSKey+opts.TLSCA != "" {
   221  			d.logger.Debug("using TLS client connection to docker", "endpoint", opts.Endpoint)
   222  			newClient, err = docker.NewTLSClient(opts.Endpoint, opts.TLSCert, opts.TLSKey, opts.TLSCA)
   223  			if err != nil {
   224  				merr.Errors = append(merr.Errors, err)
   225  			}
   226  		} else {
   227  			d.logger.Debug("using plaintext client connection to docker", "endpoint", opts.Endpoint)
   228  			newClient, err = docker.NewClient(opts.Endpoint)
   229  			if err != nil {
   230  				merr.Errors = append(merr.Errors, err)
   231  			}
   232  		}
   233  	} else {
   234  		d.logger.Debug("using client connection initialized from environment")
   235  		newClient, err = docker.NewClientFromEnv()
   236  		if err != nil {
   237  			merr.Errors = append(merr.Errors, err)
   238  		}
   239  	}
   240  
   241  	return newClient, merr.ErrorOrNil()
   242  }
   243  
   244  func isLoggingTerminalError(err error) bool {
   245  	if err == nil {
   246  		return false
   247  	}
   248  
   249  	if apiErr, ok := err.(*docker.Error); ok {
   250  		switch apiErr.Status {
   251  		case 501:
   252  			return true
   253  		}
   254  	}
   255  
   256  	terminals := []string{
   257  		"configured logging driver does not support reading",
   258  	}
   259  
   260  	for _, c := range terminals {
   261  		if strings.Contains(err.Error(), c) {
   262  			return true
   263  		}
   264  	}
   265  
   266  	return false
   267  }
   268  
   269  // nextBackoff returns the next backoff period in seconds given current backoff
   270  func nextBackoff(backoff float64) float64 {
   271  	if backoff < 0.5 {
   272  		backoff = 0.5
   273  	}
   274  
   275  	backoff = backoff * 1.15 * (1.0 + rand.Float64())
   276  	if backoff > 120 {
   277  		backoff = 120
   278  	} else if backoff < 0.5 {
   279  		backoff = 0.5
   280  	}
   281  
   282  	return backoff
   283  }