Skip to content

Commit

Permalink
Merge branch 'dev' into 'master'
Browse files Browse the repository at this point in the history
Implement OPcache metrics

See merge request crosscone/tool/opcache-exporter!1
  • Loading branch information
floodcode committed Dec 4, 2019
2 parents a2c87cc + d81f3cb commit e89f1b2
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/opcache_exporter
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.PHONY: build

build:
go build -o opcache_exporter ./cmd/exporter
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ This is a simple server that scrapes OPcache status and exports it via HTTP for
make build
```

### Running

```
$ opcache_exporter [<flags>]
Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
--web.listen-address=":9101"
Address to listen on for web interface and telemetry.
--web.telemetry-path="/metrics"
Path under which to expose metrics.
--opcache.fcgi-uri="127.0.0.1:9000"
Connection string to FastCGI server.
--opcache.script-path="" Path to PHP script which echoes json-encoded OPcache status
--opcache.script-dir="" Path to directory where temporary PHP file will be created
```

## License

The MIT License, see [LICENSE](/LICENSE)
174 changes: 174 additions & 0 deletions cmd/exporter/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package main

import (
"encoding/json"
"errors"
"io/ioutil"
"sync"

"github.com/prometheus/client_golang/prometheus"

fcgiclient "github.com/tomasen/fcgi_client"
)

const (
namespace = "opcache"
)

var (
enabledDesc = newMetric("enabled", "Is OPcache enabled.")
cacheFullDesc = newMetric("cache_full", "Is OPcache full.")
restartPendingDesc = newMetric("restart_pending", "Is restart pending.")
restartInProgressDesc = newMetric("restart_in_progress", "Is restart in progress.")

memoryUsageUsedMemoryDesc = newMetric("memory_usage_used_memory", "OPcache used memory.")
memoryUsageFreeMemoryDesc = newMetric("memory_usage_free_memory", "OPcache free memory.")
memoryUsageWastedMemoryDesc = newMetric("memory_usage_wasted_memory", "OPcache wasted memory.")
memoryUsageCurrentWastedPercentageDesc = newMetric("memory_usage_current_wasted_percentage", "OPcache current wasted percentage.")

internedStringsUsageBufferSizeDesc = newMetric("interned_strings_usage_buffer_size", "OPcache interned string buffer size.")
internedStringsUsageUsedMemoryDesc = newMetric("interned_strings_usage_used_memory", "OPcache interned string used memory.")
internedStringsUsageUsedFreeMemory = newMetric("interned_strings_usage_free_memory", "OPcache interned string free memory.")
internedStringsUsageUsedNumerOfStrings = newMetric("interned_strings_usage_number_of_strings", "OPcache interned string number of strings.")

statisticsNumCachedScripts = newMetric("statistics_num_cached_scripts", "OPcache statistics, number of cached scripts.")
statisticsNumCachedKeys = newMetric("statistics_num_cached_keys", "OPcache statistics, number of cached keys.")
statisticsMaxCachedKeys = newMetric("statistics_max_cached_keys", "OPcache statistics, max cached keys.")
statisticsHits = newMetric("statistics_hits", "OPcache statistics, hits.")
statisticsStartTime = newMetric("statistics_start_time", "OPcache statistics, start time.")
statisticsLastRestartTime = newMetric("statistics_last_restart_time", "OPcache statistics, last restart time")
statisticsOOMRestarts = newMetric("statistics_oom_restarts", "OPcache statistics, oom restarts")
statisticsHashRestarts = newMetric("statistics_hash_restarts", "OPcache statistics, hash restarts")
statisticsManualRestarts = newMetric("statistics_manual_restarts", "OPcache statistics, manual restarts")
statisticsMisses = newMetric("statistics_misses", "OPcache statistics, misses")
statisticsBlacklistMisses = newMetric("statistics_blacklist_misses", "OPcache statistics, blacklist misses")
statisticsBlacklistMissRatio = newMetric("statistics_blacklist_miss_ratio", "OPcache statistics, blacklist miss ratio")
statisticsHitRate = newMetric("statistics_hit_rate", "OPcache statistics, opcache hit rate")
)

func newMetric(metricName, metricDesc string) *prometheus.Desc {
return prometheus.NewDesc(prometheus.BuildFQName(namespace, "", metricName), metricDesc, nil, nil)
}

func boolMetric(value bool) float64 {
return map[bool]float64{true: 1, false: 0}[value]
}

func intMetric(value int64) float64 {
return float64(value)
}

// Exporter collects OPcache status from the given FastCGI URI and exports them using
// the prometheus metrics package.
type Exporter struct {
mutex sync.RWMutex

uri string
scriptPath string
}

// NewExporter returns an initialized Exporter.
func NewExporter(uri, scriptPath string) (*Exporter, error) {
exporter := &Exporter{
uri: uri,
scriptPath: scriptPath,
}

return exporter, nil
}

// Describe describes all the metrics ever exported by the OPcache exporter.
// Implements prometheus.Collector.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- enabledDesc
ch <- cacheFullDesc
ch <- restartPendingDesc
ch <- restartInProgressDesc
ch <- memoryUsageUsedMemoryDesc
ch <- memoryUsageFreeMemoryDesc
ch <- memoryUsageWastedMemoryDesc
ch <- memoryUsageCurrentWastedPercentageDesc
ch <- internedStringsUsageBufferSizeDesc
ch <- internedStringsUsageUsedMemoryDesc
ch <- internedStringsUsageUsedFreeMemory
ch <- internedStringsUsageUsedNumerOfStrings
ch <- statisticsNumCachedScripts
ch <- statisticsNumCachedKeys
ch <- statisticsMaxCachedKeys
ch <- statisticsHits
ch <- statisticsStartTime
ch <- statisticsLastRestartTime
ch <- statisticsOOMRestarts
ch <- statisticsHashRestarts
ch <- statisticsManualRestarts
ch <- statisticsMisses
ch <- statisticsBlacklistMisses
ch <- statisticsBlacklistMissRatio
ch <- statisticsHitRate
}

// Collect collects metrics of OPcache stats.
// Implements prometheus.Collector.
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.mutex.Lock() // To protect metrics from concurrent collects.
defer e.mutex.Unlock()

status, err := e.getOpcacheStatus()
if err != nil {
status = new(OPcacheStatus)
}

ch <- prometheus.MustNewConstMetric(enabledDesc, prometheus.GaugeValue, boolMetric(status.OPcacheEnabled))
ch <- prometheus.MustNewConstMetric(cacheFullDesc, prometheus.GaugeValue, boolMetric(status.CacheFull))
ch <- prometheus.MustNewConstMetric(restartPendingDesc, prometheus.GaugeValue, boolMetric(status.RestartPending))
ch <- prometheus.MustNewConstMetric(restartInProgressDesc, prometheus.GaugeValue, boolMetric(status.RestartInProgress))
ch <- prometheus.MustNewConstMetric(memoryUsageUsedMemoryDesc, prometheus.GaugeValue, intMetric(status.MemoryUsage.UsedMemory))
ch <- prometheus.MustNewConstMetric(memoryUsageFreeMemoryDesc, prometheus.GaugeValue, intMetric(status.MemoryUsage.FreeMemory))
ch <- prometheus.MustNewConstMetric(memoryUsageWastedMemoryDesc, prometheus.GaugeValue, intMetric(status.MemoryUsage.WastedMemory))
ch <- prometheus.MustNewConstMetric(memoryUsageCurrentWastedPercentageDesc, prometheus.GaugeValue, status.MemoryUsage.CurrentWastedPercentage)
ch <- prometheus.MustNewConstMetric(internedStringsUsageBufferSizeDesc, prometheus.GaugeValue, intMetric(status.InternedStringsUsage.BufferSize))
ch <- prometheus.MustNewConstMetric(internedStringsUsageUsedMemoryDesc, prometheus.GaugeValue, intMetric(status.InternedStringsUsage.UsedMemory))
ch <- prometheus.MustNewConstMetric(internedStringsUsageUsedFreeMemory, prometheus.GaugeValue, intMetric(status.InternedStringsUsage.FreeMemory))
ch <- prometheus.MustNewConstMetric(statisticsNumCachedScripts, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.NumCachedScripts))
ch <- prometheus.MustNewConstMetric(statisticsNumCachedKeys, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.NumCachedKeys))
ch <- prometheus.MustNewConstMetric(statisticsMaxCachedKeys, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.MaxCachedKeys))
ch <- prometheus.MustNewConstMetric(statisticsHits, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.Hits))
ch <- prometheus.MustNewConstMetric(statisticsStartTime, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.StartTime))
ch <- prometheus.MustNewConstMetric(statisticsLastRestartTime, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.LastRestartTime))
ch <- prometheus.MustNewConstMetric(statisticsOOMRestarts, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.OOMRestarts))
ch <- prometheus.MustNewConstMetric(statisticsHashRestarts, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.HashRestarts))
ch <- prometheus.MustNewConstMetric(statisticsManualRestarts, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.ManualRestarts))
ch <- prometheus.MustNewConstMetric(statisticsMisses, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.Misses))
ch <- prometheus.MustNewConstMetric(statisticsBlacklistMisses, prometheus.GaugeValue, intMetric(status.OPcacheStatistics.BlacklistMisses))
ch <- prometheus.MustNewConstMetric(statisticsBlacklistMissRatio, prometheus.GaugeValue, status.OPcacheStatistics.BlacklistMissRatio)
ch <- prometheus.MustNewConstMetric(statisticsHitRate, prometheus.GaugeValue, status.OPcacheStatistics.OPcacheHitRate)
}

func (e *Exporter) getOpcacheStatus() (*OPcacheStatus, error) {
client, err := fcgiclient.Dial("tcp", e.uri)
if err != nil {
return nil, err
}

env := map[string]string{
"SCRIPT_FILENAME": e.scriptPath,
}

resp, err := client.Get(env)
if err != nil {
return nil, err
}

content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

status := new(OPcacheStatus)
err = json.Unmarshal(content, status)
if err != nil {
return nil, errors.New(string(content))
}

return status, nil
}
89 changes: 89 additions & 0 deletions cmd/exporter/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"io/ioutil"
"net/http"
"os"
"strings"

"github.com/go-kit/kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"gopkg.in/alecthomas/kingpin.v2"
)

func main() {
var (
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9101").String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
fcgiURI = kingpin.Flag("opcache.fcgi-uri", "Connection string to FastCGI server.").Default("127.0.0.1:9000").String()
scriptPath = kingpin.Flag("opcache.script-path", "Path to PHP script which echoes json-encoded OPcache status").Default("").String()
scriptDir = kingpin.Flag("opcache.script-dir", "Path to directory where temporary PHP file will be created").Default("").String()
)

promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig)
kingpin.HelpFlag.Short('h')
kingpin.Parse()

logger := promlog.New(promlogConfig)

if err := run(*listenAddress, *metricsPath, *fcgiURI, *scriptPath, *scriptDir); err != nil {
level.Error(logger).Log("msg", "Error starting HTTP server", "err", err)
os.Exit(1)
}
}

func run(listenAddress, metricsPath, fcgiURI, scriptPath, scriptDir string) error {
if len(scriptPath) == 0 {
file, err := ioutil.TempFile(scriptDir, "opcache.*.php")
if err != nil {
return err
}

file.Chmod(0777)

payload := "<?php\necho(json_encode(opcache_get_status()));\n"
_, err = file.WriteString(payload)
if err != nil {
return err
}

scriptPath = file.Name()

defer file.Close()
defer os.Remove(file.Name())
}

exporter, err := NewExporter(fcgiURI, scriptPath)
if err != nil {
return err
}

prometheus.MustRegister(exporter)
prometheus.MustRegister(version.NewCollector("opcache_exporter"))

html := strings.Join([]string{
`<html>`,
` <head>`,
` <title>OPcache Exporter</title>`,
` </head>`,
` <body>`,
` <h1>OPcache Exporter</h1>`,
` <p>`,
` <a href="` + metricsPath + `">Metrics</a>`,
` </p>`,
` </body>`,
`</html>`,
}, "\n")

http.Handle(metricsPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(html))
})

return http.ListenAndServe(listenAddress, nil)
}
45 changes: 45 additions & 0 deletions cmd/exporter/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

// OPcacheStatus contains information about OPcache
type OPcacheStatus struct {
OPcacheEnabled bool `json:"opcache_enabled"`
CacheFull bool `json:"cache_full"`
RestartPending bool `json:"restart_pending"`
RestartInProgress bool `json:"restart_in_progress"`
MemoryUsage MemoryUsage `json:"memory_usage"`
InternedStringsUsage InternedStringsUsage `json:"interned_strings_usage"`
OPcacheStatistics OPcacheStatistics `json:"opcache_statistics"`
}

// MemoryUsage contains information about OPcache memory usage
type MemoryUsage struct {
UsedMemory int64 `json:"used_memory"`
FreeMemory int64 `json:"free_memory"`
WastedMemory int64 `json:"wasted_memory"`
CurrentWastedPercentage float64 `json:"current_wasted_percentage"`
}

// InternedStringsUsage contains information about OPcache interned strings usage
type InternedStringsUsage struct {
BufferSize int64 `json:"buffer_size"`
UsedMemory int64 `json:"used_memory"`
FreeMemory int64 `json:"free_memory"`
NumerOfStrings int64 `json:"number_of_strings"`
}

// OPcacheStatistics contains information about OPcache statistics
type OPcacheStatistics struct {
NumCachedScripts int64 `json:"num_cached_scripts"`
NumCachedKeys int64 `json:"num_cached_keys"`
MaxCachedKeys int64 `json:"max_cached_keys"`
Hits int64 `json:"hits"`
StartTime int64 `json:"start_time"`
LastRestartTime int64 `json:"last_restart_time"`
OOMRestarts int64 `json:"oom_restarts"`
HashRestarts int64 `json:"hash_restarts"`
ManualRestarts int64 `json:"manual_restarts"`
Misses int64 `json:"misses"`
BlacklistMisses int64 `json:"blacklist_misses"`
BlacklistMissRatio float64 `json:"blacklist_miss_ratio"`
OPcacheHitRate float64 `json:"opcache_hit_rate"`
}
5 changes: 0 additions & 5 deletions cmd/opcache_exporter/main.go

This file was deleted.

13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module gitlab.com/crosscone/tool/opcache-exporter

go 1.13

require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/go-kit/kit v0.8.0
github.com/prometheus/client_golang v0.9.1
github.com/prometheus/common v0.4.1
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)
Loading

0 comments on commit e89f1b2

Please sign in to comment.