Files
im_server/monitoring/exporter/scraper.go
T
2026-08-11 15:59:57 +08:00

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
}