├── .codecov.yml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── close-stale.yaml │ ├── generate.yaml │ └── helmrelease.yaml ├── .gitignore ├── .golangci.yml ├── .mockery.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── sloth │ ├── commands │ ├── commands.go │ ├── generate.go │ ├── helpers.go │ ├── k8scontroller.go │ ├── validate.go │ └── version.go │ └── main.go ├── deploy └── kubernetes │ ├── helm │ └── sloth │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── crds │ │ └── sloth.slok.dev_prometheusservicelevels.yaml │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── cluster-role-binding.yaml │ │ ├── cluster-role.yaml │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── pod-monitor.yaml │ │ └── service-account.yaml │ │ ├── tests │ │ ├── go.mod │ │ ├── go.sum │ │ ├── helm_chart_test.go │ │ ├── testdata │ │ │ └── output │ │ │ │ ├── cluster_role_binding_custom.yaml │ │ │ │ ├── cluster_role_binding_default.yaml │ │ │ │ ├── cluster_role_custom.yaml │ │ │ │ ├── cluster_role_default.yaml │ │ │ │ ├── configmap_slo_config.yaml │ │ │ │ ├── deployment_custom.yaml │ │ │ │ ├── deployment_custom_no_extras.yaml │ │ │ │ ├── deployment_custom_slo_config.yaml │ │ │ │ ├── deployment_default.yaml │ │ │ │ ├── pod_monitor_custom.yaml │ │ │ │ ├── pod_monitor_default.yaml │ │ │ │ ├── sa_custom.yaml │ │ │ │ └── sa_default.yaml │ │ └── values_test.go │ │ └── values.yaml │ ├── kustomization.yaml │ └── raw │ ├── sloth-with-common-plugins.yaml │ └── sloth.yaml ├── docker ├── dev │ └── Dockerfile └── prod │ └── Dockerfile ├── docs └── img │ ├── logo.png │ └── sloth_small_dashboard.png ├── examples ├── _gen │ ├── getting-started.yml │ ├── home-wifi.yml │ ├── k8s-getting-started.yml │ ├── k8s-home-wifi.yml │ ├── k8s-multifile.yml │ ├── kubernetes-apiserver.yml │ ├── multifile.yml │ ├── no-alerts.yml │ ├── openslo-getting-started.yml │ ├── openslo-kubernetes-apiserver.yml │ ├── plugin-getting-started.yml │ ├── plugin-k8s-getting-started.yml │ ├── raw-home-wifi.yml │ ├── slo-plugin-getting-started.yml │ └── slo-plugin-k8s-getting-started.yml ├── getting-started.yml ├── home-wifi.yml ├── k8s-getting-started.yml ├── k8s-home-wifi.yml ├── k8s-multifile.yml ├── kubernetes-apiserver.yml ├── multifile.yml ├── no-alerts.yml ├── openslo-getting-started.yml ├── openslo-kubernetes-apiserver.yml ├── plugin-getting-started.yml ├── plugin-k8s-getting-started.yml ├── plugins │ └── getting-started │ │ └── availability │ │ └── plugin.go ├── raw-home-wifi.yml ├── slo-plugin-getting-started.yml ├── slo-plugin-k8s-getting-started.yml └── windows │ ├── 7d.yaml │ └── custom-30d.yaml ├── go.mod ├── go.sum ├── internal ├── alert │ ├── alert.go │ ├── alert_test.go │ ├── window.go │ └── windows │ │ ├── google-28d.yaml │ │ └── google-30d.yaml ├── app │ ├── generate │ │ ├── generate.go │ │ ├── generate_test.go │ │ ├── generatemock │ │ │ └── mocks.go │ │ └── process.go │ └── kubecontroller │ │ ├── handler.go │ │ └── retriever.go ├── info │ └── info.go ├── kubernetes │ └── modelmap │ │ └── slo.go ├── log │ ├── log.go │ └── logrus │ │ └── logrus.go ├── plugin │ ├── plugin.go │ └── slo │ │ └── core │ │ ├── alert_rules_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go │ │ ├── debug_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go │ │ ├── metadata_rules_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go │ │ ├── noop_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go │ │ ├── sli_rules_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go │ │ └── validate_v1 │ │ ├── README.md │ │ ├── plugin.go │ │ └── plugin_test.go ├── pluginengine │ ├── sli │ │ ├── sli.go │ │ └── sli_test.go │ └── slo │ │ ├── custom │ │ ├── custom.go │ │ ├── github_com-caarlos0-env-v11.go │ │ ├── github_com-prometheus-common-model.go │ │ ├── github_com-prometheus-prometheus-model-rulefmt.go │ │ ├── github_com-prometheus-prometheus-promql-parser.go │ │ ├── github_com-slok-sloth-pkg-common-conventions.go │ │ ├── github_com-slok-sloth-pkg-common-model.go │ │ ├── github_com-slok-sloth-pkg-common-utils-data.go │ │ ├── github_com-slok-sloth-pkg-common-utils-prometheus.go │ │ ├── github_com-slok-sloth-pkg-common-validation.go │ │ └── github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go │ │ ├── slo.go │ │ └── slo_test.go └── storage │ ├── fs │ ├── fsmock │ │ └── mocks.go │ ├── plugin.go │ └── plugin_test.go │ ├── io │ ├── helper.go │ ├── k8s_sloth.go │ ├── k8s_sloth_test.go │ ├── openslo.go │ ├── openslo_test.go │ ├── prometheus_operator.go │ ├── prometheus_operator_test.go │ ├── sloth.go │ ├── sloth_test.go │ ├── std_prometheus.go │ └── std_prometheus_test.go │ ├── k8s │ ├── dry_run.go │ ├── fake.go │ ├── k8s.go │ └── k8s_test.go │ └── storage.go ├── pkg ├── common │ ├── conventions │ │ ├── conventions.go │ │ ├── sli.go │ │ └── slo.go │ ├── errors │ │ └── errors.go │ ├── model │ │ ├── alert.go │ │ ├── info.go │ │ └── slo_prometheus.go │ ├── utils │ │ ├── data │ │ │ └── data.go │ │ └── prometheus │ │ │ └── prometheus.go │ └── validation │ │ ├── promql.go │ │ ├── slo.go │ │ └── slo_test.go ├── kubernetes │ ├── api │ │ └── sloth │ │ │ ├── register.go │ │ │ └── v1 │ │ │ ├── README.md │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ ├── types.go │ │ │ └── zz_generated.deepcopy.go │ └── gen │ │ ├── applyconfiguration │ │ ├── internal │ │ │ └── internal.go │ │ ├── sloth │ │ │ └── v1 │ │ │ │ ├── alert.go │ │ │ │ ├── alerting.go │ │ │ │ ├── prometheusservicelevel.go │ │ │ │ ├── prometheusservicelevelspec.go │ │ │ │ ├── prometheusservicelevelstatus.go │ │ │ │ ├── sli.go │ │ │ │ ├── slievents.go │ │ │ │ ├── sliplugin.go │ │ │ │ ├── sliraw.go │ │ │ │ ├── slo.go │ │ │ │ ├── sloplugin.go │ │ │ │ └── sloplugins.go │ │ └── utils.go │ │ ├── clientset │ │ └── versioned │ │ │ ├── clientset.go │ │ │ ├── fake │ │ │ ├── clientset_generated.go │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ └── typed │ │ │ └── sloth │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── fake_prometheusservicelevel.go │ │ │ └── fake_sloth_client.go │ │ │ ├── generated_expansion.go │ │ │ ├── prometheusservicelevel.go │ │ │ └── sloth_client.go │ │ ├── crd │ │ └── sloth.slok.dev_prometheusservicelevels.yaml │ │ ├── informers │ │ └── externalversions │ │ │ ├── factory.go │ │ │ ├── generic.go │ │ │ ├── internalinterfaces │ │ │ └── factory_interfaces.go │ │ │ └── sloth │ │ │ ├── interface.go │ │ │ └── v1 │ │ │ ├── interface.go │ │ │ └── prometheusservicelevel.go │ │ └── listers │ │ └── sloth │ │ └── v1 │ │ ├── expansion_generated.go │ │ └── prometheusservicelevel.go └── prometheus │ ├── alertwindows │ └── v1 │ │ ├── README.md │ │ └── v1.go │ ├── api │ └── v1 │ │ ├── README.md │ │ └── v1.go │ └── plugin │ ├── slo │ └── v1 │ │ ├── testing │ │ └── testing.go │ │ └── v1.go │ └── v1 │ └── v1.go ├── scripts ├── build │ ├── bin │ │ ├── build-all.sh │ │ ├── build-raw.sh │ │ └── build.sh │ └── docker │ │ ├── build-image-dev.sh │ │ ├── build-image.sh │ │ ├── build-publish-image-all.sh │ │ └── publish-image.sh ├── check │ ├── check.sh │ ├── helm-test.sh │ ├── integration-test-cli.sh │ ├── integration-test-k8s.sh │ ├── integration-test.sh │ └── unit-test.sh ├── deploygen.sh ├── deps.sh ├── examplesgen.sh ├── gogen.sh └── kubegen.sh └── test └── integration ├── crd └── prometheus-operator-crd.yaml ├── k8scontroller ├── exp_base_28d_test.go ├── exp_base_7d_test.go ├── exp_base_test.go ├── exp_sli_plugins_test.go ├── exp_slo_plugins_test.go ├── helpers.go ├── k8scontroller_test.go ├── plugins │ ├── sli │ │ └── plugin1 │ │ │ └── plugin.go │ └── slo │ │ └── plugin1 │ │ └── plugin.go └── windows │ └── 7d.yaml ├── prometheus ├── generate_test.go ├── helpers.go ├── plugins │ ├── sli │ │ └── plugin1 │ │ │ └── plugin.go │ └── slo │ │ └── plugin1 │ │ └── plugin.go ├── testdata │ ├── in-base-k8s.yaml │ ├── in-base.yaml │ ├── in-invalid-version.yaml │ ├── in-multifile-k8s.yaml │ ├── in-multifile.yaml │ ├── in-openslo.yaml │ ├── in-sli-plugin.yaml │ ├── in-slo-plugin-k8s.yaml │ ├── in-slo-plugin.yaml │ ├── out-base-28d.yaml.tpl │ ├── out-base-custom-windows-7d.yaml.tpl │ ├── out-base-extra-labels.yaml.tpl │ ├── out-base-k8s.yaml.tpl │ ├── out-base-no-alerts.yaml.tpl │ ├── out-base-no-recordings.yaml.tpl │ ├── out-base.yaml.tpl │ ├── out-multifile-k8s.yaml.tpl │ ├── out-multifile.yaml.tpl │ ├── out-openslo.yaml.tpl │ ├── out-sli-plugin.yaml.tpl │ ├── out-slo-plugin-k8s.yaml.tpl │ ├── out-slo-plugin.yaml.tpl │ └── validate │ │ ├── bad │ │ ├── bad-aa.yaml │ │ ├── bad-ab.yaml │ │ ├── bad-ba.yaml │ │ ├── bad-k8s.yaml │ │ ├── bad-multi-k8s.yaml │ │ ├── bad-multi.yaml │ │ └── bad-openslo.yaml │ │ └── good │ │ ├── good-aa.yaml │ │ ├── good-ab.yaml │ │ ├── good-ba.yaml │ │ ├── good-k8s.yaml │ │ ├── good-multi-k8s.yaml │ │ ├── good-multi.yaml │ │ └── good-openslo.yaml ├── validate_test.go └── windows │ └── 7d.yaml └── testutils └── cmd.go /.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 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @slok 2 | 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v9 11 | with: 12 | days-before-stale: 60 13 | days-before-close: 15 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 | -------------------------------------------------------------------------------- /.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@v4 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@v4 23 | with: 24 | name: SLOs 25 | path: examples/_gen/ 26 | -------------------------------------------------------------------------------- /.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@v4 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | run: 3 | timeout: 3m 4 | build-tags: 5 | - integration 6 | 7 | linters: 8 | enable: 9 | - misspell 10 | - goimports 11 | - revive 12 | - gofmt 13 | #- depguard 14 | - godot 15 | 16 | linters-settings: 17 | revive: 18 | rules: 19 | # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. 20 | - name: unused-parameter 21 | disabled: true 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}} 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/.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 | -------------------------------------------------------------------------------- /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.12.1 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 }} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | - --disable-optimized-rules 55 | - --logger=default 56 | securityContext: 57 | allowPrivilegeEscalation: false 58 | resources: 59 | limits: 60 | cpu: 50m 61 | memory: 150Mi 62 | requests: 63 | cpu: 5m 64 | memory: 75Mi 65 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.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 | checksum/config: 34 | spec: 35 | serviceAccountName: sloth-test 36 | securityContext: 37 | fsGroup: 100 38 | runAsGroup: 1000 39 | runAsNonRoot: true 40 | runAsUser: 100 41 | nodeSelector: 42 | k1: v1 43 | k2: v2 44 | containers: 45 | - name: sloth 46 | image: slok/sloth-test:v1.42.42 47 | args: 48 | - kubernetes-controller 49 | - --resync-interval=17m 50 | - --workers=99 51 | - --namespace=somens 52 | - --label-selector=x=y,z!=y 53 | - --extra-labels=k1=v1 54 | - --extra-labels=k2=v2 55 | - --disable-optimized-rules 56 | - --slo-period-windows-path=/windows 57 | - --logger=default 58 | ports: 59 | - containerPort: 8081 60 | name: metrics 61 | protocol: TCP 62 | volumeMounts: 63 | - name: sloth-windows 64 | mountPath: /windows 65 | securityContext: 66 | allowPrivilegeEscalation: false 67 | resources: 68 | limits: 69 | cpu: 50m 70 | memory: 150Mi 71 | requests: 72 | cpu: 5m 73 | memory: 75Mi 74 | volumes: 75 | - name: sloth-windows 76 | configMap: 77 | defaultMode: 420 78 | name: sloth-test 79 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: sloth/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 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 | replicas: 1 16 | selector: 17 | matchLabels: 18 | app: sloth 19 | app.kubernetes.io/name: sloth 20 | app.kubernetes.io/instance: sloth 21 | template: 22 | metadata: 23 | labels: 24 | helm.sh/chart: sloth- 25 | app.kubernetes.io/managed-by: Helm 26 | app: sloth 27 | app.kubernetes.io/name: sloth 28 | app.kubernetes.io/instance: sloth 29 | annotations: 30 | kubectl.kubernetes.io/default-container: sloth 31 | spec: 32 | serviceAccountName: sloth 33 | containers: 34 | - name: sloth 35 | image: ghcr.io/slok/sloth:v0.12.0 36 | args: 37 | - kubernetes-controller 38 | - --plugins-path=/plugins 39 | - --logger=default 40 | ports: 41 | - containerPort: 8081 42 | name: metrics 43 | protocol: TCP 44 | volumeMounts: 45 | - name: sloth-common-sli-plugins 46 | mountPath: /plugins/sloth-common-sli-plugins 47 | resources: 48 | limits: 49 | cpu: 50m 50 | memory: 150Mi 51 | requests: 52 | cpu: 5m 53 | memory: 75Mi 54 | - name: git-sync-plugins 55 | image: registry.k8s.io/git-sync/git-sync:v4.4.0 56 | args: 57 | - --repo=https://github.com/slok/sloth-common-sli-plugins 58 | - --ref=main 59 | - --period=30s 60 | - --webhook-url=http://localhost:8082/-/reload 61 | volumeMounts: 62 | - name: sloth-common-sli-plugins 63 | # Default path for git-sync. 64 | mountPath: /git 65 | resources: 66 | limits: 67 | cpu: 50m 68 | memory: 100Mi 69 | requests: 70 | cpu: 5m 71 | memory: 50Mi 72 | volumes: 73 | - name: sloth-common-sli-plugins 74 | emptyDir: {} 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | "optimizedRules": false, 31 | "extraLabels": msi{ 32 | "k1": "v1", 33 | "k2": "v2", 34 | }, 35 | }, 36 | 37 | "nodeSelector": msi{ 38 | "k1": "v1", 39 | "k2": "v2", 40 | }, 41 | 42 | "commonPlugins": msi{ 43 | "enabled": true, 44 | "gitRepo": msi{ 45 | "url": "https://github.com/slok/sloth-test-common-sli-plugins", 46 | "branch": "main", 47 | }, 48 | }, 49 | 50 | "metrics": msi{ 51 | "enabled": true, 52 | "scrapeInterval": "45s", 53 | "prometheusLabels": msi{ 54 | "kp1": "vp1", 55 | "kp2": "vp2", 56 | }, 57 | }, 58 | 59 | "customSloConfig": msi{ 60 | "data": msi{ 61 | "customKey": "customValue", 62 | }, 63 | }, 64 | 65 | "securityContext": msi{ 66 | "pod": msi{ 67 | "runAsNonRoot": true, 68 | "runAsGroup": 1000, 69 | "runAsUser": 100, 70 | "fsGroup": 100, 71 | }, 72 | "container": msi{ 73 | "allowPrivilegeEscalation": false, 74 | }, 75 | }, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /deploy/kubernetes/helm/sloth/values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | imageRegistry: "" # This field cannot be empty if image.registry is also empty. 3 | 4 | labels: {} 5 | 6 | image: 7 | registry: ghcr.io # This field cannot be empty if global.imageRegistry is also empty. 8 | repository: slok/sloth 9 | tag: v0.12.0 10 | 11 | # -- Container resources: requests and limits for CPU, Memory 12 | resources: 13 | limits: 14 | cpu: 50m 15 | memory: 150Mi 16 | requests: 17 | cpu: 5m 18 | memory: 75Mi 19 | 20 | imagePullSecrets: [] 21 | # - name: secret1 22 | # - name: secret2 23 | 24 | sloth: 25 | resyncInterval: "" # The controller resync interval duration (e.g 15m). 26 | workers: 0 # The number of concurrent controller workers (e.g 5). 27 | labelSelector: "" # Sloth will handle only the ones that match the selector. 28 | namespace: "" # The namespace where sloth will the CRs to process. 29 | extraLabels: {} # Labels that will be added to all the generated SLO Rules. 30 | defaultSloPeriod: "" # The slo period used by sloth (e.g. 30d). 31 | optimizedRules: true # Reduce prom load for calculating period window burnrates. 32 | debug: 33 | enabled: false 34 | # Could be: default or json 35 | logger: default 36 | 37 | commonPlugins: 38 | enabled: true 39 | image: 40 | repository: registry.k8s.io/git-sync/git-sync 41 | tag: v4.4.0 42 | gitRepo: 43 | url: https://github.com/slok/sloth-common-sli-plugins 44 | branch: main 45 | resources: 46 | limits: 47 | cpu: 50m 48 | memory: 100Mi 49 | requests: 50 | cpu: 5m 51 | memory: 50Mi 52 | 53 | metrics: 54 | enabled: true 55 | #scrapeInterval: 30s 56 | prometheusLabels: {} 57 | 58 | customSloConfig: 59 | enabled: false 60 | path: /windows 61 | data: {} 62 | # apiVersion: sloth.slok.dev/v1 63 | # kind: AlertWindows 64 | # spec: 65 | # ... See https://sloth.dev/usage/slo-period-windows/ 66 | 67 | # add deployment pod tolerations 68 | # tolerations: 69 | # - key: kubernetes.azure.com/scalesetpriority 70 | # operator: Equal 71 | # value: spot 72 | # effect: NoSchedule 73 | 74 | # add deployment pod nodeSelector 75 | # nodeSelector: 76 | # kubernetes.io/arch: "arm64" 77 | 78 | securityContext: 79 | pod: null 80 | # fsGroup: 100 81 | # runAsGroup: 1000 82 | # runAsNonRoot: true 83 | # runAsUser: 100 84 | container: null 85 | # allowPrivilegeEscalation: false 86 | -------------------------------------------------------------------------------- /deploy/kubernetes/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - raw/sloth-with-common-plugins.yaml -------------------------------------------------------------------------------- /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.24.3-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"] -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/4f64237ae8cc5a4bf81eb23b76894a99cc4cb19f/docs/img/logo.png -------------------------------------------------------------------------------- /docs/img/sloth_small_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slok/sloth/4f64237ae8cc5a4bf81eb23b76894a99cc4cb19f/docs/img/sloth_small_dashboard.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/home-wifi.yml: -------------------------------------------------------------------------------- 1 | # This example shows a real service level used in my home to have SLOs on my wifi signal. 2 | # The metrics are extracted using unifi-poller (https://github.com/unifi-poller/unifi-poller) 3 | # that gets the information from an Ubiquiti Wifi installation. 4 | # https://community.ui.com/questions/satisfaction-percentage-in-client-properties-overview/8c940637-63d0-41de-a67b-8166cdd0ed32 5 | # 6 | # The service level has 2 SLOs based on `client_satisfaction_ratio`, this is a ratio calculated 7 | # by ubiquiti that is based on wifi drop packages, wifi signal... 8 | # We conside an SLI event the client satisfactions that currently exist, lets review the SLOs 9 | # 10 | # - `good-wifi-client-satisfaction` 11 | # - This SLO warn us that we don't have a good wifi at home. 12 | # - SLI error: We consider a bad client satisfaction (event) below 75% (0.75) 13 | # - SLO objective (95%): We are not so restrictive and we allow that that 5 of every 100 clients be below 75% 14 | # 15 | # - `risk-wifi-client-satisfaction` 16 | # - This SLO warn us that we something very bad is happenning with our home wifi. 17 | # - SLI error: We consider a bad client satisfaction (event) below 50% (0.5) 18 | # - SLO objective(99.9%): We are very restrictive and we allow that that 1 of every 1000 clients be below 50% 19 | # 20 | # `sloth generate -i ./examples/home-wifi.yml` 21 | # 22 | version: "prometheus/v1" 23 | service: "home-wifi" 24 | labels: 25 | cluster: "valhalla" 26 | component: "ubiquiti" 27 | context: "home" 28 | slos: 29 | - name: "good-wifi-client-satisfaction" 30 | objective: 95 31 | description: "Will warn us that we don't have a good wifi at home." 32 | sli: 33 | events: 34 | error_query: sum_over_time((count(unifipoller_client_satisfaction_ratio < 0.75))[{{.window}}:]) OR on() vector(0) 35 | total_query: sum_over_time((count(unifipoller_client_satisfaction_ratio))[{{.window}}:]) 36 | alerting: 37 | name: GoodWifiClientSatisfaction 38 | page_alert: 39 | labels: 40 | severity: home 41 | ticket_alert: 42 | labels: 43 | severity: warning 44 | 45 | - name: "risk-wifi-client-satisfaction" 46 | objective: 99.9 47 | description: "Will warn us that we something very bad is happenning with our home wifi." 48 | sli: 49 | events: 50 | error_query: sum_over_time((count(unifipoller_client_satisfaction_ratio < 0.5))[{{.window}}:]) OR on() vector(0) 51 | total_query: sum_over_time((count(unifipoller_client_satisfaction_ratio))[{{.window}}:]) 52 | alerting: 53 | name: RiskWifiClientSatisfaction 54 | page_alert: 55 | labels: 56 | severity: home 57 | ticket_alert: 58 | labels: 59 | severity: warning 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/k8s-multifile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD and multifile. 3 | # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. 4 | # 5 | # `sloth generate -i ./examples/k8s-multifile.yml` 6 | # 7 | apiVersion: sloth.slok.dev/v1 8 | kind: PrometheusServiceLevel 9 | metadata: 10 | name: sloth-slo-my-service 11 | namespace: monitoring 12 | spec: 13 | service: "myservice" 14 | labels: 15 | owner: "myteam" 16 | repo: "myorg/myservice" 17 | tier: "2" 18 | slos: 19 | - name: "requests-availability" 20 | objective: 99.9 21 | description: "Common SLO based on availability for HTTP request responses." 22 | sli: 23 | events: 24 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 25 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 26 | alerting: 27 | name: MyServiceHighErrorRate 28 | labels: 29 | category: "availability" 30 | annotations: 31 | summary: "High error rate on 'myservice' requests responses" 32 | pageAlert: 33 | labels: 34 | severity: pageteam 35 | routing_key: myteam 36 | ticketAlert: 37 | labels: 38 | severity: "slack" 39 | slack_channel: "#alerts-myteam" 40 | --- 41 | apiVersion: sloth.slok.dev/v1 42 | kind: PrometheusServiceLevel 43 | metadata: 44 | name: sloth-slo-my-service2 45 | namespace: monitoring 46 | spec: 47 | service: "myservice2" 48 | labels: 49 | owner: "myteam2" 50 | repo: "myorg/myservice2" 51 | tier: "1" 52 | slos: 53 | - name: "requests-availability" 54 | objective: 99.99 55 | description: "Common SLO based on availability for HTTP request responses." 56 | sli: 57 | events: 58 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 59 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 60 | alerting: 61 | name: MyServiceHighErrorRate 62 | labels: 63 | category: "availability" 64 | annotations: 65 | summary: "High error rate on 'myservice' requests responses" 66 | pageAlert: 67 | labels: 68 | severity: pageteam 69 | routing_key: myteam 70 | ticketAlert: 71 | labels: 72 | severity: "slack" 73 | slack_channel: "#alerts-myteam" 74 | -------------------------------------------------------------------------------- /examples/multifile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "prometheus/v1" 3 | service: "myservice" 4 | labels: 5 | owner: "myteam" 6 | repo: "myorg/myservice" 7 | tier: "2" 8 | slos: 9 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 10 | - name: "requests-availability" 11 | objective: 99.9 12 | description: "Common SLO based on availability for HTTP request responses." 13 | sli: 14 | events: 15 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 16 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 17 | alerting: 18 | name: MyServiceHighErrorRate 19 | labels: 20 | category: "availability" 21 | annotations: 22 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 23 | summary: "High error rate on 'myservice' requests responses" 24 | page_alert: 25 | labels: 26 | severity: pageteam 27 | routing_key: myteam 28 | ticket_alert: 29 | labels: 30 | severity: "slack" 31 | slack_channel: "#alerts-myteam" 32 | 33 | --- 34 | version: "prometheus/v1" 35 | service: "myservice2" 36 | labels: 37 | owner: "myteam2" 38 | repo: "myorg/myservice2" 39 | tier: "1" 40 | slos: 41 | # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). 42 | - name: "requests-availability" 43 | objective: 99.99 44 | description: "Common SLO based on availability for HTTP request responses." 45 | sli: 46 | events: 47 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 48 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 49 | alerting: 50 | name: MyServiceHighErrorRate 51 | labels: 52 | category: "availability" 53 | annotations: 54 | # Overwrite default Sloth SLO alert summmary on ticket and page alerts. 55 | summary: "High error rate on 'myservice' requests responses" 56 | page_alert: 57 | labels: 58 | severity: pageteam 59 | routing_key: myteam 60 | ticket_alert: 61 | labels: 62 | severity: "slack" 63 | slack_channel: "#alerts-myteam" 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/openslo-kubernetes-apiserver.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as kubernetes-apiserver.yml but using OpenSLO spec. 2 | # It will generate the Prometheus rules in a Prometheus rules format. 3 | # 4 | # Take into account that OpenSLO spec has the concept of single SLO with multiple objectives 5 | # 6 | # `sloth generate -i ./examples/openslo-kubernetes-apiserver.yml` 7 | # 8 | apiVersion: openslo/v1alpha 9 | kind: SLO 10 | metadata: 11 | name: requests-availability-openslo 12 | displayName: Requests Availability 13 | spec: 14 | service: k8s-apiserver 15 | description: "Apiserver are returning correctly the requests to the clients (kubectl users, controllers...)." 16 | budgetingMethod: Occurrences 17 | objectives: 18 | - ratioMetrics: 19 | good: 20 | source: prometheus 21 | queryType: promql 22 | query: sum(rate(apiserver_request_total{code!~"(5..|429)"}[{{.window}}])) 23 | total: 24 | source: prometheus 25 | queryType: promql 26 | query: sum(rate(apiserver_request_total[{{.window}}])) 27 | target: 0.999 28 | 29 | timeWindows: 30 | - count: 30 31 | unit: Day 32 | 33 | --- 34 | apiVersion: openslo/v1alpha 35 | kind: SLO 36 | metadata: 37 | name: requests-latency-openslo 38 | displayName: Requests Latency 39 | spec: 40 | service: k8s-apiserver 41 | description: "Apiserver responses are being fast enough and this will affect the clients (kubectl users, controllers...)." 42 | budgetingMethod: Occurrences 43 | objectives: 44 | - ratioMetrics: 45 | good: 46 | source: prometheus 47 | queryType: promql 48 | query: sum(rate(apiserver_request_duration_seconds_bucket{le="0.4",verb!="WATCH"}[{{.window}}])) 49 | total: 50 | source: prometheus 51 | queryType: promql 52 | query: sum(rate(apiserver_request_duration_seconds_count{verb!="WATCH"}[{{.window}}])) 53 | target: 0.99 54 | 55 | - ratioMetrics: 56 | good: 57 | source: prometheus 58 | queryType: promql 59 | query: sum(rate(apiserver_request_duration_seconds_bucket{le="5",verb!="WATCH"}[{{.window}}])) 60 | total: 61 | source: prometheus 62 | queryType: promql 63 | query: sum(rate(apiserver_request_duration_seconds_count{verb!="WATCH"}[{{.window}}])) 64 | target: 0.999 65 | 66 | timeWindows: 67 | - count: 30 68 | unit: Day 69 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/plugins/getting-started/availability/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 = "getting_started_availability" 15 | ) 16 | 17 | var queryTpl = template.Must(template.New("").Parse(` 18 | sum(rate(http_request_duration_seconds_count{ {{.filter}}job="{{.job}}",code=~"(5..|429)" }[{{"{{.window}}"}}])) 19 | / 20 | sum(rate(http_request_duration_seconds_count{ {{.filter}}job="{{.job}}" }[{{"{{.window}}"}}]))`)) 21 | 22 | var filterRegex = regexp.MustCompile(`([^=]+="[^=,"]+",)+`) 23 | 24 | // SLIPlugin is the getting started plugin example. 25 | // 26 | // It will return an Sloth error ratio raw query that returns the error ratio of HTTP requests based 27 | // on the HTTP response status code, taking 5xx and 429 as error events. 28 | func SLIPlugin(ctx context.Context, meta, labels, options map[string]string) (string, error) { 29 | // Get job. 30 | job, ok := options["job"] 31 | if !ok { 32 | return "", fmt.Errorf("job options is required") 33 | } 34 | 35 | // Validate labels. 36 | err := validateLabels(labels, "owner", "tier") 37 | if err != nil { 38 | return "", fmt.Errorf("invalid labels: %w", err) 39 | } 40 | 41 | // Sanitize filter. 42 | filter := options["filter"] 43 | if filter != "" { 44 | filter = strings.Trim(filter, "{}") 45 | filter = strings.Trim(filter, ",") 46 | filter = filter + "," 47 | match := filterRegex.MatchString(filter) 48 | if !match { 49 | return "", fmt.Errorf("invalid prometheus filter: %s", filter) 50 | } 51 | } 52 | 53 | // Create query. 54 | var b bytes.Buffer 55 | data := map[string]string{ 56 | "job": job, 57 | "filter": filter, 58 | } 59 | err = queryTpl.Execute(&b, data) 60 | if err != nil { 61 | return "", fmt.Errorf("could not execute template: %w", err) 62 | } 63 | 64 | return b.String(), nil 65 | } 66 | 67 | // validateLabels will check the labels exist. 68 | func validateLabels(labels map[string]string, requiredKeys ...string) error { 69 | for _, k := range requiredKeys { 70 | v, ok := labels[k] 71 | if !ok || (ok && v == "") { 72 | return fmt.Errorf("%q label is required", k) 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/slo-plugin-k8s-getting-started.yml: -------------------------------------------------------------------------------- 1 | # This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD and SLO plugins. 2 | # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. 3 | # 4 | # `sloth generate --debug -i ./examples/slo-plugin-k8s-getting-started.yml` 5 | # 6 | apiVersion: sloth.slok.dev/v1 7 | kind: PrometheusServiceLevel 8 | metadata: 9 | name: sloth-slo-my-service-with-slo-plugins 10 | namespace: monitoring 11 | spec: 12 | service: "myservice" 13 | labels: 14 | owner: "myteam" 15 | repo: "myorg/myservice" 16 | tier: "2" 17 | sloPlugins: 18 | chain: 19 | - id: "sloth.dev/core/debug/v1" 20 | priority: 9999999 21 | config: {msg: "Plugin 99"} 22 | - id: "sloth.dev/core/debug/v1" 23 | priority: -999999 24 | config: {msg: "Plugin 0"} 25 | slos: 26 | - name: "requests-availability" 27 | objective: 99.9 28 | description: "Common SLO based on availability for HTTP request responses." 29 | plugins: 30 | chain: 31 | - id: "sloth.dev/core/debug/v1" 32 | priority: 1050 33 | config: {msg: "Plugin 5"} 34 | - id: "sloth.dev/core/debug/v1" 35 | priority: -1000 36 | config: {msg: "Plugin 1"} 37 | - id: "sloth.dev/core/debug/v1" 38 | priority: 1000 39 | config: {msg: "Plugin 4"} 40 | - id: "sloth.dev/core/debug/v1" 41 | priority: -200 42 | config: {msg: "Plugin 2"} 43 | - id: "sloth.dev/core/debug/v1" 44 | config: {msg: "Plugin 3"} 45 | sli: 46 | events: 47 | errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 48 | totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 49 | alerting: 50 | name: MyServiceHighErrorRate 51 | labels: 52 | category: "availability" 53 | annotations: 54 | summary: "High error rate on 'myservice' requests responses" 55 | pageAlert: 56 | labels: 57 | severity: pageteam 58 | routing_key: myteam 59 | ticketAlert: 60 | labels: 61 | severity: "slack" 62 | slack_channel: "#alerts-myteam" 63 | -------------------------------------------------------------------------------- /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/alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/slok/sloth/pkg/common/model" 9 | ) 10 | 11 | // WindowsRepo knows how to retrieve windows based on the period of time. 12 | type WindowsRepo interface { 13 | GetWindows(ctx context.Context, period time.Duration) (*Windows, error) 14 | } 15 | 16 | // Generator knows how to generate all the required alerts based on an SLO. 17 | // The generated alerts are generic and don't depend on any specific SLO implementation. 18 | type Generator struct { 19 | windowsRepo WindowsRepo 20 | } 21 | 22 | func NewGenerator(windowsRepo WindowsRepo) Generator { 23 | return Generator{ 24 | windowsRepo: windowsRepo, 25 | } 26 | } 27 | 28 | type SLO struct { 29 | ID string 30 | TimeWindow time.Duration 31 | Objective float64 32 | } 33 | 34 | func (g Generator) GenerateMWMBAlerts(ctx context.Context, slo SLO) (*model.MWMBAlertGroup, error) { 35 | windows, err := g.windowsRepo.GetWindows(ctx, slo.TimeWindow) 36 | if err != nil { 37 | return nil, fmt.Errorf("the %s SLO period time window is not supported", slo.TimeWindow) 38 | } 39 | 40 | errorBudget := 100 - slo.Objective 41 | 42 | group := model.MWMBAlertGroup{ 43 | PageQuick: model.MWMBAlert{ 44 | ID: fmt.Sprintf("%s-page-quick", slo.ID), 45 | ShortWindow: windows.PageQuick.ShortWindow, 46 | LongWindow: windows.PageQuick.LongWindow, 47 | BurnRateFactor: windows.GetSpeedPageQuick(), 48 | ErrorBudget: errorBudget, 49 | Severity: model.PageAlertSeverity, 50 | }, 51 | PageSlow: model.MWMBAlert{ 52 | ID: fmt.Sprintf("%s-page-slow", slo.ID), 53 | ShortWindow: windows.PageSlow.ShortWindow, 54 | LongWindow: windows.PageSlow.LongWindow, 55 | BurnRateFactor: windows.GetSpeedPageSlow(), 56 | ErrorBudget: errorBudget, 57 | Severity: model.PageAlertSeverity, 58 | }, 59 | TicketQuick: model.MWMBAlert{ 60 | ID: fmt.Sprintf("%s-ticket-quick", slo.ID), 61 | ShortWindow: windows.TicketQuick.ShortWindow, 62 | LongWindow: windows.TicketQuick.LongWindow, 63 | BurnRateFactor: windows.GetSpeedTicketQuick(), 64 | ErrorBudget: errorBudget, 65 | Severity: model.TicketAlertSeverity, 66 | }, 67 | TicketSlow: model.MWMBAlert{ 68 | ID: fmt.Sprintf("%s-ticket-slow", slo.ID), 69 | ShortWindow: windows.TicketSlow.ShortWindow, 70 | LongWindow: windows.TicketSlow.LongWindow, 71 | BurnRateFactor: windows.GetSpeedTicketSlow(), 72 | ErrorBudget: errorBudget, 73 | Severity: model.TicketAlertSeverity, 74 | }, 75 | } 76 | 77 | return &group, nil 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/app/generate/process.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/slok/sloth/internal/log" 9 | "github.com/slok/sloth/pkg/common/model" 10 | pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" 11 | ) 12 | 13 | type SLOProcessorRequest struct { 14 | Info model.Info 15 | SLO model.PromSLO 16 | SLOGroup model.PromSLOGroup 17 | MWMBAlertGroup model.MWMBAlertGroup 18 | } 19 | type SLOProcessorResult struct { 20 | SLORules model.PromSLORules 21 | } 22 | 23 | // SLOProcessor is the interface that will be used to process SLO and generate rules. 24 | // This is an abstraction to be able to support multiple and different SLO plugin versions 25 | // or custom internal processors. 26 | type SLOProcessor interface { 27 | ProcessSLO(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error 28 | } 29 | 30 | // SLOProcessorFunc is a helper function to create processors easily. 31 | type SLOProcessorFunc func(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error 32 | 33 | func (s SLOProcessorFunc) ProcessSLO(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error { 34 | return s(ctx, req, res) 35 | } 36 | 37 | // NewSLOProcessorFromSLOPluginV1 will be able to map a SLO plugin v1 to the SLOProcessor interface. 38 | func NewSLOProcessorFromSLOPluginV1(pluginFactory pluginslov1.PluginFactory, logger log.Logger, config any) (SLOProcessor, error) { 39 | configData, err := json.Marshal(config) 40 | if err != nil { 41 | return nil, fmt.Errorf("could not marshal config: %w", err) 42 | } 43 | 44 | plugin, err := pluginFactory(configData, pluginslov1.AppUtils{Logger: logger}) 45 | if err != nil { 46 | return nil, fmt.Errorf("could not create plugin: %w", err) 47 | } 48 | 49 | return SLOProcessorFunc(func(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error { 50 | // Map models for slo plugin V1 version. 51 | r := &pluginslov1.Request{ 52 | Info: req.Info, 53 | SLO: req.SLO, 54 | MWMBAlertGroup: req.MWMBAlertGroup, 55 | OriginalSource: req.SLOGroup.OriginalSource, 56 | } 57 | rs := &pluginslov1.Result{ 58 | SLORules: res.SLORules, 59 | } 60 | 61 | // Process plugin. 62 | err := plugin.ProcessSLO(ctx, r, rs) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // Unmap models for slo plugin V1 version. 68 | req.Info = r.Info 69 | req.SLO = r.SLO 70 | req.MWMBAlertGroup = r.MWMBAlertGroup 71 | res.SLORules = rs.SLORules 72 | 73 | return nil 74 | }), nil 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/info/info.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | var ( 4 | // Version is the version app. 5 | Version = "dev" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "context" 4 | 5 | // Kv is a helper type for structured logging fields usage. 6 | type Kv = map[string]interface{} 7 | 8 | // Logger is the interface that the loggers used by the library will use. 9 | type Logger interface { 10 | Infof(format string, args ...interface{}) 11 | Warningf(format string, args ...interface{}) 12 | Errorf(format string, args ...interface{}) 13 | Debugf(format string, args ...interface{}) 14 | WithValues(values map[string]interface{}) Logger 15 | WithCtxValues(ctx context.Context) Logger 16 | SetValuesOnCtx(parent context.Context, values map[string]interface{}) context.Context 17 | } 18 | 19 | // Noop logger doesn't log anything. 20 | const Noop = noop(0) 21 | 22 | type noop int 23 | 24 | func (n noop) Infof(format string, args ...interface{}) {} 25 | func (n noop) Warningf(format string, args ...interface{}) {} 26 | func (n noop) Errorf(format string, args ...interface{}) {} 27 | func (n noop) Debugf(format string, args ...interface{}) {} 28 | func (n noop) WithValues(map[string]interface{}) Logger { return n } 29 | func (n noop) WithCtxValues(context.Context) Logger { return n } 30 | func (n noop) SetValuesOnCtx(parent context.Context, values Kv) context.Context { return parent } 31 | 32 | type contextKey string 33 | 34 | // contextLogValuesKey used as unique key to store log values in the context. 35 | const contextLogValuesKey = contextKey("internal-log") 36 | 37 | // CtxWithValues returns a copy of parent in which the key values passed have been 38 | // stored ready to be used using log.Logger. 39 | func CtxWithValues(parent context.Context, kv Kv) context.Context { 40 | // Maybe we have values already set. 41 | oldValues, ok := parent.Value(contextLogValuesKey).(Kv) 42 | if !ok { 43 | oldValues = Kv{} 44 | } 45 | 46 | // Copy old and received values into the new kv. 47 | newValues := Kv{} 48 | for k, v := range oldValues { 49 | newValues[k] = v 50 | } 51 | for k, v := range kv { 52 | newValues[k] = v 53 | } 54 | 55 | return context.WithValue(parent, contextLogValuesKey, newValues) 56 | } 57 | 58 | // ValuesFromCtx gets the log Key values from a context. 59 | func ValuesFromCtx(ctx context.Context) Kv { 60 | values, ok := ctx.Value(contextLogValuesKey).(Kv) 61 | if !ok { 62 | return Kv{} 63 | } 64 | 65 | return values 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/prometheus/common/model github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/promql/parser 10 | //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus github.com/slok/sloth/pkg/common/validation 11 | //go:generate yaegi extract --name custom github.com/caarlos0/env/v11 12 | 13 | // Symbols variable stores the map of custom Yaegi symbols per package. 14 | var Symbols = map[string]map[string]reflect.Value{} 15 | -------------------------------------------------------------------------------- /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/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/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/conventions'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/conventions" 7 | "go/constant" 8 | "go/token" 9 | "reflect" 10 | ) 11 | 12 | func init() { 13 | Symbols["github.com/slok/sloth/pkg/common/conventions/conventions"] = map[string]reflect.Value{ 14 | // function, constant and variable definitions 15 | "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), 16 | "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), 17 | "NameRegexp": reflect.ValueOf(&conventions.NameRegexp).Elem(), 18 | "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), 19 | "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), 20 | "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), 21 | "PromSLONameLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo\"", token.STRING, 0)), 22 | "PromSLOObjectiveLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_objective\"", token.STRING, 0)), 23 | "PromSLOServiceLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_service\"", token.STRING, 0)), 24 | "PromSLOSeverityLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_severity\"", token.STRING, 0)), 25 | "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), 26 | "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), 27 | "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), 28 | "TplWindowRegex": reflect.ValueOf(&conventions.TplWindowRegex).Elem(), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/model'. DO NOT EDIT. 2 | 3 | package custom 4 | 5 | import ( 6 | "github.com/slok/sloth/pkg/common/model" 7 | "go/constant" 8 | "go/token" 9 | "reflect" 10 | ) 11 | 12 | func init() { 13 | Symbols["github.com/slok/sloth/pkg/common/model/model"] = map[string]reflect.Value{ 14 | // function, constant and variable definitions 15 | "ModeCLIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-k8s\"", token.STRING, 0)), 16 | "ModeCLIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-openslo\"", token.STRING, 0)), 17 | "ModeCLIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-prom\"", token.STRING, 0)), 18 | "ModeControllerGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"ctrl-gen-k8s\"", token.STRING, 0)), 19 | "ModeTest": reflect.ValueOf(constant.MakeFromLiteral("\"test\"", token.STRING, 0)), 20 | "PageAlertSeverity": reflect.ValueOf(model.PageAlertSeverity), 21 | "TicketAlertSeverity": reflect.ValueOf(model.TicketAlertSeverity), 22 | "UnknownAlertSeverity": reflect.ValueOf(model.UnknownAlertSeverity), 23 | 24 | // type definitions 25 | "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), 26 | "Info": reflect.ValueOf((*model.Info)(nil)), 27 | "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), 28 | "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), 29 | "Mode": reflect.ValueOf((*model.Mode)(nil)), 30 | "PromAlertMeta": reflect.ValueOf((*model.PromAlertMeta)(nil)), 31 | "PromRuleGroup": reflect.ValueOf((*model.PromRuleGroup)(nil)), 32 | "PromSLI": reflect.ValueOf((*model.PromSLI)(nil)), 33 | "PromSLIEvents": reflect.ValueOf((*model.PromSLIEvents)(nil)), 34 | "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), 35 | "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), 36 | "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), 37 | "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), 38 | "PromSLOPluginMetadata": reflect.ValueOf((*model.PromSLOPluginMetadata)(nil)), 39 | "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), 40 | "SLOPlugins": reflect.ValueOf((*model.SLOPlugins)(nil)), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | "TimeDurationToPromStr": reflect.ValueOf(prometheus.TimeDurationToPromStr), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /internal/storage/io/prometheus_operator.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/serializer/json" 11 | 12 | kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" 13 | "github.com/slok/sloth/internal/log" 14 | "github.com/slok/sloth/internal/storage" 15 | ) 16 | 17 | func NewIOWriterPrometheusOperatorYAMLRepo(writer io.Writer, logger log.Logger) IOWriterPrometheusOperatorYAMLRepo { 18 | return IOWriterPrometheusOperatorYAMLRepo{ 19 | writer: writer, 20 | encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), 21 | logger: logger.WithValues(log.Kv{"svc": "storage.io.IOWriterPrometheusOperatorYAMLRepo"}), 22 | } 23 | } 24 | 25 | // IOWriterPrometheusOperatorYAMLRepo knows to store all the SLO rules (recordings and alerts) 26 | // grouped in an IOWriter in Kubernetes prometheus operator YAML format. 27 | type IOWriterPrometheusOperatorYAMLRepo struct { 28 | writer io.Writer 29 | encoder runtime.Encoder 30 | logger log.Logger 31 | } 32 | 33 | func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { 34 | rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) 35 | if err != nil { 36 | return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) 37 | } 38 | 39 | var b bytes.Buffer 40 | err = i.encoder.Encode(rule, &b) 41 | if err != nil { 42 | return fmt.Errorf("could encode prometheus operator object: %w", err) 43 | } 44 | 45 | rulesYaml := writeYAMLTopDisclaimer(b.Bytes()) 46 | _, err = i.writer.Write(rulesYaml) 47 | if err != nil { 48 | return fmt.Errorf("could not write top disclaimer: %w", err) 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /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/internal/storage" 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 storage.K8sMeta, slos []storage.SLORulesResult) error { 41 | r.logger.Infof("Dry run StoreSLOs") 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import "github.com/slok/sloth/pkg/common/model" 4 | 5 | // K8sMeta is the Kubernetes metadata simplified used for storage purposes. 6 | type K8sMeta struct { 7 | Kind string `validate:"required"` 8 | APIVersion string `validate:"required"` 9 | Name string `validate:"required"` 10 | UID string 11 | Namespace string 12 | Annotations map[string]string 13 | Labels map[string]string 14 | } 15 | 16 | // SLORulesResult is a common type used to store final SLO rules result in batches. 17 | type SLORulesResult struct { 18 | K8sMeta K8sMeta 19 | SLO model.PromSLO 20 | Rules model.PromSLORules 21 | } 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 | NameRegexp = regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`) 11 | 12 | // TplWindowRegex is the regex to match the {{ .window }} template variable. 13 | TplWindowRegex = regexp.MustCompile(`{{ *\.window *}}`) 14 | ) 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. 8 | PromSLIErrorMetricFmt = "slo:sli_error:ratio_rate%s" 9 | 10 | // Labels. 11 | PromSLONameLabelName = "sloth_slo" 12 | PromSLOIDLabelName = "sloth_id" 13 | PromSLOServiceLabelName = "sloth_service" 14 | PromSLOWindowLabelName = "sloth_window" 15 | PromSLOSeverityLabelName = "sloth_severity" 16 | PromSLOVersionLabelName = "sloth_version" 17 | PromSLOModeLabelName = "sloth_mode" 18 | PromSLOSpecLabelName = "sloth_spec" 19 | PromSLOObjectiveLabelName = "sloth_objective" 20 | ) 21 | 22 | // GetSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify 23 | // an SLO recorded metrics and alerts. 24 | func GetSLOIDPromLabels(s model.PromSLO) map[string]string { 25 | return map[string]string{ 26 | PromSLOIDLabelName: s.ID, 27 | PromSLONameLabelName: s.Name, 28 | PromSLOServiceLabelName: s.Service, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/common/model/alert.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // AlertSeverity is the type of alert. 6 | type AlertSeverity int 7 | 8 | const ( 9 | UnknownAlertSeverity AlertSeverity = iota 10 | PageAlertSeverity 11 | TicketAlertSeverity 12 | ) 13 | 14 | func (s AlertSeverity) String() string { 15 | switch s { 16 | case PageAlertSeverity: 17 | return "page" 18 | case TicketAlertSeverity: 19 | return "ticket" 20 | default: 21 | return "unknown" 22 | } 23 | } 24 | 25 | // MWMBAlert represents a multiwindow, multi-burn rate alert. 26 | type MWMBAlert struct { 27 | ID string 28 | ShortWindow time.Duration 29 | LongWindow time.Duration 30 | BurnRateFactor float64 31 | ErrorBudget float64 32 | Severity AlertSeverity 33 | } 34 | 35 | // MWMBAlertGroup what represents all the alerts of an SLO. 36 | // ITs divided into two groups that are made of 2 alerts: 37 | // - Page & quick: Critical alerts that trigger in high rate burn in short term. 38 | // - Page & slow: Critical alerts that trigger in high-normal rate burn in medium term. 39 | // - Ticket & slow: Warning alerts that trigger in normal rate burn in medium term. 40 | // - Ticket & slow: Warning alerts that trigger in slow rate burn in long term. 41 | type MWMBAlertGroup struct { 42 | PageQuick MWMBAlert 43 | PageSlow MWMBAlert 44 | TicketQuick MWMBAlert 45 | TicketSlow MWMBAlert 46 | } 47 | -------------------------------------------------------------------------------- /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 | ModeCLIGenKubernetes = "cli-gen-k8s" 9 | ModeCLIGenOpenSLO = "cli-gen-openslo" 10 | ModeControllerGenKubernetes = "ctrl-gen-k8s" 11 | ) 12 | 13 | // Info is the information of the app and request based for SLO generators. 14 | type Info struct { 15 | Version string 16 | Mode Mode 17 | Spec string 18 | } 19 | -------------------------------------------------------------------------------- /pkg/common/model/slo_prometheus.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | openslov1alpha "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" 7 | "github.com/prometheus/prometheus/model/rulefmt" 8 | 9 | k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 10 | prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" 11 | ) 12 | 13 | // SLI represents an SLI with custom error and total expressions. 14 | type PromSLI struct { 15 | Raw *PromSLIRaw 16 | Events *PromSLIEvents 17 | } 18 | 19 | type PromSLIRaw struct { 20 | ErrorRatioQuery string 21 | } 22 | 23 | type PromSLIEvents struct { 24 | ErrorQuery string 25 | TotalQuery string 26 | } 27 | 28 | // AlertMeta is the metadata of an alert settings. 29 | type PromAlertMeta struct { 30 | Disable bool 31 | Name string 32 | Labels map[string]string 33 | Annotations map[string]string 34 | } 35 | 36 | // PromSLO represents a service level objective configuration. 37 | type PromSLO struct { 38 | ID string 39 | Name string 40 | Description string 41 | Service string 42 | SLI PromSLI 43 | TimeWindow time.Duration 44 | Objective float64 45 | Labels map[string]string 46 | PageAlertMeta PromAlertMeta 47 | TicketAlertMeta PromAlertMeta 48 | Plugins SLOPlugins 49 | } 50 | 51 | type SLOPlugins struct { 52 | OverridePlugins bool // If true, the default, app and other declared plugins at other levels will be overridden by the ones declared in this struct. 53 | Plugins []PromSLOPluginMetadata 54 | } 55 | 56 | type PromSLOPluginMetadata struct { 57 | ID string 58 | Config any 59 | Priority int 60 | } 61 | 62 | type PromSLOGroup struct { 63 | SLOs []PromSLO 64 | OriginalSource PromSLOGroupSource 65 | } 66 | 67 | // Used to store the original source of the SLO group in case we need to make low-level decision 68 | // based on where the SLOs came from. 69 | type PromSLOGroupSource struct { 70 | K8sSlothV1 *k8sprometheusv1.PrometheusServiceLevel 71 | SlothV1 *prometheusv1.Spec 72 | OpenSLOV1Alpha *openslov1alpha.SLO 73 | } 74 | 75 | // PromSLORules are the prometheus rules required by an SLO. 76 | type PromSLORules struct { 77 | SLIErrorRecRules PromRuleGroup 78 | MetadataRecRules PromRuleGroup 79 | AlertRules PromRuleGroup 80 | } 81 | 82 | // PromRuleGroup are regular prometheus group of rules. 83 | type PromRuleGroup struct { 84 | Interval time.Duration 85 | Rules []rulefmt.Rule 86 | } 87 | -------------------------------------------------------------------------------- /pkg/common/utils/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "maps" 4 | 5 | func MergeMaps[M ~map[K]V, K comparable, V any](ms ...M) M { 6 | m := make(M) 7 | for _, m2 := range ms { 8 | maps.Copy(m, m2) 9 | } 10 | return m 11 | } 12 | 13 | type mss = map[string]string 14 | 15 | func MergeLabels(ms ...mss) mss { 16 | res := mss{} 17 | for _, m := range ms { 18 | for k, v := range m { 19 | res[k] = v 20 | } 21 | } 22 | return res 23 | } 24 | -------------------------------------------------------------------------------- /pkg/common/utils/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "time" 5 | 6 | prommodel "github.com/prometheus/common/model" 7 | ) 8 | 9 | // TimeDurationToPromStr converts from std duration to prom string duration. 10 | func TimeDurationToPromStr(t time.Duration) string { 11 | return prommodel.Duration(t).String() 12 | } 13 | 14 | // LabelsToPromFilter converts a map of labels to a Prometheus filter string. 15 | func LabelsToPromFilter(labels map[string]string) string { 16 | metricFilters := prommodel.LabelSet{} 17 | for k, v := range labels { 18 | metricFilters[prommodel.LabelName(k)] = prommodel.LabelValue(v) 19 | } 20 | 21 | return metricFilters.String() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/common/validation/promql.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "text/template" 7 | 8 | prommodel "github.com/prometheus/common/model" 9 | promqlparser "github.com/prometheus/prometheus/promql/parser" 10 | ) 11 | 12 | // PromQLDialectValidator is the SLO flavour validator for prometheus backends dialect: PromQL. 13 | const PromQLDialectValidator = promQLDialectValidator(false) 14 | 15 | type promQLDialectValidator bool 16 | 17 | func (promQLDialectValidator) ValidateLabelKey(k string) error { 18 | if k == prommodel.MetricNameLabel { 19 | return fmt.Errorf("the label key %q is not allowed", prommodel.MetricNameLabel) 20 | } 21 | if !prommodel.LabelName(k).IsValid() { 22 | return fmt.Errorf("the label key %q is not valid", k) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (promQLDialectValidator) ValidateLabelValue(k string) error { 29 | if k == "" { 30 | return fmt.Errorf("the label value is required") 31 | } 32 | 33 | if !prommodel.LabelValue(k).IsValid() { 34 | return fmt.Errorf("the label value %q is not valid", k) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (promQLDialectValidator) ValidateAnnotationKey(k string) error { 41 | if !prommodel.LabelName(k).IsValid() { 42 | return fmt.Errorf("the annotation key %q is not valid", k) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (promQLDialectValidator) ValidateAnnotationValue(k string) error { 49 | if k == "" { 50 | return fmt.Errorf("the annotation value is required") 51 | } 52 | 53 | return nil 54 | } 55 | 56 | var promExprTplAllowedFakeData = map[string]string{"window": "1m"} 57 | 58 | func (promQLDialectValidator) ValidateQueryExpression(queryExpression string) error { 59 | if queryExpression == "" { 60 | return fmt.Errorf("query is required") 61 | } 62 | 63 | // The expressions set by users can have some allowed templated data. 64 | // We are rendering the expression with fake data so prometheus can 65 | // have a final expr and check if is correct. 66 | tpl, err := template.New("expr").Parse(queryExpression) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | var tplB bytes.Buffer 72 | err = tpl.Execute(&tplB, promExprTplAllowedFakeData) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | _, err = promqlparser.ParseExpr(tplB.String()) 78 | 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /pkg/kubernetes/api/sloth/register.go: -------------------------------------------------------------------------------- 1 | package sloth 2 | 3 | const ( 4 | GroupName = "sloth.slok.dev" 5 | ) 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/v4/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/kubernetes/gen/applyconfiguration/sloth/v1/alert.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // AlertApplyConfiguration represents a declarative configuration of the Alert type for use 6 | // with apply. 7 | type AlertApplyConfiguration struct { 8 | Disable *bool `json:"disable,omitempty"` 9 | Labels map[string]string `json:"labels,omitempty"` 10 | Annotations map[string]string `json:"annotations,omitempty"` 11 | } 12 | 13 | // AlertApplyConfiguration constructs a declarative configuration of the Alert type for use with 14 | // apply. 15 | func Alert() *AlertApplyConfiguration { 16 | return &AlertApplyConfiguration{} 17 | } 18 | 19 | // WithDisable sets the Disable field in the declarative configuration to the given value 20 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 21 | // If called multiple times, the Disable field is set to the value of the last call. 22 | func (b *AlertApplyConfiguration) WithDisable(value bool) *AlertApplyConfiguration { 23 | b.Disable = &value 24 | return b 25 | } 26 | 27 | // WithLabels puts the entries into the Labels field in the declarative configuration 28 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 29 | // If called multiple times, the entries provided by each call will be put on the Labels field, 30 | // overwriting an existing map entries in Labels field with the same key. 31 | func (b *AlertApplyConfiguration) WithLabels(entries map[string]string) *AlertApplyConfiguration { 32 | if b.Labels == nil && len(entries) > 0 { 33 | b.Labels = make(map[string]string, len(entries)) 34 | } 35 | for k, v := range entries { 36 | b.Labels[k] = v 37 | } 38 | return b 39 | } 40 | 41 | // WithAnnotations puts the entries into the Annotations field in the declarative configuration 42 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 43 | // If called multiple times, the entries provided by each call will be put on the Annotations field, 44 | // overwriting an existing map entries in Annotations field with the same key. 45 | func (b *AlertApplyConfiguration) WithAnnotations(entries map[string]string) *AlertApplyConfiguration { 46 | if b.Annotations == nil && len(entries) > 0 { 47 | b.Annotations = make(map[string]string, len(entries)) 48 | } 49 | for k, v := range entries { 50 | b.Annotations[k] = v 51 | } 52 | return b 53 | } 54 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/sli.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SLIApplyConfiguration represents a declarative configuration of the SLI type for use 6 | // with apply. 7 | type SLIApplyConfiguration struct { 8 | Raw *SLIRawApplyConfiguration `json:"raw,omitempty"` 9 | Events *SLIEventsApplyConfiguration `json:"events,omitempty"` 10 | Plugin *SLIPluginApplyConfiguration `json:"plugin,omitempty"` 11 | } 12 | 13 | // SLIApplyConfiguration constructs a declarative configuration of the SLI type for use with 14 | // apply. 15 | func SLI() *SLIApplyConfiguration { 16 | return &SLIApplyConfiguration{} 17 | } 18 | 19 | // WithRaw sets the Raw field in the declarative configuration to the given value 20 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 21 | // If called multiple times, the Raw field is set to the value of the last call. 22 | func (b *SLIApplyConfiguration) WithRaw(value *SLIRawApplyConfiguration) *SLIApplyConfiguration { 23 | b.Raw = value 24 | return b 25 | } 26 | 27 | // WithEvents sets the Events 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 Events field is set to the value of the last call. 30 | func (b *SLIApplyConfiguration) WithEvents(value *SLIEventsApplyConfiguration) *SLIApplyConfiguration { 31 | b.Events = value 32 | return b 33 | } 34 | 35 | // WithPlugin sets the Plugin 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 Plugin field is set to the value of the last call. 38 | func (b *SLIApplyConfiguration) WithPlugin(value *SLIPluginApplyConfiguration) *SLIApplyConfiguration { 39 | b.Plugin = value 40 | return b 41 | } 42 | -------------------------------------------------------------------------------- /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 | type SLIEventsApplyConfiguration struct { 8 | ErrorQuery *string `json:"errorQuery,omitempty"` 9 | TotalQuery *string `json:"totalQuery,omitempty"` 10 | } 11 | 12 | // SLIEventsApplyConfiguration constructs a declarative configuration of the SLIEvents type for use with 13 | // apply. 14 | func SLIEvents() *SLIEventsApplyConfiguration { 15 | return &SLIEventsApplyConfiguration{} 16 | } 17 | 18 | // WithErrorQuery sets the ErrorQuery field in the declarative configuration to the given value 19 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 20 | // If called multiple times, the ErrorQuery field is set to the value of the last call. 21 | func (b *SLIEventsApplyConfiguration) WithErrorQuery(value string) *SLIEventsApplyConfiguration { 22 | b.ErrorQuery = &value 23 | return b 24 | } 25 | 26 | // WithTotalQuery sets the TotalQuery field in the declarative configuration to the given value 27 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 28 | // If called multiple times, the TotalQuery field is set to the value of the last call. 29 | func (b *SLIEventsApplyConfiguration) WithTotalQuery(value string) *SLIEventsApplyConfiguration { 30 | b.TotalQuery = &value 31 | return b 32 | } 33 | -------------------------------------------------------------------------------- /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 | type SLIPluginApplyConfiguration struct { 8 | ID *string `json:"id,omitempty"` 9 | Options map[string]string `json:"options,omitempty"` 10 | } 11 | 12 | // SLIPluginApplyConfiguration constructs a declarative configuration of the SLIPlugin type for use with 13 | // apply. 14 | func SLIPlugin() *SLIPluginApplyConfiguration { 15 | return &SLIPluginApplyConfiguration{} 16 | } 17 | 18 | // WithID sets the ID field in the declarative configuration to the given value 19 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 20 | // If called multiple times, the ID field is set to the value of the last call. 21 | func (b *SLIPluginApplyConfiguration) WithID(value string) *SLIPluginApplyConfiguration { 22 | b.ID = &value 23 | return b 24 | } 25 | 26 | // WithOptions puts the entries into the Options field in the declarative configuration 27 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 28 | // If called multiple times, the entries provided by each call will be put on the Options field, 29 | // overwriting an existing map entries in Options field with the same key. 30 | func (b *SLIPluginApplyConfiguration) WithOptions(entries map[string]string) *SLIPluginApplyConfiguration { 31 | if b.Options == nil && len(entries) > 0 { 32 | b.Options = make(map[string]string, len(entries)) 33 | } 34 | for k, v := range entries { 35 | b.Options[k] = v 36 | } 37 | return b 38 | } 39 | -------------------------------------------------------------------------------- /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 | type SLIRawApplyConfiguration struct { 8 | ErrorRatioQuery *string `json:"errorRatioQuery,omitempty"` 9 | } 10 | 11 | // SLIRawApplyConfiguration constructs a declarative configuration of the SLIRaw type for use with 12 | // apply. 13 | func SLIRaw() *SLIRawApplyConfiguration { 14 | return &SLIRawApplyConfiguration{} 15 | } 16 | 17 | // WithErrorRatioQuery sets the ErrorRatioQuery field in the declarative configuration to the given value 18 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 19 | // If called multiple times, the ErrorRatioQuery field is set to the value of the last call. 20 | func (b *SLIRawApplyConfiguration) WithErrorRatioQuery(value string) *SLIRawApplyConfiguration { 21 | b.ErrorRatioQuery = &value 22 | return b 23 | } 24 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | json "encoding/json" 7 | ) 8 | 9 | // SLOPluginApplyConfiguration represents a declarative configuration of the SLOPlugin type for use 10 | // with apply. 11 | type SLOPluginApplyConfiguration struct { 12 | ID *string `json:"id,omitempty"` 13 | Config *json.RawMessage `json:"config,omitempty"` 14 | Priority *int `json:"priority,omitempty"` 15 | } 16 | 17 | // SLOPluginApplyConfiguration constructs a declarative configuration of the SLOPlugin type for use with 18 | // apply. 19 | func SLOPlugin() *SLOPluginApplyConfiguration { 20 | return &SLOPluginApplyConfiguration{} 21 | } 22 | 23 | // WithID sets the ID field in the declarative configuration to the given value 24 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 25 | // If called multiple times, the ID field is set to the value of the last call. 26 | func (b *SLOPluginApplyConfiguration) WithID(value string) *SLOPluginApplyConfiguration { 27 | b.ID = &value 28 | return b 29 | } 30 | 31 | // WithConfig sets the Config field in the declarative configuration to the given value 32 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 33 | // If called multiple times, the Config field is set to the value of the last call. 34 | func (b *SLOPluginApplyConfiguration) WithConfig(value json.RawMessage) *SLOPluginApplyConfiguration { 35 | b.Config = &value 36 | return b 37 | } 38 | 39 | // WithPriority sets the Priority field in the declarative configuration to the given value 40 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 41 | // If called multiple times, the Priority field is set to the value of the last call. 42 | func (b *SLOPluginApplyConfiguration) WithPriority(value int) *SLOPluginApplyConfiguration { 43 | b.Priority = &value 44 | return b 45 | } 46 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | // SLOPluginsApplyConfiguration represents a declarative configuration of the SLOPlugins type for use 6 | // with apply. 7 | type SLOPluginsApplyConfiguration struct { 8 | OverridePrevious *bool `json:"overridePrevious,omitempty"` 9 | Chain []SLOPluginApplyConfiguration `json:"chain,omitempty"` 10 | } 11 | 12 | // SLOPluginsApplyConfiguration constructs a declarative configuration of the SLOPlugins type for use with 13 | // apply. 14 | func SLOPlugins() *SLOPluginsApplyConfiguration { 15 | return &SLOPluginsApplyConfiguration{} 16 | } 17 | 18 | // WithOverridePrevious sets the OverridePrevious field in the declarative configuration to the given value 19 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 20 | // If called multiple times, the OverridePrevious field is set to the value of the last call. 21 | func (b *SLOPluginsApplyConfiguration) WithOverridePrevious(value bool) *SLOPluginsApplyConfiguration { 22 | b.OverridePrevious = &value 23 | return b 24 | } 25 | 26 | // WithChain adds the given value to the Chain field in the declarative configuration 27 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 28 | // If called multiple times, values provided by each call will be appended to the Chain field. 29 | func (b *SLOPluginsApplyConfiguration) WithChain(values ...*SLOPluginApplyConfiguration) *SLOPluginsApplyConfiguration { 30 | for i := range values { 31 | if values[i] == nil { 32 | panic("nil value passed to WithChain") 33 | } 34 | b.Chain = append(b.Chain, *values[i]) 35 | } 36 | return b 37 | } 38 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/applyconfiguration/utils.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package applyconfiguration 4 | 5 | import ( 6 | v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 7 | internal "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/internal" 8 | slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" 9 | runtime "k8s.io/apimachinery/pkg/runtime" 10 | schema "k8s.io/apimachinery/pkg/runtime/schema" 11 | testing "k8s.io/client-go/testing" 12 | ) 13 | 14 | // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no 15 | // apply configuration type exists for the given GroupVersionKind. 16 | func ForKind(kind schema.GroupVersionKind) interface{} { 17 | switch kind { 18 | // Group=sloth.slok.dev, Version=v1 19 | case v1.SchemeGroupVersion.WithKind("Alert"): 20 | return &slothv1.AlertApplyConfiguration{} 21 | case v1.SchemeGroupVersion.WithKind("Alerting"): 22 | return &slothv1.AlertingApplyConfiguration{} 23 | case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevel"): 24 | return &slothv1.PrometheusServiceLevelApplyConfiguration{} 25 | case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevelSpec"): 26 | return &slothv1.PrometheusServiceLevelSpecApplyConfiguration{} 27 | case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevelStatus"): 28 | return &slothv1.PrometheusServiceLevelStatusApplyConfiguration{} 29 | case v1.SchemeGroupVersion.WithKind("SLI"): 30 | return &slothv1.SLIApplyConfiguration{} 31 | case v1.SchemeGroupVersion.WithKind("SLIEvents"): 32 | return &slothv1.SLIEventsApplyConfiguration{} 33 | case v1.SchemeGroupVersion.WithKind("SLIPlugin"): 34 | return &slothv1.SLIPluginApplyConfiguration{} 35 | case v1.SchemeGroupVersion.WithKind("SLIRaw"): 36 | return &slothv1.SLIRawApplyConfiguration{} 37 | case v1.SchemeGroupVersion.WithKind("SLO"): 38 | return &slothv1.SLOApplyConfiguration{} 39 | case v1.SchemeGroupVersion.WithKind("SLOPlugin"): 40 | return &slothv1.SLOPluginApplyConfiguration{} 41 | case v1.SchemeGroupVersion.WithKind("SLOPlugins"): 42 | return &slothv1.SLOPluginsApplyConfiguration{} 43 | 44 | } 45 | return nil 46 | } 47 | 48 | func NewTypeConverter(scheme *runtime.Scheme) *testing.TypeConverter { 49 | return &testing.TypeConverter{Scheme: scheme, TypeResolver: internal.Parser()} 50 | } 51 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | http "net/http" 7 | 8 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 9 | scheme "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" 10 | rest "k8s.io/client-go/rest" 11 | ) 12 | 13 | type SlothV1Interface interface { 14 | RESTClient() rest.Interface 15 | PrometheusServiceLevelsGetter 16 | } 17 | 18 | // SlothV1Client is used to interact with features provided by the sloth.slok.dev group. 19 | type SlothV1Client struct { 20 | restClient rest.Interface 21 | } 22 | 23 | func (c *SlothV1Client) PrometheusServiceLevels(namespace string) PrometheusServiceLevelInterface { 24 | return newPrometheusServiceLevels(c, namespace) 25 | } 26 | 27 | // NewForConfig creates a new SlothV1Client for the given config. 28 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 29 | // where httpClient was generated with rest.HTTPClientFor(c). 30 | func NewForConfig(c *rest.Config) (*SlothV1Client, error) { 31 | config := *c 32 | setConfigDefaults(&config) 33 | httpClient, err := rest.HTTPClientFor(&config) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return NewForConfigAndClient(&config, httpClient) 38 | } 39 | 40 | // NewForConfigAndClient creates a new SlothV1Client for the given config and http client. 41 | // Note the http client provided takes precedence over the configured transport values. 42 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*SlothV1Client, error) { 43 | config := *c 44 | setConfigDefaults(&config) 45 | client, err := rest.RESTClientForConfigAndClient(&config, h) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &SlothV1Client{client}, nil 50 | } 51 | 52 | // NewForConfigOrDie creates a new SlothV1Client for the given config and 53 | // panics if there is an error in the config. 54 | func NewForConfigOrDie(c *rest.Config) *SlothV1Client { 55 | client, err := NewForConfig(c) 56 | if err != nil { 57 | panic(err) 58 | } 59 | return client 60 | } 61 | 62 | // New creates a new SlothV1Client for the given RESTClient. 63 | func New(c rest.Interface) *SlothV1Client { 64 | return &SlothV1Client{c} 65 | } 66 | 67 | func setConfigDefaults(config *rest.Config) { 68 | gv := slothv1.SchemeGroupVersion 69 | config.GroupVersion = &gv 70 | config.APIPath = "/apis" 71 | config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() 72 | 73 | if config.UserAgent == "" { 74 | config.UserAgent = rest.DefaultKubernetesUserAgent() 75 | } 76 | } 77 | 78 | // RESTClient returns a RESTClient that is used to communicate 79 | // with API server by this client implementation. 80 | func (c *SlothV1Client) RESTClient() rest.Interface { 81 | if c == nil { 82 | return nil 83 | } 84 | return c.restClient 85 | } 86 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/kubernetes/gen/listers/sloth/v1/prometheusservicelevel.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1 4 | 5 | import ( 6 | slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" 7 | labels "k8s.io/apimachinery/pkg/labels" 8 | listers "k8s.io/client-go/listers" 9 | cache "k8s.io/client-go/tools/cache" 10 | ) 11 | 12 | // PrometheusServiceLevelLister helps list PrometheusServiceLevels. 13 | // All objects returned here must be treated as read-only. 14 | type PrometheusServiceLevelLister interface { 15 | // List lists all PrometheusServiceLevels in the indexer. 16 | // Objects returned here must be treated as read-only. 17 | List(selector labels.Selector) (ret []*slothv1.PrometheusServiceLevel, err error) 18 | // PrometheusServiceLevels returns an object that can list and get PrometheusServiceLevels. 19 | PrometheusServiceLevels(namespace string) PrometheusServiceLevelNamespaceLister 20 | PrometheusServiceLevelListerExpansion 21 | } 22 | 23 | // prometheusServiceLevelLister implements the PrometheusServiceLevelLister interface. 24 | type prometheusServiceLevelLister struct { 25 | listers.ResourceIndexer[*slothv1.PrometheusServiceLevel] 26 | } 27 | 28 | // NewPrometheusServiceLevelLister returns a new PrometheusServiceLevelLister. 29 | func NewPrometheusServiceLevelLister(indexer cache.Indexer) PrometheusServiceLevelLister { 30 | return &prometheusServiceLevelLister{listers.New[*slothv1.PrometheusServiceLevel](indexer, slothv1.Resource("prometheusservicelevel"))} 31 | } 32 | 33 | // PrometheusServiceLevels returns an object that can list and get PrometheusServiceLevels. 34 | func (s *prometheusServiceLevelLister) PrometheusServiceLevels(namespace string) PrometheusServiceLevelNamespaceLister { 35 | return prometheusServiceLevelNamespaceLister{listers.NewNamespaced[*slothv1.PrometheusServiceLevel](s.ResourceIndexer, namespace)} 36 | } 37 | 38 | // PrometheusServiceLevelNamespaceLister helps list and get PrometheusServiceLevels. 39 | // All objects returned here must be treated as read-only. 40 | type PrometheusServiceLevelNamespaceLister interface { 41 | // List lists all PrometheusServiceLevels in the indexer for a given namespace. 42 | // Objects returned here must be treated as read-only. 43 | List(selector labels.Selector) (ret []*slothv1.PrometheusServiceLevel, err error) 44 | // Get retrieves the PrometheusServiceLevel from the indexer for a given namespace and name. 45 | // Objects returned here must be treated as read-only. 46 | Get(name string) (*slothv1.PrometheusServiceLevel, error) 47 | PrometheusServiceLevelNamespaceListerExpansion 48 | } 49 | 50 | // prometheusServiceLevelNamespaceLister implements the PrometheusServiceLevelNamespaceLister 51 | // interface. 52 | type prometheusServiceLevelNamespaceLister struct { 53 | listers.ResourceIndexer[*slothv1.PrometheusServiceLevel] 54 | } 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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}" . -------------------------------------------------------------------------------- /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}" . -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/check/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | golangci-lint run -------------------------------------------------------------------------------- /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/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/... -------------------------------------------------------------------------------- /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/... -------------------------------------------------------------------------------- /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}' -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /scripts/deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go mod tidy -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/gogen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | go generate ./... 7 | mockery 8 | -------------------------------------------------------------------------------- /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.7.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/in-multifile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "prometheus/v1" 3 | service: "svc01" 4 | labels: 5 | global01k1: global01v1 6 | slos: 7 | - name: "slo1" 8 | objective: 99.9 9 | description: "This is SLO 01." 10 | labels: 11 | global02k1: global02v1 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: myServiceAlert 18 | labels: 19 | alert01k1: "alert01v1" 20 | annotations: 21 | alert02k1: "alert02k2" 22 | page_alert: 23 | labels: 24 | alert03k1: "alert03v1" 25 | ticket_alert: 26 | labels: 27 | alert04k1: "alert04v1" 28 | - name: "slo02" 29 | objective: 95 30 | description: "This is SLO 02." 31 | labels: 32 | global03k1: global03v1 33 | sli: 34 | raw: 35 | error_ratio_query: | 36 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 37 | / 38 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 39 | alerting: 40 | page_alert: 41 | disable: true 42 | ticket_alert: 43 | disable: true 44 | 45 | --- 46 | version: "prometheus/v1" 47 | service: "svc02" 48 | labels: 49 | global01k1: global01v1 50 | slos: 51 | - name: "slo1" 52 | objective: 99.99 53 | description: "This is SLO 01." 54 | labels: 55 | global02k1: global02v1 56 | sli: 57 | events: 58 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 59 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 60 | alerting: 61 | name: myServiceAlert 62 | labels: 63 | alert01k1: "alert01v1" 64 | annotations: 65 | alert02k1: "alert02k2" 66 | page_alert: 67 | labels: 68 | alert03k1: "alert03v1" 69 | ticket_alert: 70 | labels: 71 | alert04k1: "alert04v1" 72 | - name: "slo02" 73 | objective: 95 74 | description: "This is SLO 02." 75 | labels: 76 | global03k1: global03v1 77 | sli: 78 | raw: 79 | error_ratio_query: | 80 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 81 | / 82 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 83 | alerting: 84 | page_alert: 85 | disable: true 86 | ticket_alert: 87 | disable: true 88 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | # Code generated by Sloth ({{ .version }}): https://github.com/slok/sloth. 4 | # DO NOT EDIT. 5 | 6 | groups: 7 | - name: sloth-slo-alerts-svc01-slo1 8 | rules: 9 | - alert: myServiceAlert 10 | expr: | 11 | ( 12 | max(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) 13 | and 14 | max(slo:sli_error:ratio_rate1h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) 15 | ) 16 | or 17 | ( 18 | max(slo:sli_error:ratio_rate30m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (6 * 0.0009999999999999432)) without (sloth_window) 19 | and 20 | max(slo:sli_error:ratio_rate6h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (6 * 0.0009999999999999432)) without (sloth_window) 21 | ) 22 | labels: 23 | alert01k1: alert01v1 24 | alert03k1: alert03v1 25 | sloth_severity: page 26 | annotations: 27 | alert02k1: alert02k2 28 | summary: '{{"{{$labels.sloth_service}}"}} {{"{{$labels.sloth_slo}}"}} SLO error budget burn 29 | rate is over expected.' 30 | title: (page) {{"{{$labels.sloth_service}}"}} {{"{{$labels.sloth_slo}}"}} SLO error budget 31 | burn rate is too fast. 32 | - alert: myServiceAlert 33 | expr: | 34 | ( 35 | max(slo:sli_error:ratio_rate2h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (3 * 0.0009999999999999432)) without (sloth_window) 36 | and 37 | max(slo:sli_error:ratio_rate1d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (3 * 0.0009999999999999432)) without (sloth_window) 38 | ) 39 | or 40 | ( 41 | max(slo:sli_error:ratio_rate6h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (1 * 0.0009999999999999432)) without (sloth_window) 42 | and 43 | max(slo:sli_error:ratio_rate3d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (1 * 0.0009999999999999432)) without (sloth_window) 44 | ) 45 | labels: 46 | alert01k1: alert01v1 47 | alert04k1: alert04v1 48 | sloth_severity: ticket 49 | annotations: 50 | alert02k1: alert02k2 51 | summary: '{{"{{$labels.sloth_service}}"}} {{"{{$labels.sloth_slo}}"}} SLO error budget burn 52 | rate is over expected.' 53 | title: (ticket) {{"{{$labels.sloth_service}}"}} {{"{{$labels.sloth_slo}}"}} SLO error budget 54 | burn rate is too fast. 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/bad/bad-multi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "prometheus/v1" 3 | service: "svc01" 4 | labels: 5 | global01k1: global01v1 6 | slos: 7 | - name: "slo1" 8 | objective: 99.9 9 | description: "This is SLO 01." 10 | labels: 11 | global02k1: global02v1 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: myServiceAlert 18 | labels: 19 | alert01k1: "alert01v1" 20 | annotations: 21 | alert02k1: "alert02k2" 22 | page_alert: 23 | labels: 24 | alert03k1: "alert03v1" 25 | ticket_alert: 26 | labels: 27 | alert04k1: "alert04v1" 28 | - name: "slo02" 29 | objective: 95 30 | description: "This is SLO 02." 31 | labels: 32 | global03k1: global03v1 33 | sli: 34 | raw: 35 | error_ratio_query: | 36 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 37 | / 38 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 39 | alerting: 40 | page_alert: 41 | disable: true 42 | ticket_alert: 43 | disable: true 44 | 45 | --- 46 | version: "prometheus/v1" 47 | service: "svc02" 48 | labels: 49 | global01k1: global01v1 50 | slos: 51 | - name: "slo1" 52 | objective: 99.99 53 | description: "This is SLO 01." 54 | labels: 55 | global02k1: global02v1 56 | sli: 57 | events: 58 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 59 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 60 | alerting: 61 | name: myServiceAlert 62 | labels: 63 | alert01k1: "alert01v1" 64 | annotations: 65 | alert02k1: "alert02k2" 66 | page_alert: 67 | labels: 68 | alert03k1: "alert03v1" 69 | ticket_alert: 70 | labels: 71 | alert04k1: "alert04v1" 72 | - name: "slo02" 73 | objective: 95 74 | description: "This is SLO 02." 75 | labels: 76 | global03k1: global03v1 77 | sli: {} # BAD! 78 | alerting: 79 | page_alert: 80 | disable: true 81 | ticket_alert: 82 | disable: true 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/integration/prometheus/testdata/validate/good/good-multi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "prometheus/v1" 3 | service: "svc01" 4 | labels: 5 | global01k1: global01v1 6 | slos: 7 | - name: "slo1" 8 | objective: 99.9 9 | description: "This is SLO 01." 10 | labels: 11 | global02k1: global02v1 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: myServiceAlert 18 | labels: 19 | alert01k1: "alert01v1" 20 | annotations: 21 | alert02k1: "alert02k2" 22 | page_alert: 23 | labels: 24 | alert03k1: "alert03v1" 25 | ticket_alert: 26 | labels: 27 | alert04k1: "alert04v1" 28 | - name: "slo02" 29 | objective: 95 30 | description: "This is SLO 02." 31 | labels: 32 | global03k1: global03v1 33 | sli: 34 | raw: 35 | error_ratio_query: | 36 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 37 | / 38 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 39 | alerting: 40 | page_alert: 41 | disable: true 42 | ticket_alert: 43 | disable: true 44 | 45 | --- 46 | version: "prometheus/v1" 47 | service: "svc02" 48 | labels: 49 | global01k1: global01v1 50 | slos: 51 | - name: "slo1" 52 | objective: 99.99 53 | description: "This is SLO 01." 54 | labels: 55 | global02k1: global02v1 56 | sli: 57 | events: 58 | error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 59 | total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 60 | alerting: 61 | name: myServiceAlert 62 | labels: 63 | alert01k1: "alert01v1" 64 | annotations: 65 | alert02k1: "alert02k2" 66 | page_alert: 67 | labels: 68 | alert03k1: "alert03v1" 69 | ticket_alert: 70 | labels: 71 | alert04k1: "alert04v1" 72 | - name: "slo02" 73 | objective: 95 74 | description: "This is SLO 02." 75 | labels: 76 | global03k1: global03v1 77 | sli: 78 | raw: 79 | error_ratio_query: | 80 | sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) 81 | / 82 | sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) 83 | alerting: 84 | page_alert: 85 | disable: true 86 | ticket_alert: 87 | disable: true 88 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------