• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mvisonneau / gitlab-ci-pipelines-exporter / 20987261034

14 Jan 2026 08:20AM UTC coverage: 62.977% (-1.2%) from 64.156%
20987261034

push

github

mvisonneau
ci: bumped to go 1.25

3693 of 5864 relevant lines covered (62.98%)

3.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.56
/pkg/controller/pipelines.go
1
package controller
2

3
import (
4
        "context"
5
        "fmt"
6
        "reflect"
7

8
        log "github.com/sirupsen/logrus"
9
        goGitlab "gitlab.com/gitlab-org/api/client-go"
10
        "golang.org/x/exp/slices"
11

12
        "github.com/mvisonneau/gitlab-ci-pipelines-exporter/pkg/schemas"
13
)
14

15
// PullRefMetrics ..
16
func (c *Controller) PullRefMetrics(ctx context.Context, ref schemas.Ref) error {
7✔
17
        // At scale, the scheduled ref may be behind the actual state being stored
7✔
18
        // to avoid issues, we refresh it from the store before manipulating it
7✔
19
        if err := c.Store.GetRef(ctx, &ref); err != nil {
7✔
20
                return err
×
21
        }
×
22

23
        logFields := log.Fields{
7✔
24
                "project-name": ref.Project.Name,
7✔
25
                "ref":          ref.Name,
7✔
26
                "ref-kind":     ref.Kind,
7✔
27
        }
7✔
28

7✔
29
        // We need a different syntax if the ref is a merge-request
7✔
30
        var refName string
7✔
31
        if ref.Kind == schemas.RefKindMergeRequest {
8✔
32
                refName = fmt.Sprintf("refs/merge-requests/%s/head", ref.Name)
1✔
33
        } else {
7✔
34
                refName = ref.Name
6✔
35
        }
6✔
36

37
        pipelines, _, err := c.Gitlab.GetProjectPipelines(ctx, ref.Project.Name, &goGitlab.ListProjectPipelinesOptions{
7✔
38
                ListOptions: goGitlab.ListOptions{
7✔
39
                        PerPage: int(ref.Project.Pull.Pipeline.PerRef),
7✔
40
                        Page:    1,
7✔
41
                },
7✔
42
                Ref: &refName,
7✔
43
        })
7✔
44
        if err != nil {
9✔
45
                return fmt.Errorf("error fetching project pipelines for %s: %v", ref.Project.Name, err)
2✔
46
        }
2✔
47

48
        if len(pipelines) == 0 && ref.Kind == schemas.RefKindMergeRequest {
5✔
49
                refName = fmt.Sprintf("refs/merge-requests/%s/merge", ref.Name)
×
50
                pipelines, _, err = c.Gitlab.GetProjectPipelines(ctx, ref.Project.Name, &goGitlab.ListProjectPipelinesOptions{
×
51
                        // We only need the most recent pipeline
×
52
                        ListOptions: goGitlab.ListOptions{
×
53
                                PerPage: 1,
×
54
                                Page:    1,
×
55
                        },
×
56
                        Ref: &refName,
×
57
                })
×
58
                if err != nil {
×
59
                        return fmt.Errorf("error fetching project pipelines for %s: %v", ref.Project.Name, err)
×
60
                }
×
61
        }
62

63
        if len(pipelines) == 0 {
5✔
64
                log.WithFields(logFields).Debug("could not find any pipeline for the ref")
×
65

×
66
                return nil
×
67
        }
×
68

69
        // Reverse result list to have `ref`'s `LatestPipeline` untouched (compared to
70
        // default behavior) after looping over list
71
        slices.Reverse(pipelines)
5✔
72

5✔
73
        for _, apiPipeline := range pipelines {
10✔
74
                err := c.ProcessPipelinesMetrics(ctx, ref, apiPipeline)
5✔
75
                if err != nil {
5✔
76
                        log.WithFields(log.Fields{
×
77
                                "pipeline": apiPipeline.ID,
×
78
                                "error":    err,
×
79
                        }).Error("processing pipeline metrics failed")
×
80
                }
×
81
        }
82

83
        return nil
5✔
84
}
85

