diff --git a/README.md b/README.md index 0f088ed..dbc3051 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,10 @@ Usage: php-fpm-exporter [flags] Flags: - --addr string listen address for metrics handler (default "127.0.0.1:8080") - --endpoint string url for php-fpm status (default "http://127.0.0.1:9000/status") - --fastcgi string fastcgi url. If this is set, fastcgi will be used instead of HTTP + --addr string listen address for metrics handler (default "127.0.0.1:8080") + --endpoint string url for php-fpm status (default "http://127.0.0.1:9000/status") + --fastcgi string fastcgi url. If this is set, fastcgi will be used instead of HTTP + --status.timeout duration Scrape timeout for php-fpm status. If unset, then will wait forever. ``` When running, a simple healthcheck is available on `/healthz` diff --git a/cmd/php-fpm-exporter/main.go b/cmd/php-fpm-exporter/main.go index e2ab8e7..e99107a 100644 --- a/cmd/php-fpm-exporter/main.go +++ b/cmd/php-fpm-exporter/main.go @@ -13,6 +13,7 @@ func main() { endpoint = kingpin.Flag("endpoint", "url for php-fpm status").Default("http://127.0.0.1:9000/status").Envar("ENDPOINT_URL").String() fcgiEndpoint = kingpin.Flag("fastcgi", "fastcgi url. If this is set, fastcgi will be used instead of HTTP").Envar("FASTCGI_URL").String() metricsEndpoint = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics. Cannot be /").Default("/metrics").Envar("TELEMETRY_PATH").String() + statusTimeout = kingpin.Flag("status.timeout", "Scrape timeout for php-fpm status. If unset, then will wait forever.").Envar("STATUS_TIMEOUT").Duration() ) kingpin.HelpFlag.Short('h') @@ -29,6 +30,7 @@ func main() { exporter.SetFastcgi(*fcgiEndpoint), exporter.SetLogger(logger), exporter.SetMetricsEndpoint(*metricsEndpoint), + exporter.SetStatusTimeout(statusTimeout), ) if err != nil { diff --git a/collector.go b/collector.go index 882283c..2a75aa4 100644 --- a/collector.go +++ b/collector.go @@ -3,7 +3,6 @@ package exporter import ( "io/ioutil" "net/http" - "net/url" "regexp" "strconv" @@ -70,7 +69,8 @@ func (c *collector) Describe(ch chan<- *prometheus.Desc) { ch <- c.slowRequests } -func getDataFastcgi(u *url.URL) ([]byte, error) { +func (c *collector) getDataFastcgi() ([]byte, error) { + u := c.exporter.fcgiEndpoint path := u.Path host := u.Host @@ -86,13 +86,17 @@ func getDataFastcgi(u *url.URL) ([]byte, error) { "SCRIPT_NAME": path, } - fcgi, err := fcgiclient.Dial(u.Scheme, host) + fcgi, err := fcgiclient.DialTimeout(u.Scheme, host, c.exporter.statusTimeout) if err != nil { return nil, errors.Wrap(err, "fastcgi dial failed") } defer fcgi.Close() + if err = fcgi.SetTimeout(c.exporter.statusTimeout); err != nil { + return nil, errors.Wrap(err, "fastcgi SetTimeout failed") + } + resp, err := fcgi.Get(env) if err != nil { return nil, errors.Wrap(err, "fastcgi get failed") @@ -112,7 +116,8 @@ func getDataFastcgi(u *url.URL) ([]byte, error) { return body, nil } -func getDataHTTP(u *url.URL) ([]byte, error) { +func (c *collector) getDataHTTP() ([]byte, error) { + u := c.exporter.endpoint req := http.Request{ Method: "GET", URL: u, @@ -123,7 +128,7 @@ func getDataHTTP(u *url.URL) ([]byte, error) { Host: u.Host, } - resp, err := http.DefaultClient.Do(&req) + resp, err := (&http.Client{Timeout: c.exporter.statusTimeout}).Do(&req) if err != nil { return nil, errors.Wrap(err, "HTTP request failed") } @@ -150,9 +155,9 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) { ) if c.exporter.fcgiEndpoint != nil && c.exporter.fcgiEndpoint.String() != "" { - body, err = getDataFastcgi(c.exporter.fcgiEndpoint) + body, err = c.getDataFastcgi() } else { - body, err = getDataHTTP(c.exporter.endpoint) + body, err = c.getDataHTTP() } if err != nil { diff --git a/exporter.go b/exporter.go index 1bd48c9..dd7c63a 100644 --- a/exporter.go +++ b/exporter.go @@ -24,6 +24,7 @@ type Exporter struct { fcgiEndpoint *url.URL logger *zap.Logger metricsEndpoint string + statusTimeout time.Duration } // OptionsFunc is a function passed to new for setting options on a new Exporter. @@ -124,6 +125,19 @@ func SetMetricsEndpoint(path string) func(*Exporter) error { } } +// SetStatusTimeout sets the timeout for a request to the fpm status endpoint. +// Generally only used when create a new Exporter. +func SetStatusTimeout(timeout *time.Duration) func(*Exporter) error { + return func(e *Exporter) error { + if timeout == nil { + e.statusTimeout = 0 + } else { + e.statusTimeout = *timeout + } + return nil + } +} + var healthzOK = []byte("ok\n") func (e *Exporter) healthz(w http.ResponseWriter, r *http.Request) { diff --git a/go.mod b/go.mod index 46506ac..f8ac5a9 100644 --- a/go.mod +++ b/go.mod @@ -21,3 +21,7 @@ require ( golang.org/x/sync v0.0.0-20170418210838-de49d9dcd27d gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) + +replace ( + github.com/tomasen/fcgi_client => github.com/kanocz/fcgi_client v0.0.0-20210113082628-fff85c8adfb7 +) diff --git a/go.sum b/go.sum index a2ced3a..fb75c36 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v0.0.0-20170331031902-2bba0603135d h1:KmiEmEGA5sqizMpKnexwioxj8zEUSBc7p9UTQu36lpQ= github.com/golang/protobuf v0.0.0-20170331031902-2bba0603135d/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/kanocz/fcgi_client v0.0.0-20210113082628-fff85c8adfb7 h1:W0fAsQ7bC1db4k9O2X6yZvatz/0c/ISyxhmNnc6arZA= +github.com/kanocz/fcgi_client v0.0.0-20210113082628-fff85c8adfb7/go.mod h1:dHpIS7C6YjFguh5vo9QBVEojDoL3vh3v6oEho2HtNyA= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= diff --git a/vendor/github.com/tomasen/fcgi_client/fcgiclient.go b/vendor/github.com/tomasen/fcgi_client/fcgiclient.go index 6f88072..abd2597 100644 --- a/vendor/github.com/tomasen/fcgi_client/fcgiclient.go +++ b/vendor/github.com/tomasen/fcgi_client/fcgiclient.go @@ -8,20 +8,22 @@ import ( "bufio" "bytes" "encoding/binary" - "sync" "errors" + "fmt" "io" "io/ioutil" + "mime/multipart" "net" - "net/url" + "net/http" "net/http/httputil" "net/textproto" - "strconv" + "net/url" "os" - "net/http" - "mime/multipart" "path/filepath" - "time" + "strconv" + "strings" + "sync" + "time" ) const FCGI_LISTENSOCK_FILENO uint8 = 0 @@ -92,7 +94,7 @@ func (h *header) init(recType uint8, reqId uint16, contentLength int) { } type record struct { - h header + h header rbuf []byte } @@ -117,19 +119,19 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) { } buf = rec.rbuf[:int(rec.h.ContentLength)] - return + return } type FCGIClient struct { mutex sync.Mutex rwc io.ReadWriteCloser h header - buf bytes.Buffer + buf bytes.Buffer keepAlive bool reqId uint16 } -// Connects to the fcgi responder at the specified network address. +// Connects to the fcgi responder at the specified network address. // See func net.Dial for a description of the network and address parameters. func Dial(network, address string) (fcgi *FCGIClient, err error) { var conn net.Conn @@ -144,15 +146,15 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) { keepAlive: false, reqId: 1, } - + return } // Connects to the fcgi responder at the specified network address with timeout // See func net.DialTimeout for a description of the network, address and timeout parameters. func DialTimeout(network, address string, timeout time.Duration) (fcgi *FCGIClient, err error) { - - var conn net.Conn + + var conn net.Conn conn, err = net.DialTimeout(network, address, timeout) if err != nil { @@ -164,7 +166,7 @@ func DialTimeout(network, address string, timeout time.Duration) (fcgi *FCGIClie keepAlive: false, reqId: 1, } - + return } @@ -173,7 +175,27 @@ func (this *FCGIClient) Close() { this.rwc.Close() } -func (this *FCGIClient) writeRecord(recType uint8, content []byte) ( err error) { +// Set timeout for all future and pending read and write operations +func (this *FCGIClient) SetTimeout(timeout time.Duration) error { + conn, ok := this.rwc.(net.Conn) + if !ok { + return errors.New("Invalid FCGIClient.rwc") + } + + return conn.SetDeadline(time.Now().Add(timeout)) +} + +// Cancel timeout for all future and pending read and write operations +func (this *FCGIClient) CancelTimeout() error { + conn, ok := this.rwc.(net.Conn) + if !ok { + return errors.New("Invalid FCGIClient.rwc") + } + + return conn.SetDeadline(time.Time{}) +} + +func (this *FCGIClient) writeRecord(recType uint8, content []byte) (err error) { this.mutex.Lock() defer this.mutex.Unlock() this.buf.Reset() @@ -204,15 +226,15 @@ func (this *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) err } func (this *FCGIClient) writePairs(recType uint8, pairs map[string]string) error { - w := newWriter(this, recType) - b := make([]byte, 8) + w := newWriter(this, recType) + b := make([]byte, 8) nn := 0 for k, v := range pairs { m := 8 + len(k) + len(v) if m > maxWrite { // param data size exceed 65535 bytes" vl := maxWrite - 8 - len(k) - v = v[:vl] + v = v[:vl] } n := encodeSize(b, uint32(len(k))) n += encodeSize(b[n:], uint32(len(v))) @@ -236,7 +258,6 @@ func (this *FCGIClient) writePairs(recType uint8, pairs map[string]string) error return nil } - func readSize(s []byte) (uint32, int) { if len(s) == 0 { return 0, 0 @@ -320,13 +341,13 @@ func (w *streamWriter) Close() error { } type streamReader struct { - c *FCGIClient - buf []byte + c *FCGIClient + buf []byte } func (w *streamReader) Read(p []byte) (n int, err error) { - - if len(p) > 0 { + + if len(p) > 0 { if len(w.buf) == 0 { rec := &record{} w.buf, err = rec.read(w.c.rwc) @@ -334,7 +355,7 @@ func (w *streamReader) Read(p []byte) (n int, err error) { return } } - + n = len(p) if n > len(w.buf) { n = len(w.buf) @@ -342,34 +363,41 @@ func (w *streamReader) Read(p []byte) (n int, err error) { copy(p, w.buf[:n]) w.buf = w.buf[n:] } - + return } -// Do made the request and returns a io.Reader that translates the data read +// Do made the request and returns a io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (this *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { - err = this.writeBeginRequest(uint16(FCGI_RESPONDER), 0) + err = this.writeBeginRequest(uint16(FCGI_RESPONDER), 0) if err != nil { return } - + err = this.writePairs(FCGI_PARAMS, p) if err != nil { return } - body := newWriter(this, FCGI_STDIN) + body := newWriter(this, FCGI_STDIN) if req != nil { io.Copy(body, req) } body.Close() - - r = &streamReader{c:this} - return + + r = &streamReader{c: this} + return +} + +type badStringError struct { + what string + str string } -// Request returns a HTTP Response with Header and Body +func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } + +// Request returns a HTTP Response with Header and Body // from fcgi responder func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { @@ -377,55 +405,83 @@ func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http. if err != nil { return } - + rb := bufio.NewReader(r) tp := textproto.NewReader(rb) resp = new(http.Response) - + // Parse the first line of the response. + line, err := tp.ReadLine() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return nil, err + } + if i := strings.IndexByte(line, ' '); i == -1 { + err = &badStringError{"malformed HTTP response", line} + } else { + resp.Proto = line[:i] + resp.Status = strings.TrimLeft(line[i+1:], " ") + } + statusCode := resp.Status + if i := strings.IndexByte(resp.Status, ' '); i != -1 { + statusCode = resp.Status[:i] + } + if len(statusCode) != 3 { + err = &badStringError{"malformed HTTP status code", statusCode} + } + resp.StatusCode, err = strconv.Atoi(statusCode) + if err != nil || resp.StatusCode < 0 { + err = &badStringError{"malformed HTTP status code", statusCode} + } + var ok bool + if resp.ProtoMajor, resp.ProtoMinor, ok = http.ParseHTTPVersion(resp.Proto); !ok { + err = &badStringError{"malformed HTTP version", resp.Proto} + } // Parse the response headers. mimeHeader, err := tp.ReadMIMEHeader() if err != nil { - return + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return nil, err } resp.Header = http.Header(mimeHeader) - // TODO: fixTransferEncoding ? resp.TransferEncoding = resp.Header["Transfer-Encoding"] - resp.ContentLength,_ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) - + resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) + if chunked(resp.TransferEncoding) { resp.Body = ioutil.NopCloser(httputil.NewChunkedReader(rb)) } else { resp.Body = ioutil.NopCloser(rb) } - return } // Get issues a GET request to the fcgi responder. func (this *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) { - - p["REQUEST_METHOD"] = "GET" + + p["REQUEST_METHOD"] = "GET" p["CONTENT_LENGTH"] = "0" - + return this.Request(p, nil) } -// Get issues a Post request to the fcgi responder. with request body +// Get issues a Post request to the fcgi responder. with request body // in the format that bodyType specified func (this *FCGIClient) Post(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) { - - if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" { + + if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" { p["REQUEST_METHOD"] = "POST" } p["CONTENT_LENGTH"] = strconv.Itoa(l) - if len(bodyType) > 0 { - p["CONTENT_TYPE"] = bodyType - } else { - p["CONTENT_TYPE"] = "application/x-www-form-urlencoded" - } - - + if len(bodyType) > 0 { + p["CONTENT_TYPE"] = bodyType + } else { + p["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + } + return this.Request(p, body) } @@ -437,12 +493,12 @@ func (this *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *ht } // PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard, -// with form as a string key to a list values (url.Values), +// with form as a string key to a list values (url.Values), // and/or with file as a string key to a list file path. func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) - bodyType := writer.FormDataContentType() + bodyType := writer.FormDataContentType() for key, val := range data { for _, v0 := range val { @@ -452,14 +508,14 @@ func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[ } } } - + for key, val := range file { fd, e := os.Open(val) if e != nil { return nil, e } defer fd.Close() - + part, e := writer.CreateFormFile(key, filepath.Base(val)) if e != nil { return nil, e @@ -471,7 +527,7 @@ func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[ if err != nil { return } - + return this.Post(p, bodyType, buf, buf.Len()) } diff --git a/vendor/modules.txt b/vendor/modules.txt index e4da161..f967226 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -18,23 +18,23 @@ github.com/prometheus/client_golang/prometheus/promhttp github.com/prometheus/client_model/go # github.com/prometheus/common v0.0.0-20170220103846-49fee292b27b github.com/prometheus/common/expfmt -github.com/prometheus/common/model github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg +github.com/prometheus/common/model # github.com/prometheus/procfs v0.0.0-20170216223256-a1dba9ce8bae github.com/prometheus/procfs github.com/prometheus/procfs/xfs -# github.com/tomasen/fcgi_client v0.0.0-20171212193905-d32b71631a94 +# github.com/tomasen/fcgi_client v0.0.0-20171212193905-d32b71631a94 => github.com/kanocz/fcgi_client v0.0.0-20210113082628-fff85c8adfb7 github.com/tomasen/fcgi_client # go.uber.org/atomic v1.3.1 go.uber.org/atomic # go.uber.org/zap v1.4.1 go.uber.org/zap -go.uber.org/zap/internal/bufferpool -go.uber.org/zap/internal/multierror -go.uber.org/zap/zapcore go.uber.org/zap/buffer +go.uber.org/zap/internal/bufferpool go.uber.org/zap/internal/color go.uber.org/zap/internal/exit +go.uber.org/zap/internal/multierror +go.uber.org/zap/zapcore # golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2 golang.org/x/net/context # golang.org/x/sync v0.0.0-20170418210838-de49d9dcd27d