Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make app-related metrics opt-in #111

Merged
merged 5 commits into from
Mar 17, 2024
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- App-related metrics (installed, available updates) are opt-in now, mirroring the change in Nextcloud 28

## [0.6.2] - 2023-10-15

### Changed
Expand Down
62 changes: 33 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Usage of nextcloud-exporter:
-a, --addr string Address to listen on for connections. (default ":9205")
--auth-token string Authentication token. Can replace username and password when using Nextcloud 22 or newer.
-c, --config-file string Path to YAML configuration file.
--enable-info-apps Enable gathering of apps-related metrics.
--login Use interactive login to create app password.
-p, --password string Password for connecting to Nextcloud.
-s, --server string URL to Nextcloud server.
Expand All @@ -110,15 +111,16 @@ There are three methods of configuring the nextcloud-exporter (higher methods ta

All settings can also be specified through environment variables:

| Environment variable | Flag equivalent |
|----------------------------:|:------------------|
| `NEXTCLOUD_SERVER` | --server |
| `NEXTCLOUD_USERNAME` | --username |
| `NEXTCLOUD_PASSWORD` | --password |
| `NEXTCLOUD_AUTH_TOKEN` | --auth-token |
| `NEXTCLOUD_LISTEN_ADDRESS` | --addr |
| `NEXTCLOUD_TIMEOUT` | --timeout |
| `NEXTCLOUD_TLS_SKIP_VERIFY` | --tls-skip-verify |
| Environment variable | Flag equivalent |
|----------------------------:|:-------------------|
| `NEXTCLOUD_SERVER` | --server |
| `NEXTCLOUD_USERNAME` | --username |
| `NEXTCLOUD_PASSWORD` | --password |
| `NEXTCLOUD_AUTH_TOKEN` | --auth-token |
| `NEXTCLOUD_LISTEN_ADDRESS` | --addr |
| `NEXTCLOUD_TIMEOUT` | --timeout |
| `NEXTCLOUD_TLS_SKIP_VERIFY` | --tls-skip-verify |
| `NEXTCLOUD_INFO_APPS` | --enable-info-apps |

#### Configuration file

Expand All @@ -136,6 +138,8 @@ password: "example"
listenAddress: ":9205"
timeout: "5s"
tlsSkipVerify: false
info:
apps: false
```

### Loading Credentials from Files
Expand Down Expand Up @@ -183,24 +187,24 @@ scrape_configs:

These metrics are exported by `nextcloud-exporter`:

| name | description |
|----------------------------------------|------------------------------------------------------------------------|
| nextcloud_active_users_daily_total | Number of active users in the last 24 hours |
| nextcloud_active_users_hourly_total | Number of active users in the last hour |
| nextcloud_active_users_total | Number of active users for the last five minutes |
| nextcloud_apps_installed_total | Number of currently installed apps |
| nextcloud_apps_updates_available_total | Number of apps that have available updates |
| nextcloud_database_info | Contains meta information about the database as labels. Value is always 1. |
| nextcloud_database_size_bytes | Size of database in bytes as reported from engine |
| nextcloud_exporter_info | Contains meta information of the exporter. Value is always 1. |
| nextcloud_files_total | Number of files served by the instance |
| nextcloud_free_space_bytes | Free disk space in data directory in bytes |
| nextcloud_php_info | Contains meta information about PHP as labels. Value is always 1. |
| nextcloud_php_memory_limit_bytes | Configured PHP memory limit in bytes |
| nextcloud_php_upload_max_size_bytes | Configured maximum upload size in bytes |
| nextcloud_scrape_errors_total | Counts the number of scrape errors by this collector |
| nextcloud_shares_federated_total | Number of federated shares by direction `sent` / `received` |
| name | description |
|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| nextcloud_active_users_daily_total | Number of active users in the last 24 hours |
| nextcloud_active_users_hourly_total | Number of active users in the last hour |
| nextcloud_active_users_total | Number of active users for the last five minutes |
| nextcloud_apps_installed_total | Number of currently installed apps |
| nextcloud_apps_updates_available_total | Number of apps that have available updates |
| nextcloud_database_info | Contains meta information about the database as labels. Value is always 1. |
| nextcloud_database_size_bytes | Size of database in bytes as reported from engine |
| nextcloud_exporter_info | Contains meta information of the exporter. Value is always 1. |
| nextcloud_files_total | Number of files served by the instance |
| nextcloud_free_space_bytes | Free disk space in data directory in bytes |
| nextcloud_php_info | Contains meta information about PHP as labels. Value is always 1. |
| nextcloud_php_memory_limit_bytes | Configured PHP memory limit in bytes |
| nextcloud_php_upload_max_size_bytes | Configured maximum upload size in bytes |
| nextcloud_scrape_errors_total | Counts the number of scrape errors by this collector |
| nextcloud_shares_federated_total | Number of federated shares by direction `sent` / `received` |
| nextcloud_shares_total | Number of shares by type: <br> `authlink`: shared password protected links <br> `group`: shared groups <br>`link`: all shared links <br> `user`: shared users <br> `mail`: shared by mail <br> `room`: shared with room |
| nextcloud_system_info | Contains meta information about Nextcloud as labels. Value is always 1.|
| nextcloud_up | Indicates if the metrics could be scraped by the exporter: <br>`1`: successful<br>`0`: unsuccessful (server down, server/endpoint not reachable, invalid credentials, ...) |
| nextcloud_users_total | Number of users of the instance |
| nextcloud_system_info | Contains meta information about Nextcloud as labels. Value is always 1. |
| nextcloud_up | Indicates if the metrics could be scraped by the exporter: <br>`1`: successful<br>`0`: unsuccessful (server down, server/endpoint not reachable, invalid credentials, ...) |
| nextcloud_users_total | Number of users of the instance |
24 changes: 24 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
envPassword = envPrefix + "PASSWORD"
envAuthToken = envPrefix + "AUTH_TOKEN"
envTLSSkipVerify = envPrefix + "TLS_SKIP_VERIFY"
envInfoApps = envPrefix + "INFO_APPS"
)

// RunMode signals what the main application should do after parsing the options.
Expand Down Expand Up @@ -61,9 +62,15 @@ type Config struct {
Password string `yaml:"password"`
AuthToken string `yaml:"authToken"`
TLSSkipVerify bool `yaml:"tlsSkipVerify"`
Info InfoConfig `yaml:"info"`
RunMode RunMode
}

// InfoConfig contains configuration related to what information is read from serverinfo.
type InfoConfig struct {
Apps bool `yaml:"apps"`
}

var (
errValidateNoServerURL = errors.New("need to set a server URL")
errValidateNoAuth = errors.New("need to either set username/password or a token")
Expand Down Expand Up @@ -162,6 +169,7 @@ func loadConfigFromFlags(args []string) (result Config, configFile string, err e
flags.StringVarP(&result.Password, "password", "p", defaults.Password, "Password for connecting to Nextcloud.")
flags.StringVar(&result.AuthToken, "auth-token", defaults.AuthToken, "Authentication token. Can replace username and password when using Nextcloud 22 or newer.")
flags.BoolVar(&result.TLSSkipVerify, "tls-skip-verify", defaults.TLSSkipVerify, "Skip certificate verification of Nextcloud server.")
flags.BoolVar(&result.Info.Apps, "enable-info-apps", defaults.Info.Apps, "Enable gathering of apps-related metrics.")
modeLogin := flags.Bool("login", false, "Use interactive login to create app password.")
modeVersion := flags.BoolP("version", "V", false, "Show version information and exit.")

Expand Down Expand Up @@ -212,13 +220,25 @@ func loadConfigFromEnv(getEnv func(string) string) (Config, error) {
tlsSkipVerify = value
}

infoApps := false
if rawValue := getEnv(envInfoApps); rawValue != "" {
value, err := strconv.ParseBool(rawValue)
if err != nil {
return Config{}, fmt.Errorf("can not parse value for %q: %s", envInfoApps, rawValue)
}
infoApps = value
}

result := Config{
ListenAddr: getEnv(envListenAddress),
ServerURL: getEnv(envServerURL),
Username: getEnv(envUsername),
Password: getEnv(envPassword),
AuthToken: getEnv(envAuthToken),
TLSSkipVerify: tlsSkipVerify,
Info: InfoConfig{
Apps: infoApps,
},
}

if raw := getEnv(envTimeout); raw != "" {
Expand Down Expand Up @@ -263,6 +283,10 @@ func mergeConfig(base, override Config) Config {
result.TLSSkipVerify = override.TLSSkipVerify
}

if override.Info.Apps {
result.Info.Apps = override.Info.Apps
}

return result
}

Expand Down
31 changes: 31 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ func TestConfig(t *testing.T) {
TLSSkipVerify: false,
},
},
{
desc: "auth token env, skip apps",
args: []string{
"test",
},
env: map[string]string{
envServerURL: "http://localhost",
envAuthToken: "auth-token",
envInfoApps: "true",
},
wantErr: nil,
wantConfig: Config{
ListenAddr: defaults.ListenAddr,
Timeout: defaults.Timeout,
ServerURL: "http://localhost",
AuthToken: "auth-token",
Info: InfoConfig{
Apps: true,
},
},
},
{
desc: "token file",
args: []string{
Expand Down Expand Up @@ -280,6 +301,16 @@ func TestConfig(t *testing.T) {
},
wantErr: errors.New(`error reading environment variables: can not parse value for "NEXTCLOUD_TLS_SKIP_VERIFY": invalid`),
},
{
desc: "fail parsing infoSkipApps env",
args: []string{
"test",
},
env: map[string]string{
envInfoApps: "invalid",
},
wantErr: errors.New(`error reading environment variables: can not parse value for "NEXTCLOUD_INFO_APPS": invalid`),
},
}

for _, tc := range tt {
Expand Down
52 changes: 31 additions & 21 deletions internal/metrics/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,19 @@ var (
)

type nextcloudCollector struct {
log logrus.FieldLogger
infoClient client.InfoClient
log logrus.FieldLogger
infoClient client.InfoClient
appsMetrics bool

upMetric prometheus.Gauge
scrapeErrorsMetric *prometheus.CounterVec
}

func RegisterCollector(log logrus.FieldLogger, infoClient client.InfoClient) error {
func RegisterCollector(log logrus.FieldLogger, infoClient client.InfoClient, appsMetrics bool) error {
c := &nextcloudCollector{
log: log,
infoClient: infoClient,
log: log,
infoClient: infoClient,
appsMetrics: appsMetrics,

upMetric: prometheus.NewGauge(prometheus.GaugeOpts{
Name: metricPrefix + "up",
Expand Down Expand Up @@ -149,11 +151,11 @@ func (c *nextcloudCollector) collectNextcloud(ch chan<- prometheus.Metric) error
return err
}

return readMetrics(ch, status)
return readMetrics(ch, status, c.appsMetrics)
}

func readMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error {
if err := collectSimpleMetrics(ch, status); err != nil {
func readMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo, appsMetrics bool) error {
if err := collectSimpleMetrics(ch, status, appsMetrics); err != nil {
return err
}

Expand Down Expand Up @@ -190,19 +192,13 @@ func readMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) err
return nil
}

func collectSimpleMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error {
metrics := []struct {
desc *prometheus.Desc
value float64
}{
{
desc: appsInstalledDesc,
value: float64(status.Data.Nextcloud.System.Apps.Installed),
},
{
desc: appsUpdatesDesc,
value: float64(status.Data.Nextcloud.System.Apps.AvailableUpdates),
},
type simpleMetric struct {
desc *prometheus.Desc
value float64
}

func collectSimpleMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo, appsMetrics bool) error {
metrics := []simpleMetric{
{
desc: usersDesc,
value: float64(status.Data.Nextcloud.Storage.Users),
Expand Down Expand Up @@ -240,6 +236,20 @@ func collectSimpleMetrics(ch chan<- prometheus.Metric, status *serverinfo.Server
value: float64(status.Data.Server.Database.Size),
},
}

if appsMetrics {
metrics = append(metrics, []simpleMetric{
{
desc: appsInstalledDesc,
value: float64(status.Data.Nextcloud.System.Apps.Installed),
},
{
desc: appsUpdatesDesc,
value: float64(status.Data.Nextcloud.System.Apps.AvailableUpdates),
},
}...)
}

for _, m := range metrics {
metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ func main() {
log.Infof("Nextcloud server: %s Authentication using token.", cfg.ServerURL)
}

infoURL := cfg.ServerURL + serverinfo.InfoPath
infoURL := serverinfo.InfoURL(cfg.ServerURL, !cfg.Info.Apps)

if cfg.TLSSkipVerify {
log.Warn("HTTPS certificate verification is disabled.")
}

infoClient := client.New(infoURL, cfg.Username, cfg.Password, cfg.AuthToken, cfg.Timeout, userAgent, cfg.TLSSkipVerify)
if err := metrics.RegisterCollector(log, infoClient); err != nil {
if err := metrics.RegisterCollector(log, infoClient, cfg.Info.Apps); err != nil {
log.Fatalf("Failed to register collector: %s", err)
}

Expand Down
5 changes: 0 additions & 5 deletions serverinfo/serverinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import (
"strconv"
)

const (
// InfoPath contains the path to the serverinfo endpoint.
InfoPath = "/ocs/v2.php/apps/serverinfo/api/v1/info?format=json"
)

// ServerInfo contains the complete data received from the server.
type ServerInfo struct {
Meta Meta `json:"meta"`
Expand Down
14 changes: 14 additions & 0 deletions serverinfo/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package serverinfo

import (
"fmt"
)

const (
infoPathFormat = "%s/ocs/v2.php/apps/serverinfo/api/v1/info?format=json&skipApps=%v"
)

// InfoURL constructs the URL of the info endpoint from the server base URL and optional parameters.
func InfoURL(serverURL string, skipApps bool) string {
return fmt.Sprintf(infoPathFormat, serverURL, skipApps)
}
38 changes: 38 additions & 0 deletions serverinfo/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package serverinfo

import (
"testing"
)

func TestInfoURL(t *testing.T) {
tt := []struct {
desc string
serverURL string
skipApps bool
wantURL string
}{
{
desc: "do not skip apps",
serverURL: "https://nextcloud.example.com",
wantURL: "https://nextcloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json&skipApps=false",
},
{
desc: "skip apps",
serverURL: "https://nextcloud.example.com",
skipApps: true,
wantURL: "https://nextcloud.example.com/ocs/v2.php/apps/serverinfo/api/v1/info?format=json&skipApps=true",
},
}

for _, tc := range tt {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()

url := InfoURL(tc.serverURL, tc.skipApps)
if url != tc.wantURL {
t.Errorf("got url %q, want %q", url, tc.wantURL)
}
})
}
}
Loading