// SPDX-License-Identifier: GPL-3.0-or-later package apache import ( "bufio" "fmt" "io" "net/http" "strconv" "strings" "github.com/netdata/netdata/go/go.d.plugin/pkg/stm" "github.com/netdata/netdata/go/go.d.plugin/pkg/web" ) func (a *Apache) collect() (map[string]int64, error) { status, err := a.scrapeStatus() if err != nil { return nil, err } mx := stm.ToMap(status) if len(mx) == 0 { return nil, fmt.Errorf("nothing was collected from %s", a.URL) } a.once.Do(func() { a.charts = newCharts(status) }) return mx, nil } func (a *Apache) scrapeStatus() (*serverStatus, error) { req, err := web.NewHTTPRequest(a.Request) if err != nil { return nil, err } resp, err := a.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error on HTTP request '%s': %v", req.URL, err) } defer closeBody(resp) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode) } return parseResponse(resp.Body) } func parseResponse(r io.Reader) (*serverStatus, error) { s := bufio.NewScanner(r) var status serverStatus for s.Scan() { parts := strings.Split(s.Text(), ":") if len(parts) != 2 { continue } key, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) switch key { default: case "BusyServers", "IdleServers": return nil, fmt.Errorf("found '%s', Lighttpd data", key) case "BusyWorkers": status.Workers.Busy = parseInt(value) case "IdleWorkers": status.Workers.Idle = parseInt(value) case "ConnsTotal": status.Connections.Total = parseInt(value) case "ConnsAsyncWriting": status.Connections.Async.Writing = parseInt(value) case "ConnsAsyncKeepAlive": status.Connections.Async.KeepAlive = parseInt(value) case "ConnsAsyncClosing": status.Connections.Async.Closing = parseInt(value) case "Total Accesses": status.Total.Accesses = parseInt(value) case "Total kBytes": status.Total.KBytes = parseInt(value) case "Uptime": status.Uptime = parseInt(value) case "ReqPerSec": status.Averages.ReqPerSec = parseFloat(value) case "BytesPerSec": status.Averages.BytesPerSec = parseFloat(value) case "BytesPerReq": status.Averages.BytesPerReq = parseFloat(value) case "Scoreboard": status.Scoreboard = parseScoreboard(value) } } return &status, nil } func parseScoreboard(line string) *scoreboard { // “_” Waiting for Connection // “S” Starting up // “R” Reading Request // “W” Sending Reply // “K” Keepalive (read) // “D” DNS Lookup // “C” Closing connection // “L” Logging // “G” Gracefully finishing // “I” Idle cleanup of worker // “.” Open slot with no current process var sb scoreboard for _, s := range strings.Split(line, "") { switch s { case "_": sb.Waiting++ case "S": sb.Starting++ case "R": sb.Reading++ case "W": sb.Sending++ case "K": sb.KeepAlive++ case "D": sb.DNSLookup++ case "C": sb.Closing++ case "L": sb.Logging++ case "G": sb.Finishing++ case "I": sb.IdleCleanup++ case ".": sb.Open++ } } return &sb } func parseInt(value string) *int64 { v, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil } return &v } func parseFloat(value string) *float64 { v, err := strconv.ParseFloat(value, 64) if err != nil { return nil } return &v } func closeBody(resp *http.Response) { if resp != nil && resp.Body != nil { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() } }