diff --git a/.github/changelog/generate-changelog.sh b/.github/changelog/generate-changelog.sh index 610c176..a814476 100644 --- a/.github/changelog/generate-changelog.sh +++ b/.github/changelog/generate-changelog.sh @@ -25,7 +25,7 @@ if [ -z "$PREVIOUS_CHANGELOG" ] then echo "Unable to locate previous changelog contents." exit 1 -fi +fi CHANGELOG=$($(go env GOPATH)/bin/changelog-build -this-release $TARGET_SHA \ -last-release $PREVIOUS_RELEASE_SHA \ diff --git a/.github/workflows/go-generate.yml b/.github/workflows/go-generate.yml new file mode 100644 index 0000000..0989251 --- /dev/null +++ b/.github/workflows/go-generate.yml @@ -0,0 +1,25 @@ +# Terraform Provider testing workflow. +name: go-generate + +# This GitHub action runs your tests for each pull request and push. +# Optionally, you can turn it on using a schedule for regular testing. +on: + pull_request: + paths: + - 'docs/**' + - 'tools/**' + +jobs: + generate: + name: Generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 # v3.5.0 + - uses: actions/setup-go@v5 # v4.0.0 + with: + go-version-file: 'go.mod' + - run: go generate ./... + - name: git diff + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) diff --git a/.github/workflows/lint.yml b/.github/workflows/go-lint.yml similarity index 100% rename from .github/workflows/lint.yml rename to .github/workflows/go-lint.yml diff --git a/.github/workflows/goproxy.yml b/.github/workflows/go-proxy.yml similarity index 71% rename from .github/workflows/goproxy.yml rename to .github/workflows/go-proxy.yml index 650533a..ce4e2f0 100644 --- a/.github/workflows/goproxy.yml +++ b/.github/workflows/go-proxy.yml @@ -1,10 +1,8 @@ name: 'Force pkg.go.dev release sync' on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - '**/v[0-9]+.[0-9]+.[0-9]+' + release: + types: [published] jobs: build: diff --git a/.github/workflows/goreport.yml b/.github/workflows/go-report.yml similarity index 72% rename from .github/workflows/goreport.yml rename to .github/workflows/go-report.yml index 1b2b165..b166b4c 100644 --- a/.github/workflows/goreport.yml +++ b/.github/workflows/go-report.yml @@ -9,4 +9,4 @@ jobs: runs-on: group: Default steps: - - uses: creekorful/goreportcard-action@v1.0 \ No newline at end of file + - uses: creekorful/goreportcard-action@v1.0 diff --git a/.github/workflows/gotest.yml b/.github/workflows/go-test.yml similarity index 95% rename from .github/workflows/gotest.yml rename to .github/workflows/go-test.yml index f1f5ee9..14ffd0c 100644 --- a/.github/workflows/gotest.yml +++ b/.github/workflows/go-test.yml @@ -1,7 +1,7 @@ name: Unit tests on: - pull_request_target: + pull_request: workflow_dispatch: permissions: diff --git a/.github/workflows/new-release.yaml b/.github/workflows/new-release.yaml index 8423af0..ab0ef71 100644 --- a/.github/workflows/new-release.yaml +++ b/.github/workflows/new-release.yaml @@ -83,10 +83,23 @@ jobs: - name: Run Go unit tests run: | go test ./... - + generate: + needs: [pre-check] + name: Generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 # v3.5.0 + - uses: actions/setup-go@v5 # v4.0.0 + with: + go-version-file: 'go.mod' + - run: go generate ./... + - name: git diff + run: | + git diff --compact-summary --exit-code || \ + (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) # * Step 2: Create a new tag tag: - needs: [golangci-lint, pre-check, tag-already-exist, testsunit] + needs: [golangci-lint, pre-check, tag-already-exist, testsunit, generate] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/documentation.yaml b/.github/workflows/publish-doc.yaml similarity index 100% rename from .github/workflows/documentation.yaml rename to .github/workflows/publish-doc.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01cb697..7a16e44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: check-merge-conflict - id: trailing-whitespace - - id: end-of-file-fixer + args: ["--markdown-linebreak-ext=md"] - id: forbid-submodules ## GOLANG diff --git a/cmd/admission-controller/main.go b/cmd/admission-controller/main.go index b435303..571ea87 100644 --- a/cmd/admission-controller/main.go +++ b/cmd/admission-controller/main.go @@ -17,6 +17,7 @@ import ( "github.com/orange-cloudavenue/kube-image-updater/internal/httpserver" client "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" "github.com/orange-cloudavenue/kube-image-updater/internal/log" + "github.com/orange-cloudavenue/kube-image-updater/internal/metrics" ) var ( @@ -38,6 +39,9 @@ var ( ) func init() { + // Init Metrics + metrics.AdmissionController() + // webhook server running namespace (default to "default") if os.Getenv("POD_NAMESPACE") != "" { webhookNamespace = os.Getenv("POD_NAMESPACE") diff --git a/cmd/admission-controller/webhook.go b/cmd/admission-controller/webhook.go index 3a71c5b..64a350c 100644 --- a/cmd/admission-controller/webhook.go +++ b/cmd/admission-controller/webhook.go @@ -23,8 +23,8 @@ import ( // func serveHandler func ServeHandler(w http.ResponseWriter, r *http.Request) { // Prometheus metrics - metrics.AdmissionController().Total().Inc() - timeAC := metrics.AdmissionController().Duration() + metrics.AdmissionController().RequestTotal.Inc() + timeAC := metrics.AdmissionController().RequestDuration.NewTimer() defer timeAC.ObserveDuration() var body []byte @@ -35,7 +35,7 @@ func ServeHandler(w http.ResponseWriter, r *http.Request) { } if len(body) == 0 { // increment the total number of errors - metrics.AdmissionController().TotalErr().Inc() + metrics.AdmissionController().RequestErrorTotal.Inc() log.Error("empty body") http.Error(w, "empty body", http.StatusBadRequest) @@ -46,7 +46,7 @@ func ServeHandler(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") if contentType != "application/json" { // increment the total number of errors - metrics.AdmissionController().TotalErr().Inc() + metrics.AdmissionController().RequestErrorTotal.Inc() http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) return @@ -56,7 +56,7 @@ func ServeHandler(w http.ResponseWriter, r *http.Request) { ar := admissionv1.AdmissionReview{} if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { // increment the total number of errors - metrics.AdmissionController().TotalErr().Inc() + metrics.AdmissionController().RequestErrorTotal.Inc() log.WithError(err).Warn("Can't decode body") admissionResponse = &admissionv1.AdmissionResponse{ @@ -84,13 +84,13 @@ func ServeHandler(w http.ResponseWriter, r *http.Request) { resp, err := json.Marshal(admissionReview) if err != nil { // increment the total number of errors - metrics.AdmissionController().TotalErr().Inc() + metrics.AdmissionController().RequestErrorTotal.Inc() http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) } if _, err := w.Write(resp); err != nil { // increment the total number of errors - metrics.AdmissionController().TotalErr().Inc() + metrics.AdmissionController().RequestErrorTotal.Inc() http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } @@ -139,8 +139,8 @@ func mutate(ctx context.Context, ar *admissionv1.AdmissionReview) *admissionv1.A // create mutation patch for pod. func createPatch(ctx context.Context, pod *corev1.Pod) ([]byte, error) { // Metrics - increment the total number of patch - metrics.AdmissionControllerPatch().Total().Inc() - timePatch := metrics.AdmissionControllerPatch().Duration() + metrics.AdmissionController().PatchTotal.Inc() + timePatch := metrics.AdmissionController().PatchDuration.NewTimer() defer timePatch.ObserveDuration() var err error @@ -148,7 +148,7 @@ func createPatch(ctx context.Context, pod *corev1.Pod) ([]byte, error) { an := annotations.New(ctx, pod) if !an.Enabled().Get() { // increment the total number of errors - metrics.AdmissionControllerPatch().TotalErr().Inc() + metrics.AdmissionController().PatchErrorTotal.Inc() return nil, fmt.Errorf("annotation not enabled") } @@ -175,7 +175,7 @@ func createPatch(ctx context.Context, pod *corev1.Pod) ([]byte, error) { image, err = kubeClient.Image().Find(ctx, pod.Namespace, imageP.GetImageWithoutTag()) if err != nil { // increment the total number of errors - metrics.AdmissionControllerPatch().TotalErr().Inc() + metrics.AdmissionController().PatchErrorTotal.Inc() log. WithFields(logrus.Fields{ @@ -191,7 +191,7 @@ func createPatch(ctx context.Context, pod *corev1.Pod) ([]byte, error) { image, err = kubeClient.Image().Get(ctx, pod.Namespace, crdName) if err != nil { // increment the total number of errors - metrics.AdmissionControllerPatch().TotalErr().Inc() + metrics.AdmissionController().PatchErrorTotal.Inc() log. WithFields(logrus.Fields{ diff --git a/cmd/kimup/main.go b/cmd/kimup/main.go index fd04eb0..51c25ee 100644 --- a/cmd/kimup/main.go +++ b/cmd/kimup/main.go @@ -13,6 +13,7 @@ import ( "github.com/orange-cloudavenue/kube-image-updater/internal/httpserver" "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" "github.com/orange-cloudavenue/kube-image-updater/internal/log" + "github.com/orange-cloudavenue/kube-image-updater/internal/metrics" "github.com/orange-cloudavenue/kube-image-updater/internal/models" "github.com/orange-cloudavenue/kube-image-updater/internal/triggers" ) @@ -24,6 +25,13 @@ var ( ) func init() { + // Initialize the metrics + metrics.Tags() + metrics.Events() + metrics.Actions() + metrics.Rules() + metrics.Registry() + // TODO add namespace scope // Flag "loglevel" is set in log package flag.Parse() diff --git a/cmd/kimup/scheduler.go b/cmd/kimup/scheduler.go index eb7cd0d..8124204 100644 --- a/cmd/kimup/scheduler.go +++ b/cmd/kimup/scheduler.go @@ -30,9 +30,9 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { // Add event lock event.On(triggers.RefreshImage.String(), event.ListenerFunc(func(e event.Event) (err error) { // Increment the counter for the events - metrics.Events().Total().Inc() + metrics.Events().TriggeredTotal.Inc() // Start the timer for the event execution - timerEvents := metrics.Events().Duration() + timerEvents := metrics.Events().TriggeredDuration.NewTimer() defer timerEvents.ObserveDuration() if l[e.Data()["namespace"].(string)+"/"+e.Data()["image"].(string)] == nil { @@ -72,8 +72,8 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { i := utils.ImageParser(image.Spec.Image) // Prometheus metrics - Increment the counter for the registry - metrics.Registry().Total().Inc() - timerRegistry := metrics.Registry().Duration() + metrics.Registry().RequestTotal.WithLabelValues(i.GetRegistry()).Inc() + timerRegistry := metrics.Registry().RequestDuration.NewTimer(i.GetRegistry()) re, err := registry.New(ctx, image.Spec.Image, registry.Settings{ InsecureTLS: image.Spec.InsecureSkipTLSVerify, @@ -93,24 +93,25 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { timerRegistry.ObserveDuration() if err != nil { // Prometheus metrics - Increment the counter for the registry with error - metrics.Registry().TotalErr().Inc() + metrics.Registry().RequestErrorTotal.WithLabelValues(i.GetRegistry()).Inc() return err } // Prometheus metrics - Increment the counter for the tags - metrics.Tags().Total().Inc() - timerTags := metrics.Tags().Duration() + metrics.Tags().RequestTotal.Inc() + timerTags := metrics.Tags().RequestDuration.NewTimer() tagsAvailable, err := re.Tags() timerTags.ObserveDuration() if err != nil { // Prometheus metrics - Increment the counter for the tags with error - metrics.Tags().TotalErr().Inc() - + metrics.Tags().RequestErrorTotal.Inc() return err } + metrics.Tags().AvailableSum.WithLabelValues(image.Spec.Image).Observe(float64(len(tagsAvailable))) + log.Debugf("[RefreshImage] %d tags available for %s", len(tagsAvailable), image.Spec.Image) for _, rule := range image.Spec.Rules { @@ -128,8 +129,8 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { r.Init(tag, tagsAvailable, rule.Value) // Prometheus metrics - Increment the counter for the rules - metrics.Rules().Total().Inc() - timerRules := metrics.Rules().Duration() + metrics.Rules().EvaluatedTotal.Inc() + timerRules := metrics.Rules().EvaluatedDuration.NewTimer() match, newTag, err := r.Evaluate() @@ -138,7 +139,7 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { if err != nil { // Prometheus metrics - Increment the counter for the evaluated rule with error - metrics.Rules().TotalErr().Inc() + metrics.Rules().EvaluatedErrorTotal.Inc() log.Errorf("Error evaluating rule: %v", err) continue @@ -159,8 +160,8 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { }, &image, action.Data) // Prometheus metrics - Increment the counter for the actions - metrics.Actions().Total().Inc() - timerActions := metrics.Actions().Duration() + metrics.Actions().ExecutedTotal.Inc() + timerActions := metrics.Actions().ExecutedDuration.NewTimer() err = a.Execute(ctx) @@ -169,7 +170,7 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { if err != nil { // Prometheus metrics - Increment the counter for the executed action with error - metrics.Actions().TotalErr().Inc() + metrics.Actions().ExecutedErrorTotal.Inc() log.Errorf("Error executing action(%s): %v", action.Type, err) continue @@ -183,7 +184,7 @@ func initScheduler(ctx context.Context, k kubeclient.Interface) { }) // Prometheus metrics - Increment the counter for the events evaluated with error - metrics.Events().TotalErr().Inc() + metrics.Events().TriggerdErrorTotal.Inc() return retryErr }), event.Normal) } diff --git a/docs/advanced/metrics.md b/docs/advanced/metrics.md new file mode 100644 index 0000000..ad62184 --- /dev/null +++ b/docs/advanced/metrics.md @@ -0,0 +1,49 @@ +--- +hide: + - toc +--- + +# Metrics + +kimup exposes metrics to monitor the performance. The metrics are exposed in the Prometheus format and can be scraped by Prometheus or any other monitoring tool that can scrape Prometheus. + +## Settings + +The following arguments can be used to configure the metrics *(Available in kimup-operator, kimup-controller and kimup-admission-controller)*: + +| Flag | Default | Description | +| -------------- | -------- | ------------------------- | +| --metrics | false | Enable metrics collection | +| --metrics-port | :9080 | Port to expose metrics on | +| --metrics-path | /metrics | Path to expose metrics on | + + +## Metrics + +The following metrics are exposed: + +| Metrics | Description | +| ---------------------------------------------- | ----------------------------------------------------------- | +| kimup_actions_executed_duration | The duration in seconds of action performed. | +| kimup_actions_executed_error_total | The total number of action performed with error. | +| kimup_actions_executed_total | The total number of action performed. | +| kimup_admission_controller_patch_duration | The duration in seconds of patch in admission controller. | +| kimup_admission_controller_patch_error_total | The total number of patch action performed with error. | +| kimup_admission_controller_patch_total | The total number of patch action performed. | +| kimup_admission_controller_request_duration | The duration in seconds of request in admission controller. | +| kimup_admission_controller_request_error_total | The total number of request received with error. | +| kimup_admission_controller_request_total | The total number of request received. | +| kimup_events_triggerd_error_total | The total number of events triggered with error. | +| kimup_events_triggered_duration | The duration in seconds of events triggered. | +| kimup_events_triggered_total | The total number of events triggered. | +| kimup_registry_request_duration | The duration in seconds of registry evaluated. | +| kimup_registry_request_error_total | The total number of registry evaluated with error. | +| kimup_registry_request_total | The total number of registry evaluated. | +| kimup_rules_evaluated_duration | The duration in seconds of rules evaluated. | +| kimup_rules_evaluated_error_total | The total number of rules evaluated with error. | +| kimup_rules_evaluated_total | The total number of rules evaluated. | +| kimup_tags_available_sum | The total number of tags available for an image. | +| kimup_tags_request_duration | The duration in seconds of the request to list tags. | +| kimup_tags_request_error_total | The total number returned an error when calling list tags. | +| kimup_tags_request_total | The total number of requests to list tags. | + diff --git a/docs/advanced/metrics.md.tmpl b/docs/advanced/metrics.md.tmpl new file mode 100644 index 0000000..efdb891 --- /dev/null +++ b/docs/advanced/metrics.md.tmpl @@ -0,0 +1,20 @@ +--- +hide: + - toc +--- + +# Metrics + +kimup exposes metrics to monitor the performance. The metrics are exposed in the Prometheus format and can be scraped by Prometheus or any other monitoring tool that can scrape Prometheus. + +## Settings + +The following arguments can be used to configure the metrics *(Available in kimup-operator, kimup-controller and kimup-admission-controller)*: + +{{ tableSettings }} + +## Metrics + +The following metrics are exposed: + +{{ tableMetrics }} diff --git a/go.mod b/go.mod index f647651..1be4751 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,10 @@ require ( github.com/containers/image/v5 v5.32.2 github.com/containrrr/shoutrrr v0.8.0 github.com/crazy-max/diun/v4 v4.28.0 + github.com/fbiville/markdown-table-formatter v0.3.0 github.com/go-chi/chi/v5 v5.1.0 github.com/gookit/event v1.1.2 + github.com/iancoleman/strcase v0.3.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/ory/dockertest/v3 v3.11.0 github.com/prometheus/client_golang v1.20.4 diff --git a/go.sum b/go.sum index aaf89d1..c7f154c 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNYvwXQMMMIKNZop2SSho= +github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -116,6 +118,8 @@ github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= diff --git a/internal/metrics/actions.go b/internal/metrics/actions.go index 039c90a..4641009 100644 --- a/internal/metrics/actions.go +++ b/internal/metrics/actions.go @@ -5,48 +5,21 @@ import ( ) type ( - actions struct{} + actions struct { + ExecutedTotal prometheus.Counter `help:"The total number of action performed."` + ExecutedErrorTotal prometheus.Counter `help:"The total number of action performed with error."` + ExecutedDuration Histogram `help:"The duration in seconds of action performed."` + } ) -var ( - // Prometheus metrics - actionsTotal prometheus.Counter = NewCounter("actions_total", "The total number of action performed.") - actionsErrTotal prometheus.Counter = NewCounter("actions_error_total", "The total number of action performed with error.") - actionsDuration prometheus.Histogram = NewHistogram("actions_duration_seconds", "The duration in seconds of action performed.") -) +var actionsMetrics actions // Actions returns a new actions. // This is the metrics for the actions. -func Actions() *actions { - return &actions{} -} - -// Total returns the total number of action performed. -// The counter is used to observe the number of actions that have been executed. -// The counter is incremented each time an action is executed -// A good practice is to use the following pattern: -// -// metrics.Actions().Total().Inc() -func (a *actions) Total() prometheus.Counter { - return actionsTotal -} - -// TotalErr returns the total number of action performed with error. -// The counter is used to observe the number of actions that failed. -// The counter is incremented each time an action fails. -// A good practice is to use the following pattern: -// -// metrics.Actions().TotalErr().Inc() -func (a *actions) TotalErr() prometheus.Counter { - return actionsErrTotal -} +func Actions() actions { + if actionsMetrics.ExecutedTotal == nil { + actionsMetrics = initMetrics(actions{}) + } -// ExecuteDuration returns the duration of the action execution. -// A good practice is to use the following pattern: -// -// timerActions := metrics.Actions().Duration() -// -// defer timerActions.ObserveDuration() -func (a *actions) Duration() *prometheus.Timer { - return prometheus.NewTimer(actionsDuration) + return actionsMetrics } diff --git a/internal/metrics/admission-controller-patch.go b/internal/metrics/admission-controller-patch.go deleted file mode 100644 index f47b68b..0000000 --- a/internal/metrics/admission-controller-patch.go +++ /dev/null @@ -1,52 +0,0 @@ -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -type ( - admissionControllerPatch struct{} -) - -var ( - // Prometheus metrics - admissionControllerPatchTotal prometheus.Counter = NewCounter("admissionControllerPatch_total", "The total number of patch by the Admission Controller is generate.") - admissionControllerPatchErrTotal prometheus.Counter = NewCounter("admissionControllerPatch_error_total", "The total number of patch by the AdmissionController generate with error.") - admissionControllerPatchDuration prometheus.Histogram = NewHistogram("admissionControllerPatch_duration_seconds", "The duration in seconds of the generated patch by the Admission Controller.") -) - -// admissionControllerPatch returns a new admissionControllerPatch. -// This is the metrics for the admissionControllerPatch. -func AdmissionControllerPatch() *admissionControllerPatch { - return &admissionControllerPatch{} -} - -// Total returns the total number of admissionControllerPatch performed. -// The counter is used to observe the number of admissionControllerPatch that have been -// executed. The counter is incremented each time a tag is executed -// A good practice is to use the following pattern: -// -// metrics.admissionControllerPatch().Total().Inc() -func (a *admissionControllerPatch) Total() prometheus.Counter { - return admissionControllerPatchTotal -} - -// TotalErr returns the total number of admissionControllerPatch performed with error. -// The counter is used to observe the number of admissionControllerPatch that failed. -// The counter is incremented each time a tag fails. -// A good practice is to use the following pattern: -// -// metrics.admissionControllerPatch().TotalErr().Inc() -func (a *admissionControllerPatch) TotalErr() prometheus.Counter { - return admissionControllerPatchErrTotal -} - -// Duration returns the duration of the admissionControllerPatch execution. -// A good practice is to use the following pattern: -// -// timeradmissionControllerPatch := metrics.admissionControllerPatch().Duration() -// -// defer timeradmissionControllerPatch.ObserveDuration() -func (a *admissionControllerPatch) Duration() *prometheus.Timer { - return prometheus.NewTimer(admissionControllerPatchDuration) -} diff --git a/internal/metrics/admission-controller.go b/internal/metrics/admission-controller.go index 4e264cb..cb3598c 100644 --- a/internal/metrics/admission-controller.go +++ b/internal/metrics/admission-controller.go @@ -5,48 +5,24 @@ import ( ) type ( - admissionController struct{} + admissionController struct { + RequestTotal prometheus.Counter `help:"The total number of request received."` + RequestErrorTotal prometheus.Counter `help:"The total number of request received with error."` + RequestDuration Histogram `help:"The duration in seconds of request in admission controller."` + PatchTotal prometheus.Counter `help:"The total number of patch action performed."` + PatchErrorTotal prometheus.Counter `help:"The total number of patch action performed with error."` + PatchDuration Histogram `help:"The duration in seconds of patch in admission controller."` + } ) -var ( - // Prometheus metrics - admissionControllerTotal prometheus.Counter = NewCounter("admissionController_total", "The total number of action performed.") - admissionControllerTotalErr prometheus.Counter = NewCounter("admissionController_total_err", "The total number of action performed with error.") - admissionControllerDuration prometheus.Histogram = NewHistogram("admissionController_duration_seconds", "The duration in seconds of action performed.") -) +var admissionControllerMetrics admissionController // admissionController returns a new admissionController. // This is the metrics for the admissionController. -func AdmissionController() *admissionController { - return &admissionController{} -} - -// Total returns the total number of admission controller is performed. -// The counter is used to observe the number of admissionController that have been executed. -// The counter is incremented each time an admission controller is executed -// A good practice is to use the following pattern: -// -// metrics.admissionController().Total().Inc() -func (a *admissionController) Total() prometheus.Counter { - return admissionControllerTotal -} - -// TotalErr returns the total number of admission controller performed with error. -// The counter is used to observe the number of admissionController that failed. -// The counter is incremented each time an admission controller fails. -// A good practice is to use the following pattern: -// -// metrics.admissionController().TotalErr().Inc() -func (a *admissionController) TotalErr() prometheus.Counter { - return admissionControllerTotalErr -} +func AdmissionController() admissionController { + if admissionControllerMetrics.RequestTotal == nil { + admissionControllerMetrics = initMetrics(admissionController{}) + } -// ExecuteDuration returns the duration of the admission controller execution. -// A good practice is to use the following pattern: -// -// timer := metrics.AdmissionController().Duration() -// -// defer timer.ObserveDuration() -func (a *admissionController) Duration() *prometheus.Timer { - return prometheus.NewTimer(admissionControllerDuration) + return admissionControllerMetrics } diff --git a/internal/metrics/common.go b/internal/metrics/common.go new file mode 100644 index 0000000..953b348 --- /dev/null +++ b/internal/metrics/common.go @@ -0,0 +1,339 @@ +package metrics + +import ( + "fmt" + "reflect" + "strings" + + "github.com/iancoleman/strcase" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type ( + Histogram struct { + prometheus.Histogram + } + + HistogramVec struct { + *prometheus.HistogramVec + } +) + +func (h HistogramVec) NewTimer(labelsValues ...string) *prometheus.Timer { + return prometheus.NewTimer(h.WithLabelValues(labelsValues...)) +} + +func (h Histogram) NewTimer() *prometheus.Timer { + return prometheus.NewTimer(h.Histogram) +} + +func initMetrics[T any](s T) T { + // Reflect the struct type + t := reflect.TypeOf(s) + + // Get the name of the struct + name := t.Name() + + // Reflect the struct value + v := reflect.ValueOf(&s).Elem() + + // Iterate over the struct fields + for i := 0; i < t.NumField(); i++ { + // Get the field + field := t.Field(i) + + // Get the field value + fieldValue := v.Field(i) + + // Get the help tag + help := field.Tag.Get("help") + + labels := []string{} + if field.Tag.Get("labels") != "" { + // split string by comma + labels = strings.Split(field.Tag.Get("labels"), ",") + } + + // Create a new metric + switch field.Type.String() { + case "prometheus.Counter", "*prometheus.CounterVec": + if len(labels) > 0 { + fieldValue.Set(reflect.ValueOf(newCounterWithVec(buildMetricName(name, field.Name), help, labels))) + } else { + fieldValue.Set(reflect.ValueOf(newCounter(buildMetricName(name, field.Name), help))) + } + case "metrics.Histogram", "metrics.HistogramVec": + if len(labels) > 0 { + fieldValue.Set(reflect.ValueOf(newHistogramVec(buildMetricName(name, field.Name), help, labels))) + } else { + fieldValue.Set(reflect.ValueOf(newHistogram(buildMetricName(name, field.Name), help))) + } + case "prometheus.Gauge", "*prometheus.GaugeVec": + if len(labels) > 0 { + fieldValue.Set(reflect.ValueOf(newGaugeWithVec(buildMetricName(name, field.Name), help, labels))) + } else { + fieldValue.Set(reflect.ValueOf(newGauge(buildMetricName(name, field.Name), help))) + } + case "prometheus.Summary", "*prometheus.SummaryVec": + if len(labels) > 0 { + fieldValue.Set(reflect.ValueOf(newSummaryWithVec(buildMetricName(name, field.Name), help, labels))) + } else { + fieldValue.Set(reflect.ValueOf(newSummary(buildMetricName(name, field.Name), help))) + } + default: + panic(fmt.Sprintf("unsupported type %s", field.Type.String())) + } + } + + return s +} + +func buildMetricName(category, name string) string { + return fmt.Sprintf( + "kimup_%s_%s", + strcase.ToSnake(category), + strcase.ToSnake(name), + ) +} + +// newGauge creates a new Prometheus gauge +// The newGauge use a function to directly register the gauge +// The function returns a prometheus.Gauge +// +// Name: The name of the gauge +// Help: The description help text of the gauge +func newGauge(name, help string) prometheus.Gauge { + if Metrics[MetricTypeGauge] == nil { + Metrics[MetricTypeGauge] = make(map[string]interface{}) + } + + // Add the gauge to the map + Metrics[MetricTypeGauge][name] = MetricGauge{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the gauge prometheus + Gauge: promauto.NewGauge(prometheus.GaugeOpts{ + Name: name, + Help: help, + }), + } + + return Metrics[MetricTypeGauge][name].(MetricGauge).Gauge +} + +// newGaugeWithVec creates a new Prometheus gauge with labels +// The newGaugeWithVec use a function to directly register the gauge with labels +// The function returns a prometheus.GaugeVec +// +// Name: The name of the gauge +// Help: The description help text of the gauge +// Labels: The labels of the gauge +func newGaugeWithVec(name, help string, labels []string) *prometheus.GaugeVec { + if Metrics[MetricTypeGauge] == nil { + Metrics[MetricTypeGauge] = make(map[string]interface{}) + } + + // Add the gauge to the map + Metrics[MetricTypeGauge][name] = MetricGaugeVec{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the gauge prometheus + GaugeVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: name, + Help: help, + }, labels), + } + + return Metrics[MetricTypeGauge][name].(MetricGaugeVec).GaugeVec +} + +// * Counter + +// newCounter creates a new Prometheus counter +// The newCounter use a function to directly register the counter +// The function returns a prometheus.Counter +// +// Name: The name of the counter +// Help: The description help text of the counter +func newCounter(name, help string) prometheus.Counter { + if Metrics[MetricTypeCounter] == nil { + Metrics[MetricTypeCounter] = make(map[string]interface{}) + } + + // Add the counter to the map + Metrics[MetricTypeCounter][name] = MetricCounter{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the counter prometheus + Counter: promauto.NewCounter(prometheus.CounterOpts{ + Name: name, + Help: help, + }), + } + + return Metrics[MetricTypeCounter][name].(MetricCounter).Counter +} + +// newCounterWithVec creates a new Prometheus counter with labels +// The newCounterWithVec use a function to directly register the counter with labels +// The function returns a prometheus.CounterVec +// +// Name: The name of the counter +// Help: The description help text of the counter +// Labels: The labels of the counter +func newCounterWithVec(name, help string, labels []string) *prometheus.CounterVec { + if Metrics[MetricTypeCounter] == nil { + Metrics[MetricTypeCounter] = make(map[string]interface{}) + } + + // Add the counter to the map + Metrics[MetricTypeCounter][name] = MetricCounterVec{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the counter prometheus + CounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: name, + Help: help, + }, labels), + } + + return Metrics[MetricTypeCounter][name].(MetricCounterVec).CounterVec +} + +// * Summary + +// newSummary creates a new Prometheus summary +// The newSummary use a function to directly register the summary +// The function returns a prometheus.Summary +// +// Name: The name of the summary +// Help: The description help text of the summary +func newSummary(name, help string) prometheus.Summary { + if Metrics[MetricTypeSummary] == nil { + Metrics[MetricTypeSummary] = make(map[string]interface{}) + } + + // Add the summary to the map + Metrics[MetricTypeSummary][name] = MetricSummary{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the summary prometheus + Summary: promauto.NewSummary(prometheus.SummaryOpts{ + Name: name, + Help: help, + }), + } + + return Metrics[MetricTypeSummary][name].(MetricSummary).Summary +} + +// newSummaryWithVec creates a new Prometheus summary with labels +// The newSummaryWithVec use a function to directly register the summary with labels +// The function returns a prometheus.Summary +// +// Name: The name of the summary +// Help: The description help text of the summary +// Labels: The labels of the summary +func newSummaryWithVec(name, help string, labels []string) *prometheus.SummaryVec { + if Metrics[MetricTypeSummary] == nil { + Metrics[MetricTypeSummary] = make(map[string]interface{}) + } + + // Add the summary to the map + Metrics[MetricTypeSummary][name] = MetricSummaryVec{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the summary prometheus + SummaryVec: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: name, + Help: help, + }, labels), + } + + return Metrics[MetricTypeSummary][name].(MetricSummaryVec).SummaryVec +} + +// * Histogram + +// newHistogram creates a new Prometheus histogram +// The newHistogram use a function to directly register the histogram +// The function returns a prometheus.Histogram +// +// Name: The name of the histogram +// Help: The description help text of the histogram +func newHistogram(name, help string) Histogram { + if Metrics[MetricTypeHistogram] == nil { + Metrics[MetricTypeHistogram] = make(map[string]interface{}) + } + + // Add the histogram to the map + Metrics[MetricTypeHistogram][name] = MetricHistogram{ + // Create the metricBase + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the histogram prometheus + Histogram: Histogram{ + promauto.NewHistogram(prometheus.HistogramOpts{ + Name: name, + Help: help, + // Bucket configuration for microsecond durations + Buckets: []float64{0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.5, 1, 2, 5, 10}, + }), + }, + } + + return Metrics[MetricTypeHistogram][name].(MetricHistogram).Histogram +} + +// newHistogramVec creates a new Prometheus histogram with labels +// The newHistogramVec use a function to directly register the histogram with labels +// The function returns a prometheus.HistogramVec +// +// Name: The name of the histogram +// Help: The description help text of the histogram +// Labels: The labels of the histogram +func newHistogramVec(name, help string, labels []string) HistogramVec { + if Metrics[MetricTypeHistogram] == nil { + Metrics[MetricTypeHistogram] = make(map[string]interface{}) + } + + // Add the histogram to the map + Metrics[MetricTypeHistogram][name] = MetricHistogramVec{ + metricBase: metricBase{ + Name: name, + Help: help, + }, + // Create the metricBase + HistogramVec: HistogramVec{ + prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: name, + Help: help, + // Bucket configuration for microsecond durations + Buckets: []float64{0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.5, 1, 2, 5, 10}, + }, labels), + }, + } + + return Metrics[MetricTypeHistogram][name].(MetricHistogramVec).HistogramVec +} diff --git a/internal/metrics/events.go b/internal/metrics/events.go index 34a9096..e129c2d 100644 --- a/internal/metrics/events.go +++ b/internal/metrics/events.go @@ -5,49 +5,21 @@ import ( ) type ( - events struct{} + events struct { + TriggeredTotal prometheus.Counter `help:"The total number of events triggered."` + TriggerdErrorTotal prometheus.Counter `help:"The total number of events triggered with error."` + TriggeredDuration Histogram `help:"The duration in seconds of events triggered."` + } ) -var ( - // Prometheus metrics - eventsTotal prometheus.Counter = NewCounter("events_total", "The total number of events.") - eventsTotalErr prometheus.Counter = NewCounter("events_total_err", "The total number of events with error.") - eventsDuration prometheus.Histogram = NewHistogram("events_duration_seconds", "The duration in seconds of events.") -) +var eventsMetrics events // Events returns a new events. // This is the metrics for the events. -func Events() *events { - return &events{} -} - -// Total returns the total number of event performed. -// The counter is used to observe the number of events that have been executed. -// The counter is incremented each time an event is executed -// A good practice is to use the following pattern: -// -// metrics.Events().Total().Inc() -func (a *events) Total() prometheus.Counter { - return eventsTotal -} - -// TotalErr returns the total number of event performed with error. -// The counter is used to observe the number of events that failed. -// The counter is incremented each time an event fails. -// A good practice is to use the following pattern: -// -// metrics.Events().TotalErr().Inc() -func (a *events) TotalErr() prometheus.Counter { - return eventsTotalErr -} +func Events() events { + if eventsMetrics.TriggeredTotal == nil { + eventsMetrics = initMetrics(events{}) + } -// Duration returns a prometheus histogram object. -// The histogram is used to observe the duration of the events execution. -// A good practice is to use the following pattern: -// -// timerEvents := metrics.Events().Duration() -// -// defer timerEvents.ObserveDuration() -func (a *events) Duration() *prometheus.Timer { - return prometheus.NewTimer(eventsDuration) + return eventsMetrics } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index a282d0d..c468fb2 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -2,7 +2,6 @@ package metrics import ( "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" ) // metricBase is a base struct for all metrics @@ -17,14 +16,29 @@ type ( Counter prometheus.Counter } + MetricCounterVec struct { + metricBase + CounterVec *prometheus.CounterVec + } + MetricGauge struct { metricBase Gauge prometheus.Gauge } + MetricGaugeVec struct { + metricBase + GaugeVec *prometheus.GaugeVec + } + MetricHistogram struct { metricBase - Histogram prometheus.Histogram + Histogram Histogram + } + + MetricHistogramVec struct { + metricBase + HistogramVec HistogramVec } MetricSummary struct { @@ -32,7 +46,17 @@ type ( Summary prometheus.Summary } + MetricSummaryVec struct { + metricBase + SummaryVec *prometheus.SummaryVec + } + MetricType string + + Metric interface { + GetHelp() string + GetName() string + } ) const ( @@ -48,116 +72,21 @@ const ( var Metrics = make(map[MetricType]map[string]interface{}) -// NewCounter creates a new Prometheus counter -// The NewCounter use a function to directly register the counter -// The function returns a prometheus.Counter -// -// Name: The name of the counter -// Help: The description help text of the counter -func NewCounter(name, help string) prometheus.Counter { - if Metrics[MetricTypeCounter] == nil { - Metrics[MetricTypeCounter] = make(map[string]interface{}) - } - - // Add the counter to the map - Metrics[MetricTypeCounter][name] = MetricCounter{ - // Create the metricBase - metricBase: metricBase{ - Name: name, - Help: help, - }, - // Create the counter prometheus - Counter: promauto.NewCounter(prometheus.CounterOpts{ - Name: name, - Help: help, - }), - } - - return Metrics[MetricTypeCounter][name].(MetricCounter).Counter -} - -// NewGauge creates a new Prometheus gauge -// The NewGauge use a function to directly register the gauge -// The function returns a prometheus.Gauge -// -// Name: The name of the gauge -// Help: The description help text of the gauge -func NewGauge(name, help string) prometheus.Gauge { - if Metrics[MetricTypeGauge] == nil { - Metrics[MetricTypeGauge] = make(map[string]interface{}) - } - - // Add the gauge to the map - Metrics[MetricTypeGauge][name] = MetricGauge{ - // Create the metricBase - metricBase: metricBase{ - Name: name, - Help: help, - }, - // Create the gauge prometheus - Gauge: promauto.NewGauge(prometheus.GaugeOpts{ - Name: name, - Help: help, - }), - } - - return Metrics[MetricTypeGauge][name].(MetricGauge).Gauge +func InitAll() { + Actions() + Events() + Tags() + Rules() + Registry() + AdmissionController() } -// NewHistogram creates a new Prometheus histogram -// The NewHistogram use a function to directly register the histogram -// The function returns a prometheus.Histogram -// -// Name: The name of the histogram -// Help: The description help text of the histogram -func NewHistogram(name, help string) prometheus.Histogram { - if Metrics[MetricTypeHistogram] == nil { - Metrics[MetricTypeHistogram] = make(map[string]interface{}) - } - - // Add the histogram to the map - Metrics[MetricTypeHistogram][name] = MetricHistogram{ - // Create the metricBase - metricBase: metricBase{ - Name: name, - Help: help, - }, - // Create the histogram prometheus - Histogram: promauto.NewHistogram(prometheus.HistogramOpts{ - Name: name, - Help: help, - // Bucket configuration for microsecond durations - Buckets: []float64{0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.5, 1, 2, 5, 10}, - }), - } - - return Metrics[MetricTypeHistogram][name].(MetricHistogram).Histogram +// GetHelp returns the help text of the metric +func (m metricBase) GetHelp() string { + return m.Help } -// NewSummary creates a new Prometheus summary -// The NewSummary use a function to directly register the summary -// The function returns a prometheus.Summary -// -// Name: The name of the summary -// Help: The description help text of the summary -func NewSummary(name, help string) prometheus.Summary { - if Metrics[MetricTypeSummary] == nil { - Metrics[MetricTypeSummary] = make(map[string]interface{}) - } - - // Add the summary to the map - Metrics[MetricTypeSummary][name] = MetricSummary{ - // Create the metricBase - metricBase: metricBase{ - Name: name, - Help: help, - }, - // Create the summary prometheus - Summary: promauto.NewSummary(prometheus.SummaryOpts{ - Name: name, - Help: help, - }), - } - - return Metrics[MetricTypeSummary][name].(MetricSummary).Summary +// GetName returns the name of the metric +func (m metricBase) GetName() string { + return m.Name } diff --git a/internal/metrics/registry.go b/internal/metrics/registry.go index b21a5e5..8bd7aef 100644 --- a/internal/metrics/registry.go +++ b/internal/metrics/registry.go @@ -5,48 +5,21 @@ import ( ) type ( - registry struct{} + registry struct { + RequestTotal *prometheus.CounterVec `labels:"registry_name" help:"The total number of registry evaluated."` + RequestErrorTotal *prometheus.CounterVec `labels:"registry_name" help:"The total number of registry evaluated with error."` + RequestDuration HistogramVec `labels:"registry_name" help:"The duration in seconds of registry evaluated."` + } ) -var ( - // Prometheus metrics - registryTotal prometheus.Counter = NewCounter("registry_total", "The total number of registry evaluated.") - registryErrTotal prometheus.Counter = NewCounter("registry_error_total", "The total number of registry evaluated with error.") - registryDuration prometheus.Histogram = NewHistogram("registry_duration_seconds", "The duration in seconds of registry evaluated.") -) +var registryMetrics registry // Registry returns a new registry. // This is the metrics for the registry. -func Registry() *registry { - return ®istry{} -} - -// Total returns the total number of registry is called. -// The counter is used to observe the number of registry that have been executed. -// The counter is incremented each time an registry is executed -// A good practice is to use the following pattern: -// -// metrics.Registry().Total().Inc() -func (a *registry) Total() prometheus.Counter { - return registryTotal -} - -// TotalErr returns the total number of registry is called with error. -// The counter is used to observe the number of registry that failed. -// The counter is incremented each time an registry fails. -// A good practice is to use the following pattern: -// -// metrics.Registry().TotalErr().Inc() -func (a *registry) TotalErr() prometheus.Counter { - return registryErrTotal -} - -// Duration returns the duration of the registry execution. -// A good practice is to use the following pattern: -// -// timerRegistry := metrics.Registry().Duration() +func Registry() registry { + if registryMetrics.RequestTotal == nil { + registryMetrics = initMetrics(registry{}) + } -// defer timerRegistry.ObserveDuration() -func (a *registry) Duration() *prometheus.Timer { - return prometheus.NewTimer(registryDuration) + return registryMetrics } diff --git a/internal/metrics/rules.go b/internal/metrics/rules.go index 214f3a0..fa42383 100644 --- a/internal/metrics/rules.go +++ b/internal/metrics/rules.go @@ -5,47 +5,21 @@ import ( ) type ( - rules struct{} + rules struct { + EvaluatedTotal prometheus.Counter `help:"The total number of rules evaluated."` + EvaluatedErrorTotal prometheus.Counter `help:"The total number of rules evaluated with error."` + EvaluatedDuration Histogram `help:"The duration in seconds of rules evaluated."` + } ) -var ( - // Prometheus metrics - rulesTotal prometheus.Counter = NewCounter("rules_total", "The total number of rules evaluated.") - rulesErrTotal prometheus.Counter = NewCounter("rules_error_total", "The total number of rules evaluated with error.") - rulesDuration prometheus.Histogram = NewHistogram("rules_duration_seconds", "The duration in seconds of rules evaluated.") -) +var ruleMetrics rules // Rules returns a new rules. // This is the metrics for the rules. -func Rules() *rules { - return &rules{} -} - -// Total returns the total number of rule performed. -// The counter is used to observe the number of rules that have been -// executed. The counter is incremented each time a rule is executed -// A good practice is to use the following pattern: -// -// metrics.Rules().Total().Inc() -func (a *rules) Total() prometheus.Counter { - return rulesTotal -} - -// TotalErr returns the total number of rule performed with error. -// The counter is used to observe the number of rules that failed. -// The counter is incremented each time a rule fails. -// A good practice is to use the following pattern: -// -// metrics.Rules().TotalErr().Inc() -func (a *rules) TotalErr() prometheus.Counter { - return rulesErrTotal -} - -// Duration returns the duration of the rule execution. -// A good practice is to use the following pattern: -// -// timerRules := prometheus.NewTimer(metrics.Rules().Duration()) +func Rules() rules { + if ruleMetrics.EvaluatedTotal == nil { + ruleMetrics = initMetrics(rules{}) + } -func (a *rules) Duration() *prometheus.Timer { - return prometheus.NewTimer(rulesDuration) + return ruleMetrics } diff --git a/internal/metrics/tags.go b/internal/metrics/tags.go index 68b815e..ab30c0f 100644 --- a/internal/metrics/tags.go +++ b/internal/metrics/tags.go @@ -5,48 +5,22 @@ import ( ) type ( - tags struct{} + tags struct { + AvailableSum *prometheus.SummaryVec `labels:"image_name" help:"The total number of tags available for an image."` + RequestTotal prometheus.Counter `help:"The total number of requests to list tags."` + RequestErrorTotal prometheus.Counter `help:"The total number returned an error when calling list tags."` + RequestDuration Histogram `help:"The duration in seconds of the request to list tags."` + } ) -var ( - // Prometheus metrics - tagsTotal prometheus.Counter = NewCounter("tags_total", "The total number of func tags is called to list tags.") - tagsErrTotal prometheus.Counter = NewCounter("tags_error_total", "The total number return by the func tags with error.") - tagsDuration prometheus.Histogram = NewHistogram("tags_duration_seconds", "The duration in seconds for func tags to list the tags.") -) +var tagMetrics tags // Tags returns a new tags. // This is the metrics for the tags. -func Tags() *tags { - return &tags{} -} - -// Total returns the total number of func tags is called. -// The counter is used to observe the number of func tags is executed. -// The counter is incremented each time a tag is executed -// A good practice is to use the following pattern: -// -// metrics.Tags().Total().Inc() -func (a *tags) Total() prometheus.Counter { - return tagsTotal -} - -// TotalErr returns the total number of func tags called with error. -// The counter is used to observe the number of func tags that failed. -// The counter is incremented each time a tag fails. -// A good practice is to use the following pattern: -// -// metrics.Tags().TotalErr().Inc() -func (a *tags) TotalErr() prometheus.Counter { - return tagsErrTotal -} +func Tags() tags { + if tagMetrics.AvailableSum == nil { + tagMetrics = initMetrics(tags{}) + } -// Duration returns the duration of the func tags execution. -// A good practice is to use the following pattern: -// -// timerTags := metrics.Tags().Duration() -// -// defer timerTags.ObserveDuration() -func (a *tags) Duration() *prometheus.Timer { - return prometheus.NewTimer(tagsDuration) + return tagMetrics } diff --git a/main.go b/main.go new file mode 100644 index 0000000..90ea63f --- /dev/null +++ b/main.go @@ -0,0 +1,3 @@ +package main + +//go:generate go run github.com/orange-cloudavenue/kube-image-updater/tools/doc diff --git a/test/metrics/metrics_test.go b/test/metrics/metrics_test.go index b714364..c82305d 100644 --- a/test/metrics/metrics_test.go +++ b/test/metrics/metrics_test.go @@ -12,6 +12,8 @@ import ( ) func TestMetric_Counter(t *testing.T) { + metrics.InitAll() + // Test the metrics for the actions Counter list := metrics.Metrics @@ -30,6 +32,8 @@ func TestMetric_Counter(t *testing.T) { for _, m := range list[metrics.MetricTypeCounter] { // Check if the metric is a metricCounter if m, ok := m.(metrics.MetricCounter); ok { + m.Counter.Inc() + // fill struct test with data testUnit = testsCounter{ { @@ -71,10 +75,8 @@ func TestMetric_Counter(t *testing.T) { // Test the metrics for the actions Counter for _, tt := range testUnit { t.Run(tt.name, func(t *testing.T) { - counter := tt.c - counter.Inc() // Compare the metrics - if err := testutil.CollectAndCompare(counter, strings.NewReader(tt.data+tt.nameMetric+tt.value), tt.nameMetric); err != nil { + if err := testutil.CollectAndCompare(tt.c, strings.NewReader(tt.data+tt.nameMetric+tt.value), tt.nameMetric); err != nil { if !tt.error { t.Errorf("unexpected error: %v", err) } @@ -85,6 +87,8 @@ func TestMetric_Counter(t *testing.T) { } func TestMetric_Histogram(t *testing.T) { + metrics.InitAll() + // Test the metrics for the actions Histogram list := metrics.Metrics @@ -208,3 +212,147 @@ func TestMetric_Histogram(t *testing.T) { } } // end of loop over the list of metrics } + +func TestMetric_Summary(t *testing.T) { + metrics.InitAll() + // Test the metrics for the actions Summary + list := metrics.Metrics + + type testsSummary []struct { + name string + nameMetric string + data string + value string + summary prometheus.Summary + error bool + } + testUnit := make(testsSummary, 0) + + // loop over the list of metrics + for _, m := range list[metrics.MetricTypeSummary] { + // Check if the metric is a metricSummary + if m, ok := m.(metrics.MetricSummary); ok { + // fill struct test with data + testUnit = testsSummary{ + { + name: "Check Summary " + m.Name, + nameMetric: m.Name, + data: fmt.Sprintf(` +# HELP %s %s +# TYPE %s %s +`, m.Name, m.Help, m.Name, metrics.MetricTypeSummary), + value: fmt.Sprintf(` +%s{quantile="0.5"} 0.1 +%s{quantile="0.9"} 0.1 +%s_sum 0.1 +%s_count 1 +`, m.Name, m.Name, m.Name, m.Name), + summary: m.Summary, + error: false, + }, + { + name: "Check Summary with a wrong quantile " + m.Name, + nameMetric: m.Name, + data: fmt.Sprintf(` +# HELP %s %s +# TYPE %s %s +`, m.Name, m.Help, m.Name, metrics.MetricTypeSummary), + value: fmt.Sprintf(` +%s{quantile="0.5"} 0.1 +%s{quantile="0.9"} 0.1 +%s_sum 0.1 +%s_count 1 +`, m.Name, m.Name, m.Name, m.Name), + summary: m.Summary, + error: true, // Error because the quantile is wrong + }, + } // end of testsSummary struct + } // end of if m, ok := m.(metrics.MetricSummary) + + // Test the metrics for the actions Summary + for _, tt := range testUnit { + t.Run(tt.name, func(t *testing.T) { + // Get the Duration histogram + tt.summary.Observe(0.1) + + // Verify the histogram value + if err := testutil.CollectAndCompare(tt.summary, strings.NewReader(tt.data+tt.value)); err != nil { + if !tt.error { + t.Errorf("unexpected error: %v", err) + } + } + }) + } + } // end of loop over the list of metrics +} + +func TestMetric_Gauge(t *testing.T) { + metrics.InitAll() + // Test the metrics for the actions Gauge + list := metrics.Metrics + + type testsGauge []struct { + name string + nameMetric string + data string + value string + g prometheus.Gauge + error bool + } + testUnit := make(testsGauge, 0) + + // loop over the list of metrics + for _, m := range list[metrics.MetricTypeGauge] { + // Check if the metric is a metricGauge + if m, ok := m.(metrics.MetricGauge); ok { + // fill struct test with data + testUnit = testsGauge{ + { + name: "Check Gauge " + m.Name, + nameMetric: m.Name, + data: fmt.Sprintf(` +# HELP %s %s +# TYPE %s %s +`, m.Name, m.Help, m.Name, metrics.MetricTypeGauge), + value: " 0\n", + g: m.Gauge, + error: false, + }, + { + name: "Check Gauge mistake between name and TYPE description " + m.Name, + nameMetric: m.Name, + data: fmt.Sprintf(` +# HELP %s %s +# TYPE %s_mistake_error_in_TYPE %s +`, m.Name, m.Help, m.Name, metrics.MetricTypeGauge), + value: " 0\n", + g: m.Gauge, + error: true, // Error because the gauge name is not the same in the HELP description + }, + { + name: "Check Gauge mistake between name and HELP description " + m.Name, + nameMetric: m.Name, + data: fmt.Sprintf(` +# HELP %s_mistake_error_in_HELP %s +# TYPE %s %s +`, m.Name, m.Help, m.Name, metrics.MetricTypeGauge), + value: " 0\n", + g: m.Gauge, + error: true, // Error because the gauge name is not the same in the description + }, + } // end of testsGauge struct + } // end of if m, ok := m.(metrics.MetricGauge) + + // Test the metrics for the actions Gauge + for _, tt := range testUnit { + t.Run(tt.name, func(t *testing.T) { + // Compare the metrics + if err := testutil.CollectAndCompare(tt.g, strings.NewReader(tt.data+tt.nameMetric+tt.value), tt.nameMetric); err != nil { + if !tt.error { + t.Errorf("unexpected error: %v", err) + } + } + }) + } // end of loop over the list of metrics + } +} diff --git a/tools/doc/generate-doc.go b/tools/doc/generate-doc.go new file mode 100644 index 0000000..fe25af2 --- /dev/null +++ b/tools/doc/generate-doc.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "html/template" + "log" + "os" + "sort" + + "github.com/fbiville/markdown-table-formatter/pkg/markdown" + + "github.com/orange-cloudavenue/kube-image-updater/internal/metrics" + "github.com/orange-cloudavenue/kube-image-updater/internal/models" +) + +func main() { + tmplFuncs := template.FuncMap{ + "tableSettings": func() string { + sSlice := [][]string{} + + sSlice = append(sSlice, []string{fmt.Sprintf("--%s", models.MetricsFlagName), "false", "Enable metrics collection"}) + sSlice = append(sSlice, []string{fmt.Sprintf("--%s", models.MetricsPortFlagName), models.MetricsDefaultAddr, "Port to expose metrics on"}) + sSlice = append(sSlice, []string{fmt.Sprintf("--%s", models.MetricsPathFlagName), models.MetricsDefaultPath, "Path to expose metrics on"}) + + prettyPrintedTable, err := markdown.NewTableFormatterBuilder(). + WithPrettyPrint(). + Build("Flag", "Default", "Description"). + Format(sSlice) + if err != nil { + panic(err) + } + + return prettyPrintedTable + }, + "tableMetrics": func() string { + metrics.InitAll() + + mMap := map[string]string{} + + for _, mm := range metrics.Metrics { + for name, m := range mm { + mMap[name] = m.(metrics.Metric).GetHelp() + } + } + + // Extract keys from map + keys := make([]string, 0, len(mMap)) + for k := range mMap { + keys = append(keys, k) + } + + // sort the map + sort.Strings(keys) + + mSlice := [][]string{} + for _, k := range keys { + mSlice = append(mSlice, []string{k, mMap[k]}) + } + + prettyPrintedTable, err := markdown.NewTableFormatterBuilder(). + WithPrettyPrint(). + Build("Metrics", "Description"). + Format(mSlice) + if err != nil { + panic(err) + } + + return prettyPrintedTable + }, + } + + // os read file + file, err := os.ReadFile("docs/advanced/metrics.md.tmpl") + if err != nil { + log.Default().Printf("Failed to open file: %v", err) + os.Exit(1) + } + + tmpl := template.Must(template.New("metrics").Funcs(tmplFuncs).Parse(string(file))) + + // write template to file + f, err := os.Create("docs/advanced/metrics.md") + defer f.Close() + if err != nil { + log.Default().Printf("Failed to create file: %v", err) + f.Close() + os.Exit(1) //nolint: gocritic + } + if err := tmpl.Execute(f, nil); err != nil { + log.Default().Printf("Failed to execute template: %v", err) + os.Exit(1) + } + + os.Exit(0) +}