Jannick Knudsen 4 years ago
commit
f53bee042a
10 changed files with 867 additions and 0 deletions
  1. 14 0
      .gitignore
  2. 34 0
      Dockerfile
  3. 125 0
      Gopkg.lock
  4. 38 0
      Gopkg.toml
  5. 22 0
      LICENSE
  6. 57 0
      README.md
  7. 39 0
      docker-compose.yml
  8. 9 0
      docker-entrypoint.sh
  9. 222 0
      wordpress_exporter.go
  10. 307 0
      wordpress_grafana.json

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+

+ 34 - 0
Dockerfile

@@ -0,0 +1,34 @@
+FROM golang:1.12
+
+# Add Maintainer Info
+LABEL maintainer="Erwin Mueller <erwin.mueller@deventm.com>"
+
+# Set the Current Working Directory inside the container
+WORKDIR $GOPATH/src/wordpress_exporter
+
+# Copy sources.
+COPY . .
+
+# Download all the dependencies.
+RUN go get -d -v ./...
+
+# Install the package
+RUN go install -v ./...
+
+ENV WORDPRESS_DB_HOST="" \
+    WORDPRESS_DB_PORT="3306" \
+    WORDPRESS_DB_USER="" \
+    WORDPRESS_DB_PASSWORD="" \
+    WORDPRESS_DB_NAME="" \
+    WORDPRESS_TABLE_PREFIX="wp_"
+
+EXPOSE 8888
+
+ADD /docker-entrypoint.sh /docker-entrypoint.sh
+
+RUN set -x \
+  && chmod +x /docker-entrypoint.sh
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+
+CMD ["wordpress_exporter"]

+ 125 - 0
Gopkg.lock

@@ -0,0 +1,125 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+  digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
+  name = "github.com/Sirupsen/logrus"
+  packages = ["."]
+  pruneopts = "UT"
+  revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
+  version = "v1.0.5"
+
+[[projects]]
+  branch = "master"
+  digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
+  name = "github.com/beorn7/perks"
+  packages = ["quantile"]
+  pruneopts = "UT"
+  revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
+
+[[projects]]
+  digest = "1:adea5a94903eb4384abef30f3d878dc9ff6b6b5b0722da25b82e5169216dfb61"
+  name = "github.com/go-sql-driver/mysql"
+  packages = ["."]
+  pruneopts = "UT"
+  revision = "d523deb1b23d913de5bdada721a6071e71283618"
+  version = "v1.4.0"
+
+[[projects]]
+  digest = "1:15042ad3498153684d09f393bbaec6b216c8eec6d61f63dff711de7d64ed8861"
+  name = "github.com/golang/protobuf"
+  packages = ["proto"]
+  pruneopts = "UT"
+  revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
+  version = "v1.1.0"
+
+[[projects]]
+  digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
+  name = "github.com/matttproud/golang_protobuf_extensions"
+  packages = ["pbutil"]
+  pruneopts = "UT"
+  revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
+  version = "v1.0.1"
+
+[[projects]]
+  digest = "1:1bd84ab43883f0ef0129fb753f9fdabba3903eff922816aaaba8e6204fb3cde9"
+  name = "github.com/prometheus/client_golang"
+  packages = [
+    "prometheus",
+    "prometheus/promhttp",
+  ]
+  pruneopts = "UT"
+  revision = "c5b7fccd204277076155f10851dad72b76a49317"
+  version = "v0.8.0"
+
+[[projects]]
+  branch = "master"
+  digest = "1:0f37e09b3e92aaeda5991581311f8dbf38944b36a3edec61cc2d1991f527554a"
+  name = "github.com/prometheus/client_model"
+  packages = ["go"]
+  pruneopts = "UT"
+  revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f"
+
+[[projects]]
+  branch = "master"
+  digest = "1:4d291d51042ed9de40eef61a3c1b56e969d6e0f8aa5fd3da5e958ec66bee68e4"
+  name = "github.com/prometheus/common"
+  packages = [
+    "expfmt",
+    "internal/bitbucket.org/ww/goautoneg",
+    "model",
+  ]
+  pruneopts = "UT"
+  revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
+
+[[projects]]
+  branch = "master"
+  digest = "1:55d7449d6987dabf272b4e81b2f9c449f05b17415c939b68d1e82f57e3374b7f"
+  name = "github.com/prometheus/procfs"
+  packages = [
+    ".",
+    "internal/util",
+    "nfs",
+    "xfs",
+  ]
+  pruneopts = "UT"
+  revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
+
+[[projects]]
+  branch = "master"
+  digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8"
+  name = "golang.org/x/crypto"
+  packages = ["ssh/terminal"]
+  pruneopts = "UT"
+  revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
+
+[[projects]]
+  branch = "master"
+  digest = "1:72d6244a51be9611f08994aca19677fcc31676b3e7b742c37e129e6ece4ad8fc"
+  name = "golang.org/x/sys"
+  packages = [
+    "unix",
+    "windows",
+  ]
+  pruneopts = "UT"
+  revision = "1b2967e3c290b7c545b3db0deeda16e9be4f98a2"
+
+[[projects]]
+  digest = "1:c25289f43ac4a68d88b02245742347c94f1e108c534dda442188015ff80669b3"
+  name = "google.golang.org/appengine"
+  packages = ["cloudsql"]
+  pruneopts = "UT"
+  revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
+  version = "v1.1.0"
+
+[solve-meta]
+  analyzer-name = "dep"
+  analyzer-version = 1
+  input-imports = [
+    "github.com/Sirupsen/logrus",
+    "github.com/go-sql-driver/mysql",
+    "github.com/prometheus/client_golang/prometheus",
+    "github.com/prometheus/client_golang/prometheus/promhttp",
+  ]
+  solver-name = "gps-cdcl"
+  solver-version = 1

