347 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
| package phpfpm
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	"github.com/influxdata/telegraf"
 | |
| 	"github.com/influxdata/telegraf/internal"
 | |
| 	"github.com/influxdata/telegraf/internal/globpath"
 | |
| 	"github.com/influxdata/telegraf/internal/tls"
 | |
| 	"github.com/influxdata/telegraf/plugins/inputs"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	PF_POOL                 = "pool"
 | |
| 	PF_PROCESS_MANAGER      = "process manager"
 | |
| 	PF_START_SINCE          = "start since"
 | |
| 	PF_ACCEPTED_CONN        = "accepted conn"
 | |
| 	PF_LISTEN_QUEUE         = "listen queue"
 | |
| 	PF_MAX_LISTEN_QUEUE     = "max listen queue"
 | |
| 	PF_LISTEN_QUEUE_LEN     = "listen queue len"
 | |
| 	PF_IDLE_PROCESSES       = "idle processes"
 | |
| 	PF_ACTIVE_PROCESSES     = "active processes"
 | |
| 	PF_TOTAL_PROCESSES      = "total processes"
 | |
| 	PF_MAX_ACTIVE_PROCESSES = "max active processes"
 | |
| 	PF_MAX_CHILDREN_REACHED = "max children reached"
 | |
| 	PF_SLOW_REQUESTS        = "slow requests"
 | |
| )
 | |
| 
 | |
| type metric map[string]int64
 | |
| type poolStat map[string]metric
 | |
| 
 | |
| type phpfpm struct {
 | |
| 	Urls    []string
 | |
| 	Timeout internal.Duration
 | |
| 	tls.ClientConfig
 | |
| 
 | |
| 	client *http.Client
 | |
| }
 | |
| 
 | |
| var sampleConfig = `
 | |
|   ## 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"
 | |
|   ##
 | |
|   ##   - 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"]
 | |
| 
 | |
|   ## 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
 | |
| `
 | |
| 
 | |
| func (r *phpfpm) SampleConfig() string {
 | |
| 	return sampleConfig
 | |
| }
 | |
| 
 | |
| func (r *phpfpm) Description() string {
 | |
| 	return "Read metrics of phpfpm, via HTTP status page or socket"
 | |
| }
 | |
| 
 | |
| // Reads stats from all configured servers accumulates stats.
 | |
| // Returns one of the errors encountered while gather stats (if any).
 | |
