├── .gitignore
├── README.md
├── .travis.yml
├── LICENSE
├── monit_exporter_test.go
└── monit_exporter.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Monit Exporter for Prometheus
2 |
3 | Simple server that periodically scrapes monit status and exports checks information via HTTP for Prometheus.
4 |
5 | Build it:
6 | ```bash
7 | go build
8 | ```
9 |
10 | Run it:
11 |
12 | ```bash
13 | ./monit_exporter
14 | ```
15 |
16 | ## Configuration
17 |
18 | The application will look for configuration in "config.toml" file located in the same directory. Use -conf flag to override config file name and location.
19 |
20 | Configuration parameters:
21 |
22 | Parameter | Description | Default
23 | --- | --- | ---
24 | `listen_address` | address and port to bind | localhost:9388
25 | `metrics_path` | relative path to expose metrics | /metrics
26 | `ignore_ssl` | whether of not to ignore ssl errors | false
27 | `monit_scrape_uri` | uri to get monit status | http://localhost:2812/_status?format=xml&level=full
28 | `monit_user` | user for monit basic auth, if needed | none
29 | `monit_password` | password for monit status, if needed | none
30 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.9
4 | script:
5 | - go test -v ./...
6 | - go build
7 | - sha256sum monit_exporter > monit_exporter.sha256
8 | deploy:
9 | provider: releases
10 | api_key:
11 | secure: s5ih9FzIXMpqLND+FINaDFcbQ0Tmp7BJQzZZ61lJs2Y+fWZAhgqPvxR1sTweOiZKKSR4oY5irskcvU5tEK6QzaKwuQH1ivtIMZjQr1LzGVoluYe3lVm5VqJTmmvZmiCNPvKeOqPUAVYvn0l9ifAnhbyk6H60yjevlfgd1RvHwggQhdPVCX6+/XKJI2BCmrSDTc5VjfHGd/sZ/9O7iiYAmbm9zAEJB1j/f7B11DvSHJ3bxSh6KWFWO9pcx0E7+k4krKFu2ICprztAvOV007xBuivX+c8my9gZ38hFBvf0oVL2uYYG7pxhUtvJdpR3PwLv4xIVim4CSPrxC3vUN7kspEFGlXNY4fsXbTdSo+QVvDrGG+VQ/3Ab/rJMHs1hF5F8eXS6jeIkt8ktXsxHlxJ0L72qaH7/gYcdHzlvP9TN6A5QEN+cSNMn1wH1SWWdegLn+JeI2fQ6ZXed5P8xTq+6mCPRhh1zyeea6cxBwUylzanF0Vo2GH26VX5A4VoT5OBw7pRc9jpVBhDLG2Z8OeXjeJUl6txB4dJYZzcDR1NIyh1bFUEfl6id9IZrmwKtnGBr2+iK9yuG74sb/+NLDbB37r9tXQ5qFVU2dQOWkUwrzEoz3m2wFTQmjVM02eh5IbwdIFSVPLkTYP+vyQSnIYf40hsVT3l542QI4iYLfAiU/XY=
12 | file:
13 | - monit_exporter
14 | - monit_exporter.sha256
15 | skip_cleanup: true
16 | on:
17 | public_repo: commercetools/monit_exporter
18 | tags: true
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Commercetools GmbH, https://commercetools.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/monit_exporter_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "fmt"
9 | "io/ioutil"
10 | "time"
11 | )
12 |
13 | const (
14 | monitStatus = `acfbb9e9118e68d3754761a79d3aae1615046052145.23.0136736600fc566edc8b68/opt/monit/etc/monitrc172.17.0.228120Linux4.9.27-moby#1 SMP Thu May 11 04:01:18 UTC 2017x86_64420467681048572fc566edc8b681505209672232150010000.000.000.000.10.10.16.51336280.00`
15 | monitServiceName = `fc566edc8b68`
16 | monitUser = `user`
17 | monitPassword = `password`
18 | )
19 |
20 | func TestMonitStatus(t *testing.T) {
21 |
22 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 | w.Write([]byte(monitStatus))
24 | })
25 | server := httptest.NewServer(handler)
26 | config := ParseConfig()
27 | config.monit_scrape_uri = server.URL
28 | e, err := NewExporter(config)
29 | if err != nil {
30 | t.Error("Unexpected error during exporter creation")
31 | }
32 | err = e.scrape()
33 | if err != nil {
34 | t.Error("Unexpected execution error:", err)
35 | }
36 | }
37 |
38 | func TestFieldsParsing(t *testing.T) {
39 | parsedData, err := ParseMonitStatus([]byte(monitStatus))
40 | if err != nil {
41 | t.Error("Unable to parse XML:", err)
42 | }
43 | if parsedData.MonitServices[0].Name != monitServiceName {
44 | t.Errorf("want Name %d, have %d.", monitServiceName, parsedData.MonitServices[0].Name)
45 | }
46 | }
47 |
48 | func TestMonitUnavailable(t *testing.T) {
49 | mConfig := &Config{
50 | monit_scrape_uri: "http://localhost:1/status",
51 | }
52 | e, err := NewExporter(mConfig)
53 | if err != nil {
54 | t.Error("Unexpected error during exporter creation")
55 | }
56 | err = e.scrape()
57 | if err == nil {
58 | t.Error("Unexpected succsessful execution")
59 | }
60 | }
61 |
62 | func TestHttpQueryExporter(t *testing.T) {
63 | go main()
64 | time.Sleep(50 * time.Millisecond)
65 | address := "127.0.0.1:9388"
66 | resp, err := http.Get(fmt.Sprintf("http://%s/metrics", address))
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | b, err := ioutil.ReadAll(resp.Body)
71 | if err != nil {
72 | t.Error(err)
73 | }
74 | if err := resp.Body.Close(); err != nil {
75 | t.Error(err)
76 | }
77 | if want, have := http.StatusOK, resp.StatusCode; want != have {
78 | t.Errorf("want /metrics status code %d, have %d. Body:\n%s", want, have, b)
79 | }
80 | }
81 |
82 | func AuthHandler(w http.ResponseWriter, r *http.Request) {
83 | user, pass, _ := r.BasicAuth()
84 | if user == monitUser && pass == monitPassword {
85 | w.Write([]byte(monitStatus))
86 |
87 | } else {
88 | http.Error(w, "Unauthorized.", 401)
89 | }
90 | }
91 |
92 | func TestBasicAuth(t *testing.T) {
93 | handler := http.HandlerFunc(AuthHandler)
94 | server := httptest.NewServer(handler)
95 | config := ParseConfig()
96 | config.monit_scrape_uri = server.URL
97 | config.monit_user = monitUser
98 | config.monit_password = monitPassword
99 | e, err := NewExporter(config)
100 | if err != nil {
101 | t.Error("Unexpected error during exporter creation")
102 | }
103 | err = e.scrape()
104 | if err != nil {
105 | t.Error("Unexpected execution error:", err)
106 | }
107 | }
108 |
109 | func TestBasicAuthFail(t *testing.T) {
110 | handler := http.HandlerFunc(AuthHandler)
111 | server := httptest.NewServer(handler)
112 | config := ParseConfig()
113 | config.monit_scrape_uri = server.URL
114 | config.monit_user = monitUser
115 | config.monit_password = monitPassword + "qwe"
116 | e, err := NewExporter(config)
117 | if err != nil {
118 | t.Error("Unexpected error during exporter creation")
119 | }
120 | err = e.scrape()
121 | if err == nil {
122 | t.Error("Unexpected execution success:")
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/monit_exporter.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "encoding/xml"
7 | "flag"
8 | "io/ioutil"
9 | "net/http"
10 | "sync"
11 |
12 | "github.com/prometheus/client_golang/prometheus"
13 | "github.com/prometheus/log"
14 | "github.com/spf13/viper"
15 | "golang.org/x/net/html/charset"
16 | )
17 |
18 | const (
19 | namespace = "monit" // Prefix for Prometheus metrics.
20 | )
21 |
22 | var configFile = flag.String("conf", "./config.toml", "Configuration file for exporter")
23 |
24 | var serviceTypes = map[int]string{
25 | 0: "filesystem",
26 | 1: "directory",
27 | 2: "file",
28 | 3: "program with pidfile",
29 | 4: "remote host",
30 | 5: "system",
31 | 6: "fifo",
32 | 7: "program with path",
33 | 8: "network",
34 | }
35 |
36 | type monitXML struct {
37 | MonitServices []monitService `xml:"service"`
38 | }
39 |
40 | // Simplified structure of monit check.
41 | type monitService struct {
42 | Type int `xml:"type,attr"`
43 | Name string `xml:"name"`
44 | Status int `xml:"status"`
45 | Monitored string `xml:"monitor"`
46 | }
47 |
48 | // Exporter collects monit stats from the given URI and exports them using
49 | // the prometheus metrics package.
50 | type Exporter struct {
51 | config *Config
52 | mutex sync.RWMutex
53 | client *http.Client
54 |
55 | up prometheus.Gauge
56 | checkStatus *prometheus.GaugeVec
57 | }
58 |
59 | type Config struct {
60 | listen_address string
61 | metrics_path string
62 | ignore_ssl bool
63 | monit_scrape_uri string
64 | monit_user string
65 | monit_password string
66 | }
67 |
68 | func FetchMonitStatus(c *Config) ([]byte, error) {
69 | client := &http.Client{
70 | Transport: &http.Transport{
71 | TLSClientConfig: &tls.Config{InsecureSkipVerify: c.ignore_ssl},
72 | },
73 | }
74 |
75 | req, err := http.NewRequest("GET", c.monit_scrape_uri, nil)
76 | if err != nil {
77 | log.Errorf("Unable to create request: %v", err)
78 | }
79 |
80 | req.SetBasicAuth(c.monit_user, c.monit_password)
81 | resp, err := client.Do(req)
82 | if err != nil {
83 | log.Error("Unable to fetch monit status")
84 | return nil, err
85 | }
86 | data, err := ioutil.ReadAll(resp.Body)
87 | if err != nil {
88 | log.Fatal("Unable to read monit status")
89 | return nil, err
90 | }
91 | defer resp.Body.Close()
92 | return data, nil
93 | }
94 |
95 | func ParseMonitStatus(data []byte) (monitXML, error) {
96 | var statusChunk monitXML
97 | reader := bytes.NewReader(data)
98 | decoder := xml.NewDecoder(reader)
99 |
100 | // Parsing status results to structure
101 | decoder.CharsetReader = charset.NewReaderLabel
102 | err := decoder.Decode(&statusChunk)
103 | return statusChunk, err
104 | }
105 |
106 | func ParseConfig() *Config {
107 | flag.Parse()
108 |
109 | v := viper.New()
110 |
111 | v.SetDefault("listen_address", "localhost:9388")
112 | v.SetDefault("metrics_path", "/metrics")
113 | v.SetDefault("ignore_ssl", false)
114 | v.SetDefault("monit_scrape_uri", "http://localhost:2812/_status?format=xml&level=full")
115 | v.SetDefault("monit_user", "")
116 | v.SetDefault("monit_password", "")
117 | v.SetConfigFile(*configFile)
118 | v.SetConfigType("toml")
119 | err := v.ReadInConfig() // Find and read the config file
120 | if err != nil { // Handle errors reading the config file
121 | log.Printf("Error reading config file: %s. Using defaults.", err)
122 | }
123 |
124 | return &Config{
125 | listen_address: v.GetString("listen_address"),
126 | metrics_path: v.GetString("metrics_path"),
127 | ignore_ssl: v.GetBool("ignore_ssl"),
128 | monit_scrape_uri: v.GetString("monit_scrape_uri"),
129 | monit_user: v.GetString("monit_user"),
130 | monit_password: v.GetString("monit_password"),
131 | }
132 | }
133 |
134 | // Returns an initialized Exporter.
135 | func NewExporter(c *Config) (*Exporter, error) {
136 |
137 | return &Exporter{
138 | config: c,
139 | up: prometheus.NewGauge(prometheus.GaugeOpts{
140 | Namespace: namespace,
141 | Name: "exporter_up",
142 | Help: "Monit status availability",
143 | }),
144 | checkStatus: prometheus.NewGaugeVec(prometheus.GaugeOpts{
145 | Namespace: namespace,
146 | Name: "exporter_service_check",
147 | Help: "Monit service check info",
148 | },
149 | []string{"check_name", "type", "monitored"},
150 | ),
151 | }, nil
152 | }
153 |
154 | // Describe describes all the metrics ever exported by the monit exporter. It
155 | // implements prometheus.Collector.
156 | func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
157 | e.up.Describe(ch)
158 | e.checkStatus.Describe(ch)
159 | }
160 |
161 | func (e *Exporter) scrape() error {
162 | data, err := FetchMonitStatus(e.config)
163 | if err != nil {
164 | // set "monit_exporter_up" gauge to 0, remove previous metrics from e.checkStatus vector
165 | e.up.Set(0)
166 | e.checkStatus.Reset()
167 | log.Errorf("Error getting monit status: %v", err)
168 | return err
169 | } else {
170 | parsedData, err := ParseMonitStatus(data)
171 | if err != nil {
172 | e.up.Set(0)
173 | e.checkStatus.Reset()
174 | log.Errorf("Error parsing data from monit: %v", err)
175 | } else {
176 | e.up.Set(1)
177 | // Constructing metrics
178 | for _, service := range parsedData.MonitServices {
179 | e.checkStatus.With(prometheus.Labels{"check_name": service.Name, "type": serviceTypes[service.Type], "monitored": service.Monitored}).Set(float64(service.Status))
180 | }
181 | }
182 | return err
183 | }
184 | }
185 |
186 | // Collect fetches the stats from configured monit location and delivers them
187 | // as Prometheus metrics. It implements prometheus.Collector.
188 | func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
189 | e.mutex.Lock() // Protect metrics from concurrent collects.
190 | defer e.mutex.Unlock()
191 | e.checkStatus.Reset()
192 | e.scrape()
193 | e.up.Collect(ch)
194 | e.checkStatus.Collect(ch)
195 | return
196 | }
197 |
198 | func main() {
199 |
200 | config := ParseConfig()
201 | exporter, err := NewExporter(config)
202 |
203 | if err != nil {
204 | log.Fatal(err)
205 | }
206 | prometheus.MustRegister(exporter)
207 |
208 | log.Printf("Starting monit_exporter: %s", config.listen_address)
209 | http.Handle(config.metrics_path, prometheus.Handler())
210 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
211 | w.Write([]byte(`
212 |
Monit Exporter
213 |
214 | Monit Exporter
215 | Metrics
216 |
217 | `))
218 | })
219 |
220 | log.Fatal(http.ListenAndServe(config.listen_address, nil))
221 | }
222 |
--------------------------------------------------------------------------------