多租户更改

This commit is contained in:
richjohnrambo
2026-08-11 15:59:57 +08:00
parent 6c800de67d
commit c2076721fc
453 changed files with 102349 additions and 47 deletions
+2
View File
@@ -0,0 +1,2 @@
Code in this folder and nested folders is licensed under Apache 2.0
http://www.apache.org/licenses/LICENSE-2.0
+7
View File
@@ -0,0 +1,7 @@
# Monitoring Support
This directory contains code related to monitoring Tinode server. Supported monitoring services are
* [Prometheus](https://prometheus.io/)
* [InfluxDB](https://www.influxdata.com/)
See [exporter/README](./exporter/README.md) for more details.
+69
View File
@@ -0,0 +1,69 @@
# Tinode Metric Exporter
This is a simple service which reads JSON monitoring data exposed by Tinode server using [expvar](https://golang.org/pkg/expvar/) and re-publishes it in other formats. Currently the supported formats are:
* [InfluxDB](https://www.influxdata.com/) [exporter](https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint) **pushes** data to its target backend. This is the default mode.
* [Prometheus](https://prometheus.io/) [exporter](https://prometheus.io/docs/instrumenting/exporters/) which exports data in [prometheus format](https://prometheus.io/docs/concepts/data_model/). The Prometheus monitoring service is expected to **pull/scrape** data from the exporter.
## Usage
Exporters are intended to run next to (pair with) Tinode servers: one Exporter per one Tinode server, i.e. a single Exporter provides metrics from a single Tinode server.
## Configuration
The exporters are configured by command-line flags:
### Common flags
* `serve_for` specifies which monitoring service the Exporter will gather metrics for; accepted values: `influxdb`, `prometheus`; default: `influxdb`.
* `tinode_addr` is the address where the Tinode instance publishes `expvar` data to scrape; default: `http://localhost:6060/stats/expvar`.
* `listen_at` is the hostname to bind to for serving the metrics; default: `:6222`.
* `instance` is the Exporter instance name (it may be exported to the upstream backend); default: `exporter`.
* `metric_list` is a comma-separated list of metrics to export; default: `Version,LiveTopics,TotalTopics,LiveSessions,ClusterLeader,TotalClusterNodes,LiveClusterNodes,memstats.Alloc`.
### InfluxDB
* `influx_push_addr` is the address of InfluxDB target server where the data gets sent; default: `http://localhost:9999/write`.
* `influx_db_version` is the version of InfluxDB (only 1.7 and 2.0 are supported); default: `1.7`.
* `influx_organization` specifies InfluxDB organization to push metrics as; default: `test`;
* `influx_bucket` is the name of InfluxDB storage bucket to store data in (used only in InfluxDB 2.0); default: `test`.
* `influx_auth_token` - InfluxDB authentication token; no default value.
* `influx_push_interval` - InfluxDB push interval in seconds; default: `30`.
#### Example
Run InfluxDB Exporter as
```
./exporter \
--serve_for=influxdb \
--tinode_addr=http://localhost:6060/stats/expvar \
--listen_at=:6222 \
--instance=exp-0 \
--influx_push_addr=http://my-influxdb-backend.net/write \
--influx_db_version=1.7 \
--influx_organization=myOrg \
--influx_auth_token=myAuthToken123 \
--influx_push_interval=30
```
This exporter will push the collected metrics to the specified backend once every 30 seconds.
### Prometheus
* `prom_namespace` is a prefix to use for metrics names. If you are monitoring multiple tinode instances you may want to use different namespaces; default: `tinode`.
* `prom_metrics_path` is the path under which to expose the metrics for scraping; default: `/metrics`.
* `prom_timeout` is the Tinode connection timeout in seconds in response to Prometheus scrapes; default: `15`.
#### Example
Run Prometheus Exporter as
```
./exporter \
--serve_for=prometheus \
--tinode_addr=http://localhost:6060/stats/expvar \
--listen_at=:6222 \
--instance=exp-0 \
--prom_namespace=tinode \
--prom_metrics_path=/metrics \
--prom_timeout=15
```
This exporter will serve data at path /metrics, on port 6222.
Once running, configure your Prometheus monitoring installation to collect data from this exporter.
Regular → Executable
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
# This scripts build and archives binaries and supporting files.
# Supported OSs: mac (darwin), windows, linux.
goplat=( darwin darwin windows linux )
# CPUs architectures: amd64 and arm64. The same order as OSs.
goarc=( amd64 arm64 amd64 amd64 )
# Number of platform+architectures.
buildCount=${#goplat[@]}
for line in $@; do
eval "$line"
done
# Strip 'v' prefix as in v0.16.4 -> 0.16.4.
version=${tag#?}
if [ -z "$version" ]; then
# Get last git tag as release version. Tag looks like 'v.1.2.3', so strip 'v'.
version=`git describe --tags`
version=${version#?}
fi
echo "Releasing exporter $version"
GOSRC=${GOPATH}/src/github.com/tinode
pushd ${GOSRC}/chat > /dev/null
# Make sure earlier builds are deleted.
rm -f ./releases/${version}/exporter*
for (( i=0; i<${buildCount}; i++ ));
do
plat="${goplat[$i]}"
arc="${goarc[$i]}"
echo "Building ${plat}/${arc}..."
# Remove possibly existing binaries from earlier builds.
rm -f ./releases/tmp/exporter*
# Environment to cros-compile for the platform.
env GOOS="${plat}" GOARCH="${arc}" go build \
-ldflags "-s -w -X main.buildstamp=`git describe --tags`" \
-o ./releases/tmp/exporter ./monitoring/exporter > /dev/null
# Build archive. All platforms but Windows use tar for archiving. Windows uses zip.
if [ "$plat" = "windows" ]; then
# Just copy the binary with .exe appended.
cp ./releases/tmp/exporter ./releases/${version}/exporter."${plat}-${arc}".exe
else
plat2=$plat
# Rename 'darwin' tp 'mac'
if [ "$plat" = "darwin" ]; then
plat2=mac
fi
# Just copy the binary.
cp ./releases/tmp/exporter ./releases/${version}/exporter."${plat2}-${arc}"
fi
done
popd > /dev/null
+211
View File
@@ -0,0 +1,211 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/version"
)
type monitoringService int
const (
promService monitoringService = 1
influxService monitoringService = 2
)
const (
// Minimum interval between InfluxDB pushes in seconds.
minPushInterval = 10
)
type promHTTPLogger struct{}
func (l promHTTPLogger) Println(v ...interface{}) {
log.Println(v...)
}
func parseMetricList(list string) []string {
metrics := strings.Split(list, ",")
for i, m := range metrics {
metrics[i] = strings.TrimSpace(m)
}
return metrics
}
// Build version number defined by the compiler:
//
// -ldflags "-X main.buildstamp=value_to_assign_to_buildstamp"
//
// Reported to clients in response to {hi} message.
// For instance, to define the buildstamp as a timestamp of when the server was built add a
// flag to compiler command line:
//
// -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`"
//
// or to set it to git tag:
//
// -ldflags "-X main.buildstamp=`git describe --tags`"
var buildstamp = "undef"
func main() {
log.Printf("Tinode metrics exporter.")
var (
serveFor = flag.String("serve_for", "influxdb",
"Monitoring service to gather metrics for. Available: influxdb, prometheus.")
tinodeAddr = flag.String("tinode_addr", "http://localhost:6060/stats/expvar",
"Address of the Tinode instance to scrape.")
listenAt = flag.String("listen_at", ":6222",
"Host name and port to listen for incoming requests on.")
metricList = flag.String("metric_list",
"Version,LiveTopics,TotalTopics,LiveSessions,ClusterLeader,TotalClusterNodes,LiveClusterNodes,memstats.Alloc",
"Comma-separated list of numeric metrics to scrape and export.")
histoMetricList = flag.String("histo_metric_list",
"RequestLatency,OutgoingMessageSize",
"Comma-separated list of histogram metrics to scrape and export.")
instance = flag.String("instance", "exporter",
"Exporter instance name.")
// Prometheus-specific arguments.
promNamespace = flag.String("prom_namespace", "tinode", "Prometheus namespace for metrics '<namespace>_...'")
promMetricsPath = flag.String("prom_metrics_path", "/metrics", "Path under which to expose metrics for Prometheus scrapes.")
promTimeout = flag.Int("prom_timeout", 15, "Tinode connection timeout in seconds in response to Prometheus scrapes.")
// InfluxDB-specific arguments.
influxPushAddr = flag.String("influx_push_addr", "http://localhost:9999/write",
"Address of InfluxDB target server where the data gets sent.")
influxDBVersion = flag.String("influx_db_version", "1.7",
"Version of InfluxDB (only 1.7 and 2.0 are supported).")
influxOrganization = flag.String("influx_organization", "test",
"InfluxDB organization to push metrics as.")
influxBucket = flag.String("influx_bucket", "test",
"InfluxDB storage bucket to store data in (used only in InfluxDB 2.0).")
influxAuthToken = flag.String("influx_auth_token", "",
"InfluxDB authentication token.")
influxPushInterval = flag.Int("influx_push_interval", 30,
"InfluxDB push interval in seconds.")
)
flag.Parse()
var service monitoringService
if *serveFor == "prometheus" {
service = promService
} else if *serveFor == "influxdb" {
service = influxService
} else {
log.Fatal("Invalid monitoring service:" + *serveFor + "; must be either \"prometheus\" or \"influxdb\"")
}
// Validate flags.
switch service {
case promService:
if *promMetricsPath == "/" {
log.Fatal("Serving metrics from / is not supported")
}
case influxService:
if *influxOrganization == "" {
log.Fatal("Must specify --influx_organization")
}
if *influxAuthToken == "" {
log.Fatal("Must specify --influx_auth_token")
}
if *influxBucket == "" {
log.Fatal("Must specify --influx_bucket")
}
if *influxDBVersion != "1.7" && *influxDBVersion != "2.0" {
log.Fatal("The --influx_db_version must be either 1.7 or 2.0")
}
if *influxPushInterval > 0 && *influxPushInterval < minPushInterval {
*influxPushInterval = minPushInterval
log.Println("The --influx_push_interval is too low, reset to", minPushInterval)
}
}
// Index page at web root.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var servingPath string
switch service {
case promService:
servingPath = "<p>Prometheus exporter path: <a href='" + *promMetricsPath + "'>Metrics</a></p>"
case influxService:
servingPath = "<p>InfluxDB push path: <a href='/push'>Push</a></p>"
}
w.Write([]byte(`<html><head><title>Tinode Exporter</title></head><body>
<h1>Tinode Exporter</h1>
<p>Server type` + *serveFor + `</p>` + servingPath +
`<h2>Build</h2>
<pre>` + version.Info() + ` ` + version.BuildContext() + `</pre>
</body></html>`))
})
metrics := parseMetricList(*metricList)
histoMetrics := parseMetricList(*histoMetricList)
scraper := Scraper{address: *tinodeAddr, simpleMetrics: metrics, histogramMetrics: histoMetrics}
var serverTypeString string
// Create exporters.
switch service {
case promService:
serverTypeString = *serveFor
promExporter := NewPromExporter(*tinodeAddr, *promNamespace, time.Duration(*promTimeout)*time.Second, &scraper)
registry := prometheus.NewRegistry()
registry.MustRegister(promExporter)
http.Handle(*promMetricsPath,
promhttp.InstrumentMetricHandler(
registry,
promhttp.HandlerFor(
registry,
promhttp.HandlerOpts{
ErrorLog: &promHTTPLogger{},
Timeout: time.Duration(*promTimeout) * time.Second,
},
),
),
)
case influxService:
serverTypeString = fmt.Sprintf("%s, version %s", *serveFor, *influxDBVersion)
influxDBExporter := NewInfluxDBExporter(*influxDBVersion, *influxPushAddr, *influxOrganization, *influxBucket,
*influxAuthToken, *instance, &scraper)
if *influxPushInterval > 0 {
go func() {
interval := time.Duration(*influxPushInterval) * time.Second
ch := time.Tick(interval)
for {
if _, ok := <-ch; ok {
if err := influxDBExporter.Push(); err != nil {
log.Println("InfluxDB push failed:", err)
}
} else {
return
}
}
}()
} else {
log.Println("InfluxDB push interval is zero. Will not push data automatically.")
}
// Forces a data push.
http.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) {
var msg string
if err := influxDBExporter.Push(); err == nil {
msg = "HTTP 200 OK"
} else {
msg = err.Error()
}
w.Write([]byte(`<html><head><title>Tinode Push</title></head><body>
<h1>Tinode Push</h1>
<pre>` + msg + `</pre>
</body></html>`))
})
}
log.Println("Reading Tinode expvar from", *tinodeAddr)
log.Printf("Exporter running at %s. Server type %s", *listenAt, serverTypeString)
log.Fatalln(http.ListenAndServe(*listenAt, nil))
}
+330
View File
@@ -0,0 +1,330 @@
package main
import (
"log"
"time"
"github.com/prometheus/client_golang/prometheus"
)
// PromExporter collects metrics in Prometheus format from a Tinode server.
type PromExporter struct {
address string
timeout time.Duration
namespace string
scraper *Scraper
up *prometheus.Desc
version *prometheus.Desc
topicsLive *prometheus.Desc
topicsTotal *prometheus.Desc
sessionsLive *prometheus.Desc
sessionsTotal *prometheus.Desc
numGoroutines *prometheus.Desc
incomingMessagesWebsockTotal *prometheus.Desc
outgoingMessagesWebsockTotal *prometheus.Desc
incomingMessagesLongpollTotal *prometheus.Desc
outgoingMessagesLongpollTotal *prometheus.Desc
incomingMessagesGrpcTotal *prometheus.Desc
outgoingMessagesGrpcTotal *prometheus.Desc
fileDownloadsTotal *prometheus.Desc
fileUploadsTotal *prometheus.Desc
ctrlCodesTotal2xx *prometheus.Desc
ctrlCodesTotal3xx *prometheus.Desc
ctrlCodesTotal4xx *prometheus.Desc
ctrlCodesTotal5xx *prometheus.Desc
clusterLeader *prometheus.Desc
clusterSize *prometheus.Desc
clusterNodesLive *prometheus.Desc
malloced *prometheus.Desc
requestLatencyMsCount *prometheus.Desc
outgoingMessageBytesCount *prometheus.Desc
}
// NewPromExporter returns an initialized Prometheus exporter.
func NewPromExporter(server, namespace string, timeout time.Duration, scraper *Scraper) *PromExporter {
return &PromExporter{
address: server,
timeout: timeout,
namespace: namespace,
scraper: scraper,
up: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"If tinode instance is reachable.",
nil,
nil,
),
version: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "version"),
"The version of this tinode instance.",
nil,
nil,
),
topicsLive: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "topics_live_count"),
"Number of currently active topics.",
nil,
nil,
),
topicsTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "topics_total"),
"Total number of topics used during instance lifetime.",
nil,
nil,
),
sessionsLive: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "sessions_live_count"),
"Number of currently active sessions.",
nil,
nil,
),
sessionsTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "sessions_total"),
"Total number of sessions since instance start.",
nil,
nil,
),
numGoroutines: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "num_goroutines"),
"Number of currently spawned goroutines.",
nil,
nil,
),
incomingMessagesWebsockTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "incoming_messages_websock_total"),
"Total number of incoming messages via websocket.",
nil,
nil,
),
outgoingMessagesWebsockTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "outgoing_messages_websock_total"),
"Total number of outgoiing messages via websocket.",
nil,
nil,
),
incomingMessagesLongpollTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "incoming_messages_longpoll_total"),
"Total number of incoming messages via longpoll.",
nil,
nil,
),
outgoingMessagesLongpollTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "outgoing_messages_longpoll_total"),
"Total number of outgoiing messages via longpoll.",
nil,
nil,
),
incomingMessagesGrpcTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "incoming_messages_grpc_total"),
"Total number of incoming messages via grpc.",
nil,
nil,
),
outgoingMessagesGrpcTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "outgoing_messages_grpc_total"),
"Total number of outgoiing messages via grpc.",
nil,
nil,
),
fileDownloadsTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "file_downloads_total"),
"Total number of large file downloads.",
nil,
nil,
),
fileUploadsTotal: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "file_uploads_total"),
"Total number of large file uploads.",
nil,
nil,
),
ctrlCodesTotal2xx: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "ctrl_codes_total_2xx"),
"Total number of 2xx ctrl response codes.",
nil,
nil,
),
ctrlCodesTotal3xx: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "ctrl_codes_total_3xx"),
"Total number of 3xx ctrl response codes.",
nil,
nil,
),
ctrlCodesTotal4xx: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "ctrl_codes_total_4xx"),
"Total number of 4xx ctrl response codes.",
nil,
nil,
),
ctrlCodesTotal5xx: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "ctrl_codes_total_5xx"),
"Total number of 5xx ctrl response codes.",
nil,
nil,
),
clusterLeader: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cluster_leader"),
"If this cluster node is the cluster leader.",
nil,
nil,
),
clusterSize: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cluster_size"),
"Configured number of cluster nodes.",
nil,
nil,
),
clusterNodesLive: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cluster_nodes_live"),
"Number of cluster nodes believed to be live by the current node.",
nil,
nil,
),
malloced: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "malloced_bytes"),
"Number of bytes of memory allocated and in use.",
nil,
nil,
),
requestLatencyMsCount: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "request_latency_ms_count"),
"Request latency histogram (in ms).",
nil,
nil,
),
outgoingMessageBytesCount: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "outgoing_message_bytes"),
"Response size histogram (in bytes).",
nil,
nil,
),
}
}
// Describe describes all the metrics exported by the memcached exporter. It
// implements prometheus.Collector.
func (e *PromExporter) Describe(ch chan<- *prometheus.Desc) {
ch <- e.up
ch <- e.version
ch <- e.topicsLive
ch <- e.topicsTotal
ch <- e.sessionsLive
ch <- e.sessionsTotal
ch <- e.numGoroutines
ch <- e.incomingMessagesWebsockTotal
ch <- e.outgoingMessagesWebsockTotal
ch <- e.incomingMessagesLongpollTotal
ch <- e.outgoingMessagesLongpollTotal
ch <- e.incomingMessagesGrpcTotal
ch <- e.outgoingMessagesGrpcTotal
ch <- e.fileDownloadsTotal
ch <- e.fileUploadsTotal
ch <- e.ctrlCodesTotal2xx
ch <- e.ctrlCodesTotal3xx
ch <- e.ctrlCodesTotal4xx
ch <- e.ctrlCodesTotal5xx
ch <- e.clusterLeader
ch <- e.clusterSize
ch <- e.clusterNodesLive
ch <- e.malloced
ch <- e.requestLatencyMsCount
ch <- e.outgoingMessageBytesCount
}
// Collect fetches statistics from the configured Tinode instance, and
// delivers them as Prometheus metrics. It implements prometheus.Collector.
func (e *PromExporter) Collect(ch chan<- prometheus.Metric) {
up := float64(1)
if stats, err := e.scraper.Scrape(); err != nil {
log.Println("Failed to fetch or parse response", err)
up = 0
} else {
if err := e.parseStats(ch, stats); err != nil {
up = 0
}
}
ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, up)
}
func (e *PromExporter) parseStats(ch chan<- prometheus.Metric, stats map[string]interface{}) error {
err := firstError(
e.parseAndUpdate(ch, e.version, prometheus.GaugeValue, stats, "Version"),
e.parseAndUpdate(ch, e.topicsLive, prometheus.GaugeValue, stats, "LiveTopics"),
e.parseAndUpdate(ch, e.topicsTotal, prometheus.CounterValue, stats, "TotalTopics"),
e.parseAndUpdate(ch, e.sessionsLive, prometheus.GaugeValue, stats, "LiveSessions"),
e.parseAndUpdate(ch, e.sessionsTotal, prometheus.CounterValue, stats, "TotalSessions"),
e.parseAndUpdate(ch, e.numGoroutines, prometheus.GaugeValue, stats, "NumGoroutines"),
e.parseAndUpdate(ch, e.incomingMessagesWebsockTotal, prometheus.CounterValue, stats, "IncomingMessagesWebsockTotal"),
e.parseAndUpdate(ch, e.outgoingMessagesWebsockTotal, prometheus.CounterValue, stats, "OutgoingMessagesWebsockTotal"),
e.parseAndUpdate(ch, e.incomingMessagesLongpollTotal, prometheus.CounterValue, stats, "IncomingMessagesLongpollTotal"),
e.parseAndUpdate(ch, e.outgoingMessagesLongpollTotal, prometheus.CounterValue, stats, "OutgoingMessagesLongpollTotal"),
e.parseAndUpdate(ch, e.incomingMessagesGrpcTotal, prometheus.CounterValue, stats, "IncomingMessagesGrpcTotal"),
e.parseAndUpdate(ch, e.outgoingMessagesGrpcTotal, prometheus.CounterValue, stats, "OutgoingMessagesGrpcTotal"),
e.parseAndUpdate(ch, e.fileDownloadsTotal, prometheus.CounterValue, stats, "FileDownloadsTotal"),
e.parseAndUpdate(ch, e.fileUploadsTotal, prometheus.CounterValue, stats, "FileUploadsTotal"),
e.parseAndUpdate(ch, e.ctrlCodesTotal2xx, prometheus.CounterValue, stats, "CtrlCodesTotal2xx"),
e.parseAndUpdate(ch, e.ctrlCodesTotal3xx, prometheus.CounterValue, stats, "CtrlCodesTotal3xx"),
e.parseAndUpdate(ch, e.ctrlCodesTotal4xx, prometheus.CounterValue, stats, "CtrlCodesTotal4xx"),
e.parseAndUpdate(ch, e.ctrlCodesTotal5xx, prometheus.CounterValue, stats, "CtrlCodesTotal5xx"),
e.parseAndUpdate(ch, e.clusterLeader, prometheus.GaugeValue, stats, "ClusterLeader"),
e.parseAndUpdate(ch, e.clusterSize, prometheus.GaugeValue, stats, "TotalClusterNodes"),
e.parseAndUpdate(ch, e.clusterNodesLive, prometheus.GaugeValue, stats, "LiveClusterNodes"),
e.parseAndUpdate(ch, e.malloced, prometheus.GaugeValue, stats, "memstats.Alloc"),
e.parseAndUpdateHisto(ch, e.requestLatencyMsCount, stats, "RequestLatency"),
e.parseAndUpdateHisto(ch, e.outgoingMessageBytesCount, stats, "OutgoingMessageSize"),
)
return err
}
func (e *PromExporter) parseAndUpdate(ch chan<- prometheus.Metric, desc *prometheus.Desc, valueType prometheus.ValueType,
stats map[string]interface{}, key string) error {
v, err := parseNumeric(stats, key)
if err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(desc, valueType, v)
return nil
}
func (e *PromExporter) parseAndUpdateHisto(ch chan<- prometheus.Metric, desc *prometheus.Desc,
stats map[string]interface{}, key string) error {
h, err := parseHisto(stats, key)
if err != nil {
return err
}
ch <- prometheus.MustNewConstHistogram(desc, h.count, h.sum, h.buckets)
return nil
}
func firstError(errs ...error) error {
for _, v := range errs {
if v != nil {
return v
}
}
return nil
}
+166
View File
@@ -0,0 +1,166 @@
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
}