Skip to content

Commit

Permalink
Make exporter compatible with MaxScale 2.5
Browse files Browse the repository at this point in the history
  • Loading branch information
corny committed Nov 28, 2020
1 parent 5cc9ebd commit dff3bbd
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 618 deletions.
10 changes: 5 additions & 5 deletions .promu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ build:
- name: maxscale_exporter
flags: -a -tags netgo
ldflags: |
-X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}}
-X {{repoPath}}/vendor/github.com/prometheus/common/version.Revision={{.Revision}}
-X {{repoPath}}/vendor/github.com/prometheus/common/version.Branch={{.Branch}}
-X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildUser={{user}}@{{host}}
-X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}}
-X {{repoPath}}/version.Version={{.Version}}
-X {{repoPath}}/version.Revision={{.Revision}}
-X {{repoPath}}/version.Branch={{.Branch}}
-X {{repoPath}}/version.BuildUser={{user}}@{{host}}
-X {{repoPath}}/version.BuildDate={{date "20060102-15:04:05"}}
tarball:
files:
- LICENSE
Expand Down
6 changes: 2 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
FROM golang:1.8 AS build
FROM golang:1.15 AS build

WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
RUN go get github.com/VoIPGRID/maxscale_exporter
RUN make build

FROM alpine:3.10
FROM alpine

COPY --from=build /go/src/app/maxscale_exporter /bin/maxscale_exporter
USER nobody
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.build
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.8
FROM golang:1.15
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

GO ?= GO15VENDOREXPERIMENT=1 go
GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
GOARCH := $(shell $(GO) env GOARCH)
GOHOSTARCH := $(shell $(GO) env GOHOSTARCH)
Expand Down
204 changes: 204 additions & 0 deletions exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"

"github.com/prometheus/client_golang/prometheus"
)

const (
namespace = "maxscale"
)

var (
descServerUp = newDesc("server", "up", "Is the server up", serverLabelNames, prometheus.GaugeValue)
descServerMaster = newDesc("server", "master", "Is the server master", serverLabelNames, prometheus.GaugeValue)
descServerConnections = newDesc("server", "connections", "Current number of connections to the server", serverLabelNames, prometheus.GaugeValue)
descServerTotalConnections = newDesc("server", "total_connections", "Total connections", serverLabelNames, prometheus.CounterValue)
descServerReusedConnections = newDesc("server", "reused_connections", "Reused connections", serverLabelNames, prometheus.CounterValue)
descServerActiveOperations = newDesc("server", "active_operations", "Curren number of active operations", serverLabelNames, prometheus.GaugeValue)
descServiceCurrentSessions = newDesc("service", "current_sessions", "Amount of sessions currently active", serviceLabelNames, prometheus.GaugeValue)
descServiceSessionsTotal = newDesc("service", "total_sessions", "Total amount of sessions", serviceLabelNames, prometheus.CounterValue)
descQueryStatisticsRead = newDesc("query_statistics", "read", "Total reads", queryStatisticsLabelNames, prometheus.CounterValue)
descQueryStatisticsWrite = newDesc("query_statistics", "write", "Total writes", queryStatisticsLabelNames, prometheus.CounterValue)
)

type Exporter struct {
Address string // address of the maxscale instance
ctx context.Context
up prometheus.Gauge
}

var (
serverLabelNames = []string{"server"}
serviceLabelNames = []string{"service"}
queryStatisticsLabelNames = []string{"service", "server"}
)

func NewExporter(ctx context.Context, address string) *Exporter {
return &Exporter{
Address: address,
ctx: ctx,
up: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "up",
Help: "Was the last scrape of MaxScale successful?",
}),
}
}

// Describe describes all the metrics ever exported by the MaxScale exporter. It
// implements prometheus.Collector.
func (m *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- descServerUp.Desc
ch <- descServerMaster.Desc
ch <- descServerConnections.Desc
ch <- descServerTotalConnections.Desc
ch <- descServerReusedConnections.Desc
ch <- descServerActiveOperations.Desc
ch <- descServiceCurrentSessions.Desc
ch <- descServiceSessionsTotal.Desc
ch <- descQueryStatisticsRead.Desc
ch <- descQueryStatisticsWrite.Desc

ch <- m.up.Desc()
}

// Collect fetches the stats from configured MaxScale location and delivers them
// as Prometheus metrics. It implements prometheus.Collector.
func (m *Exporter) Collect(ch chan<- prometheus.Metric) {
parseErrors := false

if err := m.parseServers(ch); err != nil {
parseErrors = true
log.Print(err)
}

if err := m.parseServices(ch); err != nil {
parseErrors = true
log.Print(err)
}

m.up.Set(boolToFloat(!parseErrors))
ch <- m.up
}

const contentTypeJSON = "application/json"

func (m *Exporter) fetchJSON(path string, v interface{}) error {
url := "http://" + m.Address + "/v1" + path
// build the request
req, err := http.NewRequestWithContext(m.ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("Accept", contentTypeJSON)

// execute the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("error while getting %v: %w", url, err)
}
defer resp.Body.Close()

// check status code
if status := resp.StatusCode; status != http.StatusOK {
return fmt.Errorf("unexpected status code %v for url %v", status, url)
}

// check content type
if contentType := resp.Header.Get("Content-Type"); contentType != contentTypeJSON {
return fmt.Errorf("unexpected content type %v", contentType)
}

// decode JSON body
respObj := Response{}
err = json.NewDecoder(resp.Body).Decode(&respObj)
if err != nil {
return err
}

return json.Unmarshal(respObj.Data, v)
}

