master
go 817 lines 21.3 KB
Raw
1 //go:build cgo
2
3 package as400
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "maps"
10 "strings"
11 "sync"
12 "time"
13
14 "golang.org/x/sync/errgroup"
15
16 as400proto "github.com/netdata/netdata/go/plugins/plugin/ibm.d/protocols/as400"
17 )
18
19 type slowPathConfig struct {
20 enabled bool
21 interval time.Duration
22 maxConnections int
23 }
24
25 type messageQueueSnapshot struct {
26 metrics map[string]messageQueueInstanceMetrics
27 meta map[string]messageQueueMetrics
28 timestamp time.Time
29 err error
30 }
31
32 type jobQueueSnapshot struct {
33 metrics map[string]jobQueueInstanceMetrics
34 meta map[string]jobQueueMetrics
35 timestamp time.Time
36 err error
37 }
38
39 type outputQueueSnapshot struct {
40 metrics map[string]outputQueueInstanceMetrics
41 meta map[string]outputQueueMetrics
42 timestamp time.Time
43 err error
44 }
45
46 type subsystemSnapshot struct {
47 metrics map[string]subsystemInstanceMetrics
48 meta map[string]subsystemMetrics
49 timestamp time.Time
50 err error
51 }
52
53 type planCacheSnapshot struct {
54 values map[string]planCacheInstanceMetrics
55 meta map[string]planCacheMetrics
56 timestamp time.Time
57 err error
58 }
59
60 type slowCache struct {
61 mu sync.RWMutex
62 messageQueues messageQueueSnapshot
63 jobQueues jobQueueSnapshot
64 outputQueues outputQueueSnapshot
65 subsystems subsystemSnapshot
66 planCache planCacheSnapshot
67 latency latencyCache
68 }
69
70 func (c *Collector) slowPathActive() bool {
71 return c != nil && c.slow.config.enabled && c.slow.client != nil
72 }
73
74 func (c *slowCache) beginLatencyCycle(ts time.Time) {
75 c.latency.beginCycle(ts)
76 }
77
78 func (c *slowCache) addLatency(name string, value int64) {
79 c.latency.add(name, value)
80 }
81
82 func (c *slowCache) setMessageQueues(snapshot messageQueueSnapshot) {
83 c.mu.Lock()
84 c.messageQueues = snapshot
85 c.mu.Unlock()
86 }
87
88 func (c *slowCache) setJobQueues(snapshot jobQueueSnapshot) {
89 c.mu.Lock()
90 c.jobQueues = snapshot
91 c.mu.Unlock()
92 }
93
94 func (c *slowCache) setOutputQueues(snapshot outputQueueSnapshot) {
95 c.mu.Lock()
96 c.outputQueues = snapshot
97 c.mu.Unlock()
98 }
99
100 func (c *slowCache) setSubsystems(snapshot subsystemSnapshot) {
101 c.mu.Lock()
102 c.subsystems = snapshot
103 c.mu.Unlock()
104 }
105
106 func (c *slowCache) setPlanCache(snapshot planCacheSnapshot) {
107 c.mu.Lock()
108 c.planCache = snapshot
109 c.mu.Unlock()
110 }
111
112 func (c *slowCache) getMessageQueues() messageQueueSnapshot {
113 c.mu.RLock()
114 defer c.mu.RUnlock()
115 return cloneMessageQueueSnapshot(c.messageQueues)
116 }
117
118 func (c *slowCache) getJobQueues() jobQueueSnapshot {
119 c.mu.RLock()
120 defer c.mu.RUnlock()
121 return cloneJobQueueSnapshot(c.jobQueues)
122 }
123
124 func (c *slowCache) getOutputQueues() outputQueueSnapshot {
125 c.mu.RLock()
126 defer c.mu.RUnlock()
127 return cloneOutputQueueSnapshot(c.outputQueues)
128 }
129
130 func (c *slowCache) getSubsystems() subsystemSnapshot {
131 c.mu.RLock()
132 defer c.mu.RUnlock()
133 return cloneSubsystemSnapshot(c.subsystems)
134 }
135
136 func (c *slowCache) getPlanCache() planCacheSnapshot {
137 c.mu.RLock()
138 defer c.mu.RUnlock()
139 return clonePlanCacheSnapshot(c.planCache)
140 }
141
142 func (c *slowCache) getLatencies() (map[string]int64, time.Time) {
143 return c.latency.snapshot()
144 }
145
146 func cloneMessageQueueSnapshot(src messageQueueSnapshot) messageQueueSnapshot {
147 dst := messageQueueSnapshot{
148 timestamp: src.timestamp,
149 err: src.err,
150 }
151 if src.metrics != nil {
152 dst.metrics = make(map[string]messageQueueInstanceMetrics, len(src.metrics))
153 maps.Copy(dst.metrics, src.metrics)
154 }
155 if src.meta != nil {
156 dst.meta = make(map[string]messageQueueMetrics, len(src.meta))
157 maps.Copy(dst.meta, src.meta)
158 }
159 return dst
160 }
161
162 func cloneJobQueueSnapshot(src jobQueueSnapshot) jobQueueSnapshot {
163 dst := jobQueueSnapshot{
164 timestamp: src.timestamp,
165 err: src.err,
166 }
167 if src.metrics != nil {
168 dst.metrics = make(map[string]jobQueueInstanceMetrics, len(src.metrics))
169 maps.Copy(dst.metrics, src.metrics)
170 }
171 if src.meta != nil {
172 dst.meta = make(map[string]jobQueueMetrics, len(src.meta))
173 maps.Copy(dst.meta, src.meta)
174 }
175 return dst
176 }
177
178 func cloneOutputQueueSnapshot(src outputQueueSnapshot) outputQueueSnapshot {
179 dst := outputQueueSnapshot{
180 timestamp: src.timestamp,
181 err: src.err,
182 }
183 if src.metrics != nil {
184 dst.metrics = make(map[string]outputQueueInstanceMetrics, len(src.metrics))
185 maps.Copy(dst.metrics, src.metrics)
186 }
187 if src.meta != nil {
188 dst.meta = make(map[string]outputQueueMetrics, len(src.meta))
189 maps.Copy(dst.meta, src.meta)
190 }
191 return dst
192 }
193
194 func cloneSubsystemSnapshot(src subsystemSnapshot) subsystemSnapshot {
195 dst := subsystemSnapshot{
196 timestamp: src.timestamp,
197 err: src.err,
198 }
199 if src.metrics != nil {
200 dst.metrics = make(map[string]subsystemInstanceMetrics, len(src.metrics))
201 maps.Copy(dst.metrics, src.metrics)
202 }
203 if src.meta != nil {
204 dst.meta = make(map[string]subsystemMetrics, len(src.meta))
205 maps.Copy(dst.meta, src.meta)
206 }
207 return dst
208 }
209
210 func clonePlanCacheSnapshot(src planCacheSnapshot) planCacheSnapshot {
211 dst := planCacheSnapshot{
212 timestamp: src.timestamp,
213 err: src.err,
214 }
215 if src.values != nil {
216 dst.values = make(map[string]planCacheInstanceMetrics, len(src.values))
217 maps.Copy(dst.values, src.values)
218 }
219 if src.meta != nil {
220 dst.meta = make(map[string]planCacheMetrics, len(src.meta))
221 maps.Copy(dst.meta, src.meta)
222 }
223 return dst
224 }
225
226 func (c *Collector) startSlowPath() error {
227 c.stopSlowPath()
228
229 cfg := slowPathConfig{
230 enabled: c.SlowPath,
231 interval: time.Duration(c.SlowPathUpdateEvery),
232 maxConnections: c.SlowPathMaxConnections,
233 }
234
235 if !cfg.enabled {
236 c.Debugf("slow path disabled; running sequential-only mode")
237 c.slow.config = cfg
238 return nil
239 }
240
241 if cfg.interval <= 0 {
242 cfg.interval = 30 * time.Second
243 }
244 if cfg.maxConnections <= 0 {
245 cfg.maxConnections = 1
246 }
247
248 fastInterval := time.Duration(c.fastPathIntervalSeconds()) * time.Second
249 if fastInterval <= 0 {
250 fastInterval = time.Second
251 }
252 if cfg.interval < fastInterval {
253 c.Warningf("slow path update every %s is shorter than main update %s; using %s", cfg.interval, fastInterval, fastInterval)
254 cfg.interval = fastInterval
255 }
256
257 clientCfg := as400proto.Config{
258 DSN: c.DSN,
259 Timeout: time.Duration(c.Timeout),
260 MaxOpenConns: cfg.maxConnections,
261 }
262
263 client := as400proto.NewClient(clientCfg)
264 ctx := context.Background()
265 if err := client.Connect(ctx); err != nil {
266 return fmt.Errorf("slow path: connect failed: %w", err)
267 }
268 if err := client.Ping(ctx); err != nil {
269 _ = client.Close()
270 return fmt.Errorf("slow path: ping failed: %w", err)
271 }
272
273 runCtx, cancel := context.WithCancel(context.Background())
274 c.slow.client = client
275 c.slow.cancel = cancel
276 c.slow.config = cfg
277 c.slow.wg.Add(1)
278 go c.runSlowPath(runCtx)
279 c.Infof("slow path worker started (interval=%s, max_conns=%d)", cfg.interval, cfg.maxConnections)
280 return nil
281 }
282
283 func (c *Collector) stopSlowPath() {
284 if c.slow.cancel != nil {
285 c.slow.cancel()
286 }
287 c.slow.wg.Wait()
288 if c.slow.client != nil {
289 if err := c.slow.client.Close(); err != nil {
290 c.Errorf("slow path: closing client failed: %v", err)
291 }
292 }
293 c.slow.cancel = nil
294 c.slow.client = nil
295 c.slow.config = slowPathConfig{}
296 c.slow.cache = slowCache{}
297 }
298
299 func (c *Collector) runSlowPath(ctx context.Context) {
300 defer c.slow.wg.Done()
301
302 interval := c.slow.config.interval
303 if interval <= 0 {
304 interval = 30 * time.Second
305 }
306
307 now := time.Now()
308 beat := now
309 c.runSlowCollectors(ctx, beat)
310 nextBeat := beat.Add(interval)
311
312 for {
313 sleep := time.Until(nextBeat)
314 if sleep > 0 {
315 timer := time.NewTimer(sleep)
316 select {
317 case <-ctx.Done():
318 timer.Stop()
319 return
320 case <-timer.C:
321 }
322 } else {
323 select {
324 case <-ctx.Done():
325 return
326 default:
327 }
328 }
329
330 beat = nextBeat
331 c.runSlowCollectors(ctx, beat)
332
333 nextBeat = nextBeat.Add(interval)
334 now = time.Now()
335 for nextBeat.Before(now) {
336 nextBeat = nextBeat.Add(interval)
337 }
338 }
339 }
340
341 func (c *Collector) runSlowCollectors(ctx context.Context, beat time.Time) {
342 if ctx.Err() != nil {
343 return
344 }
345
346 c.slow.cache.beginLatencyCycle(beat)
347
348 workCtx, cancel := context.WithCancel(ctx)
349 defer cancel()
350
351 group, groupCtx := errgroup.WithContext(workCtx)
352 group.SetLimit(c.slow.config.maxConnections)
353
354 group.Go(func() error {
355 snapshot, err := c.fetchMessageQueues(groupCtx, beat, c.slowDoQuery)
356 c.slow.cache.setMessageQueues(snapshot)
357 if err != nil {
358 return fmt.Errorf("message queues: %w", err)
359 }
360 return nil
361 })
362
363 group.Go(func() error {
364 snapshot, err := c.fetchJobQueues(groupCtx, beat, c.slowDoQuery)
365 c.slow.cache.setJobQueues(snapshot)
366 if err != nil {
367 return fmt.Errorf("job queues: %w", err)
368 }
369 return nil
370 })
371
372 group.Go(func() error {
373 snapshot, err := c.fetchOutputQueues(groupCtx, beat, c.slowDoQuery)
374 c.slow.cache.setOutputQueues(snapshot)
375 if err != nil {
376 return fmt.Errorf("output queues: %w", err)
377 }
378 return nil
379 })
380
381 group.Go(func() error {
382 snapshot, err := c.fetchSubsystems(groupCtx, beat, c.slowDoQuery, c.slowDoQueryRow)
383 c.slow.cache.setSubsystems(snapshot)
384 if err != nil {
385 return fmt.Errorf("subsystems: %w", err)
386 }
387 return nil
388 })
389
390 if c.CollectPlanCacheMetrics.IsEnabled() {
391 group.Go(func() error {
392 snapshot, err := c.fetchPlanCache(groupCtx, beat, c.slowExec, c.slowDoQuery)
393 c.slow.cache.setPlanCache(snapshot)
394 if err != nil {
395 return fmt.Errorf("plan cache: %w", err)
396 }
397 return nil
398 })
399 } else {
400 c.slow.cache.setPlanCache(planCacheSnapshot{
401 timestamp: beat,
402 err: nil,
403 values: make(map[string]planCacheInstanceMetrics),
404 meta: make(map[string]planCacheMetrics),
405 })
406 }
407
408 if err := group.Wait(); err != nil && !errors.Is(err, context.Canceled) {
409 c.logErrorOnce("slow_path_error", "slow path: %s", trimDriverMessage(err))
410 } else if err == nil {
411 c.clearErrorOnce("slow_path_error")
412 }
413 }
414
415 type queryFunc func(ctx context.Context, queryName, query string, assign func(column, value string, lineEnd bool)) error
416 type queryRowFunc func(ctx context.Context, queryName, query string, assign func(column, value string)) error
417 type execFunc func(ctx context.Context, query string) error
418
419 func (c *Collector) fetchMessageQueues(ctx context.Context, beat time.Time, do queryFunc) (messageQueueSnapshot, error) {
420 snapshot := messageQueueSnapshot{
421 metrics: make(map[string]messageQueueInstanceMetrics),
422 meta: make(map[string]messageQueueMetrics),
423 timestamp: beat,
424 }
425
426 if len(c.messageQueueTargets) == 0 {
427 return snapshot, nil
428 }
429
430 var firstErr error
431
432 for _, target := range c.messageQueueTargets {
433 key := target.ID()
434 errorKey := "slow_message_queue_" + key
435 meta := messageQueueMetrics{
436 library: target.Library,
437 name: target.Name,
438 }
439 metrics := messageQueueInstanceMetrics{}
440
441 queryName := fmt.Sprintf("message_queue_%s_%s", target.Library, target.Name)
442 query := buildMessageQueueQuery(target, c.supportsMessageQueueTableFunction())
443 err := do(ctx, queryName, query, func(column, value string, lineEnd bool) {
444 switch column {
445 case "MESSAGE_COUNT":
446 metrics.Total = parseInt64OrZero(value)
447 case "INFORMATIONAL_MESSAGES":
448 metrics.Informational = parseInt64OrZero(value)
449 case "INQUIRY_MESSAGES":
450 metrics.Inquiry = parseInt64OrZero(value)
451 case "DIAGNOSTIC_MESSAGES":
452 metrics.Diagnostic = parseInt64OrZero(value)
453 case "ESCAPE_MESSAGES":
454 metrics.Escape = parseInt64OrZero(value)
455 case "NOTIFY_MESSAGES":
456 metrics.Notify = parseInt64OrZero(value)
457 case "SENDER_COPY_MESSAGES":
458 metrics.SenderCopy = parseInt64OrZero(value)
459 case "MAX_SEVERITY":
460 metrics.MaxSeverity = parseInt64OrZero(value)
461 }
462 })
463
464 if err != nil {
465 c.logQueryErrorOnce(errorKey, query, err)
466 if firstErr == nil {
467 firstErr = fmt.Errorf("message queue %s: %w", key, err)
468 }
469 continue
470 }
471 c.clearErrorOnce(errorKey)
472
473 snapshot.metrics[key] = metrics
474 snapshot.meta[key] = meta
475 }
476
477 snapshot.err = firstErr
478 return snapshot, firstErr
479 }
480
481 func (c *Collector) fetchJobQueues(ctx context.Context, beat time.Time, do queryFunc) (jobQueueSnapshot, error) {
482 snapshot := jobQueueSnapshot{
483 metrics: make(map[string]jobQueueInstanceMetrics),
484 meta: make(map[string]jobQueueMetrics),
485 timestamp: beat,
486 }
487
488 if len(c.jobQueueTargets) == 0 {
489 return snapshot, nil
490 }
491
492 var firstErr error
493
494 for _, target := range c.jobQueueTargets {
495 key := target.ID()
496 errorKey := "slow_job_queue_" + key
497 meta := jobQueueMetrics{
498 library: target.Library,
499 name: target.Name,
500 status: "UNKNOWN",
501 }
502 metrics := jobQueueInstanceMetrics{}
503 found := false
504
505 queryName := fmt.Sprintf("job_queue_%s_%s", target.Library, target.Name)
506 query := buildJobQueueQuery(target)
507 err := do(ctx, queryName, query, func(column, value string, lineEnd bool) {
508 switch column {
509 case "JOB_QUEUE_STATUS":
510 meta.status = strings.TrimSpace(value)
511 case "NUMBER_OF_JOBS":
512 metrics.NumberOfJobs = parseInt64OrZero(value)
513 case "RELEASED_JOBS":
514 meta.jobsWaiting = parseInt64OrZero(value)
515 case "SCHEDULED_JOBS":
516 meta.jobsScheduled = parseInt64OrZero(value)
517 case "HELD_JOBS":
518 meta.jobsHeld = parseInt64OrZero(value)
519 case "MAXIMUM_ACTIVE_JOBS":
520 meta.maxJobs = parseInt64OrZero(value)
521 }
522 if lineEnd {
523 found = true
524 }
525 })
526
527 if err != nil {
528 c.logQueryErrorOnce(errorKey, query, err)
529 if firstErr == nil {
530 firstErr = fmt.Errorf("job queue %s: %w", key, err)
531 }
532 continue
533 }
534 c.clearErrorOnce(errorKey)
535
536 if !found {
537 meta.status = "NOT_FOUND"
538 }
539
540 snapshot.metrics[key] = metrics
541 snapshot.meta[key] = meta
542 }
543
544 snapshot.err = firstErr
545 return snapshot, firstErr
546 }
547
548 func (c *Collector) fetchOutputQueues(ctx context.Context, beat time.Time, do queryFunc) (outputQueueSnapshot, error) {
549 snapshot := outputQueueSnapshot{
550 metrics: make(map[string]outputQueueInstanceMetrics),
551 meta: make(map[string]outputQueueMetrics),
552 timestamp: beat,
553 }
554
555 if len(c.outputQueueTargets) == 0 {
556 return snapshot, nil
557 }
558
559 var firstErr error
560
561 for _, target := range c.outputQueueTargets {
562 key := target.ID()
563 errorEntriesKey := "slow_output_queue_entries_" + key
564 errorInfoKey := "slow_output_queue_info_" + key
565 meta := outputQueueMetrics{
566 library: target.Library,
567 name: target.Name,
568 status: "UNKNOWN",
569 }
570
571 metrics := outputQueueInstanceMetrics{}
572 entriesCount := int64(0)
573 entriesUsed := false
574
575 queryName := fmt.Sprintf("output_queue_%s_%s", target.Library, target.Name)
576 entriesQuery := buildOutputQueueEntriesQuery(target)
577 err := do(ctx, queryName, entriesQuery, func(column, value string, lineEnd bool) {
578 if lineEnd {
579 entriesCount++
580 }
581 })
582 if err != nil {
583 c.logQueryErrorOnce(errorEntriesKey, entriesQuery, err)
584 if firstErr == nil {
585 firstErr = fmt.Errorf("output queue %s (entries): %w", key, err)
586 }
587 } else {
588 c.clearErrorOnce(errorEntriesKey)
589 entriesUsed = true
590 metrics.Files = entriesCount
591 }
592
593 infoQuery := buildOutputQueueInfoQuery(target)
594 viewErr := do(ctx, queryName+"_view", infoQuery, func(column, value string, lineEnd bool) {
595 switch column {
596 case "OUTPUT_QUEUE_STATUS":
597 meta.status = strings.TrimSpace(value)
598 case "NUMBER_OF_WRITERS":
599 metrics.Writers = parseInt64OrZero(value)
600 case "NUMBER_OF_FILES":
601 if !entriesUsed {
602 metrics.Files = parseInt64OrZero(value)
603 }
604 }
605 })
606 if viewErr != nil {
607 c.logQueryErrorOnce(errorInfoKey, infoQuery, viewErr)
608 if firstErr == nil {
609 firstErr = fmt.Errorf("output queue %s (info): %w", key, viewErr)
610 }
611 continue
612 }
613 c.clearErrorOnce(errorInfoKey)
614
615 metrics.Released = boolToInt(strings.EqualFold(meta.status, "RELEASED"))
616 snapshot.metrics[key] = metrics
617 snapshot.meta[key] = meta
618 }
619
620 snapshot.err = firstErr
621 return snapshot, firstErr
622 }
623
624 func (c *Collector) countSubsystemsWith(doRow queryRowFunc, ctx context.Context) (int, error) {
625 var count int64
626 err := doRow(ctx, "count_subsystems", queryCountSubsystems, func(column, value string) {
627 if column == "COUNT" {
628 if v, ok := c.parseInt64Value(value, 1); ok {
629 count = v
630 }
631 }
632 })
633 return int(count), err
634 }
635
636 func (c *Collector) fetchSubsystems(ctx context.Context, beat time.Time, do queryFunc, doRow queryRowFunc) (subsystemSnapshot, error) {
637 snapshot := subsystemSnapshot{
638 metrics: make(map[string]subsystemInstanceMetrics),
639 meta: make(map[string]subsystemMetrics),
640 timestamp: beat,
641 }
642
643 query := querySubsystems
644 if c.MaxSubsystems > 0 {
645 if total, err := c.countSubsystemsWith(doRow, ctx); err != nil {
646 c.logOnce("subsystem_count_failed", "failed to count subsystems before applying limit: %v", err)
647 } else if total > c.MaxSubsystems {
648 c.logOnce("subsystem_limit", "subsystem count (%d) exceeds limit (%d); truncating results", total, c.MaxSubsystems)
649 }
650 query = withFetchLimit(query, c.MaxSubsystems)
651 }
652
653 currentSubsystem := ""
654 err := do(ctx, "subsystems", query, func(column, value string, lineEnd bool) {
655 switch column {
656 case "SUBSYSTEM_NAME":
657 name := strings.TrimSpace(value)
658 if name == "" {
659 currentSubsystem = ""
660 return
661 }
662 if c.subsystemSelector != nil && !c.subsystemSelector.MatchString(name) {
663 currentSubsystem = ""
664 return
665 }
666 currentSubsystem = name
667 subsystem := subsystemMetrics{name: name, status: "ACTIVE"}
668 parts := strings.SplitN(name, "/", 2)
669 if len(parts) == 2 {
670 subsystem.library = parts[0]
671 subsystem.name = parts[1]
672 }
673 snapshot.meta[currentSubsystem] = subsystem
674 case "CURRENT_ACTIVE_JOBS":
675 if currentSubsystem != "" {
676 if v, ok := c.parseInt64Value(value, 1); ok {
677 if metrics, exists := snapshot.metrics[currentSubsystem]; exists {
678 metrics.CurrentActiveJobs = v
679 snapshot.metrics[currentSubsystem] = metrics
680 } else {
681 snapshot.metrics[currentSubsystem] = subsystemInstanceMetrics{CurrentActiveJobs: v}
682 }
683 }
684 }
685 case "MAXIMUM_ACTIVE_JOBS":
686 if currentSubsystem != "" {
687 if v, ok := c.parseInt64Value(value, 1); ok {
688 if metrics, exists := snapshot.metrics[currentSubsystem]; exists {
689 metrics.MaximumActiveJobs = v
690 snapshot.metrics[currentSubsystem] = metrics
691 } else {
692 snapshot.metrics[currentSubsystem] = subsystemInstanceMetrics{MaximumActiveJobs: v}
693 }
694 }
695 }
696 }
697
698 if lineEnd {
699 currentSubsystem = ""
700 }
701 })
702
703 if err != nil {
704 c.logQueryErrorOnce("slow_subsystems", query, err)
705 snapshot.err = err
706 return snapshot, err
707 }
708 c.clearErrorOnce("slow_subsystems")
709 snapshot.err = nil
710 return snapshot, nil
711 }
712
713 func (c *Collector) fetchPlanCache(ctx context.Context, beat time.Time, exec execFunc, do queryFunc) (planCacheSnapshot, error) {
714 snapshot := planCacheSnapshot{
715 values: make(map[string]planCacheInstanceMetrics),
716 meta: make(map[string]planCacheMetrics),
717 timestamp: beat,
718 }
719
720 if err := exec(ctx, callAnalyzePlanCache); err != nil {
721 c.logQueryErrorOnce("slow_plan_cache_analyze", callAnalyzePlanCache, err)
722 snapshot.err = fmt.Errorf("analyze plan cache: %w", err)
723 return snapshot, snapshot.err
724 }
725
726 var currentHeading string
727 err := do(ctx, "plan_cache_summary", queryPlanCacheSummary, func(column, value string, lineEnd bool) {
728 switch column {
729 case "HEADING":
730 currentHeading = strings.TrimSpace(value)
731 case "VALUE":
732 if currentHeading == "" {
733 return
734 }
735 key := planCacheMetricKey(currentHeading)
736 if key == "" {
737 return
738 }
739 if parsed, ok := c.parseInt64Value(value, precision); ok {
740 snapshot.values[key] = planCacheInstanceMetrics{Value: parsed}
741 snapshot.meta[key] = planCacheMetrics{heading: currentHeading}
742 }
743 }
744 if lineEnd {
745 currentHeading = ""
746 }
747 })
748
749 if err != nil {
750 c.logQueryErrorOnce("slow_plan_cache_summary", queryPlanCacheSummary, err)
751 snapshot.err = fmt.Errorf("plan cache summary: %w", err)
752 return snapshot, snapshot.err
753 }
754 c.clearErrorOnce("slow_plan_cache_summary")
755
756 return snapshot, nil
757 }
758
759 func (c *Collector) slowDoQuery(ctx context.Context, queryName, query string, assign func(column, value string, lineEnd bool)) error {
760 if c.slow.client == nil {
761 return errors.New("slow path client not initialised")
762 }
763
764 start := time.Now()
765 err := c.queryWithClient(ctx, c.slow.client, queryName, query, assign)
766 elapsed := time.Since(start)
767 c.slow.cache.addLatency(queryName, elapsed.Microseconds())
768 return err
769 }
770
771 func (c *Collector) slowDoQueryRow(ctx context.Context, queryName, query string, assign func(column, value string)) error {
772 if c.slow.client == nil {
773 return errors.New("slow path client not initialised")
774 }
775
776 start := time.Now()
777 err := c.queryRowWithClient(ctx, c.slow.client, queryName, query, assign)
778 elapsed := time.Since(start)
779 c.slow.cache.addLatency(queryName, elapsed.Microseconds())
780 return err
781 }
782
783 func (c *Collector) slowExec(ctx context.Context, query string) error {
784 if c.slow.client == nil {
785 return errors.New("slow path client not initialised")
786 }
787 start := time.Now()
788 err := c.execWithClient(ctx, c.slow.client, query)
789 elapsed := time.Since(start)
790 c.slow.cache.addLatency("analyze_plan_cache", elapsed.Microseconds())
791 return err
792 }
793
794 func (c *Collector) queryWithClient(ctx context.Context, client *as400proto.Client, queryName, query string, assign func(column, value string, lineEnd bool)) error {
795 return client.Query(ctx, query, func(columns []string, values []string) error {
796 for idx, col := range columns {
797 assign(col, values[idx], idx == len(columns)-1)
798 }
799 return nil
800 })
801 }
802
803 func (c *Collector) queryRowWithClient(ctx context.Context, client *as400proto.Client, queryName, query string, assign func(column, value string)) error {
804 return client.QueryWithLimit(ctx, query, 1, func(columns []string, values []string) error {
805 for idx, col := range columns {
806 assign(col, values[idx])
807 }
808 return nil
809 })
810 }
811
812 func (c *Collector) execWithClient(ctx context.Context, client *as400proto.Client, query string) error {
813 if err := client.Connect(ctx); err != nil {
814 return err
815 }
816 return client.Exec(ctx, query)
817 }