Skip to content

Commit

Permalink
Add new metric to show duration all pipelineruns have taken
Browse files Browse the repository at this point in the history
added new metric to show sum of durations all pipelineruns have
taken in seconds. added docs and test accordingly

https://issues.redhat.com/browse/SRVKP-6226

Signed-off-by: Zaki Shaikh <[email protected]>
  • Loading branch information
zakisk committed Sep 25, 2024
1 parent b3fd3ef commit eb56bc4
Show file tree
Hide file tree
Showing 8 changed files with 824 additions and 6 deletions.
7 changes: 4 additions & 3 deletions docs/content/docs/install/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The metrics for pipelines-as-code can be accessed through the `pipelines-as-code
pipelines-as-code supports various exporters, such as Prometheus, Google Stackdriver, and more.
You can configure these exporters by referring to the [observability configuration](../config/config-observability.yaml).

| Name | Type | Description |
| ---------- |---------|-----------------------------------------------------|
| `pipelines_as_code_pipelinerun_count` | Counter | Number of pipelineruns created by pipelines-as-code |
| Name | Type | Description |
|------------------------------------------------------|---------|--------------------------------------------------------------------|
| `pipelines_as_code_pipelinerun_count` | Counter | Number of pipelineruns created by pipelines-as-code |
| `pipelines_as_code_pipelinerun_duration_seconds_sum` | Counter | Number of seconds all pipelineruns have taken in pipelines-as-code |
46 changes: 46 additions & 0 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ var prCount = stats.Float64("pipelines_as_code_pipelinerun_count",
"number of pipeline runs by pipelines as code",
stats.UnitDimensionless)

var prDurationCount = stats.Float64("pipelines_as_code_pipelinerun_duration_seconds_sum",
"number of seconds all pipelineruns completed in by pipelines as code",
stats.UnitDimensionless)

// Recorder holds keys for metrics.
type Recorder struct {
initialized bool
provider tag.Key
eventType tag.Key
namespace tag.Key
repository tag.Key
status tag.Key
reason tag.Key
ReportingPeriod time.Duration
}

Expand Down Expand Up @@ -59,13 +65,31 @@ func NewRecorder() (*Recorder, error) {
}
r.repository = repository

status, err := tag.NewKey("status")
if err != nil {
return nil, err
}
r.status = status

reason, err := tag.NewKey("reason")
if err != nil {
return nil, err
}
r.reason = reason

err = view.Register(
&view.View{
Description: prCount.Description(),
Measure: prCount,
Aggregation: view.Count(),
TagKeys: []tag.Key{r.provider, r.eventType, r.namespace, r.repository},
},
&view.View{
Description: prDurationCount.Description(),
Measure: prDurationCount,
Aggregation: view.Sum(),
TagKeys: []tag.Key{r.namespace, r.repository, r.status, r.reason},
},
)
if err != nil {
r.initialized = false
Expand Down Expand Up @@ -96,3 +120,25 @@ func (r *Recorder) Count(provider, event, namespace, repository string) error {
metrics.Record(ctx, prCount.M(1))
return nil
}

// CountPRDuration collects duration taken by a pipelinerun in seconds accumulate them in prDurationCount.
func (r *Recorder) CountPRDuration(namespace, repository, status, reason string, duration time.Duration) error {
if !r.initialized {
return fmt.Errorf(
"ignoring the metrics recording for pipelineruns, failed to initialize the metrics recorder")
}

ctx, err := tag.New(
context.Background(),
tag.Insert(r.namespace, namespace),
tag.Insert(r.repository, repository),
tag.Insert(r.status, status),
tag.Insert(r.reason, reason),
)
if err != nil {
return err
}

metrics.Record(ctx, prDurationCount.M(duration.Seconds()))
return nil
}
38 changes: 38 additions & 0 deletions pkg/reconciler/emit_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,27 @@ package reconciler

import (
"fmt"
"time"

"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
corev1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis"
)

func (r *Reconciler) emitMetrics(pr *tektonv1.PipelineRun) error {
if err := r.countPipelineRun(pr); err != nil {
return err
}

if err := r.calculatePRDuration(pr); err != nil {
return err
}

return nil
}

func (r *Reconciler) countPipelineRun(pr *tektonv1.PipelineRun) error {
gitProvider := pr.GetAnnotations()[keys.GitProvider]
eventType := pr.GetAnnotations()[keys.EventType]
repository := pr.GetAnnotations()[keys.Repository]
Expand All @@ -27,3 +42,26 @@ func (r *Reconciler) emitMetrics(pr *tektonv1.PipelineRun) error {

return r.metrics.Count(gitProvider, eventType, pr.GetNamespace(), repository)
}

func (r *Reconciler) calculatePRDuration(pr *tektonv1.PipelineRun) error {
repository := pr.GetAnnotations()[keys.Repository]
duration := time.Duration(0)
if pr.Status.StartTime != nil {
duration = time.Since(pr.Status.StartTime.Time)
if pr.Status.CompletionTime != nil {
duration = pr.Status.CompletionTime.Sub(pr.Status.StartTime.Time)
}
}

cond := pr.Status.GetCondition(apis.ConditionSucceeded)
status := "success"
if cond.Status == corev1.ConditionFalse {
status = "failed"
if cond.Reason == tektonv1.PipelineRunReasonCancelled.String() {
status = "cancelled"
}
}
reason := cond.Reason

return r.metrics.CountPRDuration(pr.GetNamespace(), repository, status, reason, duration)
}
97 changes: 94 additions & 3 deletions pkg/reconciler/emit_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ package reconciler

import (
"testing"
"time"

"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
"github.com/openshift-pipelines/pipelines-as-code/pkg/metrics"
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"gotest.tools/v3/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
"knative.dev/pkg/metrics/metricstest"
_ "knative.dev/pkg/metrics/testing"
)

func TestEmitMetrics(t *testing.T) {
// TestCountPipelineRun tests pipelinerun count metric.
func TestCountPipelineRun(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
Expand Down Expand Up @@ -72,9 +79,93 @@ func TestEmitMetrics(t *testing.T) {
Annotations: tt.annotations,
},
}
if err = r.emitMetrics(pr); (err != nil) != tt.wantErr {
t.Errorf("emitMetrics() error = %v, wantErr %v", err != nil, tt.wantErr)
if err = r.countPipelineRun(pr); (err != nil) != tt.wantErr {
t.Errorf("countPipelineRun() error = %v, wantErr %v", err != nil, tt.wantErr)
}

metricstest.AssertMetricExists(t, "pipelines_as_code_pipelinerun_count")
})
}
}

// TestCalculatePipelineRunDuration tests pipelinerun duration metric.
func TestCalculatePipelineRunDuration(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
}{
{
name: "provider is GitHub App",
annotations: map[string]string{
keys.GitProvider: "github",
keys.EventType: "pull_request",
keys.InstallationID: "123",
keys.Repository: "pac-repo",
},
},
{
name: "provider is GitHub Enterprise App",
annotations: map[string]string{
keys.GitProvider: "github-enterprise",
keys.EventType: "pull_request",
keys.InstallationID: "123",
keys.Repository: "pac-repo",
},
},
{
name: "provider is GitHub Webhook",
annotations: map[string]string{
keys.GitProvider: "github",
keys.EventType: "pull_request",
keys.Repository: "pac-repo",
},
},
{
name: "provider is GitLab",
annotations: map[string]string{
keys.GitProvider: "gitlab",
keys.EventType: "push",
keys.Repository: "pac-repo",
},
},
}

var (
startTime = metav1.Now()
completionTime = metav1.NewTime(startTime.Time.Add(time.Minute))
)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m, err := metrics.NewRecorder()
assert.NilError(t, err)
r := &Reconciler{
metrics: m,
}
pr := &tektonv1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Namespace: "pac-ns",
Annotations: tt.annotations,
},
Status: tektonv1.PipelineRunStatus{
Status: duckv1.Status{Conditions: []apis.Condition{
{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
Reason: tektonv1.PipelineRunReasonSuccessful.String(),
},
}},
PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{
StartTime: &startTime,
CompletionTime: &completionTime,
},
},
}
if err = r.calculatePRDuration(pr); err != nil {
t.Errorf("calculatePRDuration() error = %v", err)
}

metricstest.AssertMetricExists(t, "pipelines_as_code_pipelinerun_duration_seconds_sum")
})
}
}
Loading

0 comments on commit eb56bc4

Please sign in to comment.