86
func (c *Controller) ProcessPipelinesMetrics(ctx context.Context, ref schemas.Ref, apiPipeline *goGitlab.PipelineInfo) error {
5✔
87
        finishedStatusesList := []string{
5✔
88
                "success",
5✔
89
                "failed",
5✔
90
                "skipped",
5✔
91
                "cancelled",
5✔
92
        }
5✔
93

5✔
94
        pipeline, err := c.Gitlab.GetRefPipeline(ctx, ref, apiPipeline.ID)
5✔
95
        if err != nil {
5✔
96
                return err
×
97
        }
×
98

99
        // fetch pipeline variables
100
        if ref.Project.Pull.Pipeline.Variables.Enabled {
10✔
101
                if exists, _ := c.Store.PipelineVariablesExists(ctx, pipeline); !exists {
9✔
102
                        variables, err := c.Gitlab.GetRefPipelineVariablesAsConcatenatedString(ctx, ref, pipeline)
4✔
103
                        _ = c.Store.SetPipelineVariables(ctx, pipeline, variables)
4✔
104
                        pipeline.Variables = variables
4✔
105
                        if err != nil {
4✔
106
                                return err
×
107
                        }
×
108
                } else {
1✔
109
                        variables, _ := c.Store.GetPipelineVariables(ctx, pipeline)
1✔
110
                        pipeline.Variables = variables
1✔
111
                }
1✔
112
        }
113

114
        var cachedPipeline schemas.Pipeline
5✔
115

5✔
116
        if _ = c.Store.GetPipeline(ctx, &cachedPipeline); cachedPipeline.ID == 0 || !reflect.DeepEqual(pipeline, cachedPipeline) {
10✔
117
                formerPipeline := ref.LatestPipeline
5✔
118
                ref.LatestPipeline = pipeline
5✔
119

5✔
120
                if err = c.Store.SetPipeline(ctx, pipeline); err != nil {
5✔
121
                        return err
×
122
                }
×
123

124
                // Update the ref in the store
125
                if err = c.Store.SetRef(ctx, ref); err != nil {
5✔
126
                        return err
×
127
                }
×
128

129
                labels := ref.DefaultLabelsValues()
5✔
130

5✔
131
                // If the metric does not exist yet, start with 0 instead of 1
5✔
132
                // this could cause some false positives in prometheus
5✔
133
                // when restarting the exporter otherwise
5✔
134
                runCount := schemas.Metric{
5✔
135
                        Kind:   schemas.MetricKindRunCount,
5✔
136
                        Labels: labels,
5✔
137
                }
5✔
138

5✔
139
                storeGetMetric(ctx, c.Store, &runCount)
5✔
140

5✔
141
                if formerPipeline.ID != 0 && formerPipeline.ID != ref.LatestPipeline.ID {
5✔
142
                        runCount.Value++
×
143
                }
×
144

145
                storeSetMetric(ctx, c.Store, runCount)
5✔
146

5✔
147
                storeSetMetric(ctx, c.Store, schemas.Metric{
5✔
148
                        Kind:   schemas.MetricKindCoverage,
5✔
149
                        Labels: labels,
5✔
150
                        Value:  pipeline.Coverage,
5✔
151
                })
5✔
152

5✔
153
                storeSetMetric(ctx, c.Store, schemas.Metric{
5✔
154
                        Kind:   schemas.MetricKindID,
5✔
155
                        Labels: labels,
5✔
156
                        Value:  float64(pipeline.ID),
5✔
157
                })
5✔
158

5✔
159
                emitStatusMetric(
5✔
160
                        ctx,
5✔
161
                        c.Store,
5✔
162
                        schemas.MetricKindStatus,
5✔
163
                        labels,
5✔
164
                        statusesList[:],
5✔
165
                        pipeline.Status,
5✔
166
                        ref.Project.OutputSparseStatusMetrics,
5✔
167
                )
5✔
168

5✔
169
                storeSetMetric(ctx, c.Store, schemas.Metric{
5✔
170
                        Kind:   schemas.MetricKindDurationSeconds,
5✔
171
                        Labels: labels,
5✔
172
                        Value:  pipeline.DurationSeconds,
5✔
173
                })
5✔
174

5✔
175
                storeSetMetric(ctx, c.Store, schemas.Metric{
5✔
176
                        Kind:   schemas.MetricKindQueuedDurationSeconds,
5✔
177
                        Labels: labels,
5✔
178
                        Value:  pipeline.QueuedDurationSeconds,
5✔
179
                })
5✔
180

5✔
181
                storeSetMetric(ctx, c.Store, schemas.Metric{
5✔
182
                        Kind:   schemas.MetricKindTimestamp,
5✔
183
                        Labels: labels,
5✔
184
                        Value:  pipeline.Timestamp,
5✔
185
                })
5✔
186

5✔
187
                if ref.Project.Pull.Pipeline.Jobs.Enabled {
5✔
188
                        if err := c.PullRefPipelineJobsMetrics(ctx, ref); err != nil {
×
189
                                return err
×
190
                        }
×
191
                }
192
        } else {
×
193
                if err := c.PullRefMostRecentJobsMetrics(ctx, ref); err != nil {
×
194
                        return err
×
195
                }
×
196
        }
197

198
        // fetch pipeline test report
199
        if ref.Project.Pull.Pipeline.TestReports.Enabled && slices.Contains(finishedStatusesList, ref.LatestPipeline.Status) {
6✔
200
                ref.LatestPipeline.TestReport, err = c.Gitlab.GetRefPipelineTestReport(ctx, ref)
1✔
201
                if err != nil {
1✔
202
                        return err
×
203
                }
×
204

205
                c.ProcessTestReportMetrics(ctx, ref, ref.LatestPipeline.TestReport)
1✔
206

1✔
207
                for _, ts := range ref.LatestPipeline.TestReport.TestSuites {
2✔
208
                        c.ProcessTestSuiteMetrics(ctx, ref, ts)
1✔
209
                        // fetch pipeline test cases
1✔
210
                        if ref.Project.Pull.Pipeline.TestReports.TestCases.Enabled {
2✔
211
                                for _, tc := range ts.TestCases {
2✔
212
                                        c.ProcessTestCaseMetrics(ctx, ref, ts, tc)
1✔
213
                                }
1✔
214
                        }
215
                }
216
        }
217

218
        return nil
5✔
219
}
220

