@@ -11,7 +11,6 @@ import (
1111 "sync"
1212 "time"
1313
14- "github.com/hashicorp/golang-lru/v2/expirable"
1514 "github.com/lib/pq"
1615 "go.uber.org/atomic"
1716
@@ -26,6 +25,11 @@ const (
2625 OP_CREATE_STATEMENT = "create_statement"
2726)
2827
28+ // emitInterval is the minimum amount of time that must elapse between
29+ // successive OP_CREATE_STATEMENT emissions for the same table, regardless of
30+ // the configured collect_interval.
31+ const emitInterval = 30 * time .Minute
32+
2933const (
3034 // selectAllDatabases makes use of the initial DB connection to discover other databases on the same Postgres instance
3135 selectAllDatabases = `
@@ -174,7 +178,6 @@ type tableInfo struct {
174178 database database
175179 schema schema
176180 tableName table
177- updateTime time.Time
178181 b64TableSpec string
179182}
180183
@@ -312,10 +315,6 @@ type SchemaDetailsArguments struct {
312315 ExcludeDatabases []string
313316 EntryHandler loki.EntryHandler
314317
315- CacheEnabled bool
316- CacheSize int
317- CacheTTL time.Duration
318-
319318 Logger * slog.Logger
320319
321320 dbConnectionFactory databaseConnectionFactory
@@ -329,10 +328,14 @@ type SchemaDetails struct {
329328 excludeDatabases []string
330329 entryHandler loki.EntryHandler
331330
332- // Cache of table definitions. Entries are removed after a configurable TTL.
333- // Key is a string of the form "database.schema.table".
334- // (unlike MySQL) no create/update timestamp available for detecting immediately when a table schema is changed; relying on TTL only
335- cache * expirable.LRU [string , * tableInfo ]
331+ // lastEmittedAt records the wall-clock time at which OP_CREATE_STATEMENT
332+ // was last emitted for a table. The outer key is the database name and
333+ // the inner key is "schema.table". Used to throttle logging to at most
334+ // one per emitInterval per table.
335+ lastEmittedAt map [database ]map [string ]time.Time
336+
337+ // now allows overriding time.Now() in tests
338+ now func () time.Time
336339
337340 tableRegistry * TableRegistry
338341
@@ -356,15 +359,13 @@ func NewSchemaDetails(args SchemaDetailsArguments) (*SchemaDetails, error) {
356359 collectInterval : args .CollectInterval ,
357360 excludeDatabases : args .ExcludeDatabases ,
358361 entryHandler : args .EntryHandler ,
362+ lastEmittedAt : map [database ]map [string ]time.Time {},
363+ now : time .Now ,
359364 tableRegistry : NewTableRegistry (),
360365 logger : args .Logger .With ("collector" , SchemaDetailsCollector ),
361366 running : & atomic.Bool {},
362367 }
363368
364- if args .CacheEnabled {
365- c .cache = expirable .NewLRU [string , * tableInfo ](args .CacheSize , nil , args .CacheTTL )
366- }
367-
368369 return c , nil
369370}
370371
@@ -446,6 +447,9 @@ func (c *SchemaDetails) getAllDatabases(ctx context.Context) ([]string, error) {
446447}
447448
448449func (c * SchemaDetails ) extractSchemas (ctx context.Context , dbName string , dbConnection * sql.DB ) error {
450+ now := c .now ()
451+ db := database (dbName )
452+
449453 schemaRs , err := dbConnection .QueryContext (ctx , selectSchemaNames )
450454 if err != nil {
451455 return fmt .Errorf ("failed to query pg_namespace for database %s: %w" , dbName , err )
@@ -468,71 +472,82 @@ func (c *SchemaDetails) extractSchemas(ctx context.Context, dbName string, dbCon
468472
469473 if len (schemas ) == 0 {
470474 c .logger .Info ("no schema detected from pg_namespace" , "datname" , dbName )
475+ c .pruneDatabaseThrottle (db , nil )
471476 return nil
472477 }
473478
474479 tables := []* tableInfo {}
480+ // scanComplete tracks whether we managed to iterate every schema's
481+ // tables without bailing out early. If false, we have a partial view of
482+ // the database and must skip cleanup to avoid evicting throttle entries
483+ // for tables we simply didn't get to scan this round.
484+ scanComplete := true
475485
476486 for _ , schemaName := range schemas {
477- rs , err := dbConnection .QueryContext (ctx , selectTableNames , schemaName )
478- if err != nil {
479- c .logger .Error ("failed to query tables" , "datname" , dbName , "schema" , schemaName , "err" , err )
480- break
481- }
482- defer rs .Close ()
487+ complete := func () bool {
488+ rs , err := dbConnection .QueryContext (ctx , selectTableNames , schemaName )
489+ if err != nil {
490+ c .logger .Error ("failed to query tables" , "datname" , dbName , "schema" , schemaName , "err" , err )
491+ return false
492+ }
493+ defer rs .Close ()
483494
484- for rs .Next () {
485- var tableName string
486- if err := rs .Scan (& tableName ); err != nil {
487- c .logger .Error ("failed to scan tables" , "datname" , dbName , "schema" , schemaName , "err" , err )
488- break
495+ for rs .Next () {
496+ var tableName string
497+ if err := rs .Scan (& tableName ); err != nil {
498+ c .logger .Error ("failed to scan tables" , "datname" , dbName , "schema" , schemaName , "err" , err )
499+ return false
500+ }
501+ tables = append (tables , & tableInfo {
502+ database : db ,
503+ schema : schema (schemaName ),
504+ tableName : table (tableName ),
505+ })
506+
507+ c .entryHandler .Chan () <- database_observability .BuildLokiEntry (
508+ logging .LevelInfo ,
509+ OP_TABLE_DETECTION ,
510+ fmt .Sprintf (`datname="%s" schema="%s" table="%s"` , dbName , schemaName , tableName ),
511+ )
489512 }
490- tables = append (tables , & tableInfo {
491- database : database (dbName ),
492- schema : schema (schemaName ),
493- tableName : table (tableName ),
494- updateTime : time .Now (),
495- })
496-
497- c .entryHandler .Chan () <- database_observability .BuildLokiEntry (
498- logging .LevelInfo ,
499- OP_TABLE_DETECTION ,
500- fmt .Sprintf (`datname="%s" schema="%s" table="%s"` , dbName , schemaName , tableName ),
501- )
502- }
503513
504- if err := rs .Err (); err != nil {
505- return fmt .Errorf ("failed to iterate over tables result set for database %q schema %q: %w" , dbName , schemaName , err )
514+ if err := rs .Err (); err != nil {
515+ c .logger .Error ("failed to iterate over tables result set" , "datname" , dbName , "schema" , schemaName , "err" , err )
516+ return false
517+ }
518+ return true
519+ }()
520+
521+ if ! complete {
522+ scanComplete = false
523+ break
506524 }
507525 }
508526
509- c .tableRegistry .SetTablesForDatabase (database (dbName ), tables )
527+ if scanComplete {
528+ c .tableRegistry .SetTablesForDatabase (db , tables )
529+ }
510530
511531 if len (tables ) == 0 {
512532 c .logger .Info ("no tables detected from pg_tables" , "datname" , dbName )
533+ if scanComplete {
534+ c .pruneDatabaseThrottle (db , nil )
535+ }
513536 return nil
514537 }
515538
516539 for _ , table := range tables {
517- cacheKey := fmt .Sprintf ("%s.%s.%s" , table .database , table .schema , table .tableName )
518-
519- cacheHit := false
520- if c .cache != nil {
521- if cached , ok := c .cache .Get (cacheKey ); ok {
522- table = cached
523- cacheHit = true
540+ key := schemaTableKey (table .schema , table .tableName )
541+ if inner := c .lastEmittedAt [db ]; inner != nil {
542+ if last , ok := inner [key ]; ok && now .Sub (last ) < emitInterval {
543+ continue
524544 }
525545 }
526546
527- if ! cacheHit {
528- table , err = c .fetchTableDefinitions (ctx , table , dbConnection )
529- if err != nil {
530- c .logger .Error ("failed to get table definitions" , "datname" , dbName , "schema" , table .schema , "table" , table .tableName , "err" , err )
531- continue
532- }
533- if c .cache != nil {
534- c .cache .Add (cacheKey , table )
535- }
547+ table , err = c .fetchTableDefinitions (ctx , table , dbConnection )
548+ if err != nil {
549+ c .logger .Error ("failed to get table definitions" , "datname" , dbName , "schema" , table .schema , "table" , table .tableName , "err" , err )
550+ continue
536551 }
537552
538553 c .entryHandler .Chan () <- database_observability .BuildLokiEntry (
@@ -543,11 +558,42 @@ func (c *SchemaDetails) extractSchemas(ctx context.Context, dbName string, dbCon
543558 dbName , table .schema , table .tableName , table .b64TableSpec ,
544559 ),
545560 )
561+ if c .lastEmittedAt [db ] == nil {
562+ c .lastEmittedAt [db ] = map [string ]time.Time {}
563+ }
564+ c.lastEmittedAt [db ][key ] = now
565+ }
566+
567+ if scanComplete {
568+ c .pruneDatabaseThrottle (db , tables )
546569 }
547570
548571 return nil
549572}
550573
574+ // pruneDatabaseThrottle removes entries in c.lastEmittedAt[db] whose
575+ // schema.table key is not present in the given tables. If the resulting
576+ // inner map is empty, the outer entry is also deleted. Must only be called
577+ // after a complete scan of the database's tables.
578+ func (c * SchemaDetails ) pruneDatabaseThrottle (db database , tables []* tableInfo ) {
579+ if _ , ok := c .lastEmittedAt [db ]; ! ok {
580+ return
581+ }
582+
583+ currentKeys := make (map [string ]struct {}, len (tables ))
584+ for _ , t := range tables {
585+ currentKeys [schemaTableKey (t .schema , t .tableName )] = struct {}{}
586+ }
587+ for k := range c .lastEmittedAt [db ] {
588+ if _ , ok := currentKeys [k ]; ! ok {
589+ delete (c .lastEmittedAt [db ], k )
590+ }
591+ }
592+ if len (c .lastEmittedAt [db ]) == 0 {
593+ delete (c .lastEmittedAt , db )
594+ }
595+ }
596+
551597func (c * SchemaDetails ) extractNames (ctx context.Context ) error {
552598 databases , err := c .getAllDatabases (ctx )
553599 if err != nil {
@@ -583,6 +629,19 @@ func (c *SchemaDetails) extractNames(ctx context.Context) error {
583629 }
584630 }
585631
632+ // Drop throttle entries for databases that getAllDatabases no longer
633+ // returns (dropped, CONNECT revoked, added to exclude list). Per-table
634+ // cleanup within a still-present database is handled by extractSchemas.
635+ seenDatabases := make (map [database ]struct {}, len (databases ))
636+ for _ , dbName := range databases {
637+ seenDatabases [database (dbName )] = struct {}{}
638+ }
639+ for db := range c .lastEmittedAt {
640+ if _ , ok := seenDatabases [db ]; ! ok {
641+ delete (c .lastEmittedAt , db )
642+ }
643+ }
644+
586645 return nil
587646}
588647
@@ -711,3 +770,7 @@ func (c *SchemaDetails) fetchColumnsDefinitions(ctx context.Context, databaseNam
711770
712771 return tblSpec , nil
713772}
773+
774+ func schemaTableKey (sch schema , tbl table ) string {
775+ return fmt .Sprintf ("%s.%s" , sch , tbl )
776+ }
0 commit comments