From 10db3f27ba12a9fdb27b3a77cb3c96765f6dbf36 Mon Sep 17 00:00:00 2001 From: Enrico Stahn Date: Sat, 17 Feb 2018 23:42:58 +1100 Subject: [PATCH] :sparkles: Initial version --- cmd/get.go | 103 ++++++++++++++++++++++++++ cmd/root.go | 104 ++++++++++++++++++++++++++ cmd/server.go | 85 +++++++++++++++++++++ cmd/version.go | 44 +++++++++++ glide.lock | 111 ++++++++++++++++++++++++++++ glide.yaml | 7 ++ main.go | 20 +++++ phpfpm/exporter.go | 143 ++++++++++++++++++++++++++++++++++++ phpfpm/phpfpm.go | 179 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 796 insertions(+) create mode 100644 cmd/get.go create mode 100644 cmd/root.go create mode 100644 cmd/server.go create mode 100644 cmd/version.go create mode 100644 glide.lock create mode 100644 glide.yaml create mode 100644 main.go create mode 100644 phpfpm/exporter.go create mode 100644 phpfpm/phpfpm.go diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 0000000..24b1b56 --- /dev/null +++ b/cmd/get.go @@ -0,0 +1,103 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/davecgh/go-spew/spew" + "github.com/gosuri/uitable" + "fmt" + "time" + "php-fpm_exporter/phpfpm" + "encoding/json" +) + +// Configuration variables +var ( + output string +) + +// getCmd represents the get command +var getCmd = &cobra.Command{ + Use: "get", + Short: "Returns metrics without running as a server", + Long: `"get" fetches metrics from php-fpm. Multiple addresses can be specified as follows: + +* php-fpm_exporter get --phpfpm.scrape-uri 127.0.0.1:9000 --phpfpm.scrape-uri 127.0.0.1:9001 [...] +* php-fpm_exporter get --phpfpm.scrape-uri 127.0.0.1:9000,127.0.0.1:9001,[...] +`, + Run: func(cmd *cobra.Command, args []string) { + pm := phpfpm.PoolManager{} + + for _, uri := range scrapeURIs { + pm.Add(uri) + } + + pm.Update() + + switch output { + case "json": + content, err := json.Marshal(pm) + if err != nil { + log.Fatal("Cannot encode to JSON ", err) + } + fmt.Print(string(content)) + case "text": + table := uitable.New() + table.MaxColWidth = 80 + table.Wrap = true + + pools := pm.Pools() + + for _, pool := range pools { + table.AddRow("Address:", pool.Address) + table.AddRow("Pool:", pool.Name) + table.AddRow("Start time:", time.Time(pool.StartTime).Format(time.RFC1123Z)) + table.AddRow("Start since:", pool.StartSince) + table.AddRow("Accepted connections:", pool.AcceptedConnections) + table.AddRow("Listen Queue:", pool.ListenQueue) + table.AddRow("Max Listen Queue:", pool.MaxListenQueue) + table.AddRow("Listen Queue Length:", pool.ListenQueueLength) + table.AddRow("Idle Processes:", pool.IdleProcesses) + table.AddRow("Active Processes:", pool.ActiveProcesses) + table.AddRow("Total Processes:", pool.TotalProcesses) + table.AddRow("Max active processes:", pool.MaxActiveProcesses) + table.AddRow("Max children reached:", pool.MaxChildrenReached) + table.AddRow("Slow requests:", pool.SlowRequests) + table.AddRow("") + } + + fmt.Println(table) + case "spew": + spew.Dump(pm) + default: + log.Error("Output format not valid.") + } + }, +} + +func init() { + RootCmd.AddCommand(getCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // getCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + getCmd.Flags().StringSliceVar(&scrapeURIs, "phpfpm.scrape-uri", []string{"tcp://127.0.0.1:9000/status"},"FastCGI address, e.g. unix:///tmp/php.sock;/status or tcp://127.0.0.1:9000/status") + getCmd.Flags().StringVar(&output, "out", "text","Output format. One of: text, json, spew") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9d17a2c --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,104 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/sirupsen/logrus" + "php-fpm_exporter/phpfpm" +) + +var log = logrus.New() + +var ( + cfgFile string + logLevel string +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "php-fpm_exporter", + Short: "Exports php-fpm metrics for prometheus", + Long: `php-fpm_exporter exports prometheus compatible metrics from php-fpm.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig, initLogger) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.php-fpm_exporter.yaml)") + RootCmd.PersistentFlags().StringVar(&logLevel, "log.level", "error", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".php-fpm_exporter" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".php-fpm_exporter") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} + +// initLogger configures the log level +func initLogger() { + phpfpm.SetLogger(log) + + lvl, err := logrus.ParseLevel(logLevel) + if err != nil { + lvl = logrus.InfoLevel + log.Fatalf("Could not set log level to '%v'.", logLevel) + } + + log.SetLevel(lvl) +} \ No newline at end of file diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..cedabfd --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,85 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/prometheus/client_golang/prometheus" + "net/http" + "github.com/prometheus/client_golang/prometheus/promhttp" + "php-fpm_exporter/phpfpm" +) + +// Configuration variables +var ( + listeningAddress string + metricsEndpoint string + scrapeURIs []string + customLabelNames []string + customLabelValues []string +) + +// serverCmd represents the server command +var serverCmd = &cobra.Command{ + Use: "server", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + log.Infof("Starting server on %v with path %v", listeningAddress, metricsEndpoint) + + pm := phpfpm.PoolManager{} + + for _, uri := range scrapeURIs { + pm.Add(uri) + } + + exporter := phpfpm.NewExporter(pm) + prometheus.MustRegister(exporter) + + http.Handle(metricsEndpoint, promhttp.Handler()) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + Apache Exporter + +

