├── .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/monitrc
172.17.0.2
28120
Linux4.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 | --------------------------------------------------------------------------------