You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
257 lines
7.1 KiB
257 lines
7.1 KiB
package main |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"flag" |
|
"fmt" |
|
"io/ioutil" |
|
"net/http" |
|
"net/url" |
|
"strings" |
|
"time" |
|
|
|
"github.com/prometheus/client_golang/prometheus" |
|
"github.com/prometheus/client_golang/prometheus/promhttp" |
|
"github.com/prometheus/common/log" |
|
"github.com/prometheus/common/version" |
|
) |
|
|
|
const ( |
|
metricsPath = "/metrics" |
|
namespace = "app" |
|
) |
|
|
|
var ( |
|
listenAddress = flag.String("listen", ":8080", "The address to listen on for HTTP requests.") |
|
endpointApp = flag.String("endpoint", "http://localhost:8050/stats", "HTTP API address of the application") |
|
timeoutApp = flag.Int("timeout", 500, "Connection timeout in ms") |
|
environment = flag.String("environment", "", "Optional environment which will be added to the exported metrics") |
|
prometheusConstLabel = parseConstLabel() |
|
) |
|
|
|
// run before creating the descriptor |
|
func parseConstLabel() prometheus.Labels { |
|
// parse flags in an early state, so we can retrieve the instance id |
|
flag.Parse() |
|
|
|
// generate constant label if environment is present |
|
if *environment != "" { |
|
return prometheus.Labels{"environment": *environment} |
|
} |
|
return prometheus.Labels{} |
|
} |
|
|
|
var ( |
|
// Create the prometheus descriptors |
|
descUp = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "up"), |
|
"Was the last query successful.", |
|
nil, prometheusConstLabel, |
|
) |
|
descRequestCount = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "request_count"), |
|
"How many requests processed, partitioned by status code.", |
|
[]string{"code"}, prometheusConstLabel, |
|
) |
|
descRequestCountTotal = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "request_count_total"), |
|
"How many requests processed of all status codes.", |
|
nil, prometheusConstLabel, |
|
) |
|
descRequestRates = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "request_rates"), |
|
"How many requests processed in the last second, partitioned by status code.", |
|
[]string{"code"}, prometheusConstLabel, |
|
) |
|
descRequestRatesTotal = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "request_rates_total"), |
|
"How many requests processed in the last second.", |
|
nil, prometheusConstLabel, |
|
) |
|
descDurationSum = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "duration_sum"), |
|
"How much time consumed the requests in summary.", |
|
nil, prometheusConstLabel, |
|
) |
|
descDurationAvg = prometheus.NewDesc( |
|
prometheus.BuildFQName(namespace, "", "duration_avg"), |
|
"How much time consumed the requests in average.", |
|
nil, prometheusConstLabel, |
|
) |
|
) |
|
|
|
// AppStats represent the schema of the returned json |
|
type AppStats struct { |
|
// RequestCounters are the served status codes during app lifetime |
|
RequestCounters map[string]int `json:"requestCounters"` |
|
// RequestRates are the served status codes for the last second (QPS) |
|
RequestRates map[string]int `json:"requestRates"` |
|
// Duration represent some request stats during the lifetime |
|
Duration *AppDuration `json:"duration"` |
|
} |
|
|
|
// AppDuration the schema of the returned duration part of the json |
|
type AppDuration struct { |
|
// Count is the total served request in the lifetime |
|
Count int `json:"count"` |
|
// Sum is the total time of taken time the requests have taken in seconds |
|
Sum float64 `json:"sum"` |
|
// Average time of usage a request has taken. |
|
Average float64 `json:"average"` |
|
} |
|
|
|
// appScraper is a helper to retrieve stats in a generic way |
|
type appScraper struct { |
|
endpoint string |
|
client *http.Client |
|
} |
|
|
|
// stats returns the fetched and parsed json |
|
func (s *appScraper) stats() (*AppStats, error) { |
|
var stats AppStats |
|
response, err := s.client.Get(s.endpoint) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
buf, err := ioutil.ReadAll(response.Body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
err = json.Unmarshal(buf, &stats) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// validate returned json is complete |
|
// requestCounter and requestRates should be empty maps, so we are fine here |
|
if stats.Duration == nil { |
|
return nil, errors.New("Invalid JSON returned, could not retrieve duration") |
|
} |
|
|
|
return &stats, err |
|
} |
|
|
|
// Exporter implements prometheus.Collector |
|
type Exporter struct { |
|
scraper *appScraper |
|
} |
|
|
|
// Describe implements prometheus.Describe |
|
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { |
|
ch <- descUp |
|
ch <- descRequestCount |
|
ch <- descRequestCountTotal |
|
ch <- descRequestRates |
|
ch <- descDurationSum |
|
ch <- descDurationAvg |
|
|
|
} |
|
|
|
// Collect implements prometheus.Describe.Collect |
|
func (e *Exporter) Collect(ch chan<- prometheus.Metric) { |
|
|
|
stats, err := e.scraper.stats() |
|
if err != nil { |
|
ch <- prometheus.MustNewConstMetric( |
|
descUp, prometheus.GaugeValue, 0, |
|
) |
|
log.Error("Failed to scrape app stats: ", err) |
|
return |
|
} |
|
ch <- prometheus.MustNewConstMetric( |
|
descUp, prometheus.GaugeValue, 1, |
|
) |
|
|
|
// Add counter per code on the fly (no need to update code if there are additional codes) |
|
for code, count := range stats.RequestCounters { |
|
ch <- prometheus.MustNewConstMetric( |
|
descRequestCount, prometheus.CounterValue, float64(count), code, |
|
) |
|
} |
|
|
|
// Add total of all requests |
|
ch <- prometheus.MustNewConstMetric( |
|
descRequestCountTotal, prometheus.CounterValue, float64(stats.Duration.Count), |
|
) |
|
|
|
// Add rates per code on the fly (no need to update code if there are additional codes) |
|
ratesSum := 0 |
|
for code, count := range stats.RequestRates { |
|
ch <- prometheus.MustNewConstMetric( |
|
descRequestRates, prometheus.GaugeValue, float64(count), code, |
|
) |
|
ratesSum += count |
|
} |
|
// Additional sum of the rates, such like the requestCounter |
|
ch <- prometheus.MustNewConstMetric( |
|
descRequestRatesTotal, prometheus.CounterValue, float64(ratesSum), |
|
) |
|
|
|
ch <- prometheus.MustNewConstMetric( |
|
descDurationSum, prometheus.CounterValue, stats.Duration.Sum, |
|
) |
|
ch <- prometheus.MustNewConstMetric( |
|
descDurationAvg, prometheus.GaugeValue, stats.Duration.Average, |
|
) |
|
|
|
} |
|
|
|
// NewExporter creates a new prometheus exporter and app scraper |
|
func NewExporter(endpoint string) (*Exporter, error) { |
|
|
|
if !strings.Contains(endpoint, "://") { |
|
endpoint = "http://" + endpoint |
|
} |
|
u, err := url.Parse(endpoint) |
|
if err != nil { |
|
return nil, fmt.Errorf("invalid endpoint URL: %s", err) |
|
} |
|
if u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { |
|
return nil, fmt.Errorf("invalid endpoint URL: %s", endpoint) |
|
} |
|
|
|
// use custom http client with specific timeout |
|
client := &http.Client{ |
|
Timeout: time.Duration(*timeoutApp) * time.Millisecond, |
|
} |
|
|
|
// create api client |
|
appScraper := &appScraper{ |
|
client: client, |
|
endpoint: endpoint, |
|
} |
|
|
|
return &Exporter{ |
|
scraper: appScraper, |
|
}, nil |
|
} |
|
|
|
func init() { |
|
prometheus.MustRegister(version.NewCollector(fmt.Sprintf("%s_exporter", namespace))) |
|
} |
|
|
|
func main() { |
|
|
|
exporter, err := NewExporter(*endpointApp) |
|
if err != nil { |
|
log.Fatal(err) |
|
} |
|
prometheus.MustRegister(exporter) |
|
|
|
http.Handle(metricsPath, promhttp.Handler()) |
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
w.Write([]byte(`<html> |
|
<head><title>App Exporter</title></head> |
|
<body> |
|
<h1>App Exporter</h1> |
|
<p><a href='` + metricsPath + `'>Metrics</a></p> |
|
</body> |
|
</html>`)) |
|
}) |
|
|
|
log.Infoln("Listening on", *listenAddress) |
|
log.Fatal(http.ListenAndServe(*listenAddress, nil)) |
|
}
|
|
|