Apache Exporter

+

Metrics

+ + `)) + }) + log.Fatal(http.ListenAndServe(listeningAddress, nil)) + }, +} + +func init() { + RootCmd.AddCommand(serverCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // serverCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + serverCmd.Flags().StringVar(&listeningAddress, "web.listen-address", ":9253", "Address on which to expose metrics and web interface.") + serverCmd.Flags().StringVar(&metricsEndpoint, "web.telemetry-path", "/metrics", "Path under which to expose metrics.") + serverCmd.Flags().StringSliceVar(&scrapeURIs, "phpfpm.scrape-uri", []string{"tcp://127.0.0.1:9000/status"},"FastCGI address, e.g. unix:///tmp/php.sock;/status or tcp://127.0.0.1:9000/status") + serverCmd.Flags().StringSliceVar(&customLabelNames, "phpfpm.label-name", []string{},"Name of the custom label that will be inserted.") + serverCmd.Flags().StringSliceVar(&customLabelValues, "phpfpm.label-value", []string{},"Value of the custom label that will be inserted.") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..ad1e652 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,44 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of php-fpm_exporter", + Long: `All software has versions. This is php-fpm_exporter's'`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("php-fpm_exporter v0.9 -- HEAD") + }, +} + +func init() { + RootCmd.AddCommand(versionCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // versionCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..6d475af --- /dev/null +++ b/glide.lock @@ -0,0 +1,111 @@ +hash: 41dcf6f131327ca4b18788f48b530080ea79b432b982a1a8d3114b3a2d765c09 +updated: 2018-02-15T13:47:57.155132+11:00 +imports: +- name: github.com/aws/aws-sdk-go + version: 31d8a71e731ad4df5221f6ba3372c461b56a5eda +- name: github.com/beorn7/perks + version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 + subpackages: + - quantile +- name: github.com/bmuller/arrow + version: 57b28416c629f234bd2291d3ba7b19b9a064fd37 +- name: github.com/davecgh/go-spew + version: 87df7c60d5820d0f8ae11afede5aa52325c09717 + subpackages: + - spew +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 +- name: github.com/golang/protobuf + version: bbd03ef6da3a115852eaf24c8a1c46aeb39aa175 + subpackages: + - proto +- name: github.com/gorilla/mux + version: c0091a029979286890368b4c7b301261e448e242 +- name: github.com/gosuri/uitable + version: 36ee7e946282a3fb1cfecd476ddc9b35d8847e42 +- name: github.com/hashicorp/hcl + version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/magiconair/properties + version: 51463bfca2576e06c62a8504b5c0f06d61312647 +- name: github.com/matttproud/golang_protobuf_extensions + version: c12348ce28de40eed0136aa2b644d0ee0650e56c + subpackages: + - pbutil +- name: github.com/mitchellh/go-homedir + version: b8bc1bf767474819792c23f32d8286a45736f1c6 +- name: github.com/mitchellh/mapstructure + version: d0303fe809921458f417bcf828397a65db30a7e4 +- name: github.com/newrelic/go-agent + version: f5bce3387232559bcbe6a5f8227c4bf508dac1ba +- name: github.com/pelletier/go-buffruneio + version: c37440a7cf42ac63b919c752ca73a85067e05992 +- name: github.com/pelletier/go-toml + version: fe7536c3dee2596cdd23ee9976a17c22bdaae286 +- name: github.com/prometheus/client_golang + version: a40133b69fbd73ee655606a9bf5f8b9b9bf758dd + subpackages: + - prometheus +- name: github.com/prometheus/client_model + version: 99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c + subpackages: + - go +- name: github.com/prometheus/common + version: 89604d197083d4781071d3c65855d24ecfb0a563 + subpackages: + - expfmt + - internal/bitbucket.org/ww/goautoneg + - model +- name: github.com/prometheus/procfs + version: 282c8707aa210456a825798969cc27edda34992a + subpackages: + - internal/util + - nfs + - xfs +- name: github.com/sirupsen/logrus + version: 8c0189d9f6bbf301e5d055d34268156b317016af +- name: github.com/speps/go-hashids + version: c6ced3330f133cec79e144fb11b2e4c5154059ae +- name: github.com/spf13/afero + version: 9be650865eab0c12963d8753212f4f9c66cdcf12 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 +- name: github.com/spf13/cobra + version: 8f5946caaeeff40a98d67f60c25e89c3525038a3 +- name: github.com/spf13/jwalterweatherman + version: 0efa5202c04663c757d84f90f5219c1250baf94f +- name: github.com/spf13/pflag + version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 +- name: github.com/spf13/viper + version: a1ecfa6a20bd4ef9e9caded262ee1b1b26847675 +- name: github.com/tomasen/fcgi_client + version: d32b71631a94113ee01855724d1038485a396b6f +- name: golang.org/x/crypto + version: 650f4a345ab4e5b245a3034b110ebc7299e68186 + subpackages: + - ssh/terminal +- name: golang.org/x/sys + version: 37707fdb30a5b38865cfb95e5aab41707daec7fd + subpackages: + - unix + - windows +- name: golang.org/x/text + version: 9e2f80a6ba7ed4ba13e0cd4b1f094bf916875735 + subpackages: + - transform + - unicode/norm +- name: gopkg.in/yaml.v2 + version: a83829b6f1293c91addabc89d0571c246397bbf4 +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..e714553 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,7 @@ +package: php-fpm_exporter +import: +- package: github.com/gosuri/uitable +- package: github.com/sirupsen/logrus +- package: github.com/mitchellh/go-homedir +- package: github.com/spf13/cobra +- package: github.com/spf13/viper diff --git a/main.go b/main.go new file mode 100644 index 0000000..d9cda93 --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import "php-fpm_exporter/cmd" + +func main() { + cmd.Execute() +} diff --git a/phpfpm/exporter.go b/phpfpm/exporter.go new file mode 100644 index 0000000..4a783ed --- /dev/null +++ b/phpfpm/exporter.go @@ -0,0 +1,143 @@ +package phpfpm + +import ( + "sync" + "github.com/prometheus/client_golang/prometheus" + "net/http" +) + +const ( + namespace = "phpfpm" +) + +type Exporter struct { + PoolManager PoolManager + mutex sync.Mutex + client *http.Client + + startSince *prometheus.Desc + acceptedConnections *prometheus.Desc + listenQueue *prometheus.Desc + maxListenQueue *prometheus.Desc + listenQueueLength *prometheus.Desc + idleProcesses *prometheus.Desc + activeProcesses *prometheus.Desc + totalProcesses *prometheus.Desc + maxActiveProcesses *prometheus.Desc + maxChildrenReached *prometheus.Desc + slowRequests *prometheus.Desc +} + +func NewExporter(pm PoolManager) *Exporter { + return &Exporter{ + PoolManager: pm, + + startSince: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "start_since"), + "The number of seconds since FPM has started.", + []string{"pool"}, + nil), + + acceptedConnections: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "accepted_connections"), + "The number of requests accepted by the pool.", + []string{"pool"}, + nil), + + listenQueue: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "listen_queue"), + "The number of requests in the queue of pending connections.", + []string{"pool"}, + nil), + + maxListenQueue: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "max_listen_queue"), + "The maximum number of requests in the queue of pending connections since FPM has started.", + []string{"pool"}, + nil), + + listenQueueLength: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "listen_queue_length"), + "The size of the socket queue of pending connections.", + []string{"pool"}, + nil), + + idleProcesses: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "idle_processes"), + "The number of idle processes.", + []string{"pool"}, + nil), + + activeProcesses: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "active_processes"), + "The number of active processes.", + []string{"pool"}, + nil), + + totalProcesses: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "total_processes"), + "The number of idle + active processes.", + []string{"pool"}, + nil), + + maxActiveProcesses: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "max_active_processes"), + "The maximum number of active processes since FPM has started.", + []string{"pool"}, + nil), + + maxChildrenReached: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "max_children_reached"), + "The number of times, the process limit has been reached, when pm tries to start more children (works only for pm 'dynamic' and 'ondemand').", + []string{"pool"}, + nil), + + slowRequests: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "slow_requests"), + "The number of requests that exceeded your 'request_slowlog_timeout' value.", + []string{"pool"}, + nil), + } +} + +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.mutex.Lock() + defer e.mutex.Unlock() + + e.PoolManager.Update() + + for _, pool := range e.PoolManager.Pools() { + ch <- prometheus.MustNewConstMetric(e.startSince, prometheus.CounterValue, float64(pool.AcceptedConnections), pool.Name) + ch <- prometheus.MustNewConstMetric(e.acceptedConnections, prometheus.CounterValue, float64(pool.StartSince), pool.Name) + ch <- prometheus.MustNewConstMetric(e.listenQueue, prometheus.GaugeValue, float64(pool.ListenQueue), pool.Name) + ch <- prometheus.MustNewConstMetric(e.maxListenQueue, prometheus.CounterValue, float64(pool.MaxListenQueue), pool.Name) + ch <- prometheus.MustNewConstMetric(e.listenQueueLength, prometheus.GaugeValue, float64(pool.ListenQueueLength), pool.Name) + ch <- prometheus.MustNewConstMetric(e.idleProcesses, prometheus.GaugeValue, float64(pool.IdleProcesses), pool.Name) + ch <- prometheus.MustNewConstMetric(e.activeProcesses, prometheus.GaugeValue, float64(pool.ActiveProcesses), pool.Name) + ch <- prometheus.MustNewConstMetric(e.totalProcesses, prometheus.GaugeValue, float64(pool.TotalProcesses), pool.Name) + ch <- prometheus.MustNewConstMetric(e.maxActiveProcesses, prometheus.CounterValue, float64(pool.MaxActiveProcesses), pool.Name) + ch <- prometheus.MustNewConstMetric(e.maxChildrenReached, prometheus.CounterValue, float64(pool.MaxChildrenReached), pool.Name) + ch <- prometheus.MustNewConstMetric(e.slowRequests, prometheus.CounterValue, float64(pool.SlowRequests), pool.Name) + } + + //if err := e.collect(ch); err != nil { + // log.Errorf("Error scraping apache: %s", err) + // e.scrapeFailures.Inc() + // e.scrapeFailures.Collect(ch) + //} + return +} + +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- e.startSince + ch <- e.acceptedConnections + ch <- e.listenQueue + ch <- e.maxListenQueue + ch <- e.listenQueueLength + ch <- e.idleProcesses + ch <- e.activeProcesses + ch <- e.totalProcesses + ch <- e.maxActiveProcesses + ch <- e.maxChildrenReached + ch <- e.slowRequests +} diff --git a/phpfpm/phpfpm.go b/phpfpm/phpfpm.go new file mode 100644 index 0000000..4e20a56 --- /dev/null +++ b/phpfpm/phpfpm.go @@ -0,0 +1,179 @@ +// Copyright © 2018 Enrico Stahn +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package phpfpm + +import ( + "net/url" + "github.com/tomasen/fcgi_client" + "io/ioutil" + "encoding/json" + "time" + "fmt" + "strconv" + "sync" +) + +var log logger + +type logger interface { + Debugf(string, ...interface{}) + Error(ar ...interface{}) +} + +type PoolManager struct { + pools []Pool `json:"pools"` +} + +type Pool struct { + // The address of the pool, e.g. tcp://127.0.0.1:9000 or unix:///tmp/php-fpm.sock + Address string + CollectionError error + + Name string `json:"pool"` + ProcessManager string `json:"process manager"` + StartTime Timestamp `json:"start time"` + StartSince int `json:"start since"` + AcceptedConnections int `json:"accepted conn"` + ListenQueue int `json:"listen queue"` + MaxListenQueue int `json:"max listen queue"` + ListenQueueLength 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 []PoolProcess `json:"processes"` +} + +type PoolProcess 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 float32 `json:"last request cpu"` + LastRequestMemory int `json:"last request memory"` +} + +func (pm *PoolManager) Add(uri string) Pool { + p := Pool{Address: uri} + pm.pools = append(pm.pools, p) + return p +} + +func (pm *PoolManager) Update() (err error) { + wg := &sync.WaitGroup{} + + started := time.Now() + + for idx, _ := range pm.pools { + wg.Add(1) + go func(p *Pool) { + defer wg.Done() + p.Update() + }(&pm.pools[idx]) + } + + wg.Wait() + + ended := time.Now() + + log.Debugf("Updated %v pool(s) in %v", len(pm.pools), ended.Sub(started)) + + return nil +} + +func (pm *PoolManager) Pools() []Pool { + return pm.pools +} + +// Implement custom Marshaler due to "pools" being unexported +func (pm PoolManager) MarshalJSON() ([]byte, error) { + return json.Marshal(struct{Pools []Pool `json:"pools"` }{Pools: pm.pools}) +} + +func (p *Pool) Update() (err error) { + p.CollectionError = nil + + env := make(map[string]string) + env["SCRIPT_FILENAME"] = "/status" + env["SCRIPT_NAME"] = "/status" + env["SERVER_SOFTWARE"] = "go / php-fpm_exporter " + env["REMOTE_ADDR"] = "127.0.0.1" + env["QUERY_STRING"] = "json&full" + + uri, err := url.Parse(p.Address) + if err != nil { + return p.error(err) + } + + fcgi, err := fcgiclient.DialTimeout(uri.Scheme, uri.Hostname()+":"+uri.Port(), time.Duration(3)*time.Second) + if err != nil { + return p.error(err) + } + + resp, err := fcgi.Get(env) + if err != nil { + return p.error(err) + } + + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return p.error(err) + } + + fcgi.Close() + + log.Debugf("Pool[", p.Address, "]:", string(content)) + + if err = json.Unmarshal(content, &p); err != nil { + return p.error(err) + } + + return nil +} + +func (p *Pool) error(err error) (error) { + p.CollectionError = err + log.Error(err) + return err +} + +type Timestamp time.Time + +func (t *Timestamp) MarshalJSON() ([]byte, error) { + ts := time.Time(*t).Unix() + stamp := fmt.Sprint(ts) + return []byte(stamp), nil +} + +func (t *Timestamp) UnmarshalJSON(b []byte) error { + ts, err := strconv.Atoi(string(b)) + if err != nil { + return err + } + *t = Timestamp(time.Unix(int64(ts), 0)) + return nil +} + +func SetLogger(logger logger){ + log = logger +} \ No newline at end of file