221
// ProcessTestReportMetrics ..
222
func (c *Controller) ProcessTestReportMetrics(ctx context.Context, ref schemas.Ref, tr schemas.TestReport) {
1✔
223
        testReportLogFields := log.Fields{
1✔
224
                "project-name": ref.Project.Name,
1✔
225
                "ref":          ref.Name,
1✔
226
        }
1✔
227

1✔
228
        labels := ref.DefaultLabelsValues()
1✔
229

1✔
230
        // Refresh ref state from the store
1✔
231
        if err := c.Store.GetRef(ctx, &ref); err != nil {
1✔
232
                log.WithContext(ctx).
×
233
                        WithFields(testReportLogFields).
×
234
                        WithError(err).
×
235
                        Error("getting ref from the store")
×
236

×
237
                return
×
238
        }
×
239

240
        log.WithFields(testReportLogFields).Trace("processing test report metrics")
1✔
241

1✔
242
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
243
                Kind:   schemas.MetricKindTestReportErrorCount,
1✔
244
                Labels: labels,
1✔
245
                Value:  float64(tr.ErrorCount),
1✔
246
        })
1✔
247

1✔
248
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
249
                Kind:   schemas.MetricKindTestReportFailedCount,
1✔
250
                Labels: labels,
1✔
251
                Value:  float64(tr.FailedCount),
1✔
252
        })
1✔
253

1✔
254
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
255
                Kind:   schemas.MetricKindTestReportSkippedCount,
1✔
256
                Labels: labels,
1✔
257
                Value:  float64(tr.SkippedCount),
1✔
258
        })
1✔
259

1✔
260
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
261
                Kind:   schemas.MetricKindTestReportSuccessCount,
1✔
262
                Labels: labels,
1✔
263
                Value:  float64(tr.SuccessCount),
1✔
264
        })
1✔
265

1✔
266
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
267
                Kind:   schemas.MetricKindTestReportTotalCount,
1✔
268
                Labels: labels,
1✔
269
                Value:  float64(tr.TotalCount),
1✔
270
        })
1✔
271

1✔
272
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
273
                Kind:   schemas.MetricKindTestReportTotalTime,
1✔
274
                Labels: labels,
1✔
275
                Value:  float64(tr.TotalTime),
1✔
276
        })
1✔
277
}
278

