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  }