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 }