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

Reply via email to