Elukey has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/327020 )
Change subject: Add upstream source files ...................................................................... Add upstream source files Change-Id: Ided340c6d56a11b90d12c731dacda7c5b186b25d --- A LICENSE A README.md A apache_exporter.go A apache_exporter_test.go 4 files changed, 474 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/debs/prometheus-apache-exporter refs/changes/20/327020/1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8792d9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 neezgee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..90496a4 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Apache Exporter for Prometheus + +Exports apache mod_status statistics via HTTP for Prometheus consumption. + +With working golang environment it can be built with `go get`. There is a [good article](https://machineperson.github.io/monitoring/2016/01/04/exporting-apache-metrics-to-prometheus.html) with build HOWTO and usage example. + +Help on flags: + +``` + -insecure + Ignore server certificate if using https (default false) + -log.level value + Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal, panic]. (default info) + -scrape_uri string + URI to apache stub status page (default "http://localhost/server-status/?auto") + -telemetry.address string + Address on which to expose metrics. (default ":9117") + -telemetry.endpoint string + Path under which to expose metrics. (default "/metrics") +``` + +Tested on Apache 2.2 and Apache 2.4. + diff --git a/apache_exporter.go b/apache_exporter.go new file mode 100644 index 0000000..321c1f3 --- /dev/null +++ b/apache_exporter.go @@ -0,0 +1,286 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" +) + +const ( + namespace = "apache" // For Prometheus metrics. +) + +var ( + listeningAddress = flag.String("telemetry.address", ":9117", "Address on which to expose metrics.") + metricsEndpoint = flag.String("telemetry.endpoint", "/metrics", "Path under which to expose metrics.") + scrapeURI = flag.String("scrape_uri", "http://localhost/server-status/?auto", "URI to apache stub status page.") + insecure = flag.Bool("insecure", false, "Ignore server certificate if using https.") +) + +type Exporter struct { + URI string + mutex sync.Mutex + client *http.Client + + up *prometheus.Desc + scrapeFailures prometheus.Counter + accessesTotal *prometheus.Desc + kBytesTotal *prometheus.Desc + uptime *prometheus.Desc + workers *prometheus.GaugeVec + scoreboard *prometheus.GaugeVec + connections *prometheus.GaugeVec +} + +func NewExporter(uri string) *Exporter { + return &Exporter{ + URI: uri, + up: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Could the apache server be reached", + nil, + nil), + scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: namespace, + Name: "exporter_scrape_failures_total", + Help: "Number of errors while scraping apache.", + }), + accessesTotal: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "accesses_total"), + "Current total apache accesses", + nil, + nil), + kBytesTotal: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "sent_kilobytes_total"), + "Current total kbytes sent", + nil, + nil), + uptime: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "uptime_seconds_total"), + "Current uptime in seconds", + nil, + nil), + workers: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "workers", + Help: "Apache worker statuses", + }, + []string{"state"}, + ), + scoreboard: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "scoreboard", + Help: "Apache scoreboard statuses", + }, + []string{"state"}, + ), + connections: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "connections", + Help: "Apache connection statuses", + }, + []string{"state"}, + ), + client: &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *insecure}, + }, + }, + } +} + +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + ch <- e.up + ch <- e.accessesTotal + ch <- e.kBytesTotal + ch <- e.uptime + e.scrapeFailures.Describe(ch) + e.workers.Describe(ch) + e.scoreboard.Describe(ch) + e.connections.Describe(ch) +} + +// Split colon separated string into two fields +func splitkv(s string) (string, string) { + + if len(s) == 0 { + return s, s + } + + slice := strings.SplitN(s, ":", 2) + + if len(slice) == 1 { + return slice[0], "" + } + + return strings.TrimSpace(slice[0]), strings.TrimSpace(slice[1]) +} + +func (e *Exporter) updateScoreboard(scoreboard string) { + e.scoreboard.Reset() + for _, worker_status := range scoreboard { + s := string(worker_status) + switch { + case s == "_": + e.scoreboard.WithLabelValues("idle").Inc() + case s == "S": + e.scoreboard.WithLabelValues("startup").Inc() + case s == "R": + e.scoreboard.WithLabelValues("read").Inc() + case s == "W": + e.scoreboard.WithLabelValues("reply").Inc() + case s == "K": + e.scoreboard.WithLabelValues("keepalive").Inc() + case s == "D": + e.scoreboard.WithLabelValues("dns").Inc() + case s == "C": + e.scoreboard.WithLabelValues("closing").Inc() + case s == "L": + e.scoreboard.WithLabelValues("logging").Inc() + case s == "G": + e.scoreboard.WithLabelValues("graceful_stop").Inc() + case s == "I": + e.scoreboard.WithLabelValues("idle_cleanup").Inc() + case s == ".": + e.scoreboard.WithLabelValues("open_slot").Inc() + } + } +} + +func (e *Exporter) collect(ch chan<- prometheus.Metric) error { + resp, err := e.client.Get(e.URI) + if err != nil { + ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 0) + return fmt.Errorf("Error scraping apache: %v", err) + } + ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 1) + + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + if err != nil { + data = []byte(err.Error()) + } + return fmt.Errorf("Status %s (%d): %s", resp.Status, resp.StatusCode, data) + } + + lines := strings.Split(string(data), "\n") + + connectionInfo := false + + for _, l := range lines { + key, v := splitkv(l) + if err != nil { + continue + } + + switch { + case key == "Total Accesses": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric(e.accessesTotal, prometheus.CounterValue, val) + case key == "Total kBytes": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric(e.kBytesTotal, prometheus.CounterValue, val) + case key == "Uptime": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric(e.uptime, prometheus.CounterValue, val) + case key == "BusyWorkers": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + e.workers.WithLabelValues("busy").Set(val) + case key == "IdleWorkers": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + e.workers.WithLabelValues("idle").Set(val) + case key == "Scoreboard": + e.updateScoreboard(v) + e.scoreboard.Collect(ch) + case key == "ConnsTotal": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + e.connections.WithLabelValues("total").Set(val) + connectionInfo = true + case key == "ConnsAsyncWriting": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + + e.connections.WithLabelValues("writing").Set(val) + connectionInfo = true + case key == "ConnsAsyncKeepAlive": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + e.connections.WithLabelValues("keepalive").Set(val) + connectionInfo = true + case key == "ConnsAsyncClosing": + val, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + e.connections.WithLabelValues("closing").Set(val) + connectionInfo = true + } + + } + + e.workers.Collect(ch) + if connectionInfo { + e.connections.Collect(ch) + } + + return nil +} + +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.mutex.Lock() // To protect metrics from concurrent collects. + defer e.mutex.Unlock() + if err := e.collect(ch); err != nil { + log.Errorf("Error scraping apache: %s", err) + e.scrapeFailures.Inc() + e.scrapeFailures.Collect(ch) + } + return +} + +func main() { + flag.Parse() + + exporter := NewExporter(*scrapeURI) + prometheus.MustRegister(exporter) + + log.Infof("Starting Server: %s", *listeningAddress) + http.Handle(*metricsEndpoint, prometheus.Handler()) + log.Fatal(http.ListenAndServe(*listeningAddress, nil)) +} diff --git a/apache_exporter_test.go b/apache_exporter_test.go new file mode 100644 index 0000000..420156b --- /dev/null +++ b/apache_exporter_test.go @@ -0,0 +1,143 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + apache24Status = `localhost +ServerVersion: Apache/2.4.23 (Unix) +ServerMPM: event +Server Built: Jul 29 2016 04:26:14 +CurrentTime: Friday, 29-Jul-2016 14:06:15 UTC +RestartTime: Friday, 29-Jul-2016 13:58:49 UTC +ParentServerConfigGeneration: 1 +ParentServerMPMGeneration: 0 +ServerUptimeSeconds: 445 +ServerUptime: 7 minutes 25 seconds +Load1: 0.02 +Load5: 0.02 +Load15: 0.00 +Total Accesses: 131 +Total kBytes: 138 +CPUUser: .25 +CPUSystem: .15 +CPUChildrenUser: 0 +CPUChildrenSystem: 0 +CPULoad: .0898876 +Uptime: 445 +ReqPerSec: .294382 +BytesPerSec: 317.555 +BytesPerReq: 1078.72 +BusyWorkers: 1 +IdleWorkers: 74 +ConnsTotal: 0 +ConnsAsyncWriting: 0 +ConnsAsyncKeepAlive: 0 +ConnsAsyncClosing: 0 +Scoreboard: _W___ +` + + apache24WorkerStatus = `localhost +ServerVersion: Apache/2.4.23 (Unix) OpenSSL/1.0.2h +ServerMPM: worker +Server Built: Aug 31 2016 10:54:08 +CurrentTime: Thursday, 08-Sep-2016 15:09:32 CEST +RestartTime: Thursday, 08-Sep-2016 15:08:07 CEST +ParentServerConfigGeneration: 1 +ParentServerMPMGeneration: 0 +ServerUptimeSeconds: 85 +ServerUptime: 1 minute 25 seconds +Load1: 0.00 +Load5: 0.01 +Load15: 0.05 +Total Accesses: 10 +Total kBytes: 38 +CPUUser: .05 +CPUSystem: 0 +CPUChildrenUser: 0 +CPUChildrenSystem: 0 +CPULoad: .0588235 +Uptime: 85 +ReqPerSec: .117647 +BytesPerSec: 457.788 +BytesPerReq: 3891.2 +BusyWorkers: 2 +IdleWorkers: 48 +Scoreboard: _____R_______________________K____________________.................................................................................................... +TLSSessionCacheStatus +CacheType: SHMCB +CacheSharedMemory: 512000 +CacheCurrentEntries: 0 +CacheSubcaches: 32 +CacheIndexesPerSubcaches: 88 +CacheIndexUsage: 0% +CacheUsage: 0% +CacheStoreCount: 0 +CacheReplaceCount: 0 +CacheExpireCount: 0 +CacheDiscardCount: 0 +CacheRetrieveHitCount: 0 +CacheRetrieveMissCount: 1 +CacheRemoveHitCount: 0 +CacheRemoveMissCount: 0 +` + + apache22Status = `Total Accesses: 302311 +Total kBytes: 1677830 +CPULoad: 27.4052 +Uptime: 45683 +ReqPerSec: 6.61758 +BytesPerSec: 37609.1 +BytesPerReq: 5683.21 +BusyWorkers: 2 +IdleWorkers: 8 +Scoreboard: _W_______K...................................................................................................................................................................................................................................................... +` + + metricCountApache22 = 10 + metricCountApache24 = 12 + metricCountApache24Worker = 10 + +) + +func checkApacheStatus(t *testing.T, status string, metricCount int) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(status)) + }) + server := httptest.NewServer(handler) + + e := NewExporter(server.URL) + ch := make(chan prometheus.Metric) + + go func() { + defer close(ch) + e.Collect(ch) + }() + + for i := 1; i <= metricCount; i++ { + m := <-ch + if m == nil { + t.Error("expected metric but got nil") + } + } + if <-ch != nil { + t.Error("expected closed channel") + } +} + +func TestApache22Status(t *testing.T) { + checkApacheStatus(t, apache22Status, metricCountApache22) +} + +func TestApache24Status(t *testing.T) { + checkApacheStatus(t, apache24Status, metricCountApache24) +} + +func TestApache24WorkerStatus(t *testing.T) { + checkApacheStatus(t, apache24WorkerStatus, metricCountApache24Worker) +} -- To view, visit https://gerrit.wikimedia.org/r/327020 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ided340c6d56a11b90d12c731dacda7c5b186b25d Gerrit-PatchSet: 1 Gerrit-Project: operations/debs/prometheus-apache-exporter Gerrit-Branch: master Gerrit-Owner: Elukey <ltosc...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits