Skip to content

Commit

Permalink
fix(inputs.php-fpm): Parse JSON output
Browse files Browse the repository at this point in the history
fixes: #14421
  • Loading branch information
powersj committed Dec 11, 2023
1 parent 2b731c2 commit 0c0ea62
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 6 deletions.
6 changes: 6 additions & 0 deletions plugins/inputs/phpfpm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## 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, set to JSON. Otherwise,
## will attempt to parse line-by-line. The JSON mode will produce additonal
## metrics.
# format = "status"

## Duration allowed to complete HTTP requests.
# timeout = "5s"

Expand Down
112 changes: 106 additions & 6 deletions plugins/inputs/phpfpm/phpfpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"bufio"
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -40,15 +41,49 @@ const (
PfSlowRequests = "slow requests"
)

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 metric map[string]int64
type poolStat map[string]metric

type phpfpm struct {
Urls []string
Timeout config.Duration
tls.ClientConfig
Format string `toml:"format"`
Timeout config.Duration `toml:"timeout"`
Urls []string `toml:"urls"`

tls.ClientConfig
client *http.Client
log telegraf.Logger
}

func (*phpfpm) SampleConfig() string {
Expand All @@ -61,6 +96,15 @@ func (p *phpfpm) Init() error {
return err
}

switch p.Format {
case "":
p.Format = "status"
case "status", "json":
// both valid
default:
return fmt.Errorf("invalid mode: %s", p.Format)
}

p.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
Expand Down Expand Up @@ -158,7 +202,7 @@ func (p *phpfpm) gatherFcgi(fcgi *conn, statusPath string, acc telegraf.Accumula
}, "/"+statusPath)

if len(fpmErr) == 0 && err == nil {
importMetric(bytes.NewReader(fpmOutput), acc, addr)
p.importMetric(bytes.NewReader(fpmOutput), acc, addr)
return nil
}
return fmt.Errorf("unable parse phpfpm status, error: %s; %w", string(fpmErr), err)
Expand Down Expand Up @@ -186,12 +230,20 @@ func (p *phpfpm) gatherHTTP(addr string, acc telegraf.Accumulator) error {
return fmt.Errorf("unable to get valid stat result from %q: %w", addr, err)
}

importMetric(res.Body, acc, addr)
p.importMetric(res.Body, acc, addr)
return nil
}

// Import stat data into Telegraf system
func importMetric(r io.Reader, acc telegraf.Accumulator, addr string) {
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

Expand Down Expand Up @@ -245,6 +297,54 @@ func importMetric(r io.Reader, acc telegraf.Accumulator, addr string) {
}
}

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
}

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)

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{
"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)
}
}

func expandUrls(urls []string) ([]string, error) {
addrs := make([]string, 0, len(urls))
for _, address := range urls {
Expand Down
196 changes: 196 additions & 0 deletions plugins/inputs/phpfpm/phpfpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ func TestPhpFpmGeneratesMetrics_From_Http(t *testing.T) {
acc.AssertContainsTaggedFields(t, "phpfpm", fields, tags)
}

func TestPhpFpmGeneratesJSONMetrics_From_Http(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "", r.URL.Query().Get("test"))
require.Equal(t, "", r.URL.Query().Get("json"))
w.Header().Set("Content-Type", "text/json")
w.Header().Set("Content-Length", strconv.Itoa(len(outputSampelJSON)))
_, err := fmt.Fprint(w, outputSampelJSON)
require.NoError(t, err)
}))
defer ts.Close()

url := ts.URL + "?full&json"
r := &phpfpm{
Urls: []string{url},
Format: "json",
log: testutil.Logger{},
}
require.NoError(t, r.Init())

var acc testutil.Accumulator
require.NoError(t, acc.GatherError(r.Gather))

require.Len(t, acc.Metrics, 11)
}

func TestPhpFpmGeneratesMetrics_From_Fcgi(t *testing.T) {
// Let OS find an available port
tcp, err := net.Listen("tcp", "127.0.0.1:0")
Expand Down Expand Up @@ -328,3 +353,174 @@ max active processes: 1
max children reached: 2
slow requests: 1
`

const outputSampelJSON = `
{
"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
}
]
}
`
6 changes: 6 additions & 0 deletions plugins/inputs/phpfpm/sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
## 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, set to JSON. Otherwise,
## will attempt to parse line-by-line. The JSON mode will produce additonal
## metrics.
# format = "status"

## Duration allowed to complete HTTP requests.
# timeout = "5s"

Expand Down

0 comments on commit 0c0ea62

Please sign in to comment.