github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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.InspectContainerWithOptions(docker.InspectContainerOptions{ 127 ID: opts.ContainerID, 128 }) 129 if err != nil { 130 _, notFoundOk := err.(*docker.NoSuchContainer) 131 if !notFoundOk { 132 return 133 } 134 } else if !container.State.Running { 135 return 136 } 137 } 138 }() 139 return nil 140 141 } 142 143 // openStreams open logger stdout/stderr; should be called in a background goroutine to avoid locking up 144 // process to avoid locking goroutine process 145 func (d *dockerLogger) openStreams(ctx context.Context, opts *StartOpts) (stdout, stderr io.WriteCloser, err error) { 146 d.stdLock.Lock() 147 stdoutF, stderrF := d.stdout, d.stderr 148 d.stdLock.Unlock() 149 150 if stdoutF != nil && stderrF != nil { 151 return stdoutF, stderrF, nil 152 } 153 154 // opening a fifo may block indefinitely until a reader end opens, so 155 // we preform open() without holding the stdLock, so Stop and interleave. 156 // This a defensive measure - logmon (the reader end) should be up and 157 // started before dockerLogger is started 158 if stdoutF == nil { 159 stdoutF, err = fifo.OpenWriter(opts.Stdout) 160 if err != nil { 161 return nil, nil, err 162 } 163 } 164 165 if stderrF == nil { 166 stderrF, err = fifo.OpenWriter(opts.Stderr) 167 if err != nil { 168 return nil, nil, err 169 } 170 } 171 172 if ctx.Err() != nil { 173 // Stop was called and don't need files anymore 174 stdoutF.Close() 175 stderrF.Close() 176 return nil, nil, ctx.Err() 177 } 178 179 d.stdLock.Lock() 180 d.stdout, d.stderr = stdoutF, stderrF 181 d.stdLock.Unlock() 182 183 return stdoutF, stderrF, nil 184 } 185 186 // Stop log monitoring 187 func (d *dockerLogger) Stop() error { 188 if d.cancelCtx != nil { 189 d.cancelCtx() 190 } 191 192 d.stdLock.Lock() 193 stdout, stderr := d.stdout, d.stderr 194 d.stdLock.Unlock() 195 196 if stdout != nil { 197 stdout.Close() 198 } 199 if stderr != nil { 200 stderr.Close() 201 } 202 return nil 203 } 204 205 func (d *dockerLogger) getDockerClient(opts *StartOpts) (*docker.Client, error) { 206 var err error 207 var merr multierror.Error 208 var newClient *docker.Client 209 210 // Default to using whatever is configured in docker.endpoint. If this is 211 // not specified we'll fall back on NewClientFromEnv which reads config from 212 // the DOCKER_* environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and 213 // DOCKER_CERT_PATH. This allows us to lock down the config in production 214 // but also accept the standard ENV configs for dev and test. 215 if opts.Endpoint != "" { 216 if opts.TLSCert+opts.TLSKey+opts.TLSCA != "" { 217 d.logger.Debug("using TLS client connection to docker", "endpoint", opts.Endpoint) 218 newClient, err = docker.NewTLSClient(opts.Endpoint, opts.TLSCert, opts.TLSKey, opts.TLSCA) 219 if err != nil { 220 merr.Errors = append(merr.Errors, err) 221 } 222 } else { 223 d.logger.Debug("using plaintext client connection to docker", "endpoint", opts.Endpoint) 224 newClient, err = docker.NewClient(opts.Endpoint) 225 if err != nil { 226 merr.Errors = append(merr.Errors, err) 227 } 228 } 229 } else { 230 d.logger.Debug("using client connection initialized from environment") 231 newClient, err = docker.NewClientFromEnv() 232 if err != nil { 233 merr.Errors = append(merr.Errors, err) 234 } 235 } 236 237 return newClient, merr.ErrorOrNil() 238 } 239 240 func isLoggingTerminalError(err error) bool { 241 if err == nil { 242 return false 243 } 244 245 if apiErr, ok := err.(*docker.Error); ok { 246 switch apiErr.Status { 247 case 501: 248 return true 249 } 250 } 251 252 terminals := []string{ 253 "configured logging driver does not support reading", 254 } 255 256 for _, c := range terminals { 257 if strings.Contains(err.Error(), c) { 258 return true 259 } 260 } 261 262 return false 263 } 264 265 // nextBackoff returns the next backoff period in seconds given current backoff 266 func nextBackoff(backoff float64) float64 { 267 if backoff < 0.5 { 268 backoff = 0.5 269 } 270 271 backoff = backoff * 1.15 * (1.0 + rand.Float64()) 272 if backoff > 120 { 273 backoff = 120 274 } else if backoff < 0.5 { 275 backoff = 0.5 276 } 277 278 return backoff 279 }