github.com/qubitproducts/logspray@v0.2.14/sources/docker/dockerwatcher.go (about) 1 // Copyright 2016 Qubit Digital Ltd. 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 // Package logspray is a collection of tools for streaming and indexing 14 // large volumes of dynamic logs. 15 16 package docker 17 18 import ( 19 "bufio" 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "os" 25 "regexp" 26 "strings" 27 "sync" 28 "time" 29 30 "golang.org/x/sync/errgroup" 31 32 "github.com/QubitProducts/logspray/proto/logspray" 33 "github.com/QubitProducts/logspray/sources" 34 "github.com/docker/engine-api/client" 35 "github.com/docker/engine-api/types" 36 "github.com/golang/glog" 37 "github.com/pkg/errors" 38 ) 39 40 // Watcher watches for files being added and removed from a filesystem 41 type Watcher struct { 42 ups chan []*sources.Update 43 envWhitelist []*regexp.Regexp 44 dcli *client.Client 45 root string 46 poll bool 47 48 sync.Mutex 49 running map[string]*sources.Update 50 } 51 52 // New creates a watcher that reports on the apperance, and 53 // dissapearance of sources 54 func New(opts ...Opt) (*Watcher, error) { 55 dcli, err := client.NewEnvClient() 56 if err != nil { 57 return nil, err 58 } 59 w := &Watcher{ 60 envWhitelist: []*regexp.Regexp{}, 61 dcli: dcli, 62 running: map[string]*sources.Update{}, 63 root: "", 64 } 65 for _, opt := range opts { 66 err = opt(w) 67 if err != nil { 68 return nil, err 69 } 70 } 71 72 if w.root == "" { 73 i, err := dcli.Info(context.Background()) 74 if err != nil { 75 return nil, fmt.Errorf("Failed to lookup docker root, %v", err) 76 } 77 w.root = i.DockerRootDir 78 } 79 return w, nil 80 } 81 82 // Opt is a type for configuration options for the docker Watcher 83 type Opt func(*Watcher) error 84 85 // WithDockerClient sets the default docker client 86 func WithDockerClient(cli *client.Client) Opt { 87 return func(w *Watcher) error { 88 w.dcli = cli 89 return nil 90 } 91 } 92 93 // WithEnvVarWhiteList lets you set a selection of regular 94 // experessions to match against the container environment 95 // variables 96 func WithEnvVarWhiteList(evwl []*regexp.Regexp) Opt { 97 return func(w *Watcher) error { 98 w.envWhitelist = evwl 99 return nil 100 } 101 } 102 103 // WithRoot sets the docker root to read files from 104 func WithRoot(root string) Opt { 105 return func(w *Watcher) error { 106 w.root = root 107 return nil 108 } 109 } 110 111 // WithPoll sets the default docker client 112 func WithPoll(poll bool) Opt { 113 return func(w *Watcher) error { 114 w.poll = poll 115 return nil 116 } 117 } 118 119 // startBackground creates a watcher that reports on the apperance, and 120 // dissapearance of sources 121 func (w *Watcher) startBackground(ctx context.Context) { 122 w.ups = make(chan []*sources.Update, 1) 123 go func() { 124 defer close(w.ups) 125 126 w.reconcile() 127 128 g := errgroup.Group{} 129 130 g.Go(func() error { 131 ticker := time.NewTicker(5 * time.Second) 132 defer ticker.Stop() 133 for { 134 select { 135 case <-ctx.Done(): 136 return ctx.Err() 137 case <-ticker.C: 138 w.reconcile() 139 } 140 } 141 }) 142 143 g.Go(func() error { 144 return w.watchEvs(ctx) 145 }) 146 147 g.Wait() 148 }() 149 return 150 } 151 152 func (w *Watcher) reconcileRunning(targets []*sources.Update) { 153 w.Lock() 154 defer w.Unlock() 155 updates := []*sources.Update{} 156 found := map[string]*sources.Update{} 157 158 if glog.V(1) { 159 glog.Infof("reconcile start: %d running, %d found", len(w.running), len(targets)) 160 } 161 162 for _, t := range targets { 163 _, ok := w.running[t.Target] 164 if !ok { 165 if glog.V(1) { 166 glog.Infof("reconcilliation found unwatched container %s", t.Target) 167 } 168 169 base, err := w.dockerDecorator(t.Target, w.envWhitelist) 170 if err != nil { 171 glog.Errorf("failed to fetch container information %v", err) 172 continue 173 } 174 t.Action = sources.Add 175 t.Labels = base.Labels 176 updates = append(updates, t) 177 w.running[t.Target] = t 178 } 179 found[t.Target] = t 180 } 181 for tn, t := range w.running { 182 _, ok := found[tn] 183 if !ok { 184 if glog.V(1) { 185 glog.Infof("reconcilliation found watched dead container %s", t.Target) 186 } 187 t.Action = sources.Remove 188 updates = append(updates, t) 189 delete(w.running, t.Target) 190 } 191 } 192 if len(updates) > 0 { 193 w.ups <- updates 194 } 195 } 196 197 func (w *Watcher) reconcile() error { 198 if glog.V(2) { 199 glog.Infof("Running reconcilliation") 200 } 201 202 containers, err := w.dcli.ContainerList(context.Background(), types.ContainerListOptions{}) 203 if err != nil { 204 return err 205 } 206 207 existing := []*sources.Update{} 208 for _, container := range containers { 209 if container.State == "running" { 210 existing = append(existing, &sources.Update{ 211 Target: container.ID, 212 }) 213 } 214 } 215 w.reconcileRunning(existing) 216 return nil 217 } 218 219 func (w *Watcher) watchEvs(ctx context.Context) error { 220 if glog.V(2) { 221 glog.Infof("Running watch loop") 222 } 223 224 evs, err := w.dcli.Events(context.Background(), types.EventsOptions{}) 225 if err != nil { 226 return err 227 } 228 229 scanner := bufio.NewScanner(evs) 230 for scanner.Scan() { 231 select { 232 case <-ctx.Done(): 233 return ctx.Err() 234 default: 235 } 236 func() { 237 w.Lock() 238 defer w.Unlock() 239 str := scanner.Text() 240 ev := evMessage{} 241 err := json.Unmarshal([]byte(str), &ev) 242 if err != nil { 243 glog.Errorf("error unmarshaling docker update%v\n", err) 244 return 245 } 246 247 switch ev.Action { 248 case "die": 249 if glog.V(1) { 250 glog.Infof("Saw die event for %s", ev.ID) 251 } 252 upd, ok := w.running[ev.ID] 253 if !ok { 254 glog.Error("Update for a container we didn't think was running") 255 return 256 } 257 delete(w.running, ev.ID) 258 upd.Action = sources.Remove 259 w.ups <- []*sources.Update{upd} 260 case "start": 261 base, err := w.dockerDecorator(ev.ID, w.envWhitelist) 262 if err != nil { 263 glog.Infof("failed to fetch container information", err) 264 return 265 } 266 if glog.V(1) { 267 glog.Infof("Saw start event for %s", ev.ID) 268 } 269 if _, ok := w.running[ev.ID]; ok { 270 glog.Error("start event for container we are already watching") 271 return 272 } 273 update := &sources.Update{Action: sources.Add, Target: ev.ID, Labels: base.Labels} 274 w.running[ev.ID] = update 275 w.ups <- []*sources.Update{update} 276 default: 277 } 278 }() 279 } 280 if err := scanner.Err(); err != nil { 281 return err 282 } 283 return nil 284 } 285 286 // Next should be called each time you wish to watch for an update. 287 func (w *Watcher) Next(ctx context.Context) ([]*sources.Update, error) { 288 if w.ups == nil { 289 w.startBackground(ctx) 290 } 291 292 select { 293 case u := <-w.ups: 294 if u != nil { 295 return u, nil 296 } 297 return nil, io.EOF 298 case <-ctx.Done(): 299 return nil, ctx.Err() 300 } 301 } 302 303 const ( 304 // ContainerEventType is the event type that containers generate 305 evContainerEventType = "container" 306 // DaemonEventType is the event type that daemon generate 307 evDaemonEventType = "daemon" 308 // ImageEventType is the event type that images generate 309 evImageEventType = "image" 310 // NetworkEventType is the event type that networks generate 311 evNetworkEventType = "network" 312 // PluginEventType is the event type that plugins generate 313 evPluginEventType = "plugin" 314 // VolumeEventType is the event type that volumes 315 // generate 316 evVolumeEventType = "volume" 317 ) 318 319 // Actor describes something that generates events, 320 // like a container, or a network, or a volume. 321 // It has a defined name and a set or attributes. 322 // The container attributes are its labels, other actors 323 // can generate these attributes from other properties. 324 type evActor struct { 325 ID string 326 Attributes map[string]string 327 } 328 329 // Message represents the information an event 330 // contains 331 type evMessage struct { 332 // Deprecated information from JSONMessage. 333 // With data only in container events. 334 Status string `json:"status,omitempty"` 335 State string `json:"state,omitempty"` 336 ID string `json:"id,omitempty"` 337 From string `json:"from,omitempty"` 338 339 Type string 340 Action string 341 Actor evActor 342 343 Time int64 `json:"time,omitempty"` 344 TimeNano int64 `json:"timeNano,omitempty"` 345 } 346 347 func (w *Watcher) dockerDecorator(id string, envWhitelist []*regexp.Regexp) (logspray.Message, error) { 348 hn, _ := os.Hostname() 349 msg := logspray.Message{ 350 Labels: map[string]string{ 351 "instance": hn, 352 "job": "unknown", 353 "container_id": "none", 354 }, 355 } 356 357 vars, err := w.getContainerMetadata(id) 358 if err != nil { 359 return logspray.Message{}, errors.Wrapf(err, "could no get container metadata for %v", id) 360 } 361 362 if v, ok := vars["id"]; ok { 363 msg.Labels["container_id"] = v[0] 364 } 365 366 if v, ok := vars["image"]; ok { 367 parts := strings.Split(v[0], "-") 368 id := parts[len(parts)-1] 369 name := id 370 if len(parts) > 1 { 371 name = strings.Join(parts[0:len(parts)-1], "-") 372 } 373 msg.Labels["container_image_id"] = id 374 msg.Labels["container_image_name"] = name 375 } 376 377 for _, rx := range envWhitelist { 378 for k, vs := range vars { 379 if !rx.MatchString(k) { 380 continue 381 } 382 if len(vs) != 1 { 383 if glog.V(1) { 384 glog.Infof("picking first item from environment variable %s with %d values", k, len(vars[k])) 385 } 386 } 387 msg.Labels[fmt.Sprintf("container_env_%s", strings.ToLower(k))] = vs[0] 388 } 389 } 390 391 for lk, lv := range vars { 392 if !strings.HasPrefix(lk, "label_") { 393 continue 394 } 395 msg.Labels[fmt.Sprintf("container_%s", lk)] = lv[0] 396 } 397 398 return msg, nil 399 } 400 401 // getContainerMetadata takes docker container id and returns some 402 // metadata. 403 func (w *Watcher) getContainerMetadata(id string) (map[string][]string, error) { 404 cinfo, _, err := w.dcli.ContainerInspectWithRaw(context.Background(), id, false) 405 if err != nil { 406 return nil, err 407 } 408 409 if cinfo.State.Status == "exited" { 410 return nil, errors.New("ignore exited container") 411 } 412 413 res := map[string][]string{} 414 res["id"] = []string{cinfo.ID} 415 res["image"] = []string{cinfo.Config.Image} 416 for _, str := range cinfo.Config.Env { 417 ss := strings.SplitN(str, "=", 2) 418 k := ss[0] 419 v := ss[1] 420 res[k] = append(res[k], v) 421 } 422 423 for lk, lv := range cinfo.Config.Labels { 424 res[fmt.Sprintf("label_%s", lk)] = []string{lv} 425 } 426 427 return res, nil 428 }