diff --git a/README.md b/README.md index 1d6a4f2..b97ee6e 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ The exporter parses all tagging formats by default, but individual tagging forma NOTE: Version 0.7.0 switched to the [kingpin](https://github.com/alecthomas/kingpin) flags library. With this change, flag behaviour is POSIX-ish: * long flags start with two dashes (`--version`) +* boolean long flags are disabled by prefixing with no (`--flag-name` is true, `--no-flag-name` is false) * multiple short flags can be combined (but there currently is only one) * flag processing stops at the first `--` @@ -102,6 +103,7 @@ NOTE: Version 0.7.0 switched to the [kingpin](https://github.com/alecthomas/king --web.listen-address=":9102" The address on which to expose the web interface and generated Prometheus metrics. + --web.enable-lifecycle Enable shutdown and reload via HTTP request. --web.telemetry-path="/metrics" Path under which to expose metrics. --statsd.listen-udp=":9125" @@ -154,6 +156,11 @@ NOTE: Version 0.7.0 switched to the [kingpin](https://github.com/alecthomas/king --version Show application version. ``` +## Lifecycle API + +The `statsd_exporter` has an optional lifecycle API (disabled by default) that can be used to reload or quit the exporter +by sending a `PUT` or `POST` request to the `/-/reload` or `/-/quit` endpoints. + ## Tests $ go test diff --git a/main.go b/main.go index 7ea1ac0..27a529c 100644 --- a/main.go +++ b/main.go @@ -200,23 +200,12 @@ func (u uncheckedCollector) Collect(c chan<- prometheus.Metric) { u.c.Collect(c) } -func serveHTTP(listenAddress, metricsEndpoint string, logger log.Logger) { - http.Handle(metricsEndpoint, promhttp.Handler()) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(` - StatsD Exporter - -

StatsD Exporter

-

Metrics

- - `)) - }) - level.Error(logger).Log("msg", http.ListenAndServe(listenAddress, nil)) +func serveHTTP(mux http.Handler, listenAddress string, logger log.Logger) { + level.Error(logger).Log("msg", http.ListenAndServe(listenAddress, mux)) os.Exit(1) } -func configReloader(fileName string, mapper *mapper.MetricMapper, cacheSize int, logger log.Logger, option mapper.CacheOption) { - +func sighupConfigReloader(fileName string, mapper *mapper.MetricMapper, cacheSize int, logger log.Logger, option mapper.CacheOption) { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGHUP) @@ -225,15 +214,21 @@ func configReloader(fileName string, mapper *mapper.MetricMapper, cacheSize int, level.Warn(logger).Log("msg", "Received signal but no mapping config to reload", "signal", s) continue } + level.Info(logger).Log("msg", "Received signal, attempting reload", "signal", s) - err := mapper.InitFromFile(fileName, cacheSize, option) - if err != nil { - level.Info(logger).Log("msg", "Error reloading config", "error", err) - configLoads.WithLabelValues("failure").Inc() - } else { - level.Info(logger).Log("msg", "Config reloaded successfully") - configLoads.WithLabelValues("success").Inc() - } + + reloadConfig(fileName, mapper, cacheSize, logger, option) + } +} + +func reloadConfig(fileName string, mapper *mapper.MetricMapper, cacheSize int, logger log.Logger, option mapper.CacheOption) { + err := mapper.InitFromFile(fileName, cacheSize, option) + if err != nil { + level.Info(logger).Log("msg", "Error reloading config", "error", err) + configLoads.WithLabelValues("failure").Inc() + } else { + level.Info(logger).Log("msg", "Config reloaded successfully") + configLoads.WithLabelValues("success").Inc() } } @@ -254,6 +249,7 @@ func dumpFSM(mapper *mapper.MetricMapper, dumpFilename string, logger log.Logger func main() { var ( listenAddress = kingpin.Flag("web.listen-address", "The address on which to expose the web interface and generated Prometheus metrics.").Default(":9102").String() + enableLifecycle = kingpin.Flag("web.enable-lifecycle", "Enable shutdown and reload via HTTP request.").Default("false").Bool() metricsEndpoint = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() statsdListenUDP = kingpin.Flag("statsd.listen-udp", "The UDP address on which to receive statsd metric lines. \"\" disables it.").Default(":9125").String() statsdListenTCP = kingpin.Flag("statsd.listen-tcp", "The TCP address on which to receive statsd metric lines. \"\" disables it.").Default(":9125").String() @@ -308,8 +304,6 @@ func main() { level.Info(logger).Log("msg", "Accepting StatsD Traffic", "udp", *statsdListenUDP, "tcp", *statsdListenTCP, "unixgram", *statsdListenUnixgram) level.Info(logger).Log("msg", "Accepting Prometheus Requests", "addr", *listenAddress) - go serveHTTP(*listenAddress, *metricsEndpoint, logger) - events := make(chan event.Events, *eventQueueSize) defer close(events) eventQueue := event.NewEventQueue(events, *eventFlushThreshold, *eventFlushInterval, eventsFlushed) @@ -470,10 +464,42 @@ func main() { return } + mux := http.NewServeMux() + mux.Handle(*metricsEndpoint, promhttp.Handler()) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + StatsD Exporter + +

StatsD Exporter

+

Metrics

+ + `)) + }) + if *enableLifecycle { + mux.HandleFunc("/-/reload", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut || r.Method == http.MethodPost { + if *mappingConfig == "" { + level.Warn(logger).Log("msg", "Received lifecycle api reload but no mapping config to reload") + return + } + level.Info(logger).Log("msg", "Received lifecycle api reload, attempting reload") + reloadConfig(*mappingConfig, mapper, *cacheSize, logger, cacheOption) + } + }) + mux.HandleFunc("/-/quit", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut || r.Method == http.MethodPost { + level.Info(logger).Log("msg", "Received lifecycle api quit, exiting") + os.Exit(0) + } + }) + } + + go serveHTTP(mux, *listenAddress, logger) + signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt, syscall.SIGTERM) - go configReloader(*mappingConfig, mapper, *cacheSize, logger, cacheOption) + go sighupConfigReloader(*mappingConfig, mapper, *cacheSize, logger, cacheOption) go exporter.Listen(events) <-signals