Skip to content

Commit

Permalink
feat: command line arguments implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Kutovoy committed Mar 20, 2024
1 parent 33a8a22 commit d093f1b
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 77 deletions.
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Marzban Metrics Exporter is an application designed to collect and export metric
- **System Health Monitoring**: Reports on system health indicators such as total and used memory, CPU cores, CPU usage, total number of users, active users, and bandwidth metrics.
- **User Activity Tracking**: Monitors user activity, including data limits, used traffic, lifetime used traffic, and online status within the last 2 minutes.
- **Version and Start Time Information**: Includes core version information and whether the core service has started successfully.
- **Configurable via Environment Variables**: Allows customization and configuration through environment variables, making it easy to adjust settings such as metrics port, update interval, and more.
- **Configurable via Environment Variables and Command-line Arguments**: Allows customization and configuration through both environment variables and command-line arguments, making it easy to adjust settings.
- **Support for Multiple Architectures**: Docker images are available for multiple architectures, including AMD64 and ARM64, ensuring compatibility across various deployment environments.
- **Optional BasicAuth Protection**: Provides the option to secure the metrics endpoint with BasicAuth, adding an additional layer of security.
- **Integration with Prometheus**: Designed to integrate seamlessly with Prometheus, facilitating easy setup and configuration for monitoring Marzban VPN services.
Expand Down Expand Up @@ -47,22 +47,30 @@ Below is a table of the metrics provided by Marzban Metrics Exporter:

## Configuration

Below is a table of environment variables for configuring the exporter:
Marzban Metrics Exporter can be configured using environment variables or command-line arguments. When both are provided, command-line arguments take precedence.

| Variable Name | Required | Default Value | Description |
| ------------------- | -------- | -------------------------- | ------------------------------------------------------------------- |
| `MARZBAN_BASE_URL` | Yes | `https://your-marzban-url` | URL of the Marzban management panel. |
| `MARZBAN_USERNAME` | Yes | `your-marzban-username` | Username for the Marzban panel. |
| `MARZBAN_PASSWORD` | Yes | `your-marzban-password` | Password for the Marzban panel. |
| `METRICS_PORT` | No | `9090` | Port for the metrics server. |
| `METRICS_PROTECTED` | No | `false` | Enable BasicAuth protection for metrics endpoint. |
| `METRICS_USERNAME` | No | `metricsUser` | Username for BasicAuth, effective if `METRICS_PROTECTED` is `true`. |
| `METRICS_PASSWORD` | No | `MetricsVeryHardPassword` | Password for BasicAuth, effective if `METRICS_PROTECTED` is `true`. |
| `UPDATE_INTERVAL` | No | `30` | Interval (in seconds) for metrics update. |
| `TIMEZONE` | No | `UTC` | Timezone for correct time display. |
| `INACTIVITY_TIME` | No | `2` | Time (in minutes) to determine user activity. |
Below is a table of configuration options:

## How to Run
| Variable Name | Command-Line Argument | Required | Default Value | Description |
| ------------------- | --------------------- | -------- | -------------------------- | ------------------------------------------------------------------- |
| `MARZBAN_BASE_URL` | `--marzban-base-url` | Yes | `https://your-marzban-url` | URL of the Marzban management panel. |
| `MARZBAN_USERNAME` | `--marzban-username` | Yes | `your-marzban-username` | Username for the Marzban panel. |
| `MARZBAN_PASSWORD` | `--marzban-password` | Yes | `your-marzban-password` | Password for the Marzban panel. |
| `METRICS_PORT` | `--metrics-port` | No | `9090` | Port for the metrics server. |
| `METRICS_PROTECTED` | `--metrics-protected` | No | `false` | Enable BasicAuth protection for metrics endpoint. |
| `METRICS_USERNAME` | `--metrics-username` | No | `metricsUser` | Username for BasicAuth, effective if `METRICS_PROTECTED` is `true`. |
| `METRICS_PASSWORD` | `--metrics-password` | No | `MetricsVeryHardPassword` | Password for BasicAuth, effective if `METRICS_PROTECTED` is `true`. |
| `UPDATE_INTERVAL` | `--update-interval` | No | `30` | Interval (in seconds) for metrics update. |
| `TIMEZONE` | `--timezone` | No | `UTC` | Timezone for correct time display. |
| `INACTIVITY_TIME` | `--inactivity-time` | No | `2` | Time (in minutes) to determine user activity. |

## Usage

### CLI

```bash
/marzban-exporter --marzban-base-url=<your-marzban-panel-url> --marzban-username=<your-marzban-username> --marzban-password=<your-marzban-password>
```

### Docker

