Skip to content

Commit 37db9ab

Browse files
Metrics from build tags (#83)
* Metrics from tags * Error treatments --------- Co-authored-by: Markus Blaschke <mblaschke82@gmail.com>
1 parent 7c808c0 commit 37db9ab

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

azure-devops-client/build.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/url"
7+
"strings"
78
"time"
89
)
910

@@ -31,6 +32,17 @@ type TimelineRecordList struct {
3132
List []TimelineRecord `json:"records"`
3233
}
3334

35+
type TagList struct {
36+
Count int `json:"count"`
37+
List []string `json:"value"`
38+
}
39+
40+
type Tag struct {
41+
Name string
42+
Value string
43+
Type string
44+
}
45+
3446
type TimelineRecord struct {
3547
RecordType string `json:"type"`
3648
Name string `json:"name"`
@@ -228,3 +240,87 @@ func (c *AzureDevopsClient) ListBuildTimeline(project string, buildID string) (l
228240

229241
return
230242
}
243+
244+
func (c *AzureDevopsClient) ListBuildTags(project string, buildID string) (list TagList, error error) {
245+
defer c.concurrencyUnlock()
246+
c.concurrencyLock()
247+
248+
url := fmt.Sprintf(
249+
"%v/_apis/build/builds/%v/tags",
250+
url.QueryEscape(project),
251+
url.QueryEscape(buildID),
252+
)
253+
response, err := c.rest().R().Get(url)
254+
if err := c.checkResponse(response, err); err != nil {
255+
error = err
256+
return
257+
}
258+
259+
err = json.Unmarshal(response.Body(), &list)
260+
if err != nil {
261+
error = err
262+
return
263+
}
264+
265+
return
266+
}
267+
268+
func extractTagKeyValue(tag string) (k string, v string, error error) {
269+
parts := strings.Split(tag, "=")
270+
if len(parts) != 2 {
271+
error = fmt.Errorf("could not extract key value pair from tag '%s'", tag)
272+
return
273+
}
274+
k = parts[0]
275+
v = parts[1]
276+
return
277+
}
278+
279+
func extractTagSchema(tagSchema string) (n string, t string, error error) {
280+
parts := strings.Split(tagSchema, ":")
281+
if len(parts) != 2 {
282+
error = fmt.Errorf("could not extract type from tag schema '%s'", tagSchema)
283+
return
284+
}
285+
n = parts[0]
286+
t = parts[1]
287+
return
288+
}
289+
290+
func (t *TagList) Extract() (tags map[string]string, error error) {
291+
tags = make(map[string]string)
292+
for _, t := range t.List {
293+
k, v, err := extractTagKeyValue(t)
294+
if err != nil {
295+
error = err
296+
return
297+
}
298+
tags[k] = v
299+
}
300+
return
301+
}
302+
303+
func (t *TagList) Parse(tagSchema []string) (pTags []Tag, error error) {
304+
tags, err := t.Extract()
305+
if err != nil {
306+
error = err
307+
return
308+
}
309+
for _, ts := range tagSchema {
310+
name, _type, err := extractTagSchema(ts)
311+
if err != nil {
312+
error = err
313+
return
314+
}
315+
316+
value, isPresent := tags[name]
317+
if isPresent {
318+
pTags = append(pTags, Tag{
319+
Name: name,
320+
Value: value,
321+
Type: _type,
322+
})
323+
}
324+
}
325+
return
326+
}

config/opts.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ type (
6060

6161
// query settings
6262
QueriesWithProjects []string `long:"list.query" env:"AZURE_DEVOPS_QUERIES" env-delim:" " description:"Pairs of query and project UUIDs in the form: '<queryId>@<projectId>'"`
63+
64+
// tag settings
65+
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"`
66+
TagsBuildDefinitionIdList *[]int64 `long:"tags.build.definition" env:"AZURE_DEVOPS_TAG_BUILD_DEFINITION" env-delim:" " description:"Build definition ids to query tags (IDs)"`
6367
}
6468

6569
// cache settings

metrics_build.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"strconv"
56
"strings"
67
"time"
78

@@ -25,6 +26,7 @@ type MetricsCollectorBuild struct {
2526
buildPhase *prometheus.GaugeVec
2627
buildJob *prometheus.GaugeVec
2728
buildTask *prometheus.GaugeVec
29+
buildTag *prometheus.GaugeVec
2830

2931
buildTimeProject *prometheus.SummaryVec
3032
jobTimeProject *prometheus.SummaryVec
@@ -152,6 +154,23 @@ func (m *MetricsCollectorBuild) Setup(collector *collector.Collector) {
152154
)
153155
m.Collector.RegisterMetricList("buildTask", m.prometheus.buildTask, true)
154156

157+
m.prometheus.buildTag = prometheus.NewGaugeVec(
158+
prometheus.GaugeOpts{
159+
Name: "azure_devops_build_tag",
160+
Help: "Azure DevOps build tags",
161+
},
162+
[]string{
163+
"projectID",
164+
"buildID",
165+
"buildDefinitionID",
166+
"buildNumber",
167+
"name",
168+
"type",
169+
"info",
170+
},
171+
)
172+
m.Collector.RegisterMetricList("buildTag", m.prometheus.buildTag, true)
173+
155174
m.prometheus.buildDefinition = prometheus.NewGaugeVec(
156175
prometheus.GaugeOpts{
157176
Name: "azure_devops_build_definition_info",
@@ -180,6 +199,9 @@ func (m *MetricsCollectorBuild) Collect(callback chan<- func()) {
180199
m.collectDefinition(ctx, projectLogger, callback, project)
181200
m.collectBuilds(ctx, projectLogger, callback, project)
182201
m.collectBuildsTimeline(ctx, projectLogger, callback, project)
202+
if nil != opts.AzureDevops.TagsSchema {
203+
m.collectBuildsTags(ctx, projectLogger, callback, project)
204+
}
183205
}
184206
}
185207

@@ -611,3 +633,63 @@ func (m *MetricsCollectorBuild) collectBuildsTimeline(ctx context.Context, logge
611633
}
612634
}
613635
}
636+
637+
func (m *MetricsCollectorBuild) collectBuildsTags(ctx context.Context, logger *zap.SugaredLogger, callback chan<- func(), project devopsClient.Project) {
638+
minTime := time.Now().Add(-opts.Limit.BuildHistoryDuration)
639+
list, err := AzureDevopsClient.ListBuildHistoryWithStatus(project.Id, minTime, "completed")
640+
if err != nil {
641+
logger.Error(err)
642+
return
643+
}
644+
645+
buildTag := m.Collector.GetMetricList("buildTag")
646+
647+
for _, build := range list.List {
648+
if nil == opts.AzureDevops.TagsBuildDefinitionIdList || arrayIntContains(*opts.AzureDevops.TagsBuildDefinitionIdList, build.Definition.Id) {
649+
tagRecordList, _ := AzureDevopsClient.ListBuildTags(project.Id, int64ToString(build.Id))
650+
tagList, err := tagRecordList.Parse(*opts.AzureDevops.TagsSchema)
651+
if err != nil {
652+
m.Logger().Error(err)
653+
continue
654+
}
655+
for _, tag := range tagList {
656+
657+
switch tag.Type {
658+
case "number":
659+
value, _ := strconv.ParseFloat(tag.Value, 64)
660+
buildTag.Add(prometheus.Labels{
661+
"projectID": project.Id,
662+
"buildID": int64ToString(build.Id),
663+
"buildDefinitionID": int64ToString(build.Definition.Id),
664+
"buildNumber": build.BuildNumber,
665+
"name": tag.Name,
666+
"type": "number",
667+
"info": "",
668+
}, value)
669+
case "bool":
670+
value, _ := strconv.ParseBool(tag.Value)
671+
buildTag.AddBool(prometheus.Labels{
672+
"projectID": project.Id,
673+
"buildID": int64ToString(build.Id),
674+
"buildDefinitionID": int64ToString(build.Definition.Id),
675+
"buildNumber": build.BuildNumber,
676+
"name": tag.Name,
677+
"type": "bool",
678+
"info": "",
679+
}, value)
680+
case "info":
681+
buildTag.AddInfo(prometheus.Labels{
682+
"projectID": project.Id,
683+
"buildID": int64ToString(build.Id),
684+
"buildDefinitionID": int64ToString(build.Definition.Id),
685+
"buildNumber": build.BuildNumber,
686+
"name": tag.Name,
687+
"type": "info",
688+
"info": tag.Value,
689+
})
690+
}
691+
692+
}
693+
}
694+
}
695+
}

misc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ func arrayStringContains(s []string, e string) bool {
1818
return false
1919
}
2020

21+
func arrayIntContains(s []int64, e int64) bool {
22+
for _, a := range s {
23+
if a == e {
24+
return true
25+
}
26+
}
27+
return false
28+
}
29+
2130
func timeToFloat64(v time.Time) float64 {
2231
return float64(v.Unix())
2332
}

0 commit comments

Comments
 (0)