stats_test.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. package nebula
  2. import (
  3. "context"
  4. "log/slog"
  5. "net"
  6. "strconv"
  7. "testing"
  8. "time"
  9. "github.com/slackhq/nebula/config"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. )
  13. func newTestStatsServer(t *testing.T) (*statsServer, *config.C) {
  14. t.Helper()
  15. l := slog.New(slog.DiscardHandler)
  16. ctx, cancel := context.WithCancel(context.Background())
  17. t.Cleanup(cancel)
  18. return &statsServer{
  19. l: l,
  20. ctx: ctx,
  21. }, config.NewC(l)
  22. }
  23. func setStatsConfig(c *config.C, m map[string]any) {
  24. c.Settings["stats"] = m
  25. }
  26. func currentRuntime(s *statsServer) *statsRuntime {
  27. s.runMu.Lock()
  28. defer s.runMu.Unlock()
  29. return s.run
  30. }
  31. func TestStatsServer_reload_initial_disabled(t *testing.T) {
  32. s, c := newTestStatsServer(t)
  33. setStatsConfig(c, map[string]any{"type": "none"})
  34. require.NoError(t, s.reload(c, true))
  35. assert.False(t, s.enabled.Load())
  36. assert.Nil(t, currentRuntime(s))
  37. }
  38. func TestStatsServer_reload_initial_invalidInterval(t *testing.T) {
  39. s, c := newTestStatsServer(t)
  40. setStatsConfig(c, map[string]any{
  41. "type": "graphite",
  42. "host": "127.0.0.1:0",
  43. "prefix": "test",
  44. })
  45. err := s.reload(c, true)
  46. require.Error(t, err)
  47. assert.False(t, s.enabled.Load())
  48. }
  49. func TestStatsServer_reload_initial_unknownType(t *testing.T) {
  50. s, c := newTestStatsServer(t)
  51. setStatsConfig(c, map[string]any{
  52. "type": "carbon",
  53. "interval": "1s",
  54. })
  55. err := s.reload(c, true)
  56. require.Error(t, err)
  57. assert.False(t, s.enabled.Load())
  58. }
  59. func TestStatsServer_reload_unchanged_noOp(t *testing.T) {
  60. s, c := newTestStatsServer(t)
  61. setStatsConfig(c, map[string]any{"type": "none"})
  62. require.NoError(t, s.reload(c, true))
  63. require.NoError(t, s.reload(c, false))
  64. assert.False(t, s.enabled.Load())
  65. }
  66. func TestStatsServer_reload_initial_graphite(t *testing.T) {
  67. s, c := newTestStatsServer(t)
  68. setStatsConfig(c, map[string]any{
  69. "type": "graphite",
  70. "interval": "1s",
  71. "protocol": "tcp",
  72. "host": "127.0.0.1:2003",
  73. "prefix": "test",
  74. })
  75. require.NoError(t, s.reload(c, true))
  76. assert.True(t, s.enabled.Load())
  77. // reload only records config; Start builds the runtime.
  78. assert.Nil(t, currentRuntime(s))
  79. }
  80. func TestStatsServer_reload_initial_prometheus(t *testing.T) {
  81. s, c := newTestStatsServer(t)
  82. setStatsConfig(c, map[string]any{
  83. "type": "prometheus",
  84. "interval": "1s",
  85. "listen": "127.0.0.1:0",
  86. "path": "/metrics",
  87. })
  88. require.NoError(t, s.reload(c, true))
  89. assert.True(t, s.enabled.Load())
  90. // reload only records config; Start builds the runtime.
  91. assert.Nil(t, currentRuntime(s))
  92. }
  93. func TestStatsServer_Start_graphite_blocksUntilStop(t *testing.T) {
  94. h := newGraphiteHost(t, 0)
  95. s, c := newTestStatsServer(t)
  96. setStatsConfig(c, map[string]any{
  97. "type": "graphite",
  98. "interval": "1s",
  99. "protocol": "tcp",
  100. "host": h.ln.Addr().String(),
  101. "prefix": "test",
  102. })
  103. require.NoError(t, s.reload(c, true))
  104. done := make(chan struct{})
  105. go func() {
  106. s.Start()
  107. close(done)
  108. }()
  109. // Wait for Start to publish runtime state.
  110. waitFor(t, func() bool { return currentRuntime(s) != nil })
  111. rt := currentRuntime(s)
  112. require.NotNil(t, rt)
  113. assert.Nil(t, rt.listener, "graphite has no listener")
  114. s.Stop()
  115. select {
  116. case <-done:
  117. case <-time.After(5 * time.Second):
  118. t.Fatal("graphite Start did not return after Stop")
  119. }
  120. assert.Nil(t, currentRuntime(s))
  121. }
  122. func TestStatsServer_StartStop_lifecycle(t *testing.T) {
  123. port := freeTCPPort(t)
  124. s, c := newTestStatsServer(t)
  125. setStatsConfig(c, map[string]any{
  126. "type": "prometheus",
  127. "interval": "1s",
  128. "listen": "127.0.0.1:" + port,
  129. "path": "/metrics",
  130. })
  131. require.NoError(t, s.reload(c, true))
  132. done := make(chan struct{})
  133. go func() {
  134. s.Start()
  135. close(done)
  136. }()
  137. waitForListening(t, "127.0.0.1:"+port)
  138. rt := currentRuntime(s)
  139. require.NotNil(t, rt)
  140. require.NotNil(t, rt.listener)
  141. s.Stop()
  142. select {
  143. case <-done:
  144. case <-time.After(5 * time.Second):
  145. t.Fatal("Start did not return after Stop")
  146. }
  147. assert.Nil(t, currentRuntime(s))
  148. }
  149. func TestStatsServer_reload_disable_stopsRunningRuntime(t *testing.T) {
  150. port := freeTCPPort(t)
  151. s, c := newTestStatsServer(t)
  152. setStatsConfig(c, map[string]any{
  153. "type": "prometheus",
  154. "interval": "1s",
  155. "listen": "127.0.0.1:" + port,
  156. "path": "/metrics",
  157. })
  158. require.NoError(t, s.reload(c, true))
  159. done := make(chan struct{})
  160. go func() {
  161. s.Start()
  162. close(done)
  163. }()
  164. waitForListening(t, "127.0.0.1:"+port)
  165. setStatsConfig(c, map[string]any{"type": "none"})
  166. require.NoError(t, s.reload(c, false))
  167. select {
  168. case <-done:
  169. case <-time.After(5 * time.Second):
  170. t.Fatal("Start did not return after reload disabled stats")
  171. }
  172. assert.False(t, s.enabled.Load())
  173. assert.Nil(t, currentRuntime(s))
  174. }
  175. func TestStatsServer_reload_changeListener_restartsListener(t *testing.T) {
  176. port1 := freeTCPPort(t)
  177. s, c := newTestStatsServer(t)
  178. setStatsConfig(c, map[string]any{
  179. "type": "prometheus",
  180. "interval": "1s",
  181. "listen": "127.0.0.1:" + port1,
  182. "path": "/metrics",
  183. })
  184. require.NoError(t, s.reload(c, true))
  185. firstDone := make(chan struct{})
  186. go func() {
  187. s.Start()
  188. close(firstDone)
  189. }()
  190. waitForListening(t, "127.0.0.1:"+port1)
  191. first := currentRuntime(s)
  192. require.NotNil(t, first)
  193. port2 := freeTCPPort(t)
  194. setStatsConfig(c, map[string]any{
  195. "type": "prometheus",
  196. "interval": "1s",
  197. "listen": "127.0.0.1:" + port2,
  198. "path": "/metrics",
  199. })
  200. require.NoError(t, s.reload(c, false))
  201. select {
  202. case <-firstDone:
  203. case <-time.After(5 * time.Second):
  204. t.Fatal("old Start did not return after reload")
  205. }
  206. waitForListening(t, "127.0.0.1:"+port2)
  207. second := currentRuntime(s)
  208. require.NotNil(t, second)
  209. assert.NotSame(t, first, second, "expected a new runtime after listen address change")
  210. s.Stop()
  211. }
  212. func TestStatsServer_Stop_beforeStart_doesNotBlock(t *testing.T) {
  213. s, c := newTestStatsServer(t)
  214. setStatsConfig(c, map[string]any{
  215. "type": "prometheus",
  216. "interval": "1s",
  217. "listen": "127.0.0.1:0",
  218. "path": "/metrics",
  219. })
  220. require.NoError(t, s.reload(c, true))
  221. stopped := make(chan struct{})
  222. go func() {
  223. s.Stop()
  224. close(stopped)
  225. }()
  226. select {
  227. case <-stopped:
  228. case <-time.After(time.Second):
  229. t.Fatal("Stop hung with no runtime started")
  230. }
  231. }
  232. func TestStatsServer_configTest_validatesWithoutSpawning(t *testing.T) {
  233. s, c := newTestStatsServer(t)
  234. s.configTest = true
  235. setStatsConfig(c, map[string]any{
  236. "type": "prometheus",
  237. "interval": "1s",
  238. "listen": "127.0.0.1:0",
  239. "path": "/metrics",
  240. })
  241. require.NoError(t, s.reload(c, true))
  242. s.Start()
  243. assert.Nil(t, currentRuntime(s))
  244. }
  245. func TestStatsServer_ctxCancel_unblocksStart(t *testing.T) {
  246. // Ensures ctx cancellation alone (no explicit Stop) tears down both
  247. // graphite and prom Start invocations.
  248. port := freeTCPPort(t)
  249. l := slog.New(slog.DiscardHandler)
  250. ctx, cancel := context.WithCancel(context.Background())
  251. defer cancel()
  252. s := &statsServer{l: l, ctx: ctx}
  253. c := config.NewC(l)
  254. setStatsConfig(c, map[string]any{
  255. "type": "prometheus",
  256. "interval": "1s",
  257. "listen": "127.0.0.1:" + port,
  258. "path": "/metrics",
  259. })
  260. require.NoError(t, s.reload(c, true))
  261. done := make(chan struct{})
  262. go func() {
  263. s.Start()
  264. close(done)
  265. }()
  266. waitForListening(t, "127.0.0.1:"+port)
  267. cancel()
  268. select {
  269. case <-done:
  270. case <-time.After(5 * time.Second):
  271. t.Fatal("Start did not return after ctx cancel")
  272. }
  273. }
  274. func TestStatsServer_listenerBindFailure_sameCfgReloadRetries(t *testing.T) {
  275. // Hold the port so ListenAndServe will fail on first Start.
  276. blocker, err := net.Listen("tcp", "127.0.0.1:0")
  277. require.NoError(t, err)
  278. port := strconv.Itoa(blocker.Addr().(*net.TCPAddr).Port)
  279. s, c := newTestStatsServer(t)
  280. setStatsConfig(c, map[string]any{
  281. "type": "prometheus",
  282. "interval": "1s",
  283. "listen": "127.0.0.1:" + port,
  284. "path": "/metrics",
  285. })
  286. require.NoError(t, s.reload(c, true))
  287. done := make(chan struct{})
  288. go func() {
  289. s.Start()
  290. close(done)
  291. }()
  292. select {
  293. case <-done:
  294. case <-time.After(5 * time.Second):
  295. t.Fatal("Start did not return after bind failure")
  296. }
  297. // Bind failure should have dropped the cached config so a same-cfg
  298. // SIGHUP can retry.
  299. s.runMu.Lock()
  300. cfgAfterFailure := s.runCfg
  301. s.runMu.Unlock()
  302. assert.Nil(t, cfgAfterFailure)
  303. // Free the port and reload with the same config; Start should fire again.
  304. require.NoError(t, blocker.Close())
  305. require.NoError(t, s.reload(c, false))
  306. waitForListening(t, "127.0.0.1:"+port)
  307. require.NotNil(t, currentRuntime(s))
  308. s.Stop()
  309. }
  310. func waitForListening(t *testing.T, addr string) {
  311. t.Helper()
  312. waitFor(t, func() bool {
  313. conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
  314. if err != nil {
  315. return false
  316. }
  317. _ = conn.Close()
  318. return true
  319. })
  320. }
  321. func freeTCPPort(t *testing.T) string {
  322. t.Helper()
  323. ln, err := net.Listen("tcp", "127.0.0.1:0")
  324. require.NoError(t, err)
  325. port := ln.Addr().(*net.TCPAddr).Port
  326. require.NoError(t, ln.Close())
  327. return strconv.Itoa(port)
  328. }