package haproxy import ( "encoding/csv" "fmt" "io" "net" "net/http" "net/url" "path/filepath" "strconv" "strings" "sync" "time" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/internal/errchan" "github.com/influxdata/telegraf/plugins/inputs" ) //CSV format: https://cbonte.github.io/haproxy-dconv/1.5/configuration.html#9.1 const ( HF_PXNAME = 0 // 0. pxname [LFBS]: proxy name HF_SVNAME = 1 // 1. svname [LFBS]: service name (FRONTEND for frontend, BACKEND for backend, any name for server/listener) HF_QCUR = 2 //2. qcur [..BS]: current queued requests. For the backend this reports the number queued without a server assigned. HF_QMAX = 3 //3. qmax [..BS]: max value of qcur HF_SCUR = 4 // 4. scur [LFBS]: current sessions HF_SMAX = 5 //5. smax [LFBS]: max sessions HF_SLIM = 6 //6. slim [LFBS]: configured session limit HF_STOT = 7 //7. stot [LFBS]: cumulative number of connections HF_BIN = 8 //8. bin [LFBS]: bytes in HF_BOUT = 9 //9. bout [LFBS]: bytes out HF_DREQ = 10 //10. dreq [LFB.]: requests denied because of security concerns. HF_DRESP = 11 //11. dresp [LFBS]: responses denied because of security concerns. HF_EREQ = 12 //12. ereq [LF..]: request errors. Some of the possible causes are: HF_ECON = 13 //13. econ [..BS]: number of requests that encountered an error trying to HF_ERESP = 14 //14. eresp [..BS]: response errors. srv_abrt will be counted here also. Some other errors are: - write error on the client socket (won't be counted for the server stat) - failure applying filters to the response. HF_WRETR = 15 //15. wretr [..BS]: number of times a connection to a server was retried. HF_WREDIS = 16 //16. wredis [..BS]: number of times a request was redispatched to another server. The server value counts the number of times that server was switched away from. HF_STATUS = 17 //17. status [LFBS]: status (UP/DOWN/NOLB/MAINT/MAINT(via)...) HF_WEIGHT = 18 //18. weight [..BS]: total weight (backend), server weight (server) HF_ACT = 19 //19. act [..BS]: number of active servers (backend), server is active (server) HF_BCK = 20 //20. bck [..BS]: number of backup servers (backend), server is backup (server) HF_CHKFAIL = 21 //21. chkfail [...S]: number of failed checks. (Only counts checks failed when the server is up.) HF_CHKDOWN = 22 //22. chkdown [..BS]: number of UP->DOWN transitions. The backend counter counts transitions to the whole backend being down, rather than the sum of the counters for each server. HF_LASTCHG = 23 //23. lastchg [..BS]: number of seconds since the last UP<->DOWN transition HF_DOWNTIME = 24 //24. downtime [..BS]: total downtime (in seconds). The value for the backend is the downtime for the whole backend, not the sum of the server downtime. HF_QLIMIT = 25 //25. qlimit [...S]: configured maxqueue for the server, or nothing in the value is 0 (default, meaning no limit) HF_PID = 26 //26. pid [LFBS]: process id (0 for first instance, 1 for second, ...) HF_IID = 27 //27. iid [LFBS]: unique proxy id HF_SID = 28 //28. sid [L..S]: server id (unique inside a proxy) HF_THROTTLE = 29 //29. throttle [...S]: current throttle percentage for the server, when slowstart is active, or no value if not in slowstart. HF_LBTOT = 30 //30. lbtot [..BS]: total number of times a server was selected, either for new sessions, or when re-dispatching. The server counter is the number of times that server was selected. HF_TRACKED = 31 //31. tracked [...S]: id of proxy/server if tracking is enabled. HF_TYPE = 32 //32. type [LFBS]: (0 = frontend, 1 = backend, 2 = server, 3 = socket/listener) HF_RATE = 33 //33. rate [.FBS]: number of sessions per second over last elapsed second HF_RATE_LIM = 34 //34. rate_lim [.F..]: configured limit on new sessions per second HF_RATE_MAX = 35 //35. rate_max [.FBS]: max number of new sessions per second HF_CHECK_STATUS = 36 //36. check_status [...S]: status of last health check, one of: HF_CHECK_CODE = 37 //37. check_code [...S]: layer5-7 code, if available HF_CHECK_DURATION = 38 //38. check_duration [...S]: time in ms took to finish last health check HF_HRSP_1xx = 39 //39. hrsp_1xx [.FBS]: http responses with 1xx code HF_HRSP_2xx = 40 //40. hrsp_2xx [.FBS]: http responses with 2xx code HF_HRSP_3xx = 41 //41. hrsp_3xx [.FBS]: http responses with 3xx code HF_HRSP_4xx = 42 //42. hrsp_4xx [.FBS]: http responses with 4xx code HF_HRSP_5xx = 43 //43. hrsp_5xx [.FBS]: http responses with 5xx code HF_HRSP_OTHER = 44 //44. hrsp_other [.FBS]: http responses with other codes (protocol error) HF_HANAFAIL = 45 //45. hanafail [...S]: failed health checks details HF_REQ_RATE = 46 //46. req_rate [.F..]: HTTP requests per second over last elapsed second HF_REQ_RATE_MAX = 47 //47. req_rate_max [.F..]: max number of HTTP requests per second observed HF_REQ_TOT = 48 //48. req_tot [.F..]: total number of HTTP requests received HF_CLI_ABRT = 49 //49. cli_abrt [..BS]: number of data transfers aborted by the client HF_SRV_ABRT = 50 //50. srv_abrt [..BS]: number of data transfers aborted by the server (inc. in eresp) HF_COMP_IN = 51 //51. comp_in [.FB.]: number of HTTP response bytes fed to the compressor HF_COMP_OUT = 52 //52. comp_out [.FB.]: number of HTTP response bytes emitted by the compressor HF_COMP_BYP = 53 //53. comp_byp [.FB.]: number of bytes that bypassed the HTTP compressor (CPU/BW limit) HF_COMP_RSP = 54 //54. comp_rsp [.FB.]: number of HTTP responses that were compressed HF_LASTSESS = 55 //55. lastsess [..BS]: number of seconds since last session assigned to server/backend HF_LAST_CHK = 56 //56. last_chk [...S]: last health check contents or textual error HF_LAST_AGT = 57 //57. last_agt [...S]: last agent check contents or textual error HF_QTIME = 58 //58. qtime [..BS]: HF_CTIME = 59 //59. ctime [..BS]: HF_RTIME = 60 //60. rtime [..BS]: (0 for TCP) HF_TTIME = 61 //61. ttime [..BS]: the average total session time in ms over the 1024 last requests ) type haproxy struct { Servers []string client *http.Client } var sampleConfig = ` ## An array of address to gather stats about. Specify an ip on hostname ## with optional port. ie localhost, 10.10.3.33:1936, etc. ## Make sure you specify the complete path to the stats endpoint ## including the protocol, ie http://10.10.3.33:1936/haproxy?stats # ## If no servers are specified, then default to 127.0.0.1:1936/haproxy?stats servers = ["http://myhaproxy.com:1936/haproxy?stats"] ## ## You can also use local socket with standard wildcard globbing. ## Server address not starting with 'http' will be treated as a possible ## socket, so both examples below are valid. ## servers = ["socket:/run/haproxy/admin.sock", "/run/haproxy/*.sock"] ` func (r *haproxy) SampleConfig() string { return sampleConfig } func (r *haproxy) Description() string { return "Read metrics of haproxy, via socket or csv stats page" } // Reads stats from all configured servers accumulates stats. // Returns one of the errors encountered while gather stats (if any). func (g *haproxy) Gather(acc telegraf.Accumulator) error { if len(g.Servers) == 0 { return g.gatherServer("http://127.0.0.1:1936/haproxy?stats", acc) } endpoints := make([]string, 0, len(g.Servers)) for _, endpoint := range g.Servers { if strings.HasPrefix(endpoint, "http") { endpoints = append(endpoints, endpoint) continue } socketPath := getSocketAddr(endpoint) matches, err := filepath.Glob(socketPath) if err != nil { return err } if len(matches) == 0 { endpoints = append(endpoints, socketPath) } else { for _, match := range matches { endpoints = append(endpoints, match) } } } var wg sync.WaitGroup errChan := errchan.New(len(endpoints)) wg.Add(len(endpoints)) for _, server := range endpoints { go func(serv string) { defer wg.Done() errChan.C <- g.gatherServer(serv, acc) }(server) } wg.Wait() return errChan.Error() } func (g *haproxy) gatherServerSocket(addr string, acc telegraf.Accumulator) error { socketPath := getSocketAddr(addr) c, err := net.Dial("unix", socketPath) if err != nil { return fmt.Errorf("Could not connect to socket '%s': %s", addr, err) } _, errw := c.Write([]byte("show stat\n")) if errw != nil { return fmt.Errorf("Could not write to socket '%s': %s", addr, errw) } return importCsvResult(c, acc, socketPath) } func (g *haproxy) gatherServer(addr string, acc telegraf.Accumulator) error { if !strings.HasPrefix(addr, "http") { return g.gatherServerSocket(addr, acc) } if g.client == nil { tr := &http.Transport{ResponseHeaderTimeout: time.Duration(3 * time.Second)} client := &http.Client{ Transport: tr, Timeout: time.Duration(4 * time.Second), } g.client = client } if !strings.HasSuffix(addr, ";csv") { addr += "/;csv" } u, err := url.Parse(addr) if err != nil { return fmt.Errorf("Unable parse server address '%s': %s", addr, err) } req, err := http.NewRequest("GET", addr, nil) if u.User != nil { p, _ := u.User.Password() req.SetBasicAuth(u.User.Username(), p) } res, err := g.client.Do(req) if err != nil { return fmt.Errorf("Unable to connect to haproxy server '%s': %s", addr, err) } if res.StatusCode != 200 { return fmt.Errorf("Unable to get valid stat result from '%s', http response code : %d", addr, res.StatusCode) } return importCsvResult(res.Body, acc, u.Host) } func getSocketAddr(sock string) string { socketAddr := strings.Split(sock, ":") if len(socketAddr) >= 2 { return socketAddr[1] } else { return socketAddr[0] } } func importCsvResult(r io.Reader, acc telegraf.Accumulator, host string) error { csv := csv.NewReader(r) result, err := csv.ReadAll() now := time.Now() for _, row := range result { fields := make(map[string]interface{}) tags := map[string]string{ "server": host, "proxy": row[HF_PXNAME], "sv": row[HF_SVNAME], } for field, v := range row { switch field { case HF_QCUR: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["qcur"] = ival } case HF_QMAX: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["qmax"] = ival } case HF_SCUR: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["scur"] = ival } case HF_SMAX: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["smax"] = ival } case HF_SLIM: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["slim"] = ival } case HF_STOT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["stot"] = ival } case HF_BIN: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["bin"] = ival } case HF_BOUT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["bout"] = ival } case HF_DREQ: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["dreq"] = ival } case HF_DRESP: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["dresp"] = ival } case HF_EREQ: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["ereq"] = ival } case HF_ECON: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["econ"] = ival } case HF_ERESP: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["eresp"] = ival } case HF_WRETR: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["wretr"] = ival } case HF_WREDIS: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["wredis"] = ival } case HF_ACT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["active_servers"] = ival } case HF_BCK: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["backup_servers"] = ival } case HF_DOWNTIME: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["downtime"] = ival } case HF_THROTTLE: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["throttle"] = ival } case HF_LBTOT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["lbtot"] = ival } case HF_RATE: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["rate"] = ival } case HF_RATE_MAX: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["rate_max"] = ival } case HF_CHECK_DURATION: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["check_duration"] = ival } case HF_HRSP_1xx: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["http_response.1xx"] = ival } case HF_HRSP_2xx: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["http_response.2xx"] = ival } case HF_HRSP_3xx: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["http_response.3xx"] = ival } case HF_HRSP_4xx: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["http_response.4xx"] = ival } case HF_HRSP_5xx: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["http_response.5xx"] = ival } case HF_REQ_RATE: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["req_rate"] = ival } case HF_REQ_RATE_MAX: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["req_rate_max"] = ival } case HF_REQ_TOT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["req_tot"] = ival } case HF_CLI_ABRT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["cli_abort"] = ival } case HF_SRV_ABRT: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["srv_abort"] = ival } case HF_QTIME: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["qtime"] = ival } case HF_CTIME: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["ctime"] = ival } case HF_RTIME: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["rtime"] = ival } case HF_TTIME: ival, err := strconv.ParseUint(v, 10, 64) if err == nil { fields["ttime"] = ival } } } acc.AddFields("haproxy", fields, tags, now) } return err } func init() { inputs.Add("haproxy", func() telegraf.Input { return &haproxy{} }) }