Skip to content

Commit

Permalink
Issue #124. (#135)
Browse files Browse the repository at this point in the history
* Issue #124.

An attempt to clean up the code
that converts Artifactory strings
to numbers accepted in Prometheus.

* Unit tests plus sonarcloud suggestion.

* Names unification.
  • Loading branch information
KacperPerschke authored Mar 1, 2024
1 parent 8dfb2ca commit f851647
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 93 deletions.
118 changes: 118 additions & 0 deletions collector/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package collector

import (
"fmt"
"regexp"
"strings"
)

const (
logDbgKeyArtNum = `artifactory.number`
)

// convArtiToPromBool is a very interesting appendix.
// Something needs it, but what and why?
// It would be quite nice if it was written here why such a thing is needed.
func convArtiToPromBool(b bool) float64 {
if b {
return 1
}
return 0
}

const (
pattOneNumber = `^(?P<number>[[:digit:]]{1,3}(?:,[[:digit:]]{3})*)(?:\.[[:digit:]]{1,2})? ?(?P<multiplier>%|bytes|[KMGT]B)?$`
)

var (
reOneNumber = regexp.MustCompile(pattOneNumber)
)

func (e *Exporter) convArtiToPromNumber(artiNum string) (float64, error) {
e.logger.Debug(
"Attempting to convert a string from artifactory representing a number.",
logDbgKeyArtNum, artiNum,
)

if !reOneNumber.MatchString(artiNum) {
e.logger.Debug(
"The arti number did not match known templates.",
logDbgKeyArtNum, artiNum,
)
err := fmt.Errorf(
`The string '%s' does not match pattern '%s'.`,
artiNum,
pattOneNumber,
)
return 0, err
}

groups := extractNamedGroups(artiNum, reOneNumber)

// The following `strings.replace` is for those cases that contain a comma
// thousands separator. In other cases, unnecessary, but cheaper than if.
// Sorry.
f, err := e.convNumber(
strings.Replace(groups["number"], `,`, ``, -1),
)
if err != nil {
return 0, err
}

mAsString, mIsPresent := groups["multiplier"]
if !mIsPresent {
return f, nil
}
m, err := e.convMultiplier(mAsString)
if err != nil {
return 0, err
}

return f * m, nil
}

const (
pattTBytesPercent = `^(?P<tbytes>[[:digit:]]+(?:\.[[:digit:]]{1,2})?) TB \((?P<percent>[[:digit:]]{1,2}(?:\.[[:digit:]]{1,2})?)%\)$`
)

var (
reTBytesPercent = regexp.MustCompile(pattTBytesPercent)
)

func (e *Exporter) convArtiToPromSizeAndUsage(artiSize string) (float64, float64, error) {
e.logger.Debug(
"Attempting to convert a string from artifactory representing a number.",
logDbgKeyArtNum, artiSize,
)

if !reTBytesPercent.MatchString(artiSize) {
e.logger.Debug(
"The arti number did not match known templates.",
logDbgKeyArtNum, artiSize,
)
err := fmt.Errorf(
`The string '%s' does not match '%s' pattern.`,
artiSize,
pattTBytesPercent,
)
return 0, 0, err
}

groups := extractNamedGroups(artiSize, reTBytesPercent)

b, err := e.convNumber(groups["tbytes"])
if err != nil {
return 0, 0, err
}
mulTB, _ := e.convMultiplier(`TB`)
size := b * mulTB

p, err := e.convNumber(groups["percent"])
if err != nil {
return 0, 0, err
}
mulPercent, _ := e.convMultiplier(`%`)
percent := p * mulPercent

return size, percent, nil
}
54 changes: 54 additions & 0 deletions collector/converter_internals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package collector

import (
"fmt"
"math"
"regexp"
"strconv"
)

var mulConvDriver = map[string]float64{
`%`: 0.01,
`bytes`: 1,
`KB`: math.Exp2(10),
`MB`: math.Exp2(20),
`GB`: math.Exp2(30),
`TB`: math.Exp2(40),
}

func (e *Exporter) convMultiplier(m string) (float64, error) {
mul, present := mulConvDriver[m]
if present {
return mul, nil
}
e.logger.Error(
"The string was not recognized as a known multiplier.",
"artifactory.number.multiplier", m,
)
return 0, fmt.Errorf(`Could not recognise '%s; as multiplier`, m)
}

func (e *Exporter) convNumber(n string) (float64, error) {
f, err := strconv.ParseFloat(n, 64)
if err != nil {
e.logger.Error(
"String not convertible to float64",
"string", n,
)
return 0, err
}
return f, nil
}

type reCaptureGroups map[string]string

func extractNamedGroups(artiNum string, re *regexp.Regexp) reCaptureGroups {
match := re.FindStringSubmatch(artiNum)
groupsFound := make(reCaptureGroups)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
groupsFound[name] = match[i]
}
}
return groupsFound
}
96 changes: 96 additions & 0 deletions collector/converter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package collector

// The purpose of this test is to check whether the given regular pattern
// captures cases from the application logs?
// They are not complete nor comprehensive.
// They also don't test the negative path.
// Fell free to make them better.

import (
"testing"

l "github.com/peimanja/artifactory_exporter/logger"
)

var fakeExporter = Exporter{
logger: l.New(
l.Config{
Format: l.FormatDefault,
Level: l.LevelDefault,
},
),
}

