| 1 | //go:build cgo |
| 2 | |
| 3 | package mp |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "math" |
| 9 | "strings" |
| 10 | "sync" |
| 11 | |
| 12 | "github.com/netdata/netdata/go/plugins/pkg/matcher" |
| 13 | "github.com/netdata/netdata/go/plugins/pkg/prometheus" |
| 14 | "github.com/netdata/netdata/go/plugins/plugin/ibm.d/framework" |
| 15 | "github.com/netdata/netdata/go/plugins/plugin/ibm.d/modules/websphere/common" |
| 16 | "github.com/netdata/netdata/go/plugins/plugin/ibm.d/modules/websphere/mp/contexts" |
| 17 | "github.com/netdata/netdata/go/plugins/plugin/ibm.d/protocols/openmetrics" |
| 18 | ) |
| 19 | |
| 20 | // Collector implements the WebSphere MicroProfile module using the ibm.d framework. |
| 21 | type Collector struct { |
| 22 | framework.Collector |
| 23 | |
| 24 | Config `yaml:",inline" json:",inline"` |
| 25 | |
| 26 | once sync.Once |
| 27 | |
| 28 | client *openmetrics.Client |
| 29 | |
| 30 | identity common.Identity |
| 31 | |
| 32 | restSelector matcher.Matcher |
| 33 | |
| 34 | mpMetricsVersion string |
| 35 | serverType string |
| 36 | } |
| 37 | |
| 38 | func (c *Collector) initOnce() { |
| 39 | c.once.Do(func() { |
| 40 | }) |
| 41 | } |
| 42 | |
| 43 | // CollectOnce performs a single scrape. |
| 44 | func (c *Collector) CollectOnce() error { |
| 45 | c.initOnce() |
| 46 | if c.client == nil { |
| 47 | return errors.New("openmetrics client not initialised") |
| 48 | } |
| 49 | |
| 50 | timeout := c.Config.ClientConfig.Timeout.Duration() |
| 51 | if timeout <= 0 { |
| 52 | timeout = defaultTimeout |
| 53 | } |
| 54 | |
| 55 | ctx, cancel := context.WithTimeout(context.Background(), timeout) |
| 56 | defer cancel() |
| 57 | |
| 58 | series, err := c.client.FetchSeries(ctx, nil) |
| 59 | if err != nil { |
| 60 | return err |
| 61 | } |
| 62 | |
| 63 | agg := make(map[string]int64) |
| 64 | restData := make(map[restKey]*restMetrics) |
| 65 | |
| 66 | c.mpMetricsVersion = detectMetricsVersion(series) |
| 67 | if c.serverType == "" { |
| 68 | c.serverType = "Liberty MicroProfile" |
| 69 | } |
| 70 | |
| 71 | for _, sample := range series { |
| 72 | name := sample.Labels.Get("__name__") |
| 73 | if name == "" { |
| 74 | continue |
| 75 | } |
| 76 | |
| 77 | scope := sample.Labels.Get("mp_scope") |
| 78 | normalized := normalizeMetricName(name, scope) |
| 79 | |
| 80 | method := sample.Labels.Get("method") |
| 81 | endpoint := sample.Labels.Get("endpoint") |
| 82 | |
| 83 | if method != "" && endpoint != "" { |
| 84 | if !c.CollectRESTMetrics.IsEnabled() { |
| 85 | continue |
| 86 | } |
| 87 | c.collectRESTMetric(restData, method, endpoint, normalized, sample.Value) |
| 88 | continue |
| 89 | } |
| 90 | |
| 91 | if !c.CollectJVMMetrics.IsEnabled() && isCoreMetric(normalized) { |
| 92 | continue |
| 93 | } |
| 94 | |
| 95 | c.collectCoreMetric(agg, normalized, sample.Value) |
| 96 | } |
| 97 | |
| 98 | c.exportCoreMetrics(agg) |
| 99 | c.exportRESTMetrics(restData) |
| 100 | c.Debugf("exported core metrics (keys=%d) rest endpoints=%d", len(agg), len(restData)) |
| 101 | |
| 102 | labels := c.identity.Labels() |
| 103 | if c.mpMetricsVersion != "" && c.mpMetricsVersion != "unknown" { |
| 104 | labels["mp_metrics_version"] = c.mpMetricsVersion |
| 105 | } |
| 106 | if c.serverType != "" { |
| 107 | labels["server_type"] = c.serverType |
| 108 | } |
| 109 | c.SetGlobalLabels(labels) |
| 110 | |
| 111 | return nil |
| 112 | } |
| 113 | |
| 114 | var _ framework.CollectorImpl = (*Collector)(nil) |
| 115 | |
| 116 | type restKey struct { |
| 117 | Method string |
| 118 | Endpoint string |
| 119 | } |
| 120 | |
| 121 | type restMetrics struct { |
| 122 | Requests int64 |
| 123 | ResponseTime int64 |
| 124 | hasRequests bool |
| 125 | hasResponse bool |
| 126 | } |
| 127 | |
| 128 | func normalizeMetricName(name, scope string) string { |
| 129 | if scope != "" && !strings.HasPrefix(name, scope+"_") { |
| 130 | return scope + "_" + name |
| 131 | } |
| 132 | return name |
| 133 | } |
| 134 | |
| 135 | func detectMetricsVersion(series prometheus.Series) string { |
| 136 | hasLabelScope := false |
| 137 | hasPrefix := false |
| 138 | hasVendor := false |
| 139 | |
| 140 | for _, s := range series { |
| 141 | name := s.Labels.Get("__name__") |
| 142 | scope := s.Labels.Get("mp_scope") |
| 143 | if scope != "" { |
| 144 | hasLabelScope = true |
| 145 | if scope == "vendor" { |
| 146 | hasVendor = true |
| 147 | } |
| 148 | } |
| 149 | if strings.HasPrefix(name, "base_") || strings.HasPrefix(name, "vendor_") { |
| 150 | hasPrefix = true |
| 151 | } |
| 152 | if strings.HasPrefix(name, "vendor_") { |
| 153 | hasVendor = true |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | if hasLabelScope { |
| 158 | return "5.1" |
| 159 | } |
| 160 | if hasPrefix { |
| 161 | if hasVendor { |
| 162 | return "3.0" |
| 163 | } |
| 164 | return "4.0" |
| 165 | } |
| 166 | return "unknown" |
| 167 | } |
| 168 | |
| 169 | func (c *Collector) collectCoreMetric(agg map[string]int64, name string, value float64) { |
| 170 | switch name { |
| 171 | case "base_memory_usedHeap_bytes": |
| 172 | agg["heap_used"] = int64(value) |
| 173 | case "base_memory_committedHeap_bytes": |
| 174 | agg["heap_committed"] = int64(value) |
| 175 | case "base_memory_maxHeap_bytes": |
| 176 | agg["heap_max"] = int64(value) |
| 177 | case "vendor_memory_heapUtilization_percent": |
| 178 | agg["heap_util"] = common.FormatPercent(value) |
| 179 | case "base_gc_total": |
| 180 | agg["gc_total"] = int64(value) |
| 181 | case "base_gc_time_seconds": |
| 182 | agg["gc_time_ms"] = secondsToMillis(value) |
| 183 | case "vendor_gc_time_per_cycle_seconds": |
| 184 | agg["gc_time_cycle_ms"] = secondsToMillis(value) |
| 185 | case "base_thread_count": |
| 186 | agg["thread_total"] = int64(value) |
| 187 | case "base_thread_daemon_count": |
| 188 | agg["thread_daemon"] = int64(value) |
| 189 | case "base_thread_max_count": |
| 190 | agg["thread_peak"] = int64(value) |
| 191 | case "base_cpu_processCpuLoad_percent": |
| 192 | agg["cpu_process"] = common.FormatPercent(value) |
| 193 | case "vendor_cpu_processCpuUtilization_percent": |
| 194 | agg["cpu_util"] = common.FormatPercent(value) |
| 195 | case "base_cpu_processCpuTime_seconds": |
| 196 | agg["cpu_time_ms"] = secondsToMillis(value) |
| 197 | case "threadpool_activeThreads": |
| 198 | agg["threadpool_active"] = int64(value) |
| 199 | case "threadpool_size": |
| 200 | agg["threadpool_size"] = int64(value) |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | func secondsToMillis(v float64) int64 { |
| 205 | return int64(math.Round(v * 1000)) |
| 206 | } |
| 207 | |
| 208 | func (c *Collector) collectRESTMetric(rest map[restKey]*restMetrics, method, endpoint, name string, value float64) { |
| 209 | target := method + " " + endpoint |
| 210 | if c.restSelector != nil && !c.restSelector.MatchString(target) { |
| 211 | return |
| 212 | } |
| 213 | |
| 214 | key := restKey{Method: method, Endpoint: endpoint} |
| 215 | metrics := rest[key] |
| 216 | if metrics == nil { |
| 217 | if c.MaxRESTEndpoints > 0 && len(rest) >= c.MaxRESTEndpoints { |
| 218 | return |
| 219 | } |
| 220 | metrics = &restMetrics{} |
| 221 | rest[key] = metrics |
| 222 | } |
| 223 | |
| 224 | lower := strings.ToLower(name) |
| 225 | switch { |
| 226 | case strings.Contains(lower, "request") && (strings.Contains(lower, "total") || strings.Contains(lower, "count")): |
| 227 | metrics.Requests = int64(value) |
| 228 | metrics.hasRequests = true |
| 229 | case strings.Contains(lower, "time") || strings.Contains(lower, "duration"): |
| 230 | metrics.ResponseTime = secondsToMillis(value) |
| 231 | metrics.hasResponse = true |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | func (c *Collector) exportCoreMetrics(agg map[string]int64) { |
| 236 | labels := contexts.EmptyLabels{} |
| 237 | |
| 238 | used := agg["heap_used"] |
| 239 | committed := agg["heap_committed"] |
| 240 | free := max(committed-used, 0) |
| 241 | contexts.JVM.HeapUsage.Set(c.State, labels, contexts.JVMHeapUsageValues{ |
| 242 | Used: used, |
| 243 | Free: free, |
| 244 | }) |
| 245 | contexts.JVM.HeapCommitted.Set(c.State, labels, contexts.JVMHeapCommittedValues{Committed: committed}) |
| 246 | contexts.JVM.HeapMax.Set(c.State, labels, contexts.JVMHeapMaxValues{Limit: agg["heap_max"]}) |
| 247 | contexts.JVM.HeapUtilization.Set(c.State, labels, contexts.JVMHeapUtilizationValues{Utilization: agg["heap_util"]}) |
| 248 | |
| 249 | contexts.JVM.GCCollections.Set(c.State, labels, contexts.JVMGCCollectionsValues{Rate: agg["gc_total"]}) |
| 250 | perCycle := agg["gc_time_cycle_ms"] |
| 251 | contexts.JVM.GCTime.Set(c.State, labels, contexts.JVMGCTimeValues{ |
| 252 | Total: agg["gc_time_ms"], |
| 253 | Per_cycle: perCycle, |
| 254 | }) |
| 255 | |
| 256 | totalThreads := agg["thread_total"] |
| 257 | daemon := agg["thread_daemon"] |
| 258 | other := max(totalThreads-daemon, 0) |
| 259 | contexts.JVM.ThreadsCurrent.Set(c.State, labels, contexts.JVMThreadsCurrentValues{ |
| 260 | Daemon: daemon, |
| 261 | Other: other, |
| 262 | }) |
| 263 | contexts.JVM.ThreadsPeak.Set(c.State, labels, contexts.JVMThreadsPeakValues{Peak: agg["thread_peak"]}) |
| 264 | |
| 265 | contexts.CPU.Usage.Set(c.State, contexts.EmptyLabels{}, contexts.CPUUsageValues{ |
| 266 | Process: agg["cpu_process"], |
| 267 | Utilization: agg["cpu_util"], |
| 268 | }) |
| 269 | contexts.CPU.Time.Set(c.State, contexts.EmptyLabels{}, contexts.CPUTimeValues{Total: agg["cpu_time_ms"]}) |
| 270 | |
| 271 | active := agg["threadpool_active"] |
| 272 | size := agg["threadpool_size"] |
| 273 | idle := max(size-active, 0) |
| 274 | contexts.Vendor.ThreadPoolUsage.Set(c.State, labels, contexts.VendorThreadPoolUsageValues{ |
| 275 | Active: active, |
| 276 | Idle: idle, |
| 277 | }) |
| 278 | contexts.Vendor.ThreadPoolSize.Set(c.State, labels, contexts.VendorThreadPoolSizeValues{Size: size}) |
| 279 | } |
| 280 | |
| 281 | func (c *Collector) exportRESTMetrics(rest map[restKey]*restMetrics) { |
| 282 | for key, metrics := range rest { |
| 283 | labels := contexts.RESTEndpointLabels{Method: key.Method, Endpoint: key.Endpoint} |
| 284 | if metrics.hasRequests { |
| 285 | contexts.RESTEndpoint.Requests.Set(c.State, labels, contexts.RESTEndpointRequestsValues{Requests: metrics.Requests}) |
| 286 | } |
| 287 | if metrics.hasResponse { |
| 288 | contexts.RESTEndpoint.ResponseTime.Set(c.State, labels, contexts.RESTEndpointResponseTimeValues{Average: metrics.ResponseTime}) |
| 289 | } |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | func isCoreMetric(name string) bool { |
| 294 | if strings.HasPrefix(name, "base_") || strings.HasPrefix(name, "vendor_") { |
| 295 | return true |
| 296 | } |
| 297 | if strings.HasPrefix(name, "threadpool_") { |
| 298 | return true |
| 299 | } |
| 300 | return false |
| 301 | } |