+ 38 - 0
Gopkg.toml

@@ -0,0 +1,38 @@
+# Gopkg.toml example
+#
+# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+#   name = "github.com/user/project"
+#   version = "1.0.0"
+#
+# [[constraint]]
+#   name = "github.com/user/project2"
+#   branch = "dev"
+#   source = "github.com/myfork/project2"
+#
+# [[override]]
+#   name = "github.com/x/y"
+#   version = "2.4.0"
+#
+# [prune]
+#   non-go = false
+#   go-tests = true
+#   unused-packages = true
+
+
+[[constraint]]
+  name = "github.com/Sirupsen/logrus"
+  version = "1.0.5"
+
+[[constraint]]
+  name = "github.com/prometheus/client_golang"
+  version = "0.8.0"
+
+[prune]
+  go-tests = true
+  unused-packages = true

+ 22 - 0
LICENSE

@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2018 Konstantinos Katsamakas
+
+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.
+

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# wordpress_exporter
+Prometheus exporter for WordPress
+
+# Install wordpress_exporter
+```sh
+$ go get github.com/kotsis/wordpress_exporter
+```
+
+# Usage of wordpress_exporter
+```sh
+$ wordpress_exporter -wpconfig=/path/to/wp-config
+```
+or
+```sh
+$ wordpress_exporter -host=127.0.0.1 -port=3306 -user=uuuu -db=dddd -tableprefix=wp_ -pass=xxxx
+```
+
+It starts serving metrics at http://localhost:8888/metrics
+
+# Prometheus configuration for wordpress_exporter
+For Prometheus to start scraping the metrics you have to edit /etc/prometheus/prometheus.yml and add:
+
+```sh
+  - job_name: 'wordpress'
+    # metrics_path defaults to '/metrics'
+    # scheme defaults to 'http'.
+    static_configs:
+    - targets: ['localhost:8888']
+```
+
+the above is valid if the exporter runs at the same host as prometheus service. If prometheus runs
+in a docker container perhaps you will need to change localhost with the IP of the host system, something like 172.17.0.1
+
+# WordPress service with docker-compose
+
+Here is provided a quick WordPress service setup with docker-compose for testing the wordpress_exporter.
+You can go in $GOPATH/src/github.com/kotsis/wordress_exporter and run:
+```sh
+$ sudo docker-compose up -d
+```
+
+Now a wordpress is being served at : http://localhost:8000 where you must visit and create a user with a password.
+Then you can login in WordPress and create posts, users etc.
+
+Next you must start the wordpress_exporter
+```sh
+$ wordpress_exporter -port=33306 -db=wordpress -user=wordpress -pass=wordpress1234
+```
+
+You will see the metrics from those actions.
+
+# Grafana
+You can find a WordPress dashboard in $GOPATH/src/github.com/kotsis/wordress_exporter/wordpress_grafana.json
+
+For it to work you must define in Grafana a new Prometheus data source as prom1
+This must be the Prometheus instance that is scrapin metrics from wordpress_exporter.
+Then you can import the above json file and start viewing the metrics.

