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 }