1
0

stats_test.go 9.4 KB

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