1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,120 @@
# PHP-FPM Input Plugin
Get phpfpm stats using either HTTP status page or fpm socket.
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Read metrics of phpfpm, via HTTP status page or socket
[[inputs.phpfpm]]
## An array of addresses to gather stats about. Specify an ip or hostname
## with optional port and path
##
## Plugin can be configured in three modes (either can be used):
## - http: the URL must start with http:// or https://, ie:
## "http://localhost/status"
## "http://192.168.130.1/status?full"
##
## - unixsocket: path to fpm socket, ie:
## "/var/run/php5-fpm.sock"
## or using a custom fpm status path:
## "/var/run/php5-fpm.sock:fpm-custom-status-path"
## glob patterns are also supported:
## "/var/run/php*.sock"
##
## - fcgi: the URL must start with fcgi:// or cgi://, and port must be present, ie:
## "fcgi://10.0.0.12:9000/status"
## "cgi://10.0.10.12:9001/status"
##
## Example of multiple gathering from local socket and remote host
## urls = ["http://192.168.1.20/status", "/tmp/fpm.sock"]
urls = ["http://localhost/status"]
## Format of stats to parse, set to "status" or "json"
## If the user configures the URL to return JSON (e.g.
## http://localhost/status?json), set to JSON. Otherwise, will attempt to
## parse line-by-line. The JSON mode will produce additional metrics.
# format = "status"
## Duration allowed to complete HTTP requests.
# timeout = "5s"
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
```
When using `unixsocket`, you have to ensure that telegraf runs on same
host, and socket path is accessible to telegraf user.
## Metrics
- phpfpm
- tags:
- pool
- url
- fields:
- accepted_conn
- listen_queue
- max_listen_queue
- listen_queue_len
- idle_processes
- active_processes
- total_processes
- max_active_processes
- max_children_reached
- slow_requests
- phpfpm_process
- tags:
- pool
- request method
- request uri
- script
- url
- user
- fields:
- pid
- content length
- last request cpu
- last request memory
- request duration
- requests
- start time
- start since
- state
## Example Output
```text
phpfpm,pool=www accepted_conn=13i,active_processes=2i,idle_processes=1i,listen_queue=0i,listen_queue_len=0i,max_active_processes=2i,max_children_reached=0i,max_listen_queue=0i,slow_requests=0i,total_processes=3i 1453011293083331187
phpfpm,pool=www2 accepted_conn=12i,active_processes=1i,idle_processes=2i,listen_queue=0i,listen_queue_len=0i,max_active_processes=2i,max_children_reached=0i,max_listen_queue=0i,slow_requests=0i,total_processes=3i 1453011293083691422
phpfpm,pool=www3 accepted_conn=11i,active_processes=1i,idle_processes=2i,listen_queue=0i,listen_queue_len=0i,max_active_processes=2i,max_children_reached=0i,max_listen_queue=0i,slow_requests=0i,total_processes=3i 1453011293083691658
```
With the JSON output, additional metrics around processes are generated:
```text
phpfpm,pool=www,url=http://127.0.0.1:44637?full&json accepted_conn=3879i,active_processes=1i,idle_processes=9i,listen_queue=0i,listen_queue_len=0i,max_active_processes=3i,max_children_reached=0i,max_listen_queue=0i,slow_requests=0i,start_since=4901i,total_processes=10i
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status?json&full,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=583i,last_request_cpu=0,last_request_memory=0,request_duration=159i,requests=386i,start_time=1702044927i,state="Running"
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=584i,last_request_cpu=0,last_request_memory=2097152,request_duration=174i,requests=390i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=585i,last_request_cpu=104.93,last_request_memory=2097152,request_duration=9530i,requests=389i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=586i,last_request_cpu=0,last_request_memory=2097152,request_duration=127i,requests=399i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=587i,last_request_cpu=0,last_request_memory=2097152,request_duration=9713i,requests=382i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=588i,last_request_cpu=0,last_request_memory=2097152,request_duration=133i,requests=383i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status?json,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=589i,last_request_cpu=0,last_request_memory=2097152,request_duration=154i,requests=381i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=590i,last_request_cpu=0,last_request_memory=2097152,request_duration=108i,requests=397i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=591i,last_request_cpu=110.28,last_request_memory=2097152,request_duration=9068i,requests=381i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=592i,last_request_cpu=64.27,last_request_memory=2097152,request_duration=15559i,requests=391i,start_time=1702044927i,state="Idle"
```

View file

@ -0,0 +1,315 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package phpfpm
// This file implements FastCGI from the perspective of a child process.
import (
"errors"
"fmt"
"io"
"net/http"
"net/http/cgi"
"strings"
"sync"
"time"
)
// request holds the state for an in-progress request. As soon as it's complete,
// it's converted to an http.Request.
type request struct {
pw *io.PipeWriter
reqID uint16
params map[string]string
buf [1024]byte
rawParams []byte
keepConn bool
}
func newRequest(reqID uint16, flags uint8) *request {
r := &request{
reqID: reqID,
params: make(map[string]string),
keepConn: flags&flagKeepConn != 0,
}
r.rawParams = r.buf[:0]
return r
}
// parseParams reads an encoded []byte into Params.
func (r *request) parseParams() {
text := r.rawParams
r.rawParams = nil
for len(text) > 0 {
keyLen, n := readSize(text)
if n == 0 {
return
}
text = text[n:]
valLen, n := readSize(text)
if n == 0 {
return
}
text = text[n:]
if int(keyLen)+int(valLen) > len(text) {
return
}
key := readString(text, keyLen)
text = text[keyLen:]
val := readString(text, valLen)
text = text[valLen:]
r.params[key] = val
}
}
// response implements http.ResponseWriter.
type response struct {
req *request
header http.Header
w *bufWriter
wroteHeader bool
}
func newResponse(c *child, req *request) *response {
return &response{
req: req,
header: http.Header{},
w: newWriter(c.conn, typeStdout, req.reqID),
}
}
// Header returns the HTTP headers for the response.
func (r *response) Header() http.Header {
return r.header
}
func (r *response) Write(data []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
return r.w.Write(data)
}
// WriteHeader sends an HTTP response header with the provided status code.
func (r *response) WriteHeader(code int) {
if r.wroteHeader {
return
}
r.wroteHeader = true
if code == http.StatusNotModified {
// Must not have body.
r.header.Del("Content-Type")
r.header.Del("Content-Length")
r.header.Del("Transfer-Encoding")
} else if r.header.Get("Content-Type") == "" {
r.header.Set("Content-Type", "text/html; charset=utf-8")
}
if r.header.Get("Date") == "" {
r.header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
}
fmt.Fprintf(r.w, "Status: %d %s\r\n", code, http.StatusText(code))
//nolint:errcheck // unable to propagate
r.header.Write(r.w)
//nolint:errcheck // unable to propagate
r.w.WriteString("\r\n")
}
// Flush sends any buffered data to the client.
func (r *response) Flush() {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
_ = r.w.Flush()
}
// Close closes the connection or resource associated with the response.
// It ensures proper cleanup of resources.
func (r *response) Close() error {
r.Flush()
return r.w.Close()
}
type child struct {
conn *conn
handler http.Handler
mu sync.Mutex // protects requests:
requests map[uint16]*request // keyed by request ID
}
func newChild(rwc io.ReadWriteCloser, handler http.Handler) *child {
return &child{
conn: newConn(rwc),
handler: handler,
requests: make(map[uint16]*request),
}
}
func (c *child) serve() {
defer c.conn.Close()
defer c.cleanUp()
var rec record
for {
if err := rec.read(c.conn.rwc); err != nil {
return
}
if err := c.handleRecord(&rec); err != nil {
return
}
}
}
var errCloseConn = errors.New("fcgi: connection should be closed")
var emptyBody = io.NopCloser(strings.NewReader(""))
// errRequestAborted is returned by Read when a handler attempts to read the
// body of a request that has been aborted by the web server.
var errRequestAborted = errors.New("fcgi: request aborted by web server")
// errConnClosed is returned by Read when a handler attempts to read the body of
// a request after the connection to the web server has been closed.
var errConnClosed = errors.New("fcgi: connection to web server closed")
func (c *child) handleRecord(rec *record) error {
c.mu.Lock()
req, ok := c.requests[rec.h.ID]
c.mu.Unlock()
if !ok && rec.h.Type != typeBeginRequest && rec.h.Type != typeGetValues {
// The spec says to ignore unknown request IDs.
return nil
}
switch rec.h.Type {
case typeBeginRequest:
if req != nil {
// The server is trying to begin a request with the same ID
// as an in-progress request. This is an error.
return errors.New("fcgi: received ID that is already in-flight")
}
var br beginRequest
if err := br.read(rec.content()); err != nil {
return err
}
if br.role != roleResponder {
return c.conn.writeEndRequest(rec.h.ID, 0, statusUnknownRole)
}
req = newRequest(rec.h.ID, br.flags)
c.mu.Lock()
c.requests[rec.h.ID] = req
c.mu.Unlock()
return nil
case typeParams:
// NOTE(eds): Technically a key-value pair can straddle the boundary
// between two packets. We buffer until we've received all parameters.
if len(rec.content()) > 0 {
req.rawParams = append(req.rawParams, rec.content()...)
return nil
}
req.parseParams()
return nil
case typeStdin:
content := rec.content()
if req.pw == nil {
var body io.ReadCloser
if len(content) > 0 {
// body could be an io.LimitReader, but it shouldn't matter
// as long as both sides are behaving.
body, req.pw = io.Pipe()
} else {
body = emptyBody
}
go c.serveRequest(req, body)
}
if len(content) > 0 {
// TODO(eds): This blocks until the handler reads from the pipe.
// If the handler takes a long time, it might be a problem.
if _, err := req.pw.Write(content); err != nil {
return err
}
} else if req.pw != nil {
if err := req.pw.Close(); err != nil {
return err
}
}
return nil
case typeGetValues:
values := map[string]string{"FCGI_MPXS_CONNS": "1"}
return c.conn.writePairs(typeGetValuesResult, 0, values)
case typeData:
// If the filter role is implemented, read the data stream here.
return nil
case typeAbortRequest:
c.mu.Lock()
delete(c.requests, rec.h.ID)
c.mu.Unlock()
if err := c.conn.writeEndRequest(rec.h.ID, 0, statusRequestComplete); err != nil {
return err
}
if req.pw != nil {
req.pw.CloseWithError(errRequestAborted)
}
if !req.keepConn {
// connection will close upon return
return errCloseConn
}
return nil
default:
b := make([]byte, 8)
b[0] = byte(rec.h.Type)
return c.conn.writeRecord(typeUnknownType, 0, b)
}
}
func (c *child) serveRequest(req *request, body io.ReadCloser) {
r := newResponse(c, req)
httpReq, err := cgi.RequestFromMap(req.params)
if err != nil {
// there was an error reading the request
r.WriteHeader(http.StatusInternalServerError)
if err := c.conn.writeRecord(typeStderr, req.reqID, []byte(err.Error())); err != nil {
return
}
} else {
httpReq.Body = body
c.handler.ServeHTTP(r, httpReq)
}
r.Close()
c.mu.Lock()
delete(c.requests, req.reqID)
c.mu.Unlock()
if err := c.conn.writeEndRequest(req.reqID, 0, statusRequestComplete); err != nil {
return
}
// Consume the entire body, so the host isn't still writing to
// us when we close the socket below in the !keepConn case,
// otherwise we'd send a RST. (golang.org/issue/4183)
// TODO(bradfitz): also bound this copy in time. Or send
// some sort of abort request to the host, so the host
// can properly cut off the client sending all the data.
// For now just bound it a little and
io.CopyN(io.Discard, body, 100<<20) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
body.Close()
if !req.keepConn {
c.conn.Close()
}
}
func (c *child) cleanUp() {
c.mu.Lock()
defer c.mu.Unlock()
for _, req := range c.requests {
if req.pw != nil {
// race with call to Close in c.serveRequest doesn't matter because
// Pipe(Reader|Writer).Close are idempotent
req.pw.CloseWithError(errConnClosed)
}
}
}

View file

@ -0,0 +1,270 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package phpfpm implements the FastCGI protocol.
// Currently only the responder role is supported.
// The protocol is defined at http://www.fastcgi.com/drupal/node/6?q=node/22
package phpfpm
// This file defines the raw protocol and some utilities used by the child and
// the host.
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
)
// recType is a record type, as defined by
// http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S8
type recType uint8
const (
typeBeginRequest recType = 1
typeAbortRequest recType = 2
typeEndRequest recType = 3
typeParams recType = 4
typeStdin recType = 5
typeStdout recType = 6
typeStderr recType = 7
typeData recType = 8
typeGetValues recType = 9
typeGetValuesResult recType = 10
typeUnknownType recType = 11
)
// keep the connection between web-server and responder open after request
const flagKeepConn = 1
const (
maxWrite = 65535 // maximum record body
maxPad = 255
)
const (
roleResponder = iota + 1 // only Responders are implemented.
roleAuthorizer
roleFilter
)
const (
statusRequestComplete = iota
statusCantMultiplex
statusOverloaded
statusUnknownRole
)
type header struct {
Version uint8
Type recType
ID uint16
ContentLength uint16
PaddingLength uint8
Reserved uint8
}
type beginRequest struct {
role uint16
flags uint8
reserved [5]uint8 //nolint:unused // Memory reservation
}
func (br *beginRequest) read(content []byte) error {
if len(content) != 8 {
return errors.New("fcgi: invalid begin request record")
}
br.role = binary.BigEndian.Uint16(content)
br.flags = content[2]
return nil
}
// for padding so we don't have to allocate all the time
// not synchronized because we don't care what the contents are
var pad [maxPad]byte
func (h *header) init(recType recType, reqID uint16, contentLength int) {
h.Version = 1
h.Type = recType
h.ID = reqID
h.ContentLength = uint16(contentLength)
h.PaddingLength = uint8(-contentLength & 7)
}
// conn sends records over rwc
type conn struct {
mutex sync.Mutex
rwc io.ReadWriteCloser
// to avoid allocations
buf bytes.Buffer
h header
}
func newConn(rwc io.ReadWriteCloser) *conn {
return &conn{rwc: rwc}
}
// Close closes the FastCGI connection and releases resources.
func (c *conn) Close() error {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.rwc.Close()
}
type record struct {
h header
buf [maxWrite + maxPad]byte
}
func (rec *record) read(r io.Reader) (err error) {
if err := binary.Read(r, binary.BigEndian, &rec.h); err != nil {
return err
}
if rec.h.Version != 1 {
return errors.New("fcgi: invalid header version")
}
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
return err
}
return nil
}
func (rec *record) content() []byte {
return rec.buf[:rec.h.ContentLength]
}
// writeRecord writes and sends a single record.
func (c *conn) writeRecord(recType recType, reqID uint16, b []byte) error {
c.mutex.Lock()
defer c.mutex.Unlock()
c.buf.Reset()
c.h.init(recType, reqID, len(b))
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
return err
}
c.buf.Write(b)
c.buf.Write(pad[:c.h.PaddingLength])
_, err := c.rwc.Write(c.buf.Bytes())
return err
}
func (c *conn) writeBeginRequest(reqID, role uint16, flags uint8) error {
b := [8]byte{byte(role >> 8), byte(role), flags}
return c.writeRecord(typeBeginRequest, reqID, b[:])
}
func (c *conn) writeEndRequest(reqID uint16, appStatus int, protocolStatus uint8) error {
b := make([]byte, 8)
binary.BigEndian.PutUint32(b, uint32(appStatus))
b[4] = protocolStatus
return c.writeRecord(typeEndRequest, reqID, b)
}
func (c *conn) writePairs(recType recType, reqID uint16, pairs map[string]string) error {
w := newWriter(c, recType, reqID)
b := make([]byte, 8)
for k, v := range pairs {
n := encodeSize(b, uint32(len(k)))
n += encodeSize(b[n:], uint32(len(v)))
if _, err := w.Write(b[:n]); err != nil {
return err
}
if _, err := w.WriteString(k); err != nil {
return err
}
if _, err := w.WriteString(v); err != nil {
return err
}
}
return w.Close()
}
func readSize(s []byte) (uint32, int) {
if len(s) == 0 {
return 0, 0
}
size, n := uint32(s[0]), 1
if size&(1<<7) != 0 {
if len(s) < 4 {
return 0, 0
}
n = 4
size = binary.BigEndian.Uint32(s)
size &^= 1 << 31
}
return size, n
}
func readString(s []byte, size uint32) string {
if size > uint32(len(s)) {
return ""
}
return string(s[:size])
}
func encodeSize(b []byte, size uint32) int {
if size > 127 {
size |= 1 << 31
binary.BigEndian.PutUint32(b, size)
return 4
}
b[0] = byte(size)
return 1
}
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
// Closed.
type bufWriter struct {
closer io.Closer
*bufio.Writer
}
// Close flushes any buffered data to the underlying writer and releases resources.
func (w *bufWriter) Close() error {
if err := w.Writer.Flush(); err != nil {
w.closer.Close()
return err
}
return w.closer.Close()
}
func newWriter(c *conn, recType recType, reqID uint16) *bufWriter {
s := &streamWriter{c: c, recType: recType, reqID: reqID}
w := bufio.NewWriterSize(s, maxWrite)
return &bufWriter{s, w}
}
// streamWriter abstracts out the separation of a stream into discrete records.
// It only writes maxWrite bytes at a time.
type streamWriter struct {
c *conn
recType recType
reqID uint16
}
func (w *streamWriter) Write(p []byte) (int, error) {
nn := 0
for len(p) > 0 {
n := len(p)
if n > maxWrite {
n = maxWrite
}
if err := w.c.writeRecord(w.recType, w.reqID, p[:n]); err != nil {
return nn, err
}
nn += n
p = p[n:]
}
return nn, nil
}
// Close closes the underlying stream and releases resources.
func (w *streamWriter) Close() error {
// send empty record to close the stream
return w.c.writeRecord(w.recType, w.reqID, nil)
}

View file

@ -0,0 +1,100 @@
package phpfpm
import (
"errors"
"io"
"net"
"strconv"
"strings"
"time"
)
// Create an fcgi client
func newFcgiClient(timeout time.Duration, h string, args ...interface{}) (*conn, error) {
var con net.Conn
if len(args) != 1 {
return nil, errors.New("fcgi: not enough params")
}
var err error
switch args[0].(type) {
case int:
addr := h + ":" + strconv.FormatInt(int64(args[0].(int)), 10)
if timeout == 0 {
con, err = net.Dial("tcp", addr)
} else {
con, err = net.DialTimeout("tcp", addr, timeout)
}
case string:
laddr := net.UnixAddr{Name: args[0].(string), Net: h}
con, err = net.DialUnix(h, nil, &laddr)
default:
return nil, errors.New("fcgi: we only accept int (port) or string (socket) params")
}
if err != nil {
return nil, err
}
if timeout != 0 {
if err := con.SetDeadline(time.Now().Add(timeout)); err != nil {
return nil, err
}
}
return &conn{rwc: con}, nil
}
func (c *conn) request(env map[string]string, requestData string) (retout, reterr []byte, err error) {
defer c.rwc.Close()
var reqID uint16 = 1
err = c.writeBeginRequest(reqID, uint16(roleResponder), 0)
if err != nil {
return nil, nil, err
}
err = c.writePairs(typeParams, reqID, env)
if err != nil {
return nil, nil, err
}
if len(requestData) > 0 {
if err := c.writeRecord(typeStdin, reqID, []byte(requestData)); err != nil {
return nil, nil, err
}
}
rec := &record{}
var err1 error
// receive until EOF or FCGI_END_REQUEST
READ_LOOP:
for {
err1 = rec.read(c.rwc)
if err1 != nil && strings.Contains(err1.Error(), "use of closed network connection") {
if !errors.Is(err1, io.EOF) {
err = err1
}
break
}
if err1 != nil && strings.Contains(err1.Error(), "i/o timeout") {
if !errors.Is(err1, io.EOF) {
err = err1
}
break
}
switch rec.h.Type {
case typeStdout:
retout = append(retout, rec.content()...)
case typeStderr:
reterr = append(reterr, rec.content()...)
case typeEndRequest:
fallthrough
default:
break READ_LOOP
}
}
return retout, reterr, err
}

View file

@ -0,0 +1,277 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package phpfpm
import (
"bytes"
"errors"
"io"
"net/http"
"testing"
)
const requestID uint16 = 1
var sizeTests = []struct {
size uint32
bytes []byte
}{
{0, []byte{0x00}},
{127, []byte{0x7F}},
{128, []byte{0x80, 0x00, 0x00, 0x80}},
{1000, []byte{0x80, 0x00, 0x03, 0xE8}},
{33554431, []byte{0x81, 0xFF, 0xFF, 0xFF}},
}
func TestSize(t *testing.T) {
b := make([]byte, 4)
for i, test := range sizeTests {
n := encodeSize(b, test.size)
if !bytes.Equal(b[:n], test.bytes) {
t.Errorf("%d expected %x, encoded %x", i, test.bytes, b)
}
size, n := readSize(test.bytes)
if size != test.size {
t.Errorf("%d expected %d, read %d", i, test.size, size)
}
if len(test.bytes) != n {
t.Errorf("%d did not consume all the bytes", i)
}
}
}
var streamTests = []struct {
desc string
recType recType
reqID uint16
content []byte
raw []byte
}{
{"single record", typeStdout, 1, nil,
[]byte{1, byte(typeStdout), 0, 1, 0, 0, 0, 0},
},
// this data will have to be split into two records
{"two records", typeStdin, 300, make([]byte, 66000),
bytes.Join([][]byte{
// header for the first record
{1, byte(typeStdin), 0x01, 0x2C, 0xFF, 0xFF, 1, 0},
make([]byte, 65536),
// header for the second
{1, byte(typeStdin), 0x01, 0x2C, 0x01, 0xD1, 7, 0},
make([]byte, 472),
// header for the empty record
{1, byte(typeStdin), 0x01, 0x2C, 0, 0, 0, 0},
},
nil),
},
}
type nilCloser struct {
io.ReadWriter
}
func (*nilCloser) Close() error { return nil }
func TestStreams(t *testing.T) {
var rec record
outer:
for _, test := range streamTests {
buf := bytes.NewBuffer(test.raw)
var content []byte
for buf.Len() > 0 {
if err := rec.read(buf); err != nil {
t.Errorf("%s: error reading record: %v", test.desc, err)
continue outer
}
content = append(content, rec.content()...)
}
if rec.h.Type != test.recType {
t.Errorf("%s: got type %d expected %d", test.desc, rec.h.Type, test.recType)
continue
}
if rec.h.ID != test.reqID {
t.Errorf("%s: got request ID %d expected %d", test.desc, rec.h.ID, test.reqID)
continue
}
if !bytes.Equal(content, test.content) {
t.Errorf("%s: read wrong content", test.desc)
continue
}
buf.Reset()
c := newConn(&nilCloser{buf})
w := newWriter(c, test.recType, test.reqID)
if _, err := w.Write(test.content); err != nil {
t.Errorf("%s: error writing record: %v", test.desc, err)
continue
}
if err := w.Close(); err != nil {
t.Errorf("%s: error closing stream: %v", test.desc, err)
continue
}
if !bytes.Equal(buf.Bytes(), test.raw) {
t.Errorf("%s: wrote wrong content", test.desc)
}
}
}
type writeOnlyConn struct {
buf []byte
}
func (c *writeOnlyConn) Write(p []byte) (int, error) {
c.buf = append(c.buf, p...)
return len(p), nil
}
func (*writeOnlyConn) Read([]byte) (int, error) {
return 0, errors.New("conn is write-only")
}
func (*writeOnlyConn) Close() error {
return nil
}
func TestGetValues(t *testing.T) {
var rec record
rec.h.Type = typeGetValues
wc := new(writeOnlyConn)
c := newChild(wc, nil)
err := c.handleRecord(&rec)
if err != nil {
t.Fatalf("handleRecord: %v", err)
}
const want = "\x01\n\x00\x00\x00\x12\x06\x00" +
"\x0f\x01FCGI_MPXS_CONNS1" +
"\x00\x00\x00\x00\x00\x00\x01\n\x00\x00\x00\x00\x00\x00"
if got := string(wc.buf); got != want {
t.Errorf(" got: %q\nwant: %q\n", got, want)
}
}
func nameValuePair11(nameData, valueData string) []byte {
return bytes.Join(
[][]byte{
{byte(len(nameData)), byte(len(valueData))},
[]byte(nameData),
[]byte(valueData),
},
nil,
)
}
func makeRecord(
recordType recType,
contentData []byte,
) []byte {
requestIDB1 := byte(requestID >> 8)
requestIDB0 := byte(requestID)
contentLength := len(contentData)
contentLengthB1 := byte(contentLength >> 8)
contentLengthB0 := byte(contentLength)
return bytes.Join([][]byte{
{1, byte(recordType), requestIDB1, requestIDB0, contentLengthB1,
contentLengthB0, 0, 0},
contentData,
},
nil)
}
// a series of FastCGI records that start a request and begin sending the
// request body
var streamBeginTypeStdin = bytes.Join([][]byte{
// set up request 1
makeRecord(typeBeginRequest, []byte{0, byte(roleResponder), 0, 0, 0, 0, 0, 0}),
// add required parameters to request 1
makeRecord(typeParams, nameValuePair11("REQUEST_METHOD", "GET")),
makeRecord(typeParams, nameValuePair11("SERVER_PROTOCOL", "HTTP/1.1")),
makeRecord(typeParams, nil),
// begin sending body of request 1
makeRecord(typeStdin, []byte("0123456789abcdef")),
},
nil)
var cleanUpTests = []struct {
input []byte
err error
}{
// confirm that child.handleRecord closes req.pw after aborting req
{
bytes.Join([][]byte{
streamBeginTypeStdin,
makeRecord(typeAbortRequest, nil),
},
nil),
errRequestAborted,
},
// confirm that child.serve closes all pipes after error reading record
{
bytes.Join([][]byte{
streamBeginTypeStdin,
nil,
},
nil),
errConnClosed,
},
}
type nopWriteCloser struct {
io.ReadWriter
}
func (nopWriteCloser) Close() error {
return nil
}
// Test that child.serve closes the bodies of aborted requests and closes the
// bodies of all requests before returning. Causes deadlock if either condition
// isn't met. See issue 6934.
func TestChildServeCleansUp(t *testing.T) {
for _, tt := range cleanUpTests {
input := make([]byte, len(tt.input))
copy(input, tt.input)
rc := nopWriteCloser{bytes.NewBuffer(input)}
done := make(chan bool)
c := newChild(rc, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// block on reading body of request
_, err := io.Copy(io.Discard, r.Body)
if !errors.Is(err, tt.err) {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %v, actual: %v", tt.err, err)
}
// not reached if body of request isn't closed
done <- true
}))
go c.serve()
// wait for body of request to be closed or all goroutines to block
<-done
}
}
type rwNopCloser struct {
io.Reader
io.Writer
}
func (rwNopCloser) Close() error {
return nil
}
// Verifies it doesn't crash. Issue 11824.
func TestMalformedParams(_ *testing.T) {
input := []byte{
// beginRequest, requestId=1, contentLength=8, role=1, keepConn=1
1, 1, 0, 1, 0, 8, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
// params, requestId=1, contentLength=10, k1Len=50, v1Len=50 (malformed, wrong length)
1, 4, 0, 1, 0, 10, 0, 0, 50, 50, 3, 4, 5, 6, 7, 8, 9, 10,
// end of params
1, 4, 0, 1, 0, 0, 0, 0,
}
rw := rwNopCloser{bytes.NewReader(input), io.Discard}
c := newChild(rw, http.DefaultServeMux)
c.serve()
}

View file

@ -0,0 +1,404 @@
//go:generate ../../../tools/readme_config_includer/generator
package phpfpm
import (
"bufio"
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/globpath"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const (
pfPool = "pool"
pfStartSince = "start since"
pfAcceptedConn = "accepted conn"
pfListenQueue = "listen queue"
pfMaxListenQueue = "max listen queue"
pfListenQueueLen = "listen queue len"
pfIdleProcesses = "idle processes"
pfActiveProcesses = "active processes"
pfTotalProcesses = "total processes"
pfMaxActiveProcesses = "max active processes"
pfMaxChildrenReached = "max children reached"
pfSlowRequests = "slow requests"
)
type Phpfpm struct {
Format string `toml:"format"`
Timeout config.Duration `toml:"timeout"`
Urls []string `toml:"urls"`
Log telegraf.Logger `toml:"-"`
tls.ClientConfig
client *http.Client
}
type jsonMetrics struct {
Pool string `json:"pool"`
ProcessManager string `json:"process manager"`
StartTime int `json:"start time"`
StartSince int `json:"start since"`
AcceptedConn int `json:"accepted conn"`
ListenQueue int `json:"listen queue"`
MaxListenQueue int `json:"max listen queue"`
ListenQueueLen int `json:"listen queue len"`
IdleProcesses int `json:"idle processes"`
ActiveProcesses int `json:"active processes"`
TotalProcesses int `json:"total processes"`
MaxActiveProcesses int `json:"max active processes"`
MaxChildrenReached int `json:"max children reached"`
SlowRequests int `json:"slow requests"`
Processes []struct {
Pid int `json:"pid"`
State string `json:"state"`
StartTime int `json:"start time"`
StartSince int `json:"start since"`
Requests int `json:"requests"`
RequestDuration int `json:"request duration"`
RequestMethod string `json:"request method"`
RequestURI string `json:"request uri"`
ContentLength int `json:"content length"`
User string `json:"user"`
Script string `json:"script"`
LastRequestCPU float64 `json:"last request cpu"`
LastRequestMemory float64 `json:"last request memory"`
} `json:"processes"`
}
type metricStat map[string]int64
type poolStat map[string]metricStat
func (*Phpfpm) SampleConfig() string {
return sampleConfig
}
func (p *Phpfpm) Init() error {
if len(p.Urls) == 0 {
p.Urls = []string{"http://127.0.0.1/status"}
}
tlsCfg, err := p.ClientConfig.TLSConfig()
if err != nil {
return err
}
switch p.Format {
case "":
p.Format = "status"
case "status", "json":
// both valid
default:
return fmt.Errorf("invalid format: %s", p.Format)
}
p.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
Timeout: time.Duration(p.Timeout),
}
return nil
}
func (p *Phpfpm) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
for _, serv := range expandUrls(acc, p.Urls) {
wg.Add(1)
go func(serv string) {
defer wg.Done()
acc.AddError(p.gatherServer(serv, acc))
}(serv)
}
wg.Wait()
return nil
}
// Request status page to get stat raw data and import it
func (p *Phpfpm) gatherServer(addr string, acc telegraf.Accumulator) error {
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
return p.gatherHTTP(addr, acc)
}
var (
fcgi *conn
socketPath string
statusPath string
)
var err error
if strings.HasPrefix(addr, "fcgi://") || strings.HasPrefix(addr, "cgi://") {
u, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("unable parse server address %q: %w", addr, err)
}
socketAddr := strings.Split(u.Host, ":")
if len(socketAddr) < 2 {
return fmt.Errorf("url does not follow required 'address:port' format: %s", u.Host)
}
fcgiIP := socketAddr[0]
fcgiPort, err := strconv.Atoi(socketAddr[1])
if err != nil {
return fmt.Errorf("unable to parse server port %q: %w", socketAddr[1], err)
}
fcgi, err = newFcgiClient(time.Duration(p.Timeout), fcgiIP, fcgiPort)
if err != nil {
return err
}
if len(u.Path) > 1 {
statusPath = strings.Trim(u.Path, "/")
} else {
statusPath = "status"
}
} else {
socketPath, statusPath = unixSocketPaths(addr)
if statusPath == "" {
statusPath = "status"
}
fcgi, err = newFcgiClient(time.Duration(p.Timeout), "unix", socketPath)
}
if err != nil {
return err
}
return p.gatherFcgi(fcgi, statusPath, acc, addr)
}
// Gather stat using fcgi protocol
func (p *Phpfpm) gatherFcgi(fcgi *conn, statusPath string, acc telegraf.Accumulator, addr string) error {
fpmOutput, fpmErr, err := fcgi.request(map[string]string{
"SCRIPT_NAME": "/" + statusPath,
"SCRIPT_FILENAME": statusPath,
"REQUEST_METHOD": "GET",
"CONTENT_LENGTH": "0",
"SERVER_PROTOCOL": "HTTP/1.0",
"SERVER_SOFTWARE": "go / fcgiclient ",
"REMOTE_ADDR": "127.0.0.1",
}, "/"+statusPath)
if len(fpmErr) == 0 && err == nil {
p.importMetric(bytes.NewReader(fpmOutput), acc, addr)
return nil
}
return fmt.Errorf("unable parse phpfpm status, error: %s; %w", string(fpmErr), err)
}
// Gather stat using http protocol
func (p *Phpfpm) gatherHTTP(addr string, acc telegraf.Accumulator) error {
u, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("unable parse server address %q: %w", addr, err)
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return fmt.Errorf("unable to create new request %q: %w", addr, err)
}
res, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("unable to connect to phpfpm status page %q: %w", addr, err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("unable to get valid stat result from %q: %w", addr, err)
}
p.importMetric(res.Body, acc, addr)
return nil
}
// Import stat data into Telegraf system
func (p *Phpfpm) importMetric(r io.Reader, acc telegraf.Accumulator, addr string) {
if p.Format == "json" {
p.parseJSON(r, acc, addr)
} else {
parseLines(r, acc, addr)
}
}
func parseLines(r io.Reader, acc telegraf.Accumulator, addr string) {
stats := make(poolStat)
var currentPool string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
statLine := scanner.Text()
keyvalue := strings.Split(statLine, ":")
if len(keyvalue) < 2 {
continue
}
fieldName := strings.Trim(keyvalue[0], " ")
// We start to gather data for a new pool here
if fieldName == pfPool {
currentPool = strings.Trim(keyvalue[1], " ")
stats[currentPool] = make(metricStat)
continue
}
// Start to parse metric for current pool
switch fieldName {
case pfStartSince,
pfAcceptedConn,
pfListenQueue,
pfMaxListenQueue,
pfListenQueueLen,
pfIdleProcesses,
pfActiveProcesses,
pfTotalProcesses,
pfMaxActiveProcesses,
pfMaxChildrenReached,
pfSlowRequests:
fieldValue, err := strconv.ParseInt(strings.Trim(keyvalue[1], " "), 10, 64)
if err == nil {
stats[currentPool][fieldName] = fieldValue
}
}
}
// Finally, we push the pool metric
for pool := range stats {
tags := map[string]string{
"pool": pool,
"url": addr,
}
fields := make(map[string]interface{})
for k, v := range stats[pool] {
fields[strings.ReplaceAll(k, " ", "_")] = v
}
acc.AddFields("phpfpm", fields, tags)
}
}
func (p *Phpfpm) parseJSON(r io.Reader, acc telegraf.Accumulator, addr string) {
var metrics jsonMetrics
if err := json.NewDecoder(r).Decode(&metrics); err != nil {
p.Log.Errorf("Unable to decode JSON response: %s", err)
return
}
timestamp := time.Now()
tags := map[string]string{
"pool": metrics.Pool,
"url": addr,
}
fields := map[string]any{
"start_since": metrics.StartSince,
"accepted_conn": metrics.AcceptedConn,
"listen_queue": metrics.ListenQueue,
"max_listen_queue": metrics.MaxListenQueue,
"listen_queue_len": metrics.ListenQueueLen,
"idle_processes": metrics.IdleProcesses,
"active_processes": metrics.ActiveProcesses,
"total_processes": metrics.TotalProcesses,
"max_active_processes": metrics.MaxActiveProcesses,
"max_children_reached": metrics.MaxChildrenReached,
"slow_requests": metrics.SlowRequests,
}
acc.AddFields("phpfpm", fields, tags, timestamp)
for _, process := range metrics.Processes {
tags := map[string]string{
"pool": metrics.Pool,
"url": addr,
"user": process.User,
"request_uri": process.RequestURI,
"request_method": process.RequestMethod,
"script": process.Script,
}
fields := map[string]any{
"pid": process.Pid,
"state": process.State,
"start_time": process.StartTime,
"requests": process.Requests,
"request_duration": process.RequestDuration,
"content_length": process.ContentLength,
"last_request_cpu": process.LastRequestCPU,
"last_request_memory": process.LastRequestMemory,
}
acc.AddFields("phpfpm_process", fields, tags, timestamp)
}
}
func expandUrls(acc telegraf.Accumulator, urls []string) []string {
addrs := make([]string, 0, len(urls))
for _, address := range urls {
if isNetworkURL(address) {
addrs = append(addrs, address)
continue
}
paths, err := globUnixSocket(address)
if err != nil {
acc.AddError(err)
continue
}
addrs = append(addrs, paths...)
}
return addrs
}
func globUnixSocket(address string) ([]string, error) {
pattern, status := unixSocketPaths(address)
glob, err := globpath.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("could not compile glob %q: %w", pattern, err)
}
paths := glob.Match()
if len(paths) == 0 {
return nil, fmt.Errorf("socket doesn't exist %q", pattern)
}
addresses := make([]string, 0, len(paths))
for _, path := range paths {
if status != "" {
path = path + ":" + status
}
addresses = append(addresses, path)
}
return addresses, nil
}
func unixSocketPaths(addr string) (socketPath, statusPath string) {
socketAddr := strings.Split(addr, ":")
if len(socketAddr) >= 2 {
socketPath = socketAddr[0]
statusPath = socketAddr[1]
} else {
socketPath = socketAddr[0]
statusPath = ""
}
return socketPath, statusPath
}
func isNetworkURL(addr string) bool {
return strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") || strings.HasPrefix(addr, "fcgi://") || strings.HasPrefix(addr, "cgi://")
}
func init() {
inputs.Add("phpfpm", func() telegraf.Input {
return &Phpfpm{}
})
}

View file

@ -0,0 +1,495 @@
//go:build !windows
// TODO: Windows - should be enabled for Windows when super asterisk is fixed on Windows
// https://github.com/influxdata/telegraf/issues/6248
package phpfpm
import (
"bytes"
"crypto/rand"
_ "embed"
"encoding/binary"
"fmt"
"net"
"net/http"
"net/http/fcgi"
"net/http/httptest"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
type statServer struct{}
// We create a fake server to return test data
func (statServer) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", strconv.Itoa(len(outputSample)))
fmt.Fprint(w, outputSample)
}
func TestPhpFpmGeneratesMetrics_From_Http(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("test") != "ok" {
w.WriteHeader(http.StatusInternalServerError)
t.Errorf("Not equal, expected: %q, actual: %q", "ok", r.URL.Query().Get("test"))
return
}
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", strconv.Itoa(len(outputSample)))
if _, err := fmt.Fprint(w, outputSample); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer ts.Close()
url := ts.URL + "?test=ok"
r := &Phpfpm{
Urls: []string{url},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(r.Gather))
tags := map[string]string{
"pool": "www",
"url": url,
}
fields := map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
}
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
}
func TestPhpFpmGeneratesJSONMetrics_From_Http(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/json")
w.Header().Set("Content-Length", strconv.Itoa(len(outputSampleJSON)))
if _, err := fmt.Fprint(w, string(outputSampleJSON)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
parser := &influx.Parser{}
require.NoError(t, parser.Init())
expected, err := testutil.ParseMetricsFromFile("testdata/expected.out", parser)
require.NoError(t, err)
input := &Phpfpm{
Urls: []string{server.URL + "?full&json"},
Format: "json",
Log: &testutil.Logger{},
}
require.NoError(t, input.Init())
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(input.Gather))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime(), testutil.IgnoreTags("url"))
}
func TestPhpFpmGeneratesMetrics_From_Fcgi(t *testing.T) {
// Let OS find an available port
tcp, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "Cannot initialize test server")
defer tcp.Close()
s := statServer{}
go fcgi.Serve(tcp, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
// Now we tested again above server
r := &Phpfpm{
Urls: []string{"fcgi://" + tcp.Addr().String() + "/status"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(r.Gather))
tags := map[string]string{
"pool": "www",
"url": r.Urls[0],
}
fields := map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
}
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
}
func TestPhpFpmTimeout_From_Fcgi(t *testing.T) {
// Let OS find an available port
tcp, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "Cannot initialize test server")
defer tcp.Close()
const timeout = 200 * time.Millisecond
go func() {
conn, err := tcp.Accept()
if err != nil {
return // ignore the returned error as we cannot do anything about it anyway
}
defer conn.Close()
// Sleep longer than the timeout
time.Sleep(2 * timeout)
}()
// Now we tested again above server
r := &Phpfpm{
Urls: []string{"fcgi://" + tcp.Addr().String() + "/status"},
Timeout: config.Duration(timeout),
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
start := time.Now()
var acc testutil.Accumulator
require.Error(t, acc.GatherError(r.Gather))
require.Empty(t, acc.GetTelegrafMetrics())
require.GreaterOrEqual(t, time.Since(start), timeout)
}
// TestPhpFpmCrashWithTimeout_From_Fcgi show issue #15175: when timeout is enabled
// and nothing is listening on specified port, a nil pointer was dereferenced.
func TestPhpFpmCrashWithTimeout_From_Fcgi(t *testing.T) {
tcp, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "Cannot initialize test server")
tcpAddress := tcp.Addr().String()
// Yes close the tcp port now. The listenner is only used to find a free
// port and then make it free. This test hope that nothing will re-use the
// port in meantime.
tcp.Close()
const timeout = 200 * time.Millisecond
// Now we tested again above server
r := &Phpfpm{
Urls: []string{"fcgi://" + tcpAddress + "/status"},
Timeout: config.Duration(timeout),
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.Error(t, acc.GatherError(r.Gather))
require.Empty(t, acc.GetTelegrafMetrics())
}
func TestPhpFpmGeneratesMetrics_From_Socket(t *testing.T) {
// Create a socket in /tmp because we always have write permission and if the
// removing of socket fail when system restart /tmp is clear so
// we don't have junk files around
var randomNumber int64
require.NoError(t, binary.Read(rand.Reader, binary.LittleEndian, &randomNumber))
tcp, err := net.Listen("unix", fmt.Sprintf("/tmp/test-fpm%d.sock", randomNumber))
require.NoError(t, err, "Cannot initialize server on port ")
defer tcp.Close()
s := statServer{}
go fcgi.Serve(tcp, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
r := &Phpfpm{
Urls: []string{tcp.Addr().String()},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(r.Gather))
tags := map[string]string{
"pool": "www",
"url": r.Urls[0],
}
fields := map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
}
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
}
func TestPhpFpmGeneratesMetrics_From_Multiple_Sockets_With_Glob(t *testing.T) {
// Create a socket in /tmp because we always have write permission and if the
// removing of socket fail when system restart /tmp is clear so
// we don't have junk files around
var randomNumber int64
require.NoError(t, binary.Read(rand.Reader, binary.LittleEndian, &randomNumber))
socket1 := fmt.Sprintf("/tmp/test-fpm%d.sock", randomNumber)
tcp1, err := net.Listen("unix", socket1)
require.NoError(t, err, "Cannot initialize server on port ")
defer tcp1.Close()
require.NoError(t, binary.Read(rand.Reader, binary.LittleEndian, &randomNumber))
socket2 := fmt.Sprintf("/tmp/test-fpm%d.sock", randomNumber)
tcp2, err := net.Listen("unix", socket2)
require.NoError(t, err, "Cannot initialize server on port ")
defer tcp2.Close()
s := statServer{}
go fcgi.Serve(tcp1, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
go fcgi.Serve(tcp2, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
r := &Phpfpm{
Urls: []string{"/tmp/test-fpm[\\-0-9]*.sock"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc1, acc2 testutil.Accumulator
require.NoError(t, acc1.GatherError(r.Gather))
require.NoError(t, acc2.GatherError(r.Gather))
tags1 := map[string]string{
"pool": "www",
"url": socket1,
}
tags2 := map[string]string{
"pool": "www",
"url": socket2,
}
fields := map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
}
acc1.AssertContainsTaggedFields(t, "phpfpm", fields, tags1)
acc2.AssertContainsTaggedFields(t, "phpfpm", fields, tags2)
}
func TestPhpFpmGeneratesMetrics_From_Socket_Custom_Status_Path(t *testing.T) {
// Create a socket in /tmp because we always have write permission. If the
// removing of socket fail we won't have junk files around. Cuz when system
// restart, it clears out /tmp
var randomNumber int64
require.NoError(t, binary.Read(rand.Reader, binary.LittleEndian, &randomNumber))
tcp, err := net.Listen("unix", fmt.Sprintf("/tmp/test-fpm%d.sock", randomNumber))
require.NoError(t, err, "Cannot initialize server on port ")
defer tcp.Close()
s := statServer{}
go fcgi.Serve(tcp, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
r := &Phpfpm{
Urls: []string{tcp.Addr().String() + ":custom-status-path"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(r.Gather))
tags := map[string]string{
"pool": "www",
"url": r.Urls[0],
}
fields := map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
}
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
}
// When not passing server config, we default to localhost
// We just want to make sure we did request stat from localhost
func TestPhpFpmDefaultGetFromLocalhost(t *testing.T) {
r := &Phpfpm{
Urls: []string{"http://bad.localhost:62001/status"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.ErrorContains(t, acc.GatherError(r.Gather), "/status")
}
func TestPhpFpmGeneratesMetrics_Throw_Error_When_Fpm_Status_Is_Not_Responding(t *testing.T) {
if testing.Short() {
t.Skip("Skipping long test in short mode")
}
r := &Phpfpm{
Urls: []string{"http://aninvalidone"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
err := acc.GatherError(r.Gather)
require.ErrorContains(t, err, `unable to connect to phpfpm status page "http://aninvalidone"`)
require.ErrorContains(t, err, `lookup aninvalidone`)
}
func TestPhpFpmGeneratesMetrics_Throw_Error_When_Socket_Path_Is_Invalid(t *testing.T) {
r := &Phpfpm{
Urls: []string{"/tmp/invalid.sock"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
var acc testutil.Accumulator
require.ErrorContains(t, acc.GatherError(r.Gather), `socket doesn't exist "/tmp/invalid.sock"`)
}
const outputSample = `
pool: www
process manager: dynamic
start time: 11/Oct/2015:23:38:51 +0000
start since: 1991
accepted conn: 3
listen queue: 1
max listen queue: 0
listen queue len: 0
idle processes: 1
active processes: 1
total processes: 2
max active processes: 1
max children reached: 2
slow requests: 1
`
//go:embed testdata/phpfpm.json
var outputSampleJSON []byte
func TestPhpFpmParseJSON_Log_Error_Without_Panic_When_When_JSON_Is_Invalid(t *testing.T) {
// Capture the logging output for checking
logger := &testutil.CaptureLogger{Name: "inputs.phpfpm"}
plugin := &Phpfpm{Log: logger}
require.NoError(t, plugin.Init())
// parse valid JSON without panic and without log output
validJSON := outputSampleJSON
require.NotPanics(t, func() { plugin.parseJSON(bytes.NewReader(validJSON), &testutil.NopAccumulator{}, "") })
require.Empty(t, logger.NMessages())
// parse invalid JSON without panic but with log output
invalidJSON := []byte("X")
require.NotPanics(t, func() { plugin.parseJSON(bytes.NewReader(invalidJSON), &testutil.NopAccumulator{}, "") })
require.Contains(t, logger.Errors(), "E! [inputs.phpfpm] Unable to decode JSON response: invalid character 'X' looking for beginning of value")
}
func TestGatherDespiteUnavailable(t *testing.T) {
// Let OS find an available port
tcp, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "Cannot initialize test server")
defer tcp.Close()
s := statServer{}
go fcgi.Serve(tcp, s) //nolint:errcheck // ignore the returned error as we cannot do anything about it anyway
// Now we tested again above server
r := &Phpfpm{
Urls: []string{"fcgi://" + tcp.Addr().String() + "/status", "/lala"},
Log: &testutil.Logger{},
}
require.NoError(t, r.Init())
expected := []telegraf.Metric{
metric.New(
"phpfpm",
map[string]string{
"pool": "www",
"url": r.Urls[0],
},
map[string]interface{}{
"start_since": int64(1991),
"accepted_conn": int64(3),
"listen_queue": int64(1),
"max_listen_queue": int64(0),
"listen_queue_len": int64(0),
"idle_processes": int64(1),
"active_processes": int64(1),
"total_processes": int64(2),
"max_active_processes": int64(1),
"max_children_reached": int64(2),
"slow_requests": int64(1),
},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.ErrorContains(t, acc.GatherError(r.Gather), "socket doesn't exist")
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
}

View file

@ -0,0 +1,40 @@
# Read metrics of phpfpm, via HTTP status page or socket
[[inputs.phpfpm]]
## An array of addresses to gather stats about. Specify an ip or hostname
## with optional port and path
##
## Plugin can be configured in three modes (either can be used):
## - http: the URL must start with http:// or https://, ie:
## "http://localhost/status"
## "http://192.168.130.1/status?full"
##
## - unixsocket: path to fpm socket, ie:
## "/var/run/php5-fpm.sock"
## or using a custom fpm status path:
## "/var/run/php5-fpm.sock:fpm-custom-status-path"
## glob patterns are also supported:
## "/var/run/php*.sock"
##
## - fcgi: the URL must start with fcgi:// or cgi://, and port must be present, ie:
## "fcgi://10.0.0.12:9000/status"
## "cgi://10.0.10.12:9001/status"
##
## Example of multiple gathering from local socket and remote host
## urls = ["http://192.168.1.20/status", "/tmp/fpm.sock"]
urls = ["http://localhost/status"]
## Format of stats to parse, set to "status" or "json"
## If the user configures the URL to return JSON (e.g.
## http://localhost/status?json), set to JSON. Otherwise, will attempt to
## parse line-by-line. The JSON mode will produce additional metrics.
# format = "status"
## Duration allowed to complete HTTP requests.
# timeout = "5s"
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false

View file

@ -0,0 +1,11 @@
phpfpm,pool=www,url=http://127.0.0.1:44637?full&json accepted_conn=3879i,active_processes=1i,idle_processes=9i,listen_queue=0i,listen_queue_len=0i,max_active_processes=3i,max_children_reached=0i,max_listen_queue=0i,slow_requests=0i,start_since=4901i,total_processes=10i
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status?json&full,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=583i,last_request_cpu=0,last_request_memory=0,request_duration=159i,requests=386i,start_time=1702044927i,state="Running"
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=584i,last_request_cpu=0,last_request_memory=2097152,request_duration=174i,requests=390i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=585i,last_request_cpu=104.93,last_request_memory=2097152,request_duration=9530i,requests=389i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=586i,last_request_cpu=0,last_request_memory=2097152,request_duration=127i,requests=399i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=587i,last_request_cpu=0,last_request_memory=2097152,request_duration=9713i,requests=382i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=588i,last_request_cpu=0,last_request_memory=2097152,request_duration=133i,requests=383i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/fpm-status?json,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=589i,last_request_cpu=0,last_request_memory=2097152,request_duration=154i,requests=381i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/ping,script=-,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=590i,last_request_cpu=0,last_request_memory=2097152,request_duration=108i,requests=397i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=591i,last_request_cpu=110.28,last_request_memory=2097152,request_duration=9068i,requests=381i,start_time=1702044927i,state="Idle"
phpfpm_process,pool=www,request_method=GET,request_uri=/index.php,script=script.php,url=http://127.0.0.1:44637?full&json,user=- content_length=0i,pid=592i,last_request_cpu=64.27,last_request_memory=2097152,request_duration=15559i,requests=391i,start_time=1702044927i,state="Idle"

View file

@ -0,0 +1,168 @@
{
"pool": "www",
"process manager": "static",
"start time": 1702044927,
"start since": 4901,
"accepted conn": 3879,
"listen queue": 0,
"max listen queue": 0,
"listen queue len": 0,
"idle processes": 9,
"active processes": 1,
"total processes": 10,
"max active processes": 3,
"max children reached": 0,
"slow requests": 0,
"processes": [
{
"pid": 583,
"state": "Running",
"start time": 1702044927,
"start since": 4901,
"requests": 386,
"request duration": 159,
"request method": "GET",
"request uri": "/fpm-status?json&full",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 0
},
{
"pid": 584,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 390,
"request duration": 174,
"request method": "GET",
"request uri": "/fpm-status",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 585,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 389,
"request duration": 9530,
"request method": "GET",
"request uri": "/index.php",
"content length": 0,
"user": "-",
"script": "script.php",
"last request cpu": 104.93,
"last request memory": 2097152
},
{
"pid": 586,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 399,
"request duration": 127,
"request method": "GET",
"request uri": "/ping",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 587,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 382,
"request duration": 9713,
"request method": "GET",
"request uri": "/index.php",
"content length": 0,
"user": "-",
"script": "script.php",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 588,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 383,
"request duration": 133,
"request method": "GET",
"request uri": "/ping",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 589,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 381,
"request duration": 154,
"request method": "GET",
"request uri": "/fpm-status?json",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 590,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 397,
"request duration": 108,
"request method": "GET",
"request uri": "/ping",
"content length": 0,
"user": "-",
"script": "-",
"last request cpu": 0,
"last request memory": 2097152
},
{
"pid": 591,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 381,
"request duration": 9068,
"request method": "GET",
"request uri": "/index.php",
"content length": 0,
"user": "-",
"script": "script.php",
"last request cpu": 110.28,
"last request memory": 2097152
},
{
"pid": 592,
"state": "Idle",
"start time": 1702044927,
"start since": 4901,
"requests": 391,
"request duration": 15559,
"request method": "GET",
"request uri": "/index.php",
"content length": 0,
"user": "-",
"script": "script.php",
"last request cpu": 64.27,
"last request memory": 2097152
}
]
}