Skip to content

Commit

Permalink
Metrics from build tags (#83)
Browse files Browse the repository at this point in the history
* Metrics from tags

* Error treatments

---------

Co-authored-by: Markus Blaschke <[email protected]>
  • Loading branch information
gucaldeira and mblaschke authored Jun 1, 2024
1 parent 7c808c0 commit 37db9ab
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
96 changes: 96 additions & 0 deletions azure-devops-client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
)

Expand Down Expand Up @@ -31,6 +32,17 @@ type TimelineRecordList struct {
List []TimelineRecord `json:"records"`
}

type TagList struct {
Count int `json:"count"`
List []string `json:"value"`
}

type Tag struct {
Name string
Value string
Type string
}

type TimelineRecord struct {
RecordType string `json:"type"`
Name string `json:"name"`
Expand Down Expand Up @@ -228,3 +240,87 @@ func (c *AzureDevopsClient) ListBuildTimeline(project string, buildID string) (l

return
}

func (c *AzureDevopsClient) ListBuildTags(project string, buildID string) (list TagList, error error) {
defer c.concurrencyUnlock()
c.concurrencyLock()

url := fmt.Sprintf(
"%v/_apis/build/builds/%v/tags",
url.QueryEscape(project),
url.QueryEscape(buildID),
)
response, err := c.rest().R().Get(url)
if err := c.checkResponse(response, err); err != nil {
error = err
return
}

err = json.Unmarshal(response.Body(), &list)
if err != nil {
error = err
return
}

return
}

func extractTagKeyValue(tag string) (k string, v string, error error) {
parts := strings.Split(tag, "=")
if len(parts) != 2 {
error = fmt.Errorf("could not extract key value pair from tag '%s'", tag)
return
}
k = parts[0]
v = parts[1]
return
}

func extractTagSchema(tagSchema string) (n string, t string, error error) {
parts := strings.Split(tagSchema, ":")
if len(parts) != 2 {
error = fmt.Errorf("could not extract type from tag schema '%s'", tagSchema)
return
}
n = parts[0]
t = parts[1]
return
}

func (t *TagList) Extract() (tags map[string]string, error error) {
tags = make(map[string]string)
for _, t := range t.List {
k, v, err := extractTagKeyValue(t)
if err != nil {
error = err
return
}
tags[k] = v
}
return
}

func (t *TagList) Parse(tagSchema []string) (pTags []Tag, error error) {
tags, err := t.Extract()
if err != nil {
error = err
return
}
for _, ts := range tagSchema {
name, _type, err := extractTagSchema(ts)
if err != nil {
error = err
return
}

value, isPresent := tags[name]
if isPresent {
pTags = append(pTags, Tag{
Name: name,
Value: value,
Type: _type,
})
}
}
return
}
4 changes: 4 additions & 0 deletions config/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ type (

// query settings
QueriesWithProjects []string `long:"list.query" env:"AZURE_DEVOPS_QUERIES" env-delim:" " description:"Pairs of query and project UUIDs in the form: '<queryId>@<projectId>'"`

// tag settings
TagsSchema *[]string `long:"tags.schema" env:"AZURE_DEVOPS_TAG_SCHEMA" env-delim:" " description:"Tags to be extracted from builds in the format 'tagName:type' with following types: number, info, bool"`
TagsBuildDefinitionIdList *[]int64 `long:"tags.build.definition" env:"AZURE_DEVOPS_TAG_BUILD_DEFINITION" env-delim:" " description:"Build definition ids to query tags (IDs)"`
}

// cache settings
Expand Down
82 changes: 82 additions & 0 deletions metrics_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"strconv"
"strings"
"time"

Expand All @@ -25,6 +26,7 @@ type MetricsCollectorBuild struct {
buildPhase *prometheus.GaugeVec
buildJob *prometheus.GaugeVec
buildTask *prometheus.GaugeVec
buildTag *prometheus.GaugeVec

buildTimeProject *prometheus.SummaryVec
jobTimeProject *prometheus.SummaryVec
Expand Down Expand Up @@ -152,6 +154,23 @@ func (m *MetricsCollectorBuild) Setup(collector *collector.Collector) {
)
m.Collector.RegisterMetricList("buildTask", m.prometheus.buildTask, true)

m.prometheus.buildTag = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azure_devops_build_tag",
Help: "Azure DevOps build tags",
},
[]string{
"projectID",
"buildID",
"buildDefinitionID",
"buildNumber",
"name",
"type",
"info",
},
)
m.Collector.RegisterMetricList("buildTag", m.prometheus.buildTag, true)

m.prometheus.buildDefinition = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "azure_devops_build_definition_info",
Expand Down Expand Up @@ -180,6 +199,9 @@ func (m *MetricsCollectorBuild) Collect(callback chan<- func()) {
m.collectDefinition(ctx, projectLogger, callback, project)
m.collectBuilds(ctx, projectLogger, callback, project)
m.collectBuildsTimeline(ctx, projectLogger, callback, project)
if nil != opts.AzureDevops.TagsSchema {
m.collectBuildsTags(ctx, projectLogger, callback, project)
}
}
}

Expand Down Expand Up @@ -611,3 +633,63 @@ func (m *MetricsCollectorBuild) collectBuildsTimeline(ctx context.Context, logge
}
}
}

func (m *MetricsCollectorBuild) collectBuildsTags(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) {
minTime := time.Now().Add(-opts.Limit.BuildHistoryDuration)
list, err := AzureDevopsClient.ListBuildHistoryWithStatus(project.Id, minTime, "completed")
if err != nil {
logger.Error(err)
return
}

buildTag := m.Collector.GetMetricList("buildTag")

for _, build := range list.List {
if nil == opts.AzureDevops.TagsBuildDefinitionIdList || arrayIntContains(*opts.AzureDevops.TagsBuildDefinitionIdList, build.Definition.Id) {
tagRecordList, _ := AzureDevopsClient.ListBuildTags(project.Id, int64ToString(build.Id))
tagList, err := tagRecordList.Parse(*opts.AzureDevops.TagsSchema)
if err != nil {
m.Logger().Error(err)
continue
}
for _, tag := range tagList {

switch tag.Type {
case "number":
value, _ := strconv.ParseFloat(tag.Value, 64)
buildTag.Add(prometheus.Labels{
"projectID": project.Id,
"buildID": int64ToString(build.Id),
"buildDefinitionID": int64ToString(build.Definition.Id),
"buildNumber": build.BuildNumber,
"name": tag.Name,
"type": "number",
"info": "",
}, value)
case "bool":
value, _ := strconv.ParseBool(tag.Value)
buildTag.AddBool(prometheus.Labels{
"projectID": project.Id,
"buildID": int64ToString(build.Id),
"buildDefinitionID": int64ToString(build.Definition.Id),
"buildNumber": build.BuildNumber,
"name": tag.Name,
"type": "bool",
"info": "",
}, value)
case "info":
buildTag.AddInfo(prometheus.Labels{
"projectID": project.Id,
"buildID": int64ToString(build.Id),
"buildDefinitionID": int64ToString(build.Definition.Id),
"buildNumber": build.BuildNumber,
"name": tag.Name,
"type": "info",
"info": tag.Value,
})
}

}
}
}
}
9 changes: 9 additions & 0 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ func arrayStringContains(s []string, e string) bool {
return false
}

func arrayIntContains(s []int64, e int64) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

func timeToFloat64(v time.Time) float64 {
return float64(v.Unix())
}

0 comments on commit 37db9ab

Please sign in to comment.