| func (g *phpfpm) Gather(acc telegraf.Accumulator) error {
 | |
| 	if len(g.Urls) == 0 {
 | |
| 		return g.gatherServer("http://127.0.0.1/status", acc)
 | |
| 	}
 | |
| 
 | |
| 	var wg sync.WaitGroup
 | |
| 
 | |
| 	urls, err := expandUrls(g.Urls)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	for _, serv := range urls {
 | |
| 		wg.Add(1)
 | |
| 		go func(serv string) {
 | |
| 			defer wg.Done()
 | |
| 			acc.AddError(g.gatherServer(serv, acc))
 | |
| 		}(serv)
 | |
| 	}
 | |
| 
 | |
| 	wg.Wait()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Request status page to get stat raw data and import it
 | |
| func (g *phpfpm) gatherServer(addr string, acc telegraf.Accumulator) error {
 | |
| 	if g.client == nil {
 | |
| 		tlsCfg, err := g.ClientConfig.TLSConfig()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		tr := &http.Transport{
 | |
| 			TLSClientConfig: tlsCfg,
 | |
| 		}
 | |
| 		g.client = &http.Client{
 | |
| 			Transport: tr,
 | |
| 			Timeout:   g.Timeout.Duration,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
 | |
| 		return g.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 '%s': %s", addr, err)
 | |
| 		}
 | |
| 		socketAddr := strings.Split(u.Host, ":")
 | |
| 		fcgiIp := socketAddr[0]
 | |
| 		fcgiPort, _ := strconv.Atoi(socketAddr[1])
 | |
| 		fcgi, err = newFcgiClient(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("unix", socketPath)
 | |
| 	}
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return g.gatherFcgi(fcgi, statusPath, acc, addr)
 | |
| }
 | |
| 
 | |
| // Gather stat using fcgi protocol
 | |
| func (g *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 {
 | |
| 		importMetric(bytes.NewReader(fpmOutput), acc, addr)
 | |
| 		return nil
 | |
| 	} else {
 | |
| 		return fmt.Errorf("Unable parse phpfpm status. Error: %v %v", string(fpmErr), err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Gather stat using http protocol
 | |
| func (g *phpfpm) gatherHttp(addr string, acc telegraf.Accumulator) error {
 | |
| 	u, err := url.Parse(addr)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("Unable parse server address '%s': %s", addr, err)
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", fmt.Sprintf("%s://%s%s", u.Scheme,
 | |
| 		u.Host, u.Path), nil)
 | |
| 	res, err := g.client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("Unable to connect to phpfpm status page '%s': %v",
 | |
| 			addr, err)
 | |
| 	}
 | |
| 	defer res.Body.Close()
 | |
| 
 | |
| 	if res.StatusCode != 200 {
 | |
| 		return fmt.Errorf("Unable to get valid stat result from '%s': %v",
 | |
| 			addr, err)
 | |
| 	}
 | |
| 
 | |
| 	importMetric(res.Body, acc, addr)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Import stat data into Telegraf system
 | |
| func importMetric(r io.Reader, acc telegraf.Accumulator, addr string) (poolStat, error) {
 | |
| 	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 == PF_POOL {
 | |
| 			currentPool = strings.Trim(keyvalue[1], " ")
 | |
| 			stats[currentPool] = make(metric)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Start to parse metric for current pool
 | |
| 		switch fieldName {
 | |
| 		case PF_START_SINCE,
 | |
| 			PF_ACCEPTED_CONN,
 | |
| 			PF_LISTEN_QUEUE,
 | |
| 			PF_MAX_LISTEN_QUEUE,
 | |
| 			PF_LISTEN_QUEUE_LEN,
 | |
| 			PF_IDLE_PROCESSES,
 | |
| 			PF_ACTIVE_PROCESSES,
 | |
| 			PF_TOTAL_PROCESSES,
 | |
| 			PF_MAX_ACTIVE_PROCESSES,
 | |
| 			PF_MAX_CHILDREN_REACHED,
 | |
| 			PF_SLOW_REQUESTS:
 | |
| 			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.Replace(k, " ", "_", -1)] = v
 | |
| 		}
 | |
| 		acc.AddFields("phpfpm", fields, tags)
 | |
| 	}
 | |
| 
 | |
| 	return stats, nil
 | |
| }
 | |
| 
 | |
| func expandUrls(urls []string) ([]string, error) {
 | |
| 	addrs := make([]string, 0, len(urls))
 | |
| 	for _, url := range urls {
 | |
| 		if isNetworkURL(url) {
 | |
| 			addrs = append(addrs, url)
 | |
| 			continue
 | |
| 		}
 | |
| 		paths, err := globUnixSocket(url)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		addrs = append(addrs, paths...)
 | |
| 	}
 | |
| 	return addrs, nil
 | |
| }
 | |
| 
 | |
| func globUnixSocket(url string) ([]string, error) {
 | |
| 	pattern, status := unixSocketPaths(url)
 | |
| 	glob, err := globpath.Compile(pattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not compile glob %q: %v", pattern, err)
 | |
| 	}
 | |
| 	paths := glob.Match()
 | |
| 	if len(paths) == 0 {
 | |
| 		if _, err := os.Stat(paths[0]); err != nil {
 | |
| 			if os.IsNotExist(err) {
 | |
| 				return nil, fmt.Errorf("Socket doesn't exist  '%s': %s", pattern, err)
 | |
| 			}
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	addrs := make([]string, 0, len(paths))
 | |
| 
 | |
| 	for _, path := range paths {
 | |
| 		if status != "" {
 | |
| 			status = fmt.Sprintf(":%s", status)
 | |
| 		}
 | |
| 		addrs = append(addrs, fmt.Sprintf("%s%s", path, status))
 | |
| 	}
 | |
| 
 | |
| 	return addrs, nil
 | |
| }
 | |
| 
 | |
| func unixSocketPaths(addr string) (string, string) {
 | |
| 	var 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{}
 | |
| 	})
 | |
| }
 |