master
go 263 lines 6.66 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package scrape
4
5 import (
6 "fmt"
7 "strconv"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/logger"
13 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14
15 "github.com/vmware/govmomi/performance"
16 "github.com/vmware/govmomi/vim25/types"
17 vsantypes "github.com/vmware/govmomi/vsan/types"
18 )
19
20 type Client interface {
21 Version() string
22 PerformanceMetrics([]types.PerfQuerySpec) ([]performance.EntityMetric, error)
23 VSANPerfMetrics(types.ManagedObjectReference, []vsantypes.VsanPerfQuerySpec) ([]vsantypes.VsanPerfEntityMetricCSV, error)
24 VSANSpaceUsage(types.ManagedObjectReference) (*vsantypes.VsanSpaceUsage, error)
25 VSANHealth(types.ManagedObjectReference) (string, error)
26 }
27
28 func New(client Client) *Scraper {
29 v := &Scraper{
30 Client: client,
31 vsanWarnings: make(map[string]bool),
32 }
33 v.calcMaxQuery()
34 return v
35 }
36
37 type Scraper struct {
38 *logger.Logger
39 Client
40 maxQuery int
41 vsanWarnings map[string]bool
42 vsanWarningsLock sync.Mutex
43 }
44
45 // Default settings for vCenter 6.5 and above is 256, prior versions of vCenter have this set to 64.
46 func (s *Scraper) calcMaxQuery() {
47 major, minor, err := parseVersion(s.Version())
48 if err != nil || major < 6 || (major == 6 && minor < 5) {
49 s.maxQuery = 64
50 return
51 }
52 s.maxQuery = 256
53 }
54
55 func (s *Scraper) ScrapeHosts(hosts rs.Hosts) []performance.EntityMetric {
56 t := time.Now()
57 pqs := newHostsPerfQuerySpecs(hosts)
58 ms := s.scrapeMetrics(pqs)
59 s.Debugf("scraping : scraped metrics for %d/%d hosts, process took %s",
60 len(ms),
61 len(hosts),
62 time.Since(t),
63 )
64 return ms
65 }
66
67 func (s *Scraper) ScrapeVMs(vms rs.VMs) []performance.EntityMetric {
68 t := time.Now()
69 pqs := newVMsPerfQuerySpecs(vms)
70 ms := s.scrapeMetrics(pqs)
71 s.Debugf("scraping : scraped metrics for %d/%d vms, process took %s",
72 len(ms),
73 len(vms),
74 time.Since(t),
75 )
76 return ms
77 }
78
79 func (s *Scraper) ScrapeDatastores(datastores rs.Datastores) []performance.EntityMetric {
80 t := time.Now()
81 pqs := newDatastoresPerfQuerySpecs(datastores)
82 ms := s.scrapeMetrics(pqs)
83 s.Debugf("scraping : scraped metrics for %d/%d datastores, process took %s",
84 len(ms),
85 len(datastores),
86 time.Since(t),
87 )
88 return ms
89 }
90
91 func (s *Scraper) ScrapeClusters(clusters rs.Clusters) []performance.EntityMetric {
92 t := time.Now()
93 pqs := newClustersPerfQuerySpecs(clusters)
94 ms := s.scrapeMetrics(pqs)
95 s.Debugf("scraping : scraped metrics for %d/%d clusters, process took %s",
96 len(ms),
97 len(clusters),
98 time.Since(t),
99 )
100 return ms
101 }
102
103 func (s *Scraper) scrapeMetrics(pqs []types.PerfQuerySpec) []performance.EntityMetric {
104 tc := newThrottledCaller(5)
105 var ms []performance.EntityMetric
106 lock := &sync.Mutex{}
107
108 chunks := chunkify(pqs, s.maxQuery)
109 for _, chunk := range chunks {
110 pqs := chunk
111 job := func() {
112 s.scrape(&ms, lock, pqs)
113 }
114 tc.call(job)
115 }
116 tc.wait()
117
118 return ms
119 }
120
121 func (s *Scraper) scrape(metrics *[]performance.EntityMetric, lock *sync.Mutex, pqs []types.PerfQuerySpec) {
122 m, err := s.PerformanceMetrics(pqs)
123 if err != nil {
124 s.Limit(logKeyPerfQueryError+perfQuerySpecEntityType(pqs), 1, recurringLogEvery).
125 Errorf("scrape vSphere performance metrics: query_specs=%d entities=[%s]: %v", len(pqs), describePerfQuerySpecs(pqs), err)
126 return
127 }
128
129 lock.Lock()
130 *metrics = append(*metrics, m...)
131 lock.Unlock()
132 }
133
134 func chunkify(pqs []types.PerfQuerySpec, chunkSize int) (chunks [][]types.PerfQuerySpec) {
135 for i := 0; i < len(pqs); i += chunkSize {
136 end := min(i+chunkSize, len(pqs))
137 chunks = append(chunks, pqs[i:end])
138 }
139 return chunks
140 }
141
142 func describePerfQuerySpecs(pqs []types.PerfQuerySpec) string {
143 if len(pqs) == 0 {
144 return "none"
145 }
146
147 const limit = 5
148 refs := make([]string, 0, min(len(pqs), limit))
149 for _, pq := range pqs[:min(len(pqs), limit)] {
150 refs = append(refs, fmt.Sprintf("%s/%s", pq.Entity.Type, pq.Entity.Value))
151 }
152 if len(pqs) > limit {
153 refs = append(refs, fmt.Sprintf("+%d more", len(pqs)-limit))
154 }
155
156 return strings.Join(refs, ",")
157 }
158
159 func perfQuerySpecEntityType(pqs []types.PerfQuerySpec) string {
160 if len(pqs) == 0 {
161 return "none"
162 }
163 return pqs[0].Entity.Type
164 }
165
166 const (
167 pqsMaxSample = 1
168 pqsIntervalID = 20
169 pqsFormat = "normal"
170
171 recurringLogEvery = time.Hour
172 logKeyPerfQueryError = "vsphere:perf-query-error:"
173 )
174
175 func newHostsPerfQuerySpecs(hosts rs.Hosts) []types.PerfQuerySpec {
176 pqs := make([]types.PerfQuerySpec, 0, len(hosts))
177 for _, host := range hosts {
178 if !host.IsPoweredOn() || len(host.MetricList) == 0 {
179 continue
180 }
181 pq := types.PerfQuerySpec{
182 Entity: host.Ref,
183 MaxSample: pqsMaxSample,
184 MetricId: host.MetricList,
185 IntervalId: pqsIntervalID,
186 Format: pqsFormat,
187 }
188 pqs = append(pqs, pq)
189 }
190 return pqs
191 }
192
193 func newVMsPerfQuerySpecs(vms rs.VMs) []types.PerfQuerySpec {
194 pqs := make([]types.PerfQuerySpec, 0, len(vms))
195 for _, vm := range vms {
196 if !vm.IsPoweredOn() || len(vm.MetricList) == 0 {
197 continue
198 }
199 pq := types.PerfQuerySpec{
200 Entity: vm.Ref,
201 MaxSample: pqsMaxSample,
202 MetricId: vm.MetricList,
203 IntervalId: pqsIntervalID,
204 Format: pqsFormat,
205 }
206 pqs = append(pqs, pq)
207 }
208 return pqs
209 }
210
211 // Datastores, clusters, and resource pools do not support real-time (20s) collection.
212 // Minimum supported interval is 300s (5 min historical).
213 const pqsHistoricalIntervalID = 300
214
215 func newDatastoresPerfQuerySpecs(datastores rs.Datastores) []types.PerfQuerySpec {
216 pqs := make([]types.PerfQuerySpec, 0, len(datastores))
217 for _, ds := range datastores {
218 if !ds.Accessible || len(ds.MetricList) == 0 {
219 continue
220 }
221 pq := types.PerfQuerySpec{
222 Entity: ds.Ref,
223 MaxSample: pqsMaxSample,
224 MetricId: ds.MetricList,
225 IntervalId: pqsHistoricalIntervalID,
226 Format: pqsFormat,
227 }
228 pqs = append(pqs, pq)
229 }
230 return pqs
231 }
232
233 func newClustersPerfQuerySpecs(clusters rs.Clusters) []types.PerfQuerySpec {
234 pqs := make([]types.PerfQuerySpec, 0, len(clusters))
235 for _, c := range clusters {
236 if len(c.MetricList) == 0 {
237 continue
238 }
239 pq := types.PerfQuerySpec{
240 Entity: c.Ref,
241 MaxSample: pqsMaxSample,
242 MetricId: c.MetricList,
243 IntervalId: pqsHistoricalIntervalID,
244 Format: pqsFormat,
245 }
246 pqs = append(pqs, pq)
247 }
248 return pqs
249 }
250
251 func parseVersion(version string) (major, minor int, err error) {
252 parts := strings.Split(version, ".")
253 if len(parts) < 2 {
254 return 0, 0, fmt.Errorf("parse vSphere API version %q: expected <major>.<minor>", version)
255 }
256 if major, err = strconv.Atoi(parts[0]); err != nil {
257 return 0, 0, fmt.Errorf("parse vSphere API version major component %q from %q: %w", parts[0], version, err)
258 }
259 if minor, err = strconv.Atoi(parts[1]); err != nil {
260 return 0, 0, fmt.Errorf("parse vSphere API version minor component %q from %q: %w", parts[1], version, err)
261 }
262 return major, minor, nil
263 }