| //go:build !windows | |
| package metrics | |
| import ( | |
| "context" | |
| "net" | |
| "net/http" | |
| "os" | |
| "sync" | |
| "time" | |
| "github.com/containerd/log" | |
| gometrics "github.com/docker/go-metrics" | |
| "github.com/moby/moby/v2/daemon/pkg/plugin" | |
| "github.com/moby/moby/v2/pkg/plugingetter" | |
| "github.com/moby/moby/v2/pkg/plugins" | |
| "github.com/opencontainers/runtime-spec/specs-go" | |
| "github.com/pkg/errors" | |
| ) | |
| const pluginType = "MetricsCollector" | |
| // Plugin represents a metrics collector plugin | |
| type Plugin interface { | |
| StartMetrics() error | |
| StopMetrics() error | |
| } | |
| type metricsPluginAdapter struct { | |
| client *plugins.Client | |
| } | |
| func (a *metricsPluginAdapter) StartMetrics() error { | |
| return a.client.Call("/MetricsCollector.StartMetrics", nil, nil) | |
| } | |
| func (a *metricsPluginAdapter) StopMetrics() error { | |
| return a.client.Call("/MetricsCollector.StopMetrics", nil, nil) | |
| } | |
| func makePluginAdapter(p plugingetter.CompatPlugin) (Plugin, error) { | |
| adapted := p.Client() | |
| return &metricsPluginAdapter{adapted}, nil | |
| } | |
| // RegisterPlugin starts the metrics server listener and registers the metrics plugin | |
| // callback with the plugin store | |
| func RegisterPlugin(store *plugin.Store, path string) error { | |
| if err := listen(path); err != nil { | |
| return err | |
| } | |
| store.RegisterRuntimeOpt(pluginType, func(s *specs.Spec) { | |
| f := plugin.WithSpecMounts([]specs.Mount{ | |
| {Type: "bind", Source: path, Destination: "/run/docker/metrics.sock", Options: []string{"bind", "ro"}}, | |
| }) | |
| f(s) | |
| }) | |
| store.Handle(pluginType, 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 := store.Get(name, pluginType, plugingetter.Lookup) | |
| if err != nil { | |
| return | |
| } | |
| adapter, err := makePluginAdapter(p) | |
| if err != nil { | |
| log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error creating plugin adapter") | |
| } | |
| if err := adapter.StartMetrics(); err != nil { | |
| log.G(context.TODO()).WithError(err).WithField("plugin", p.Name()).Error("Error starting metrics collector plugin") | |
| } | |
| }) | |
| return nil | |
| } | |
| // CleanupPlugin stops metrics collection for all plugins | |
| func CleanupPlugin(store plugingetter.PluginGetter) { | |
| var wg sync.WaitGroup | |
| for _, p := range store.GetAllManagedPluginsByCap(pluginType) { | |
| wg.Go(func() { | |
| adapter, err := makePluginAdapter(p) | |
| if err != nil { | |
| log.G(context.TODO()).WithFields(log.Fields{ | |
| "error": err, | |
| "plugin": p.Name(), | |
| }).Error("Error creating metrics plugin adapter") | |
| return | |
| } | |
| if err := adapter.StopMetrics(); err != nil { | |
| log.G(context.TODO()).WithFields(log.Fields{ | |
| "error": err, | |
| "plugin": p.Name(), | |
| }).Error("Error stopping plugin metrics collection") | |
| } | |
| }) | |
| } | |
| wg.Wait() | |
| if listener != nil { | |
| _ = listener.Close() | |
| } | |
| } | |
| var listener net.Listener | |
| func listen(path string) error { | |
| _ = os.Remove(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", gometrics.Handler()) | |
| go func() { | |
| log.G(context.TODO()).Debugf("metrics API listening on %s", l.Addr()) | |
| srv := &http.Server{ | |
| Handler: mux, | |
| ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. | |
| } | |
| if err := srv.Serve(l); err != nil && !errors.Is(err, net.ErrClosed) { | |
| log.G(context.TODO()).WithError(err).Error("error serving metrics API") | |
| } | |
| }() | |
| listener = l | |
| return nil | |
| } |