Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ dist/

.direnv
.vagrant
.jj

test.toml
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,37 @@ running `tc -s` in a modern way.

## config.toml

It is possible to filter the interface to fetch data from by using a `config.toml` with the following
It is possible to configure the exporter through a `config.toml` with the following
structure and keys

* `listen-address`: specifies the address on which the exporter will be running
* `log-level`: specifies the log level based on [slog levels](https://pkg.go.dev/log/slog#Level)
```go
const (
LevelDebug Level = -4
LevelInfo Level = 0
LevelWarn Level = 4
LevelError Level = 8
)
```
* `log-level`: specifies the log level. One of `debug`, `info`, `warn`, `error`.
* `[netns.<netns name>]`: Map that specifies which network namespaces to monitor by name
* `interfaces`: string array with the names of the interfaces that should be exported
* `[collector]`: enable or disable individual collectors by name. Keys use the
same names as the equivalent `--collector.<name>` CLI flags
(e.g. `qdisc`, `class`, `fq_codel`, `htb_qdisc`, `htb_class`, `hfsc_qdisc`,
`hfsc_class`, `service_curve`, `cbq`, `choke`, `codel`, `fq`, `pie`, `red`,
`sfb`, `sfq`).
* `disable-defaults`: when true, disables every collector that isn't
explicitly enabled (equivalent to `--collector.disable-defaults`).

CLI flags always win over the values in the config file. Unknown keys in the
`[collector]` section are logged at WARN level and otherwise ignored.

```
listen-address = ":9704"
log-level = 0
log-level = "info"

[netns.default]
interfaces = ['dummy','eno1']

[netns.netns01]
interfaces = ['dummy01']

[collector]
disable-defaults = false
fq_codel = true
htb_qdisc = true
htb_class = true
```
165 changes: 84 additions & 81 deletions cmd/tc_exporter/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package main

import (
"fmt"
"log/slog"
"net/http"
"os"

"net/http/pprof"

"github.com/alecthomas/kong"
"github.com/alecthomas/kingpin/v2"
tcexporter "github.com/fbegyn/tc_exporter/collector"
"github.com/jsimonetti/rtnetlink"
"github.com/prometheus/client_golang/prometheus"
Expand All @@ -17,40 +16,39 @@ import (
"github.com/spf13/viper"
)

var cli App

// Config datasructure representing the configuration file
type Config struct {
NetNS map[string]NS
Filters tcexporter.FilterHolder `mapstructure:"filters"`
ListenAddress string `mapstructure:"listen-address"`
LogLevel string `mapstructure:"log-level"`
NetNS map[string]NS `mapstructure:"netns"`
Filters tcexporter.FilterHolder `mapstructure:"filters"`
Collector map[string]bool `mapstructure:"collector"`
}

// NS holds a type alias so we can use it in the config file
type NS struct {
Interfaces []string `name:"interfaces" mapstructure:"interfaces"`
}

// App holds are the
type App struct {
Config string `help:"location of the config path" name:"config-file"`
LogLevel string `help:"slog based log level" default:"info" name:"log-level"`
ListenAddres string `help:"address to listen on" default:":9704" name:"listen-address"`
QdiscEnable bool `help:"enable the qdisc collector" negatable:"" default:"true" name:"collector-qdisc"`
ClassEnable bool `help:"enable the class collector" negatable:"" default:"true" name:"collector-class"`
CbqEnable bool `help:"enable the cbq collector" negatable:"" default:"false" name:"collector-cbq"`
ChokeEnable bool `help:"enable the choke collector" negatable:"" default:"false" name:"collector-choke"`
CodelEnable bool `help:"enable the codel collector" negatable:"" default:"false" name:"collector-codel"`
FqEnable bool `help:"enable the fq collector" negatable:"" default:"false" name:"collector-fq"`
FqcodelEnable bool `help:"enable the fqcodel collector" negatable:"" default:"false" name:"collector-fqcodel"`
HfscEnable bool `help:"enable the hfsc collector" negatable:"" default:"false" name:"collector-hfsc"`
HtbEnable bool `help:"enable the htb collector" negatable:"" default:"false" name:"collector-htb"`
PieEnable bool `help:"enable the pie collector" negatable:"" default:"false" name:"collector-pie"`
RedEnable bool `help:"enable the red collector" negatable:"" default:"false" name:"collector-red"`
SfbEnable bool `help:"enable the sfb collector" negatable:"" default:"false" name:"collector-sfb"`
SfqEnable bool `help:"enable the sfq collector" negatable:"" default:"false" name:"collector-sfq"`
// explicitFlags records non-collector flags the user passed on the CLI, so we
// know to ignore the equivalent config-file values for those keys.
var explicitFlags = map[string]bool{}

func markExplicit(name string) kingpin.Action {
return func(*kingpin.ParseContext) error {
explicitFlags[name] = true
return nil
}
}

func (a *App) Run(logger *slog.Logger, cfg Config) error {
var (
configFile = kingpin.Flag("config-file", "location of the config path").Default("").String()
logLevel = kingpin.Flag("log-level", "slog based log level").Default("info").Action(markExplicit("log-level")).String()
listenAddress = kingpin.Flag("listen-address", "address to listen on").Default(":9704").Action(markExplicit("listen-address")).String()
disableDefaults = kingpin.Flag("collector.disable-defaults", "Set all collectors to disabled by default.").Default("false").Action(markExplicit("collector.disable-defaults")).Bool()
)

func run(logger *slog.Logger, cfg Config, listen string) error {
// registering application information
prometheus.MustRegister(NewVersionCollector("tc_exporter"))

Expand All @@ -65,25 +63,8 @@ func (a *App) Run(logger *slog.Logger, cfg Config) error {
netns[ns] = interfaces
}

enabledCollectors := map[string]bool{
"qdisc": a.QdiscEnable,
"class": a.ClassEnable,
"cbq": a.CbqEnable,
"choke": a.ChokeEnable,
"codel": a.CodelEnable,
"fq": a.FqEnable,
"fq_codel": a.FqcodelEnable,
"hfsc": a.HfscEnable,
"service_curve": a.HfscEnable,
"htb": a.HtbEnable,
"pie": a.PieEnable,
"red": a.RedEnable,
"sfb": a.SfbEnable,
"sfq": a.SfqEnable,
}

// initialise the collector with the configured subcollectors
collector, err := tcexporter.NewTcCollector(netns, enabledCollectors, cfg.Filters, logger)
collector, err := tcexporter.NewTcCollector(netns, cfg.Filters, logger)
if err != nil {
slog.Error("failed to create TC collector", "err", err.Error())
return err
Expand Down Expand Up @@ -120,63 +101,85 @@ func (a *App) Run(logger *slog.Logger, cfg Config) error {
mux.Handle("/", landingPage)

// Start listening for HTTP connections.
slog.Info("starting TC exporter", "listen-address", a.ListenAddres)
if err := http.ListenAndServe(a.ListenAddres, mux); err != nil {
slog.Info("starting TC exporter", "listen-address", listen)
if err := http.ListenAndServe(listen, mux); err != nil {
slog.Error("cannot start TC exporter", "err", err.Error())
}
return nil
}

func main() {
// CLI arguments parsing
appCtx := kong.Parse(&cli,
kong.Name("tc-exporter"),
kong.Description("prometheus exporter for linux traffic control"),
kong.UsageOnError(),
kong.Vars{
"version": Version,
},
)
kingpin.CommandLine.Name = "tc-exporter"
kingpin.CommandLine.Help = "prometheus exporter for linux traffic control"
kingpin.HelpFlag.Short('h')
kingpin.Version(Version)
kingpin.Parse()

// Read config first so its values can feed into logger setup and collector
// state. CLI flags (already parsed) win over config; we track explicitness
// via explicitFlags / forcedCollectors.
var config Config
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/tc-exporter/") // path to look for the config file in
viper.AddConfigPath("$HOME/.tc-exporter") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
if *configFile != "" {
viper.AddConfigPath(*configFile)
}
if err := viper.ReadInConfig(); err != nil {
slog.Error("error reading config file", "err", err)
os.Exit(10)
}
if err := viper.Unmarshal(&config); err != nil {
slog.Error("error unmarshalling config file", "err", err)
os.Exit(11)
}

var logLevel slog.Level
switch cli.LogLevel {
// Resolve log level: CLI explicit > config file > default ("info").
level := *logLevel
if !explicitFlags["log-level"] && config.LogLevel != "" {
level = config.LogLevel
}
var slogLevel slog.Level
switch level {
case "info":
logLevel = slog.LevelInfo
slogLevel = slog.LevelInfo
case "error":
logLevel = slog.LevelError
slogLevel = slog.LevelError
case "warn":
logLevel = slog.LevelWarn
slogLevel = slog.LevelWarn
case "debug":
logLevel = slog.LevelDebug
slogLevel = slog.LevelDebug
default:
logLevel = slog.LevelInfo
slogLevel = slog.LevelInfo
}

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slogLevel}))
slog.SetDefault(logger)

var config Config
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("toml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/tc-exporter/") // path to look for the config file in
viper.AddConfigPath("$HOME/.tc-exporter") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.AddConfigPath(cli.Config)
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
slog.Error("error reading config file", "err", err, "path", "test")
os.Exit(10)
// Resolve collector configuration. disable-defaults is a reserved key in
// the [collector] section; CLI flag wins over config.
disable := *disableDefaults
if !explicitFlags["collector.disable-defaults"] {
if v, ok := config.Collector["disable-defaults"]; ok {
disable = v
}
}
err = viper.Unmarshal(&config)
if err != nil { // Handle errors reading the config file
slog.Error("error unmarshalling config file", "err", err, "path", "test")
os.Exit(11)
delete(config.Collector, "disable-defaults")
if disable {
tcexporter.DisableDefaultCollectors()
}
fmt.Println(config)
tcexporter.ApplyCollectorConfig(config.Collector, logger)

err = appCtx.Run(logger, config)
if err != nil {
slog.Error("failed to run kong app", "error", err)
// Resolve listen address: CLI explicit > config file > default.
listen := *listenAddress
if !explicitFlags["listen-address"] && config.ListenAddress != "" {
listen = config.ListenAddress
}

if err := run(logger, config, listen); err != nil {
slog.Error("failed to run tc exporter", "error", err)
os.Exit(2)
}
}
Expand Down
4 changes: 4 additions & 0 deletions collector/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type ClassCollector struct {
stats stats
}

func init() {
registerCollector("class", true, NewClassCollector)
}

// NewClassCollector create a new ClassCollector given a network interface
func NewClassCollector(netns map[string][]rtnetlink.LinkMessage, clog *slog.Logger) (ObjectCollector, error) {
// Setup logger for the class collector
Expand Down
Loading