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 }