github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-php-fpm/lib/php-fpm.go (about)

     1  //go:build linux
     2  
     3  package mpphpfpm
     4  
     5  import (
     6  	"context"
     7  	"encoding/json"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"net/url"
    14  	"strings"
    15  	"time"
    16  
    17  	mp "github.com/mackerelio/go-mackerel-plugin-helper"
    18  )
    19  
    20  // PhpFpmPlugin mackerel plugin
    21  type PhpFpmPlugin struct {
    22  	URL         string
    23  	Prefix      string
    24  	LabelPrefix string
    25  	Timeout     uint
    26  	Socket      SocketFlag
    27  }
    28  
    29  // SocketFlag represents -socket flag.
    30  type SocketFlag struct {
    31  	u       *url.URL
    32  	Network string
    33  	Address string
    34  }
    35  
    36  func (p *SocketFlag) String() string {
    37  	if p.u == nil {
    38  		return ""
    39  	}
    40  	return p.u.String()
    41  }
    42  
    43  // Set implements flag.Value interface.
    44  func (p *SocketFlag) Set(s string) error {
    45  	*p = SocketFlag{}
    46  	u, err := parseSocketFlag(s)
    47  	if err != nil {
    48  		return err
    49  	}
    50  	switch u.Scheme {
    51  	case "tcp":
    52  		p.Network = "tcp"
    53  		p.Address = u.Host
    54  	case "unix":
    55  		p.Network = "unix"
    56  		p.Address = u.Path
    57  	default:
    58  		return fmt.Errorf("unknown scheme: %s", u.Scheme)
    59  	}
    60  	p.u = u
    61  	return nil
    62  }
    63  
    64  const defaultFCGIPort = "9000"
    65  
    66  func parseSocketFlag(s string) (*url.URL, error) {
    67  	u, err := url.Parse(s)
    68  	if err != nil {
    69  		return parseHostOrErr(s, err)
    70  	}
    71  	switch {
    72  	case u.Scheme == "tcp" && u.Host != "":
    73  		if u.Port() == "" {
    74  			u.Host = net.JoinHostPort(u.Host, defaultFCGIPort)
    75  		}
    76  	case u.Scheme == "unix" && u.Path != "":
    77  		// do nothing
    78  	case u.Scheme == "" && u.Path != "":
    79  		u.Scheme = "unix"
    80  	case u.Scheme == "" && u.Host != "":
    81  		u.Scheme = "tcp"
    82  	default:
    83  		return parseHostOrErr(s, fmt.Errorf("can't parse socket url: %s", s))
    84  	}
    85  	return u, nil
    86  }
    87  
    88  func parseHostOrErr(s string, retErr error) (*url.URL, error) {
    89  	if _, _, err := net.SplitHostPort(s); err != nil {
    90  		return nil, retErr
    91  	}
    92  	// RFC 952 describes that hostname is composed of ASCII characters [0-9a-z-].
    93  	// net.SplitHostPort splits colon separated string, but it don't verify hostname and port is valid.
    94  	// Therefore we should return an error when either hostname or port contains invalid charcters.
    95  	if strings.ContainsAny(s, "/#") {
    96  		return nil, retErr
    97  	}
    98  	return &url.URL{
    99  		Scheme: "tcp",
   100  		Host:   s,
   101  	}, nil
   102  }
   103  
   104  // Transport returns http.RoundTripper corresponding to the flag.
   105  func (p *SocketFlag) Transport() http.RoundTripper {
   106  	switch p.Network {
   107  	case "tcp", "unix":
   108  		return &FastCGITransport{
   109  			Network: p.Network,
   110  			Address: p.Address,
   111  		}
   112  	default:
   113  		return nil // http.DefaultTransport
   114  	}
   115  }
   116  
   117  // PhpFpmStatus struct for PhpFpmPlugin mackerel plugin
   118  type PhpFpmStatus struct {
   119  	Pool               string `json:"pool"`
   120  	ProcessManager     string `json:"process manager"`
   121  	StartTime          uint64 `json:"start time"`
   122  	StartSince         uint64 `json:"start since"`
   123  	AcceptedConn       uint64 `json:"accepted conn"`
   124  	ListenQueue        uint64 `json:"listen queue"`
   125  	MaxListenQueue     uint64 `json:"max listen queue"`
   126  	ListenQueueLen     uint64 `json:"listen queue len"`
   127  	IdleProcesses      uint64 `json:"idle processes"`
   128  	ActiveProcesses    uint64 `json:"active processes"`
   129  	TotalProcesses     uint64 `json:"total processes"`
   130  	MaxActiveProcesses uint64 `json:"max active processes"`
   131  	MaxChildrenReached uint64 `json:"max children reached"`
   132  	SlowRequests       uint64 `json:"slow requests"`
   133  	MemoryPeak         uint64 `json:"memory peak"`
   134  }
   135  
   136  // MetricKeyPrefix interface for PluginWithPrefix
   137  func (p PhpFpmPlugin) MetricKeyPrefix() string {
   138  	return p.Prefix
   139  }
   140  
   141  // GraphDefinition interface for mackerelplugin
   142  func (p PhpFpmPlugin) GraphDefinition() map[string]mp.Graphs {
   143  	return map[string]mp.Graphs{
   144  		"processes": {
   145  			Label: p.LabelPrefix + " Processes",
   146  			Unit:  "integer",
   147  			Metrics: []mp.Metrics{
   148  				{Name: "total_processes", Label: "Total Processes", Diff: false, Type: "uint64"},
   149  				{Name: "active_processes", Label: "Active Processes", Diff: false, Type: "uint64"},
   150  				{Name: "idle_processes", Label: "Idle Processes", Diff: false, Type: "uint64"},
   151  			},
   152  		},
   153  		"max_active_processes": {
   154  			Label: p.LabelPrefix + " Max Active Processes",
   155  			Unit:  "integer",
   156  			Metrics: []mp.Metrics{
   157  				{Name: "max_active_processes", Label: "Max Active Processes", Diff: false, Type: "uint64"},
   158  			},
   159  		},
   160  		"max_children_reached": {
   161  			Label: p.LabelPrefix + " Max Children Reached",
   162  			Unit:  "integer",
   163  			Metrics: []mp.Metrics{
   164  				{Name: "max_children_reached", Label: "Max Children Reached", Diff: false, Type: "uint64"},
   165  			},
   166  		},
   167  		"queue": {
   168  			Label: p.LabelPrefix + " Queue",
   169  			Unit:  "integer",
   170  			Metrics: []mp.Metrics{
   171  				{Name: "listen_queue", Label: "Listen Queue", Diff: false, Type: "uint64"},
   172  				{Name: "listen_queue_len", Label: "Listen Queue Len", Diff: false, Type: "uint64"},
   173  			},
   174  		},
   175  		"max_listen_queue": {
   176  			Label: p.LabelPrefix + " Max Listen Queue",
   177  			Unit:  "integer",
   178  			Metrics: []mp.Metrics{
   179  				{Name: "max_listen_queue", Label: "Max Listen Queue", Diff: false, Type: "uint64"},
   180  			},
   181  		},
   182  		"slow_requests": {
   183  			Label: p.LabelPrefix + " Slow Requests",
   184  			Unit:  "integer",
   185  			Metrics: []mp.Metrics{
   186  				{Name: "slow_requests", Label: "Slow Requests Counter", Diff: false, Type: "uint64"},
   187  				{Name: "slow_requests_delta", Label: "Slow Requests Delta", Diff: true, Type: "uint64"},
   188  			},
   189  		},
   190  		"memory_peak": {
   191  			Label: p.LabelPrefix + " Memory Peak",
   192  			Unit:  "bytes",
   193  			Metrics: []mp.Metrics{
   194  				{Name: "memory_peak", Label: "Memory Peak", Diff: false, Type: "uint64"},
   195  			},
   196  		},
   197  	}
   198  }
   199  
   200  // FetchMetrics interface for mackerelplugin
   201  func (p PhpFpmPlugin) FetchMetrics() (map[string]interface{}, error) {
   202  	status, err := getStatus(p)
   203  	if err != nil {
   204  		return nil, fmt.Errorf("Failed to fetch PHP-FPM metrics: %s", err) // nolint
   205  	}
   206  
   207  	result := map[string]interface{}{
   208  		"total_processes":      status.TotalProcesses,
   209  		"active_processes":     status.ActiveProcesses,
   210  		"idle_processes":       status.IdleProcesses,
   211  		"max_active_processes": status.MaxActiveProcesses,
   212  		"max_children_reached": status.MaxChildrenReached,
   213  		"listen_queue":         status.ListenQueue,
   214  		"listen_queue_len":     status.ListenQueueLen,
   215  		"max_listen_queue":     status.MaxListenQueue,
   216  		"slow_requests":        status.SlowRequests,
   217  		"slow_requests_delta":  status.SlowRequests,
   218  	}
   219  
   220  	if status.MemoryPeak > 0 {
   221  		result["memory_peak"] = status.MemoryPeak
   222  	}
   223  
   224  	return result, nil
   225  }
   226  
   227  func getStatus(p PhpFpmPlugin) (*PhpFpmStatus, error) {
   228  	url := p.URL
   229  	timeout := time.Duration(time.Duration(p.Timeout) * time.Second)
   230  	client := http.Client{
   231  		Timeout:   timeout,
   232  		Transport: p.Socket.Transport(),
   233  	}
   234  
   235  	req, err := http.NewRequest(http.MethodGet, url, nil)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	req.Header.Set("User-Agent", "mackerel-plugin-php-fpm")
   240  	if timeout > 0 {
   241  		ctx, cancel := context.WithTimeout(context.Background(), timeout)
   242  		defer cancel()
   243  		req = req.WithContext(ctx)
   244  	}
   245  
   246  	res, err := client.Do(req)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	defer res.Body.Close()
   251  
   252  	body, err := io.ReadAll(res.Body)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	var status *PhpFpmStatus
   258  	if err := json.Unmarshal(body, &status); err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	return status, nil
   263  }
   264  
   265  // Do the plugin
   266  func Do() {
   267  	optURL := flag.String("url", "http://localhost/status?json", "PHP-FPM status page URL")
   268  	optPrefix := flag.String("metric-key-prefix", "php-fpm", "Metric key prefix")
   269  	optLabelPrefix := flag.String("metric-label-prefix", "PHP-FPM", "Metric label prefix")
   270  	optTimeout := flag.Uint("timeout", 5, "Timeout")
   271  	optTempfile := flag.String("tempfile", "", "Temp file name")
   272  	var socketFlag SocketFlag
   273  	flag.Var(&socketFlag, "socket", "Unix domain socket `path or URL`")
   274  	flag.Parse()
   275  
   276  	p := PhpFpmPlugin{
   277  		URL:         *optURL,
   278  		Prefix:      *optPrefix,
   279  		LabelPrefix: *optLabelPrefix,
   280  		Timeout:     *optTimeout,
   281  		Socket:      socketFlag,
   282  	}
   283  	helper := mp.NewMackerelPlugin(p)
   284  	helper.Tempfile = *optTempfile
   285  
   286  	helper.Run()
   287  }