浏览代码

Merge pull request #32874 from cpuguy83/metrics_plugins

Add support for metrics plugins
Tibor Vass 8 年之前
父节点
当前提交
680084b2a2

+ 11 - 0
daemon/daemon.go

@@ -106,6 +106,7 @@ type Daemon struct {
 	defaultIsolation          containertypes.Isolation // Default isolation mode on Windows
 	defaultIsolation          containertypes.Isolation // Default isolation mode on Windows
 	clusterProvider           cluster.Provider
 	clusterProvider           cluster.Provider
 	cluster                   Cluster
 	cluster                   Cluster
+	metricsPluginListener     net.Listener
 
 
 	machineMemory uint64
 	machineMemory uint64
 
 
@@ -593,6 +594,12 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe
 	d.PluginStore = pluginStore
 	d.PluginStore = pluginStore
 	logger.RegisterPluginGetter(d.PluginStore)
 	logger.RegisterPluginGetter(d.PluginStore)
 
 
+	metricsSockPath, err := d.listenMetricsSock()
+	if err != nil {
+		return nil, err
+	}
+	registerMetricsPluginCallback(d.PluginStore, metricsSockPath)
+
 	// Plugin system initialization should happen before restore. Do not change order.
 	// Plugin system initialization should happen before restore. Do not change order.
 	d.pluginManager, err = plugin.NewManager(plugin.ManagerConfig{
 	d.pluginManager, err = plugin.NewManager(plugin.ManagerConfig{
 		Root:               filepath.Join(config.Root, "plugins"),
 		Root:               filepath.Join(config.Root, "plugins"),
@@ -821,6 +828,8 @@ func (daemon *Daemon) Shutdown() error {
 	if daemon.configStore.LiveRestoreEnabled && daemon.containers != nil {
 	if daemon.configStore.LiveRestoreEnabled && daemon.containers != nil {
 		// check if there are any running containers, if none we should do some cleanup
 		// check if there are any running containers, if none we should do some cleanup
 		if ls, err := daemon.Containers(&types.ContainerListOptions{}); len(ls) != 0 || err != nil {
 		if ls, err := daemon.Containers(&types.ContainerListOptions{}); len(ls) != 0 || err != nil {
+			// metrics plugins still need some cleanup
+			daemon.cleanupMetricsPlugins()
 			return nil
 			return nil
 		}
 		}
 	}
 	}
@@ -861,6 +870,8 @@ func (daemon *Daemon) Shutdown() error {
 		daemon.DaemonLeavesCluster()
 		daemon.DaemonLeavesCluster()
 	}
 	}
 
 
+	daemon.cleanupMetricsPlugins()
+
 	// Shutdown plugins after containers and layerstore. Don't change the order.
 	// Shutdown plugins after containers and layerstore. Don't change the order.
 	daemon.pluginShutdown()
 	daemon.pluginShutdown()
 
 

+ 66 - 0
daemon/metrics.go

@@ -1,12 +1,19 @@
 package daemon
 package daemon
 
 
 import (
 import (
+	"path/filepath"
 	"sync"
 	"sync"
 
 
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/pkg/mount"
+	"github.com/docker/docker/pkg/plugingetter"
 	"github.com/docker/go-metrics"
 	"github.com/docker/go-metrics"
+	"github.com/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus"
 )
 )
 
 
+const metricsPluginType = "MetricsCollector"
+
 var (
 var (
 	containerActions          metrics.LabeledTimer
 	containerActions          metrics.LabeledTimer
 	containerStates           metrics.LabeledGauge
 	containerStates           metrics.LabeledGauge
@@ -106,3 +113,62 @@ func (ctr *stateCounter) Collect(ch chan<- prometheus.Metric) {
 	ch <- prometheus.MustNewConstMetric(ctr.desc, prometheus.GaugeValue, float64(paused), "paused")
 	ch <- prometheus.MustNewConstMetric(ctr.desc, prometheus.GaugeValue, float64(paused), "paused")
 	ch <- prometheus.MustNewConstMetric(ctr.desc, prometheus.GaugeValue, float64(stopped), "stopped")
 	ch <- prometheus.MustNewConstMetric(ctr.desc, prometheus.GaugeValue, float64(stopped), "stopped")
 }
 }
+
+func (d *Daemon) cleanupMetricsPlugins() {
+	ls := d.PluginStore.GetAllManagedPluginsByCap(metricsPluginType)
+	var wg sync.WaitGroup
+	wg.Add(len(ls))
+
+	for _, p := range ls {
+		go func() {
+			defer wg.Done()
+			pluginStopMetricsCollection(p)
+		}()
+	}
+	wg.Wait()
+
+	if d.metricsPluginListener != nil {
+		d.metricsPluginListener.Close()
+	}
+}
+
+type metricsPlugin struct {
+	plugingetter.CompatPlugin
+}
+
+func (p metricsPlugin) sock() string {
+	return "metrics.sock"
+}
+
+func (p metricsPlugin) sockBase() string {
+	return filepath.Join(p.BasePath(), "run", "docker")
+}
+
+func pluginStartMetricsCollection(p plugingetter.CompatPlugin) error {
+	type metricsPluginResponse struct {
+		Err string
+	}
+	var res metricsPluginResponse
+	if err := p.Client().Call(metricsPluginType+".StartMetrics", nil, &res); err != nil {
+		return errors.Wrap(err, "could not start metrics plugin")
+	}
+	if res.Err != "" {
+		return errors.New(res.Err)
+	}
+	return nil
+}
+
+func pluginStopMetricsCollection(p plugingetter.CompatPlugin) {
+	if err := p.Client().Call(metricsPluginType+".StopMetrics", nil, nil); err != nil {
+		logrus.WithError(err).WithField("name", p.Name()).Error("error stopping metrics collector")
+	}
+
+	mp := metricsPlugin{p}
+	sockPath := filepath.Join(mp.sockBase(), mp.sock())
+	if err := mount.Unmount(sockPath); err != nil {
+		if mounted, _ := mount.Mounted(sockPath); mounted {
+			logrus.WithError(err).WithField("name", p.Name()).WithField("socket", sockPath).Error("error unmounting metrics socket for plugin")
+		}
+	}
+	return
+}

+ 86 - 0
daemon/metrics_unix.go

@@ -0,0 +1,86 @@
+// +build !windows
+
+package daemon
+
+import (
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/pkg/mount"
+	"github.com/docker/docker/pkg/plugingetter"
+	"github.com/docker/docker/pkg/plugins"
+	metrics "github.com/docker/go-metrics"
+	"github.com/pkg/errors"
+)
+
+func (daemon *Daemon) listenMetricsSock() (string, error) {
+	path := filepath.Join(daemon.configStore.ExecRoot, "metrics.sock")
+	syscall.Unlink(path)
+	l, err := net.Listen("unix", path)
+	if err != nil {
+		return "", errors.Wrap(err, "error setting up metrics plugin listener")
+	}
+
+	mux := http.NewServeMux()
+	mux.Handle("/metrics", metrics.Handler())
+	go func() {
+		http.Serve(l, mux)
+	}()
+	daemon.metricsPluginListener = l
+	return path, nil
+}
+
+func registerMetricsPluginCallback(getter plugingetter.PluginGetter, sockPath string) {
+	getter.Handle(metricsPluginType, func(name string, client *plugins.Client) {
+		// Use lookup since nothing in the system can really reference it, no need
+		// to protect against removal
+		p, err := getter.Get(name, metricsPluginType, plugingetter.Lookup)
+		if err != nil {
+			return
+		}
+
+		mp := metricsPlugin{p}
+		sockBase := mp.sockBase()
+		if err := os.MkdirAll(sockBase, 0755); err != nil {
+			logrus.WithError(err).WithField("name", name).WithField("path", sockBase).Error("error creating metrics plugin base path")
+			return
+		}
+
+		defer func() {
+			if err != nil {
+				os.RemoveAll(sockBase)
+			}
+		}()
+
+		pluginSockPath := filepath.Join(sockBase, mp.sock())
+		_, err = os.Stat(pluginSockPath)
+		if err == nil {
+			mount.Unmount(pluginSockPath)
+		} else {
+			logrus.WithField("path", pluginSockPath).Debugf("creating plugin socket")
+			f, err := os.OpenFile(pluginSockPath, os.O_CREATE, 0600)
+			if err != nil {
+				return
+			}
+			f.Close()
+		}
+
+		if err := mount.Mount(sockPath, pluginSockPath, "none", "bind,ro"); err != nil {
+			logrus.WithError(err).WithField("name", name).Error("could not mount metrics socket to plugin")
+			return
+		}
+
+		if err := pluginStartMetricsCollection(p); err != nil {
+			if err := mount.Unmount(pluginSockPath); err != nil {
+				if mounted, _ := mount.Mounted(pluginSockPath); mounted {
+					logrus.WithError(err).WithField("sock_path", pluginSockPath).Error("error unmounting metrics socket from plugin during cleanup")
+				}
+			}
+			logrus.WithError(err).WithField("name", name).Error("error while initializing metrics plugin")
+		}
+	})
+}

+ 12 - 0
daemon/metrics_unsupported.go

@@ -0,0 +1,12 @@
+// +build windows
+
+package daemon
+
+import "github.com/docker/docker/pkg/plugingetter"
+
+func registerMetricsPluginCallback(getter plugingetter.PluginGetter, sockPath string) {
+}
+
+func (daemon *Daemon) listenMetricsSock() (string, error) {
+	return "", nil
+}

+ 2 - 0
docs/extend/config.md

@@ -61,6 +61,8 @@ Config provides the base accessible fields for working with V0 plugin format
 
 
         - **docker.logdriver/1.0**
         - **docker.logdriver/1.0**
 
 
+        - **docker.metricscollector/1.0**
+
     - **`socket`** *string*
     - **`socket`** *string*
 
 
       socket is the name of the socket the engine should use to communicate with the plugins.
       socket is the name of the socket the engine should use to communicate with the plugins.

+ 85 - 0
docs/extend/plugins_metrics.md

@@ -0,0 +1,85 @@
+---
+title: "Docker metrics collector plugins"
+description: "Metrics plugins."
+keywords: "Examples, Usage, plugins, docker, documentation, user guide, metrics"
+---
+
+<!-- This file is maintained within the docker/docker Github
+     repository at https://github.com/docker/docker/. Make all
+     pull requests against that repo. If you see this file in
+     another repository, consider it read-only there, as it will
+     periodically be overwritten by the definitive file. Pull
+     requests which include edits to this file in other repositories
+     will be rejected.
+-->
+
+# Metrics Collector Plugins
+
+Docker exposes internal metrics based on the prometheus format. Metrics plugins
+enable accessing these metrics in a consistent way by providing a Unix
+socket at a predefined path where the plugin can scrape the metrics.
+
+> **Note**: that while the plugin interface for metrics is non-experimental, the naming
+of the metrics and metric labels is still considered experimental and may change
+in a future version.
+
+## Creating a metrics plugin
+
+You must currently set `PropagatedMount` in the plugin `config.json` to
+`/run/docker`. This allows the plugin to receive updated mounts
+(the bind-mounted socket) from Docker after the plugin is already configured.
+
+## MetricsCollector protocol
+
+Metrics plugins must register as implementing the`MetricsCollector` interface
+in `config.json`.
+
+On Unix platforms, the socket is located at `/run/docker/metrics.sock` in the
+plugin's rootfs.
+
+`MetricsCollector` must implement two endpoints:
+
+### `MetricsCollector.StartMetrics`
+
+Signals to the plugin that the metrics socket is now available for scraping
+
+**Request**
+```json
+{}
+```
+
+The request has no playload.
+
+**Response**
+```json
+{
+	"Err": ""
+}
+```
+
+If an error occurred during this request, add an error message to the `Err` field
+in the response. If no error then you can either send an empty response (`{}`)
+or an empty value for the `Err` field. Errors will only be logged.
+
+### `MetricsCollector.StopMetrics`
+
+Signals to the plugin that the metrics socket is no longer available.
+This may happen when the daemon is shutting down.
+
+**Request**
+```json
+{}
+```
+
+The request has no playload.
+
+**Response**
+```json
+{
+	"Err": ""
+}
+```
+
+If an error occurred during this request, add an error message to the `Err` field
+in the response. If no error then you can either send an empty response (`{}`)
+or an empty value for the `Err` field. Errors will only be logged.

+ 2 - 2
docs/reference/commandline/plugin_ls.md

@@ -55,7 +55,7 @@ than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "b
 The currently supported filters are:
 The currently supported filters are:
 
 
 * enabled (boolean - true or false, 0 or 1)
 * enabled (boolean - true or false, 0 or 1)
-* capability (string - currently `volumedriver`, `networkdriver`, `ipamdriver`, or `authz`)
+* capability (string - currently `volumedriver`, `networkdriver`, `ipamdriver`, `logdriver`, `metricscollector`, or `authz`)
 
 
 #### enabled
 #### enabled
 
 
@@ -65,7 +65,7 @@ The `enabled` filter matches on plugins enabled or disabled.
 
 
 The `capability` filter matches on plugin capabilities. One plugin
 The `capability` filter matches on plugin capabilities. One plugin
 might have multiple capabilities. Currently `volumedriver`, `networkdriver`,
 might have multiple capabilities. Currently `volumedriver`, `networkdriver`,
-`ipamdriver`, and `authz` are supported capabilities.
+`ipamdriver`, `logdriver`, `metricscollector`, and `authz` are supported capabilities.
 
 
 ```bash
 ```bash
 $ docker plugin install --disable tiborvass/no-remove
 $ docker plugin install --disable tiborvass/no-remove

+ 23 - 0
integration-cli/docker_cli_plugins_test.go

@@ -3,12 +3,14 @@ package main
 import (
 import (
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
+	"net/http"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/integration-cli/checker"
 	"github.com/docker/docker/integration-cli/checker"
 	"github.com/docker/docker/integration-cli/cli"
 	"github.com/docker/docker/integration-cli/cli"
+	"github.com/docker/docker/integration-cli/daemon"
 	icmd "github.com/docker/docker/pkg/testutil/cmd"
 	icmd "github.com/docker/docker/pkg/testutil/cmd"
 	"github.com/go-check/check"
 	"github.com/go-check/check"
 )
 )
@@ -455,3 +457,24 @@ func (s *DockerSuite) TestPluginUpgrade(c *check.C) {
 	dockerCmd(c, "volume", "inspect", "bananas")
 	dockerCmd(c, "volume", "inspect", "bananas")
 	dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "ls -lh /apple/core")
 	dockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "ls -lh /apple/core")
 }
 }
+
+func (s *DockerSuite) TestPluginMetricsCollector(c *check.C) {
+	testRequires(c, DaemonIsLinux, Network, SameHostDaemon, IsAmd64)
+	d := daemon.New(c, dockerBinary, dockerdBinary, daemon.Config{})
+	d.Start(c)
+	defer d.Stop(c)
+
+	name := "cpuguy83/docker-metrics-plugin-test:latest"
+	r := cli.Docker(cli.Args("plugin", "install", "--grant-all-permissions", name), cli.Daemon(d))
+	c.Assert(r.Error, checker.IsNil, check.Commentf(r.Combined()))
+
+	// plugin lisens on localhost:19393 and proxies the metrics
+	resp, err := http.Get("http://localhost:19393/metrics")
+	c.Assert(err, checker.IsNil)
+	defer resp.Body.Close()
+
+	b, err := ioutil.ReadAll(resp.Body)
+	c.Assert(err, checker.IsNil)
+	// check that a known metric is there... don't epect this metric to change over time.. probably safe
+	c.Assert(string(b), checker.Contains, "container_actions")
+}