Skip to content

Commit

Permalink
feat(HACBS-1728): implement metrics for the Internal Service controller
Browse files Browse the repository at this point in the history
- adds metrics for internal-services-controller
- update go.mod
- adds tests for /metrics

Signed-off-by: Leandro Mendes <[email protected]>
  • Loading branch information
theflockers committed Mar 2, 2023
1 parent 739e4af commit be96277
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 8 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY loader/ loader/
COPY metrics/ metrics/
COPY tekton/ tekton/

# Build
Expand Down
14 changes: 10 additions & 4 deletions api/v1alpha1/internalrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ limitations under the License.
package v1alpha1

import (
"time"

"github.com/redhat-appstudio/internal-services/metrics"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time"
)

// HandlingReason represents a reason for the InternalRequest "InternalRequestSucceeded" condition.
Expand Down Expand Up @@ -83,7 +85,6 @@ type InternalRequestStatus struct {
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Succeeded",type=string,JSONPath=`.status.conditions[?(@.type=="InternalRequestSucceeded")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="InternalRequestSucceeded")].reason`

// InternalRequest is the Schema for the internalrequests API.
type InternalRequest struct {
metav1.TypeMeta `json:",inline"`
Expand Down Expand Up @@ -117,12 +118,15 @@ func (ir *InternalRequest) HasSucceeded() bool {

// MarkFailed registers the completion time and changes the Succeeded condition to False with the provided message.
func (ir *InternalRequest) MarkFailed(message string) {
if ir.HasCompleted() {
if !ir.HasStarted() || ir.HasCompleted() {
return
}

ir.Status.CompletionTime = &metav1.Time{Time: time.Now()}
ir.setStatusConditionWithMessage(InternalRequestSucceededConditionType, metav1.ConditionFalse, InternalRequestFailed, message)

go metrics.RegisterCompletedInternalRequest(ir.Spec.Request, ir.Namespace, InternalRequestFailed.String(),
ir.Status.StartTime, ir.Status.CompletionTime, false)
}

// MarkInvalid changes the Succeeded condition to False with the provided reason and message.
Expand All @@ -146,12 +150,14 @@ func (ir *InternalRequest) MarkRunning() {

// MarkSucceeded registers the completion time and changes the Succeeded condition to True.
func (ir *InternalRequest) MarkSucceeded() {
if ir.HasCompleted() {
if !ir.HasStarted() || ir.HasCompleted() {
return
}

ir.Status.CompletionTime = &metav1.Time{Time: time.Now()}
ir.setStatusCondition(InternalRequestSucceededConditionType, metav1.ConditionTrue, InternalRequestSucceeded)

go metrics.RegisterCompletedInternalRequest(ir.Spec.Request, ir.Namespace, InternalRequestSucceeded.String(), ir.Status.StartTime, ir.Status.CompletionTime, true)
}

// setStatusCondition creates a new condition with the given InternalRequestSucceededConditionType, status and reason. Then, it sets this new condition,
Expand Down
2 changes: 1 addition & 1 deletion config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bases:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
- ../monitoring

patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
Expand Down
7 changes: 7 additions & 0 deletions config/monitoring/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: Kustomization
apiVersion: kustomize.config.k8s.io/v1beta1
namespace: grafana-operator-system
generatorOptions:
disableNameSuffixHash: true
resources:
- prometheus/monitor.yaml
20 changes: 20 additions & 0 deletions config/monitoring/prometheus/monitor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
control-plane: controller-manager
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
port: https
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
tlsConfig:
insecureSkipVerify: true
selector:
matchLabels:
control-plane: controller-manager
8 changes: 6 additions & 2 deletions controllers/internalrequest/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package internalrequest

import (
"fmt"
"reflect"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libhandler "github.com/operator-framework/operator-lib/handler"
Expand All @@ -27,7 +29,6 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"reflect"
ctrl "sigs.k8s.io/controller-runtime"
)

Expand Down Expand Up @@ -96,6 +97,7 @@ var _ = Describe("PipelineRun", Ordered, func() {
Err: fmt.Errorf("not found"),
},
})
adapter.internalRequest.MarkRunning()

result, err := adapter.EnsurePipelineExists()
Expect(result.CancelRequest && !result.RequeueRequest).To(BeTrue())
Expand Down Expand Up @@ -161,8 +163,8 @@ var _ = Describe("PipelineRun", Ordered, func() {
Err: fmt.Errorf("not found"),
},
})
adapter.internalRequest.MarkRunning()
adapter.internalRequest.MarkSucceeded()

result, err := adapter.EnsurePipelineRunIsDeleted()
Expect(!result.CancelRequest && result.RequeueRequest).To(BeTrue())
Expect(err).NotTo(BeNil())
Expand Down Expand Up @@ -376,13 +378,15 @@ var _ = Describe("PipelineRun", Ordered, func() {
})

It("should set the InternalRequest as succeeded if the PipelineRun succeeded", func() {
adapter.internalRequest.MarkRunning()
pipelineRun := &tektonv1beta1.PipelineRun{}
pipelineRun.Status.MarkSucceeded("", "")
Expect(adapter.registerInternalRequestPipelineRunStatus(pipelineRun)).To(BeNil())
Expect(adapter.internalRequest.HasSucceeded()).To(BeTrue())
})

It("should set the InternalRequest as failed if the PipelineRun failed", func() {
adapter.internalRequest.MarkRunning()
pipelineRun := &tektonv1beta1.PipelineRun{}
pipelineRun.Status.MarkFailed("", "")
Expect(adapter.registerInternalRequestPipelineRunStatus(pipelineRun)).To(BeNil())
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/onsi/ginkgo/v2 v2.5.0
github.com/onsi/gomega v1.24.1
github.com/operator-framework/operator-lib v0.11.0
github.com/prometheus/client_golang v1.13.0
github.com/redhat-appstudio/operator-goodies v0.0.0-20230113090719-a8a43f600367
github.com/tektoncd/pipeline v0.42.0
k8s.io/api v0.25.4
Expand Down Expand Up @@ -63,7 +64,6 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
Expand Down
79 changes: 79 additions & 0 deletions metrics/internalrequest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package metrics

import (
"strconv"

"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)

var (
InternalRequestAttemptConcurrentTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "internal_request_attempt_concurrent_requests",
Help: "Total number of concurrent InternalRequest attempts",
},
)

InternalRequestAttemptDurationSeconds = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "internal_request_attempt_duration_seconds",
Help: "Time from the moment the InternalRequest starts being processed until it completes",
Buckets: []float64{10, 20, 40, 60, 150, 300, 450, 900, 1800, 3600},
},
[]string{"request", "namespace", "reason", "succeeded"},
)

InternalRequestAttemptTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "internal_request_attempt_total",
Help: "Total number of InternalRequests processed by the operator",
},
[]string{"request", "namespace", "reason", "succeeded"},
)
)

// RegisterCompletedInternalRequest decrements the 'internal_request_attempt_concurrent_total' metric, increments `internal_request_attempt_total`
// and registers a new observation for 'internal_request_attempt_duration_seconds' with the elapsed time from the moment the
// InternalRequest attempt started (InternalRequest marked as 'Running').
func RegisterCompletedInternalRequest(request, namespace, reason string, startTime, completionTime *metav1.Time, succeeded bool) {
labels := prometheus.Labels{
"request": request,
"namespace": namespace,
"reason": reason,
"succeeded": strconv.FormatBool(succeeded),
}
InternalRequestAttemptConcurrentTotal.Dec()
InternalRequestAttemptDurationSeconds.With(labels).Observe(completionTime.Sub(startTime.Time).Seconds())
InternalRequestAttemptTotal.With(labels).Inc()
}

// RegisterNewInternalRequest increments the number of the 'internal_request_attempt_concurrent_total' metric which represents the number of concurrent running InternalRequests.
func RegisterNewInternalRequest(creationTime metav1.Time, startTime *metav1.Time) {
InternalRequestAttemptConcurrentTotal.Inc()
}

func init() {
metrics.Registry.MustRegister(
InternalRequestAttemptConcurrentTotal,
InternalRequestAttemptDurationSeconds,
InternalRequestAttemptTotal,
)
}
102 changes: 102 additions & 0 deletions metrics/internalrequest_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package metrics

import (
"context"
"fmt"
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"sigs.k8s.io/controller-runtime/pkg/envtest"

ctrl "sigs.k8s.io/controller-runtime"
)

type inputHeader struct {
Name string
Help string
}

func TestMetricsRelease(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Metrics Release Test Suite")
}

var (
testEnv *envtest.Environment
ctx context.Context
cancel context.CancelFunc
)

var _ = BeforeSuite(func() {
ctx, cancel = context.WithCancel(context.TODO())
testEnv = &envtest.Environment{}

cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())

k8sManager, _ := ctrl.NewManager(cfg, ctrl.Options{
MetricsBindAddress: ":8081",
LeaderElection: false,
})

go func() {
defer GinkgoRecover()
Expect(k8sManager.Start(ctx)).To(Succeed())
}()
})

