1package main
2
3import (
4	"crypto/tls"
5	"encoding/json"
6	"flag"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"log"
11	"net/http"
12	"os"
13	"time"
14
15	"github.com/prometheus/client_golang/prometheus"
16	"github.com/prometheus/client_golang/prometheus/promhttp"
17	"github.com/prometheus/common/version"
18)
19
20type NginxVts struct {
21	HostName     string `json:"hostName"`
22	NginxVersion string `json:"nginxVersion"`
23	LoadMsec     int64  `json:"loadMsec"`
24	NowMsec      int64  `json:"nowMsec"`
25	Connections  struct {
26		Active   uint64 `json:"active"`
27		Reading  uint64 `json:"reading"`
28		Writing  uint64 `json:"writing"`
29		Waiting  uint64 `json:"waiting"`
30		Accepted uint64 `json:"accepted"`
31		Handled  uint64 `json:"handled"`
32		Requests uint64 `json:"requests"`
33	} `json:"connections"`
34	ServerZones   map[string]Server              `json:"serverZones"`
35	UpstreamZones map[string][]Upstream          `json:"upstreamZones"`
36	FilterZones   map[string]map[string]Upstream `json:"filterZones"`
37	CacheZones    map[string]Cache               `json:"cacheZones"`
38}
39
40type Server struct {
41	RequestCounter uint64 `json:"requestCounter"`
42	InBytes        uint64 `json:"inBytes"`
43	OutBytes       uint64 `json:"outBytes"`
44	RequestMsec    uint64 `json:"requestMsec"`
45	Responses      struct {
46		OneXx       uint64 `json:"1xx"`
47		TwoXx       uint64 `json:"2xx"`
48		ThreeXx     uint64 `json:"3xx"`
49		FourXx      uint64 `json:"4xx"`
50		FiveXx      uint64 `json:"5xx"`
51		Miss        uint64 `json:"miss"`
52		Bypass      uint64 `json:"bypass"`
53		Expired     uint64 `json:"expired"`
54		Stale       uint64 `json:"stale"`
55		Updating    uint64 `json:"updating"`
56		Revalidated uint64 `json:"revalidated"`
57		Hit         uint64 `json:"hit"`
58		Scarce      uint64 `json:"scarce"`
59	} `json:"responses"`
60	OverCounts struct {
61		MaxIntegerSize float64 `json:"maxIntegerSize"`
62		RequestCounter uint64  `json:"requestCounter"`
63		InBytes        uint64  `json:"inBytes"`
64		OutBytes       uint64  `json:"outBytes"`
65		OneXx          uint64  `json:"1xx"`
66		TwoXx          uint64  `json:"2xx"`
67		ThreeXx        uint64  `json:"3xx"`
68		FourXx         uint64  `json:"4xx"`
69		FiveXx         uint64  `json:"5xx"`
70		Miss           uint64  `json:"miss"`
71		Bypass         uint64  `json:"bypass"`
72		Expired        uint64  `json:"expired"`
73		Stale          uint64  `json:"stale"`
74		Updating       uint64  `json:"updating"`
75		Revalidated    uint64  `json:"revalidated"`
76		Hit            uint64  `json:"hit"`
77		Scarce         uint64  `json:"scarce"`
78	} `json:"overCounts"`
79}
80
81type Upstream struct {
82	Server         string `json:"server"`
83	RequestCounter uint64 `json:"requestCounter"`
84	InBytes        uint64 `json:"inBytes"`
85	OutBytes       uint64 `json:"outBytes"`
86	Responses      struct {
87		OneXx   uint64 `json:"1xx"`
88		TwoXx   uint64 `json:"2xx"`
89		ThreeXx uint64 `json:"3xx"`
90		FourXx  uint64 `json:"4xx"`
91		FiveXx  uint64 `json:"5xx"`
92	} `json:"responses"`
93	ResponseMsec uint64 `json:"responseMsec"`
94	RequestMsec  uint64 `json:"requestMsec"`
95	Weight       uint64 `json:"weight"`
96	MaxFails     uint64 `json:"maxFails"`
97	FailTimeout  uint64 `json:"failTimeout"`
98	Backup       bool   `json:"backup"`
99	Down         bool   `json:"down"`
100	OverCounts   struct {
101		MaxIntegerSize float64 `json:"maxIntegerSize"`
102		RequestCounter uint64  `json:"requestCounter"`
103		InBytes        uint64  `json:"inBytes"`
104		OutBytes       uint64  `json:"outBytes"`
105		OneXx          uint64  `json:"1xx"`
106		TwoXx          uint64  `json:"2xx"`
107		ThreeXx        uint64  `json:"3xx"`
108		FourXx         uint64  `json:"4xx"`
109		FiveXx         uint64  `json:"5xx"`
110	} `json:"overCounts"`
111}
112
113type Cache struct {
114	MaxSize   uint64 `json:"maxSize"`
115	UsedSize  uint64 `json:"usedSize"`
116	InBytes   uint64 `json:"inBytes"`
117	OutBytes  uint64 `json:"outBytes"`
118	Responses struct {
119		Miss        uint64 `json:"miss"`
120		Bypass      uint64 `json:"bypass"`
121		Expired     uint64 `json:"expired"`
122		Stale       uint64 `json:"stale"`
123		Updating    uint64 `json:"updating"`
124		Revalidated uint64 `json:"revalidated"`
125		Hit         uint64 `json:"hit"`
126		Scarce      uint64 `json:"scarce"`
127	} `json:"responses"`
128	OverCounts struct {
129		MaxIntegerSize float64 `json:"maxIntegerSize"`
130		InBytes        uint64  `json:"inBytes"`
131		OutBytes       uint64  `json:"outBytes"`
132		Miss           uint64  `json:"miss"`
133		Bypass         uint64  `json:"bypass"`
134		Expired        uint64  `json:"expired"`
135		Stale          uint64  `json:"stale"`
136		Updating       uint64  `json:"updating"`
137		Revalidated    uint64  `json:"revalidated"`
138		Hit            uint64  `json:"hit"`
139		Scarce         uint64  `json:"scarce"`
140	} `json:"overCounts"`
141}
142
143type Exporter struct {
144	URI string
145
146	infoMetric                                                  *prometheus.Desc
147	serverMetrics, upstreamMetrics, filterMetrics, cacheMetrics map[string]*prometheus.Desc
148}
149
150func newServerMetric(metricName string, docString string, labels []string) *prometheus.Desc {
151	return prometheus.NewDesc(
152		prometheus.BuildFQName(*metricsNamespace, "server", metricName),
153		docString, labels, nil,
154	)
155}
156
157func newUpstreamMetric(metricName string, docString string, labels []string) *prometheus.Desc {
158	return prometheus.NewDesc(
159		prometheus.BuildFQName(*metricsNamespace, "upstream", metricName),
160		docString, labels, nil,
161	)
162}
163
164func newFilterMetric(metricName string, docString string, labels []string) *prometheus.Desc {
165	return prometheus.NewDesc(
166		prometheus.BuildFQName(*metricsNamespace, "filter", metricName),
167		docString, labels, nil,
168	)
169}
170
171func newCacheMetric(metricName string, docString string, labels []string) *prometheus.Desc {
172	return prometheus.NewDesc(
173		prometheus.BuildFQName(*metricsNamespace, "cache", metricName),
174		docString, labels, nil,
175	)
176}
177
178func NewExporter(uri string) *Exporter {
179	return &Exporter{
180		URI:        uri,
181		infoMetric: newServerMetric("info", "nginx info", []string{"hostName", "nginxVersion"}),
182		serverMetrics: map[string]*prometheus.Desc{
183			"connections": newServerMetric("connections", "nginx connections", []string{"status"}),
184			"requests":    newServerMetric("requests", "requests counter", []string{"host", "code"}),
185			"bytes":       newServerMetric("bytes", "request/response bytes", []string{"host", "direction"}),
186			"cache":       newServerMetric("cache", "cache counter", []string{"host", "status"}),
187			"requestMsec": newServerMetric("requestMsec", "average of request processing times in milliseconds", []string{"host"}),
188		},
189		upstreamMetrics: map[string]*prometheus.Desc{
190			"requests":     newUpstreamMetric("requests", "requests counter", []string{"upstream", "code", "backend"}),
191			"bytes":        newUpstreamMetric("bytes", "request/response bytes", []string{"upstream", "direction", "backend"}),
192			"responseMsec": newUpstreamMetric("responseMsec", "average of only upstream/backend response processing times in milliseconds", []string{"upstream", "backend"}),
193			"requestMsec":  newUpstreamMetric("requestMsec", "average of request processing times in milliseconds", []string{"upstream", "backend"}),
194		},
195		filterMetrics: map[string]*prometheus.Desc{
196			"requests":     newFilterMetric("requests", "requests counter", []string{"filter", "filterName", "code"}),
197			"bytes":        newFilterMetric("bytes", "request/response bytes", []string{"filter", "filterName", "direction"}),
198			"responseMsec": newFilterMetric("responseMsec", "average of only upstream/backend response processing times in milliseconds", []string{"filter", "filterName"}),
199			"requestMsec":  newFilterMetric("requestMsec", "average of request processing times in milliseconds", []string{"filter", "filterName"}),
200		},
201		cacheMetrics: map[string]*prometheus.Desc{
202			"requests": newCacheMetric("requests", "cache requests counter", []string{"zone", "status"}),
203			"bytes":    newCacheMetric("bytes", "cache request/response bytes", []string{"zone", "direction"}),
204		},
205	}
206}
207
208func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
209	for _, m := range e.serverMetrics {
210		ch <- m
211	}
212	for _, m := range e.upstreamMetrics {
213		ch <- m
214	}
215	for _, m := range e.filterMetrics {
216		ch <- m
217	}
218	for _, m := range e.cacheMetrics {
219		ch <- m
220	}
221}
222
223func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
224	body, err := fetchHTTP(e.URI, time.Duration(*nginxScrapeTimeout)*time.Second)()
225	if err != nil {
226		log.Println("fetchHTTP failed", err)
227		return
228	}
229	defer body.Close()
230
231	data, err := ioutil.ReadAll(body)
232	if err != nil {
233		log.Println("ioutil.ReadAll failed", err)
234		return
235	}
236
237	var nginxVtx NginxVts
238	err = json.Unmarshal(data, &nginxVtx)
239	if err != nil {
240		log.Println("json.Unmarshal failed", err)
241		return
242	}
243
244	// info
245	uptime := (nginxVtx.NowMsec - nginxVtx.LoadMsec) / 1000
246	ch <- prometheus.MustNewConstMetric(e.infoMetric, prometheus.GaugeValue, float64(uptime), nginxVtx.HostName, nginxVtx.NginxVersion)
247
248	// connections
249	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Active), "active")
250	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Reading), "reading")
251	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Waiting), "waiting")
252	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Writing), "writing")
253	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Accepted), "accepted")
254	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Handled), "handled")
255	ch <- prometheus.MustNewConstMetric(e.serverMetrics["connections"], prometheus.GaugeValue, float64(nginxVtx.Connections.Requests), "requests")
256
257	// ServerZones
258	for host, s := range nginxVtx.ServerZones {
259		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.RequestCounter), host, "total")
260		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.Responses.OneXx), host, "1xx")
261		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.Responses.TwoXx), host, "2xx")
262		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.Responses.ThreeXx), host, "3xx")
263		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.Responses.FourXx), host, "4xx")
264		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requests"], prometheus.CounterValue, float64(s.Responses.FiveXx), host, "5xx")
265
266		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Bypass), host, "bypass")
267		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Expired), host, "expired")
268		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Hit), host, "hit")
269		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Miss), host, "miss")
270		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Revalidated), host, "revalidated")
271		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Scarce), host, "scarce")
272		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Stale), host, "stale")
273		ch <- prometheus.MustNewConstMetric(e.serverMetrics["cache"], prometheus.CounterValue, float64(s.Responses.Updating), host, "updating")
274
275		ch <- prometheus.MustNewConstMetric(e.serverMetrics["bytes"], prometheus.CounterValue, float64(s.InBytes), host, "in")
276		ch <- prometheus.MustNewConstMetric(e.serverMetrics["bytes"], prometheus.CounterValue, float64(s.OutBytes), host, "out")
277
278		ch <- prometheus.MustNewConstMetric(e.serverMetrics["requestMsec"], prometheus.GaugeValue, float64(s.RequestMsec), host)
279
280	}
281
282	// UpstreamZones
283	for name, upstreamList := range nginxVtx.UpstreamZones {
284		for _, s := range upstreamList {
285			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["responseMsec"], prometheus.GaugeValue, float64(s.ResponseMsec), name, s.Server)
286			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requestMsec"], prometheus.GaugeValue, float64(s.RequestMsec), name, s.Server)
287
288			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.RequestCounter), name, "total", s.Server)
289			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.Responses.OneXx), name, "1xx", s.Server)
290			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.Responses.TwoXx), name, "2xx", s.Server)
291			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.Responses.ThreeXx), name, "3xx", s.Server)
292			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.Responses.FourXx), name, "4xx", s.Server)
293			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["requests"], prometheus.CounterValue, float64(s.Responses.FiveXx), name, "5xx", s.Server)
294
295			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["bytes"], prometheus.CounterValue, float64(s.InBytes), name, "in", s.Server)
296			ch <- prometheus.MustNewConstMetric(e.upstreamMetrics["bytes"], prometheus.CounterValue, float64(s.OutBytes), name, "out", s.Server)
297		}
298	}
299
300	// FilterZones
301	for filter, values := range nginxVtx.FilterZones {
302		for name, stat := range values {
303			ch <- prometheus.MustNewConstMetric(e.filterMetrics["responseMsec"], prometheus.GaugeValue, float64(stat.ResponseMsec), filter, name)
304			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requestMsec"], prometheus.GaugeValue, float64(stat.RequestMsec), filter, name)
305			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.RequestCounter), filter, name, "total")
306			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.Responses.OneXx), filter, name, "1xx")
307			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.Responses.TwoXx), filter, name, "2xx")
308			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.Responses.ThreeXx), filter, name, "3xx")
309			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.Responses.FourXx), filter, name, "4xx")
310			ch <- prometheus.MustNewConstMetric(e.filterMetrics["requests"], prometheus.CounterValue, float64(stat.Responses.FiveXx), filter, name, "5xx")
311
312			ch <- prometheus.MustNewConstMetric(e.filterMetrics["bytes"], prometheus.CounterValue, float64(stat.InBytes), filter, name, "in")
313			ch <- prometheus.MustNewConstMetric(e.filterMetrics["bytes"], prometheus.CounterValue, float64(stat.OutBytes), filter, name, "out")
314		}
315	}
316
317	// CacheZones
318	for zone, s := range nginxVtx.CacheZones {
319		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Bypass), zone, "bypass")
320		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Expired), zone, "expired")
321		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Hit), zone, "hit")
322		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Miss), zone, "miss")
323		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Revalidated), zone, "revalidated")
324		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Scarce), zone, "scarce")
325		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Stale), zone, "stale")
326		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["requests"], prometheus.CounterValue, float64(s.Responses.Updating), zone, "updating")
327
328		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["bytes"], prometheus.CounterValue, float64(s.InBytes), zone, "in")
329		ch <- prometheus.MustNewConstMetric(e.cacheMetrics["bytes"], prometheus.CounterValue, float64(s.OutBytes), zone, "out")
330	}
331}
332
333func fetchHTTP(uri string, timeout time.Duration) func() (io.ReadCloser, error) {
334	http.DefaultClient.Timeout = timeout
335	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: *insecure}
336
337	return func() (io.ReadCloser, error) {
338		resp, err := http.DefaultClient.Get(uri)
339		if err != nil {
340			return nil, err
341		}
342		if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
343			resp.Body.Close()
344			return nil, fmt.Errorf("HTTP status %d", resp.StatusCode)
345		}
346		return resp.Body, nil
347	}
348}
349
350var (
351	showVersion        = flag.Bool("version", false, "Print version information.")
352	listenAddress      = flag.String("telemetry.address", ":9913", "Address on which to expose metrics.")
353	metricsEndpoint    = flag.String("telemetry.endpoint", "/metrics", "Path under which to expose metrics.")
354	metricsNamespace   = flag.String("metrics.namespace", "nginx", "Prometheus metrics namespace.")
355	nginxScrapeURI     = flag.String("nginx.scrape_uri", "http://localhost/status", "URI to nginx stub status page")
356	insecure           = flag.Bool("insecure", true, "Ignore server certificate if using https")
357	nginxScrapeTimeout = flag.Int("nginx.scrape_timeout", 2, "The number of seconds to wait for an HTTP response from the nginx.scrape_uri")
358	goMetrics          = flag.Bool("go.metrics", false, "Export process and go metrics.")
359)
360
361func init() {
362	prometheus.MustRegister(version.NewCollector("nginx_vts_exporter"))
363}
364
365func main() {
366	flag.Parse()
367
368	if *showVersion {
369		fmt.Fprintln(os.Stdout, version.Print("nginx_vts_exporter"))
370		os.Exit(0)
371	}
372
373	log.Printf("Starting nginx_vts_exporter %s", version.Info())
374	log.Printf("Build context %s", version.BuildContext())
375
376	exporter := NewExporter(*nginxScrapeURI)
377	prometheus.MustRegister(exporter)
378
379	if !(*goMetrics) {
380		prometheus.Unregister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{
381			PidFn: func() (int, error) {
382				return os.Getpid(), nil
383			},
384		}))
385		prometheus.Unregister(prometheus.NewGoCollector())
386	}
387
388	http.Handle(*metricsEndpoint, promhttp.Handler())
389	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
390		w.Write([]byte(`<html>
391			<head><title>Nginx Exporter</title></head>
392			<body>
393			<h1>Nginx Exporter</h1>
394			<p><a href="` + *metricsEndpoint + `">Metrics</a></p>
395			</body>
396			</html>`))
397	})
398
399	log.Printf("Starting Server at : %s", *listenAddress)
400	log.Printf("Metrics endpoint: %s", *metricsEndpoint)
401	log.Printf("Metrics namespace: %s", *metricsNamespace)
402	log.Printf("Scraping information from : %s", *nginxScrapeURI)
403	log.Fatal(http.ListenAndServe(*listenAddress, nil))
404}
405