Expand Down Expand Up @@ -105,8 +113,8 @@ Ensure to replace `<your-marzban-panel-url>`, `<your-marzban-username>`, `<your-

## TODO

- 🚀 Ensure all necessary environment variables are set and validate them at startup.
- Implement command line arguments for passing configuration options.
- Ensure all necessary environment variables are set and validate them at startup.
- Implement command line arguments for passing configuration options.
- ⏳ Create a Grafana dashboard tailored for the Marzban Metrics Exporter to visualize the collected metrics effectively.

## Contribute
Expand Down
18 changes: 9 additions & 9 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func sendRequest(url, token string) ([]byte, error) {
}

func FetchNodesStatus(token string) {
url := fmt.Sprintf("%s/api/nodes", config.BaseURL)
url := fmt.Sprintf("%s/api/nodes", config.CLIConfig.BaseURL)
body, err := sendRequest(url, token)
if err != nil {
log.Println("Error making request for nodes status:", err)
Expand All @@ -61,7 +61,7 @@ func FetchNodesStatus(token string) {
}

func FetchNodesUsage(token string) {
url := fmt.Sprintf("%s/api/nodes/usage", config.BaseURL)
url := fmt.Sprintf("%s/api/nodes/usage", config.CLIConfig.BaseURL)
body, err := sendRequest(url, token)
if err != nil {
log.Println("Error making request for nodes usage:", err)
Expand All @@ -85,7 +85,7 @@ func FetchNodesUsage(token string) {
}

func FetchSystemStats(token string) {
url := fmt.Sprintf("%s/api/system", config.BaseURL)
url := fmt.Sprintf("%s/api/system", config.CLIConfig.BaseURL)
body, err := sendRequest(url, token)
if err != nil {
log.Println("Error making request for system stats:", err)
Expand All @@ -111,7 +111,7 @@ func FetchSystemStats(token string) {
}

func FetchCoreStatus(token string) {
url := fmt.Sprintf("%s/api/core", config.BaseURL)
url := fmt.Sprintf("%s/api/core", config.CLIConfig.BaseURL)
body, err := sendRequest(url, token)
if err != nil {
log.Println("Error making request for core status:", err)
Expand Down Expand Up @@ -139,7 +139,7 @@ func FetchCoreStatus(token string) {
}

func FetchUsersStats(token string) {
url := fmt.Sprintf("%s/api/users", config.BaseURL)
url := fmt.Sprintf("%s/api/users", config.CLIConfig.BaseURL)
body, err := sendRequest(url, token)
if err != nil {
log.Println("Error making request for user stats:", err)
Expand All @@ -152,7 +152,7 @@ func FetchUsersStats(token string) {
return
}

location, err := time.LoadLocation(config.TimeZone)
location, err := time.LoadLocation(config.CLIConfig.TimeZone)
if err != nil {
log.Println("Error setting timezone:", err)
return
Expand All @@ -167,7 +167,7 @@ func FetchUsersStats(token string) {
continue
}
onlineAt = onlineAt.In(location)
if now.Sub(onlineAt) <= time.Duration(config.InactivityTime)*time.Minute {
if now.Sub(onlineAt) <= time.Duration(config.CLIConfig.InactivityTime)*time.Minute {
onlineValue = 1
}
}
Expand All @@ -181,8 +181,8 @@ func FetchUsersStats(token string) {
}

func GetAuthToken() (string, error) {
url := fmt.Sprintf("%s/api/admin/token", config.BaseURL)
data := []byte(fmt.Sprintf("username=%s&password=%s", config.ApiUsername, config.ApiPassword))
url := fmt.Sprintf("%s/api/admin/token", config.CLIConfig.BaseURL)
data := []byte(fmt.Sprintf("username=%s&password=%s", config.CLIConfig.ApiUsername, config.CLIConfig.ApiPassword))

req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
Expand Down
50 changes: 10 additions & 40 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,18 @@
package config

import (
"os"
"strconv"
)
"marzban-exporter/models"

var (
Port = getEnv("METRICS_PORT", "9090")
ProtectedMetrics = getEnvAsBool("METRICS_PROTECTED", false)
MetricsUsername = getEnv("METRICS_USERNAME", "metricsUser")
MetricsPassword = getEnv("METRICS_PASSWORD", "MetricsVeryHardPassword")
UpdateInterval = getEnvAsInt("UPDATE_INTERVAL", 30)
TimeZone = getEnv("TIMEZONE", "UTC")
InactivityTime = getEnvAsInt("INACTIVITY_TIME", 2)
BaseURL = getEnv("MARZBAN_BASE_URL", "")
ApiUsername = getEnv("MARZBAN_USERNAME", "")
ApiPassword = getEnv("MARZBAN_PASSWORD", "")
"github.com/alecthomas/kong"
)

func getEnv(key, defaultValue string) string {
value, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}
return value
}

func getEnvAsInt(key string, defaultValue int) int {
valueStr := getEnv(key, "")
if valueStr == "" {
return defaultValue
}

value, err := strconv.Atoi(valueStr)
if err != nil {
return defaultValue
}
return value
}
var CLIConfig models.CLI

func getEnvAsBool(key string, defaultValue bool) bool {
valStr := getEnv(key, "")
if val, err := strconv.ParseBool(valStr); err == nil {
return val
}
return defaultValue
func Parse() {
ctx := kong.Parse(&CLIConfig,
kong.Name("marzban-exporter"),
kong.Description("A command-line application for exporting Marzban metrics."),
)
// Use ctx if needed
_ = ctx
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
)

require (
github.com/alecthomas/kong v0.9.0
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/google/uuid v1.4.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
Expand All @@ -12,6 +18,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
Expand Down
32 changes: 21 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func init() {
func BasicAuthMiddleware(username, password string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.ProtectedMetrics {
if config.CLIConfig.ProtectedMetrics {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("WWW-Authenticate", `Basic realm="metrics"`)
Expand All @@ -58,6 +58,7 @@ func BasicAuthMiddleware(username, password string) func(http.Handler) http.Hand
}

func main() {
config.Parse()
token, err := api.GetAuthToken()
if err != nil {
log.Println("Error getting auth token:", err)
Expand All @@ -66,27 +67,36 @@ func main() {

s := gocron.NewScheduler(time.Local)

s.Every(config.UpdateInterval).Seconds().Do(func() {
s.Every(config.CLIConfig.UpdateInterval).Seconds().Do(func() {
log.Print("Starting to collect NodesStatus metrics")
go api.FetchNodesStatus(token)
log.Print("Finished collecting NodesStatus metrics")
})
s.Every(config.UpdateInterval).Seconds().Do(func() {
s.Every(config.CLIConfig.UpdateInterval).Seconds().Do(func() {
log.Print("Starting to collect NodesUsage metrics")
go api.FetchNodesUsage(token)
log.Print("Finished collecting NodesUsage metrics")
})
s.Every(config.UpdateInterval).Seconds().Do(func() {
s.Every(config.CLIConfig.UpdateInterval).Seconds().Do(func() {
log.Print("Starting to collect SystemStats metrics")
go api.FetchSystemStats(token)
log.Print("Finished collecting SystemStats metrics")
})
s.Every(config.UpdateInterval).Seconds().Do(func() {
s.Every(config.CLIConfig.UpdateInterval).Seconds().Do(func() {
log.Print("Starting to collect UsersStats metrics")
go api.FetchUsersStats(token)
log.Print("Finished collecting UsersStats metrics")
})

s.Every(config.UpdateInterval).Seconds().Do(func() {
s.Every(config.CLIConfig.UpdateInterval).Seconds().Do(func() {
log.Print("Starting to collect CoreStatus metrics")
go api.FetchCoreStatus(token)
log.Print("Finished collecting CoreStatus metrics")
})

go s.StartAsync()

http.Handle("/metrics", BasicAuthMiddleware(config.MetricsUsername,
config.MetricsPassword)(promhttp.Handler()))
log.Printf("Starting server on :%s", config.Port)
log.Fatal(http.ListenAndServe(":"+config.Port, nil))
http.Handle("/metrics", BasicAuthMiddleware(config.CLIConfig.MetricsUsername,
config.CLIConfig.MetricsPassword)(promhttp.Handler()))
log.Printf("Starting server on :%s", config.CLIConfig.Port)
log.Fatal(http.ListenAndServe(":"+config.CLIConfig.Port, nil))
}
13 changes: 13 additions & 0 deletions models/models.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package models

type CLI struct {
Port string `name:"metrics-port" help:"Port to listen on" default:"9090" env:"METRICS_PORT"`
ProtectedMetrics bool `name:"metrics-protected" help:"Whether metrics are protected by basic auth" default:"false" env:"METRICS_PROTECTED"`
MetricsUsername string `name:"metrics-username" help:"Username for metrics if protected by basic auth" default:"metricsUser" env:"METRICS_USERNAME"`
MetricsPassword string `name:"metrics-password" help:"Password for metrics if protected by basic auth" default:"MetricsVeryHardPassword" env:"METRICS_PASSWORD"`
UpdateInterval int `name:"update-interval" help:"Interval for metrics update in seconds" default:"30" env:"UPDATE_INTERVAL"`
TimeZone string `name:"timezone" help:"Timezone used in the application" default:"UTC" env:"TIMEZONE"`
InactivityTime int `name:"inactivity-time" help:"Time in minutes after which a user is considered inactive" default:"2" env:"INACTIVITY_TIME"`
BaseURL string `name:"marzban-base-url" help:"Marzban panel base URL" env:"MARZBAN_BASE_URL" required:""`
ApiUsername string `name:"marzban-username" help:"Marzban panel username" env:"MARZBAN_USERNAME" required:""`
ApiPassword string `name:"marzban-password" help:"Marzban panel password" env:"MARZBAN_PASSWORD" required:""`
}

type AuthTokenResponse struct {
AccessToken string `json:"access_token"`
}
Expand Down

0 comments on commit d093f1b

Please sign in to comment.