@@ -204,3 +204,270 @@ metrics:
204204 assert .GreaterOrEqual (t , count , len (specialNames ), "expected metrics to be stored in test_metric table" )
205205 })
206206}
207+
208+ // TestMain_Integration_ConfigChanges tests that source configuration changes
209+ // are properly applied to running gatherers
210+ func TestMain_Integration_ConfigChanges (t * testing.T ) {
211+ tempDir := t .TempDir ()
212+
213+ pg , tearDown , err := testutil .SetupPostgresContainer ()
214+ require .NoError (t , err )
215+ defer tearDown ()
216+
217+ connStr , err := pg .ConnectionString (testutil .TestContext , "sslmode=disable" )
218+ require .NoError (t , err )
219+
220+ var gotExit int32
221+ Exit = func (code int ) { gotExit = int32 (code ) }
222+ defer func () { Exit = os .Exit }()
223+
224+ metricsYaml := filepath .Join (tempDir , "metrics.yaml" )
225+ sourcesYaml := filepath .Join (tempDir , "sources.yaml" )
226+
227+ require .NoError (t , os .WriteFile (metricsYaml , []byte (`
228+ metrics:
229+ test_metric:
230+ sqls:
231+ 11: select (extract(epoch from now()) * 1e9)::int8 as epoch_ns, 1 as value
232+ presets:
233+ test_preset:
234+ metrics:
235+ test_metric: 1
236+ ` ), 0644 ))
237+
238+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
239+ - name: test_source
240+ conn_str: ` + connStr + `
241+ kind: postgres
242+ is_enabled: true
243+ custom_tags:
244+ environment: production
245+ version: "1.0"
246+ custom_metrics:
247+ test_metric: 1
248+ ` ), 0644 ))
249+
250+ os .Args = []string {
251+ "pgwatch" ,
252+ "--metrics" , metricsYaml ,
253+ "--sources" , sourcesYaml ,
254+ "--sink" , connStr ,
255+ "--refresh=2" ,
256+ "--web-disable" ,
257+ }
258+
259+ sinkConn , err := pgx .Connect (context .Background (), connStr )
260+ require .NoError (t , err )
261+ defer sinkConn .Close (context .Background ())
262+
263+ go main ()
264+
265+ // Below tests are expected to run sequentially and depend on
266+ // data generated by each other
267+
268+ t .Run ("Ensure tag changes are applied" , func (t * testing.T ) {
269+ // Wait for some initial metrics to be written
270+ time .Sleep (2 * time .Second )
271+
272+ var tagData map [string ]string
273+ err = sinkConn .QueryRow (context .Background (),
274+ `SELECT tag_data FROM test_metric
275+ WHERE dbname = 'test_source'
276+ ORDER BY time DESC LIMIT 1` ).Scan (& tagData )
277+ require .NoError (t , err )
278+ assert .Equal (t , "production" , tagData ["environment" ], "initial environment tag should be 'production'" )
279+ assert .Equal (t , "1.0" , tagData ["version" ], "initial version tag should be '1.0'" )
280+
281+ // Update custom_tags
282+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
283+ - name: test_source
284+ conn_str: ` + connStr + `
285+ kind: postgres
286+ is_enabled: true
287+ custom_tags:
288+ environment: staging
289+ version: "2.0"
290+ new_tag: added
291+ custom_metrics:
292+ test_metric: 1
293+ ` ), 0644 ))
294+
295+ // Wait for config reload and new metrics with updated tags
296+ time .Sleep (3 * time .Second )
297+
298+ err = sinkConn .QueryRow (context .Background (),
299+ `SELECT tag_data FROM test_metric
300+ WHERE dbname = 'test_source'
301+ ORDER BY time DESC LIMIT 1` ).Scan (& tagData )
302+ require .NoError (t , err )
303+ assert .Equal (t , "staging" , tagData ["environment" ], "updated environment tag should be 'staging'" )
304+ assert .Equal (t , "2.0" , tagData ["version" ], "updated version tag should be '2.0'" )
305+ assert .Equal (t , "added" , tagData ["new_tag" ], "new_tag should be present" )
306+ })
307+
308+ t .Run ("Ensure metric interval changes are applied" , func (t * testing.T ) {
309+ // Get collection interval before change
310+ var epochNsBefore []int64
311+ rows , err := sinkConn .Query (context .Background (),
312+ `SELECT (data->>'epoch_ns')::bigint as epoch_ns
313+ FROM test_metric
314+ WHERE dbname = 'test_source'
315+ ORDER BY time DESC LIMIT 2` )
316+ require .NoError (t , err )
317+ for rows .Next () {
318+ var epochNs int64
319+ require .NoError (t , rows .Scan (& epochNs ))
320+ epochNsBefore = append (epochNsBefore , epochNs )
321+ }
322+ rows .Close ()
323+ require .GreaterOrEqual (t , len (epochNsBefore ), 2 , "we need at least 2 measurements" )
324+
325+ // Calculate interval before change
326+ intervalBefore := float64 (epochNsBefore [0 ] - epochNsBefore [1 ]) / 1e9
327+ assert .InDelta (t , 1.0 , intervalBefore , 0.5 , "interval should be approximately 1 second" )
328+
329+ // Change interval to 2 seconds
330+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
331+ - name: test_source
332+ conn_str: ` + connStr + `
333+ kind: postgres
334+ is_enabled: true
335+ custom_metrics:
336+ test_metric: 2
337+ ` ), 0644 ))
338+
339+ time .Sleep (5 * time .Second )
340+
341+ // Get collection interval after change
342+ var epochNsAfter []int64
343+ rows , err = sinkConn .Query (context .Background (),
344+ `SELECT (data->>'epoch_ns')::bigint as epoch_ns
345+ FROM test_metric
346+ WHERE dbname = 'test_source'
347+ ORDER BY time DESC LIMIT 2` )
348+ require .NoError (t , err )
349+ for rows .Next () {
350+ var epochNs int64
351+ require .NoError (t , rows .Scan (& epochNs ))
352+ epochNsAfter = append (epochNsAfter , epochNs )
353+ }
354+ rows .Close ()
355+ require .GreaterOrEqual (t , len (epochNsAfter ), 2 , "we need at least 2 measurements after interval change" )
356+
357+ // Calculate interval after change
358+ intervalAfter := float64 (epochNsAfter [0 ] - epochNsAfter [1 ]) / 1e9
359+ assert .InDelta (t , 2.0 , intervalAfter , 0.5 , "new interval should be approximately 2 seconds" )
360+ assert .Greater (t , intervalAfter , intervalBefore , "new interval should be greater than old interval" )
361+ })
362+
363+ t .Run ("Ensure conn str changes are applied" , func (t * testing.T ) {
364+ // Count rows before connection string change
365+ var countBefore int
366+ err = sinkConn .QueryRow (context .Background (),
367+ `SELECT count(*) FROM test_metric WHERE dbname = 'test_source'` ).Scan (& countBefore )
368+ require .NoError (t , err )
369+ require .Greater (t , countBefore , 0 )
370+
371+ // Change to invalid connection string
372+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
373+ - name: test_source
374+ conn_str: postgres://invalid:invalid@localhost:59999/nonexistent
375+ kind: postgres
376+ is_enabled: true
377+ custom_metrics:
378+ test_metric: 1
379+ ` ), 0644 ))
380+
381+ // Wait for config reload and failed metric fetches
382+ time .Sleep (4 * time .Second )
383+
384+ // Count rows after connection string change
385+ var countAfter int
386+ err = sinkConn .QueryRow (context .Background (),
387+ `SELECT count(*) FROM public.test_metric WHERE dbname = 'test_source'` ).Scan (& countAfter )
388+ require .NoError (t , err )
389+
390+ assert .LessOrEqual (t , countAfter - countBefore , 2 )
391+ })
392+
393+ t .Run ("Ensure preset intervals updates are applied - issue #1091" , func (t * testing.T ) {
394+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
395+ - name: test_source
396+ conn_str: ` + connStr + `
397+ kind: postgres
398+ is_enabled: true
399+ custom_tags:
400+ version: "1.0"
401+ preset_metrics: test_preset
402+ ` ), 0644 ))
403+
404+ // Wait for reload and some metrics collection
405+ time .Sleep (4 * time .Second )
406+
407+ var epochNsBefore []int64
408+ rows , err := sinkConn .Query (context .Background (),
409+ `SELECT (data->>'epoch_ns')::bigint as epoch_ns
410+ FROM public.test_metric
411+ WHERE dbname = 'test_source'
412+ ORDER BY time DESC LIMIT 2` )
413+ require .NoError (t , err )
414+ for rows .Next () {
415+ var epochNs int64
416+ require .NoError (t , rows .Scan (& epochNs ))
417+ epochNsBefore = append (epochNsBefore , epochNs )
418+ }
419+ rows .Close ()
420+ require .GreaterOrEqual (t , len (epochNsBefore ), 2 , "should have at least 2 measurements" )
421+
422+ // Calculate interval before change
423+ intervalBefore := float64 (epochNsBefore [0 ] - epochNsBefore [1 ]) / 1e9
424+ assert .InDelta (t , 1.0 , intervalBefore , 0.5 , "interval should be approximately 1 second" )
425+
426+ require .NoError (t , os .WriteFile (sourcesYaml , []byte (`
427+ - name: test_source
428+ conn_str: ` + connStr + `
429+ kind: postgres
430+ is_enabled: true
431+ custom_tags:
432+ version: "2.0" # to force a reload - triggering the bug
433+ preset_metrics: test_preset
434+ ` ), 0644 ))
435+
436+ require .NoError (t , os .WriteFile (metricsYaml , []byte (`
437+ metrics:
438+ test_metric:
439+ sqls:
440+ 11: select (extract(epoch from now()) * 1e9)::int8 as epoch_ns, 1 as value
441+ presets:
442+ test_preset:
443+ metrics:
444+ test_metric: 2
445+ ` ), 0644 ))
446+
447+ // Wait for config reload and some metrics
448+ time .Sleep (5 * time .Second )
449+
450+ var epochNsAfter []int64
451+ rows , err = sinkConn .Query (context .Background (),
452+ `SELECT (data->>'epoch_ns')::bigint as epoch_ns
453+ FROM public.test_metric
454+ WHERE dbname = 'test_source'
455+ ORDER BY time DESC LIMIT 2` )
456+ require .NoError (t , err )
457+ for rows .Next () {
458+ var epochNs int64
459+ require .NoError (t , rows .Scan (& epochNs ))
460+ epochNsAfter = append (epochNsAfter , epochNs )
461+ }
462+ rows .Close ()
463+ require .GreaterOrEqual (t , len (epochNsAfter ), 2 , "should have at least 2 measurements" )
464+
465+ // Calculate interval after change
466+ intervalAfter := float64 (epochNsAfter [0 ] - epochNsAfter [1 ]) / 1e9
467+ assert .InDelta (t , 2.0 , intervalAfter , 0.5 , "interval should be approximately 2 seconds" )
468+ })
469+
470+ cancel ()
471+ <- mainCtx .Done ()
472+ assert .Equal (t , cmdopts .ExitCodeOK , gotExit )
473+ }
0 commit comments