1 package reaper
2
3 import (
4 "cmp"
5 "context"
6 "errors"
7 "fmt"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/cybertec-postgresql/pgwatch/v6/internal/db"
13 "github.com/cybertec-postgresql/pgwatch/v6/internal/log"
14 "github.com/cybertec-postgresql/pgwatch/v6/internal/metrics"
15 "github.com/cybertec-postgresql/pgwatch/v6/internal/sources"
16 "github.com/jackc/pgx/v5"
17 )
18
19 const minTickInterval = 1
20
21 var _ Reaper = (*DbConnReaper)(nil)
22
23
24
25
26
27 type DbConnReaper struct {
28 reaper *reaper
29 md *sources.DbConn
30 lastFetch map[string]time.Time
31 lastUptimeS int64
32
33 logParserStarted bool
34
35 degradedMu sync.RWMutex
36 degradedMetrics map[string]struct{}
37 }
38
39
40 func NewDbConnReaper(r *reaper, md *sources.DbConn) *DbConnReaper {
41 return &DbConnReaper{
42 reaper: r,
43 md: md,
44 lastFetch: make(map[string]time.Time),
45 degradedMetrics: make(map[string]struct{}),
46 }
47 }
48
49
50 func (sr *DbConnReaper) isDegraded(name string) bool {
51 sr.degradedMu.RLock()
52 _, ok := sr.degradedMetrics[name]
53 sr.degradedMu.RUnlock()
54 return ok
55 }
56
57
58 func (sr *DbConnReaper) markDegraded(name string) {
59 sr.degradedMu.Lock()
60 sr.degradedMetrics[name] = struct{}{}
61 sr.degradedMu.Unlock()
62 }
63
64
65 func (sr *DbConnReaper) clearDegraded(name string) {
66 sr.degradedMu.Lock()
67 delete(sr.degradedMetrics, name)
68 sr.degradedMu.Unlock()
69 }
70
71
72
73 func (sr *DbConnReaper) activeMetrics() map[string]time.Duration {
74 c := make(map[string]time.Duration)
75 for k, v := range sr.md.ActiveMetrics() {
76 c[k] = time.Duration(v) * time.Second
77 }
78 return c
79 }
80
81
82 func GCDSlice(vals []int) int {
83 if len(vals) == 0 {
84 return 0
85 }
86 g := vals[0]
87 for _, v := range vals[1:] {
88 for v != 0 {
89 g, v = v, g%v
90 }
91 }
92 return g
93 }
94
95
96 func (sr *DbConnReaper) calcTickInterval() time.Duration {
97 am := sr.activeMetrics()
98 intervals := make([]int, 0, len(am))
99 for _, d := range am {
100 intervals = append(intervals, max(int(d.Seconds()), minTickInterval))
101 }
102 return time.Duration(max(GCDSlice(intervals), minTickInterval)) * time.Second
103 }
104
105
106 func (sr *DbConnReaper) cacheKey(m metrics.Metric, name string) string {
107 age := sr.reaper.Metrics.CacheAge()
108 if m.IsInstanceLevel && age > 0 && sr.md.GetMetricInterval(name) < age {
109 return fmt.Sprintf("%s:%s", sr.md.GetClusterIdentifier(), name)
110 }
111 return ""
112 }
113
114
115
116 func (sr *DbConnReaper) isRoleExcluded(m metrics.Metric) bool {
117 sr.md.RLock()
118 defer sr.md.RUnlock()
119 return (m.PrimaryOnly() && sr.md.IsInRecovery) || (m.StandbyOnly() && !sr.md.IsInRecovery)
120 }
121
122
123
124 func (sr *DbConnReaper) sendEnvelope(ctx context.Context, name, storageName string, data metrics.Measurements) {
125 log.GetLogger(ctx).WithField("metric", name).WithField("rows", len(data)).Info("measurements fetched")
126 sr.reaper.AddSysinfoToMeasurements(data, sr.md)
127 sr.reaper.measurementCh <- metrics.MeasurementEnvelope{
128 DBName: sr.md.Name,
129 MetricName: cmp.Or(storageName, name),
130 Data: data,
131 CustomTags: sr.md.CustomTags,
132 }
133 }
134
135
136
137 func (sr *DbConnReaper) dispatchMetricData(ctx context.Context, name string, metric metrics.Metric, data metrics.Measurements) {
138 if key := sr.cacheKey(metric, name); key != "" {
139 sr.reaper.measurementCache.Put(key, data)
140 }
141 sr.sendEnvelope(ctx, name, metric.StorageName, data)
142 if name == "db_stats" {
143 sr.detectServerRestart(ctx, data)
144 }
145 }
146
147
148 type batchEntry struct {
149 metricName string
150 metric metrics.Metric
151 sql string
152 }
153
154
155
156 func (sr *DbConnReaper) Reap(ctx context.Context) {
157 l := log.GetLogger(ctx).WithField("source", sr.md.Name)
158 ctx = log.WithLogger(ctx, l)
159 var err error
160 for {
161 if err = sr.md.FetchRuntimeInfo(ctx, false); err != nil {
162 l.WithError(err).Warning("could not refresh runtime info")
163 }
164
165 now := time.Now()
166 var batch []batchEntry
167
168 for name, interval := range sr.activeMetrics() {
169 if interval <= 0 {
170 continue
171 }
172 if lf := sr.lastFetch[name]; !lf.IsZero() && now.Sub(lf) < interval {
173 continue
174 }
175
176 metric, ok := metricDefs.GetMetricDef(name)
177 if !ok || sr.isRoleExcluded(metric) {
178 continue
179 }
180 switch {
181 case name == specialMetricServerLogEventCounts:
182 if !sr.logParserStarted {
183 sr.logParserStarted = true
184 go func() {
185 if e := sr.runLogParser(ctx); e != nil {
186 l.WithError(e).Error("log parser error")
187 }
188 }()
189 }
190 case IsDirectlyFetchableMetric(sr.md, name):
191 err = sr.fetchOSMetric(ctx, name)
192 sr.lastFetch[name] = time.Now()
193 case name == specialMetricChangeEvents || name == specialMetricInstanceUp:
194 err = sr.fetchSpecialMetric(ctx, name, metric.StorageName)
195 sr.lastFetch[name] = time.Now()
196 default:
197 if cached := sr.reaper.GetMeasurementCache(sr.cacheKey(metric, name)); len(cached) > 0 {
198 l.WithField("metric", name).Info("instance level cache hit")
199 sr.sendEnvelope(ctx, name, metric.StorageName, cached)
200 sr.lastFetch[name] = time.Now()
201 break
202 }
203 sr.md.RLock()
204 version := sr.md.Version
205 sr.md.RUnlock()
206 sql := metric.GetSQL(version)
207 if sql == "" {
208 l.WithField("source", sr.md.Name).WithField("version", version).Warning("no SQL found for metric version")
209 sr.lastFetch[name] = time.Now()
210 break
211 }
212 if sr.isDegraded(name) {
213 if err = sr.fetchMetric(ctx, batchEntry{metricName: name, metric: metric, sql: sql}); err != nil {
214 l.WithError(err).WithField("metric", name).Error("degraded metric fetch failed")
215 } else {
216 l.WithField("metric", name).Info("degraded metric recovered, returning to batch execution")
217 sr.clearDegraded(name)
218 }
219 sr.lastFetch[name] = time.Now()
220 break
221 }
222 batch = append(batch, batchEntry{metricName: name, metric: metric, sql: sql})
223 continue
224 }
225 if err != nil {
226 l.WithError(err).WithField("metric", name).Error("failed to fetch metric")
227 }
228 }
229
230 if len(batch) > 0 {
231 if sr.md.IsPostgresSource() {
232 err = sr.executeBatch(ctx, batch)
233 } else {
234 for _, e := range batch {
235 err = errors.Join(err, sr.fetchMetric(ctx, e))
236 }
237 }
238
239 if errs, ok := err.(interface{ Unwrap() []error }); ok {
240 for _, err = range errs.Unwrap() {
241 l.WithError(err).Error("failed to fetch metric")
242 }
243 } else if err != nil {
244 l.WithError(err).Error("failed to fetch metric")
245 }
246
247 now := time.Now()
248 for _, e := range batch {
249 sr.lastFetch[e.metricName] = now
250 }
251 }
252 select {
253 case <-ctx.Done():
254 return
255 case <-time.After(sr.calcTickInterval()):
256 }
257 }
258 }
259
260
261
262
263
264
265
266
267 func (sr *DbConnReaper) executeBatch(ctx context.Context, entries []batchEntry) error {
268 batch := &pgx.Batch{}
269 for _, e := range entries {
270 batch.Queue(e.sql)
271 }
272
273
274
275
276
277 bctx, cancel := db.WithFetchTimeout(ctx, "batch", sr.calcTickInterval())
278 defer cancel()
279
280 br := sr.md.Conn.SendBatch(bctx, batch)
281 defer func() { _ = br.Close() }()
282
283 var (
284 errs error
285 retries []batchEntry
286 )
287 for _, e := range entries {
288 rows, err := br.Query()
289 if err != nil {
290
291 retries = append(retries, e)
292 continue
293 }
294 errs = errors.Join(errs, sr.CollectAndDispatch(ctx, rows, e.metricName, e.metric))
295 }
296
297 for _, e := range retries {
298 if err := sr.fetchMetric(ctx, e); err != nil {
299 errs = errors.Join(errs, fmt.Errorf("failed to fetch metric %s: %v", e.metricName, err))
300 log.GetLogger(ctx).WithField("metric", e.metricName).Warning("metric degraded after repeated failures, switching to individual fetch")
301 sr.markDegraded(e.metricName)
302 }
303 }
304 return errs
305 }
306
307
308 func (sr *DbConnReaper) fetchMetric(ctx context.Context, entry batchEntry) error {
309
310
311
312 interval := sr.md.GetMetricInterval(entry.metricName)
313 op := "fetch " + entry.metricName
314 fctx, cancel := db.WithFetchTimeout(ctx, op, interval)
315 defer cancel()
316 if err := sr.fetchMetricOnce(fctx, entry); err != nil {
317 return fmt.Errorf("%s: %w", op, err)
318 }
319 return nil
320 }
321
322
323 func (sr *DbConnReaper) fetchMetricOnce(ctx context.Context, entry batchEntry) error {
324 rows, err := sr.md.Conn.Query(ctx, entry.sql, pgx.QueryExecModeSimpleProtocol)
325 if err != nil {
326 return err
327 }
328 return sr.CollectAndDispatch(ctx, rows, entry.metricName, entry.metric)
329 }
330
331
332 func (sr *DbConnReaper) CollectAndDispatch(ctx context.Context, rows pgx.Rows, name string, metric metrics.Metric) error {
333 data, err := pgx.CollectRows(rows, metrics.RowToMeasurement)
334 if err != nil {
335 return err
336 }
337 if len(data) > 0 {
338 sr.dispatchMetricData(ctx, name, metric, data)
339 }
340 return nil
341 }
342
343
344 func (sr *DbConnReaper) fetchOSMetric(ctx context.Context, name string) error {
345 msg, err := sr.reaper.FetchStatsDirectlyFromOS(ctx, sr.md, name)
346 if err != nil {
347 return fmt.Errorf("could not read metric from OS: %v", err)
348 }
349 if msg != nil && len(msg.Data) > 0 {
350 log.GetLogger(ctx).WithField("metric", name).WithField("rows", len(msg.Data)).Info("measurements fetched")
351 sr.reaper.measurementCh <- *msg
352 }
353 return nil
354 }
355
356
357 func (sr *DbConnReaper) fetchSpecialMetric(ctx context.Context, name, storageName string) error {
358 var (
359 data metrics.Measurements
360 err error
361 )
362 switch name {
363 case specialMetricChangeEvents:
364 data, err = sr.reaper.GetObjectChangesMeasurement(ctx, sr.md)
365 case specialMetricInstanceUp:
366 data, err = sr.reaper.GetInstanceUpMeasurement(ctx, sr.md)
367 }
368 if err != nil {
369 return fmt.Errorf("failed to fetch special metric: %v", err)
370 }
371 if len(data) > 0 {
372 sr.sendEnvelope(ctx, name, storageName, data)
373 }
374 return err
375 }
376
377
378 func (sr *DbConnReaper) runLogParser(ctx context.Context) error {
379 lp, err := NewLogParser(ctx, sr.md, sr.reaper.measurementCh)
380 if err != nil {
381 return fmt.Errorf("failed to initialize log parser: %v", err)
382 }
383 if err := lp.ParseLogs(); err != nil {
384 return fmt.Errorf("log parser error: %v", err)
385 }
386 return nil
387 }
388
389
390
391 func (sr *DbConnReaper) detectServerRestart(ctx context.Context, data metrics.Measurements) {
392 if len(data) == 0 {
393 return
394 }
395 uptimeS, ok := data[0]["postmaster_uptime_s"].(int64)
396 if !ok {
397 return
398 }
399 prev := sr.lastUptimeS
400 sr.lastUptimeS = uptimeS
401 if prev > 0 && uptimeS < prev {
402 l := log.GetLogger(ctx)
403 l.Warning("Detected server restart (or failover)")
404 entry := metrics.NewMeasurement(data.GetEpoch())
405 entry["details"] = "Detected server restart (or failover)"
406 sr.reaper.measurementCh <- metrics.MeasurementEnvelope{
407 DBName: sr.md.Name,
408 MetricName: "object_changes",
409 Data: metrics.Measurements{entry},
410 CustomTags: sr.md.CustomTags,
411 }
412 }
413 }
414
415
416
417
418 func QueryMeasurements(ctx context.Context, md *sources.DbConn, sql string, args ...any) (metrics.Measurements, error) {
419 if strings.TrimSpace(sql) == "" {
420 return nil, errors.New("empty SQL")
421 }
422 qctx, cancel := db.WithOpTimeout(ctx, "change_detection_query", db.ChangeDetectionTimeout)
423 defer cancel()
424
425 if !md.IsPostgresSource() {
426 args = append([]any{pgx.QueryExecModeSimpleProtocol}, args...)
427 }
428
429 rows, err := md.Conn.Query(qctx, sql, args...)
430 if err == nil {
431 return pgx.CollectRows(rows, metrics.RowToMeasurement)
432 }
433 if errors.Is(err, context.DeadlineExceeded) {
434 return nil, fmt.Errorf("change_detection_query: %w", context.Cause(qctx))
435 }
436 return nil, err
437 }
438
439 func (r *reaper) DetectSprocChanges(ctx context.Context, md *sources.DbConn) (changeCounts ChangeDetectionResults) {
440 detectedChanges := make(metrics.Measurements, 0)
441 var firstRun bool
442 l := log.GetLogger(ctx)
443 changeCounts.Target = "functions"
444 l.Debug("checking for sproc changes...")
445 if _, ok := md.ChangeState["sproc_hashes"]; !ok {
446 firstRun = true
447 md.ChangeState["sproc_hashes"] = make(map[string]string)
448 }
449 mvp, ok := metricDefs.GetMetricDef("sproc_hashes")
450 if !ok {
451 l.Error("could not get sproc_hashes sql")
452 return
453 }
454 data, err := QueryMeasurements(ctx, md, mvp.GetSQL(int(md.Version)))
455 if err != nil {
456 l.Error(err)
457 return
458 }
459 for _, dr := range data {
460 objIdent := dr["tag_sproc"].(string) + dbMetricJoinStr + dr["tag_oid"].(string)
461 prevHash, ok := md.ChangeState["sproc_hashes"][objIdent]
462 ll := l.WithField("sproc", dr["tag_sproc"]).WithField("oid", dr["tag_oid"])
463 if ok {
464 if prevHash != dr["md5"].(string) {
465 ll.Debug("change detected")
466 dr["event"] = "alter"
467 detectedChanges = append(detectedChanges, dr)
468 md.ChangeState["sproc_hashes"][objIdent] = dr["md5"].(string)
469 changeCounts.Altered++
470 }
471 } else {
472 if !firstRun {
473 ll.Debug("new sproc detected")
474 dr["event"] = "create"
475 detectedChanges = append(detectedChanges, dr)
476 changeCounts.Created++
477 }
478 md.ChangeState["sproc_hashes"][objIdent] = dr["md5"].(string)
479 }
480 }
481
482 if !firstRun && len(md.ChangeState["sproc_hashes"]) != len(data) {
483 currentOidMap := make(map[string]bool)
484 for _, dr := range data {
485 currentOidMap[dr["tag_sproc"].(string)+dbMetricJoinStr+dr["tag_oid"].(string)] = true
486 }
487 for sprocIdent := range md.ChangeState["sproc_hashes"] {
488 _, ok := currentOidMap[sprocIdent]
489 if !ok {
490 splits := strings.Split(sprocIdent, dbMetricJoinStr)
491 l.WithField("sproc", splits[0]).WithField("oid", splits[1]).Debug("deleted sproc detected")
492 m := metrics.NewMeasurement(data.GetEpoch())
493 m["event"] = "drop"
494 m["tag_sproc"] = splits[0]
495 m["tag_oid"] = splits[1]
496 detectedChanges = append(detectedChanges, m)
497 delete(md.ChangeState["sproc_hashes"], sprocIdent)
498 changeCounts.Dropped++
499 }
500 }
501 }
502 l.Debugf("sproc changes detected: %d", len(detectedChanges))
503 if len(detectedChanges) > 0 {
504 r.measurementCh <- metrics.MeasurementEnvelope{
505 DBName: md.Name,
506 MetricName: "sproc_changes",
507 Data: detectedChanges,
508 CustomTags: md.CustomTags,
509 }
510 }
511 return changeCounts
512 }
513
514 func (r *reaper) DetectTableChanges(ctx context.Context, md *sources.DbConn) ChangeDetectionResults {
515 detectedChanges := make(metrics.Measurements, 0)
516 var firstRun bool
517 var changeCounts ChangeDetectionResults
518 l := log.GetLogger(ctx)
519 changeCounts.Target = "tables"
520 l.Debug("checking for table changes...")
521 if _, ok := md.ChangeState["table_hashes"]; !ok {
522 firstRun = true
523 md.ChangeState["table_hashes"] = make(map[string]string)
524 }
525 mvp, ok := metricDefs.GetMetricDef("table_hashes")
526 if !ok {
527 l.Error("could not get table_hashes sql")
528 return changeCounts
529 }
530 data, err := QueryMeasurements(ctx, md, mvp.GetSQL(int(md.Version)))
531 if err != nil {
532 l.Error(err)
533 return changeCounts
534 }
535 for _, dr := range data {
536 objIdent := dr["tag_table"].(string)
537 prevHash, ok := md.ChangeState["table_hashes"][objIdent]
538 ll := l.WithField("table", dr["tag_table"])
539 if ok {
540 if prevHash != dr["md5"].(string) {
541 ll.Debug("change detected")
542 dr["event"] = "alter"
543 detectedChanges = append(detectedChanges, dr)
544 md.ChangeState["table_hashes"][objIdent] = dr["md5"].(string)
545 changeCounts.Altered++
546 }
547 } else {
548 if !firstRun {
549 ll.Debug("new table detected")
550 dr["event"] = "create"
551 detectedChanges = append(detectedChanges, dr)
552 changeCounts.Created++
553 }
554 md.ChangeState["table_hashes"][objIdent] = dr["md5"].(string)
555 }
556 }
557
558 if !firstRun && len(md.ChangeState["table_hashes"]) != len(data) {
559 deletedTables := make([]string, 0)
560
561 currentTableMap := make(map[string]bool)
562 for _, dr := range data {
563 currentTableMap[dr["tag_table"].(string)] = true
564 }
565 for table := range md.ChangeState["table_hashes"] {
566 _, ok := currentTableMap[table]
567 if !ok {
568 l.WithField("table", table).Debug("deleted table detected")
569 influxEntry := metrics.NewMeasurement(data.GetEpoch())
570 influxEntry["event"] = "drop"
571 influxEntry["tag_table"] = table
572 detectedChanges = append(detectedChanges, influxEntry)
573 deletedTables = append(deletedTables, table)
574 changeCounts.Dropped++
575 }
576 }
577 for _, deletedTable := range deletedTables {
578 delete(md.ChangeState["table_hashes"], deletedTable)
579 }
580 }
581 l.Debugf("table changes detected: %d", len(detectedChanges))
582 if len(detectedChanges) > 0 {
583 r.measurementCh <- metrics.MeasurementEnvelope{
584 DBName: md.Name,
585 MetricName: "table_changes",
586 Data: detectedChanges,
587 CustomTags: md.CustomTags,
588 }
589 }
590 return changeCounts
591 }
592
593 func (r *reaper) DetectIndexChanges(ctx context.Context, md *sources.DbConn) ChangeDetectionResults {
594 detectedChanges := make(metrics.Measurements, 0)
595 var firstRun bool
596 var changeCounts ChangeDetectionResults
597 l := log.GetLogger(ctx)
598 changeCounts.Target = "indexes"
599 l.Debug("checking for index changes...")
600 if _, ok := md.ChangeState["index_hashes"]; !ok {
601 firstRun = true
602 md.ChangeState["index_hashes"] = make(map[string]string)
603 }
604 mvp, ok := metricDefs.GetMetricDef("index_hashes")
605 if !ok {
606 l.Error("could not get index_hashes sql")
607 return changeCounts
608 }
609 data, err := QueryMeasurements(ctx, md, mvp.GetSQL(int(md.Version)))
610 if err != nil {
611 l.Error(err)
612 return changeCounts
613 }
614 for _, dr := range data {
615 objIdent := dr["tag_index"].(string)
616 prevHash, ok := md.ChangeState["index_hashes"][objIdent]
617 ll := l.WithField("index", dr["tag_index"]).WithField("table", dr["table"])
618 if ok {
619 if prevHash != (dr["md5"].(string) + dr["is_valid"].(string)) {
620 ll.Debug("change detected")
621 dr["event"] = "alter"
622 detectedChanges = append(detectedChanges, dr)
623 md.ChangeState["index_hashes"][objIdent] = dr["md5"].(string) + dr["is_valid"].(string)
624 changeCounts.Altered++
625 }
626 } else {
627 if !firstRun {
628 ll.Debug("new index detected")
629 dr["event"] = "create"
630 detectedChanges = append(detectedChanges, dr)
631 changeCounts.Created++
632 }
633 md.ChangeState["index_hashes"][objIdent] = dr["md5"].(string) + dr["is_valid"].(string)
634 }
635 }
636
637 if !firstRun && len(md.ChangeState["index_hashes"]) != len(data) {
638 deletedIndexes := make([]string, 0)
639
640 currentIndexMap := make(map[string]bool)
641 for _, dr := range data {
642 currentIndexMap[dr["tag_index"].(string)] = true
643 }
644 for indexName := range md.ChangeState["index_hashes"] {
645 _, ok := currentIndexMap[indexName]
646 if !ok {
647 l.WithField("index", indexName).Debug("deleted index detected")
648 influxEntry := metrics.NewMeasurement(data.GetEpoch())
649 influxEntry["event"] = "drop"
650 influxEntry["tag_index"] = indexName
651 detectedChanges = append(detectedChanges, influxEntry)
652 deletedIndexes = append(deletedIndexes, indexName)
653 changeCounts.Dropped++
654 }
655 }
656 for _, deletedIndex := range deletedIndexes {
657 delete(md.ChangeState["index_hashes"], deletedIndex)
658 }
659 }
660 l.Debugf("index changes detected: %d", len(detectedChanges))
661 if len(detectedChanges) > 0 {
662 r.measurementCh <- metrics.MeasurementEnvelope{
663 DBName: md.Name,
664 MetricName: "index_changes",
665 Data: detectedChanges,
666 CustomTags: md.CustomTags,
667 }
668 }
669 return changeCounts
670 }
671
672 func (r *reaper) DetectPrivilegeChanges(ctx context.Context, md *sources.DbConn) ChangeDetectionResults {
673 detectedChanges := make(metrics.Measurements, 0)
674 var firstRun bool
675 var changeCounts ChangeDetectionResults
676 l := log.GetLogger(ctx)
677 changeCounts.Target = "privileges"
678 l.Debug("checking object privilege changes...")
679 if _, ok := md.ChangeState["object_privileges"]; !ok {
680 firstRun = true
681 md.ChangeState["object_privileges"] = make(map[string]string)
682 }
683 mvp, ok := metricDefs.GetMetricDef("privilege_changes")
684 if !ok || mvp.GetSQL(int(md.Version)) == "" {
685 l.Warning("could not get SQL for 'privilege_changes'. cannot detect privilege changes")
686 return changeCounts
687 }
688
689 data, err := QueryMeasurements(ctx, md, mvp.GetSQL(int(md.Version)))
690 if err != nil {
691 l.Error(err)
692 return changeCounts
693 }
694 currentState := make(map[string]bool)
695 for _, dr := range data {
696 objIdent := fmt.Sprintf("%s#:#%s#:#%s#:#%s", dr["object_type"], dr["tag_role"], dr["tag_object"], dr["privilege_type"])
697 ll := l.WithField("role", dr["tag_role"]).
698 WithField("object_type", dr["object_type"]).
699 WithField("object", dr["tag_object"]).
700 WithField("privilege_type", dr["privilege_type"])
701 if firstRun {
702 md.ChangeState["object_privileges"][objIdent] = ""
703 } else {
704 _, ok := md.ChangeState["object_privileges"][objIdent]
705 if !ok {
706 ll.Debug("new object privileges detected")
707 dr["event"] = "GRANT"
708 detectedChanges = append(detectedChanges, dr)
709 changeCounts.Created++
710 md.ChangeState["object_privileges"][objIdent] = ""
711 }
712 currentState[objIdent] = true
713 }
714 }
715
716 if !firstRun && len(currentState) > 0 {
717 for objPrevRun := range md.ChangeState["object_privileges"] {
718 if _, ok := currentState[objPrevRun]; !ok {
719 splits := strings.Split(objPrevRun, "#:#")
720 l.WithField("role", splits[1]).
721 WithField("object_type", splits[0]).
722 WithField("object", splits[2]).
723 WithField("privilege_type", splits[3]).
724 Debug("removed object privileges detected")
725 revokeEntry := metrics.NewMeasurement(data.GetEpoch())
726 revokeEntry["object_type"] = splits[0]
727 revokeEntry["tag_role"] = splits[1]
728 revokeEntry["tag_object"] = splits[2]
729 revokeEntry["privilege_type"] = splits[3]
730 revokeEntry["event"] = "REVOKE"
731 detectedChanges = append(detectedChanges, revokeEntry)
732 changeCounts.Dropped++
733 delete(md.ChangeState["object_privileges"], objPrevRun)
734 }
735 }
736 }
737 l.Debugf("object privilege changes detected: %d", len(detectedChanges))
738 if len(detectedChanges) > 0 {
739 r.measurementCh <- metrics.MeasurementEnvelope{
740 DBName: md.Name,
741 MetricName: "privilege_changes",
742 Data: detectedChanges,
743 CustomTags: md.CustomTags,
744 }
745 }
746 return changeCounts
747 }
748
749 func (r *reaper) DetectConfigurationChanges(ctx context.Context, md *sources.DbConn) ChangeDetectionResults {
750 detectedChanges := make(metrics.Measurements, 0)
751 var firstRun bool
752 var changeCounts ChangeDetectionResults
753 l := log.GetLogger(ctx)
754 changeCounts.Target = "settings"
755 l.Debug("checking for configuration changes...")
756 if _, ok := md.ChangeState["configuration_hashes"]; !ok {
757 firstRun = true
758 md.ChangeState["configuration_hashes"] = make(map[string]string)
759 }
760 mvp, ok := metricDefs.GetMetricDef("configuration_hashes")
761 if !ok {
762 l.Error("could not get configuration_hashes sql")
763 return changeCounts
764 }
765 rows, err := md.Conn.Query(ctx, mvp.GetSQL(md.Version))
766 if err != nil {
767 l.Error(err)
768 return changeCounts
769 }
770 defer rows.Close()
771 var (
772 objIdent, objValue string
773 epoch int64
774 )
775 for rows.Next() {
776 if rows.Scan(&epoch, &objIdent, &objValue) != nil {
777 return changeCounts
778 }
779 prevРash, ok := md.ChangeState["configuration_hashes"][objIdent]
780 ll := l.WithField("setting", objIdent)
781 if ok {
782 if prevРash != objValue {
783 ll.Warningf("settings change detected: %s = %s (prev: %s)", objIdent, objValue, prevРash)
784 detectedChanges = append(detectedChanges, metrics.Measurement{
785 metrics.EpochColumnName: epoch,
786 "tag_setting": objIdent,
787 "value": objValue,
788 "event": "alter"})
789 md.ChangeState["configuration_hashes"][objIdent] = objValue
790 changeCounts.Altered++
791 }
792 } else {
793 md.ChangeState["configuration_hashes"][objIdent] = objValue
794 if firstRun {
795 continue
796 }
797 ll.Debug("new setting detected")
798 detectedChanges = append(detectedChanges, metrics.Measurement{
799 metrics.EpochColumnName: epoch,
800 "tag_setting": objIdent,
801 "value": objValue,
802 "event": "create"})
803 changeCounts.Created++
804 }
805 }
806 l.Debugf("configuration changes detected: %d", len(detectedChanges))
807 if len(detectedChanges) > 0 {
808 r.measurementCh <- metrics.MeasurementEnvelope{
809 DBName: md.Name,
810 MetricName: "configuration_changes",
811 Data: detectedChanges,
812 CustomTags: md.CustomTags,
813 }
814 }
815 return changeCounts
816 }
817
818
819
820 func (r *reaper) GetInstanceUpMeasurement(ctx context.Context, md *sources.DbConn) (metrics.Measurements, error) {
821 return metrics.Measurements{
822 metrics.Measurement{
823 metrics.EpochColumnName: time.Now().UnixNano(),
824 "instance_up": func() int {
825 if md.Conn.Ping(ctx) == nil {
826 return 1
827 }
828 return 0
829 }(),
830 },
831 }, nil
832 }
833
834 func (r *reaper) GetObjectChangesMeasurement(ctx context.Context, md *sources.DbConn) (metrics.Measurements, error) {
835 md.Lock()
836 defer md.Unlock()
837 spN := r.DetectSprocChanges(ctx, md)
838 tblN := r.DetectTableChanges(ctx, md)
839 idxN := r.DetectIndexChanges(ctx, md)
840 cnfN := r.DetectConfigurationChanges(ctx, md)
841 privN := r.DetectPrivilegeChanges(ctx, md)
842 if spN.Total()+tblN.Total()+idxN.Total()+cnfN.Total()+privN.Total() == 0 {
843 return nil, nil
844 }
845 m := metrics.NewMeasurement(time.Now().UnixNano())
846 m["details"] = strings.Join([]string{spN.String(), tblN.String(), idxN.String(), cnfN.String(), privN.String()}, " ")
847 return metrics.Measurements{m}, nil
848 }
849