Skip to content

Commit

Permalink
Make app-related metrics opt-in (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
xperimental authored Mar 17, 2024
1 parent 8c1ed36 commit 9e80c94
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 57 deletions.
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)
}
})
}
}

0 comments on commit 9e80c94

Please sign in to comment.