Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new metric to show duration all pipelineruns have taken #1764

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
zakisk marked this conversation as resolved.
Show resolved Hide resolved
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},
savitaashture marked this conversation as resolved.
Show resolved Hide resolved
},
)
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)
savitaashture marked this conversation as resolved.
Show resolved Hide resolved
}
}

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)
}
190 changes: 187 additions & 3 deletions pkg/reconciler/emit_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ 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
tags map[string]string
wantErr bool
}{
{
Expand All @@ -23,6 +31,10 @@ func TestEmitMetrics(t *testing.T) {
keys.EventType: "pull_request",
keys.InstallationID: "123",
},
tags: map[string]string{
"provider": "github-app",
"event-type": "pull_request",
},
wantErr: false,
},
{
Expand All @@ -32,6 +44,10 @@ func TestEmitMetrics(t *testing.T) {
keys.EventType: "pull_request",
keys.InstallationID: "123",
},
tags: map[string]string{
"provider": "github-enterprise-app",
"event-type": "pull_request",
},
wantErr: false,
},
{
Expand All @@ -40,6 +56,10 @@ func TestEmitMetrics(t *testing.T) {
keys.GitProvider: "github",
keys.EventType: "pull_request",
},
tags: map[string]string{
"provider": "github-webhook",
"event-type": "pull_request",
},
wantErr: false,
},
{
Expand All @@ -48,6 +68,10 @@ func TestEmitMetrics(t *testing.T) {
keys.GitProvider: "gitlab",
keys.EventType: "push",
},
tags: map[string]string{
"provider": "gitlab-webhook",
"event-type": "push",
},
wantErr: false,
},
{
Expand All @@ -62,6 +86,7 @@ func TestEmitMetrics(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metricstest.Unregister("pipelines_as_code_pipelinerun_count")
m, err := metrics.NewRecorder()
assert.NilError(t, err)
r := &Reconciler{
Expand All @@ -72,9 +97,168 @@ 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)
// checks that metric is unregistered successfully and there is no metric
// before emitting new pr count metric.
metricstest.AssertNoMetric(t, "pipelines_as_code_pipelinerun_count")

if err = r.countPipelineRun(pr); (err != nil) != tt.wantErr {
t.Errorf("countPipelineRun() error = %v, wantErr %v", err != nil, tt.wantErr)
}

if !tt.wantErr {
metricstest.CheckCountData(t, "pipelines_as_code_pipelinerun_count", tt.tags, 1)
}
})
}
}

// TestCalculatePipelineRunDuration tests pipelinerun duration metric.
func TestCalculatePipelineRunDuration(t *testing.T) {
startTime := metav1.Now()
tests := []struct {
name string
annotations map[string]string
conditionType apis.ConditionType
status corev1.ConditionStatus
reason string
completionTime metav1.Time
tags map[string]string
}{
{
name: "pipelinerun succeeded",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionTrue,
reason: tektonv1.PipelineRunReasonSuccessful.String(),
completionTime: metav1.NewTime(startTime.Time.Add(time.Minute)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonSuccessful.String(),
"repository": "pac-repo",
"status": "success",
},
},
{
name: "pipelinerun completed",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionTrue,
reason: tektonv1.PipelineRunReasonCompleted.String(),
completionTime: metav1.NewTime(startTime.Time.Add(time.Minute)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonCompleted.String(),
"repository": "pac-repo",
"status": "success",
},
},
{
name: "pipelinerun failed",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionFalse,
reason: tektonv1.PipelineRunReasonFailed.String(),
completionTime: metav1.NewTime(startTime.Time.Add(2 * time.Minute)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonFailed.String(),
"repository": "pac-repo",
"status": "failed",
},
},
{
name: "pipelinerun cancelled",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionFalse,
reason: tektonv1.PipelineRunReasonCancelled.String(),
completionTime: metav1.NewTime(startTime.Time.Add(2 * time.Second)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonCancelled.String(),
"repository": "pac-repo",
"status": "cancelled",
},
},
{
name: "pipelinerun timed out",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionFalse,
reason: tektonv1.PipelineRunReasonTimedOut.String(),
completionTime: metav1.NewTime(startTime.Time.Add(10 * time.Minute)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonTimedOut.String(),
"repository": "pac-repo",
"status": "failed",
},
},
{
name: "pipelinerun failed due to couldn't get pipeline",
annotations: map[string]string{
keys.Repository: "pac-repo",
},
conditionType: apis.ConditionSucceeded,
status: corev1.ConditionFalse,
reason: tektonv1.PipelineRunReasonCouldntGetPipeline.String(),
completionTime: metav1.NewTime(startTime.Time.Add(time.Second)),
tags: map[string]string{
"namespace": "pac-ns",
"reason": tektonv1.PipelineRunReasonCouldntGetPipeline.String(),
"repository": "pac-repo",
"status": "failed",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
metricstest.Unregister("pipelines_as_code_pipelinerun_duration_seconds_sum")
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: tt.conditionType,
Status: tt.status,
Reason: tt.reason,
},
}},
PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{
StartTime: &startTime,
CompletionTime: &tt.completionTime,
},
},
}
// checks that metric is unregistered successfully and there is no metric
// before emitting new pr duration metric.
metricstest.AssertNoMetric(t, "pipelines_as_code_pipelinerun_duration_seconds_sum")

if err = r.calculatePRDuration(pr); err != nil {
t.Errorf("calculatePRDuration() error = %v", err)
}

duration := tt.completionTime.Sub(startTime.Time)
metricstest.CheckSumData(t, "pipelines_as_code_pipelinerun_duration_seconds_sum", tt.tags, duration.Seconds())
})
}
}
Loading
Loading