Skip to content

Commit

Permalink
feat: Optionally extract replication status for artifactory_replicati…
Browse files Browse the repository at this point in the history
…on_enabled metric (#88)

Co-authored-by: Peiman Jafari <[email protected]>
  • Loading branch information
peimanja and Peiman Jafari authored Dec 7, 2022
1 parent 3d4c857 commit e2fa887
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 33 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ Flags:
URI on which to scrape JFrog Artifactory.
--artifactory.ssl-verify Flag that enables SSL certificate verification for the scrape URI
--artifactory.timeout=5s Timeout for trying to get stats from JFrog Artifactory.
--optional-metric=metric-name ...
optional metric to be enabled. Pass multiple times to enable multiple optional metrics.
--log.level=info Only log messages with the given severity or above. One of: [debug, info, warn, error]
--log.format=logfmt Output format of log messages. One of: [logfmt, json]
--version Show application version.
Expand All @@ -138,6 +140,7 @@ Flags:
| `artifactory.scrape-uri`<br/>`ARTI_SCRAPE_URI` | No | `http://localhost:8081/artifactory` | URI on which to scrape JFrog Artifactory. |
| `artifactory.ssl-verify`<br/>`ARTI_SSL_VERIFY` | No | `true` | Flag that enables SSL certificate verification for the scrape URI. |
| `artifactory.timeout`<br/>`ARTI_TIMEOUT` | No | `5s` | Timeout for trying to get stats from JFrog Artifactory. |
| `optional-metric`| No | | optional metric to be enabled. Pass multiple times to enable multiple optional metrics. |
| `log.level` | No | `info` | Only log messages with the given severity or above. One of: [debug, info, warn, error]. |
| `log.format` | No | `logfmt` | Output format of log messages. One of: [logfmt, json]. |
| `ARTI_USERNAME` | *No | | User to access Artifactory |
Expand All @@ -157,7 +160,7 @@ Some metrics are not available with Artifactory OSS license. The exporter return
| artifactory_exporter_total_scrapes | Current total artifactory scrapes. | | &#9989; |
| artifactory_exporter_total_api_errors | Current total Artifactory API errors when scraping for stats. | | &#9989; |
| artifactory_exporter_json_parse_failures |Number of errors while parsing Json. | | &#9989; |
| artifactory_replication_enabled | Replication status for an Artifactory repository (1 = enabled). | `name`, `type`, `cron_exp` | |
| artifactory_replication_enabled | Replication status for an Artifactory repository (1 = enabled). | `name`, `type`, `cron_exp`, `status` | |
| artifactory_security_groups | Number of Artifactory groups. | | |
| artifactory_security_users | Number of Artifactory users for each realm. | `realm` | |
| artifactory_storage_artifacts | Total artifacts count stored in Artifactory. | | &#9989; |
Expand All @@ -181,6 +184,13 @@ Some metrics are not available with Artifactory OSS license. The exporter return
| artifactory_system_license | License type and expiry as labels, seconds to expiration as value | `type`, `licensed_to`, `expires` | &#9989; |
| artifactory_system_version | Version and revision of Artifactory as labels. | `version`, `revision` | &#9989; |

#### Optional metrics

Some metrics are expensive to compute and are disabled by default. To enable them, use `--optional-metric=metric_name` flag. Use this with caution as it may impact the performance in Artifactory instances with many repositories.

Supported optional metrics:

* `replication_status` - Extracts status of replication for each repository which has replication enabled. Enabling this will add the `status` label to `artifactory_replication_enabled` metric.

### Grafana Dashboard

Expand Down
22 changes: 12 additions & 10 deletions artifactory/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (

// Client represents Artifactory HTTP Client
type Client struct {
URI string
authMethod string
cred config.Credentials
client *http.Client
logger log.Logger
URI string
authMethod string
cred config.Credentials
optionalMetrics config.OptionalMetrics
client *http.Client
logger log.Logger
}

// NewClient returns an initialized Artifactory HTTP Client.
Expand All @@ -25,10 +26,11 @@ func NewClient(conf *config.Config) *Client {
Transport: tr,
}
return &Client{
URI: conf.ArtiScrapeURI,
authMethod: conf.Credentials.AuthMethod,
cred: *conf.Credentials,
client: client,
logger: conf.Logger,
URI: conf.ArtiScrapeURI,
authMethod: conf.Credentials.AuthMethod,
cred: *conf.Credentials,
optionalMetrics: conf.OptionalMetrics,
client: client,
logger: conf.Logger,
}
}
29 changes: 29 additions & 0 deletions artifactory/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package artifactory

import (
"encoding/json"
"fmt"

"github.com/go-kit/kit/log/level"
)

const replicationEndpoint = "replications"
const replicationStatusEndpoint = "replication"

// Replication represents single element of API respond from replication endpoint
type Replication struct {
Expand All @@ -21,12 +23,17 @@ type Replication struct {
EnableEventReplication bool `json:"enableEventReplication"`
CheckBinaryExistenceInFilestore bool `json:"checkBinaryExistenceInFilestore"`
SyncStatistics bool `json:"syncStatistics"`
Status string `json:"status"`
}
type Replications struct {
Replications []Replication
NodeId string
}

type ReplicationStatus struct {
Status string `json:"status"`
}

// FetchReplications makes the API call to replication endpoint and returns []Replication
func (c *Client) FetchReplications() (Replications, error) {
var replications Replications
Expand All @@ -47,5 +54,27 @@ func (c *Client) FetchReplications() (Replications, error) {
endpoint: replicationEndpoint,
}
}

if c.optionalMetrics.ReplicationStatus {
level.Debug(c.logger).Log("msg", "Fetching replications status")
for i, replication := range replications.Replications {
var status ReplicationStatus
if replication.Enabled {
statusResp, err := c.FetchHTTP(fmt.Sprintf("%s/%s", replicationStatusEndpoint, replication.RepoKey))
if err != nil {
return replications, err
}
if err := json.Unmarshal(statusResp.Body, &status); err != nil {
level.Error(c.logger).Log("msg", "There was an issue when try to unmarshal replication status respond")
return replications, &UnmarshalError{
message: err.Error(),
endpoint: fmt.Sprintf("%s/%s", replicationStatusEndpoint, replication.RepoKey),
}
}
replications.Replications[i].Status = status.Status
}
}
}

return replications, nil
}
2 changes: 1 addition & 1 deletion collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var (
defaultLabelNames = []string{"node_id"}
filestoreLabelNames = append([]string{"storage_type", "storage_dir"}, defaultLabelNames...)
repoLabelNames = append([]string{"name", "type", "package_type"}, defaultLabelNames...)
replicationLabelNames = append([]string{"name", "type", "url", "cron_exp"}, defaultLabelNames...)
replicationLabelNames = append([]string{"name", "type", "url", "cron_exp", "status"}, defaultLabelNames...)
)

func newMetric(metricName string, subsystem string, docString string, labelNames []string) *prometheus.Desc {
Expand Down
5 changes: 3 additions & 2 deletions collector/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ func (e *Exporter) exportReplications(ch chan<- prometheus.Metric) error {
rType := strings.ToLower(replication.ReplicationType)
rURL := strings.ToLower(replication.URL)
cronExp := replication.CronExp
level.Debug(e.logger).Log("msg", "Registering metric", "metric", metricName, "repo", replication.RepoKey, "type", rType, "url", rURL, "cron", cronExp, "value", enabled)
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, enabled, repo, rType, rURL, cronExp, replications.NodeId)
status := replication.Status
level.Debug(e.logger).Log("msg", "Registering metric", "metric", metricName, "repo", replication.RepoKey, "type", rType, "url", rURL, "cron", cronExp, "status", status, "value", enabled)
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, enabled, repo, rType, rURL, cronExp, status, replications.NodeId)
}
}
}
Expand Down
57 changes: 38 additions & 19 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import (
)

var (
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Envar("WEB_LISTEN_ADDR").Default(":9531").String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Envar("WEB_TELEMETRY_PATH").Default("/metrics").String()
artiScrapeURI = kingpin.Flag("artifactory.scrape-uri", "URI on which to scrape JFrog Artifactory.").Envar("ARTI_SCRAPE_URI").Default("http://localhost:8081/artifactory").String()
artiSSLVerify = kingpin.Flag("artifactory.ssl-verify", "Flag that enables SSL certificate verification for the scrape URI").Envar("ARTI_SSL_VERIFY").Default("false").Bool()
artiTimeout = kingpin.Flag("artifactory.timeout", "Timeout for trying to get stats from JFrog Artifactory.").Envar("ARTI_TIMEOUT").Default("5s").Duration()
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Envar("WEB_LISTEN_ADDR").Default(":9531").String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Envar("WEB_TELEMETRY_PATH").Default("/metrics").String()
artiScrapeURI = kingpin.Flag("artifactory.scrape-uri", "URI on which to scrape JFrog Artifactory.").Envar("ARTI_SCRAPE_URI").Default("http://localhost:8081/artifactory").String()
artiSSLVerify = kingpin.Flag("artifactory.ssl-verify", "Flag that enables SSL certificate verification for the scrape URI").Envar("ARTI_SSL_VERIFY").Default("false").Bool()
artiTimeout = kingpin.Flag("artifactory.timeout", "Timeout for trying to get stats from JFrog Artifactory.").Envar("ARTI_TIMEOUT").Default("5s").Duration()
optionalMetrics = kingpin.Flag("optional-metric", "optional metric to be enabled. Pass multiple times to enable multiple optional metrics.").PlaceHolder("metric-name").Strings()
)