279
// ProcessTestSuiteMetrics ..
280
func (c *Controller) ProcessTestSuiteMetrics(ctx context.Context, ref schemas.Ref, ts schemas.TestSuite) {
1✔
281
        testSuiteLogFields := log.Fields{
1✔
282
                "project-name":    ref.Project.Name,
1✔
283
                "ref":             ref.Name,
1✔
284
                "test-suite-name": ts.Name,
1✔
285
        }
1✔
286

1✔
287
        labels := ref.DefaultLabelsValues()
1✔
288
        labels["test_suite_name"] = ts.Name
1✔
289

1✔
290
        // Refresh ref state from the store
1✔
291
        if err := c.Store.GetRef(ctx, &ref); err != nil {
1✔
292
                log.WithContext(ctx).
×
293
                        WithFields(testSuiteLogFields).
×
294
                        WithError(err).
×
295
                        Error("getting ref from the store")
×
296

×
297
                return
×
298
        }
×
299

300
        log.WithFields(testSuiteLogFields).Trace("processing test suite metrics")
1✔
301

1✔
302
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
303
                Kind:   schemas.MetricKindTestSuiteErrorCount,
1✔
304
                Labels: labels,
1✔
305
                Value:  float64(ts.ErrorCount),
1✔
306
        })
1✔
307

1✔
308
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
309
                Kind:   schemas.MetricKindTestSuiteFailedCount,
1✔
310
                Labels: labels,
1✔
311
                Value:  float64(ts.FailedCount),
1✔
312
        })
1✔
313

1✔
314
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
315
                Kind:   schemas.MetricKindTestSuiteSkippedCount,
1✔
316
                Labels: labels,
1✔
317
                Value:  float64(ts.SkippedCount),
1✔
318
        })
1✔
319

1✔
320
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
321
                Kind:   schemas.MetricKindTestSuiteSuccessCount,
1✔
322
                Labels: labels,
1✔
323
                Value:  float64(ts.SuccessCount),
1✔
324
        })
1✔
325

1✔
326
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
327
                Kind:   schemas.MetricKindTestSuiteTotalCount,
1✔
328
                Labels: labels,
1✔
329
                Value:  float64(ts.TotalCount),
1✔
330
        })
1✔
331

1✔
332
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
333
                Kind:   schemas.MetricKindTestSuiteTotalTime,
1✔
334
                Labels: labels,
1✔
335
                Value:  ts.TotalTime,
1✔
336
        })
1✔
337
}
338

339
func (c *Controller) ProcessTestCaseMetrics(ctx context.Context, ref schemas.Ref, ts schemas.TestSuite, tc schemas.TestCase) {
1✔
340
        testCaseLogFields := log.Fields{
1✔
341
                "project-name":     ref.Project.Name,
1✔
342
                "ref":              ref.Name,
1✔
343
                "test-suite-name":  ts.Name,
1✔
344
                "test-case-name":   tc.Name,
1✔
345
                "test-case-status": tc.Status,
1✔
346
        }
1✔
347

1✔
348
        labels := ref.DefaultLabelsValues()
1✔
349
        labels["test_suite_name"] = ts.Name
1✔
350
        labels["test_case_name"] = tc.Name
1✔
351
        labels["test_case_classname"] = tc.Classname
1✔
352

1✔
353
        // Get the existing ref from the store
1✔
354
        if err := c.Store.GetRef(ctx, &ref); err != nil {
1✔
355
                log.WithContext(ctx).
×
356
                        WithFields(testCaseLogFields).
×
357
                        WithError(err).
×
358
                        Error("getting ref from the store")
×
359

×
360
                return
×
361
        }
×
362

363
        log.WithFields(testCaseLogFields).Trace("processing test case metrics")
1✔
364

1✔
365
        storeSetMetric(ctx, c.Store, schemas.Metric{
1✔
366
                Kind:   schemas.MetricKindTestCaseExecutionTime,
1✔
367
                Labels: labels,
1✔
368
                Value:  tc.ExecutionTime,
1✔
369
        })
1✔
370

1✔
371
        emitStatusMetric(
1✔
372
                ctx,
1✔
373
                c.Store,
1✔
374
                schemas.MetricKindTestCaseStatus,
1✔
375
                labels,
1✔
376
                statusesList[:],
1✔
377
                tc.Status,
1✔
378
                ref.Project.OutputSparseStatusMetrics,
1✔
379
        )
1✔
380
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc