├── 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 |
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 |
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 |
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 |
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 |
27 | {{ if .Data.SLOData.WarningAlertName }}
28 | FIRING
29 | {{ else }}
30 | OK
31 | {{ end }}
32 |
33 |
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 |
--------------------------------------------------------------------------------