var optionalMetricsList = []string{"replication_status"}

// Credentials represents Username and Password or API Key for
// Artifactory Authentication
type Credentials struct {
Expand All @@ -30,15 +33,20 @@ type Credentials struct {
AccessToken string `required:"false" envconfig:"ARTI_ACCESS_TOKEN"`
}

type OptionalMetrics struct {
ReplicationStatus bool
}

// Config represents all configuration options for running the Exporter.
type Config struct {
ListenAddress string
MetricsPath string
ArtiScrapeURI string
Credentials *Credentials
ArtiSSLVerify bool
ArtiTimeout time.Duration
Logger log.Logger
ListenAddress string
MetricsPath string
ArtiScrapeURI string
Credentials *Credentials
ArtiSSLVerify bool
ArtiTimeout time.Duration
OptionalMetrics OptionalMetrics
Logger log.Logger
}

// NewConfig Creates new Artifactory exporter Config
Expand Down Expand Up @@ -69,14 +77,25 @@ func NewConfig() (*Config, error) {
return nil, err
}

optMetrics := OptionalMetrics{}
for _, metric := range *optionalMetrics {
switch metric {
case "replication_status":
optMetrics.ReplicationStatus = true
default:
return nil, fmt.Errorf("unknown optional metric: %s. Valid optional metrics are: %s", metric, optionalMetricsList)
}
}

return &Config{
ListenAddress: *listenAddress,
MetricsPath: *metricsPath,
ArtiScrapeURI: *artiScrapeURI,
Credentials: &credentials,
ArtiSSLVerify: *artiSSLVerify,
ArtiTimeout: *artiTimeout,
Logger: logger,
ListenAddress: *listenAddress,
MetricsPath: *metricsPath,
ArtiScrapeURI: *artiScrapeURI,
Credentials: &credentials,
ArtiSSLVerify: *artiSSLVerify,
ArtiTimeout: *artiTimeout,
OptionalMetrics: optMetrics,
Logger: logger,
}, nil

}

0 comments on commit e2fa887

Please sign in to comment.