github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/docker/docklog/docker_logger.go (about)

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