+ 39 - 0
docker-compose.yml

@@ -0,0 +1,39 @@
+version: '3.3'
+
+services:
+   db:
+     image: mysql:5.7
+     ports:
+       - "33306:3306"
+     restart: always
+     environment:
+       MYSQL_ROOT_PASSWORD: somewordpress1234
+       MYSQL_DATABASE: wordpress
+       MYSQL_USER: wordpress
+       MYSQL_PASSWORD: wordpress1234
+
+   wordpress:
+     depends_on:
+       - db
+     image: wordpress:latest
+     ports:
+       - "8000:80"
+     restart: always
+     environment:
+       WORDPRESS_DB_HOST: db:3306
+       WORDPRESS_DB_USER: wordpress
+       WORDPRESS_DB_PASSWORD: wordpress1234
+       WORDPRESS_DB_NAME: wordpress
+
+   wordpress_exporter:
+     depends_on:
+       - db
+     image: erwin82/wordpress_exporter:latest
+     ports:
+       - "8888:8888"
+     restart: always
+     environment:
+       WORDPRESS_DB_HOST: db
+       WORDPRESS_DB_USER: wordpress
+       WORDPRESS_DB_PASSWORD: wordpress1234
+       WORDPRESS_DB_NAME: wordpress

+ 9 - 0
docker-entrypoint.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+if [[ -n "${WORDPRESS_DB_HOST}" ]]; then
+    exec "$@" -host="${WORDPRESS_DB_HOST}" -port="${WORDPRESS_DB_PORT}" -user="${WORDPRESS_DB_USER}" -db="${WORDPRESS_DB_NAME}" -tableprefix="${WORDPRESS_TABLE_PREFIX}" -pass="${WORDPRESS_DB_PASSWORD}"
+else
+    exec "$@"
+fi
+

+ 222 - 0
wordpress_exporter.go

