stats.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. package nebula
  2. import (
  3. "context"
  4. "crypto/fips140"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "net/http"
  10. "runtime"
  11. "strconv"
  12. "sync"
  13. "sync/atomic"
  14. "time"
  15. mp "github.com/nbrownus/go-metrics-prometheus"
  16. "github.com/prometheus/client_golang/prometheus"
  17. "github.com/prometheus/client_golang/prometheus/promhttp"
  18. "github.com/rcrowley/go-metrics"
  19. "github.com/slackhq/nebula/config"
  20. )
  21. // statsServer owns nebula's stats subsystem: the periodic metric capture
  22. // goroutine and (for prometheus) an HTTP listener. It mirrors the lifecycle
  23. // shape of dnsServer: constructor wires the reload callback, reload records
  24. // config, Start builds and runs the runtime, Stop tears it down.
  25. type statsServer struct {
  26. l *slog.Logger
  27. ctx context.Context
  28. buildVersion string
  29. configTest bool
  30. // enabled mirrors "stats configured to a real backend". Start consults
  31. // it so callers don't need to know the gating rules.
  32. enabled atomic.Bool
  33. runMu sync.Mutex
  34. runCfg *statsConfig
  35. run *statsRuntime // non-nil while a runtime is live
  36. // Closed when the last runtime's capture loop has returned. Stop waits on it so nothing captures once it returns,
  37. // and a reload's new runtime never overlaps the old one, go-metrics keeps its GC capture state in unguarded globals
  38. loopDone chan struct{}
  39. // See newStatsServerFromConfig, never changes after it
  40. emitStats func()
  41. }
  42. // statsRuntime is the live state owned by a single Start invocation. Start
  43. // stashes a pointer under runMu; Stop and Start's own exit path use pointer
  44. // equality to tell "my runtime" apart from one that replaced it after a
  45. // reload.
  46. type statsRuntime struct {
  47. cancel context.CancelFunc
  48. listener *http.Server // nil for graphite
  49. }
  50. // statsConfig is the snapshot of stats-related config that drives the runtime.
  51. // It is comparable with == so reload can detect "no change" cheaply.
  52. type statsConfig struct {
  53. typ string
  54. interval time.Duration
  55. graphite graphiteConfig
  56. prom promConfig
  57. }
  58. type graphiteConfig struct {
  59. protocol string
  60. host string
  61. // resolvedAddr is the string form of host resolved at config-load time.
  62. // Including it in the struct means a SIGHUP picks up DNS changes even
  63. // when stats.host hasn't been edited.
  64. resolvedAddr string
  65. prefix string
  66. }
  67. type promConfig struct {
  68. listen string
  69. path string
  70. namespace string
  71. subsystem string
  72. }
  73. // newStatsServerFromConfig builds a statsServer, applies the initial config,
  74. // and registers a reload callback. The reload callback is registered before
  75. // the initial config is applied so a SIGHUP can later enable, fix, or disable
  76. // stats even if the initial application failed.
  77. //
  78. // Start is safe to call unconditionally: it no-ops when stats are disabled.
  79. // The returned pointer is always non-nil, even on error.
  80. //
  81. // emitStats, if set, updates metrics on every capture, ahead of the exporters so each export sees what it just set. It
  82. // is fixed here, before the reload callback exists, so every runtime has it.
  83. func newStatsServerFromConfig(ctx context.Context, l *slog.Logger, c *config.C, buildVersion string, configTest bool, emitStats func()) (*statsServer, error) {
  84. s := &statsServer{
  85. l: l,
  86. ctx: ctx,
  87. buildVersion: buildVersion,
  88. configTest: configTest,
  89. emitStats: emitStats,
  90. }
  91. c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
  92. if err := s.reload(c, false); err != nil {
  93. s.l.Error("Failed to reload stats from config", "error", err)
  94. r.Failed()
  95. }
  96. })
  97. if err := s.reload(c, true); err != nil {
  98. return s, err
  99. }
  100. return s, nil
  101. }
  102. // reload records the latest config. On the initial call it only records it;
  103. // Control.Start is what launches the first runtime via statsStart. On later
  104. // calls it reconciles the running runtime with the new config:
  105. //
  106. // - newly enabled -> spawn Start
  107. // - newly disabled -> Stop the runtime
  108. // - config changed (still enabled) -> Stop the old, Start the new
  109. // - no change -> no-op
  110. func (s *statsServer) reload(c *config.C, initial bool) error {
  111. newCfg, err := loadStatsConfig(c)
  112. if err != nil {
  113. return err
  114. }
  115. enabled := newCfg.typ != "" && newCfg.typ != "none"
  116. s.runMu.Lock()
  117. sameCfg := s.runCfg != nil && *s.runCfg == newCfg
  118. s.runCfg = &newCfg
  119. s.runMu.Unlock()
  120. s.enabled.Store(enabled)
  121. if initial || sameCfg {
  122. return nil
  123. }
  124. // Even with nothing running, a runtime whose listener failed can still be finishing its last pass
  125. s.Stop()
  126. // Bind here rather than in the background, so a reload that can't listen says so
  127. run, err := s.start()
  128. if err != nil {
  129. return err
  130. }
  131. if run != nil {
  132. go run()
  133. }
  134. return nil
  135. }
  136. // Start builds the runtime from the latest config, spawns the capture loop,
  137. // and blocks until Stop is called or ctx fires. For prometheus it also serves
  138. // the HTTP listener. For graphite it blocks on the capture loop's context.
  139. // Safe to call when stats are disabled or already running (both no-op).
  140. func (s *statsServer) Start() {
  141. run, err := s.start()
  142. if err != nil {
  143. s.l.Error("Failed to start the prometheus stats listener", "error", err)
  144. return
  145. }
  146. if run != nil {
  147. run()
  148. }
  149. }
  150. // start claims the runtime and binds the prometheus listener. The returned run serves it and blocks until Stop or ctx
  151. // ends it. A nil run and no error means stats are off or already running.
  152. func (s *statsServer) start() (func(), error) {
  153. if !s.enabled.Load() || s.configTest {
  154. return nil, nil
  155. }
  156. s.runMu.Lock()
  157. defer s.runMu.Unlock()
  158. if s.ctx.Err() != nil || s.run != nil || s.runCfg == nil {
  159. return nil, nil
  160. }
  161. cfg := *s.runCfg
  162. runCtx, cancel := context.WithCancel(s.ctx)
  163. captureFns, listener := s.buildRuntime(runCtx, cfg)
  164. var ln net.Listener
  165. if listener != nil {
  166. var err error
  167. ln, err = net.Listen("tcp", listener.Addr)
  168. if err != nil {
  169. cancel()
  170. // Drop the config so a SIGHUP with the same one tries again once the problem is fixed
  171. s.runCfg = nil
  172. return nil, err
  173. }
  174. }
  175. rt := &statsRuntime{cancel: cancel, listener: listener}
  176. s.run = rt
  177. done := make(chan struct{})
  178. s.loopDone = done
  179. return func() { s.serve(runCtx, rt, captureFns, cfg.interval, ln, done) }, nil
  180. }
  181. // serve runs the capture loop, and the prometheus listener on ln, until Stop or ctx ends them.
  182. func (s *statsServer) serve(runCtx context.Context, rt *statsRuntime, captureFns []func(), interval time.Duration, ln net.Listener, done chan struct{}) {
  183. // Prime before serving, so a scrape that lands before the first tick sees real values instead of zeros (issue #907)
  184. for _, fn := range captureFns {
  185. fn()
  186. }
  187. go captureStatsLoop(runCtx, interval, captureFns, done)
  188. cleanExit := true
  189. if rt.listener == nil {
  190. // Graphite: no HTTP listener to serve; block until teardown.
  191. <-runCtx.Done()
  192. } else {
  193. cleanExit = s.serveListener(rt.listener, ln)
  194. }
  195. // Clear our runtime only if nothing has replaced it. Stop races through
  196. // here too but leaves s.run == nil, so the pointer check skips.
  197. s.runMu.Lock()
  198. if s.run == rt {
  199. rt.cancel()
  200. s.run = nil
  201. // A listener that exited with an error leaves runCfg cached as if it
  202. // were applied. Drop it so a SIGHUP with the same config re-triggers
  203. // Start once the user fixes the underlying problem.
  204. if !cleanExit {
  205. s.runCfg = nil
  206. }
  207. }
  208. s.runMu.Unlock()
  209. }
  210. // serveListener serves listener on ln and ensures ctx cancellation unblocks it.
  211. // Returns true if the listener exited cleanly (Stop, ctx cancellation, or any
  212. // other http.ErrServerClosed path), false on an unexpected error.
  213. func (s *statsServer) serveListener(listener *http.Server, ln net.Listener) bool {
  214. // Per-invocation watcher: ctx cancellation triggers a listener shutdown
  215. // which in turn unblocks Serve. Closing `done` on exit keeps
  216. // the watcher from outliving this call.
  217. done := make(chan struct{})
  218. go func() {
  219. select {
  220. case <-s.ctx.Done():
  221. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  222. defer cancel()
  223. if err := listener.Shutdown(shutdownCtx); err != nil {
  224. s.l.Warn("Failed to shut down prometheus stats listener", "error", err)
  225. }
  226. case <-done:
  227. }
  228. }()
  229. defer close(done)
  230. s.l.Info("Starting prometheus stats listener", "addr", ln.Addr())
  231. err := listener.Serve(ln)
  232. if err == nil || errors.Is(err, http.ErrServerClosed) {
  233. return true
  234. }
  235. s.l.Error("Prometheus stats listener exited", "error", err)
  236. return false
  237. }
  238. // Stop tears down the active runtime, if any. Idempotent.
  239. func (s *statsServer) Stop() {
  240. // Held until the last pass is done, a Start that slipped in after s.run cleared would capture alongside it
  241. s.runMu.Lock()
  242. defer s.runMu.Unlock()
  243. rt := s.run
  244. s.run = nil
  245. if rt != nil {
  246. rt.cancel()
  247. if rt.listener != nil {
  248. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  249. if err := rt.listener.Shutdown(shutdownCtx); err != nil {
  250. s.l.Warn("Failed to shut down prometheus stats listener", "error", err)
  251. }
  252. cancel()
  253. }
  254. }
  255. // Nothing captures once Stop returns. The last runtime is cancelled by now, whoever cleared it, so this is at most
  256. // the pass it was in. Nothing in a pass takes runMu
  257. if s.loopDone != nil {
  258. <-s.loopDone
  259. }
  260. }
  261. // buildRuntime produces the capture functions and, for prometheus, an un-served
  262. // http.Server from cfg. cfg has already been validated by loadStatsConfig.
  263. // A graphite send in progress is abandoned when ctx is done.
  264. func (s *statsServer) buildRuntime(ctx context.Context, cfg statsConfig) ([]func(), *http.Server) {
  265. // rcrowley/go-metrics guards these registrations with a private sync.Once,
  266. // so subsequent reloads are no-ops.
  267. metrics.RegisterDebugGCStats(metrics.DefaultRegistry)
  268. metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry)
  269. captureFns := []func(){
  270. func() { metrics.CaptureDebugGCStatsOnce(metrics.DefaultRegistry) },
  271. func() { metrics.CaptureRuntimeMemStatsOnce(metrics.DefaultRegistry) },
  272. }
  273. if s.emitStats != nil {
  274. captureFns = append(captureFns, s.emitStats)
  275. }
  276. switch cfg.typ {
  277. case "graphite":
  278. // loadStatsConfig already resolved and validated the address; re-parse
  279. // the resolved form (no DNS lookup) to get a *net.TCPAddr.
  280. addr, _ := net.ResolveTCPAddr(cfg.graphite.protocol, cfg.graphite.resolvedAddr)
  281. gcfg := graphiteConfigExport{
  282. Addr: addr,
  283. Registry: metrics.DefaultRegistry,
  284. FlushInterval: cfg.interval,
  285. DurationUnit: time.Nanosecond,
  286. Prefix: cfg.graphite.prefix,
  287. Percentiles: []float64{0.5, 0.75, 0.95, 0.99, 0.999},
  288. }
  289. captureFns = append(captureFns, func() {
  290. if err := graphiteOnce(ctx, gcfg, graphiteTimeout); err != nil && ctx.Err() == nil {
  291. s.l.Error("Graphite export failed", "error", err)
  292. }
  293. })
  294. s.l.Info("Starting graphite stats",
  295. "interval", cfg.interval,
  296. "prefix", cfg.graphite.prefix,
  297. "addr", addr,
  298. )
  299. return captureFns, nil
  300. case "prometheus":
  301. pr := prometheus.NewRegistry()
  302. pClient := mp.NewPrometheusProvider(metrics.DefaultRegistry, cfg.prom.namespace, cfg.prom.subsystem, pr, cfg.interval)
  303. captureFns = append(captureFns, func() {
  304. if err := pClient.UpdatePrometheusMetricsOnce(); err != nil {
  305. s.l.Error("Prometheus metrics update failed", "error", err)
  306. }
  307. })
  308. g := prometheus.NewGauge(prometheus.GaugeOpts{
  309. Namespace: cfg.prom.namespace,
  310. Subsystem: cfg.prom.subsystem,
  311. Name: "info",
  312. Help: "Version information for the Nebula binary",
  313. ConstLabels: prometheus.Labels{
  314. "version": s.buildVersion,
  315. "goversion": runtime.Version(),
  316. "boringcrypto": strconv.FormatBool(boringEnabled()),
  317. "fips140Version": fips140.Version(),
  318. "fips140Enabled": strconv.FormatBool(fips140.Enabled()),
  319. "fips140Enforced": strconv.FormatBool(fips140.Enforced()),
  320. },
  321. })
  322. pr.MustRegister(g)
  323. g.Set(1)
  324. // promhttp.HandlerOpts.ErrorLog needs a stdlib-shaped Println logger,
  325. // so bridge our slog.Logger back to a *log.Logger that emits at Error.
  326. errLog := slog.NewLogLogger(s.l.Handler(), slog.LevelError)
  327. mux := http.NewServeMux()
  328. mux.Handle(cfg.prom.path, promhttp.HandlerFor(pr, promhttp.HandlerOpts{ErrorLog: errLog}))
  329. return captureFns, &http.Server{Addr: cfg.prom.listen, Handler: mux}
  330. }
  331. return captureFns, nil
  332. }
  333. // captureStatsLoop runs each fn on every tick of d until ctx is cancelled, then
  334. // closes done.
  335. func captureStatsLoop(ctx context.Context, d time.Duration, fns []func(), done chan struct{}) {
  336. defer close(done)
  337. t := time.NewTicker(d)
  338. defer t.Stop()
  339. for {
  340. select {
  341. case <-ctx.Done():
  342. return
  343. case <-t.C:
  344. for _, fn := range fns {
  345. fn()
  346. }
  347. }
  348. }
  349. }
  350. func loadStatsConfig(c *config.C) (statsConfig, error) {
  351. cfg := statsConfig{
  352. typ: c.GetString("stats.type", ""),
  353. }
  354. if cfg.typ == "" || cfg.typ == "none" {
  355. return cfg, nil
  356. }
  357. cfg.interval = c.GetDuration("stats.interval", 0)
  358. if cfg.interval <= 0 {
  359. return cfg, fmt.Errorf("stats.interval was an invalid duration: %s", c.GetString("stats.interval", ""))
  360. }
  361. switch cfg.typ {
  362. case "graphite":
  363. cfg.graphite.protocol = c.GetString("stats.protocol", "tcp")
  364. cfg.graphite.host = c.GetString("stats.host", "")
  365. if cfg.graphite.host == "" {
  366. return cfg, errors.New("stats.host can not be empty")
  367. }
  368. addr, err := net.ResolveTCPAddr(cfg.graphite.protocol, cfg.graphite.host)
  369. if err != nil {
  370. return cfg, fmt.Errorf("error while setting up graphite sink: %s", err)
  371. }
  372. cfg.graphite.resolvedAddr = addr.String()
  373. cfg.graphite.prefix = c.GetString("stats.prefix", "nebula")
  374. case "prometheus":
  375. cfg.prom.listen = c.GetString("stats.listen", "")
  376. if cfg.prom.listen == "" {
  377. return cfg, errors.New("stats.listen should not be empty")
  378. }
  379. cfg.prom.path = c.GetString("stats.path", "")
  380. if cfg.prom.path == "" {
  381. return cfg, errors.New("stats.path should not be empty")
  382. }
  383. cfg.prom.namespace = c.GetString("stats.namespace", "")
  384. cfg.prom.subsystem = c.GetString("stats.subsystem", "")
  385. default:
  386. return cfg, fmt.Errorf("stats.type was not understood: %s", cfg.typ)
  387. }
  388. return cfg, nil
  389. }