mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Add support for metrics plugins
Allows for a plugin type that can be used to scrape metrics. This is useful because metrics are not neccessarily at a standard location... `--metrics-addr` must be set, and must currently be a TCP socket. Even if metrics are done via a unix socket, there's no guarentee where the socket may be located on the system, making bind-mounting such a socket into a container difficult (and racey, failure-prone on daemon restart). Metrics plugins side-step this issue by always listening on a unix socket and then bind-mounting that into a known path in the plugin container. Note there has been similar work in the past (and ultimately punted at the time) for consistent access to the Docker API from within a container. Why not add metrics to the Docker API and just provide a plugin with access to the Docker API? Certainly this can be useful, but gives a lot of control/access to a plugin that may only need the metrics. We can look at supporting API plugins separately for this reason. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
parent
1245866249
commit
0e8e8f0f31
8 changed files with 287 additions and 2 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
daemon/metrics_unix.go
Normal file
86
daemon/metrics_unix.go
Normal file
|
@ -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
daemon/metrics_unsupported.go
Normal file
12
daemon/metrics_unsupported.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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
docs/extend/plugins_metrics.md
Normal file
85
docs/extend/plugins_metrics.md
Normal file
|
@ -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.
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue