iperf3exporter/main.go

330 lines
8.8 KiB
Go
Raw Normal View History

2021-10-20 10:08:56 +02:00
//nolint:gochecknoglobals
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.xsfx.dev/logginghandler"
)
var (
version = "dev"
commit = "none"
date = "unknown"
)
// rootCmd is the command line interface.
var rootCmd = &cobra.Command{
Use: "iperf3exporter",
Run: func(cmd *cobra.Command, args []string) {
if versionFlag {
fmt.Printf("iperf3exporter %s, commit %s, %s", version, commit, date) //nolint:forbidigo
return
}
http.Handle("/probe", logginghandler.Handler(http.HandlerFunc(probeHandler)))
log.Info().Str("listen", c.Exporter.Listen).Msg("starting...")
log.Fatal().Err(http.ListenAndServe(c.Exporter.Listen, nil)).Msg("goodbye")
},
}
// config is a struct that a config file can get unmarshaled to.
type config struct {
Exporter struct {
Listen string
Timeout time.Duration
ProcessMetrics bool `mapstructure:"process_metrics"`
}
Log struct {
JSON bool
Colors bool
}
Iperf3 struct {
Time int
}
}
// c is a global config struct instance.
var c config
var (
cfgFile string
versionFlag bool
)
var (
downloadSentBitsPerSecond = metrics.NewFloatCounter("iperf3_download_sent_bits_per_second")
downloadSentSeconds = metrics.NewFloatCounter("iperf3_download_sent_seconds")
downloadSentBytes = metrics.NewFloatCounter("iperf3_download_sent_bytes")
downloadReceivedBitsPerSecond = metrics.NewFloatCounter("iperf3_download_received_bits_per_second")
downloadReceivedSeconds = metrics.NewFloatCounter("iperf3_download_received_seconds")
downloadReceivedBytes = metrics.NewFloatCounter("iperf3_download_received_bytes")
)
var (
uploadSentBitsPerSecond = metrics.NewFloatCounter("iperf3_upload_sent_bits_per_second")
uploadSentSeconds = metrics.NewFloatCounter("iperf3_upload_sent_seconds")
uploadSentBytes = metrics.NewFloatCounter("iperf3_upload_sent_bytes")
uploadReceivedBitsPerSecond = metrics.NewFloatCounter("iperf3_upload_received_bits_per_second")
uploadReceivedSeconds = metrics.NewFloatCounter("iperf3_upload_received_seconds")
uploadReceivedBytes = metrics.NewFloatCounter("iperf3_upload_received_bytes")
)
//nolint:tagliatelle
type iperfResult struct {
End struct {
SumSent struct {
Seconds float64 `json:"seconds"`
Bytes float64 `json:"bytes"`
BitsPerSecond float64 `json:"bits_per_second"`
} `json:"sum_sent"`
SumReceived struct {
Seconds float64 `json:"seconds"`
Bytes float64 `json:"bytes"`
BitsPerSecond float64 `json:"bits_per_second"`
} `json:"sum_received"`
} `json:"end"`
}
func runIperf(ctx context.Context, target string, cmdArgs []string, logger zerolog.Logger) (iperfResult, error) {
args := []string{
"-J",
"-c",
target,
"-t", strconv.Itoa(c.Iperf3.Time),
}
args = append(args, cmdArgs...)
cmd := exec.CommandContext(ctx, "iperf3", args...)
logger.Debug().Str("cmd", cmd.String()).Msg("created command")
// Buffers to store stdout and stderr.
var outb, errb bytes.Buffer
cmd.Stdout = &outb
cmd.Stderr = &errb
if err := cmd.Run(); err != nil {
logger.Debug().
Str("stdout", outb.String()).
Str("stderr", errb.String()).
Msg("output from failed run")
return iperfResult{}, fmt.Errorf("could not run command: %w", err)
}
// Unmarshal the output to the iperf struct.
var p iperfResult
if err := json.Unmarshal(outb.Bytes(), &p); err != nil {
return iperfResult{}, fmt.Errorf("could not unmarshal result: %w", err)
}
return p, nil
}
func download(ctx context.Context, target string, logger zerolog.Logger) error {
r, err := runIperf(
ctx,
target,
[]string{"-R"},
logger,
)
if err != nil {
return fmt.Errorf("could not get download metrics: %w", err)
}
downloadSentBitsPerSecond.Set(r.End.SumSent.BitsPerSecond)
downloadSentBytes.Set(r.End.SumSent.Bytes)
downloadSentSeconds.Set(r.End.SumSent.Seconds)
downloadReceivedBitsPerSecond.Set(r.End.SumReceived.BitsPerSecond)
downloadReceivedBytes.Set(r.End.SumReceived.Bytes)
downloadReceivedSeconds.Set(r.End.SumReceived.Seconds)
return nil
}
func upload(ctx context.Context, target string, logger zerolog.Logger) error {
r, err := runIperf(
ctx,
target,
[]string{},
logger,
)
if err != nil {
return fmt.Errorf("could not get upload metrics: %w", err)
}
uploadSentBitsPerSecond.Set(r.End.SumSent.BitsPerSecond)
uploadSentBytes.Set(r.End.SumSent.Bytes)
uploadSentSeconds.Set(r.End.SumSent.Seconds)
uploadReceivedBitsPerSecond.Set(r.End.SumReceived.BitsPerSecond)
uploadReceivedBytes.Set(r.End.SumReceived.Bytes)
uploadReceivedSeconds.Set(r.End.SumReceived.Seconds)
return nil
}
func probeHandler(w http.ResponseWriter, r *http.Request) {
logger := logginghandler.Logger(r)
// Extract target.
target := r.URL.Query().Get("target")
if target == "" {
logger.Error().Msg("could not find target in url params")
http.Error(w, "could not find target in url params", http.StatusUnprocessableEntity)
return
}
logger.Debug().Str("target", target).Msg("extracted target from url params")
ctx, cancel := context.WithTimeout(context.Background(), c.Exporter.Timeout)
defer cancel()
logger.Info().Msg("getting download metrics")
if err := download(ctx, target, logger); err != nil {
logger.Error().Err(err).Msg("could not create download metrics")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
logger.Info().Msg("getting upload metrics")
if err := upload(ctx, target, logger); err != nil {
logger.Error().Err(err).Msg("could not create upload metrics")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
logger.Info().Msg("done scraping")
metrics.WritePrometheus(w, c.Exporter.ProcessMetrics)
}
func init() { //nolint:gochecknoinits,funlen
cobra.OnInitialize(initConfig)
// Version.
rootCmd.PersistentFlags().BoolVarP(&versionFlag, "version", "v", false, "print version")
// Config.
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
// Exporter.Listen.
rootCmd.PersistentFlags().String("listen", "127.0.0.1:9119", "listen string")
if err := viper.BindPFlag("exporter.listen", rootCmd.PersistentFlags().Lookup("listen")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("exporter.listen", "127.0.0.1:9119")
// Exporter.Timeout.
rootCmd.PersistentFlags().Duration("timeout", time.Minute, "scraping timeout")
if err := viper.BindPFlag("exporter.timeout", rootCmd.PersistentFlags().Lookup("timeout")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("exporter.timeout", time.Minute)
// Exporter.ProcessMetrics.
rootCmd.PersistentFlags().Bool("process-metrics", true, "exporter process metrics")
if err := viper.BindPFlag("exporter.processmetrics", rootCmd.PersistentFlags().Lookup("process-metrics")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("exporter.processmetrics", true)
// Log.JSON.
rootCmd.PersistentFlags().Bool("log-json", false, "JSON log output")
if err := viper.BindPFlag("log.json", rootCmd.PersistentFlags().Lookup("log-json")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("log.json", false)
// Log.Colors.
rootCmd.PersistentFlags().Bool("log-colors", true, "colorful log output")
2021-10-20 10:08:56 +02:00
if err := viper.BindPFlag("log.colors", rootCmd.PersistentFlags().Lookup("log-colors")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("log.colors", true)
// Iperf3.Time.
rootCmd.PersistentFlags().Int("time", 5, "time in seconds to transmit for") //nolint:gomnd
if err := viper.BindPFlag("iperf3.time", rootCmd.PersistentFlags().Lookup("time")); err != nil {
log.Fatal().Err(err).Msg("could not bind flag")
}
viper.SetDefault("iperf3.time", 5) //nolint:gomnd
}
func initConfig() {
// Load config file.
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
if err := viper.ReadInConfig(); err != nil {
log.Fatal().Err(err).Msg("error on reading config")
}
}
// Environment variable handling.
viper.SetEnvPrefix("IPERF3EXPORTER")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
// Unmarshal config.
if err := viper.Unmarshal(&c); err != nil {
log.Fatal().Err(err).Msg("could not unmarshal config")
}
// Setting up logging.
switch c.Log.JSON {
case false:
if c.Log.Colors {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
} else {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, NoColor: true})
}
case true:
log.Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
}
log.Logger = log.With().Caller().Logger()
}
func main() {
if err := rootCmd.Execute(); err != nil {
log.Fatal().Err(err).Msg("goodbye")
}
}