diff --git a/artifactory/system.go b/artifactory/system.go index d3f5fb4..f21aa4c 100644 --- a/artifactory/system.go +++ b/artifactory/system.go @@ -2,6 +2,10 @@ package artifactory import ( "encoding/json" + "fmt" + "slices" + "strings" + "time" ) const ( @@ -69,6 +73,40 @@ type LicenseInfo struct { NodeId string } +func (l LicenseInfo) IsOSS() bool { + var afOSSLicenseTypes = []string{ + `community edition for c/c++`, + `jcr edition`, + `oss`, + } + return slices.Contains( + afOSSLicenseTypes, + l.TypeNormalized(), + ) +} + +func (l LicenseInfo) TypeNormalized() string { + return strings.ToLower(l.Type) +} + +const USAFullDate = "Jan 2, 2006" + +func (l LicenseInfo) ValidSeconds() (int64, error) { + if l.IsOSS() { + return 0, nil + } + validThroughTime, err := time.Parse(USAFullDate, l.ValidThrough) + if err != nil { + return 0, fmt.Errorf( + "unparsable ‘validThrough’ license field: %w", + err, + ) + } + validThroughEpoch := validThroughTime.Unix() + timeNowEpoch := time.Now().Unix() + return validThroughEpoch - timeNowEpoch, nil +} + // FetchLicense makes the API call to license endpoint and returns LicenseInfo func (c *Client) FetchLicense() (LicenseInfo, error) { var licenseInfo LicenseInfo diff --git a/collector/collector.go b/collector/collector.go index 9d55ee4..f8f075e 100755 --- a/collector/collector.go +++ b/collector/collector.go @@ -1,8 +1,6 @@ package collector import ( - "strings" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/version" ) @@ -134,52 +132,16 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { func (e *Exporter) scrape(ch chan<- prometheus.Metric) (up float64) { e.totalScrapes.Inc() - // Collect License info - var licenseType string - license, err := e.client.FetchLicense() - if err != nil { - e.totalAPIErrors.Inc() - return 0 - } - licenseType = strings.ToLower(license.Type) - // Some API endpoints are not available in OSS - if licenseType != "oss" && licenseType != "jcr edition" && licenseType != "community edition for c/c++" { - for metricName, metric := range securityMetrics { - switch metricName { - case "users": - err := e.exportUsersCount(metricName, metric, ch) - if err != nil { - return 0 - } - case "groups": - err := e.exportGroups(metricName, metric, ch) - if err != nil { - return 0 - } - case "certificates": - err := e.exportCertificates(metricName, metric, ch) - if err != nil { - return 0 - } - } - } - err = e.exportReplications(ch) - if err != nil { - return 0 - } - } - // Collect and export open metrics if e.optionalMetrics.OpenMetrics { - err = e.exportOpenMetrics(ch) + err := e.exportOpenMetrics(ch) if err != nil { return 0 } } // Collect and export system metrics - err = e.exportSystem(license, ch) - if err != nil { + if err := e.exportSystem(ch); err != nil { return 0 } @@ -200,7 +162,7 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) (up float64) { // Get Downloaded and Created items for all repo in the last 1 and 5 minutes and add it to repoSummaryList if e.optionalMetrics.Artifacts { - repoSummaryList, err = e.getTotalArtifacts(repoSummaryList) + repoSummaryList, err := e.getTotalArtifacts(repoSummaryList) if err != nil { return 0 } diff --git a/collector/security.go b/collector/security.go index efa86a8..f50e871 100644 --- a/collector/security.go +++ b/collector/security.go @@ -26,6 +26,32 @@ func (e *Exporter) countUsersPerRealm(users []artifactory.User) realmUserCounts return usersPerRealm } +func (e *Exporter) exportAllSecurityMetrics(ch chan<- prometheus.Metric) error { + for metricName, metric := range securityMetrics { + switch metricName { + case "users": + err := e.exportUsersCount(metricName, metric, ch) + if err != nil { + return err + } + case "groups": + err := e.exportGroups(metricName, metric, ch) + if err != nil { + return err + } + case "certificates": + err := e.exportCertificates(metricName, metric, ch) + if err != nil { + return err + } + } + } + if err := e.exportReplications(ch); err != nil { + return err + } + return nil +} + func (e *Exporter) exportUsersCount(metricName string, metric *prometheus.Desc, ch chan<- prometheus.Metric) error { // Fetch Artifactory Users users, err := e.client.FetchUsers() diff --git a/collector/system.go b/collector/system.go index f72ce65..33c1c2c 100644 --- a/collector/system.go +++ b/collector/system.go @@ -1,16 +1,11 @@ package collector import ( - "strings" - "time" - "github.com/prometheus/client_golang/prometheus" - - "github.com/peimanja/artifactory_exporter/artifactory" ) -func (e *Exporter) exportSystem(license artifactory.LicenseInfo, ch chan<- prometheus.Metric) error { - health, err := e.client.FetchHealth() +func (e *Exporter) exportSystem(ch chan<- prometheus.Metric) error { + healthInfo, err := e.client.FetchHealth() if err != nil { e.logger.Error( "Couldn't scrape Artifactory when fetching system/ping", @@ -28,33 +23,59 @@ func (e *Exporter) exportSystem(license artifactory.LicenseInfo, ch chan<- prome e.totalAPIErrors.Inc() return err } + licenseInfo, err := e.client.FetchLicense() + if err != nil { + e.logger.Error( + "Couldn't scrape Artifactory when fetching system/license", + "err", err.Error(), + ) + e.totalAPIErrors.Inc() + return err + } + licenseValSec, err := licenseInfo.ValidSeconds() + if err != nil { + e.logger.Warn( + "Couldn't get Artifactory license validity", + "err", err.Error(), + ) // To preserve the operation, we do nothing but log the event, + } - licenseType := strings.ToLower(license.Type) for metricName, metric := range systemMetrics { switch metricName { case "healthy": - ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, convArtiToPromBool(health.Healthy), health.NodeId) + ch <- prometheus.MustNewConstMetric( + metric, + prometheus.GaugeValue, + convArtiToPromBool(healthInfo.Healthy), + healthInfo.NodeId, + ) case "version": - ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, 1, buildInfo.Version, buildInfo.Revision, buildInfo.NodeId) + ch <- prometheus.MustNewConstMetric( + metric, + prometheus.GaugeValue, + 1, + buildInfo.Version, + buildInfo.Revision, + buildInfo.NodeId, + ) case "license": - var validThrough float64 - timeNow := float64(time.Now().Unix()) - switch licenseType { - case "oss", "jcr edition", "community edition for c/c++": - validThrough = timeNow - default: - if validThroughTime, err := time.Parse("Jan 2, 2006", license.ValidThrough); err != nil { - e.logger.Warn( - "Couldn't parse Artifactory license ValidThrough", - "err", err.Error(), - ) - validThrough = timeNow - } else { - validThrough = float64(validThroughTime.Unix()) - } - } - ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, validThrough-timeNow, licenseType, license.LicensedTo, license.ValidThrough, license.NodeId) + ch <- prometheus.MustNewConstMetric( + metric, + prometheus.GaugeValue, + float64(licenseValSec), // Prometheus expects a float type. + licenseInfo.TypeNormalized(), + licenseInfo.LicensedTo, + licenseInfo.ValidThrough, + licenseInfo.NodeId, + ) } } + if !licenseInfo.IsOSS() { // Some endpoints are only available commercially. + err := e.exportAllSecurityMetrics(ch) + if err != nil { + return err + } + } + return nil }