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 }