func TestConvNum(t *testing.T) {
tests := []struct {
input string
want float64
}{
{
input: `8 bytes`,
want: 8,
},
{
input: `8,888.88 MB`,
want: 9319743488.00,
},
{
input: `88.88 GB`,
want: 94489280512.00,
},
{
input: `888.88 GB`,
want: 953482739712.00,
},
}
for _, tc := range tests {
got, err := fakeExporter.convArtiToPromNumber(tc.input)
if err != nil {
t.Fatalf(`An error '%v' occurred during conversion.`, err)
}
if tc.want != got {
t.Fatalf(`Want %f, but got %f.`, tc.want, got)
}
}
}

func TestConvTwoNum(t *testing.T) {
tests := []struct {
input string
want []float64
}{
{
input: `3.33 TB (3.3%)`,
want: []float64{3661373720494.080078, 0.033},
},

{
input: `6.66 TB (6.66%)`,
want: []float64{7322747440988.160156, 0.0666},
},

{
input: `11.11 TB (11.1%)`,
want: []float64{12215574184591.359375, 0.111},
},

{
input: `99.99 TB (99.99%)`,
want: []float64{109940167661322.234375, 0.9999},
},
}
for _, tc := range tests {
gotSize, gotPercent, err := fakeExporter.convArtiToPromSizeAndUsage(tc.input)
if err != nil {
t.Fatalf(`An error '%v' occurred during conversion.`, err)
}
wantSize := tc.want[0]
if wantSize != gotSize {
t.Fatalf(`Problem with size. Want %f, but got %f.`, wantSize, gotSize)
}
wantPercent := tc.want[1]
if wantPercent != gotPercent {
t.Fatalf(`Problem with percentage. Want %f, but got %f.`, wantPercent, gotPercent)
}
}
}
2 changes: 1 addition & 1 deletion collector/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (e *Exporter) exportReplications(ch chan<- prometheus.Metric) error {
for metricName, metric := range replicationMetrics {
switch metricName {
case "enabled":
enabled := b2f(replication.Enabled)
enabled := convArtiToPromBool(replication.Enabled)
repo := replication.RepoKey
rType := strings.ToLower(replication.ReplicationType)
rURL := strings.ToLower(replication.URL)
Expand Down
21 changes: 16 additions & 5 deletions collector/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func (e *Exporter) exportCount(metricName string, metric *prometheus.Desc, count
e.jsonParseFailures.Inc()
return
}
value, err := e.removeCommas(count)
value, err := e.convArtiToPromNumber(count)
if err != nil {
e.jsonParseFailures.Inc()
e.logger.Error(
Expand All @@ -38,7 +38,7 @@ func (e *Exporter) exportSize(metricName string, metric *prometheus.Desc, size s
e.jsonParseFailures.Inc()
return
}
value, err := e.bytesConverter(size)
value, err := e.convArtiToPromNumber(size)
if err != nil {
e.jsonParseFailures.Inc()
e.logger.Error(
Expand All @@ -61,7 +61,11 @@ func (e *Exporter) exportFilestore(metricName string, metric *prometheus.Desc, s
e.jsonParseFailures.Inc()
return
}
value, err := e.bytesConverter(size)
value, percent, err := e.convArtiToPromSizeAndUsage(size)
/*
* What should you use the percentage for?
* Maybe Issue #126?
*/
if err != nil {
e.jsonParseFailures.Inc()
e.logger.Warn(
Expand All @@ -75,6 +79,7 @@ func (e *Exporter) exportFilestore(metricName string, metric *prometheus.Desc, s
logDbgMsgRegMetric,
"metric", metricName,
"value", value,
"percent", percent,
)
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, value, fileStoreType, fileStoreDir, nodeId)
}
Expand Down Expand Up @@ -112,7 +117,7 @@ func (e *Exporter) extractRepo(storageInfo artifactory.StorageInfo) ([]repoSumma
rs.FilesCount = float64(repo.FilesCount)
rs.ItemsCount = float64(repo.ItemsCount)
rs.PackageType = strings.ToLower(repo.PackageType)
rs.UsedSpace, err = e.bytesConverter(repo.UsedSpace)
rs.UsedSpace, err = e.convArtiToPromNumber(repo.UsedSpace)
if err != nil {
e.logger.Warn(
"There was an issue parsing repo UsedSpace",
Expand All @@ -125,7 +130,13 @@ func (e *Exporter) extractRepo(storageInfo artifactory.StorageInfo) ([]repoSumma
if repo.Percentage == "N/A" {
rs.Percentage = 0
} else {
rs.Percentage, err = e.removeCommas(repo.Percentage)
/* WARNING!
* Previous e.removeCommas have been returning float from range [0.0, 100.0]
* Actual convNumArtiToProm returns float from range [0.0, 1.0]
* The application's behavior in this matter requires
* close observation in the near future.
*/
rs.Percentage, err = e.convArtiToPromNumber(repo.Percentage)
if err != nil {
e.logger.Warn(
"There was an issue parsing repo Percentage",
Expand Down
2 changes: 1 addition & 1 deletion collector/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (e *Exporter) exportSystem(license artifactory.LicenseInfo, ch chan<- prome
for metricName, metric := range systemMetrics {
switch metricName {
case "healthy":
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, b2f(health.Healthy), health.NodeId)
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, convArtiToPromBool(health.Healthy), health.NodeId)
case "version":
ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, 1, buildInfo.Version, buildInfo.Revision, buildInfo.NodeId)
case "license":
Expand Down
Loading

0 comments on commit f851647

Please sign in to comment.