├── docs └── img │ ├── logo.png │ └── sloth_small_dashboard.png ├── scripts ├── deps.sh ├── check │ ├── check.sh │ ├── integration-test.sh │ ├── integration-test-cli.sh │ ├── integration-test-k8s.sh │ ├── unit-test.sh │ └── helm-test.sh ├── gogen.sh ├── build │ ├── bin │ │ ├── build-all.sh │ │ ├── build-raw.sh │ │ └── build.sh │ └── docker │ │ ├── publish-image.sh │ │ ├── build-image-dev.sh │ │ ├── build-image.sh │ │ └── build-publish-image-all.sh ├── kubegen.sh ├── examplesgen.sh └── deploygen.sh ├── pkg ├── kubernetes │ ├── api │ │ └── sloth │ │ │ ├── register.go │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ └── register.go │ └── gen │ │ ├── clientset │ │ └── versioned │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── typed │ │ │ └── sloth │ │ │ │ └── v1 │ │ │ │ ├── doc.go │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_sloth_client.go │ │ │ │ └── fake_prometheusservicelevel.go │ │ │ │ └── generated_expansion.go │ │ │ └── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ ├── listers │ │ └── sloth │ │ │ └── v1 │ │ │ └── expansion_generated.go │ │ ├── informers │ │ └── externalversions │ │ │ ├── internalinterfaces │ │ │ └── factory_interfaces.go │ │ │ ├── sloth │ │ │ ├── interface.go │ │ │ └── v1 │ │ │ │ └── interface.go │ │ │ └── generic.go │ │ └── applyconfiguration │ │ ├── internal │ │ └── internal.go │ │ └── sloth │ │ └── v1 │ │ ├── sliraw.go │ │ ├── sliplugin.go │ │ └── slievents.go ├── lib │ ├── log │ │ └── log.go │ ├── pkg_example_test.go │ └── bench_test.go ├── common │ ├── model │ │ ├── k8s.go │ │ ├── gen_result.go │ │ └── info.go │ ├── conventions │ │ ├── sli.go │ │ ├── conventions.go │ │ └── slo.go │ ├── errors │ │ └── errors.go │ └── utils │ │ ├── prometheus │ │ └── prometheus.go │ │ ├── data │ │ └── data.go │ │ └── k8s │ │ ├── k8s.go │ │ └── k8s_test.go └── prometheus │ ├── plugin │ ├── k8stransform │ │ └── v1 │ │ │ └── v1.go │ ├── v1 │ │ └── v1.go │ └── slo │ │ └── v1 │ │ ├── testing │ │ └── testing.go │ │ └── v1.go │ └── alertwindows │ └── v1 │ └── v1.go ├── internal ├── http │ ├── ui │ │ ├── static │ │ │ └── img │ │ │ │ └── favicon │ │ │ │ ├── favicon-32x32.png │ │ │ │ └── apple-touch-icon.png │ │ ├── templates │ │ │ ├── slo │ │ │ │ └── list-all.tmpl │ │ │ ├── app │ │ │ │ ├── slo │ │ │ │ │ ├── comp_slo_data.tmpl │ │ │ │ │ ├── page.tmpl │ │ │ │ │ ├── comp_budget_chart.tmpl │ │ │ │ │ ├── comp_sli_chart.tmpl │ │ │ │ │ └── comp_stats.tmpl │ │ │ │ └── services │ │ │ │ │ └── page.tmpl │ │ │ └── shared │ │ │ │ ├── nav.tmpl │ │ │ │ ├── footer.tmpl │ │ │ │ └── head.tmpl │ │ ├── handler_index.go │ │ ├── metrics.go │ │ ├── handler_service_details.go │ │ ├── midleware.go │ │ ├── routes.go │ │ ├── handler_index_test.go │ │ ├── handler_service_details_test.go │ │ ├── common_uplot.go │ │ └── common_test.go │ └── backend │ │ ├── metrics │ │ └── metrics.go │ │ ├── app │ │ └── app.go │ │ ├── storage │ │ ├── storage.go │ │ └── prometheus │ │ │ ├── cli.go │ │ │ └── slo_hydrate.go │ │ └── model │ │ └── model_test.go ├── storage │ ├── io │ │ ├── helper.go │ │ └── k8s_obj.go │ └── k8s │ │ └── dry_run.go ├── plugin │ ├── plugin.go │ ├── slo │ │ ├── core │ │ │ ├── noop_v1 │ │ │ │ ├── README.md │ │ │ │ ├── plugin.go │ │ │ │ └── plugin_test.go │ │ │ ├── validate_v1 │ │ │ │ ├── plugin.go │ │ │ │ └── README.md │ │ │ ├── alert_rules_v1 │ │ │ │ └── README.md │ │ │ ├── metadata_rules_v1 │ │ │ │ └── README.md │ │ │ ├── debug_v1 │ │ │ │ ├── README.md │ │ │ │ ├── plugin.go │ │ │ │ └── plugin_test.go │ │ │ └── sli_rules_v1 │ │ │ │ └── README.md │ │ └── contrib │ │ │ ├── info_labels_v1 │ │ │ ├── README.md │ │ │ └── plugin.go │ │ │ ├── denominator_corrected_rules_v1 │ │ │ └── README.md │ │ │ └── rule_intervals_v1 │ │ │ ├── README.md │ │ │ └── plugin.go │ └── k8stransform │ │ └── prom_operator_prometheus_rule_v1 │ │ └── plugin.go ├── pluginengine │ ├── slo │ │ └── custom │ │ │ ├── github_com-slok-sloth-pkg-common-utils-data.go │ │ │ ├── github_com-slok-sloth-pkg-common-utils-prometheus.go │ │ │ ├── github_com-prometheus-prometheus-model-rulefmt.go │ │ │ ├── custom.go │ │ │ ├── github_com-caarlos0-env-v11.go │ │ │ ├── github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go │ │ │ └── github_com-slok-sloth-pkg-common-validation.go │ └── k8stransform │ │ └── custom │ │ ├── github_com-slok-sloth-pkg-common-utils-data.go │ │ ├── github_com-slok-sloth-pkg-common-utils-k8s.go │ │ ├── github_com-slok-sloth-pkg-common-utils-prometheus.go │ │ ├── custom.go │ │ ├── github_com-caarlos0-env-v11.go │ │ └── github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go ├── info │ └── info.go ├── alert │ └── windows │ │ ├── google-28d.yaml │ │ └── google-30d.yaml ├── log │ └── logrus │ │ └── logrus.go └── app │ └── kubecontroller │ └── retriever.go ├── deploy └── kubernetes │ ├── kustomization.yaml │ └── helm │ └── sloth │ ├── Chart.yaml │ ├── templates │ ├── service-account.yaml │ ├── configmap.yaml │ ├── cluster-role.yaml │ ├── cluster-role-binding.yaml │ ├── pod-monitor.yaml │ └── _helpers.tpl │ ├── tests │ ├── testdata │ │ └── output │ │ │ ├── sa_default.yaml │ │ │ ├── sa_custom.yaml │ │ │ ├── configmap_slo_config.yaml │ │ │ ├── cluster_role_binding_default.yaml │ │ │ ├── pod_monitor_default.yaml │ │ │ ├── cluster_role_default.yaml │ │ │ ├── cluster_role_binding_custom.yaml │ │ │ ├── cluster_role_custom.yaml │ │ │ ├── pod_monitor_custom.yaml │ │ │ └── deployment_custom_no_extras.yaml │ └── values_test.go │ └── .helmignore ├── .codecov.yml ├── .gitignore ├── test └── integration │ ├── prometheus │ ├── testdata │ │ ├── in-sli-plugin.yaml │ │ ├── in-openslo.yaml │ │ ├── validate │ │ │ ├── good │ │ │ │ ├── good-openslo.yaml │ │ │ │ ├── good-aa.yaml │ │ │ │ ├── good-ab.yaml │ │ │ │ ├── good-ba.yaml │ │ │ │ └── good-k8s.yaml │ │ │ └── bad │ │ │ │ ├── bad-openslo.yaml │ │ │ │ ├── bad-aa.yaml │ │ │ │ ├── bad-ab.yaml │ │ │ │ ├── bad-ba.yaml │ │ │ │ └── bad-k8s.yaml │ │ ├── in-slo-plugin.yaml │ │ ├── in-base.yaml │ │ ├── in-invalid-version.yaml │ │ ├── in-slo-plugin-k8s.yaml │ │ └── in-base-k8s.yaml │ ├── windows │ │ └── 7d.yaml │ ├── plugins │ │ ├── slo │ │ │ └── plugin1 │ │ │ │ └── plugin.go │ │ └── sli │ │ │ └── plugin1 │ │ │ └── plugin.go │ ├── helpers.go │ └── validate_test.go │ ├── k8scontroller │ ├── windows │ │ └── 7d.yaml │ └── plugins │ │ ├── slo │ │ └── plugin1 │ │ │ └── plugin.go │ │ └── sli │ │ └── plugin1 │ │ └── plugin.go │ └── testutils │ └── cmd.go ├── examples ├── windows │ ├── 7d.yaml │ └── custom-30d.yaml ├── no-alerts.yml ├── openslo-getting-started.yml ├── plugin-getting-started.yml ├── contrib-denominator-corrected.yaml ├── getting-started.yml ├── k8s-getting-started.yml ├── plugin-k8s-getting-started.yml ├── victoria-metrics.yml ├── raw-home-wifi.yml ├── k8s-home-wifi.yml └── slo-plugin-getting-started.yml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── helmrelease.yaml │ ├── generate.yaml │ └── close-stale.yaml ├── cmd └── sloth │ └── commands │ ├── version.go │ └── commands.go ├── .mockery.yml ├── .golangci.yml └── docker └── prod └── Dockerfile /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/HEAD/docs/img/logo.png -------------------------------------------------------------------------------- /scripts/deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go mod tidy -------------------------------------------------------------------------------- /pkg/kubernetes/api/sloth/register.go: -------------------------------------------------------------------------------- 1 | package sloth 2 | 3 | const ( 4 | GroupName = "sloth.slok.dev" 5 | ) 6 | -------------------------------------------------------------------------------- /scripts/check/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | golangci-lint run -------------------------------------------------------------------------------- /docs/img/sloth_small_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/HEAD/docs/img/sloth_small_dashboard.png -------------------------------------------------------------------------------- /scripts/gogen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go generate ./... 7 | mockery 8 | -------------------------------------------------------------------------------- /pkg/kubernetes/api/sloth/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +groupName=sloth.slok.dev 3 | // +versionName=v1 4 | 5 | package v1 6 | -------------------------------------------------------------------------------- /internal/http/ui/static/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/HEAD/internal/http/ui/static/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /internal/http/ui/static/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/HEAD/internal/http/ui/static/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /scripts/check/integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go test -race -tags='integration' -v ./test/integration/... -------------------------------------------------------------------------------- /deploy/kubernetes/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - raw/sloth-with-common-plugins.yaml -------------------------------------------------------------------------------- /pkg/lib/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/slok/sloth/internal/log" 4 | 5 | type Logger = log.Logger 6 | type Kv = log.Kv 7 | 8 | var Noop = log.Noop 9 | -------------------------------------------------------------------------------- /scripts/check/integration-test-cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go test -race -tags='integration' -v ./test/integration/prometheus/... 7 | -------------------------------------------------------------------------------- /scripts/check/integration-test-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go test -race -tags='integration' -v ./test/integration/k8scontroller/... -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated fake clientset. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated typed clients. 4 | package v1 5 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // Package fake has the automatically generated clients. 4 | package fake 5 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package contains the scheme of the automatically generated clientset. 4 | package scheme 5 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | type PrometheusServiceLevelExpansion interface{} 6 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: sloth 3 | description: Base chart for Sloth. 4 | type: application 5 | home: https://github.com/slok/sloth 6 | kubeVersion: ">= 1.19.0-0" 7 | version: 0.15.0 8 | -------------------------------------------------------------------------------- /internal/http/ui/templates/slo/list-all.tmpl: -------------------------------------------------------------------------------- 1 | {{define "slo_list_all"}} 2 | 3 | 4 | 5 | {{template "shared_head" .}} 6 | 7 | {{template "shared_footer" .}} 8 | 9 | 10 | {{end}} -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..90 # First number represents red, and second represents green. 3 | status: 4 | patch: false 5 | project: 6 | default: 7 | # Allow going down 1% before being a failure. 8 | threshold: 1% 9 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "sloth.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "sloth.labels" . | nindent 4 }} 9 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/slo/comp_slo_data.tmpl: -------------------------------------------------------------------------------- 1 | {{define "app_slo_comp_slo_data"}} 2 |
3 | {{template "app_slo_comp_stats" .}} 4 | {{template "app_slo_comp_sli_chart" .}} 5 | {{template "app_slo_comp_budget_chart" .}} 6 |
7 | {{end}} -------------------------------------------------------------------------------- /scripts/check/unit-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go test -race -coverprofile=.test_coverage.txt $(go list ./... | grep -v /test/integration ) 7 | go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $3}' -------------------------------------------------------------------------------- /internal/http/ui/handler_index.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func (u ui) handlerIndex() http.HandlerFunc { 8 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 9 | urls.RedirectToURL(w, r, urls.AppURL("/services")) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/common/model/k8s.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // K8sMeta is the Kubernetes simplified metadata used on different parts of Sloth logic like K8s storage. 4 | type K8sMeta struct { 5 | Name string 6 | Namespace string 7 | Annotations map[string]string 8 | Labels map[string]string 9 | } 10 | -------------------------------------------------------------------------------- /scripts/check/helm-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | cd ./deploy/kubernetes/helm/sloth/tests 7 | go test -race -coverprofile=.test_coverage.txt $(go list ./... | grep -v /test/integration ) 8 | go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $3}' -------------------------------------------------------------------------------- /scripts/build/bin/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | # Build all. 7 | ostypes=("Linux" "Darwin" "Windows" "ARM") 8 | for ostype in "${ostypes[@]}" 9 | do 10 | ostype="${ostype}" ./scripts/build/bin/build.sh 11 | done 12 | 13 | # Create checksums. 14 | checksums_dir="./bin" 15 | cd ${checksums_dir} && sha256sum * > ./checksums.txt 16 | -------------------------------------------------------------------------------- /pkg/common/conventions/sli.go: -------------------------------------------------------------------------------- 1 | package conventions 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | promutils "github.com/slok/sloth/pkg/common/utils/prometheus" 8 | ) 9 | 10 | // GetSLIErrorMetric returns the SLI error Prometheus metric name. 11 | func GetSLIErrorMetric(window time.Duration) string { 12 | return fmt.Sprintf(PromSLIErrorMetricFmt, promutils.TimeDurationToPromStr(window)) 13 | } 14 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.customSloConfig.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "sloth.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "sloth.labels" . | nindent 4 }} 9 | data: 10 | window.yaml: | 11 | {{- toYaml .Values.customSloConfig.data | nindent 4 }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /internal/http/ui/metrics.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | gohttpmetrics "github.com/slok/go-http-metrics/metrics" 5 | ) 6 | 7 | // MetricsRecorder is the service used to record metrics in the HTTP API handler. 8 | type MetricsRecorder interface { 9 | gohttpmetrics.Recorder 10 | } 11 | 12 | var noopMetricsRecorder = struct { 13 | gohttpmetrics.Recorder 14 | }{ 15 | Recorder: gohttpmetrics.Dummy, 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Vendor directory 15 | vendor/ 16 | 17 | # Test coverage. 18 | .test_coverage.txt 19 | 20 | # Binaries 21 | /bin 22 | 23 | # mise/asdf 24 | .tool-versions 25 | -------------------------------------------------------------------------------- /internal/storage/io/helper.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/slok/sloth/internal/info" 7 | ) 8 | 9 | var yamlTopdisclaimer = fmt.Sprintf(` 10 | --- 11 | # Code generated by Sloth (%s): https://github.com/slok/sloth. 12 | # DO NOT EDIT. 13 | 14 | `, info.Version) 15 | 16 | func writeYAMLTopDisclaimer(bs []byte) []byte { 17 | return append([]byte(yamlTopdisclaimer), bs...) 18 | } 19 | -------------------------------------------------------------------------------- /scripts/build/docker/publish-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | 6 | [ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1; 7 | [ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1; 8 | 9 | DEF_ARCH=amd64 10 | ARCH=${ARCH:-$DEF_ARCH} 11 | 12 | IMAGE_TAG_ARCH="${IMAGE}:${VERSION}-${ARCH}" 13 | 14 | echo "Pushing image ${IMAGE_TAG_ARCH}..." 15 | docker push ${IMAGE_TAG_ARCH} 16 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/sa_default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/service-account.yaml 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: sloth 7 | namespace: default 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: sloth 14 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/sa_custom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/service-account.yaml 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: sloth-test 7 | namespace: custom 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: test 14 | label-from: test 15 | -------------------------------------------------------------------------------- /internal/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "embed" 4 | 5 | var ( 6 | //go:embed slo 7 | // Default SLO plugins. These are the default set of SLO plugins that are embedded in the binary. 8 | EmbeddedDefaultSLOPlugins embed.FS 9 | 10 | //go:embed k8stransform 11 | // Default K8s transform plugins. These are the default set of K8s transform plugins that are embedded in the binary. 12 | EmbeddedDefaultK8sTransformPlugins embed.FS 13 | ) 14 | -------------------------------------------------------------------------------- /scripts/build/docker/build-image-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | 6 | [ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1; 7 | [ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1; 8 | [ -z "$DOCKER_FILE_PATH" ] && echo "DOCKER_FILE_PATH env var is required." && exit 1; 9 | 10 | # Build image. 11 | echo "Building dev image ${IMAGE}:${VERSION}..." 12 | docker build \ 13 | -t "${IMAGE}:${VERSION}" \ 14 | -f "${DOCKER_FILE_PATH}" . -------------------------------------------------------------------------------- /pkg/common/model/gen_result.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // PromSLOGroupResult is the result of generating standard Prometheus SLO rules from SLO definitions as SLO group. 4 | type PromSLOGroupResult struct { 5 | OriginalSource PromSLOGroupSource 6 | SLOResults []PromSLOResult 7 | } 8 | 9 | // PromSLOResult is the result of generating standard Prometheus SLO rules from SLO definitions. 10 | type PromSLOResult struct { 11 | SLO PromSLO 12 | PrometheusRules PromSLORules 13 | } 14 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "sloth.fullname" . }} 6 | labels: 7 | {{- include "sloth.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: ["sloth.slok.dev"] 10 | resources: ["*"] 11 | verbs: ["*"] 12 | 13 | - apiGroups: ["monitoring.coreos.com"] 14 | resources: ["prometheusrules"] 15 | verbs: ["create", "list", "get", "update", "watch"] 16 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/configmap_slo_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/configmap.yaml 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: sloth-test 7 | namespace: custom 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: test 14 | label-from: test 15 | data: 16 | window.yaml: | 17 | customKey: customValue 18 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/listers/sloth/v1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // PrometheusServiceLevelListerExpansion allows custom methods to be added to 6 | // PrometheusServiceLevelLister. 7 | type PrometheusServiceLevelListerExpansion interface{} 8 | 9 | // PrometheusServiceLevelNamespaceListerExpansion allows custom methods to be added to 10 | // PrometheusServiceLevelNamespaceLister. 11 | type PrometheusServiceLevelNamespaceListerExpansion interface{} 12 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | 25 | # Custom. 26 | tests/ 27 | -------------------------------------------------------------------------------- /internal/http/ui/templates/shared/nav.tmpl: -------------------------------------------------------------------------------- 1 | {{define "shared_nav"}} 2 |
3 | 15 |
16 | {{end}} -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "sloth.fullname" . }} 6 | labels: 7 | {{- include "sloth.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: {{ include "sloth.fullname" . }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "sloth.fullname" . }} 15 | namespace: {{ .Release.Namespace }} 16 | -------------------------------------------------------------------------------- /internal/http/ui/handler_service_details.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | ) 8 | 9 | func (u ui) handlerServiceDetails() http.HandlerFunc { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | svcID := chi.URLParam(r, URLParamServiceID) 12 | 13 | currentURL := urls.AppURL("/slos") 14 | currentURL = urls.AddQueryParm(currentURL, queryParamSLOServiceID, svcID) 15 | 16 | http.Redirect(w, r, currentURL, http.StatusSeeOther) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-sli-plugin.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | owner: myteam 5 | tier: "2" 6 | slos: 7 | - name: "slo1" 8 | objective: 99.9 9 | description: "This is SLO 01." 10 | sli: 11 | plugin: 12 | id: integration_test 13 | options: 14 | job: svc01 15 | filter: guybrush="threepwood",melee="island" 16 | alerting: 17 | page_alert: 18 | disable: true 19 | ticket_alert: 20 | disable: true 21 | -------------------------------------------------------------------------------- /examples/windows/7d.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: AlertWindows 3 | spec: 4 | sloPeriod: 7d 5 | page: 6 | quick: 7 | errorBudgetPercent: 8 8 | shortWindow: 5m 9 | longWindow: 1h 10 | slow: 11 | errorBudgetPercent: 12.5 12 | shortWindow: 30m 13 | longWindow: 6h 14 | ticket: 15 | quick: 16 | errorBudgetPercent: 20 17 | shortWindow: 2h 18 | longWindow: 1d 19 | slow: 20 | errorBudgetPercent: 42 21 | shortWindow: 6h 22 | longWindow: 3d 23 | -------------------------------------------------------------------------------- /examples/windows/custom-30d.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: AlertWindows 3 | spec: 4 | sloPeriod: 30d 5 | page: 6 | quick: 7 | errorBudgetPercent: 1 8 | shortWindow: 2m 9 | longWindow: 30m 10 | slow: 11 | errorBudgetPercent: 2 12 | shortWindow: 15m 13 | longWindow: 3h 14 | ticket: 15 | quick: 16 | errorBudgetPercent: 5 17 | shortWindow: 1h 18 | longWindow: 12h 19 | slow: 20 | errorBudgetPercent: 5 21 | shortWindow: 3h 22 | longWindow: 36h 23 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/noop_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/noop/v1 2 | 3 | This plugin performs no operation and is intended purely as an example or placeholder. It can be used to test the plugin chain mechanism or serve as a minimal reference implementation for building new SLO plugins. 4 | 5 | ## Config 6 | 7 | None 8 | 9 | ## Env vars 10 | 11 | None 12 | 13 | ## Order requirement 14 | 15 | None 16 | 17 | ## Usage examples 18 | 19 | ### No-op plugin in chain 20 | 21 | ```yaml 22 | chain: 23 | - id: "sloth.dev/core/noop/v1" 24 | ``` 25 | -------------------------------------------------------------------------------- /pkg/common/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | var ( 6 | // ErrNoSLORules will be used when there are no rules to store. The upper layer 7 | // could ignore or handle the error in cases where there wasn't an output. 8 | ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") 9 | 10 | // ErrNotFound will be used when a resource has not been found. 11 | ErrNotFound = fmt.Errorf("resource not found") 12 | 13 | // ErrRequired will be used when a required field is not set. 14 | ErrRequired = fmt.Errorf("required") 15 | ) 16 | -------------------------------------------------------------------------------- /test/integration/prometheus/windows/7d.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: AlertWindows 3 | spec: 4 | sloPeriod: 7d 5 | page: 6 | quick: 7 | errorBudgetPercent: 8 8 | shortWindow: 5m 9 | longWindow: 1h 10 | slow: 11 | errorBudgetPercent: 12.5 12 | shortWindow: 30m 13 | longWindow: 6h 14 | ticket: 15 | quick: 16 | errorBudgetPercent: 20 17 | shortWindow: 2h 18 | longWindow: 24h 19 | slow: 20 | errorBudgetPercent: 42 21 | shortWindow: 6h 22 | longWindow: 72h 23 | -------------------------------------------------------------------------------- /test/integration/k8scontroller/windows/7d.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: AlertWindows 3 | spec: 4 | sloPeriod: 7d 5 | page: 6 | quick: 7 | errorBudgetPercent: 8 8 | shortWindow: 5m 9 | longWindow: 1h 10 | slow: 11 | errorBudgetPercent: 12.5 12 | shortWindow: 30m 13 | longWindow: 6h 14 | ticket: 15 | quick: 16 | errorBudgetPercent: 20 17 | shortWindow: 2h 18 | longWindow: 24h 19 | slow: 20 | errorBudgetPercent: 42 21 | shortWindow: 6h 22 | longWindow: 72h 23 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/data'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/utils/data" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/utils/data/data"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "MergeLabels": reflect.ValueOf(data.MergeLabels), 14 | "SplitYAML": reflect.ValueOf(data.SplitYAML), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-data.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/data'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/utils/data" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/utils/data/data"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "MergeLabels": reflect.ValueOf(data.MergeLabels), 14 | "SplitYAML": reflect.ValueOf(data.SplitYAML), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners. 2 | * @slok 3 | 4 | # Contrib SLO plugins default owners. 5 | /internal/plugin/slo/contrib/ @slok 6 | 7 | # Specific contrib SLO owners. 8 | /internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/ @cxdy @slok @wbollock 9 | /internal/plugin/slo/contrib/denominator_corrected_rules/v1/ @slok 10 | /internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock 11 | /internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock 12 | /internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok 13 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/noop_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 8 | ) 9 | 10 | const ( 11 | PluginVersion = "prometheus/slo/v1" 12 | PluginID = "sloth.dev/core/noop/v1" 13 | ) 14 | 15 | func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 16 | return plugin{}, nil 17 | } 18 | 19 | type plugin struct{} 20 | 21 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /scripts/kubegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.9.0 7 | GEN_DIRECTORY="pkg/kubernetes/gen" 8 | 9 | echo "Cleaning gen directory" 10 | rm -rf ./${GEN_DIRECTORY} 11 | 12 | docker run --rm -it -v ${PWD}:/app "${IMAGE_GEN}" \ 13 | --apis-in ./pkg/kubernetes/api \ 14 | --go-gen-out ./${GEN_DIRECTORY} \ 15 | --crd-gen-out ./${GEN_DIRECTORY}/crd \ 16 | --apply-configurations 17 | 18 | echo "Copying crd to helm chart..." 19 | rm ./deploy/kubernetes/helm/sloth/crds/* 20 | cp "${GEN_DIRECTORY}/crd"/* deploy/kubernetes/helm/sloth/crds/ 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | ignore: 8 | # Ignore Kubernetes dependencies to have full control on them. 9 | - dependency-name: "k8s.io/*" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | - package-ecosystem: "docker" 15 | directory: "/docker/dev" 16 | schedule: 17 | interval: "daily" 18 | - package-ecosystem: "docker" 19 | directory: "/docker/prod" 20 | schedule: 21 | interval: "daily" 22 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/cluster_role_binding_default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/cluster-role-binding.yaml 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: sloth 7 | labels: 8 | helm.sh/chart: sloth- 9 | app.kubernetes.io/managed-by: Helm 10 | app: sloth 11 | app.kubernetes.io/name: sloth 12 | app.kubernetes.io/instance: sloth 13 | roleRef: 14 | apiGroup: rbac.authorization.k8s.io 15 | kind: ClusterRole 16 | name: sloth 17 | subjects: 18 | - kind: ServiceAccount 19 | name: sloth 20 | namespace: default 21 | -------------------------------------------------------------------------------- /internal/info/info.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import "runtime/debug" 4 | 5 | var ( 6 | // Version is the version app. 7 | Version = "" 8 | ) 9 | 10 | func init() { 11 | if Version != "" { 12 | return 13 | } 14 | 15 | // If not set, get the information from the runtime in case Sloth has been used as a library. 16 | info, ok := debug.ReadBuildInfo() 17 | if ok { 18 | // Search for sloth as a library. 19 | for _, d := range info.Deps { 20 | if d.Path == "github.com/slok/sloth" { 21 | Version = d.Version 22 | return 23 | } 24 | } 25 | } 26 | 27 | // If still not set, then set to dev. 28 | Version = "dev" 29 | } 30 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/pod_monitor_default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/pod-monitor.yaml 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: PodMonitor 5 | metadata: 6 | name: sloth 7 | namespace: default 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: sloth 14 | spec: 15 | selector: 16 | matchLabels: 17 | app: sloth 18 | app.kubernetes.io/name: sloth 19 | app.kubernetes.io/instance: sloth 20 | podMetricsEndpoints: 21 | - port: metrics 22 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/slo/page.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "app_slo"}} 3 | 4 | 5 | {{template "shared_head" .}} 6 | 7 | 8 | {{template "shared_nav" .}} 9 |
10 |

{{.Data.SLOData.ServiceID}} / {{.Data.SLOData.Name}}

11 |
12 | {{- range $key, $value := .Data.SLOData.GroupLabels -}} 13 | {{ $key }}: {{ $value }} 14 | 15 | {{- end -}} 16 |
17 |
18 | {{template "app_slo_comp_slo_data" .}} 19 |
20 | 21 | {{template "shared_footer" .}} 22 | 23 | {{end}} -------------------------------------------------------------------------------- /pkg/common/model/info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Mode string 4 | 5 | const ( 6 | ModeTest = "test" 7 | ModeCLIGenPrometheus = "cli-gen-prom" 8 | ModeAPIGenPrometheus = "api-gen-prom" 9 | ModeCLIGenKubernetes = "cli-gen-k8s" 10 | ModeAPIGenKubernetes = "api-gen-k8s" 11 | ModeControllerGenKubernetes = "ctrl-gen-k8s" 12 | ModeCLIGenOpenSLO = "cli-gen-openslo" 13 | ModeAPIGenOpenSLO = "api-gen-openslo" 14 | ) 15 | 16 | // Info is the information of the app and request based for SLO generators. 17 | type Info struct { 18 | Version string 19 | Mode Mode 20 | Spec string 21 | } 22 | -------------------------------------------------------------------------------- /cmd/sloth/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/alecthomas/kingpin/v2" 8 | 9 | "github.com/slok/sloth/internal/info" 10 | ) 11 | 12 | type versionCommand struct{} 13 | 14 | // NewVersionCommand returns the version command. 15 | func NewVersionCommand(app *kingpin.Application) Command { 16 | c := &versionCommand{} 17 | app.Command("version", "Shows version.") 18 | 19 | return c 20 | } 21 | 22 | func (versionCommand) Name() string { return "version" } 23 | func (versionCommand) Run(ctx context.Context, config RootConfig) error { 24 | fmt.Fprint(config.Stdout, info.Version) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/cluster_role_default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/cluster-role.yaml 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: sloth 7 | labels: 8 | helm.sh/chart: sloth- 9 | app.kubernetes.io/managed-by: Helm 10 | app: sloth 11 | app.kubernetes.io/name: sloth 12 | app.kubernetes.io/instance: sloth 13 | rules: 14 | - apiGroups: ["sloth.slok.dev"] 15 | resources: ["*"] 16 | verbs: ["*"] 17 | 18 | - apiGroups: ["monitoring.coreos.com"] 19 | resources: ["prometheusrules"] 20 | verbs: ["create", "list", "get", "update", "watch"] 21 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/cluster_role_binding_custom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/cluster-role-binding.yaml 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: sloth-test 7 | labels: 8 | helm.sh/chart: sloth- 9 | app.kubernetes.io/managed-by: Helm 10 | app: sloth 11 | app.kubernetes.io/name: sloth 12 | app.kubernetes.io/instance: test 13 | label-from: test 14 | roleRef: 15 | apiGroup: rbac.authorization.k8s.io 16 | kind: ClusterRole 17 | name: sloth-test 18 | subjects: 19 | - kind: ServiceAccount 20 | name: sloth-test 21 | namespace: custom 22 | -------------------------------------------------------------------------------- /internal/http/ui/midleware.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/slok/sloth/internal/log" 7 | ) 8 | 9 | type chiMiddleware = func(next http.Handler) http.Handler 10 | 11 | func (u ui) registerGlobalMiddlewares() { 12 | u.router.Use( 13 | u.logMiddleware(), 14 | ) 15 | } 16 | 17 | func (u ui) logMiddleware() chiMiddleware { 18 | return func(next http.Handler) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | u.logger.WithValues(log.Kv{ 21 | "url": r.URL, 22 | "method": r.Method, 23 | }).Debugf("Request received") 24 | 25 | next.ServeHTTP(w, r) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/http/ui/templates/shared/footer.tmpl: -------------------------------------------------------------------------------- 1 | {{define "shared_footer"}} 2 |
3 |
4 | 5 | 15 |
16 | 17 | 18 | 19 | {{end}} -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-k8s.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/k8s'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/utils/k8s" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/utils/k8s/k8s"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "PromRuleGroupToUnstructuredPromOperator": reflect.ValueOf(k8s.PromRuleGroupToUnstructuredPromOperator), 14 | "UnstructuredToYAMLString": reflect.ValueOf(k8s.UnstructuredToYAMLString), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/build/docker/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | 6 | [ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1; 7 | [ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1; 8 | [ -z "$DOCKER_FILE_PATH" ] && echo "DOCKER_FILE_PATH env var is required." && exit 1; 9 | 10 | # By default use amd64 architecture. 11 | DEF_ARCH=amd64 12 | ARCH=${ARCH:-$DEF_ARCH} 13 | 14 | IMAGE_TAG_ARCH="${IMAGE}:${VERSION}-${ARCH}" 15 | 16 | # Build image. 17 | echo "Building image ${IMAGE_TAG_ARCH}..." 18 | docker build \ 19 | --build-arg VERSION="${VERSION}" \ 20 | --build-arg ARCH="${ARCH}" \ 21 | -t "${IMAGE_TAG_ARCH}" \ 22 | -f "${DOCKER_FILE_PATH}" . -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/cluster_role_custom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/cluster-role.yaml 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: sloth-test 7 | labels: 8 | helm.sh/chart: sloth- 9 | app.kubernetes.io/managed-by: Helm 10 | app: sloth 11 | app.kubernetes.io/name: sloth 12 | app.kubernetes.io/instance: test 13 | label-from: test 14 | rules: 15 | - apiGroups: ["sloth.slok.dev"] 16 | resources: ["*"] 17 | verbs: ["*"] 18 | 19 | - apiGroups: ["monitoring.coreos.com"] 20 | resources: ["prometheusrules"] 21 | verbs: ["create", "list", "get", "update", "watch"] 22 | -------------------------------------------------------------------------------- /.mockery.yml: -------------------------------------------------------------------------------- 1 | dir: '{{.InterfaceDir}}/{{.SrcPackageName}}mock' 2 | filename: mocks.go 3 | force-file-write: true 4 | structname: '{{.InterfaceName}}' 5 | pkgname: '{{.SrcPackageName}}mock' 6 | template: testify 7 | packages: 8 | github.com/slok/sloth/internal/app/generate: {interfaces: {SLOPluginGetter}} 9 | github.com/slok/sloth/internal/storage/fs: {interfaces: {SLIPluginLoader,SLOPluginLoader, K8sTransformPluginLoader}} 10 | github.com/slok/sloth/internal/http/backend/storage: {interfaces: {SLOGetter, ServiceGetter}} 11 | github.com/slok/sloth/internal/http/backend/storage/prometheus: {interfaces: {PrometheusAPIClient}} 12 | github.com/slok/sloth/internal/http/ui: {interfaces: {ServiceApp}} 13 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/pod-monitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.metrics.enabled }} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: PodMonitor 5 | metadata: 6 | name: {{ include "sloth.fullname" . }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "sloth.labels" . | nindent 4 }} 10 | {{- with .Values.metrics.prometheusLabels }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | selector: 15 | matchLabels: 16 | {{- include "sloth.selectorLabels" . | nindent 6 }} 17 | podMetricsEndpoints: 18 | - port: metrics 19 | {{- with .Values.metrics.scrapeInterval }} 20 | interval: {{.}} 21 | {{- end }} 22 | {{- end }} -------------------------------------------------------------------------------- /internal/alert/windows/google-28d.yaml: -------------------------------------------------------------------------------- 1 | # Common and safe 4 weeks windows. 2 | # 3 | # Numbers obtained from https://sre.google/workbook/alerting-on-slos/#recommended_parameters_for_an_slo_based_a. 4 | apiVersion: "sloth.slok.dev/v1" 5 | kind: "AlertWindows" 6 | spec: 7 | sloPeriod: 28d 8 | page: 9 | quick: 10 | errorBudgetPercent: 2 11 | shortWindow: 5m 12 | longWindow: 1h 13 | slow: 14 | errorBudgetPercent: 5 15 | shortWindow: 30m 16 | longWindow: 6h 17 | ticket: 18 | quick: 19 | errorBudgetPercent: 10 20 | shortWindow: 2h 21 | longWindow: 1d 22 | slow: 23 | errorBudgetPercent: 10 24 | shortWindow: 6h 25 | longWindow: 3d -------------------------------------------------------------------------------- /internal/alert/windows/google-30d.yaml: -------------------------------------------------------------------------------- 1 | # Common and safe month windows. 2 | # 3 | # Numbers obtained from https://sre.google/workbook/alerting-on-slos/#recommended_parameters_for_an_slo_based_a. 4 | apiVersion: "sloth.slok.dev/v1" 5 | kind: "AlertWindows" 6 | spec: 7 | sloPeriod: 30d 8 | page: 9 | quick: 10 | errorBudgetPercent: 2 11 | shortWindow: 5m 12 | longWindow: 1h 13 | slow: 14 | errorBudgetPercent: 5 15 | shortWindow: 30m 16 | longWindow: 6h 17 | ticket: 18 | quick: 19 | errorBudgetPercent: 10 20 | shortWindow: 2h 21 | longWindow: 1d 22 | slow: 23 | errorBudgetPercent: 10 24 | shortWindow: 6h 25 | longWindow: 3d 26 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/pod_monitor_custom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/pod-monitor.yaml 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: PodMonitor 5 | metadata: 6 | name: sloth-test 7 | namespace: custom 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: test 14 | label-from: test 15 | kp1: vp1 16 | kp2: vp2 17 | spec: 18 | selector: 19 | matchLabels: 20 | app: sloth 21 | app.kubernetes.io/name: sloth 22 | app.kubernetes.io/instance: test 23 | podMetricsEndpoints: 24 | - port: metrics 25 | interval: 45s 26 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/prometheus'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/utils/prometheus" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/utils/prometheus/prometheus"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "LabelsToPromFilter": reflect.ValueOf(prometheus.LabelsToPromFilter), 14 | "PromStrToTimeDuration": reflect.ValueOf(prometheus.PromStrToTimeDuration), 15 | "TimeDurationToPromStr": reflect.ValueOf(prometheus.TimeDurationToPromStr), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/prometheus'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/utils/prometheus" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/utils/prometheus/prometheus"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "LabelsToPromFilter": reflect.ValueOf(prometheus.LabelsToPromFilter), 14 | "PromStrToTimeDuration": reflect.ValueOf(prometheus.PromStrToTimeDuration), 15 | "TimeDurationToPromStr": reflect.ValueOf(prometheus.TimeDurationToPromStr), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | build-tags: 4 | - integration 5 | linters: 6 | enable: 7 | - godot 8 | - misspell 9 | - revive 10 | settings: 11 | revive: 12 | rules: 13 | # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. 14 | - name: unused-parameter 15 | disabled: true 16 | staticcheck: 17 | checks: 18 | - all 19 | # Omit embedded fields from selector expression. 20 | # https://staticcheck.dev/docs/checks/#QF1008 21 | - -QF1008 22 | exclusions: 23 | generated: lax 24 | presets: 25 | - comments 26 | - std-error-handling 27 | formatters: 28 | enable: 29 | - gofmt 30 | - goimports 31 | -------------------------------------------------------------------------------- /scripts/examplesgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: ai:ts=8:sw=8:noet 3 | set -efCo pipefail 4 | export SHELLOPTS 5 | IFS=$'\t\n' 6 | 7 | command -v go >/dev/null 2>&1 || { 8 | echo 'please install go' 9 | exit 1 10 | } 11 | 12 | SLOS_PATH="${SLOS_PATH:-./examples}" 13 | [ -z "$SLOS_PATH" ] && echo "SLOS_PATH env is needed" && exit 1 14 | 15 | GEN_PATH="${GEN_PATH:-./examples/_gen}" 16 | [ -z "$GEN_PATH" ] && echo "GEN_PATH env is needed" && exit 1 17 | 18 | mkdir -p "${GEN_PATH}" 19 | 20 | # We already know that we are building sloth for each SLO, good enough, this way we can check 21 | # the current development version. 22 | go run ./cmd/sloth/ generate -i "${SLOS_PATH}" -o "${GEN_PATH}" -p "${SLOS_PATH}" --extra-labels "cmd=examplesgen.sh" -e "_gen|windows" 23 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-openslo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: openslo/v1alpha 2 | kind: SLO 3 | metadata: 4 | name: slo1 5 | displayName: Integration test SLO1 6 | spec: 7 | service: svc01 8 | description: "this is SLO1." 9 | budgetingMethod: Occurrences 10 | objectives: 11 | - ratioMetrics: 12 | good: 13 | source: prometheus 14 | queryType: promql 15 | query: sum(rate(http_request_duration_seconds_count{job="myservice",code!~"(5..|429)"}[{{.window}}])) 16 | total: 17 | source: prometheus 18 | queryType: promql 19 | query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | target: 0.999 21 | timeWindows: 22 | - count: 30 23 | unit: Day 24 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-openslo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: openslo/v1alpha 2 | kind: SLO 3 | metadata: 4 | name: slo1 5 | displayName: Integration test SLO1 6 | spec: 7 | service: svc01 8 | description: "this is SLO1." 9 | budgetingMethod: Occurrences 10 | objectives: 11 | - ratioMetrics: 12 | good: 13 | source: prometheus 14 | queryType: promql 15 | query: sum(rate(http_request_duration_seconds_count{job="myservice",code!~"(5..|429)"}[{{.window}}])) 16 | total: 17 | source: prometheus 18 | queryType: promql 19 | query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | target: 0.999 21 | timeWindows: 22 | - count: 30 23 | unit: Day 24 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/bad/bad-openslo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: openslo/v1alpha 2 | kind: SLO 3 | metadata: 4 | name: slo1 5 | displayName: Integration test SLO1 6 | spec: 7 | service: svc01 8 | description: "this is SLO1." 9 | budgetingMethod: Occurrences 10 | objectives: 11 | - ratioMetrics: 12 | good: 13 | source: prometheus 14 | queryType: promql 15 | query: sum(rate(http_request_duration_seconds_count{job="myservice",code!~"(5..|429)"}[{{.window}}])) 16 | total: 17 | source: prometheus 18 | queryType: promql 19 | query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | target: 0.999 21 | timeWindows: 22 | - count: 28 # BAD! 23 | unit: Day 24 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_sloth_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | v1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" 7 | rest "k8s.io/client-go/rest" 8 | testing "k8s.io/client-go/testing" 9 | ) 10 | 11 | type FakeSlothV1 struct { 12 | *testing.Fake 13 | } 14 | 15 | func (c *FakeSlothV1) PrometheusServiceLevels(namespace string) v1.PrometheusServiceLevelInterface { 16 | return newFakePrometheusServiceLevels(c, namespace) 17 | } 18 | 19 | // RESTClient returns a RESTClient that is used to communicate 20 | // with API server by this client implementation. 21 | func (c *FakeSlothV1) RESTClient() rest.Interface { 22 | var ret *rest.RESTClient 23 | return ret 24 | } 25 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/services/page.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "app_services"}} 3 | 4 | 5 | {{template "shared_head" .}} 6 | 7 | {{template "shared_nav" .}} 8 |
9 |

Services

10 |
11 | 22 |
23 |
24 |
25 | {{template "app_services_comp_service_list" .}} 26 |
27 | 28 | {{template "shared_footer" .}} 29 | 30 | {{end}} -------------------------------------------------------------------------------- /internal/log/logrus/logrus.go: -------------------------------------------------------------------------------- 1 | package logrus 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/slok/sloth/internal/log" 9 | ) 10 | 11 | type logger struct { 12 | *logrus.Entry 13 | } 14 | 15 | // NewLogrus returns a new log.Logger for a logrus implementation. 16 | func NewLogrus(l *logrus.Entry) log.Logger { 17 | return logger{Entry: l} 18 | } 19 | 20 | func (l logger) WithValues(kv log.Kv) log.Logger { 21 | newLogger := l.Entry.WithFields(kv) 22 | return NewLogrus(newLogger) 23 | } 24 | 25 | func (l logger) WithCtxValues(ctx context.Context) log.Logger { 26 | return l.WithValues(log.ValuesFromCtx(ctx)) 27 | } 28 | 29 | func (l logger) SetValuesOnCtx(parent context.Context, values log.Kv) context.Context { 30 | return log.CtxWithValues(parent, values) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/build/bin/build-raw.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | # Env vars that can be set. 7 | # - EXTENSION: The binary out extension. 8 | # - VERSION: Version for the binary. 9 | # - GOOS: OS compiling target 10 | # - GOARCH: Arch compiling target. 11 | # - GOARM: ARM version. 12 | 13 | version_path="github.com/slok/sloth/internal/info.Version" 14 | src=./cmd/sloth 15 | out=./bin/sloth 16 | 17 | # Prepare flags. 18 | final_out=${out}${EXTENSION:-} 19 | ldf_cmp="-s -w -extldflags '-static'" 20 | f_ver="-X ${version_path}=${VERSION:-dev}" 21 | 22 | # Build binary. 23 | echo "[*] Building binary at ${final_out} (GOOS=${GOOS:-}, GOARCH=${GOARCH:-}, GOARM=${GOARM:-}, VERSION=${VERSION:-}, EXTENSION=${EXTENSION:-})" 24 | CGO_ENABLED=0 go build -o ${final_out} --ldflags "${ldf_cmp} ${f_ver}" -buildvcs=false ${src} 25 | -------------------------------------------------------------------------------- /scripts/build/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | build_script="./scripts/build/bin/build-raw.sh" 7 | ostype=${ostype:-"native"} 8 | 9 | echo "[+] Build OS type selected: ${ostype}" 10 | 11 | if [ $ostype == 'Linux' ]; then 12 | EXTENSION="-linux-amd64" GOOS="linux" GOARCH="amd64" ${build_script} 13 | elif [ $ostype == 'Darwin' ]; then 14 | EXTENSION="-darwin-amd64" GOOS="darwin" GOARCH="amd64" ${build_script} 15 | EXTENSION="-darwin-arm64" GOOS="darwin" GOARCH="arm64" ${build_script} 16 | elif [ $ostype == 'Windows' ]; then 17 | EXTENSION="-windows-amd64.exe" GOOS="windows" GOARCH="amd64" ${build_script} 18 | elif [ $ostype == 'ARM' ]; then 19 | EXTENSION="-linux-arm64" GOOS="linux" GOARCH="arm64" ${build_script} 20 | EXTENSION="-linux-arm-v7" GOOS="linux" GOARCH="arm" GOARM="7" ${build_script} 21 | else 22 | # Native. 23 | ${build_script} 24 | fi 25 | -------------------------------------------------------------------------------- /internal/http/backend/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Recorder interface { 9 | MeasureStorageOperationDuration(ctx context.Context, op string, t time.Duration, err error) 10 | MeasurePrometheusStorageBackgroundCacheRefresh(ctx context.Context, t time.Duration, err error) 11 | MeasurePrometheusAPIClientOperation(ctx context.Context, op string, t time.Duration, err error) 12 | } 13 | 14 | type noopRecorder bool 15 | 16 | var NoopRecorder Recorder = noopRecorder(false) 17 | 18 | func (r noopRecorder) MeasureStorageOperationDuration(ctx context.Context, op string, t time.Duration, err error) { 19 | } 20 | 21 | func (r noopRecorder) MeasurePrometheusStorageBackgroundCacheRefresh(ctx context.Context, t time.Duration, err error) { 22 | } 23 | 24 | func (r noopRecorder) MeasurePrometheusAPIClientOperation(ctx context.Context, op string, t time.Duration, err error) { 25 | } 26 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/validate_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/slok/sloth/pkg/common/validation" 9 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 10 | ) 11 | 12 | const ( 13 | PluginVersion = "prometheus/slo/v1" 14 | PluginID = "sloth.dev/core/validate/v1" 15 | ) 16 | 17 | func NewPlugin(_ json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 18 | return plugin{ 19 | appUtils: appUtils, 20 | }, nil 21 | } 22 | 23 | type plugin struct { 24 | appUtils pluginslov1.AppUtils 25 | } 26 | 27 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 28 | err := validation.ValidateSLO(request.SLO, validation.PromQLDialectValidator) 29 | if err != nil { 30 | return fmt.Errorf("invalid slo %q: %w", request.SLO.ID, err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/alert_rules_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/alert_rules/v1 2 | 3 | This plugin generates multi-window, multi-burn-rate (MWMB) Prometheus alerting rules for SLOs based on pre-existing SLI recording rules. It is part of Sloth's default behavior and is responsible for producing both **page** and **ticket** severity alerts, depending on the SLO configuration. 4 | 5 | It supports advanced alerting patterns using short and long burn windows to detect fast and slow error budget consumption. 6 | 7 | ## Config 8 | 9 | None 10 | 11 | ## Env vars 12 | 13 | None 14 | 15 | ## Order requirement 16 | 17 | This plugin should generally run after validation plugins. 18 | 19 | ## Usage examples 20 | 21 | ### Default usage (auto-loaded) 22 | 23 | This plugin is automatically executed by default when no custom plugin chain is defined. 24 | 25 | ### Explicit inclusion 26 | 27 | ```yaml 28 | chain: 29 | - id: "sloth.dev/core/alert_rules/v1" 30 | ``` 31 | -------------------------------------------------------------------------------- /scripts/deploygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: ai:ts=8:sw=8:noet 3 | set -efCo pipefail 4 | export SHELLOPTS 5 | IFS=$'\t\n' 6 | 7 | command -v helm >/dev/null 2>&1 || { echo 'please install helm'; exit 1; } 8 | 9 | HELM_CHART_PATH="${HELM_CHART_PATH:-./deploy/kubernetes/helm/sloth}" 10 | [ -z "$HELM_CHART_PATH" ] && echo "HELM_CHART_PATH env is needed" && exit 1; 11 | 12 | GEN_PATH="${GEN_PATH:-./deploy/kubernetes/raw}" 13 | [ -z "$GEN_PATH" ] && echo "GEN_PATH env is needed" && exit 1; 14 | 15 | mkdir -p "${GEN_PATH}" 16 | 17 | echo "[*] Rendering chart without plugins..." 18 | rm "${GEN_PATH}/sloth.yaml" 19 | helm template sloth "${HELM_CHART_PATH}" \ 20 | --namespace "monitoring" \ 21 | --set "commonPlugins.enabled=false" > "${GEN_PATH}/sloth.yaml" 22 | 23 | echo "[*] Rendering chart with plugins..." 24 | rm "${GEN_PATH}/sloth-with-common-plugins.yaml" 25 | helm template sloth "${HELM_CHART_PATH}" \ 26 | --namespace "monitoring" > "${GEN_PATH}/sloth-with-common-plugins.yaml" -------------------------------------------------------------------------------- /.github/workflows/helmrelease.yaml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "deploy/kubernetes/helm/sloth/Chart.yaml" 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Configure Git 20 | run: | 21 | git config user.name "$GITHUB_ACTOR" 22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 23 | 24 | - name: Install Helm 25 | uses: azure/setup-helm@v4 26 | with: 27 | version: v3.17.0 28 | 29 | - name: Run chart-releaser 30 | uses: helm/chart-releaser-action@v1 31 | with: 32 | charts_dir: deploy/kubernetes/helm 33 | env: 34 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 35 | CR_RELEASE_NAME_TEMPLATE: "sloth-helm-chart-{{ .Version }}" 36 | -------------------------------------------------------------------------------- /.github/workflows/generate.yaml: -------------------------------------------------------------------------------- 1 | # Sample job that allows you to download the generated files as Artifacts from the Github Actions page 2 | 3 | name: SLO generation 4 | 5 | on: 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | generate-slo-job-1: 11 | name: Generate the SLOs 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - name: download and setup generator binary 16 | run: | 17 | wget https://github.com/slok/sloth/releases/download/v0.9.0/sloth-linux-amd64 18 | chmod +x sloth-linux-amd64 19 | ./sloth-linux-amd64 generate -i ./examples/getting-started.yml -o ./examples/_gen/getting-started.yml 20 | ./sloth-linux-amd64 generate -i ./examples/no-alerts.yml -o ./examples/_gen/no-alerts.yml 21 | - name: "Upload directory with generated SLOs" 22 | uses: actions/upload-artifact@v6 23 | with: 24 | name: SLOs 25 | path: examples/_gen/ 26 | -------------------------------------------------------------------------------- /examples/no-alerts.yml: -------------------------------------------------------------------------------- 1 | # This example shows a simple service level by implementing a single SLO without alerts. 2 | # It disables page (critical) and ticket (warning) alerts. 3 | # The SLO SLI measures the event errors as the http request respones with the code >=500 and 429. 4 | # 5 | # `sloth generate -i ./examples/no-alerts.yml` 6 | # 7 | version: "prometheus/v1" 8 | service: "myapp" 9 | labels: 10 | owner: "myteam" 11 | slos: 12 | - name: "http-availability" 13 | objective: 99.99 14 | description: "Common SLO based on availability for HTTP request responses." 15 | sli: 16 | events: 17 | error_query: | 18 | sum( 19 | rate(http_request_duration_seconds_count{job="myapp", code=~"(5..|429)"}[{{.window}}]) 20 | ) 21 | total_query: | 22 | sum( 23 | rate(http_request_duration_seconds_count{job="myapp"}[{{.window}}]) 24 | ) 25 | alerting: 26 | page_alert: 27 | disable: true 28 | ticket_alert: 29 | disable: true 30 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package internalinterfaces 4 | 5 | import ( 6 | time "time" 7 | 8 | versioned "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | cache "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 15 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 16 | 17 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 18 | type SharedInformerFactory interface { 19 | Start(stopCh <-chan struct{}) 20 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 21 | } 22 | 23 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 24 | type TweakListOptionsFunc func(*v1.ListOptions) 25 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/metadata_rules_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/metadata_rules/v1 2 | 3 | This plugin generates a standard set of Prometheus recording rules that provide metadata about the SLO. These rules are used by Sloth by default and help to enrich the SLO with information such as burn rates, objective ratios, time period lengths, and general descriptive labels. 4 | 5 | It is automatically included by Sloth unless explicitly disabled. While it does not need custom configuration, understanding its output can be useful for integration with dashboards or alerting systems. 6 | 7 | ## Config 8 | 9 | None 10 | 11 | ## Env vars 12 | 13 | None 14 | 15 | ## Order requirement 16 | 17 | This plugin should generally run after validation plugins. 18 | 19 | ## Usage examples 20 | 21 | ### Default usage (auto-loaded) 22 | 23 | This plugin is automatically executed by default when no custom plugin chain is defined. 24 | 25 | ### Explicit inclusion 26 | 27 | ```yaml 28 | 29 | chain: 30 | - id: "sloth.dev/core/metadata_rules/v1" 31 | ``` 32 | -------------------------------------------------------------------------------- /pkg/common/utils/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | prommodel "github.com/prometheus/common/model" 8 | ) 9 | 10 | // TimeDurationToPromStr converts from std duration to prom string duration. 11 | func TimeDurationToPromStr(t time.Duration) string { 12 | return prommodel.Duration(t).String() 13 | } 14 | 15 | // PromStrToTimeDuration converts from prom string duration to std duration. 16 | func PromStrToTimeDuration(t string) (time.Duration, error) { 17 | d, err := prommodel.ParseDuration(t) 18 | if err != nil { 19 | return 0, fmt.Errorf("could not parse prom duration %q: %w", t, err) 20 | } 21 | return time.Duration(d), nil 22 | } 23 | 24 | // LabelsToPromFilter converts a map of labels to a Prometheus filter string. 25 | func LabelsToPromFilter(labels map[string]string) string { 26 | metricFilters := prommodel.LabelSet{} 27 | for k, v := range labels { 28 | metricFilters[prommodel.LabelName(k)] = prommodel.LabelValue(v) 29 | } 30 | 31 | return metricFilters.String() 32 | } 33 | -------------------------------------------------------------------------------- /examples/openslo-getting-started.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as getting-started.yml but using OpenSLO spec. 2 | # It will generate the Prometheus rules in a Prometheus rules format. 3 | # 4 | # `sloth generate -i ./examples/openslo-getting-started.yml` 5 | # 6 | apiVersion: openslo/v1alpha 7 | kind: SLO 8 | metadata: 9 | name: sloth-slo-my-service 10 | displayName: Requests Availability 11 | spec: 12 | service: my-service 13 | description: "Common SLO based on availability for HTTP request responses." 14 | budgetingMethod: Occurrences 15 | objectives: 16 | - ratioMetrics: 17 | good: 18 | source: prometheus 19 | queryType: promql 20 | query: sum(rate(http_request_duration_seconds_count{job="myservice",code!~"(5..|429)"}[{{.window}}])) 21 | total: 22 | source: prometheus 23 | queryType: promql 24 | query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 25 | target: 0.999 26 | timeWindows: 27 | - count: 30 28 | unit: Day 29 | -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/custom.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import ( 4 | "reflect" 5 | 6 | _ "github.com/caarlos0/env/v11" // Used only by yaegi plugins, not by Sloth. 7 | ) 8 | 9 | //go:generate yaegi extract --name custom github.com/caarlos0/env/v11 10 | 11 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1 12 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/conventions 13 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/model 14 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/data 15 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/prometheus 16 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/k8s 17 | 18 | //go:generate yaegi extract --name custom k8s.io/apimachinery/pkg/apis/meta/v1/unstructured 19 | 20 | // Symbols variable stores the map of custom Yaegi symbols per package. 21 | var Symbols = map[string]map[string]reflect.Value{} 22 | -------------------------------------------------------------------------------- /pkg/common/conventions/conventions.go: -------------------------------------------------------------------------------- 1 | package conventions 2 | 3 | import "regexp" 4 | 5 | var ( 6 | // NameRegexp is the regex to validate SLO, SLI and in general safe names and IDs. 7 | // Names must: 8 | // - Start and end with an alphanumeric. 9 | // - Contain alphanumeric, `.`, '_', and '-'. 10 | NameRegexpStr = `^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$` 11 | NameRegexp = regexp.MustCompile(NameRegexpStr) 12 | 13 | // TplSLIQueryWindowVarRegex is the regex to match the {{ .window }} template variable used in the SLI queries. 14 | TplSLIQueryWindowVarRegex = regexp.MustCompile(`{{ *\.window *}}`) 15 | 16 | // TplSLIQueryWindowVarName is the name of the window template variable used in the SLI queries. 17 | TplSLIQueryWindowVarName = "window" 18 | ) 19 | 20 | const ( 21 | PromRuleGroupNameSLOSLIPrefix = "sloth-slo-sli-recordings-" 22 | PromRuleGroupNameSLOMetadataPrefix = "sloth-slo-meta-recordings-" 23 | PromRuleGroupNameSLOAlertsPrefix = "sloth-slo-alerts-" 24 | PromRuleGroupNameSLOExtraRulesPrefix = "sloth-slo-extra-rules-" 25 | ) 26 | -------------------------------------------------------------------------------- /.github/workflows/close-stale.yaml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v10 11 | with: 12 | days-before-stale: 90 13 | days-before-close: 30 14 | stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." 15 | close-issue-message: "This issue was closed because it has been stale for 15 days with no activity." 16 | stale-pr-message: "This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." 17 | close-pr-message: "This PR was closed because it has been stale for 15 days with no activity." 18 | stale-issue-label: stale 19 | stale-pr-label: stale 20 | exempt-issue-labels: no-stale 21 | exempt-pr-labels: no-stale 22 | exempt-draft-pr: true 23 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/prometheus/prometheus/model/rulefmt'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/prometheus/prometheus/model/rulefmt" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/prometheus/prometheus/model/rulefmt/rulefmt"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "Parse": reflect.ValueOf(rulefmt.Parse), 14 | "ParseFile": reflect.ValueOf(rulefmt.ParseFile), 15 | 16 | // type definitions 17 | "Error": reflect.ValueOf((*rulefmt.Error)(nil)), 18 | "Rule": reflect.ValueOf((*rulefmt.Rule)(nil)), 19 | "RuleGroup": reflect.ValueOf((*rulefmt.RuleGroup)(nil)), 20 | "RuleGroupNode": reflect.ValueOf((*rulefmt.RuleGroupNode)(nil)), 21 | "RuleGroups": reflect.ValueOf((*rulefmt.RuleGroups)(nil)), 22 | "RuleNode": reflect.ValueOf((*rulefmt.RuleNode)(nil)), 23 | "WrappedError": reflect.ValueOf((*rulefmt.WrappedError)(nil)), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/http/backend/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/slok/sloth/internal/http/backend/storage" 8 | ) 9 | 10 | type AppConfig struct { 11 | ServiceGetter storage.ServiceGetter 12 | SLOGetter storage.SLOGetter 13 | TimeNowFunc func() time.Time 14 | } 15 | 16 | func (c *AppConfig) defaults() error { 17 | if c.ServiceGetter == nil { 18 | return fmt.Errorf("service getter is required") 19 | } 20 | if c.SLOGetter == nil { 21 | return fmt.Errorf("slo getter is required") 22 | } 23 | if c.TimeNowFunc == nil { 24 | c.TimeNowFunc = time.Now 25 | } 26 | 27 | return nil 28 | } 29 | 30 | type App struct { 31 | serviceGetter storage.ServiceGetter 32 | sloGetter storage.SLOGetter 33 | timeNowFunc func() time.Time 34 | } 35 | 36 | func NewApp(config AppConfig) (*App, error) { 37 | if err := config.defaults(); err != nil { 38 | return nil, err 39 | } 40 | 41 | return &App{ 42 | serviceGetter: config.ServiceGetter, 43 | sloGetter: config.SLOGetter, 44 | timeNowFunc: config.TimeNowFunc, 45 | }, nil 46 | } 47 | -------------------------------------------------------------------------------- /docker/prod/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set also `ARCH` ARG here so we can use it on all the `FROM`s. 2 | ARG ARCH 3 | 4 | FROM golang:1.25-alpine as build-stage 5 | 6 | LABEL org.opencontainers.image.source=https://github.com/slok/sloth 7 | 8 | RUN apk --no-cache add \ 9 | g++ \ 10 | git \ 11 | make \ 12 | curl \ 13 | bash 14 | 15 | # Required by the built script for setting verion and cross-compiling. 16 | ARG VERSION 17 | ENV VERSION=${VERSION} 18 | ARG ARCH 19 | ENV GOARCH=${ARCH} 20 | 21 | # Compile. 22 | WORKDIR /src 23 | COPY . . 24 | RUN ./scripts/build/bin/build-raw.sh 25 | 26 | 27 | # Although we are on an specific architecture (normally linux/amd64) our go binary has been built for 28 | # ${ARCH} specific architecture. 29 | # To make portable our building process we base our final image on that same architecture as the binary 30 | # to obtain a resulting ${ARCH} image independently where we are building this image. 31 | FROM gcr.io/distroless/static:nonroot-${ARCH} 32 | 33 | COPY --from=build-stage /src/bin/sloth /usr/local/bin/sloth 34 | 35 | ENTRYPOINT ["/usr/local/bin/sloth"] -------------------------------------------------------------------------------- /examples/plugin-getting-started.yml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "myservice" 3 | labels: 4 | owner: "myteam" 5 | repo: "myorg/myservice" 6 | tier: "2" 7 | slos: 8 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 9 | - name: "requests-availability" 10 | objective: 99.9 11 | description: "Common SLO based on availability for HTTP request responses." 12 | sli: 13 | plugin: 14 | id: "getting_started_availability" 15 | options: 16 | job: "myservice" 17 | filter: 'f1="v1",f2="v2"' 18 | alerting: 19 | name: MyServiceHighErrorRate 20 | labels: 21 | category: "availability" 22 | annotations: 23 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 24 | summary: "High error rate on 'myservice' requests responses" 25 | page_alert: 26 | labels: 27 | severity: pageteam 28 | routing_key: myteam 29 | ticket_alert: 30 | labels: 31 | severity: "slack" 32 | slack_channel: "#alerts-myteam" 33 | -------------------------------------------------------------------------------- /examples/contrib-denominator-corrected.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: PrometheusServiceLevel 3 | metadata: 4 | name: svc 5 | namespace: test-ns 6 | spec: 7 | service: "svc01" 8 | labels: 9 | global01k1: global01v1 10 | sloPlugins: 11 | chain: 12 | - id: "sloth.dev/contrib/denominator_corrected_rules/v1" 13 | slos: 14 | - name: "slo1" 15 | objective: 99.9 16 | description: "This is SLO 01." 17 | labels: 18 | global02k1: global02v1 19 | sli: 20 | events: 21 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 22 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 23 | alerting: 24 | name: myServiceAlert 25 | labels: 26 | alert01k1: "alert01v1" 27 | annotations: 28 | alert02k1: "alert02k2" 29 | pageAlert: 30 | labels: 31 | alert03k1: "alert03v1" 32 | ticketAlert: 33 | labels: 34 | alert04k1: "alert04v1" 35 | -------------------------------------------------------------------------------- /internal/plugin/slo/contrib/info_labels_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/contrib/info_labels/v1 2 | 3 | This plugin adds labels to the `info` metric created by Sloth metadata recording rules. 4 | 5 | ## Config 6 | 7 | - `labels` (**Required**, `map[string]string`): The labels to be added to the metric. 8 | - `metricName` (**Optional**, `string`): If you want to customize the info metric where the labels will be added, by default Sloth info metadata metric: `sloth_slo_info`. 9 | 10 | ## Env vars 11 | 12 | None 13 | 14 | ## Order requirement 15 | 16 | This plugin should run after metadata rules generation plugins. 17 | 18 | ## Usage examples 19 | 20 | ### Custom labels 21 | 22 | ```yaml 23 | chain: 24 | - id: "sloth.dev/contrib/info_labels/v1" 25 | config: 26 | labels: 27 | label_k_1: label_v_2 28 | label_k_3: label_v_4 29 | ``` 30 | 31 | ### Custom info name 32 | 33 | ```yaml 34 | chain: 35 | - id: "sloth.dev/contrib/info_labels/v1" 36 | config: 37 | metricName: 🦥_info 38 | labels: 39 | label_k_1: label_v_2 40 | label_k_3: label_v_4 41 | ``` 42 | -------------------------------------------------------------------------------- /internal/http/ui/templates/shared/head.tmpl: -------------------------------------------------------------------------------- 1 | {{define "shared_head"}} 2 | 3 | 4 | 5 | 6 | {{.Common.Title}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{end}} -------------------------------------------------------------------------------- /internal/plugin/slo/core/validate_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/validate/v1 2 | 3 | This plugin validates the SLO specification to ensure it is correct and well-formed according to the **Prometheus SLO dialect**. It is the **first plugin executed** by Sloth and acts as a safety check before any rules are generated or other plugins are run. 4 | 5 | This plugin is **enabled by default** and should only be disabled if you're using a custom backend (e.g., VictoriaMetrics, Loki) that requires a different validation logic. In that case, you should replace this plugin with your own validator plugin tailored to the target system. 6 | 7 | ## Config 8 | 9 | None 10 | 11 | ## Env vars 12 | 13 | None 14 | 15 | ## Order requirement 16 | 17 | This plugin must be placed **first** in the plugin chain to validate the SLO before any further processing is done. 18 | 19 | ## Usage examples 20 | 21 | ### Default usage (auto-loaded) 22 | 23 | This plugin is automatically executed as the first step in the default plugin chain. 24 | 25 | ### Explicit usage 26 | 27 | ```yaml 28 | chain: 29 | - id: "sloth.dev/core/validate/v1" 30 | ``` 31 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/debug_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/debug/v1 2 | 3 | A simple debug plugin used for testing and debugging purposes. For example it can be used to print the SLO mutations of the objects while developing other plugins in a plugin chain easily. 4 | 5 | The plugin will use `debug` level on the logger, so you will need to run sloth with in debug mode to check the debug messages from this plugin. 6 | 7 | ## Config 8 | 9 | - `msg`(**Optional**): A custom message to be logged by the plugin. 10 | - `result`(**Optional**): If `true` logs the plugin received result struct. 11 | - `request`(**Optional**): If `true` logs the plugin received request struct. 12 | 13 | ## Env vars 14 | 15 | None 16 | 17 | ## Order requirement 18 | 19 | None 20 | 21 | ## Usage examples 22 | 23 | ### Simple message log 24 | 25 | ```yaml 26 | chain: 27 | - id: "sloth.dev/core/debug/v1" 28 | config: 29 | msg: "Hello world" 30 | ``` 31 | 32 | ### Log everything as last plugin 33 | 34 | ```yaml 35 | chain: 36 | - id: "sloth.dev/core/debug/v1" 37 | priority: 9999999 38 | config: {msg: "Last plugin", result: true, request: true} 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/getting-started.yml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "myservice" 3 | labels: 4 | owner: "myteam" 5 | repo: "myorg/myservice" 6 | tier: "2" 7 | slos: 8 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 9 | - name: "requests-availability" 10 | objective: 99.9 11 | description: "Common SLO based on availability for HTTP request responses." 12 | sli: 13 | events: 14 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 15 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 16 | alerting: 17 | name: MyServiceHighErrorRate 18 | labels: 19 | category: "availability" 20 | annotations: 21 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 22 | summary: "High error rate on 'myservice' requests responses" 23 | page_alert: 24 | labels: 25 | severity: pageteam 26 | routing_key: myteam 27 | ticket_alert: 28 | labels: 29 | severity: "slack" 30 | slack_channel: "#alerts-myteam" 31 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/informers/externalversions/sloth/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package sloth 4 | 5 | import ( 6 | internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" 7 | v1 "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/sloth/v1" 8 | ) 9 | 10 | // Interface provides access to each of this group's versions. 11 | type Interface interface { 12 | // V1 provides access to shared informers for resources in V1. 13 | V1() v1.Interface 14 | } 15 | 16 | type group struct { 17 | factory internalinterfaces.SharedInformerFactory 18 | namespace string 19 | tweakListOptions internalinterfaces.TweakListOptionsFunc 20 | } 21 | 22 | // New returns a new Interface. 23 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 24 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 25 | } 26 | 27 | // V1 returns a new v1.Interface. 28 | func (g *group) V1() v1.Interface { 29 | return v1.New(g.factory, g.namespace, g.tweakListOptions) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/common/utils/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "maps" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func MergeMaps[M ~map[K]V, K comparable, V any](ms ...M) M { 11 | m := make(M) 12 | for _, m2 := range ms { 13 | maps.Copy(m, m2) 14 | } 15 | return m 16 | } 17 | 18 | func MergeLabels(ms ...map[string]string) map[string]string { 19 | res := map[string]string{} 20 | for _, m := range ms { 21 | for k, v := range m { 22 | res[k] = v 23 | } 24 | } 25 | return res 26 | } 27 | 28 | var ( 29 | splitMarkRe = regexp.MustCompile("(?m)^---") 30 | rmCommentsRe = regexp.MustCompile("(?m)^#.*$") 31 | ) 32 | 33 | func SplitYAML(data []byte) []string { 34 | // Santize. 35 | data = bytes.TrimSpace(data) 36 | data = rmCommentsRe.ReplaceAll(data, []byte("")) 37 | 38 | // Split (YAML can declare multiple files in the same file using `---`). 39 | dataSplit := splitMarkRe.Split(string(data), -1) 40 | 41 | // Remove empty splits. 42 | nonEmptyData := []string{} 43 | for _, d := range dataSplit { 44 | d = strings.TrimSpace(d) 45 | if d != "" { 46 | nonEmptyData = append(nonEmptyData, d) 47 | } 48 | } 49 | 50 | return nonEmptyData 51 | } 52 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package internal 4 | 5 | import ( 6 | fmt "fmt" 7 | sync "sync" 8 | 9 | typed "sigs.k8s.io/structured-merge-diff/v6/typed" 10 | ) 11 | 12 | func Parser() *typed.Parser { 13 | parserOnce.Do(func() { 14 | var err error 15 | parser, err = typed.NewParser(schemaYAML) 16 | if err != nil { 17 | panic(fmt.Sprintf("Failed to parse schema: %v", err)) 18 | } 19 | }) 20 | return parser 21 | } 22 | 23 | var parserOnce sync.Once 24 | var parser *typed.Parser 25 | var schemaYAML = typed.YAMLObject(`types: 26 | - name: __untyped_atomic_ 27 | scalar: untyped 28 | list: 29 | elementType: 30 | namedType: __untyped_atomic_ 31 | elementRelationship: atomic 32 | map: 33 | elementType: 34 | namedType: __untyped_atomic_ 35 | elementRelationship: atomic 36 | - name: __untyped_deduced_ 37 | scalar: untyped 38 | list: 39 | elementType: 40 | namedType: __untyped_atomic_ 41 | elementRelationship: atomic 42 | map: 43 | elementType: 44 | namedType: __untyped_deduced_ 45 | elementRelationship: separable 46 | `) 47 | -------------------------------------------------------------------------------- /pkg/prometheus/plugin/k8stransform/v1/v1.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | "github.com/slok/sloth/pkg/common/model" 9 | ) 10 | 11 | // Version is this plugin type version. 12 | const Version = "prometheus/k8stransform/v1" 13 | 14 | // PluginVersion is the version of the plugin (e.g: `prometheus/k8stransform/v1`). 15 | type PluginVersion = string 16 | 17 | const PluginVersionName = "PluginVersion" 18 | 19 | // PluginID is the ID of the plugin (e.g: sloth.dev/my-test-plugin/v1). 20 | type PluginID = string 21 | 22 | const PluginIDName = "PluginID" 23 | 24 | type K8sObjects struct { 25 | Items []*unstructured.Unstructured 26 | } 27 | 28 | // PluginFactoryName is the required name for the plugin factory. 29 | const PluginFactoryName = "NewPlugin" 30 | 31 | type PluginFactory = func() (Plugin, error) 32 | 33 | // Plugin knows how to transform K8s objects, these transformers should be simple and 34 | // only focused on transforming K8s objects generated from SLOs. 35 | type Plugin interface { 36 | TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*K8sObjects, error) 37 | } 38 | -------------------------------------------------------------------------------- /test/integration/k8scontroller/plugins/slo/plugin1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | utilsdata "github.com/slok/sloth/pkg/common/utils/data" 8 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 9 | ) 10 | 11 | const ( 12 | PluginVersion = "prometheus/slo/v1" 13 | PluginID = "integration-tests/plugin1" 14 | ) 15 | 16 | type Config struct { 17 | Labels map[string]string `json:"labels,omitempty"` 18 | } 19 | 20 | func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 21 | cfg := Config{} 22 | err := json.Unmarshal(configData, &cfg) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return plugin{ 28 | config: cfg, 29 | }, nil 30 | } 31 | 32 | type plugin struct { 33 | config Config 34 | } 35 | 36 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 37 | for i, r := range result.SLORules.MetadataRecRules.Rules { 38 | if r.Record == "sloth_slo_info" { 39 | r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) 40 | result.SLORules.MetadataRecRules.Rules[i] = r 41 | break 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /test/integration/prometheus/plugins/slo/plugin1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | utilsdata "github.com/slok/sloth/pkg/common/utils/data" 8 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 9 | ) 10 | 11 | const ( 12 | PluginVersion = "prometheus/slo/v1" 13 | PluginID = "integration-tests/plugin1" 14 | ) 15 | 16 | type Config struct { 17 | Labels map[string]string `json:"labels,omitempty"` 18 | } 19 | 20 | func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 21 | cfg := Config{} 22 | err := json.Unmarshal(configData, &cfg) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return plugin{ 28 | config: cfg, 29 | }, nil 30 | } 31 | 32 | type plugin struct { 33 | config Config 34 | } 35 | 36 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 37 | for i, r := range result.SLORules.MetadataRecRules.Rules { 38 | if r.Record == "sloth_slo_info" { 39 | r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) 40 | result.SLORules.MetadataRecRules.Rules[i] = r 41 | break 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/prometheus/plugin/v1/v1.go: -------------------------------------------------------------------------------- 1 | // package plugin has all the API to load prometheus plugins using Yaegi. 2 | // It uses aliases and common types to easy the dynamic plugin load so we don't need 3 | // to import this package as a library (remove dependencies/external libs from plugins). 4 | // 5 | // We use map[string]string and let the plugin make the correct conversion of types because 6 | // dealing with interfaces on dynamic plugins can lead to bugs and unwanted behaviour, so we 7 | // play it safe. 8 | package plugin 9 | 10 | import "context" 11 | 12 | // Version is this plugin type version. 13 | const Version = "prometheus/v1" 14 | 15 | // SLIPluginVersion is the version of the plugin (e.g: `prometheus/v1`). 16 | type SLIPluginVersion = string 17 | 18 | // SLIPluginID is the ID of the plugin. 19 | type SLIPluginID = string 20 | 21 | // Metada keys. 22 | const ( 23 | SLIPluginMetaService = "service" 24 | SLIPluginMetaSLO = "slo" 25 | SLIPluginMetaObjective = "objective" 26 | ) 27 | 28 | // SLIPlugin knows how to generate SLIs based on data options. 29 | // 30 | // This is the type the SLI plugins need to implement. 31 | type SLIPlugin = func(ctx context.Context, meta, labels, options map[string]string) (query string, err error) 32 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-slo-plugin.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | owner: myteam 5 | tier: "2" 6 | slo_plugins: 7 | chain: 8 | - id: "integration-tests/plugin1" 9 | priority: 9999999 10 | config: {labels: {"k1": "v1", "k2": "v2"}} 11 | - id: "integration-tests/plugin1" 12 | priority: -999999 13 | config: {labels: {"k3": "v3"}} # These should be replaced because is before defaults 14 | slos: 15 | - name: "slo1" 16 | objective: 99.9 17 | description: "This is SLO 01." 18 | sli: 19 | events: 20 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 21 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 22 | plugins: 23 | chain: 24 | - id: "integration-tests/plugin1" 25 | config: {labels: {"k4": "v4"}} 26 | - id: "integration-tests/plugin1" 27 | priority: 1000 28 | config: {labels: {"k2": "v0", "k5": "v5"}} # k2 should be replaced by a (9999999 priority) plugin. 29 | alerting: 30 | page_alert: 31 | disable: true 32 | ticket_alert: 33 | disable: true 34 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/informers/externalversions/sloth/v1/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" 7 | ) 8 | 9 | // Interface provides access to all the informers in this group version. 10 | type Interface interface { 11 | // PrometheusServiceLevels returns a PrometheusServiceLevelInformer. 12 | PrometheusServiceLevels() PrometheusServiceLevelInformer 13 | } 14 | 15 | type version struct { 16 | factory internalinterfaces.SharedInformerFactory 17 | namespace string 18 | tweakListOptions internalinterfaces.TweakListOptionsFunc 19 | } 20 | 21 | // New returns a new Interface. 22 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 23 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 24 | } 25 | 26 | // PrometheusServiceLevels returns a PrometheusServiceLevelInformer. 27 | func (v *version) PrometheusServiceLevels() PrometheusServiceLevelInformer { 28 | return &prometheusServiceLevelInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 29 | } 30 | -------------------------------------------------------------------------------- /internal/http/ui/routes.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/slok/go-http-metrics/middleware/std" 8 | "github.com/slok/sloth/pkg/common/conventions" 9 | ) 10 | 11 | const ( 12 | URLPathAppPrefix = "/app" 13 | 14 | URLParamServiceID = "serviceID" 15 | URLParamSLOID = "sloID" 16 | ) 17 | 18 | const ( 19 | sloIDRegexStr = `[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9](:[a-zA-Z0-9\-\+_=]+)?` 20 | ) 21 | 22 | func (u ui) registerStaticFilesRoutes() { 23 | u.staticFilesRouter.Handle("/*", http.StripPrefix(ServePrefix, http.FileServer(http.FS(staticFS)))) 24 | } 25 | 26 | func (u ui) registerRoutes() { 27 | u.wrapGet("/", u.handlerIndex()) 28 | 29 | // App. 30 | u.wrapGet(URLPathAppPrefix+"/services", u.handlerSelectService()) 31 | u.wrapGet(URLPathAppPrefix+"/slos", u.handlerSelectSLO()) 32 | u.wrapGet(URLPathAppPrefix+fmt.Sprintf("/services/{%s:%s}", URLParamServiceID, conventions.NameRegexpStr), u.handlerServiceDetails()) 33 | u.wrapGet(URLPathAppPrefix+fmt.Sprintf("/slos/{%s:%s}", URLParamSLOID, sloIDRegexStr), u.handlerSLODetails()) 34 | } 35 | 36 | func (u ui) wrapGet(pattern string, h http.HandlerFunc) { 37 | u.router.With( 38 | // Add endpoint middlewares. 39 | std.HandlerProvider(pattern, u.metricsMiddleware), 40 | ).Get(pattern, h) 41 | } 42 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/custom.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import ( 4 | "reflect" 5 | 6 | _ "github.com/caarlos0/env/v11" // Used only by yaegi plugins, not by Sloth. 7 | ) 8 | 9 | //go:generate yaegi extract --name custom github.com/caarlos0/env/v11 10 | 11 | //go:generate yaegi extract --name custom github.com/prometheus/common/model 12 | //go:generate yaegi extract --name custom github.com/prometheus/prometheus/model/rulefmt 13 | //go:generate yaegi extract --name custom github.com/prometheus/prometheus/promql/parser 14 | 15 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 16 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/conventions 17 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/model 18 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/data 19 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/prometheus 20 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/validation 21 | 22 | //go:generate yaegi extract --name custom github.com/VictoriaMetrics/metricsql 23 | 24 | // Symbols variable stores the map of custom Yaegi symbols per package. 25 | var Symbols = map[string]map[string]reflect.Value{} 26 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliraw.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SLIRawApplyConfiguration represents a declarative configuration of the SLIRaw type for use 6 | // with apply. 7 | // 8 | // SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI 9 | // is already calculated by other recording rule, system... 10 | type SLIRawApplyConfiguration struct { 11 | // ErrorRatioQuery is a Prometheus query that will get the raw error ratio (0-1) for the SLO. 12 | ErrorRatioQuery *string `json:"errorRatioQuery,omitempty"` 13 | } 14 | 15 | // SLIRawApplyConfiguration constructs a declarative configuration of the SLIRaw type for use with 16 | // apply. 17 | func SLIRaw() *SLIRawApplyConfiguration { 18 | return &SLIRawApplyConfiguration{} 19 | } 20 | 21 | // WithErrorRatioQuery sets the ErrorRatioQuery field in the declarative configuration to the given value 22 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 23 | // If called multiple times, the ErrorRatioQuery field is set to the value of the last call. 24 | func (b *SLIRawApplyConfiguration) WithErrorRatioQuery(value string) *SLIRawApplyConfiguration { 25 | b.ErrorRatioQuery = &value 26 | return b 27 | } 28 | -------------------------------------------------------------------------------- /internal/http/ui/handler_index_test.go: -------------------------------------------------------------------------------- 1 | package ui_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHandlerIndex(t *testing.T) { 12 | tests := map[string]struct { 13 | request func() *http.Request 14 | mock func(m mocks) 15 | expBody []string 16 | expHeaders http.Header 17 | expCode int 18 | }{ 19 | "Entering the index should redirect to the services selection page.": { 20 | request: func() *http.Request { 21 | return httptest.NewRequest(http.MethodGet, "/u", nil) 22 | }, 23 | mock: func(m mocks) {}, 24 | expHeaders: http.Header{ 25 | "Content-Type": {"text/html; charset=utf-8"}, 26 | "Location": {"/u/app/services"}, 27 | }, 28 | expCode: 307, 29 | expBody: []string{}, 30 | }, 31 | } 32 | 33 | for name, test := range tests { 34 | t.Run(name, func(t *testing.T) { 35 | assert := assert.New(t) 36 | 37 | m := newMocks(t) 38 | test.mock(m) 39 | 40 | h := newTestUIHandler(t, m) 41 | 42 | w := httptest.NewRecorder() 43 | h.ServeHTTP(w, test.request()) 44 | 45 | assert.Equal(test.expCode, w.Code) 46 | assert.Equal(test.expHeaders, w.Header()) 47 | assertContainsHTTPResponseBody(t, test.expBody, w) 48 | }) 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/debug_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 8 | ) 9 | 10 | const ( 11 | PluginVersion = "prometheus/slo/v1" 12 | PluginID = "sloth.dev/core/debug/v1" 13 | ) 14 | 15 | type Config struct { 16 | CustomMsg string `json:"msg,omitempty"` 17 | ShowResult bool `json:"result,omitempty"` 18 | ShowRequest bool `json:"request,omitempty"` 19 | } 20 | 21 | func NewPlugin(configData json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 22 | cfg := Config{} 23 | err := json.Unmarshal(configData, &cfg) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return plugin{ 29 | config: cfg, 30 | appUtils: appUtils, 31 | }, nil 32 | } 33 | 34 | type plugin struct { 35 | config Config 36 | appUtils pluginslov1.AppUtils 37 | } 38 | 39 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 40 | if p.config.CustomMsg != "" { 41 | p.appUtils.Logger.Debugf("%s", p.config.CustomMsg) 42 | } 43 | 44 | if p.config.ShowRequest { 45 | p.appUtils.Logger.Debugf("%+v", *request) 46 | } 47 | 48 | if p.config.ShowResult { 49 | p.appUtils.Logger.Debugf("%+v", *result) 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/http/ui/handler_service_details_test.go: -------------------------------------------------------------------------------- 1 | package ui_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHandlerServiceDetails(t *testing.T) { 12 | tests := map[string]struct { 13 | request func() *http.Request 14 | mock func(m mocks) 15 | expBody []string 16 | expHeaders http.Header 17 | expCode int 18 | }{ 19 | "Listing the service details should redirect to the SLO listing page with the service ID as filter.": { 20 | request: func() *http.Request { 21 | return httptest.NewRequest(http.MethodGet, "/u/app/services/svc-1", nil) 22 | }, 23 | mock: func(m mocks) {}, 24 | expHeaders: http.Header{ 25 | "Content-Type": {"text/html; charset=utf-8"}, 26 | "Location": {"/u/app/slos?slo-service-id=svc-1"}, 27 | }, 28 | expCode: 303, 29 | }, 30 | } 31 | 32 | for name, test := range tests { 33 | t.Run(name, func(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | m := newMocks(t) 37 | test.mock(m) 38 | 39 | h := newTestUIHandler(t, m) 40 | 41 | w := httptest.NewRecorder() 42 | h.ServeHTTP(w, test.request()) 43 | 44 | assert.Equal(test.expCode, w.Code) 45 | assert.Equal(test.expHeaders, w.Header()) 46 | assertContainsHTTPResponseBody(t, test.expBody, w) 47 | }) 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/http/ui/common_uplot.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | // Check: https://github.com/leeoniya/uPlot/tree/master/docs. 4 | type uPlotSLIChart struct { 5 | Title string `json:"title"` 6 | Width int `json:"width"` 7 | Height int `json:"height"` 8 | TSs []int `json:"timestamps"` 9 | SLIs []*float64 `json:"sli_values"` 10 | SLOObjective float64 `json:"slo_objective"` 11 | } 12 | 13 | func (u *uPlotSLIChart) defaults() error { 14 | if u.Title == "" { 15 | u.Title = "SLI over time" 16 | } 17 | 18 | if u.Height == 0 { 19 | u.Height = 400 20 | } 21 | return nil 22 | } 23 | 24 | // Check: https://github.com/leeoniya/uPlot/tree/master/docs. 25 | type uPlotBudgetBurnChart struct { 26 | Title string `json:"title"` 27 | ColorLineOk bool `json:"color_line_ok"` 28 | Width int `json:"width"` 29 | Height int `json:"height"` 30 | TSs []int `json:"timestamps"` 31 | RealBurned []*float64 `json:"real_burned_values"` 32 | PerfectBurned []*float64 `json:"perfect_burned_values"` 33 | } 34 | 35 | func (u *uPlotBudgetBurnChart) defaults() error { 36 | if u.Title == "" { 37 | u.Title = "Budget Burn" 38 | } 39 | 40 | if u.Height == 0 { 41 | u.Height = 400 42 | } 43 | return nil 44 | } 45 | 46 | func float64Ptr(f float64) *float64 { 47 | return &f 48 | } 49 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-base.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | page_alert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticket_alert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 95 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/slo/comp_budget_chart.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "app_slo_comp_budget_chart" }} 2 | 3 |
4 |
5 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 32 |
33 | {{ end }} -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-invalid-version.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v999" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | page_alert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticket_alert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 95 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-aa.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 95 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-ab.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 95 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-ba.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 95 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/bad/bad-aa.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 101 # BAD! 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/bad/bad-ab.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 101 # BAD! 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/bad/bad-ba.yaml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "svc01" 3 | labels: 4 | global01k1: global01v1 5 | slos: 6 | - name: "slo1" 7 | objective: 99.9 8 | description: "This is SLO 01." 9 | labels: 10 | global02k1: global02v1 11 | sli: 12 | events: 13 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 14 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 15 | alerting: 16 | name: myServiceAlert 17 | labels: 18 | alert01k1: "alert01v1" 19 | annotations: 20 | alert02k1: "alert02k2" 21 | pageAlert: 22 | labels: 23 | alert03k1: "alert03v1" 24 | ticketAlert: 25 | labels: 26 | alert04k1: "alert04v1" 27 | - name: "slo02" 28 | objective: 101 # BAD! 29 | description: "This is SLO 02." 30 | labels: 31 | global03k1: global03v1 32 | sli: 33 | raw: 34 | error_ratio_query: | 35 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 36 | / 37 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 38 | alerting: 39 | page_alert: 40 | disable: true 41 | ticket_alert: 42 | disable: true 43 | -------------------------------------------------------------------------------- /examples/k8s-getting-started.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD. 2 | # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. 3 | # 4 | # `sloth generate -i ./examples/k8s-getting-started.yml` 5 | # 6 | apiVersion: sloth.slok.dev/v1 7 | kind: PrometheusServiceLevel 8 | metadata: 9 | name: sloth-slo-my-service 10 | namespace: monitoring 11 | spec: 12 | service: "myservice" 13 | labels: 14 | owner: "myteam" 15 | repo: "myorg/myservice" 16 | tier: "2" 17 | slos: 18 | - name: "requests-availability" 19 | objective: 99.9 20 | description: "Common SLO based on availability for HTTP request responses." 21 | sli: 22 | events: 23 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 24 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 25 | alerting: 26 | name: MyServiceHighErrorRate 27 | labels: 28 | category: "availability" 29 | annotations: 30 | summary: "High error rate on 'myservice' requests responses" 31 | pageAlert: 32 | labels: 33 | severity: pageteam 34 | routing_key: myteam 35 | ticketAlert: 36 | labels: 37 | severity: "slack" 38 | slack_channel: "#alerts-myteam" 39 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: PrometheusServiceLevel 3 | metadata: 4 | name: svc 5 | namespace: test-ns 6 | spec: 7 | service: "svc01" 8 | labels: 9 | global01k1: global01v1 10 | sloPlugins: 11 | chain: 12 | - id: "integration-tests/plugin1" 13 | priority: 9999999 14 | config: {labels: {"k1": "v1", "k2": "v2"}} 15 | - id: "integration-tests/plugin1" 16 | priority: -999999 17 | config: {labels: {"k3": "v3"}} # These should be replaced because is before defaults 18 | slos: 19 | - name: "slo1" 20 | objective: 99.9 21 | description: "This is SLO 01." 22 | labels: 23 | global02k1: global02v1 24 | sli: 25 | events: 26 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 27 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 28 | plugins: 29 | chain: 30 | - id: "integration-tests/plugin1" 31 | config: {labels: {"k4": "v4"}} 32 | - id: "integration-tests/plugin1" 33 | priority: 1000 34 | config: {labels: {"k2": "v0", "k5": "v5"}} # k2 should be replaced by a (9999999 priority) plugin. 35 | alerting: 36 | pageAlert: 37 | disable: true 38 | ticketAlert: 39 | disable: true 40 | -------------------------------------------------------------------------------- /internal/http/ui/common_test.go: -------------------------------------------------------------------------------- 1 | package ui_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/slok/sloth/internal/http/ui" 15 | "github.com/slok/sloth/internal/http/ui/uimock" 16 | ) 17 | 18 | var trimSpaceMultilineRegexp = regexp.MustCompile(`(?m)(^\s+|\s+$)`) 19 | 20 | func assertContainsHTTPResponseBody(t *testing.T, exp []string, resp *httptest.ResponseRecorder) { 21 | // Sanitize got HTML so we make easier to check content. 22 | got := resp.Body.String() 23 | got = trimSpaceMultilineRegexp.ReplaceAllString(got, "") 24 | got = strings.ReplaceAll(got, "\n", " ") 25 | 26 | // Check each expected snippet. 27 | for _, e := range exp { 28 | assert.Contains(t, got, e) 29 | } 30 | } 31 | 32 | type mocks struct { 33 | ServiceApp *uimock.ServiceApp 34 | } 35 | 36 | func newMocks(t *testing.T) mocks { 37 | return mocks{ 38 | ServiceApp: &uimock.ServiceApp{}, 39 | } 40 | } 41 | 42 | // Always now is an specific time for tests idempotency. 43 | var testTimeNow, _ = time.Parse(time.RFC3339, "2025-11-15T01:02:03Z") 44 | 45 | func newTestUIHandler(t *testing.T, m mocks) http.Handler { 46 | h, err := ui.NewUI(ui.UIConfig{ 47 | ServiceApp: m.ServiceApp, 48 | TimeNowFunc: func() time.Time { return testTimeNow }, 49 | }) 50 | require.NoError(t, err) 51 | 52 | return h 53 | } 54 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var scheme = runtime.NewScheme() 15 | var codecs = serializer.NewCodecFactory(scheme) 16 | 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | slothv1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/sli_rules_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/core/sli_rules/v1 2 | 3 | This plugin generates the Prometheus **SLI error ratio recording rules** for each required time window in the SLO. These rules are used by other plugins (such as alerting and metadata) and are a foundational part of Sloth's default behavior. 4 | 5 | It supports both **event-based** and **raw query-based** SLIs, and it includes an optional optimization mode to reduce Prometheus resource usage by computing longer windows from short-window recording rules. This plugin is executed automatically by default in Sloth. 6 | 7 | ## Config 8 | 9 | - `disableOptimized`(**Optional**, `bool`): If `true`, disables optimized rule generation for long SLI windows. Optimized rules use short-window recording rules to derive long-window SLIs with lower Prometheus resource usage, at the cost of reduced accuracy. Defaults to `false`. 10 | 11 | ## Env vars 12 | 13 | None 14 | 15 | ## Order requirement 16 | 17 | This plugin should generally run after validation plugins. 18 | 19 | ## Usage examples 20 | 21 | ### Default usage (auto-loaded) 22 | 23 | This plugin is automatically executed by default when no custom plugin chain is defined. 24 | 25 | ### With optimizations 26 | 27 | ```yaml 28 | chain: 29 | - id: "sloth.dev/core/sli_rules/v1" 30 | ``` 31 | 32 | ### Disable optimization 33 | 34 | ```yaml 35 | chain: 36 | - id: "sloth.dev/core/sli_rules/v1" 37 | config: 38 | disableOptimized: true 39 | ``` 40 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "sloth.name" -}} 2 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 3 | {{- end }} 4 | 5 | {{- define "sloth.fullname" -}} 6 | {{- if .Values.fullnameOverride }} 7 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 8 | {{- else }} 9 | {{- $name := default .Chart.Name .Values.nameOverride }} 10 | {{- if contains $name .Release.Name }} 11 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 12 | {{- else }} 13 | {{- printf "%s-%s" $name .Release.Name | trunc 63 | trimSuffix "-" }} 14 | {{- end }} 15 | {{- end }} 16 | {{- end }} 17 | 18 | 19 | {{- define "sloth.labels" -}} 20 | helm.sh/chart: {{ include "sloth.chart" . }} 21 | {{- if .Chart.AppVersion }} 22 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 23 | {{- end }} 24 | app.kubernetes.io/managed-by: {{ .Release.Service }} 25 | {{ include "sloth.selectorLabels" . }} 26 | {{- with .Values.labels }} 27 | {{ toYaml . }} 28 | {{- end }} 29 | {{- end }} 30 | 31 | 32 | 33 | {{- define "sloth.selectorLabels" -}} 34 | app: {{ include "sloth.name" . }} 35 | app.kubernetes.io/name: {{ include "sloth.name" . }} 36 | app.kubernetes.io/instance: {{ .Release.Name }} 37 | {{- end }} 38 | 39 | {{- define "sloth.chart" -}} 40 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 41 | {{- end }} 42 | {{- define "sloth.imagePullSecrets" -}} 43 | {{- range .Values.imagePullSecrets }} 44 | - {{ toYaml . | trim }} 45 | {{- end }} 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /test/integration/testutils/cmd.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var multiSpaceRegex = regexp.MustCompile(" +") 14 | 15 | // RunSloth executes sloth command. 16 | func RunSloth(ctx context.Context, env []string, cmdApp, cmdArgs string, nolog bool) (stdout, stderr []byte, err error) { 17 | // Sanitize command. 18 | cmdArgs = strings.TrimSpace(cmdArgs) 19 | cmdArgs = multiSpaceRegex.ReplaceAllString(cmdArgs, " ") 20 | 21 | // Split into args. 22 | args := strings.Split(cmdArgs, " ") 23 | 24 | // Create command. 25 | var outData, errData bytes.Buffer 26 | cmd := exec.CommandContext(ctx, cmdApp, args...) 27 | cmd.Stdout = &outData 28 | cmd.Stderr = &errData 29 | 30 | // Set env. 31 | newEnv := append([]string{}, env...) 32 | newEnv = append(newEnv, os.Environ()...) 33 | if nolog { 34 | newEnv = append(newEnv, 35 | "SLOTH_NO_LOG=true", 36 | "SLOTH_NO_COLOR=true", 37 | ) 38 | } 39 | cmd.Env = newEnv 40 | 41 | // Run. 42 | err = cmd.Run() 43 | 44 | return outData.Bytes(), errData.Bytes(), err 45 | } 46 | 47 | func SlothVersion(ctx context.Context, slothBinary string) (string, error) { 48 | stdout, stderr, err := RunSloth(ctx, []string{}, slothBinary, "version", false) 49 | if err != nil { 50 | return "", fmt.Errorf("could not obtain versions: %s: %w", stderr, err) 51 | } 52 | 53 | version := string(stdout) 54 | version = strings.TrimSpace(version) 55 | 56 | return version, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package scheme 4 | 5 | import ( 6 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var Scheme = runtime.NewScheme() 15 | var Codecs = serializer.NewCodecFactory(Scheme) 16 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | slothv1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(Scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/sloth/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/alecthomas/kingpin/v2" 8 | 9 | "github.com/slok/sloth/internal/log" 10 | ) 11 | 12 | const ( 13 | // LoggerTypeDefault is the logger default type. 14 | LoggerTypeDefault = "default" 15 | // LoggerTypeJSON is the logger json type. 16 | LoggerTypeJSON = "json" 17 | ) 18 | 19 | // Command represents an application command, all commands that want to be executed 20 | // should implement and setup on main. 21 | type Command interface { 22 | Name() string 23 | Run(ctx context.Context, config RootConfig) error 24 | } 25 | 26 | // RootConfig represents the root command configuration and global configuration 27 | // for all the commands. 28 | type RootConfig struct { 29 | // Global flags. 30 | Debug bool 31 | NoLog bool 32 | NoColor bool 33 | LoggerType string 34 | 35 | // Global instances. 36 | Stdin io.Reader 37 | Stdout io.Writer 38 | Stderr io.Writer 39 | Logger log.Logger 40 | } 41 | 42 | // NewRootConfig initializes the main root configuration. 43 | func NewRootConfig(app *kingpin.Application) *RootConfig { 44 | c := &RootConfig{} 45 | 46 | // Register. 47 | app.Flag("debug", "Enable debug mode.").BoolVar(&c.Debug) 48 | app.Flag("no-log", "Disable logger.").BoolVar(&c.NoLog) 49 | app.Flag("no-color", "Disable logger color.").BoolVar(&c.NoColor) 50 | app.Flag("logger", "Selects the logger type.").Default(LoggerTypeDefault).EnumVar(&c.LoggerType, LoggerTypeDefault, LoggerTypeJSON) 51 | 52 | return c 53 | } 54 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/in-base-k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: PrometheusServiceLevel 3 | metadata: 4 | name: svc 5 | namespace: test-ns 6 | spec: 7 | service: "svc01" 8 | labels: 9 | global01k1: global01v1 10 | slos: 11 | - name: "slo1" 12 | objective: 99.9 13 | description: "This is SLO 01." 14 | labels: 15 | global02k1: global02v1 16 | sli: 17 | events: 18 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 19 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | alerting: 21 | name: myServiceAlert 22 | labels: 23 | alert01k1: "alert01v1" 24 | annotations: 25 | alert02k1: "alert02k2" 26 | pageAlert: 27 | labels: 28 | alert03k1: "alert03v1" 29 | ticketAlert: 30 | labels: 31 | alert04k1: "alert04v1" 32 | - name: "slo02" 33 | objective: 95 34 | description: "This is SLO 02." 35 | labels: 36 | global03k1: global03v1 37 | sli: 38 | raw: 39 | errorRatioQuery: | 40 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 41 | / 42 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 43 | alerting: 44 | pageAlert: 45 | disable: true 46 | ticketAlert: 47 | disable: true 48 | -------------------------------------------------------------------------------- /internal/http/backend/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/slok/sloth/internal/http/backend/model" 8 | ) 9 | 10 | // ServiceAndAlerts groups a service with its SLO alerts. 11 | type ServiceAndAlerts struct { 12 | Service model.Service 13 | ServiceStats model.ServiceStats 14 | Alerts []model.SLOAlerts 15 | } 16 | 17 | type ServiceGetter interface { 18 | ListAllServiceAndAlerts(ctx context.Context) ([]ServiceAndAlerts, error) 19 | ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]ServiceAndAlerts, error) 20 | } 21 | 22 | type SLOInstantDetails struct { 23 | SLO model.SLO 24 | BudgetDetails model.SLOBudgetDetails 25 | Alerts model.SLOAlerts 26 | } 27 | 28 | type SLOGetter interface { 29 | ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]SLOInstantDetails, error) 30 | ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID, sloSearchInput string) ([]SLOInstantDetails, error) 31 | ListSLOInstantDetails(ctx context.Context) ([]SLOInstantDetails, error) 32 | ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]SLOInstantDetails, error) 33 | GetSLOInstantDetails(ctx context.Context, sloID string) (*SLOInstantDetails, error) 34 | GetSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration) ([]model.DataPoint, error) 35 | GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from, to time.Time) ([]model.DataPoint, error) 36 | } 37 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/bad/bad-k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: PrometheusServiceLevel 3 | metadata: 4 | name: svc 5 | namespace: test-ns 6 | spec: 7 | service: "" # BAD! 8 | labels: 9 | global01k1: global01v1 10 | slos: 11 | - name: "slo1" 12 | objective: 99.9 13 | description: "This is SLO 01." 14 | labels: 15 | global02k1: global02v1 16 | sli: 17 | events: 18 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 19 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | alerting: 21 | name: myServiceAlert 22 | labels: 23 | alert01k1: "alert01v1" 24 | annotations: 25 | alert02k1: "alert02k2" 26 | pageAlert: 27 | labels: 28 | alert03k1: "alert03v1" 29 | ticketAlert: 30 | labels: 31 | alert04k1: "alert04v1" 32 | - name: "slo02" 33 | objective: 95 34 | description: "This is SLO 02." 35 | labels: 36 | global03k1: global03v1 37 | sli: 38 | raw: 39 | errorRatioQuery: | 40 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 41 | / 42 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 43 | alerting: 44 | pageAlert: 45 | disable: true 46 | ticketAlert: 47 | disable: true 48 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: sloth.slok.dev/v1 2 | kind: PrometheusServiceLevel 3 | metadata: 4 | name: svc 5 | namespace: test-ns 6 | spec: 7 | service: "svc01" 8 | labels: 9 | global01k1: global01v1 10 | slos: 11 | - name: "slo1" 12 | objective: 99.9 13 | description: "This is SLO 01." 14 | labels: 15 | global02k1: global02v1 16 | sli: 17 | events: 18 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 19 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 20 | alerting: 21 | name: myServiceAlert 22 | labels: 23 | alert01k1: "alert01v1" 24 | annotations: 25 | alert02k1: "alert02k2" 26 | pageAlert: 27 | labels: 28 | alert03k1: "alert03v1" 29 | ticketAlert: 30 | labels: 31 | alert04k1: "alert04v1" 32 | - name: "slo02" 33 | objective: 95 34 | description: "This is SLO 02." 35 | labels: 36 | global03k1: global03v1 37 | sli: 38 | raw: 39 | errorRatioQuery: | 40 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 41 | / 42 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 43 | alerting: 44 | pageAlert: 45 | disable: true 46 | ticketAlert: 47 | disable: true 48 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/slo/comp_sli_chart.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "app_slo_comp_sli_chart" }} 2 | 3 |
4 |
5 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 36 |
37 | {{ end }} -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/values_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | type msi = map[string]interface{} 4 | 5 | func defaultValues() msi { 6 | return msi{} 7 | } 8 | 9 | func customValues() msi { 10 | return msi{ 11 | "global": msi{ 12 | "imageRegistry": "", 13 | }, 14 | 15 | "labels": msi{ 16 | "label-from": "test", 17 | }, 18 | 19 | "image": msi{ 20 | "registry": "slok", 21 | "repository": "sloth-test", 22 | "tag": "v1.42.42", 23 | }, 24 | 25 | "sloth": msi{ 26 | "resyncInterval": "17m", 27 | "workers": 99, 28 | "labelSelector": `x=y,z!=y`, 29 | "namespace": "somens", 30 | "extraLabels": msi{ 31 | "k1": "v1", 32 | "k2": "v2", 33 | }, 34 | }, 35 | 36 | "nodeSelector": msi{ 37 | "k1": "v1", 38 | "k2": "v2", 39 | }, 40 | 41 | "commonPlugins": msi{ 42 | "enabled": true, 43 | "gitRepo": msi{ 44 | "url": "https://github.com/slok/sloth-test-common-sli-plugins", 45 | "branch": "main", 46 | }, 47 | }, 48 | 49 | "metrics": msi{ 50 | "enabled": true, 51 | "scrapeInterval": "45s", 52 | "prometheusLabels": msi{ 53 | "kp1": "vp1", 54 | "kp2": "vp2", 55 | }, 56 | }, 57 | 58 | "customSloConfig": msi{ 59 | "data": msi{ 60 | "customKey": "customValue", 61 | }, 62 | }, 63 | 64 | "securityContext": msi{ 65 | "pod": msi{ 66 | "runAsNonRoot": true, 67 | "runAsGroup": 1000, 68 | "runAsUser": 100, 69 | "fsGroup": 100, 70 | }, 71 | "container": msi{ 72 | "allowPrivilegeEscalation": false, 73 | }, 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/plugin/slo/contrib/info_labels_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/slok/sloth/pkg/common/conventions" 9 | utilsdata "github.com/slok/sloth/pkg/common/utils/data" 10 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 11 | ) 12 | 13 | const ( 14 | PluginVersion = "prometheus/slo/v1" 15 | PluginID = "sloth.dev/contrib/info_labels/v1" 16 | ) 17 | 18 | type Config struct { 19 | Labels map[string]string `json:"labels,omitempty"` 20 | MetricName string `json:"metricName,omitempty"` 21 | } 22 | 23 | func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 24 | config := Config{} 25 | err := json.Unmarshal(configData, &config) 26 | if err != nil { 27 | return nil, fmt.Errorf("invalid config: %w", err) 28 | } 29 | 30 | if config.MetricName == "" { 31 | config.MetricName = conventions.PromMetaSLOInfoMetric 32 | } 33 | 34 | if len(config.Labels) == 0 { 35 | return nil, fmt.Errorf("at least one label is required") 36 | } 37 | 38 | return plugin{config: config}, nil 39 | } 40 | 41 | type plugin struct { 42 | config Config 43 | } 44 | 45 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 46 | for i, r := range result.SLORules.MetadataRecRules.Rules { 47 | if r.Record == p.config.MetricName { 48 | r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) 49 | result.SLORules.MetadataRecRules.Rules[i] = r 50 | break 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/app/kubecontroller/retriever.go: -------------------------------------------------------------------------------- 1 | package kubecontroller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spotahome/kooper/v2/controller" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/watch" 11 | "k8s.io/client-go/tools/cache" 12 | 13 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 14 | ) 15 | 16 | // RetrieverKubernetesRepository is the service to manage k8s resources by the Kubernetes controller retrievers. 17 | type RetrieverKubernetesRepository interface { 18 | ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) 19 | WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) 20 | } 21 | 22 | // NewPrometheusServiceLevelsRetriver returns the retriever for Prometheus service levels events. 23 | func NewPrometheusServiceLevelsRetriver(ns string, labelSelector labels.Selector, repo RetrieverKubernetesRepository) controller.Retriever { 24 | return controller.MustRetrieverFromListerWatcher(&cache.ListWatch{ 25 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 26 | options.LabelSelector = labelSelector.String() 27 | return repo.ListPrometheusServiceLevels(context.Background(), ns, options) 28 | }, 29 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 30 | options.LabelSelector = labelSelector.String() 31 | return repo.WatchPrometheusServiceLevels(context.Background(), ns, options) 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /examples/plugin-k8s-getting-started.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as home-wifi.yml but using Sloth Kubernetes CRD. 2 | # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. 3 | # 4 | # `sloth generate -i ./examples/plugin-k8s-home-wifi.yml` -p ./examples 5 | # 6 | apiVersion: sloth.slok.dev/v1 7 | kind: PrometheusServiceLevel 8 | metadata: 9 | name: sloth-slo-home-wifi 10 | namespace: monitoring 11 | labels: 12 | prometheus: prometheus 13 | role: alert-rules 14 | app: sloth 15 | spec: 16 | service: "myservice" 17 | labels: 18 | owner: "myteam" 19 | repo: "myorg/myservice" 20 | tier: "2" 21 | slos: 22 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 23 | - name: "requests-availability" 24 | objective: 99.9 25 | description: "Common SLO based on availability for HTTP request responses." 26 | sli: 27 | plugin: 28 | id: "getting_started_availability" 29 | options: 30 | job: "myservice" 31 | filter: 'f1="v1",f2="v2"' 32 | alerting: 33 | name: MyServiceHighErrorRate 34 | labels: 35 | category: "availability" 36 | annotations: 37 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 38 | summary: "High error rate on 'myservice' requests responses" 39 | page_alert: 40 | labels: 41 | severity: pageteam 42 | routing_key: myteam 43 | ticket_alert: 44 | labels: 45 | severity: "slack" 46 | slack_channel: "#alerts-myteam" 47 | -------------------------------------------------------------------------------- /test/integration/prometheus/helpers.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/slok/sloth/test/integration/testutils" 11 | ) 12 | 13 | type Config struct { 14 | Binary string 15 | } 16 | 17 | func (c *Config) defaults() error { 18 | if c.Binary == "" { 19 | c.Binary = "sloth" 20 | } 21 | 22 | _, err := exec.LookPath(c.Binary) 23 | if err != nil { 24 | return fmt.Errorf("sloth binary missing in %q: %w", c.Binary, err) 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // NewIntegrationConfig prepares the configuration for integration tests, if the configuration is not ready 31 | // it will skip the test. 32 | func NewConfig(t *testing.T) Config { 33 | const ( 34 | envSlothBin = "SLOTH_INTEGRATION_BINARY" 35 | ) 36 | 37 | c := Config{ 38 | Binary: os.Getenv(envSlothBin), 39 | } 40 | 41 | err := c.defaults() 42 | if err != nil { 43 | t.Skipf("Skipping due to invalid config: %s", err) 44 | } 45 | 46 | return c 47 | } 48 | 49 | func RunSlothGenerate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { 50 | env := []string{ 51 | fmt.Sprintf("SLOTH_PLUGINS_PATH=%s", "./plugins"), 52 | } 53 | 54 | return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("generate %s", cmdArgs), true) 55 | } 56 | 57 | func RunSlothValidate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { 58 | env := []string{ 59 | fmt.Sprintf("SLOTH_PLUGINS_PATH=%s", "./plugins"), 60 | } 61 | 62 | return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("validate %s", cmdArgs), true) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package externalversions 4 | 5 | import ( 6 | fmt "fmt" 7 | 8 | v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | cache "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 14 | // sharedInformers based on type 15 | type GenericInformer interface { 16 | Informer() cache.SharedIndexInformer 17 | Lister() cache.GenericLister 18 | } 19 | 20 | type genericInformer struct { 21 | informer cache.SharedIndexInformer 22 | resource schema.GroupResource 23 | } 24 | 25 | // Informer returns the SharedIndexInformer. 26 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 27 | return f.informer 28 | } 29 | 30 | // Lister returns the GenericLister. 31 | func (f *genericInformer) Lister() cache.GenericLister { 32 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 33 | } 34 | 35 | // ForResource gives generic access to a shared informer of the matching type 36 | // TODO extend this to unknown resources with a client pool 37 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 38 | switch resource { 39 | // Group=sloth.slok.dev, Version=v1 40 | case v1.SchemeGroupVersion.WithResource("prometheusservicelevels"): 41 | return &genericInformer{resource: resource.GroupResource(), informer: f.Sloth().V1().PrometheusServiceLevels().Informer()}, nil 42 | 43 | } 44 | 45 | return nil, fmt.Errorf("no informer found for %v", resource) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/kubernetes/api/sloth/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | 8 | "github.com/slok/sloth/pkg/kubernetes/api/sloth" 9 | ) 10 | 11 | const ( 12 | version = "v1" 13 | ) 14 | 15 | // SchemeGroupVersion is group version used to register these objects. 16 | var SchemeGroupVersion = schema.GroupVersion{Group: sloth.GroupName, Version: version} 17 | 18 | // Kind takes an unqualified kind and returns back a Group qualified GroupKind. 19 | func Kind(kind string) schema.GroupKind { 20 | return VersionKind(kind).GroupKind() 21 | } 22 | 23 | // VersionKind takes an unqualified kind and returns back a Group qualified GroupVersionKind. 24 | func VersionKind(kind string) schema.GroupVersionKind { 25 | return SchemeGroupVersion.WithKind(kind) 26 | } 27 | 28 | // Resource takes an unqualified resource and returns a Group qualified GroupResource. 29 | func Resource(resource string) schema.GroupResource { 30 | return SchemeGroupVersion.WithResource(resource).GroupResource() 31 | } 32 | 33 | var ( 34 | // SchemeBuilder initializes a scheme builder. 35 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 36 | // AddToScheme is a global function that registers this API group & version to a scheme. 37 | AddToScheme = SchemeBuilder.AddToScheme 38 | ) 39 | 40 | // Adds the list of known types to Scheme. 41 | func addKnownTypes(scheme *runtime.Scheme) error { 42 | scheme.AddKnownTypes(SchemeGroupVersion, 43 | &PrometheusServiceLevel{}, 44 | &PrometheusServiceLevelList{}, 45 | ) 46 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /examples/victoria-metrics.yml: -------------------------------------------------------------------------------- 1 | version: prometheus/v1 2 | service: foo-bar 3 | labels: 4 | owner: foo 5 | repo: content/foo-bar 6 | generated: true 7 | type: latency 8 | application: slowpoke 9 | slo_plugins: 10 | overridePrevious: true 11 | chain: 12 | - id: sloth.dev/contrib/validate_victoria_metrics/v1 13 | - id: sloth.dev/core/sli_rules/v1 14 | - id: sloth.dev/core/metadata_rules/v1 15 | - id: sloth.dev/core/alert_rules/v1 16 | slos: 17 | - name: grpc-latency-percentile-90 18 | objective: 90 19 | description: '"grpc" 90 percentile Latency SLO for grade "A"' 20 | labels: 21 | actual_grade: A 22 | target_grade: A 23 | actual_le: 0.2 24 | target_le: 0.2 25 | sli: 26 | events: 27 | error_query: 1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[{{.window}}]))) 28 | total_query: vector(1)[{{.window}}] 29 | alerting: 30 | name: slo_foo-bar_grpc-latency-percentile-90_fail 31 | labels: 32 | objective: 90 33 | objective_reversed: 10 34 | - name: grpc-latency-percentile-99 35 | objective: 99 36 | description: '"grpc" 99 percentile Latency SLO for grade "A"' 37 | labels: 38 | actual_grade: A 39 | target_grade: A 40 | actual_le: 0.4 41 | target_le: 0.4 42 | sli: 43 | events: 44 | error_query: 1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[{{.window}}]))) 45 | total_query: vector(1)[{{.window}}] 46 | alerting: 47 | name: slo_foo-bar_grpc-latency-percentile-99_fail 48 | labels: 49 | objective: 99 50 | objective_reversed: 1 -------------------------------------------------------------------------------- /pkg/common/conventions/slo.go: -------------------------------------------------------------------------------- 1 | package conventions 2 | 3 | import "github.com/slok/sloth/pkg/common/model" 4 | 5 | // Prometheus metrics conventions. 6 | const ( 7 | // Metrics SLI. 8 | PromSLIErrorMetric = "slo:sli_error:ratio_rate" 9 | PromSLIErrorMetricFmt = PromSLIErrorMetric + "%s" 10 | 11 | // Metrics meta. 12 | PromMetaSLOObjectiveRatioMetric = "slo:objective:ratio" 13 | PromMetaSLOErrorBudgetRatioMetric = "slo:error_budget:ratio" 14 | PromMetaSLOTimePeriodDaysMetric = "slo:time_period:days" 15 | PromMetaSLOCurrentBurnRateRatioMetric = "slo:current_burn_rate:ratio" 16 | PromMetaSLOPeriodBurnRateRatioMetric = "slo:period_burn_rate:ratio" 17 | PromMetaSLOPeriodErrorBudgetRemainingRatioMetric = "slo:period_error_budget_remaining:ratio" 18 | PromMetaSLOInfoMetric = "sloth_slo_info" 19 | 20 | // Labels. 21 | PromSLONameLabelName = "sloth_slo" 22 | PromSLOIDLabelName = "sloth_id" 23 | PromSLOServiceLabelName = "sloth_service" 24 | PromSLOWindowLabelName = "sloth_window" 25 | PromSLOSeverityLabelName = "sloth_severity" 26 | PromSLOVersionLabelName = "sloth_version" 27 | PromSLOModeLabelName = "sloth_mode" 28 | PromSLOSpecLabelName = "sloth_spec" 29 | PromSLOObjectiveLabelName = "sloth_objective" 30 | ) 31 | 32 | // GetSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify 33 | // an SLO recorded metrics and alerts. 34 | func GetSLOIDPromLabels(s model.PromSLO) map[string]string { 35 | return map[string]string{ 36 | PromSLOIDLabelName: s.ID, 37 | PromSLONameLabelName: s.Name, 38 | PromSLOServiceLabelName: s.Service, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/lib/pkg_example_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | sloth "github.com/slok/sloth/pkg/lib" 9 | ) 10 | 11 | // This example shows a basic usage of sloth library by exposing sloth SLO generation functionality as a rest HTTP API. 12 | func Example() { 13 | // Check with `curl -XPOST http://127.0.0.1:8080/sloth/generate -d "$(cat ./examples/getting-started.yml)"`. 14 | 15 | gen, err := sloth.NewPrometheusSLOGenerator(sloth.PrometheusSLOGeneratorConfig{ 16 | ExtraLabels: map[string]string{"source": "slothlib-example"}, 17 | }) 18 | if err != nil { 19 | panic(fmt.Errorf("could not create SLO generator: %w", err)) 20 | } 21 | 22 | mux := http.NewServeMux() 23 | mux.HandleFunc("POST /sloth/generate", func(w http.ResponseWriter, r *http.Request) { 24 | // Get body from request. 25 | body, err := io.ReadAll(r.Body) 26 | if err != nil { 27 | http.Error(w, "failed to read request body", http.StatusBadRequest) 28 | return 29 | } 30 | defer r.Body.Close() 31 | 32 | // Generate SLOs. 33 | result, err := gen.GenerateFromRaw(r.Context(), body) 34 | if err != nil { 35 | http.Error(w, fmt.Sprintf("could not generate SLOs: %v", err), http.StatusInternalServerError) 36 | return 37 | } 38 | w.WriteHeader(http.StatusOK) 39 | err = gen.WriteResultAsPrometheusStd(r.Context(), *result, w) 40 | if err != nil { 41 | http.Error(w, fmt.Sprintf("could not write result: %v", err), http.StatusInternalServerError) 42 | return 43 | } 44 | }) 45 | 46 | httpServer := &http.Server{Addr: ":8080", Handler: mux} 47 | 48 | fmt.Println("Starting server at :8080") 49 | 50 | err = httpServer.ListenAndServe() 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/raw-home-wifi.yml: -------------------------------------------------------------------------------- 1 | # This example shows another less accurate or simpler way of creating the home wifi SLO. 2 | # 3 | # The metrics already give us a metric in ratio for each wifi connection satisfaction, instead of getting 4 | # good and bad events as connection with a minimum satisfaction ratio, we will calculate the averate of all 5 | # ratio satisfaction connections over the time window. 6 | # So we can't use the `events` SLI because we are not going to divide bad and total events. 7 | # 8 | # - `wifi-client-satisfaction` 9 | # - This SLO warn us that we have an average wifi connection satisfaction. 10 | # - SLI error: Calculated internally by ubiquitis metrics, we use directly the ratio. 11 | # - SLO objective (95%): We allow the average wifi connection satisfaction is >=95% 12 | # 13 | # `sloth generate -i ./examples/raw-home-wifi.yml` 14 | # 15 | version: "prometheus/v1" 16 | service: "home-wifi" 17 | labels: 18 | cluster: "valhalla" 19 | component: "ubiquiti" 20 | context: "home" 21 | slos: 22 | - name: "wifi-client-satisfaction" 23 | objective: 95 24 | description: "Warn us that we have an average wifi connection satisfaction." 25 | sli: 26 | raw: 27 | # Get the averate satisfaction ratio and rest 1 (max good) to get the error ratio. 28 | error_ratio_query: | 29 | 1 - ( 30 | sum(sum_over_time(unifipoller_client_satisfaction_ratio[{{.window}}])) 31 | / 32 | sum(count_over_time(unifipoller_client_satisfaction_ratio[{{.window}}])) 33 | ) 34 | alerting: 35 | name: WifiClientSatisfaction 36 | page_alert: 37 | labels: 38 | severity: home 39 | ticket_alert: 40 | labels: 41 | severity: warning 42 | -------------------------------------------------------------------------------- /internal/storage/k8s/dry_run.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/watch" 8 | 9 | "github.com/slok/sloth/internal/log" 10 | "github.com/slok/sloth/pkg/common/model" 11 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 12 | ) 13 | 14 | type DryRunApiserverRepository struct { 15 | svc ApiserverRepository 16 | logger log.Logger 17 | } 18 | 19 | // NewDryRunApiserverRepository returns a new Kubernetes Service that will dry-run that will only do real ReadOnly operations. 20 | func NewDryRunApiserverRepository(svc ApiserverRepository, logger log.Logger) DryRunApiserverRepository { 21 | return DryRunApiserverRepository{ 22 | svc: svc, 23 | logger: logger.WithValues(log.Kv{"service": "storage.k8s.DryRunApiserverRepository"}), 24 | } 25 | } 26 | 27 | func (r DryRunApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { 28 | return r.svc.ListPrometheusServiceLevels(ctx, ns, opts) 29 | } 30 | 31 | func (r DryRunApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { 32 | return r.svc.WatchPrometheusServiceLevels(ctx, ns, opts) 33 | } 34 | 35 | func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { 36 | r.logger.Infof("Dry run EnsurePrometheusServiceLevelStatus") 37 | return nil 38 | } 39 | 40 | func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { 41 | r.logger.Infof("Dry run StoreSLOs") 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/http/backend/storage/prometheus/cli.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" 8 | "github.com/prometheus/common/model" 9 | "github.com/slok/sloth/internal/http/backend/metrics" 10 | ) 11 | 12 | // PrometheusAPIClient is an interface that defines the methods we use from the Prometheus client. 13 | // We define it so we can add flexibility like easily mocking in tests or wrap it for functionality. 14 | type PrometheusAPIClient interface { 15 | prometheusv1.API 16 | } 17 | 18 | func NewMeasuredPrometheusAPIClient(metricsRecorder metrics.Recorder, promcli PrometheusAPIClient) PrometheusAPIClient { 19 | return measuredPrometheusAPIClient{ 20 | PrometheusAPIClient: promcli, 21 | metricsRecorder: metricsRecorder, 22 | } 23 | } 24 | 25 | type measuredPrometheusAPIClient struct { 26 | PrometheusAPIClient 27 | metricsRecorder metrics.Recorder 28 | } 29 | 30 | func (m measuredPrometheusAPIClient) Query(ctx context.Context, query string, ts time.Time, opts ...prometheusv1.Option) (v model.Value, w prometheusv1.Warnings, err error) { 31 | start := time.Now() 32 | defer func() { 33 | m.metricsRecorder.MeasurePrometheusAPIClientOperation(ctx, "Query", time.Since(start), err) 34 | }() 35 | return m.PrometheusAPIClient.Query(ctx, query, ts, opts...) 36 | } 37 | 38 | func (m measuredPrometheusAPIClient) QueryRange(ctx context.Context, query string, r prometheusv1.Range, opts ...prometheusv1.Option) (v model.Value, w prometheusv1.Warnings, err error) { 39 | start := time.Now() 40 | defer func() { 41 | m.metricsRecorder.MeasurePrometheusAPIClientOperation(ctx, "QueryRange", time.Since(start), err) 42 | }() 43 | return m.PrometheusAPIClient.QueryRange(ctx, query, r, opts...) 44 | } 45 | -------------------------------------------------------------------------------- /internal/http/ui/templates/app/slo/comp_stats.tmpl: -------------------------------------------------------------------------------- 1 | {{define "app_slo_comp_stats"}} 2 | 3 |
7 | 8 |
9 |
10 | Current Burning budget 11 | 12 | 13 | 14 |
15 |
{{.Data.SLOData.BurningBudgetPercent | prettyPercent}}
16 |
17 |
18 |
19 | Remaining budget on period (Window) 20 | 21 | 22 | 23 |
24 |
{{.Data.SLOData.RemainingBudgetWindowPercent | prettyPercent}}
25 |
26 |
Warning Alert
27 | {{ if .Data.SLOData.WarningAlertName }} 28 |
FIRING
29 | {{ else }} 30 |
OK
31 | {{ end }} 32 |
33 |
Critical Alert
34 | {{ if .Data.SLOData.CriticalAlertName }} 35 |
FIRING
36 | {{ else }} 37 |
OK
38 | {{ end }} 39 |
40 |
41 | {{ end }} -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 7 | slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" 8 | typedslothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" 9 | gentype "k8s.io/client-go/gentype" 10 | ) 11 | 12 | // fakePrometheusServiceLevels implements PrometheusServiceLevelInterface 13 | type fakePrometheusServiceLevels struct { 14 | *gentype.FakeClientWithListAndApply[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList, *slothv1.PrometheusServiceLevelApplyConfiguration] 15 | Fake *FakeSlothV1 16 | } 17 | 18 | func newFakePrometheusServiceLevels(fake *FakeSlothV1, namespace string) typedslothv1.PrometheusServiceLevelInterface { 19 | return &fakePrometheusServiceLevels{ 20 | gentype.NewFakeClientWithListAndApply[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList, *slothv1.PrometheusServiceLevelApplyConfiguration]( 21 | fake.Fake, 22 | namespace, 23 | v1.SchemeGroupVersion.WithResource("prometheusservicelevels"), 24 | v1.SchemeGroupVersion.WithKind("PrometheusServiceLevel"), 25 | func() *v1.PrometheusServiceLevel { return &v1.PrometheusServiceLevel{} }, 26 | func() *v1.PrometheusServiceLevelList { return &v1.PrometheusServiceLevelList{} }, 27 | func(dst, src *v1.PrometheusServiceLevelList) { dst.ListMeta = src.ListMeta }, 28 | func(list *v1.PrometheusServiceLevelList) []*v1.PrometheusServiceLevel { 29 | return gentype.ToPointerSlice(list.Items) 30 | }, 31 | func(list *v1.PrometheusServiceLevelList, items []*v1.PrometheusServiceLevel) { 32 | list.Items = gentype.FromPointerSlice(items) 33 | }, 34 | ), 35 | fake, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/common/utils/k8s/k8s.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | "github.com/slok/sloth/pkg/common/model" 9 | promutils "github.com/slok/sloth/pkg/common/utils/prometheus" 10 | ) 11 | 12 | // UnstructuredToYAMLString converts an unstructured map to a YAML string. 13 | // This is useful for creating YAML content in ConfigMap data fields or similar use cases. 14 | func UnstructuredToYAMLString(data any) (string, error) { 15 | yamlBytes, err := yaml.Marshal(data) 16 | if err != nil { 17 | return "", fmt.Errorf("could not marshal to YAML: %w", err) 18 | } 19 | return string(yamlBytes), nil 20 | } 21 | 22 | // PromRuleGroupToUnstructuredPromOperator transforms a Prometheus rule group to a PromOperator unstructured rule map. 23 | func PromRuleGroupToUnstructuredPromOperator(p model.PromRuleGroup) map[string]any { 24 | rules := []any{} // Be aware, unstructured wants []any not []map[string]any. 25 | for _, rule := range p.Rules { 26 | r := map[string]any{ 27 | "expr": rule.Expr, 28 | } 29 | 30 | if rule.Record != "" { 31 | r["record"] = rule.Record 32 | } 33 | 34 | if rule.Alert != "" { 35 | r["alert"] = rule.Alert 36 | } 37 | 38 | if len(rule.Labels) > 0 { 39 | r["labels"] = mapStringStringToMapStringAny(rule.Labels) 40 | } 41 | if len(rule.Annotations) > 0 { 42 | r["annotations"] = mapStringStringToMapStringAny(rule.Annotations) 43 | } 44 | 45 | rules = append(rules, r) 46 | } 47 | r := map[string]any{ 48 | "name": p.Name, 49 | "rules": rules, 50 | } 51 | if p.Interval != 0 { 52 | r["interval"] = promutils.TimeDurationToPromStr(p.Interval) 53 | } 54 | 55 | return r 56 | } 57 | 58 | func mapStringStringToMapStringAny(in map[string]string) map[string]any { 59 | out := make(map[string]any) 60 | for k, v := range in { 61 | out[k] = v 62 | } 63 | return out 64 | } 65 | -------------------------------------------------------------------------------- /internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/contrib/denominator_corrected_rules/v1 2 | 3 | ## High level explanation 4 | 5 | Plugin ported from [#459 Sloth PR][PR]. Full details are in the PR. 6 | 7 | **Note:** This plugin replaces all SLI recording rules and adds new metadata rules. 8 | 9 | This plugin adjusts SLOs for services with seasonal traffic patterns (for example: high traffic during the day, very low traffic at night). 10 | Normally, SLOs treat all burn rates the same. But during low-traffic periods, even a few failed requests can cause false alerts and pages. 11 | 12 | To fix this, the plugin applies a correction factor based on request volume. The burn rate impact scales with traffic levels, higher traffic means failures weigh more, and lower traffic means they weigh less. 13 | 14 | If your service experiences low request volumes at certain times and you see noisy alerts, this plugin can help. 15 | 16 | More details in the original [PR]. 17 | 18 | ## Config 19 | 20 | - `disableOptimized`(**Optional**, `bool`): If `true`, disables optimized rule generation for long SLI windows. Optimized rules use short-window recording rules to derive long-window SLIs with lower Prometheus resource usage, at the cost of reduced accuracy. Defaults to `false`. 21 | 22 | ## Env vars 23 | 24 | None 25 | 26 | ## Order requirement 27 | 28 | This plugin should run after rule generation plugins. 29 | 30 | ## Usage examples 31 | 32 | ### Regular usage 33 | 34 | ```yaml 35 | sloPlugins: 36 | chain: 37 | - id: "sloth.dev/contrib/denominator_corrected_rules/v1" 38 | ``` 39 | 40 | ### Disable optimization 41 | 42 | ```yaml 43 | sloPlugins: 44 | chain: 45 | - id: "sloth.dev/contrib/denominator_corrected_rules/v1" 46 | config: 47 | disableOptimized: true 48 | ``` 49 | 50 | 51 | [PR]: https://github.com/slok/sloth/pull/459 -------------------------------------------------------------------------------- /pkg/prometheus/plugin/slo/v1/testing/testing.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/slok/sloth/internal/log" 10 | pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" 11 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 12 | ) 13 | 14 | type TestPluginConfig struct { 15 | PluginFilePath string 16 | PluginConfiguration json.RawMessage 17 | } 18 | 19 | func (c *TestPluginConfig) defaults() error { 20 | if c.PluginFilePath == "" { 21 | c.PluginFilePath = "./plugin.go" 22 | } 23 | 24 | if c.PluginConfiguration == nil { 25 | c.PluginConfiguration = []byte("{}") 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // NewTestPlugin is a helper util to load a plugin using the engine that 32 | // will use Sloth. In the sense of an acceptance/integration test. 33 | // 34 | // This has benefits over loading the plugin directly with Go, by using this method 35 | // you will be sure that what is executed is what the sloth will execute at runtime, 36 | // so, if you use a not supported feature or the engine has a bug, this will be 37 | // detected on the tests instead of Sloth runtime on execution. 38 | func NewTestPlugin(ctx context.Context, config TestPluginConfig) (pluginslov1.Plugin, error) { 39 | err := config.defaults() 40 | if err != nil { 41 | return nil, fmt.Errorf("invalid configuration: %w", err) 42 | } 43 | 44 | pluginSource, err := os.ReadFile(config.PluginFilePath) 45 | if err != nil { 46 | return nil, fmt.Errorf("could not read plugin source code: %w", err) 47 | } 48 | plugin, err := pluginengineslo.PluginLoader.LoadRawPlugin(ctx, string(pluginSource)) 49 | if err != nil { 50 | return nil, fmt.Errorf("could not load plugin source code: %w", err) 51 | } 52 | 53 | return plugin.PluginV1Factory(config.PluginConfiguration, pluginslov1.AppUtils{ 54 | Logger: log.Noop, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /internal/http/backend/model/model_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSLOGroupLabelsIDMarshal(t *testing.T) { 10 | tests := map[string]struct { 11 | sloID string 12 | labels map[string]string 13 | expID string 14 | }{ 15 | "Marshalling grouped labels into the SLO ID should be marshaled correctly.": { 16 | sloID: "test1", 17 | labels: map[string]string{ 18 | "k1": "v1", 19 | "k2": "v2", 20 | }, 21 | expID: "test1:azE9djEsazI9djI=", 22 | }, 23 | } 24 | 25 | for name, tc := range tests { 26 | t.Run(name, func(t *testing.T) { 27 | assert := assert.New(t) 28 | 29 | id := SLOGroupLabelsIDMarshal(tc.sloID, tc.labels) 30 | assert.Equal(tc.expID, id) 31 | }) 32 | } 33 | } 34 | 35 | func TestSLOGroupLabelsIDUnmarshal(t *testing.T) { 36 | tests := map[string]struct { 37 | id string 38 | expSLOID string 39 | expLabels map[string]string 40 | expErr bool 41 | }{ 42 | "A id with labels should return the information.": { 43 | id: "test1:azE9djEsazI9djI=", 44 | expSLOID: "test1", 45 | expLabels: map[string]string{ 46 | "k1": "v1", 47 | "k2": "v2", 48 | }, 49 | expErr: false, 50 | }, 51 | 52 | "A id without labels should return the information.": { 53 | id: "test1", 54 | expSLOID: "test1", 55 | expErr: false, 56 | }, 57 | 58 | "A id with incorrect labels should fail.": { 59 | id: "test1:dsadasdasdsa", 60 | expErr: true, 61 | }, 62 | } 63 | 64 | for name, test := range tests { 65 | t.Run(name, func(t *testing.T) { 66 | assert := assert.New(t) 67 | 68 | sloID, labels, err := SLOGroupLabelsIDUnmarshal(test.id) 69 | if test.expErr { 70 | assert.Error(err) 71 | } else if assert.NoError(err) { 72 | assert.Equal(test.expSLOID, sloID) 73 | assert.Equal(test.expLabels, labels) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliplugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SLIPluginApplyConfiguration represents a declarative configuration of the SLIPlugin type for use 6 | // with apply. 7 | // 8 | // SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. 9 | type SLIPluginApplyConfiguration struct { 10 | // Name is the name of the plugin that needs to load. 11 | ID *string `json:"id,omitempty"` 12 | // Options are the options used for the plugin. 13 | Options map[string]string `json:"options,omitempty"` 14 | } 15 | 16 | // SLIPluginApplyConfiguration constructs a declarative configuration of the SLIPlugin type for use with 17 | // apply. 18 | func SLIPlugin() *SLIPluginApplyConfiguration { 19 | return &SLIPluginApplyConfiguration{} 20 | } 21 | 22 | // WithID sets the ID field in the declarative configuration to the given value 23 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 24 | // If called multiple times, the ID field is set to the value of the last call. 25 | func (b *SLIPluginApplyConfiguration) WithID(value string) *SLIPluginApplyConfiguration { 26 | b.ID = &value 27 | return b 28 | } 29 | 30 | // WithOptions puts the entries into the Options field in the declarative configuration 31 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 32 | // If called multiple times, the entries provided by each call will be put on the Options field, 33 | // overwriting an existing map entries in Options field with the same key. 34 | func (b *SLIPluginApplyConfiguration) WithOptions(entries map[string]string) *SLIPluginApplyConfiguration { 35 | if b.Options == nil && len(entries) > 0 { 36 | b.Options = make(map[string]string, len(entries)) 37 | } 38 | for k, v := range entries { 39 | b.Options[k] = v 40 | } 41 | return b 42 | } 43 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: sloth-test 7 | namespace: custom 8 | labels: 9 | helm.sh/chart: sloth- 10 | app.kubernetes.io/managed-by: Helm 11 | app: sloth 12 | app.kubernetes.io/name: sloth 13 | app.kubernetes.io/instance: test 14 | label-from: test 15 | spec: 16 | replicas: 1 17 | selector: 18 | matchLabels: 19 | app: sloth 20 | app.kubernetes.io/name: sloth 21 | app.kubernetes.io/instance: test 22 | template: 23 | metadata: 24 | labels: 25 | helm.sh/chart: sloth- 26 | app.kubernetes.io/managed-by: Helm 27 | app: sloth 28 | app.kubernetes.io/name: sloth 29 | app.kubernetes.io/instance: test 30 | label-from: test 31 | annotations: 32 | kubectl.kubernetes.io/default-container: sloth 33 | spec: 34 | serviceAccountName: sloth-test 35 | securityContext: 36 | fsGroup: 100 37 | runAsGroup: 1000 38 | runAsNonRoot: true 39 | runAsUser: 100 40 | nodeSelector: 41 | k1: v1 42 | k2: v2 43 | containers: 44 | - name: sloth 45 | image: slok/sloth-test:v1.42.42 46 | args: 47 | - kubernetes-controller 48 | - --resync-interval=17m 49 | - --workers=99 50 | - --namespace=somens 51 | - --label-selector=x=y,z!=y 52 | - --extra-labels=k1=v1 53 | - --extra-labels=k2=v2 54 | - --logger=default 55 | securityContext: 56 | allowPrivilegeEscalation: false 57 | resources: 58 | limits: 59 | cpu: 50m 60 | memory: 150Mi 61 | requests: 62 | cpu: 5m 63 | memory: 75Mi 64 | -------------------------------------------------------------------------------- /test/integration/prometheus/plugins/sli/plugin1/plugin.go: -------------------------------------------------------------------------------- 1 | package availability 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | const ( 13 | SLIPluginVersion = "prometheus/v1" 14 | SLIPluginID = "integration_test" 15 | ) 16 | 17 | var tpl = template.Must(template.New("").Parse(` 18 | sum(rate(integration_test{ {{.filter}}job="{{.job}}",code=~"(5..|429)" }[{{"{{.window}}"}}])) 19 | / 20 | sum(rate(integration_test{ {{.filter}}job="{{.job}}" }[{{"{{.window}}"}}]))`)) 21 | 22 | var filterRegex = regexp.MustCompile(`([^=]+="[^=,"]+",)+`) 23 | 24 | func SLIPlugin(ctx context.Context, meta, labels, options map[string]string) (string, error) { 25 | // Get job. 26 | job, ok := options["job"] 27 | if !ok { 28 | return "", fmt.Errorf("job options is required") 29 | } 30 | 31 | // Validate labels. 32 | err := validateLabels(labels, "owner", "tier") 33 | if err != nil { 34 | return "", fmt.Errorf("invalid labels: %w", err) 35 | } 36 | 37 | // Sanitize filter. 38 | filter := options["filter"] 39 | if filter != "" { 40 | filter = strings.Trim(filter, "{}") 41 | filter = strings.Trim(filter, ",") 42 | filter = filter + "," 43 | match := filterRegex.MatchString(filter) 44 | if !match { 45 | return "", fmt.Errorf("invalid prometheus filter: %s", filter) 46 | } 47 | } 48 | 49 | // Create query. 50 | var b bytes.Buffer 51 | data := map[string]string{ 52 | "job": job, 53 | "filter": filter, 54 | } 55 | err = tpl.Execute(&b, data) 56 | if err != nil { 57 | return "", fmt.Errorf("could not execute template: %w", err) 58 | } 59 | 60 | return b.String(), nil 61 | } 62 | 63 | func validateLabels(labels map[string]string, requiredKeys ...string) error { 64 | for _, k := range requiredKeys { 65 | v, ok := labels[k] 66 | if !ok || (ok && v == "") { 67 | return fmt.Errorf("%q label is required", k) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /examples/k8s-home-wifi.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as home-wifi.yml but using Sloth Kubernetes CRD. 2 | # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. 3 | # 4 | # `sloth generate -i ./examples/k8s-home-wifi.yml` 5 | # 6 | apiVersion: sloth.slok.dev/v1 7 | kind: PrometheusServiceLevel 8 | metadata: 9 | name: sloth-slo-home-wifi 10 | namespace: monitoring 11 | labels: 12 | prometheus: prometheus 13 | role: alert-rules 14 | app: sloth 15 | spec: 16 | service: "home-wifi" 17 | labels: 18 | cluster: "valhalla" 19 | component: "ubiquiti" 20 | context: "home" 21 | slos: 22 | - name: "good-wifi-client-satisfaction" 23 | objective: 95 24 | description: "Will warn us that we don't have a good wifi at home." 25 | sli: 26 | events: 27 | errorQuery: sum_over_time((count(unifipoller_client_satisfaction_ratio < 0.75))[{{.window}}:]) OR on() vector(0) 28 | totalQuery: sum_over_time((count(unifipoller_client_satisfaction_ratio))[{{.window}}:]) 29 | alerting: 30 | name: GoodWifiClientSatisfaction 31 | pageAlert: 32 | labels: 33 | severity: home 34 | ticketAlert: 35 | labels: 36 | severity: warning 37 | 38 | - name: "risk-wifi-client-satisfaction" 39 | objective: 99.9 40 | description: "Will warn us that we something very bad is happenning with our home wifi." 41 | sli: 42 | events: 43 | errorQuery: sum_over_time((count(unifipoller_client_satisfaction_ratio < 0.5))[{{.window}}:]) OR on() vector(0) 44 | totalQuery: sum_over_time((count(unifipoller_client_satisfaction_ratio))[{{.window}}:]) 45 | alerting: 46 | name: RiskWifiClientSatisfaction 47 | pageAlert: 48 | labels: 49 | severity: home 50 | ticketAlert: 51 | labels: 52 | severity: warning 53 | -------------------------------------------------------------------------------- /pkg/prometheus/alertwindows/v1/v1.go: -------------------------------------------------------------------------------- 1 | // Package v1 2 | 3 | package v1 4 | 5 | import prometheusmodel "github.com/prometheus/common/model" 6 | 7 | const Kind = "AlertWindows" 8 | const APIVersion = "sloth.slok.dev/v1" 9 | 10 | //go:generate gomarkdoc -o ./README.md ./ 11 | 12 | type AlertWindows struct { 13 | Kind string `yaml:"kind"` 14 | APIVersion string `yaml:"apiVersion"` 15 | Spec Spec `yaml:"spec"` 16 | } 17 | 18 | // Spec represents the root type of the Alerting window. 19 | type Spec struct { 20 | // SLOPeriod is the full slo period used for this windows. 21 | SLOPeriod prometheusmodel.Duration `yaml:"sloPeriod"` 22 | // Page represents the configuration for the page alerting windows. 23 | Page PageWindow `yaml:"page"` 24 | // Ticket represents the configuration for the ticket alerting windows. 25 | Ticket TicketWindow `yaml:"ticket"` 26 | } 27 | 28 | // PageWindow represents the configuration for page alerting. 29 | type PageWindow struct { 30 | QuickSlowWindow `yaml:",inline"` 31 | } 32 | 33 | // PageWindow represents the configuration for ticket alerting. 34 | type TicketWindow struct { 35 | QuickSlowWindow `yaml:",inline"` 36 | } 37 | 38 | type QuickSlowWindow struct { 39 | // Quick represents the windows for the quick alerting trigger. 40 | Quick Window `yaml:"quick"` 41 | // Slow represents the windows for the slow alerting trigger. 42 | Slow Window `yaml:"slow"` 43 | } 44 | 45 | type Window struct { 46 | // ErrorBudgetPercent is the max error budget consumption allowed in the window. 47 | ErrorBudgetPercent float64 `yaml:"errorBudgetPercent"` 48 | // Shortwindow is the window that will stop the alerts when a huge amount of 49 | // error budget has been consumed but the error has already gone. 50 | ShortWindow prometheusmodel.Duration `yaml:"shortWindow"` 51 | // Longwindow is the window used to get the error budget for all the window. 52 | LongWindow prometheusmodel.Duration `yaml:"longWindow"` 53 | } 54 | -------------------------------------------------------------------------------- /test/integration/k8scontroller/plugins/sli/plugin1/plugin.go: -------------------------------------------------------------------------------- 1 | package availability 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | const ( 13 | SLIPluginVersion = "prometheus/v1" 14 | SLIPluginID = "integration_test" 15 | ) 16 | 17 | var tpl = template.Must(template.New("").Parse(` 18 | sum(rate(integration_test{ {{.filter}}job="{{.job}}",code=~"(5..|429)" }[{{"{{.window}}"}}])) 19 | / 20 | sum(rate(integration_test{ {{.filter}}job="{{.job}}" }[{{"{{.window}}"}}]))`)) 21 | 22 | var filterRegex = regexp.MustCompile(`([^=]+="[^=,"]+",)+`) 23 | 24 | func SLIPlugin(ctx context.Context, meta, labels, options map[string]string) (string, error) { 25 | // Get job. 26 | job, ok := options["job"] 27 | if !ok { 28 | return "", fmt.Errorf("job options is required") 29 | } 30 | 31 | // Validate labels. 32 | err := validateLabels(labels, "owner", "tier") 33 | if err != nil { 34 | return "", fmt.Errorf("invalid labels: %w", err) 35 | } 36 | 37 | // Sanitize filter. 38 | filter := options["filter"] 39 | if filter != "" { 40 | filter = strings.Trim(filter, "{}") 41 | filter = strings.Trim(filter, ",") 42 | filter = filter + "," 43 | match := filterRegex.MatchString(filter) 44 | if !match { 45 | return "", fmt.Errorf("invalid prometheus filter: %s", filter) 46 | } 47 | } 48 | 49 | // Create query. 50 | var b bytes.Buffer 51 | data := map[string]string{ 52 | "job": job, 53 | "filter": filter, 54 | } 55 | err = tpl.Execute(&b, data) 56 | if err != nil { 57 | return "", fmt.Errorf("could not execute template: %w", err) 58 | } 59 | 60 | return b.String(), nil 61 | } 62 | 63 | func validateLabels(labels map[string]string, requiredKeys ...string) error { 64 | for _, k := range requiredKeys { 65 | v, ok := labels[k] 66 | if !ok || (ok && v == "") { 67 | return fmt.Errorf("%q label is required", k) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/lib/bench_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/slok/sloth/pkg/lib" 9 | ) 10 | 11 | func BenchmarkLibGenerateAndWrite(b *testing.B) { 12 | const sloSpec = ` 13 | --- 14 | version: "prometheus/v1" 15 | service: "myservice" 16 | labels: 17 | owner: "myteam" 18 | repo: "myorg/myservice" 19 | tier: "2" 20 | slos: 21 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 22 | - name: "requests-availability" 23 | objective: 99.9 24 | description: "Common SLO based on availability for HTTP request responses." 25 | labels: 26 | category: availability 27 | sli: 28 | events: 29 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 30 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 31 | alerting: 32 | name: "MyServiceHighErrorRate" 33 | labels: 34 | category: "availability" 35 | annotations: 36 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 37 | summary: "High error rate on 'myservice' requests responses" 38 | page_alert: 39 | labels: 40 | severity: "pageteam" 41 | routing_key: "myteam" 42 | ticket_alert: 43 | labels: 44 | severity: "slack" 45 | slack_channel: "#alerts-myteam" 46 | ` 47 | 48 | gen, err := lib.NewPrometheusSLOGenerator(lib.PrometheusSLOGeneratorConfig{ 49 | ExtraLabels: map[string]string{"source": "slothlib-example"}, 50 | }) 51 | if err != nil { 52 | b.Fatal(err) 53 | } 54 | 55 | for b.Loop() { 56 | ctx := context.Background() 57 | 58 | slo, err := gen.GenerateFromRaw(ctx, []byte(sloSpec)) 59 | if err != nil { 60 | b.Fatal(err) 61 | } 62 | 63 | err = gen.WriteResultAsPrometheusStd(ctx, *slo, io.Discard) 64 | if err != nil { 65 | b.Fatal(err) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-caarlos0-env-v11.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/caarlos0/env/v11'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/caarlos0/env/v11" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/caarlos0/env/v11/env"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "GetFieldParams": reflect.ValueOf(env.GetFieldParams), 14 | "GetFieldParamsWithOptions": reflect.ValueOf(env.GetFieldParamsWithOptions), 15 | "Parse": reflect.ValueOf(env.Parse), 16 | "ParseWithOptions": reflect.ValueOf(env.ParseWithOptions), 17 | "ToMap": reflect.ValueOf(env.ToMap), 18 | 19 | // type definitions 20 | "AggregateError": reflect.ValueOf((*env.AggregateError)(nil)), 21 | "EmptyEnvVarError": reflect.ValueOf((*env.EmptyEnvVarError)(nil)), 22 | "EmptyVarError": reflect.ValueOf((*env.EmptyVarError)(nil)), 23 | "EnvVarIsNotSetError": reflect.ValueOf((*env.EnvVarIsNotSetError)(nil)), 24 | "FieldParams": reflect.ValueOf((*env.FieldParams)(nil)), 25 | "LoadFileContentError": reflect.ValueOf((*env.LoadFileContentError)(nil)), 26 | "NoParserError": reflect.ValueOf((*env.NoParserError)(nil)), 27 | "NoSupportedTagOptionError": reflect.ValueOf((*env.NoSupportedTagOptionError)(nil)), 28 | "NotStructPtrError": reflect.ValueOf((*env.NotStructPtrError)(nil)), 29 | "OnSetFn": reflect.ValueOf((*env.OnSetFn)(nil)), 30 | "Options": reflect.ValueOf((*env.Options)(nil)), 31 | "ParseError": reflect.ValueOf((*env.ParseError)(nil)), 32 | "ParseValueError": reflect.ValueOf((*env.ParseValueError)(nil)), 33 | "ParserFunc": reflect.ValueOf((*env.ParserFunc)(nil)), 34 | "VarIsNotSetError": reflect.ValueOf((*env.VarIsNotSetError)(nil)), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/github_com-caarlos0-env-v11.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/caarlos0/env/v11'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/caarlos0/env/v11" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/caarlos0/env/v11/env"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "GetFieldParams": reflect.ValueOf(env.GetFieldParams), 14 | "GetFieldParamsWithOptions": reflect.ValueOf(env.GetFieldParamsWithOptions), 15 | "Parse": reflect.ValueOf(env.Parse), 16 | "ParseWithOptions": reflect.ValueOf(env.ParseWithOptions), 17 | "ToMap": reflect.ValueOf(env.ToMap), 18 | 19 | // type definitions 20 | "AggregateError": reflect.ValueOf((*env.AggregateError)(nil)), 21 | "EmptyEnvVarError": reflect.ValueOf((*env.EmptyEnvVarError)(nil)), 22 | "EmptyVarError": reflect.ValueOf((*env.EmptyVarError)(nil)), 23 | "EnvVarIsNotSetError": reflect.ValueOf((*env.EnvVarIsNotSetError)(nil)), 24 | "FieldParams": reflect.ValueOf((*env.FieldParams)(nil)), 25 | "LoadFileContentError": reflect.ValueOf((*env.LoadFileContentError)(nil)), 26 | "NoParserError": reflect.ValueOf((*env.NoParserError)(nil)), 27 | "NoSupportedTagOptionError": reflect.ValueOf((*env.NoSupportedTagOptionError)(nil)), 28 | "NotStructPtrError": reflect.ValueOf((*env.NotStructPtrError)(nil)), 29 | "OnSetFn": reflect.ValueOf((*env.OnSetFn)(nil)), 30 | "Options": reflect.ValueOf((*env.Options)(nil)), 31 | "ParseError": reflect.ValueOf((*env.ParseError)(nil)), 32 | "ParseValueError": reflect.ValueOf((*env.ParseValueError)(nil)), 33 | "ParserFunc": reflect.ValueOf((*env.ParserFunc)(nil)), 34 | "VarIsNotSetError": reflect.ValueOf((*env.VarIsNotSetError)(nil)), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/slo-plugin-getting-started.yml: -------------------------------------------------------------------------------- 1 | version: "prometheus/v1" 2 | service: "myservice" 3 | labels: 4 | owner: "myteam" 5 | repo: "myorg/myservice" 6 | tier: "2" 7 | slo_plugins: 8 | chain: 9 | - id: "sloth.dev/core/debug/v1" 10 | priority: 9999999 11 | config: {msg: "Plugin 99"} 12 | - id: "sloth.dev/core/debug/v1" 13 | priority: -999999 14 | config: {msg: "Plugin 0"} 15 | 16 | slos: 17 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 18 | - name: "requests-availability" 19 | objective: 99.9 20 | description: "Common SLO based on availability for HTTP request responses." 21 | plugins: 22 | chain: 23 | - id: "sloth.dev/core/debug/v1" 24 | priority: 1050 25 | config: {msg: "Plugin 5"} 26 | - id: "sloth.dev/core/debug/v1" 27 | priority: -1000 28 | config: {msg: "Plugin 1"} 29 | - id: "sloth.dev/core/debug/v1" 30 | priority: 1000 31 | config: {msg: "Plugin 4"} 32 | - id: "sloth.dev/core/debug/v1" 33 | priority: -200 34 | config: {msg: "Plugin 2"} 35 | - id: "sloth.dev/core/debug/v1" 36 | config: {msg: "Plugin 3"} 37 | 38 | sli: 39 | events: 40 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 41 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 42 | alerting: 43 | name: MyServiceHighErrorRate 44 | labels: 45 | category: "availability" 46 | annotations: 47 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 48 | summary: "High error rate on 'myservice' requests responses" 49 | page_alert: 50 | labels: 51 | severity: pageteam 52 | routing_key: myteam 53 | ticket_alert: 54 | labels: 55 | severity: "slack" 56 | slack_channel: "#alerts-myteam" 57 | -------------------------------------------------------------------------------- /internal/plugin/slo/contrib/rule_intervals_v1/README.md: -------------------------------------------------------------------------------- 1 | # sloth.dev/contrib/rule_intervals/v1 2 | 3 | This plugin sets Prom rule evaluation intervals to the Prometheus generated rules. The intervals can be different depending on the type of rules, SLI, metadata and alerts. A default interval can be set for all rules. 4 | 5 | ## Config 6 | 7 | | Field | Type | Required | Default | Description | 8 | |-------------------|--------------------------|----------|---------|-------------------------------------------------------------------| 9 | | `interval.default` | Prom time duration string | Yes | — | Fallback interval to use if no other interval is set. | 10 | | `interval.sliError`| Prom time duration string | No | — | Evaluation rule interval for generated SLI error rules. | 11 | | `interval.metadata`| Prom time duration string | No | — | Evaluation rule interval for generated metadata rules. | 12 | | `interval.alert` | Prom time duration string | No | — | Evaluation rule interval for generated alert rules. | 13 | 14 | ## Env var 15 | 16 | None 17 | 18 | ## Order requirement 19 | 20 | This plugin must be placed **after** all rule generation. 21 | 22 | ## Usage examples 23 | 24 | ### Specific settings in an SLO group 25 | 26 | ```yaml 27 | slo_plugins: 28 | chain: 29 | - id: sloth.dev/contrib/rule_intervals/v1 30 | config: 31 | interval: 32 | default: 1m 33 | sliError: 42s 34 | metadata: 55s 35 | alert: 11s 36 | ``` 37 | 38 | ### Add a custom default interval to all rules and SLOs 39 | 40 | By adding a default interval at app level, all generated rules by sloth will have that interval 41 | 42 | ```bash 43 | sloth generate \ 44 | -i ./examples/getting-started.yml \ 45 | -s '{"id": "sloth.dev/contrib/rule_intervals/v1","config":{"interval": {"default":"2m"}}}' 46 | ``` 47 | -------------------------------------------------------------------------------- /pkg/common/utils/k8s/k8s_test.go: -------------------------------------------------------------------------------- 1 | package k8s_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/slok/sloth/pkg/common/utils/k8s" 10 | ) 11 | 12 | func TestUnstructuredToYAMLString(t *testing.T) { 13 | tests := map[string]struct { 14 | data map[string]any 15 | expYAML string 16 | expErr bool 17 | }{ 18 | "Empty map should return empty YAML": { 19 | data: map[string]any{}, 20 | expYAML: "{}\n", 21 | }, 22 | "Simple map should be marshaled correctly": { 23 | data: map[string]any{ 24 | "name": "test", 25 | "age": 42, 26 | }, 27 | expYAML: "age: 42\nname: test\n", 28 | }, 29 | "Nested map should be marshaled correctly": { 30 | data: map[string]any{ 31 | "metadata": map[string]any{ 32 | "name": "test-name", 33 | "namespace": "test-ns", 34 | }, 35 | "data": map[string]any{ 36 | "key1": "value1", 37 | "key2": "value2", 38 | }, 39 | }, 40 | expYAML: `data: 41 | key1: value1 42 | key2: value2 43 | metadata: 44 | name: test-name 45 | namespace: test-ns 46 | `, 47 | }, 48 | "Map with slice should be marshaled correctly": { 49 | data: map[string]any{ 50 | "groups": []any{ 51 | map[string]any{ 52 | "name": "group1", 53 | "rules": []any{ 54 | map[string]any{ 55 | "record": "rec1", 56 | "expr": "exp1", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | expYAML: `groups: 63 | - name: group1 64 | rules: 65 | - expr: exp1 66 | record: rec1 67 | `, 68 | }, 69 | } 70 | 71 | for name, test := range tests { 72 | t.Run(name, func(t *testing.T) { 73 | assert := assert.New(t) 74 | require := require.New(t) 75 | 76 | yamlStr, err := k8s.UnstructuredToYAMLString(test.data) 77 | 78 | if test.expErr { 79 | require.Error(err) 80 | } else { 81 | require.NoError(err) 82 | assert.Equal(test.expYAML, yamlStr) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /scripts/build/docker/build-publish-image-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | [ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1; 7 | [ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1; 8 | 9 | # Build and publish images for all architectures. 10 | archs=("amd64" "arm64" "arm" "ppc64le" "s390x") 11 | for arch in "${archs[@]}"; do 12 | ARCH="${arch}" ./scripts/build/docker/build-image.sh 13 | ARCH="${arch}" ./scripts/build/docker/publish-image.sh 14 | done 15 | 16 | IMAGE_TAG="${IMAGE}:${VERSION}" 17 | 18 | # Create manifest to join all arch images under one virtual tag. 19 | MANIFEST="docker manifest create -a ${IMAGE_TAG}" 20 | for arch in "${archs[@]}"; do 21 | MANIFEST="${MANIFEST} ${IMAGE_TAG}-${arch}" 22 | done 23 | eval "${MANIFEST}" 24 | 25 | # Annotate each arch manifest to set which image is build for which CPU architecture. 26 | for arch in "${archs[@]}"; do 27 | docker manifest annotate --arch "${arch}" "${IMAGE_TAG}" "${IMAGE_TAG}-${arch}" 28 | done 29 | 30 | # Push virual tag metadata. 31 | docker manifest push "${IMAGE_TAG}" 32 | 33 | # Same as the regular virtual tag but for `:latest`. 34 | if [ ! -z "${TAG_IMAGE_LATEST:-}" ]; then 35 | IMAGE_TAG_LATEST="${IMAGE}:latest" 36 | 37 | # Clean latest manifest in case there is one. 38 | docker manifest rm ${IMAGE_TAG_LATEST} || true 39 | 40 | # Create manifest to join all arch images under one virtual tag. 41 | MANIFEST_LATEST="docker manifest create -a ${IMAGE_TAG_LATEST}" 42 | for arch in "${archs[@]}"; do 43 | MANIFEST_LATEST="${MANIFEST_LATEST} ${IMAGE_TAG}-${arch}" 44 | done 45 | eval "${MANIFEST_LATEST}" 46 | 47 | # Annotate each arch manifest to set which image is build for which CPU architecture. 48 | for arch in "${archs[@]}"; do 49 | docker manifest annotate --arch "${arch}" "${IMAGE_TAG_LATEST}" "${IMAGE_TAG}-${arch}" 50 | done 51 | 52 | # Push virual tag metadata. 53 | docker manifest push "${IMAGE_TAG_LATEST}" 54 | fi -------------------------------------------------------------------------------- /test/integration/prometheus/validate_test.go: -------------------------------------------------------------------------------- 1 | package prometheus_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/slok/sloth/test/integration/prometheus" 10 | ) 11 | 12 | func TestPrometheusValidate(t *testing.T) { 13 | // Tests config. 14 | config := prometheus.NewConfig(t) 15 | 16 | // Tests. 17 | tests := map[string]struct { 18 | valCmdArgs string 19 | expErr bool 20 | }{ 21 | "Discovery of good specs should validate correctly.": { 22 | valCmdArgs: "--input ./testdata/validate/good", 23 | }, 24 | 25 | "Discovery of bad specs should validate with failures.": { 26 | valCmdArgs: "--input ./testdata/validate/bad", 27 | expErr: true, 28 | }, 29 | 30 | "Discovery of all specs should validate with failures.": { 31 | valCmdArgs: "--input ./testdata/validate", 32 | expErr: true, 33 | }, 34 | 35 | "Discovery of all specs excluding bads should validate correctly.": { 36 | valCmdArgs: "--input ./testdata/validate --fs-exclude bad", 37 | }, 38 | 39 | "Discovery of all specs including only good should validate correctly.": { 40 | valCmdArgs: "--input ./testdata/validate --fs-include good", 41 | }, 42 | 43 | "Discovery of none specs should fail.": { 44 | valCmdArgs: "--input ./testdata/validate --fs-exclude .*", 45 | expErr: true, 46 | }, 47 | 48 | "Discovery of all specs excluding bad and including a bad one should validate correctly because exclude has preference.": { 49 | valCmdArgs: "--input ./testdata/validate --fs-exclude bad --fs-include .*-aa.*", 50 | }, 51 | } 52 | 53 | for name, test := range tests { 54 | t.Run(name, func(t *testing.T) { 55 | assert := assert.New(t) 56 | 57 | // Run with context to stop on test end. 58 | ctx, cancel := context.WithCancel(context.Background()) 59 | defer cancel() 60 | 61 | _, _, err := prometheus.RunSlothValidate(ctx, config, test.valCmdArgs) 62 | 63 | if test.expErr { 64 | assert.Error(err) 65 | } else { 66 | assert.NoError(err) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/prometheus/plugin/slo/v1'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "context" 7 | "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 8 | "go/constant" 9 | "go/token" 10 | "reflect" 11 | ) 12 | 13 | func init() { 14 | Symbols["github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/v1"] = map[string]reflect.Value{ 15 | // function, constant and variable definitions 16 | "PluginFactoryName": reflect.ValueOf(constant.MakeFromLiteral("\"NewPlugin\"", token.STRING, 0)), 17 | "PluginIDName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginID\"", token.STRING, 0)), 18 | "PluginVersionName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginVersion\"", token.STRING, 0)), 19 | "Version": reflect.ValueOf(constant.MakeFromLiteral("\"prometheus/slo/v1\"", token.STRING, 0)), 20 | 21 | // type definitions 22 | "AppUtils": reflect.ValueOf((*v1.AppUtils)(nil)), 23 | "Plugin": reflect.ValueOf((*v1.Plugin)(nil)), 24 | "PluginFactory": reflect.ValueOf((*v1.PluginFactory)(nil)), 25 | "PluginID": reflect.ValueOf((*v1.PluginID)(nil)), 26 | "PluginVersion": reflect.ValueOf((*v1.PluginVersion)(nil)), 27 | "Request": reflect.ValueOf((*v1.Request)(nil)), 28 | "Result": reflect.ValueOf((*v1.Result)(nil)), 29 | 30 | // interface wrapper definitions 31 | "_Plugin": reflect.ValueOf((*_github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin)(nil)), 32 | } 33 | } 34 | 35 | // _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin is an interface wrapper for Plugin type 36 | type _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin struct { 37 | IValue interface{} 38 | WProcessSLO func(ctx context.Context, request *v1.Request, result *v1.Result) error 39 | } 40 | 41 | func (W _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin) ProcessSLO(ctx context.Context, request *v1.Request, result *v1.Result) error { 42 | return W.WProcessSLO(ctx, request, result) 43 | } 44 | -------------------------------------------------------------------------------- /internal/storage/io/k8s_obj.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/serializer/json" 10 | 11 | "github.com/slok/sloth/internal/log" 12 | "github.com/slok/sloth/pkg/common/model" 13 | plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" 14 | ) 15 | 16 | func NewIOWriterK8sObjectYAMLRepo(writer io.Writer, transformer plugink8stransformv1.Plugin, logger log.Logger) IOWriterK8sObjectYAMLRepo { 17 | return IOWriterK8sObjectYAMLRepo{ 18 | writer: writer, 19 | encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), 20 | logger: logger.WithValues(log.Kv{"svc": "storage.io.IOWriterK8sObjectYAMLRepo"}), 21 | transformer: transformer, 22 | } 23 | } 24 | 25 | // IOWriterK8sObjectYAMLRepo knows to store all the SLO rules (recordings and alerts) 26 | // grouped in an IOWriter in Kubernetes K8sObject YAML format. 27 | type IOWriterK8sObjectYAMLRepo struct { 28 | writer io.Writer 29 | encoder runtime.Encoder 30 | logger log.Logger 31 | transformer plugink8stransformv1.Plugin 32 | } 33 | 34 | func (i IOWriterK8sObjectYAMLRepo) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { 35 | k8sObjs, err := i.transformer.TransformK8sObjects(ctx, kmeta, slos) 36 | if err != nil { 37 | return fmt.Errorf("could not transform k8s objects using plugin: %w", err) 38 | } 39 | 40 | for _, obj := range k8sObjs.Items { 41 | obj := obj.DeepCopy() 42 | 43 | l := obj.GetLabels() 44 | if l == nil { 45 | l = map[string]string{} 46 | } 47 | l["app.kubernetes.io/component"] = "SLO" 48 | l["app.kubernetes.io/managed-by"] = "sloth" 49 | obj.SetLabels(l) 50 | 51 | _, err := i.writer.Write([]byte(yamlTopdisclaimer)) 52 | if err != nil { 53 | return fmt.Errorf("could not write top disclaimer: %w", err) 54 | } 55 | err = i.encoder.Encode(obj, i.writer) 56 | if err != nil { 57 | return fmt.Errorf("could encode k8s object: %w", err) 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/plugin/slo/contrib/rule_intervals_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | prommodel "github.com/prometheus/common/model" 10 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 11 | ) 12 | 13 | const ( 14 | PluginVersion = "prometheus/slo/v1" 15 | PluginID = "sloth.dev/contrib/rule_intervals/v1" 16 | ) 17 | 18 | type ConfigInterval struct { 19 | Default prommodel.Duration `json:"default,omitempty"` 20 | SLIError prommodel.Duration `json:"sliError,omitempty"` 21 | Metadata prommodel.Duration `json:"metadata,omitempty"` 22 | Alert prommodel.Duration `json:"alert,omitempty"` 23 | } 24 | type Config struct { 25 | Interval ConfigInterval `json:"interval,omitempty"` 26 | } 27 | 28 | func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { 29 | config := Config{} 30 | err := json.Unmarshal(configData, &config) 31 | if err != nil { 32 | return nil, fmt.Errorf("invalid config: %w", err) 33 | } 34 | 35 | if config.Interval.Default == 0 { 36 | return nil, fmt.Errorf("at least default interval is required") 37 | } 38 | 39 | return plugin{config: config}, nil 40 | } 41 | 42 | type plugin struct { 43 | config Config 44 | } 45 | 46 | func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { 47 | result.SLORules.SLIErrorRecRules.Interval = time.Duration(p.config.Interval.Default) 48 | if p.config.Interval.SLIError != 0 { 49 | result.SLORules.SLIErrorRecRules.Interval = time.Duration(p.config.Interval.SLIError) 50 | } 51 | 52 | result.SLORules.MetadataRecRules.Interval = time.Duration(p.config.Interval.Default) 53 | if p.config.Interval.Metadata != 0 { 54 | result.SLORules.MetadataRecRules.Interval = time.Duration(p.config.Interval.Metadata) 55 | } 56 | 57 | result.SLORules.AlertRules.Interval = time.Duration(p.config.Interval.Default) 58 | if p.config.Interval.Alert != 0 { 59 | result.SLORules.AlertRules.Interval = time.Duration(p.config.Interval.Alert) 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/http/backend/storage/prometheus/slo_hydrate.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/slok/sloth/internal/http/backend/model" 8 | ) 9 | 10 | type sloInstantData struct { 11 | SLOID string 12 | SlothID string 13 | Name string 14 | ServiceID string 15 | Objective float64 16 | SpecName string 17 | SlothVersion string 18 | SlothMode string 19 | NonGroupingLabels map[string]struct{} // SLO labels to ignore for grouping purposes so we know all the labels that are not used for grouping SLOs by labels. 20 | SLOPeriod time.Duration 21 | SLIWindows []time.Duration 22 | GroupLabels map[string]string 23 | IsGrouped bool 24 | Alerts *model.SLOAlerts 25 | BurnedPeriodRollingWindowRatio float64 26 | BurningCurrentRatio float64 27 | } 28 | 29 | type slosInstantData struct { 30 | slosBySlothID map[string]*sloInstantData 31 | slosBySLOID map[string]*sloInstantData 32 | } 33 | 34 | // slosHydrator is the interface used to hydrate SLOs data, this gives us the ability to create 35 | // a simple to follow chain of SLO hydrating data. 36 | type sloInstantsHydrater interface { 37 | HydrateSLOInstant(ctx context.Context, slos *slosInstantData) error 38 | } 39 | 40 | type sloInstantsHydraterFunc func(ctx context.Context, slos *slosInstantData) error 41 | 42 | func (f sloInstantsHydraterFunc) HydrateSLOInstant(ctx context.Context, slos *slosInstantData) error { 43 | return f(ctx, slos) 44 | } 45 | 46 | func newSLOsInstantHydraterChain(hydraters ...sloInstantsHydrater) sloInstantsHydrater { 47 | return sloInstantsHydraterFunc(func(ctx context.Context, slos *slosInstantData) error { 48 | for _, hydrater := range hydraters { 49 | err := hydrater.HydrateSLOInstant(ctx, slos) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/noop_v1/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | plugin "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" 11 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 12 | pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" 13 | ) 14 | 15 | func TestPlugin(t *testing.T) { 16 | tests := map[string]struct { 17 | config json.RawMessage 18 | req pluginslov1.Request 19 | expRes pluginslov1.Result 20 | expErr bool 21 | }{ 22 | "A noop plugin should noop.": { 23 | config: json.RawMessage{}, 24 | req: pluginslov1.Request{}, 25 | expRes: pluginslov1.Result{}, 26 | }, 27 | } 28 | 29 | for name, test := range tests { 30 | t.Run(name, func(t *testing.T) { 31 | require := require.New(t) 32 | assert := assert.New(t) 33 | 34 | plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) 35 | require.NoError(err) 36 | 37 | res := pluginslov1.Result{} 38 | err = plugin.ProcessSLO(t.Context(), &test.req, &res) 39 | if test.expErr { 40 | assert.Error(err) 41 | } else if assert.NoError(err) { 42 | assert.Equal(test.expRes, res) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func BenchmarkPluginYaegi(b *testing.B) { 49 | plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{}) 50 | if err != nil { 51 | b.Fatal(err) 52 | } 53 | 54 | for b.Loop() { 55 | err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) 56 | if err != nil { 57 | b.Fatal(err) 58 | } 59 | } 60 | } 61 | 62 | func BenchmarkPluginGo(b *testing.B) { 63 | plugin, err := plugin.NewPlugin(nil, pluginslov1.AppUtils{}) 64 | if err != nil { 65 | b.Fatal(err) 66 | } 67 | 68 | for b.Loop() { 69 | err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) 70 | if err != nil { 71 | b.Fatal(err) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/prometheus/plugin/slo/v1/v1.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/slok/sloth/internal/log" 8 | "github.com/slok/sloth/pkg/common/model" 9 | ) 10 | 11 | // Version is this plugin type version. 12 | const Version = "prometheus/slo/v1" 13 | 14 | // PluginVersion is the version of the plugin (e.g: `prometheus/slo/v1`). 15 | type PluginVersion = string 16 | 17 | const PluginVersionName = "PluginVersion" 18 | 19 | // PluginID is the ID of the plugin (e.g: sloth.dev/my-test-plugin/v1). 20 | type PluginID = string 21 | 22 | const PluginIDName = "PluginID" 23 | 24 | // AppUtils are app utils plugins can use in their logic. 25 | type AppUtils struct { 26 | Logger log.Logger 27 | } 28 | 29 | type Request struct { 30 | // Info about the application and execution, normally used as metadata. 31 | Info model.Info 32 | // OriginalSource is the original specification of the SLO came from, this is informative data that 33 | // can be used to make decision on plugins, it should be used only as RO. 34 | // The information used on the generation is the SLO model itself not this one. 35 | OriginalSource model.PromSLOGroupSource 36 | // The SLO to process and generate the final Prom rules. 37 | SLO model.PromSLO 38 | // The SLO MWMBAlertGroup selected. 39 | MWMBAlertGroup model.MWMBAlertGroup 40 | } 41 | 42 | type Result struct { 43 | SLORules model.PromSLORules 44 | } 45 | 46 | // PluginFactoryName is the required name for the plugin factory. 47 | const PluginFactoryName = "NewPlugin" 48 | 49 | type PluginFactory = func(config json.RawMessage, appUtils AppUtils) (Plugin, error) 50 | 51 | // Plugin knows how to process SLOs in a chain of plugins. 52 | // * The plugin processor can change the result argument of the SLO processing with the resulting prometheus rules. 53 | // * The plugin processor can also modify the request object, but this is not recommended as it can lead to unexpected behavior. 54 | // 55 | // This is the type the SLO plugins need to implement. 56 | type Plugin interface { 57 | ProcessSLO(ctx context.Context, request *Request, result *Result) error 58 | } 59 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/slievents.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SLIEventsApplyConfiguration represents a declarative configuration of the SLIEvents type for use 6 | // with apply. 7 | // 8 | // SLIEvents is an SLI that is calculated as the division of bad events and total events, giving 9 | // a ratio SLI. Normally this is the most common ratio type. 10 | type SLIEventsApplyConfiguration struct { 11 | // ErrorQuery is a Prometheus query that will get the number/count of events 12 | // that we consider that are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). 13 | // Requires the usage of `{{.window}}` template variable. 14 | ErrorQuery *string `json:"errorQuery,omitempty"` 15 | // TotalQuery is a Prometheus query that will get the total number/count of events 16 | // for the SLO (e.g "all http requests"...). 17 | // Requires the usage of `{{.window}}` template variable. 18 | TotalQuery *string `json:"totalQuery,omitempty"` 19 | } 20 | 21 | // SLIEventsApplyConfiguration constructs a declarative configuration of the SLIEvents type for use with 22 | // apply. 23 | func SLIEvents() *SLIEventsApplyConfiguration { 24 | return &SLIEventsApplyConfiguration{} 25 | } 26 | 27 | // WithErrorQuery sets the ErrorQuery field in the declarative configuration to the given value 28 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 29 | // If called multiple times, the ErrorQuery field is set to the value of the last call. 30 | func (b *SLIEventsApplyConfiguration) WithErrorQuery(value string) *SLIEventsApplyConfiguration { 31 | b.ErrorQuery = &value 32 | return b 33 | } 34 | 35 | // WithTotalQuery sets the TotalQuery field in the declarative configuration to the given value 36 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 37 | // If called multiple times, the TotalQuery field is set to the value of the last call. 38 | func (b *SLIEventsApplyConfiguration) WithTotalQuery(value string) *SLIEventsApplyConfiguration { 39 | b.TotalQuery = &value 40 | return b 41 | } 42 | -------------------------------------------------------------------------------- /internal/plugin/slo/core/debug_v1/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | plugin "github.com/slok/sloth/internal/plugin/slo/core/debug_v1" 11 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 12 | pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" 13 | ) 14 | 15 | func TestPlugin(t *testing.T) { 16 | tests := map[string]struct { 17 | config json.RawMessage 18 | req pluginslov1.Request 19 | expRes pluginslov1.Result 20 | expErr bool 21 | }{ 22 | "A log plugin should log.": { 23 | config: []byte(`{"msg":"test"}`), 24 | req: pluginslov1.Request{}, 25 | expRes: pluginslov1.Result{}, 26 | }, 27 | } 28 | 29 | for name, test := range tests { 30 | t.Run(name, func(t *testing.T) { 31 | require := require.New(t) 32 | assert := assert.New(t) 33 | 34 | plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) 35 | require.NoError(err) 36 | 37 | res := pluginslov1.Result{} 38 | err = plugin.ProcessSLO(t.Context(), &test.req, &res) 39 | if test.expErr { 40 | assert.Error(err) 41 | } else if assert.NoError(err) { 42 | assert.Equal(test.expRes, res) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func BenchmarkPluginYaegi(b *testing.B) { 49 | plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: []byte(`{}`)}) 50 | if err != nil { 51 | b.Fatal(err) 52 | } 53 | 54 | for b.Loop() { 55 | err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) 56 | if err != nil { 57 | b.Fatal(err) 58 | } 59 | } 60 | } 61 | 62 | func BenchmarkPluginGo(b *testing.B) { 63 | plugin, err := plugin.NewPlugin([]byte(`{}`), pluginslov1.AppUtils{}) 64 | if err != nil { 65 | b.Fatal(err) 66 | } 67 | 68 | for b.Loop() { 69 | err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) 70 | if err != nil { 71 | b.Fatal(err) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | 8 | "github.com/slok/sloth/pkg/common/model" 9 | k8sutils "github.com/slok/sloth/pkg/common/utils/k8s" 10 | plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" 11 | ) 12 | 13 | const ( 14 | PluginVersion = "prometheus/k8stransform/v1" 15 | PluginID = "sloth.dev/k8stransform/prom-operator-prometheus-rule/v1" 16 | ) 17 | 18 | func NewPlugin() (plugink8stransformv1.Plugin, error) { 19 | return plugin{}, nil 20 | } 21 | 22 | type plugin struct{} 23 | 24 | func (p plugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { 25 | u := &unstructured.Unstructured{} 26 | u.SetAPIVersion("monitoring.coreos.com/v1") 27 | u.SetKind("PrometheusRule") 28 | u.SetNamespace(kmeta.Namespace) 29 | u.SetName(kmeta.Name) 30 | u.SetLabels(kmeta.Labels) 31 | u.SetAnnotations(kmeta.Annotations) 32 | 33 | groups := []any{} 34 | for _, slo := range sloResult.SLOResults { 35 | if len(slo.PrometheusRules.SLIErrorRecRules.Rules) > 0 { 36 | groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.SLIErrorRecRules)) 37 | } 38 | if len(slo.PrometheusRules.MetadataRecRules.Rules) > 0 { 39 | groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.MetadataRecRules)) 40 | } 41 | if len(slo.PrometheusRules.AlertRules.Rules) > 0 { 42 | groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.AlertRules)) 43 | } 44 | 45 | for _, extraRG := range slo.PrometheusRules.ExtraRules { 46 | // Skip empty extra rule groups. 47 | if len(extraRG.Rules) == 0 { 48 | continue 49 | } 50 | groups = append(groups, 51 | k8sutils.PromRuleGroupToUnstructuredPromOperator(extraRG), 52 | ) 53 | } 54 | } 55 | 56 | u.Object["spec"] = map[string]any{ 57 | "groups": groups, 58 | } 59 | 60 | return &plugink8stransformv1.K8sObjects{ 61 | Items: []*unstructured.Unstructured{u}, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "context" 7 | "github.com/slok/sloth/pkg/common/model" 8 | "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" 9 | "go/constant" 10 | "go/token" 11 | "reflect" 12 | ) 13 | 14 | func init() { 15 | Symbols["github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1/v1"] = map[string]reflect.Value{ 16 | // function, constant and variable definitions 17 | "PluginFactoryName": reflect.ValueOf(constant.MakeFromLiteral("\"NewPlugin\"", token.STRING, 0)), 18 | "PluginIDName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginID\"", token.STRING, 0)), 19 | "PluginVersionName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginVersion\"", token.STRING, 0)), 20 | "Version": reflect.ValueOf(constant.MakeFromLiteral("\"prometheus/k8stransform/v1\"", token.STRING, 0)), 21 | 22 | // type definitions 23 | "K8sObjects": reflect.ValueOf((*v1.K8sObjects)(nil)), 24 | "Plugin": reflect.ValueOf((*v1.Plugin)(nil)), 25 | "PluginFactory": reflect.ValueOf((*v1.PluginFactory)(nil)), 26 | "PluginID": reflect.ValueOf((*v1.PluginID)(nil)), 27 | "PluginVersion": reflect.ValueOf((*v1.PluginVersion)(nil)), 28 | 29 | // interface wrapper definitions 30 | "_Plugin": reflect.ValueOf((*_github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin)(nil)), 31 | } 32 | } 33 | 34 | // _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin is an interface wrapper for Plugin type 35 | type _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin struct { 36 | IValue interface{} 37 | WTransformK8sObjects func(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*v1.K8sObjects, error) 38 | } 39 | 40 | func (W _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*v1.K8sObjects, error) { 41 | return W.WTransformK8sObjects(ctx, kmeta, sloResult) 42 | } 43 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-validation.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/validation'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/validation" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/slok/sloth/pkg/common/validation/validation"] = map[string]reflect.Value{ 12 | // function, constant and variable definitions 13 | "PromQLDialectValidator": reflect.ValueOf(validation.PromQLDialectValidator), 14 | "ValidateSLO": reflect.ValueOf(validation.ValidateSLO), 15 | 16 | // type definitions 17 | "SLODialectValidator": reflect.ValueOf((*validation.SLODialectValidator)(nil)), 18 | 19 | // interface wrapper definitions 20 | "_SLODialectValidator": reflect.ValueOf((*_github_com_slok_sloth_pkg_common_validation_SLODialectValidator)(nil)), 21 | } 22 | } 23 | 24 | // _github_com_slok_sloth_pkg_common_validation_SLODialectValidator is an interface wrapper for SLODialectValidator type 25 | type _github_com_slok_sloth_pkg_common_validation_SLODialectValidator struct { 26 | IValue interface{} 27 | WValidateAnnotationKey func(k string) error 28 | WValidateAnnotationValue func(k string) error 29 | WValidateLabelKey func(k string) error 30 | WValidateLabelValue func(k string) error 31 | WValidateQueryExpression func(queryExpression string) error 32 | } 33 | 34 | func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationKey(k string) error { 35 | return W.WValidateAnnotationKey(k) 36 | } 37 | func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationValue(k string) error { 38 | return W.WValidateAnnotationValue(k) 39 | } 40 | func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelKey(k string) error { 41 | return W.WValidateLabelKey(k) 42 | } 43 | func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelValue(k string) error { 44 | return W.WValidateLabelValue(k) 45 | } 46 | func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateQueryExpression(queryExpression string) error { 47 | return W.WValidateQueryExpression(queryExpression) 48 | } 49 | --------------------------------------------------------------------------------