var _ = AfterSuite(func() {
cancel()
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})

// createCounterReader creates a prometheus counter type string with the given parameters to be used as input data
// for 'strings.NewReader' in Prometheus 'client_golang' function 'testutil.CollectAndCompare'.
func createCounterReader(header inputHeader, labels string, renderHeader bool, count int) string {
readerData := ""
if renderHeader {
readerData = fmt.Sprintf("# HELP %s %s\n# TYPE %s counter\n", header.Name, header.Help, header.Name)
}
readerData += fmt.Sprintf("%s{%s} %d\n", header.Name, labels, count)

return readerData
}

// createHistogramReader creates a prometheus histogram type string with the given parameters to be used as input data
// for 'strings.NewReader' in Prometheus 'client_golang' function 'testutil.CollectAndCompare'.
func createHistogramReader(header inputHeader, timeBuckets []string, bucketsData []int, labels string, sum float64, count int) string {
readerData := fmt.Sprintf("# HELP %s %s\n# TYPE %s histogram\n", header.Name, header.Help, header.Name)
for i, bucket := range timeBuckets {
readerData += fmt.Sprintf("%s_bucket{%sle=\"%s\"} %d\n", header.Name, labels, bucket, bucketsData[i])
}

if labels != "" {
readerData += fmt.Sprintf("%s_sum{%s} %f\n\n", header.Name, labels, sum)
readerData += fmt.Sprintf("%s_count{%s} %d\n", header.Name, labels, count)
} else {
readerData += fmt.Sprintf("%s_sum %f\n%s_count %d\n", header.Name, sum, header.Name, count)
}

return readerData
}
Loading

0 comments on commit be96277

Please sign in to comment.