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

212 lines
6.7 KiB
Go

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))
}