master
go 339 lines 9.58 KB
Raw
1 // Package pmi provides a reusable PerfServlet (PMI) client used by WebSphere collectors.
2 // SPDX-License-Identifier: GPL-3.0-or-later
3
4 package pmi
5
6 import (
7 "context"
8 "encoding/xml"
9 "errors"
10 "fmt"
11 "io"
12 "net/http"
13 "net/url"
14 "strings"
15 "time"
16
17 "github.com/netdata/netdata/go/plugins/pkg/confopt"
18 "github.com/netdata/netdata/go/plugins/pkg/web"
19 )
20
21 // Config captures the options required to communicate with the PerfServlet endpoint.
22 type Config struct {
23 // URL is the full PerfServlet endpoint (e.g. https://host:9443/wasPerfTool/servlet/perfservlet).
24 URL string
25
26 // StatsType controls the PMI statistics level (basic, extended, all, custom).
27 StatsType string
28
29 // HTTPConfig mirrors go.d's HTTP configuration for TLS/auth proxy handling.
30 HTTPConfig web.HTTPConfig
31 }
32
33 // Client handles HTTP transport and XML decoding for PMI snapshots.
34 type Client struct {
35 cfg Config
36 httpClient *http.Client
37 baseURL string
38 }
39
40 // NewClient validates the configuration and prepares an HTTP client.
41 func NewClient(cfg Config) (*Client, error) {
42 if time.Duration(cfg.HTTPConfig.ClientConfig.Timeout) <= 0 {
43 cfg.HTTPConfig.ClientConfig.Timeout = confopt.Duration(5 * time.Second)
44 }
45
46 httpClient, err := web.NewHTTPClient(cfg.HTTPConfig.ClientConfig)
47 if err != nil {
48 return nil, fmt.Errorf("pmi protocol: creating http client failed: %w", err)
49 }
50
51 return NewClientWithHTTP(cfg, httpClient)
52 }
53
54 // NewClientWithHTTP allows supplying a custom *http.Client, primarily for tests.
55 func NewClientWithHTTP(cfg Config, httpClient *http.Client) (*Client, error) {
56 if httpClient == nil {
57 return nil, errors.New("pmi protocol: http client is required")
58 }
59
60 normalizedCfg, baseURL, err := normalizeConfig(cfg)
61 if err != nil {
62 return nil, err
63 }
64
65 return &Client{
66 cfg: normalizedCfg,
67 httpClient: httpClient,
68 baseURL: baseURL,
69 }, nil
70 }
71
72 func normalizeConfig(cfg Config) (Config, string, error) {
73 trimmedURL := strings.TrimSpace(cfg.URL)
74 if trimmedURL == "" {
75 return cfg, "", errors.New("pmi protocol: url is required")
76 }
77
78 parsedURL, err := url.Parse(trimmedURL)
79 if err != nil {
80 return cfg, "", fmt.Errorf("pmi protocol: invalid url: %w", err)
81 }
82 if parsedURL.Path == "" || parsedURL.Path == "/" {
83 return cfg, "", errors.New("pmi protocol: url must include the PerfServlet path (e.g., /wasPerfTool/servlet/perfservlet)")
84 }
85
86 statsType := strings.ToLower(strings.TrimSpace(cfg.StatsType))
87 switch statsType {
88 case "", "extended":
89 statsType = "extended"
90 case "basic", "all", "custom":
91 // accepted as-is
92 default:
93 statsType = "extended"
94 }
95 cfg.StatsType = statsType
96
97 return cfg, parsedURL.String(), nil
98 }
99
100 // Close releases resources held by the underlying HTTP client.
101 func (c *Client) Close() {
102 if c.httpClient != nil {
103 c.httpClient.CloseIdleConnections()
104 }
105 }
106
107 // Fetch retrieves a PMI snapshot and normalises the data tree.
108 func (c *Client) Fetch(ctx context.Context) (*Snapshot, error) {
109 if c.httpClient == nil {
110 return nil, errors.New("pmi protocol: client not initialised")
111 }
112
113 reqURL := c.baseURL + "?stats=" + url.QueryEscape(c.cfg.StatsType)
114
115 req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
116 if err != nil {
117 return nil, fmt.Errorf("pmi protocol: creating request failed: %w", err)
118 }
119
120 if c.cfg.HTTPConfig.Username != "" || c.cfg.HTTPConfig.Password != "" {
121 req.SetBasicAuth(c.cfg.HTTPConfig.Username, c.cfg.HTTPConfig.Password)
122 }
123
124 resp, err := c.httpClient.Do(req)
125 if err != nil {
126 if ctxErr := ctx.Err(); ctxErr != nil {
127 return nil, fmt.Errorf("pmi protocol: request cancelled: %w", ctxErr)
128 }
129 return nil, fmt.Errorf("pmi protocol: request failed: %w", err)
130 }
131 defer resp.Body.Close()
132
133 if resp.StatusCode != http.StatusOK {
134 body, _ := io.ReadAll(resp.Body)
135 return nil, fmt.Errorf("pmi protocol: unexpected status %d: %s", resp.StatusCode, string(body))
136 }
137
138 decoder := xml.NewDecoder(resp.Body)
139
140 type decodeResult struct {
141 snapshot *Snapshot
142 err error
143 }
144 resultCh := make(chan decodeResult, 1)
145
146 go func() {
147 var snapshot Snapshot
148 err := decoder.Decode(&snapshot)
149 resultCh <- decodeResult{snapshot: &snapshot, err: err}
150 }()
151
152 select {
153 case <-ctx.Done():
154 return nil, fmt.Errorf("pmi protocol: decoding cancelled: %w", ctx.Err())
155 case res := <-resultCh:
156 if res.err != nil {
157 return nil, fmt.Errorf("pmi protocol: decoding failed: %w", res.err)
158 }
159 res.snapshot.Normalize()
160 return res.snapshot, nil
161 }
162 }
163
164 // Snapshot represents a full PMI response tree.
165 type Snapshot struct {
166 XMLName xml.Name `xml:"PerformanceMonitor"`
167 ResponseStatus string `xml:"responseStatus,attr"`
168 Version string `xml:"version,attr"`
169 Nodes []Node `xml:"Node"`
170 Stats []Stat `xml:"Stat"`
171 }
172
173 // Normalize ensures backward-compatible pointers and propagates paths within the tree.
174 func (s *Snapshot) Normalize() {
175 for i := range s.Nodes {
176 s.Nodes[i].normalize()
177 }
178 for i := range s.Stats {
179 s.Stats[i].normalize("", s.Stats[i].Name)
180 }
181 }
182
183 // Node represents a PMI node element.
184 type Node struct {
185 XMLName xml.Name `xml:"Node"`
186 Name string `xml:"name,attr"`
187 Servers []Server `xml:"Server"`
188 }
189
190 func (n *Node) normalize() {
191 for i := range n.Servers {
192 n.Servers[i].normalize(n.Name)
193 }
194 }
195
196 // Server gathers stats for a specific WebSphere server instance.
197 type Server struct {
198 XMLName xml.Name `xml:"Server"`
199 Name string `xml:"name,attr"`
200 Stats []Stat `xml:"Stat"`
201 }
202
203 func (s *Server) normalize(nodeName string) {
204 base := s.Name
205 if nodeName != "" {
206 base = nodeName + "/" + base
207 }
208 for i := range s.Stats {
209 s.Stats[i].normalize(base, s.Stats[i].Name)
210 }
211 }
212
213 // Stat represents an individual PMI statistic entry.
214 type Stat struct {
215 XMLName xml.Name `xml:"Stat"`
216 Name string `xml:"name,attr"`
217 Type string `xml:"type,attr"`
218 ID string `xml:"id,attr"`
219 Path string `xml:"path,attr"`
220 Value *Value `xml:"Value"`
221 CountStatistics []CountStatistic `xml:"CountStatistic"`
222 TimeStatistics []TimeStatistic `xml:"TimeStatistic"`
223 RangeStatistics []RangeStatistic `xml:"RangeStatistic"`
224 BoundedRangeStatistics []BoundedRangeStat `xml:"BoundedRangeStatistic"`
225 DoubleStatistics []DoubleStatistic `xml:"DoubleStatistic"`
226 AverageStatistics []AverageStatistic `xml:"AverageStatistic"`
227 SubStats []Stat `xml:"Stat"`
228
229 CountStatistic *CountStatistic `xml:"-"`
230 TimeStatistic *TimeStatistic `xml:"-"`
231 RangeStatistic *RangeStatistic `xml:"-"`
232 BoundedRangeStatistic *BoundedRangeStat `xml:"-"`
233 DoubleStatistic *DoubleStatistic `xml:"-"`
234 AverageStatistic *AverageStatistic `xml:"-"`
235 }
236
237 func (s *Stat) normalize(parentPath, fallbackName string) {
238 if len(s.CountStatistics) > 0 {
239 s.CountStatistic = &s.CountStatistics[0]
240 }
241 if len(s.TimeStatistics) > 0 {
242 s.TimeStatistic = &s.TimeStatistics[0]
243 }
244 if len(s.RangeStatistics) > 0 {
245 s.RangeStatistic = &s.RangeStatistics[0]
246 }
247 if len(s.BoundedRangeStatistics) > 0 {
248 s.BoundedRangeStatistic = &s.BoundedRangeStatistics[0]
249 }
250 if len(s.DoubleStatistics) > 0 {
251 s.DoubleStatistic = &s.DoubleStatistics[0]
252 }
253 if len(s.AverageStatistics) > 0 {
254 s.AverageStatistic = &s.AverageStatistics[0]
255 }
256
257 name := s.Name
258 if name == "" {
259 name = fallbackName
260 }
261 if parentPath == "" {
262 s.Path = name
263 } else if name != "" {
264 s.Path = parentPath + "/" + name
265 } else {
266 s.Path = parentPath
267 }
268
269 for i := range s.SubStats {
270 s.SubStats[i].normalize(s.Path, s.SubStats[i].Name)
271 }
272 }
273
274 // Value captures the textual value for simple statistics.
275 type Value struct {
276 Value string `xml:",chardata"`
277 }
278
279 // CountStatistic represents PMI count metrics.
280 type CountStatistic struct {
281 Name string `xml:"name,attr"`
282 Count string `xml:"count,attr"`
283 Unit string `xml:"unit,attr"`
284 }
285
286 // TimeStatistic captures PMI time-based measurements.
287 type TimeStatistic struct {
288 Name string `xml:"name,attr"`
289 Count string `xml:"count,attr"`
290 Total string `xml:"total,attr"`
291 TotalTime string `xml:"totalTime,attr"`
292 Mean string `xml:"mean,attr"`
293 Min string `xml:"min,attr"`
294 Max string `xml:"max,attr"`
295 Unit string `xml:"unit,attr"`
296 }
297
298 // RangeStatistic describes PMI range values.
299 type RangeStatistic struct {
300 Name string `xml:"name,attr"`
301 Current string `xml:"value,attr"`
302 Integral string `xml:"integral,attr"`
303 Mean string `xml:"mean,attr"`
304 HighWaterMark string `xml:"highWaterMark,attr"`
305 LowWaterMark string `xml:"lowWaterMark,attr"`
306 Unit string `xml:"unit,attr"`
307 }
308
309 // BoundedRangeStat includes range metrics with explicit bounds.
310 type BoundedRangeStat struct {
311 Name string `xml:"name,attr"`
312 Current string `xml:"value,attr"`
313 Integral string `xml:"integral,attr"`
314 Mean string `xml:"mean,attr"`
315 LowerBound string `xml:"lowerBound,attr"`
316 UpperBound string `xml:"upperBound,attr"`
317 HighWaterMark string `xml:"highWaterMark,attr"`
318 LowWaterMark string `xml:"lowWaterMark,attr"`
319 Unit string `xml:"unit,attr"`
320 }
321
322 // DoubleStatistic captures PMI floating point metrics.
323 type DoubleStatistic struct {
324 Name string `xml:"name,attr"`
325 Double string `xml:"double,attr"`
326 Unit string `xml:"unit,attr"`
327 }
328
329 // AverageStatistic stores PMI average-based metrics.
330 type AverageStatistic struct {
331 Name string `xml:"name,attr"`
332 Count string `xml:"count,attr"`
333 Total string `xml:"total,attr"`
334 Mean string `xml:"mean,attr"`
335 Min string `xml:"min,attr"`
336 Max string `xml:"max,attr"`
337 SumOfSquares string `xml:"sumOfSquares,attr"`
338 Unit string `xml:"unit,attr"`
339 }