167 lines
4.1 KiB
Go
167 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Scraper collects metrics from a tinode server.
|
|
type Scraper struct {
|
|
// Target Tinode server address.
|
|
address string
|
|
// List of simple numeric metrics to scrape.
|
|
simpleMetrics []string
|
|
// List of histogram metrics to scrape.
|
|
histogramMetrics []string
|
|
}
|
|
|
|
// Histogram struct.
|
|
type histogram struct {
|
|
count uint64
|
|
sum float64
|
|
buckets map[float64]uint64
|
|
}
|
|
|
|
var errKeyNotFound = errors.New("key not found")
|
|
var errMalformed = errors.New("input malformed")
|
|
|
|
// CollectRaw gathers all metrics from the configured Tinode instance,
|
|
// and returns them as a map.
|
|
func (s *Scraper) CollectRaw() (map[string]interface{}, error) {
|
|
stats, err := s.Scrape()
|
|
if err != nil {
|
|
log.Println("Failed to fetch or parse response", err)
|
|
return nil, err
|
|
}
|
|
metrics, err := s.parseStatsRaw(stats)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics["up"] = 1.0
|
|
return metrics, nil
|
|
}
|
|
|
|
// Scrape fetches the data from Tinode server using HTTP GET then decodes the response.
|
|
func (s *Scraper) Scrape() (map[string]interface{}, error) {
|
|
resp, err := http.Get(s.address)
|
|
if err != nil {
|
|
log.Println("Failed to connect to server", err)
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var stats map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&stats)
|
|
return stats, err
|
|
}
|
|
|
|
func (s *Scraper) parseStatsRaw(stats map[string]interface{}) (map[string]interface{}, error) {
|
|
metrics := make(map[string]interface{})
|
|
for _, key := range s.simpleMetrics {
|
|
if val, err := parseNumeric(stats, key); err == nil {
|
|
metrics[key] = val
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
for _, key := range s.histogramMetrics {
|
|
if val, err := parseHisto(stats, key); err == nil {
|
|
metrics[key] = val
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return metrics, nil
|
|
}
|
|
|
|
// Extracts a simple histogram from `stats` and returns a cumulative histogram
|
|
// corresponding to the simple histogram.
|
|
// Returns: (count, sum, buckets, error) tuple.
|
|
func parseHisto(stats map[string]interface{}, key string) (*histogram, error) {
|
|
// Histogram is presented as a json with the predefined fields: count, sum, count_per_bucket, bounds.
|
|
count, err := parseNumeric(stats, key+".count")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sum, err := parseNumeric(stats, key+".sum")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buckets, err := parseList(stats, key+".count_per_bucket")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bounds, err := parseList(stats, key+".bounds")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n := len(buckets)
|
|
if n != len(bounds)+1 {
|
|
return nil, errMalformed
|
|
}
|
|
result := make(map[float64]uint64)
|
|
s := uint64(0)
|
|
for i, v := range bounds {
|
|
s += uint64(buckets[i])
|
|
result[v] = s
|
|
}
|
|
return &histogram{count: uint64(count), sum: sum, buckets: result}, nil
|
|
}
|
|
|
|
// Extracts a list of numerics from `stats` for the given path.
|
|
func parseList(stats map[string]interface{}, path string) ([]float64, error) {
|
|
value, err := parseMetric(stats, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
listval, ok := value.([]interface{})
|
|
if !ok {
|
|
log.Println("Value at path is not a float64 array:", path, value)
|
|
return nil, errMalformed
|
|
}
|
|
result := []float64{}
|
|
for _, v := range listval {
|
|
result = append(result, v.(float64))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Extracts a numeric from `stats` for the given path.
|
|
func parseNumeric(stats map[string]interface{}, path string) (float64, error) {
|
|
value, err := parseMetric(stats, path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
floatval, ok := value.(float64)
|
|
if !ok {
|
|
log.Println("Value at path is not a float64:", path, value)
|
|
return 0, errKeyNotFound
|
|
}
|
|
return floatval, nil
|
|
}
|
|
|
|
// Extracts a metric from `stats` for the given path.
|
|
func parseMetric(stats map[string]interface{}, path string) (interface{}, error) {
|
|
parts := strings.Split(path, ".")
|
|
var value interface{}
|
|
var found bool
|
|
value = stats
|
|
for i := 0; i < len(parts); i++ {
|
|
subset, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
log.Println("Invalid key path:", path)
|
|
return 0, errKeyNotFound
|
|
}
|
|
value, found = subset[parts[i]]
|
|
if !found {
|
|
log.Println("Invalid key path:", path, "(", parts[i], ")")
|
|
return 0, errKeyNotFound
|
|
}
|
|
}
|
|
|
|
return value, nil
|
|
}
|