func boolToFloat(value bool) float64 {
if value {
return 1
}

return 0
}

func (m *Exporter) parseServers(ch chan<- prometheus.Metric) error {
var servers []ServerData
err := m.fetchJSON("/servers", &servers)
if err != nil {
return err
}

for _, server := range servers {
ch <- descServerConnections.new(
float64(server.Attributes.Statistics.Connections),
server.Attributes.Name,
)

ch <- descServerReusedConnections.new(
float64(server.Attributes.Statistics.ReusedConnections),
server.Attributes.Name,
)
ch <- descServerTotalConnections.new(
float64(server.Attributes.Statistics.TotalConnections),
server.Attributes.Name,
)
ch <- descServerActiveOperations.new(
float64(server.Attributes.Statistics.ActiveOperations),
server.Attributes.Name,
)

ch <- descServerMaster.new(
boolToFloat(strings.HasPrefix(server.Attributes.State, "Master,")),
server.Attributes.Name,
)
ch <- descServerUp.new(
boolToFloat(strings.HasSuffix(server.Attributes.State, ", Running")),
server.Attributes.Name,
)
}

return nil
}

func (m *Exporter) parseServices(ch chan<- prometheus.Metric) error {
var services []ServiceData
err := m.fetchJSON("/services", &services)
if err != nil {
return err
}

for _, service := range services {
ch <- descServiceCurrentSessions.new(
float64(service.Attributes.Statistics.Connections),
service.ID,
)

for _, statistics := range service.Attributes.RouterDiagnostics.ServerQueryStatistics {
labelValues := []string{service.ID, statistics.ID}

ch <- descQueryStatisticsRead.new(
float64(statistics.Read),
labelValues...,
)
ch <- descQueryStatisticsWrite.new(
float64(statistics.Write),
labelValues...,
)
}
}

return nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ go 1.15

require (
github.com/prometheus/client_golang v1.7.1
github.com/prometheus/common v0.15.0 // indirect
github.com/prometheus/common v0.15.0
github.com/prometheus/procfs v0.2.0 // indirect
)
26 changes: 26 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import "github.com/prometheus/client_golang/prometheus"

type Metric struct {
Desc *prometheus.Desc
ValueType prometheus.ValueType
}

func (m *Metric) new(value float64, labelValues ...string) prometheus.Metric {
return prometheus.MustNewConstMetric(
m.Desc,
m.ValueType,
value,
labelValues...,
)
}

func newDesc(subsystem, name, help string, variableLabels []string, t prometheus.ValueType) Metric {
return Metric{
prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, name),
help, variableLabels, nil,
), t,
}
}
84 changes: 84 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"encoding/json"
)

type Response struct {
Data json.RawMessage `json:"data"`
}

type ServerData struct {
ID string `json:"id"`
Attributes struct {
GtidBinlogPos string `json:"gtid_binlog_pos"`
GtidCurrentPos string `json:"gtid_current_pos"`
LastEvent string `json:"last_event"`
MasterID int `json:"master_id"`
Name string `json:"name"`
NodeID int `json:"node_id"`
ReadOnly bool `json:"read_only"`
ReplicationLag int `json:"replication_lag"`
ServerID int `json:"server_id"`
State string `json:"state"`
Statistics struct {
ActiveOperations int `json:"active_operations"`
AdaptiveAvgSelectTime string `json:"adaptive_avg_select_time"`
ConnectionPoolEmpty int `json:"connection_pool_empty"`
Connections int `json:"connections"`
MaxConnections int `json:"max_connections"`
MaxPoolSize int `json:"max_pool_size"`
PersistentConnections int `json:"persistent_connections"`
ReusedConnections int `json:"reused_connections"`
RoutedPackets int `json:"routed_packets"`
TotalConnections int `json:"total_connections"`
} `json:"statistics"`
TriggeredAt string `json:"triggered_at"`
VersionString string `json:"version_string"`
} `json:"attributes"`
}

type ServiceData struct {
Attributes struct {
Connections int `json:"connections"`
Listeners []struct {
Attributes struct {
State string `json:"state"`
} `json:"attributes"`
ID string `json:"id"`
Type string `json:"type"`
} `json:"listeners"`
Router string `json:"router"`
RouterDiagnostics struct {
Queries int `json:"queries"`
ReplayedTransactions int `json:"replayed_transactions"`
RoTransactions int `json:"ro_transactions"`
RouteAll int `json:"route_all"`
RouteMaster int `json:"route_master"`
RouteSlave int `json:"route_slave"`
RwTransactions int `json:"rw_transactions"`
ServerQueryStatistics []ServerQueryStatistics `json:"server_query_statistics"`
} `json:"router_diagnostics"`
Started string `json:"started"`
State string `json:"state"`
Statistics struct {
ActiveOperations int `json:"active_operations"`
Connections int `json:"connections"`
MaxConnections int `json:"max_connections"`
RoutedPackets int `json:"routed_packets"`
TotalConnections int `json:"total_connections"`
} `json:"statistics"`
TotalConnections int `json:"total_connections"`
} `json:"attributes"`
ID string `json:"id"`
}

type ServerQueryStatistics struct {
AvgSelectsPerSession int `json:"avg_selects_per_session"`
AvgSessActivePct float64 `json:"avg_sess_active_pct"`
AvgSessDuration string `json:"avg_sess_duration"`
ID string `json:"id"`
Read int `json:"read"`
Total int `json:"total"`
Write int `json:"write"`
}
Loading

0 comments on commit dff3bbd

Please sign in to comment.