1// Copyright © 2018 Enrico Stahn <enrico.stahn@gmail.com> 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14// Package phpfpm provides convenient access to PHP-FPM pool data 15package phpfpm 16 17import ( 18 "encoding/json" 19 "fmt" 20 "io/ioutil" 21 "net/url" 22 "regexp" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 fcgiclient "github.com/tomasen/fcgi_client" 29) 30 31// PoolProcessRequestIdle defines a process that is idle. 32const PoolProcessRequestIdle string = "Idle" 33 34// PoolProcessRequestRunning defines a process that is running. 35const PoolProcessRequestRunning string = "Running" 36 37// PoolProcessRequestFinishing defines a process that is about to finish. 38const PoolProcessRequestFinishing string = "Finishing" 39 40// PoolProcessRequestReadingHeaders defines a process that is reading headers. 41const PoolProcessRequestReadingHeaders string = "Reading headers" 42 43// PoolProcessRequestInfo defines a process that is getting request information. Was changed in PHP 7.4 to PoolProcessRequestInfo74 44const PoolProcessRequestInfo string = "Getting request informations" 45const PoolProcessRequestInfo74 string = "Getting request information" 46 47// PoolProcessRequestEnding defines a process that is about to end. 48const PoolProcessRequestEnding string = "Ending" 49 50var log logger 51 52type logger interface { 53 Info(ar ...interface{}) 54 Infof(string, ...interface{}) 55 Debug(ar ...interface{}) 56 Debugf(string, ...interface{}) 57 Error(ar ...interface{}) 58 Errorf(string, ...interface{}) 59} 60 61// PoolManager manages all configured Pools 62type PoolManager struct { 63 Pools []Pool `json:"pools"` 64} 65 66// Pool describes a single PHP-FPM pool that can be reached via a Socket or TCP address 67type Pool struct { 68 // The address of the pool, e.g. tcp://127.0.0.1:9000 or unix:///tmp/php-fpm.sock 69 Address string `json:"-"` 70 ScrapeError error `json:"-"` 71 ScrapeFailures int64 `json:"-"` 72 Name string `json:"pool"` 73 ProcessManager string `json:"process manager"` 74 StartTime timestamp `json:"start time"` 75 StartSince int64 `json:"start since"` 76 AcceptedConnections int64 `json:"accepted conn"` 77 ListenQueue int64 `json:"listen queue"` 78 MaxListenQueue int64 `json:"max listen queue"` 79 ListenQueueLength int64 `json:"listen queue len"` 80 IdleProcesses int64 `json:"idle processes"` 81 ActiveProcesses int64 `json:"active processes"` 82 TotalProcesses int64 `json:"total processes"` 83 MaxActiveProcesses int64 `json:"max active processes"` 84 MaxChildrenReached int64 `json:"max children reached"` 85 SlowRequests int64 `json:"slow requests"` 86 Processes []PoolProcess `json:"processes"` 87} 88 89type requestDuration int64 90 91// PoolProcess describes a single PHP-FPM process. A pool can have multiple processes. 92type PoolProcess struct { 93 PID int64 `json:"pid"` 94 State string `json:"state"` 95 StartTime int64 `json:"start time"` 96 StartSince int64 `json:"start since"` 97 Requests int64 `json:"requests"` 98 RequestDuration requestDuration `json:"request duration"` 99 RequestMethod string `json:"request method"` 100 RequestURI string `json:"request uri"` 101 ContentLength int64 `json:"content length"` 102 User string `json:"user"` 103 Script string `json:"script"` 104 LastRequestCPU float64 `json:"last request cpu"` 105 LastRequestMemory int64 `json:"last request memory"` 106} 107 108// PoolProcessStateCounter holds the calculated metrics for pool processes. 109type PoolProcessStateCounter struct { 110 Running int64 111 Idle int64 112 Finishing int64 113 ReadingHeaders int64 114 Info int64 115 Ending int64 116} 117 118// Add will add a pool to the pool manager based on the given URI. 119func (pm *PoolManager) Add(uri string) Pool { 120 p := Pool{Address: uri} 121 pm.Pools = append(pm.Pools, p) 122 return p 123} 124 125// Update will run the pool.Update() method concurrently on all Pools. 126func (pm *PoolManager) Update() (err error) { 127 wg := &sync.WaitGroup{} 128 129 started := time.Now() 130 131 for idx := range pm.Pools { 132 wg.Add(1) 133 go func(p *Pool) { 134 defer wg.Done() 135 if err := p.Update(); err != nil { 136 log.Error(err) 137 } 138 }(&pm.Pools[idx]) 139 } 140 141 wg.Wait() 142 143 ended := time.Now() 144 145 log.Debugf("Updated %v pool(s) in %v", len(pm.Pools), ended.Sub(started)) 146 147 return nil 148} 149 150// Update will connect to PHP-FPM and retrieve the latest data for the pool. 151func (p *Pool) Update() (err error) { 152 p.ScrapeError = nil 153 154 scheme, address, path, err := parseURL(p.Address) 155 if err != nil { 156 return p.error(err) 157 } 158 159 fcgi, err := fcgiclient.DialTimeout(scheme, address, time.Duration(3)*time.Second) 160 if err != nil { 161 return p.error(err) 162 } 163 164 defer fcgi.Close() 165 166 env := map[string]string{ 167 "SCRIPT_FILENAME": path, 168 "SCRIPT_NAME": path, 169 "SERVER_SOFTWARE": "go / php-fpm_exporter", 170 "REMOTE_ADDR": "127.0.0.1", 171 "QUERY_STRING": "json&full", 172 } 173 174 resp, err := fcgi.Get(env) 175 if err != nil { 176 return p.error(err) 177 } 178 179 defer resp.Body.Close() 180 181 content, err := ioutil.ReadAll(resp.Body) 182 if err != nil { 183 return p.error(err) 184 } 185 186 content = JSONResponseFixer(content) 187 188 log.Debugf("Pool[%v]: %v", p.Address, string(content)) 189 190 if err = json.Unmarshal(content, &p); err != nil { 191 log.Errorf("Pool[%v]: %v", p.Address, string(content)) 192 return p.error(err) 193 } 194 195 return nil 196} 197 198func (p *Pool) error(err error) error { 199 p.ScrapeError = err 200 p.ScrapeFailures++ 201 log.Error(err) 202 return err 203} 204 205// JSONResponseFixer resolves encoding issues with PHP-FPMs JSON response 206func JSONResponseFixer(content []byte) []byte { 207 c := string(content) 208 re := regexp.MustCompile(`(,"request uri":)"(.*?)"(,"content length":)`) 209 matches := re.FindAllStringSubmatch(c, -1) 210 211 for _, match := range matches { 212 requestURI, _ := json.Marshal(match[2]) 213 214 sold := match[0] 215 snew := match[1] + string(requestURI) + match[3] 216 217 c = strings.ReplaceAll(c, sold, snew) 218 } 219 220 return []byte(c) 221} 222 223// CountProcessState return the calculated metrics based on the reported processes. 224func CountProcessState(processes []PoolProcess) (active int64, idle int64, total int64) { 225 for idx := range processes { 226 switch processes[idx].State { 227 case PoolProcessRequestRunning: 228 active++ 229 case PoolProcessRequestIdle: 230 idle++ 231 case PoolProcessRequestEnding: 232 case PoolProcessRequestFinishing: 233 case PoolProcessRequestInfo: 234 case PoolProcessRequestInfo74: 235 case PoolProcessRequestReadingHeaders: 236 active++ 237 default: 238 log.Errorf("Unknown process state '%v'", processes[idx].State) 239 } 240 } 241 242 return active, idle, active + idle 243} 244 245// parseURL creates elements to be passed into fcgiclient.DialTimeout 246func parseURL(rawurl string) (scheme string, address string, path string, err error) { 247 uri, err := url.Parse(rawurl) 248 if err != nil { 249 return uri.Scheme, uri.Host, uri.Path, err 250 } 251 252 scheme = uri.Scheme 253 254 switch uri.Scheme { 255 case "unix": 256 result := strings.Split(uri.Path, ";") 257 address = result[0] 258 if len(result) > 1 { 259 path = result[1] 260 } 261 default: 262 address = uri.Host 263 path = uri.Path 264 } 265 266 return 267} 268 269type timestamp time.Time 270 271// MarshalJSON customise JSON for timestamp 272func (t *timestamp) MarshalJSON() ([]byte, error) { 273 ts := time.Time(*t).Unix() 274 stamp := fmt.Sprint(ts) 275 return []byte(stamp), nil 276} 277 278// UnmarshalJSON customise JSON for timestamp 279func (t *timestamp) UnmarshalJSON(b []byte) error { 280 ts, err := strconv.Atoi(string(b)) 281 if err != nil { 282 return err 283 } 284 *t = timestamp(time.Unix(int64(ts), 0)) 285 return nil 286} 287 288// This is because of bug in php-fpm that can return 'request duration' which can't 289// fit to int64. For details check links: 290// https://bugs.php.net/bug.php?id=62382 291// https://serverfault.com/questions/624977/huge-request-duration-value-for-a-particular-php-script 292func (rd *requestDuration) MarshalJSON() ([]byte, error) { 293 stamp := fmt.Sprint(rd) 294 return []byte(stamp), nil 295} 296 297func (rd *requestDuration) UnmarshalJSON(b []byte) error { 298 rdc, err := strconv.Atoi(string(b)) 299 if err != nil { 300 *rd = 0 301 } else { 302 *rd = requestDuration(rdc) 303 } 304 return nil 305} 306 307// SetLogger configures the used logger 308func SetLogger(logger logger) { 309 log = logger 310} 311