多租户更改
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user