@@ -0,0 +1,222 @@
+package main
+
+import (
+    "net/http"
+
+    log "github.com/Sirupsen/logrus"
+    "github.com/prometheus/client_golang/prometheus/promhttp"
+    "github.com/prometheus/client_golang/prometheus"
+
+    "flag"
+    "fmt"
+    "os"
+    "io/ioutil"
+    "strings"
+    "regexp"
+
+    "database/sql"
+    _ "github.com/go-sql-driver/mysql"
+)
+
+//This is my collector metrics
+type wpCollector struct {
+    numPostsMetric *prometheus.Desc
+    numCommentsMetric *prometheus.Desc
+    numUsersMetric *prometheus.Desc
+
+    db_host string
+    db_name string
+    db_user string
+    db_pass string
+    db_table_prefix string
+}
+
+//This is a constructor for my wpCollector struct
+func newWordPressCollector(host string, dbname string, username string, pass string, table_prefix string) *wpCollector {
+    return &wpCollector{
+        numPostsMetric: prometheus.NewDesc("wp_num_posts_metric",
+                        "Shows the number of total posts in the WordPress site",
+                        nil, nil,
+        ),
+        numCommentsMetric: prometheus.NewDesc("wp_num_comments_metric",
+                           "Shows the number of total comments in the WordPress site",
+                           nil, nil,
+        ),
+        numUsersMetric: prometheus.NewDesc("wp_num_users_metric",
+                        "Shows the number of registered users in the WordPress site",
+                        nil, nil,
+        ),
+
+        db_host: host,
+        db_name: dbname,
+        db_user: username,
+        db_pass: pass,
+        db_table_prefix: table_prefix,
+    }
+}
+
+//Describe method is required for a prometheus.Collector type
+func (collector *wpCollector) Describe(ch chan<- *prometheus.Desc) {
+
+    //We set the metrics
+    ch <- collector.numPostsMetric
+    ch <- collector.numCommentsMetric
+    ch <- collector.numUsersMetric
+}
+
+//Collect method is required for a prometheus.Collector type
+func (collector *wpCollector) Collect(ch chan<- prometheus.Metric) {
+
+    //We run DB queries here to retrieve the metrics we care about
+    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", collector.db_user, collector.db_pass, collector.db_host, collector.db_name)
+
+    db, err := sql.Open("mysql", dsn)
+    if(err != nil){
+        fmt.Fprintf(os.Stderr, "Error connecting to database: %s ...\n", err)
+        os.Exit(1)
+    }
+    defer db.Close()
+
+    //select count(*) as num_users from wp_users;
+    var num_users float64
+    q1 := fmt.Sprintf("select count(*) as num_users from %susers;", collector.db_table_prefix)
+    err = db.QueryRow(q1).Scan(&num_users)
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    //select count(*) as num_comments from wp_comments;
+    var num_comments float64
+    q2 := fmt.Sprintf("select count(*) as num_comments from %scomments;", collector.db_table_prefix)
+    err = db.QueryRow(q2).Scan(&num_comments)
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    //select count(*) as num_posts from wp_posts WHERE post_type='post' AND post_status!='auto-draft';
+    var num_posts float64
+    q3 := fmt.Sprintf("select count(*) as num_posts from %sposts WHERE post_type='post' AND post_status!='auto-draft';", collector.db_table_prefix)
+    err = db.QueryRow(q3).Scan(&num_posts)
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    //Write latest value for each metric in the prometheus metric channel.
+    //Note that you can pass CounterValue, GaugeValue, or UntypedValue types here.
+    ch <- prometheus.MustNewConstMetric(collector.numPostsMetric, prometheus.CounterValue, num_posts)
+    ch <- prometheus.MustNewConstMetric(collector.numCommentsMetric, prometheus.CounterValue, num_comments)
+    ch <- prometheus.MustNewConstMetric(collector.numUsersMetric, prometheus.CounterValue, num_users)
+
+}
+
+func main() {
+
+    wpConfPtr := flag.String("wpconfig", "", "Path for wp-config.php file of the WordPress site you wish to monitor")
+    wpHostPtr := flag.String("host", "127.0.0.1", "Hostname or Address for DB server")
+    wpPortPtr := flag.String("port", "3306", "DB server port")
+    wpNamePtr := flag.String("db", "", "DB name")
+    wpUserPtr := flag.String("user", "", "DB user for connection")
+    wpPassPtr := flag.String("pass", "", "DB password for connection")
+    wpTablePrefixPtr := flag.String("tableprefix", "wp_", "Table prefix for WordPress tables")
+
+    flag.Parse()
+
+    if *wpConfPtr == "" {
+        db_host := fmt.Sprintf("%s:%s", *wpHostPtr, *wpPortPtr)
+        db_name := *wpNamePtr
+        db_user := *wpUserPtr
+        db_password := *wpPassPtr
+        table_prefix := *wpTablePrefixPtr
+
+        if db_name == "" {
+            fmt.Fprintf(os.Stderr, "flag -db=dbname required!\n")
+            os.Exit(1)
+        }
+
+        if db_user == "" {
+            fmt.Fprintf(os.Stderr, "flag -user=username required!\n")
+            os.Exit(1)
+        }
+
+        //We create the collector
+        collector := newWordPressCollector(db_host, db_name, db_user, db_password, table_prefix)
+        prometheus.MustRegister(collector)
+
+        //no path supplied error
+        //fmt.Fprintf(os.Stderr, "flag -wpconfig=/path/to/wp-config/ required!\n")
+        //os.Exit(1)
+    } else{
+        var wpconfig_file strings.Builder
+        wpconfig_file.WriteString(*wpConfPtr)
+
+        if strings.HasSuffix(*wpConfPtr, "/") {
+            wpconfig_file.WriteString("wp-config.php")
+        }else{
+            wpconfig_file.WriteString("/wp-config.php")
+        }
+
+        //try to read wp-config.php file from path
+        dat, err := ioutil.ReadFile(wpconfig_file.String())
+        if(err != nil){
+            panic(err)
+        }
+        fmt.Printf("Read :%v bytes\n", len(dat))
+
+        //We must locate with regular expressions the MySQL connection credentials and the table prefix
+        //define('DB_HOST', 'xxxxxxx');
+        r, _ := regexp.Compile(`define\(['"]DB_HOST['"].*?,.*?['"](.*?)['"].*?\);`)
+        res := r.FindStringSubmatch(string(dat[:len(dat)]))
+        if(res == nil){
+            fmt.Fprintf(os.Stderr, "Error could not find DB_HOST in wp-config.php ...\n")
+            os.Exit(1)
+        }
+        db_host := res[1]
+    
+        //define('DB_NAME', 'xxxxxxx');
+        r, _ = regexp.Compile(`define\(['"]DB_NAME['"].*?,.*?['"](.*?)['"].*?\);`)
+        res = r.FindStringSubmatch(string(dat[:len(dat)]))
+        if(res == nil){
+            fmt.Fprintf(os.Stderr, "Error could not find DB_NAME in wp-config.php ...\n")
+            os.Exit(1)
+        }
+        db_name := res[1]
+
+        //define('DB_USER', 'xxxxxxx');
+        r, _ = regexp.Compile(`define\(['"]DB_USER['"].*?,.*?['"](.*?)['"].*?\);`)
+        res = r.FindStringSubmatch(string(dat[:len(dat)]))
+        if(res == nil){
+            fmt.Fprintf(os.Stderr, "Error could not find DB_USER in wp-config.php ...\n")
+            os.Exit(1)
+        }
+        db_user := res[1]
+
+        //define('DB_PASSWORD', 'xxxxxxx');
+        r, _ = regexp.Compile(`define\(['"]DB_PASSWORD['"].*?,.*?['"](.*?)['"].*?\);`)
+        res = r.FindStringSubmatch(string(dat[:len(dat)]))
+        if(res == nil){
+            fmt.Fprintf(os.Stderr, "Error could not find DB_PASSWORD in wp-config.php ...\n")
+            os.Exit(1)
+        }
+        db_password := res[1]
+
+        //$table_prefix  = 'wp_';
+        r, _ = regexp.Compile(`\$table_prefix.*?=.*?['"](.*?)['"];`)
+        res = r.FindStringSubmatch(string(dat[:len(dat)]))
+        if(res == nil){
+            fmt.Fprintf(os.Stderr, "Error could not find $table_prefix in wp-config.php ...\n")
+            os.Exit(1)
+        }
+        table_prefix := res[1]
+
+        //We create the collector
+        collector := newWordPressCollector(db_host, db_name, db_user, db_password, table_prefix)
+        prometheus.MustRegister(collector)
+    }
+
+    //This section will start the HTTP server and expose
+    //any metrics on the /metrics endpoint.
+    http.Handle("/metrics", promhttp.Handler())
+    log.Info("Beginning to serve on port :8888")
+    log.Fatal(http.ListenAndServe(":8888", nil))
+}
+

+ 307 - 0
wordpress_grafana.json

@@ -0,0 +1,307 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": 4,
+  "links": [],
+  "panels": [
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "prom1",
+      "format": "none",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 6,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": false,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "wp_num_comments_metric",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A"
+        }
+      ],
+      "thresholds": "",
+      "title": "Number of comments",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": true,
+      "colors": [
+        "#629e51",
+        "#5195ce",
+        "#d44a3a"
+      ],
+      "datasource": "prom1",
+      "format": "none",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 2,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": false,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "__name__",
+      "targets": [
+        {
+          "expr": "wp_num_users_metric",
+          "format": "time_series",
+          "instant": false,
+          "intervalFactor": 1,
+          "refId": "A"
+        }
+      ],
+      "thresholds": "",
+      "title": "WordPress number of registered users",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "prom1",
+      "fill": 1,
+      "gridPos": {
+        "h": 9,
+        "w": 12,
+        "x": 0,
+        "y": 9
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "wp_num_posts_metric",
+          "format": "time_series",
+          "instant": false,
+          "intervalFactor": 1,
+          "legendFormat": "",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Number of WordPress posts",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "WordPress exporter dashboard",
+  "uid": "qtFzy1dik",
+  "version": 6
+}