github.com/ilhicas/nomad@v0.11.8/drivers/docker/docklog/docker_logger.go (about)

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