├── go.ver ├── .github ├── pint │ ├── pint.hcl │ └── rules │ │ ├── 2.yaml │ │ └── 1.yml ├── CODEOWNERS ├── workflows │ ├── deps.yml │ ├── goreleaser.yml │ ├── semgrep.yml │ ├── go-mod-tidy.yml │ ├── examples.yml │ ├── docs.yml │ ├── test.yml │ ├── ci.yml │ ├── lint.yml │ ├── benchmark.yml │ └── pages.yml ├── dependabot.yml └── spellcheck │ ├── config.yml │ └── wordlist.txt ├── .gitignore ├── codecov.yml ├── docs ├── _config.yml ├── examples │ ├── simple.hcl │ ├── ci.hcl │ ├── templates.hcl │ ├── ignore_error_metrics.hcl │ ├── selectors.hcl │ ├── labels.hcl │ └── discovery.hcl ├── checks │ ├── index.md │ ├── ignore │ │ └── file.md │ ├── rule │ │ └── owner.md │ ├── pint │ │ └── comment.md │ ├── promql │ │ └── syntax.md │ └── alerts │ │ └── for.md └── _includes │ └── js │ └── custom.js ├── cmd └── pint │ ├── tests │ ├── 0034_version.txt │ ├── 0030_parse_string.txt │ ├── 0035_bad_loglevel.txt │ ├── 0107_ci_git_invalid_log_level.txt │ ├── 0045_parse_no_query.txt │ ├── 0193_parse_unary.txt │ ├── 0061_lint_workers_zero.txt │ ├── 0062_lint_no_args.txt │ ├── 0064_watch_no_path.txt │ ├── 0051_watch_severity_invalid.txt │ ├── 0044_parse_error.txt │ ├── 0002_nothing_to_lint.txt │ ├── 0223_watch_rule_files_no_args.txt │ ├── 0224_watch_rule_files_multi_args.txt │ ├── 0106_ci_git_branch_error.txt │ ├── 0189_config_bad_loglevel.txt │ ├── 0013_issue49_1.txt │ ├── 0096_bad_symlink.txt │ ├── 0026_aggregate_empty_name.txt │ ├── 0229_watch_health_endpoint.txt │ ├── 0014_issue49_2.txt │ ├── 0227_watch_pidfile_error.txt │ ├── 0126_lint_fail_on_invalid.txt │ ├── 0142_keep_firing_for.txt │ ├── 0228_watch_pidfile_remove_error.txt │ ├── 0199_json_no_dir.txt │ ├── 0127_lint_fail_on_fatal_but_got_warning.txt │ ├── 0206_parser_schema_err.txt │ ├── 0195_checkstyle_lint_no_dir.txt │ ├── 0091_lint_min_severity_invalid.txt │ ├── 0047_parse_4.txt │ ├── 0089_lint_min_severity_bug.txt │ ├── 0079_check_promql_series_invalid.txt │ ├── 0191_lint_dup_prom.txt │ ├── 0021_ignore_all.txt │ ├── 0225_watch_glob_dup_prometheus.txt │ ├── 0046_parse_3.txt │ ├── 0136_annotation_regex_key.txt │ ├── 0226_watch_rule_files_dup_prometheus.txt │ ├── 0205_parser_schema_thanos_ok.txt │ ├── 0059_templated_check_bad_template.txt │ ├── 0081_rulefmt.txt │ ├── 0114_config_env_expand_error.txt │ ├── 0043_watch_cancel.txt │ ├── 0078_repeated_group.txt │ ├── 0133_tls_certs_bad.txt │ ├── 0063_lint_offline.txt │ ├── 0005_false_positive.txt │ ├── 0036_ci_basebranch.txt │ ├── 0082_ci_base_branch_flag.txt │ ├── 0077_strict_error_owner.txt │ ├── 0128_lint_fail_on_warning_only.txt │ ├── 0138_annoation_regex_key_required.txt │ ├── 0177_rule_name.txt │ ├── 0050_watch_severity_fatal.txt │ ├── 0090_lint_min_severity_info.txt │ ├── 0146_discovery_filepath_bad_template.txt │ ├── 0102_prometheus_basic_auth_empty.txt │ ├── 0074_strict_error.txt │ ├── 0056_prometheus_required.txt │ ├── 0010_syntax_check.txt │ ├── 0175_strict_multi_doc.txt │ ├── 0092_dir_symlink.txt │ ├── 0181_range_query_max.txt │ ├── 0202_report_recording_rules.txt │ ├── 0174_auth_publicURI.txt │ ├── 0204_parser_schema_thanos_err.txt │ ├── 0006_rr_labels.txt │ ├── 0125_lint_fail_on_warning.txt │ ├── 0129_tls_cacert_bad.txt │ ├── 0137_annotation_regex_key_fail.txt │ ├── 0165_pint_comment_error.txt │ ├── 0192_ci_broken_removed_rule.txt │ ├── 0208_lint_full_path.txt │ ├── 0067_relaxed.txt │ ├── 0184_ci_file_ignore.txt │ ├── 0190_ci_dup_prom.txt │ ├── 0041_watch.txt │ ├── 0101_prometheus_basic_auth.txt │ ├── 0038_disable_checks_regex.txt │ ├── 0147_discovery_filepath_error.txt │ ├── 0214_gitlab_no_auth_token.txt │ ├── 0099_symlink_outside_glob.txt │ ├── 0187_ci_noop_yaml_parse.txt │ ├── 0004_fail_invalid_yaml.txt │ ├── 0182_range_query_custom_severity.txt │ ├── 0185_state_empty.txt │ ├── 0169_watch_rule_files_noprom.txt │ ├── 0215_github_no_auth_token.txt │ ├── 0131_tls_cacert_bad_skipVerify.txt │ ├── 0153_ci_bitbucket_missing_token.txt │ ├── 0203_parser_schema_prom_err.txt │ ├── 0097_rule_file_symlink_error.txt │ ├── 0180_parser_exclude_md.txt │ ├── 0130_tls_ca_good.txt │ ├── 0217_github_missing_pr_number.txt │ ├── 0179_parser_exclude.txt │ ├── 0132_tls_certs.txt │ ├── 0065_ci_include.txt │ ├── 0139_ci_exclude.txt │ ├── 0154_ci_discovery_error.txt │ ├── 0216_github_invalid_pr_number.txt │ ├── 0001_match_path.txt │ ├── 0104_file_ignore_prom.txt │ ├── 0140_ci_include_exclude.txt │ ├── 0178_parser_include.txt │ ├── 0220_lint_min_severity_fatal_ok.txt │ ├── 0120_ci_fail_on_invalid.txt │ ├── 0166_invalid_label.txt │ ├── 0029_ci_too_many_commits.txt │ ├── 0111_snooze.txt │ ├── 0194_checkstyle_lint.txt │ ├── 0055_prometheus_failover.txt │ ├── 0058_templated_check.txt │ ├── 0162_ci_deleted_dependency.txt │ ├── 0201_json_ci_no_dir.txt │ ├── 0158_lint_teamcity.txt │ ├── 0121_rule_for.txt │ ├── 0124_ci_base_branch_flag.txt │ ├── 0197_checkstyle_ci_no_dir.txt │ ├── 0172_rule_dependency_symlink_delete.txt │ ├── 0027_ci_branch.txt │ ├── 0105_too_many_samples.txt │ ├── 0168_watch_rule_files.txt │ ├── 0049_watch_severity_warning.txt │ ├── 0048_watch_limit.txt │ ├── 0060_ci_noop.txt │ ├── 0095_rulefmt_symlink.txt │ └── 0143_keep_firing_for.txt │ ├── bench │ ├── README.md │ └── Makefile │ ├── logger.go │ ├── version.go │ └── config.go ├── .markdownlint.json ├── tools ├── gomajor │ ├── go.mod │ └── go.sum ├── benchstat │ ├── go.mod │ └── go.sum ├── deadcode │ ├── go.mod │ └── go.sum └── betteralign │ └── go.mod ├── internal ├── config │ ├── ci.go │ ├── owners.go │ ├── report.go │ ├── options │ │ ├── call.go │ │ └── selector.go │ ├── checks.go │ ├── rule_name.go │ ├── ci_test.go │ ├── range_query.go │ ├── aggregate.go │ ├── reject.go │ ├── range_query_test.go │ ├── __snapshots__ │ │ ├── 027.snap │ │ ├── 029.snap │ │ └── 028.snap │ ├── rule_link.go │ ├── check.go │ ├── reject_test.go │ ├── checks_test.go │ ├── annotation.go │ ├── for.go │ ├── cost_test.go │ └── alerts.go ├── parser │ └── utils │ │ └── vectorselector.go ├── output │ ├── color.go │ ├── ranges.go │ └── humanize.go ├── log │ ├── log.go │ └── log_test.go ├── comments │ └── comments_internal_test.go ├── promapi │ ├── keylock.go │ ├── errors_test.go │ └── metrics.go ├── checks │ ├── rule_report_test.go │ ├── promql_syntax_test.go │ └── rule_report_test.snap └── reporter │ ├── json.go │ └── summary_test.go ├── Dockerfile.amd64 ├── Dockerfile.arm64 ├── Dockerfile └── README.md /go.ver: -------------------------------------------------------------------------------- 1 | 1.25.5 2 | -------------------------------------------------------------------------------- /.github/pint/pint.hcl: -------------------------------------------------------------------------------- 1 | parser { 2 | include = [".github/pint/rules/.*"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pint 2 | /.cover 3 | /dist/ 4 | /.vscode/ 5 | /cmd/pint/bench/rules 6 | /.tmp/ 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: false 3 | notify: 4 | wait_for_ci: false 5 | github_checks: 6 | annotations: true 7 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pdmosses/just-the-docs 2 | aux_links: 3 | "cloudflare/pint on GitHub": 4 | - "//github.com/cloudflare/pint" 5 | -------------------------------------------------------------------------------- /cmd/pint/tests/0034_version.txt: -------------------------------------------------------------------------------- 1 | exec pint version 2 | cmp stdout stdout.txt 3 | ! stderr . 4 | 5 | -- stdout.txt -- 6 | unknown (revision: unknown) 7 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": false, 4 | "MD024": { 5 | "siblings_only": true 6 | }, 7 | "MD025": false 8 | } -------------------------------------------------------------------------------- /cmd/pint/tests/0030_parse_string.txt: -------------------------------------------------------------------------------- 1 | exec pint parse '"foo"' 2 | cmp stdout stdout.txt 3 | ! stderr . 4 | 5 | -- stdout.txt -- 6 | ++ node: "foo" 7 | StringLiteral: 8 | * Type: string 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS file lists people and teams responsible for code in this 2 | # repository. 3 | # See https://help.github.com/articles/about-codeowners/ for details. 4 | 5 | * @prymitive 6 | -------------------------------------------------------------------------------- /cmd/pint/tests/0035_bad_loglevel.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l invalid --no-color lint rules 2 | ! stdout . 3 | stderr 'ERROR Execution completed with error\(s\) err="failed to set log level: ''invalid'' is not a valid log level"' 4 | -------------------------------------------------------------------------------- /cmd/pint/tests/0107_ci_git_invalid_log_level.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l xxx --no-color ci 2 | ! stdout . 3 | stderr 'ERROR Execution completed with error\(s\) err="failed to set log level: ''xxx'' is not a valid log level"' 4 | -------------------------------------------------------------------------------- /docs/examples/simple.hcl: -------------------------------------------------------------------------------- 1 | # This is a simplified config that only uses a single Prometheus 2 | # server for all checks. 3 | 4 | prometheus "prod" { 5 | uri = "https://prod.example.com" 6 | timeout = "1m" 7 | } 8 | -------------------------------------------------------------------------------- /cmd/pint/tests/0045_parse_no_query.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color parse 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="a query string is required" 7 | -------------------------------------------------------------------------------- /cmd/pint/tests/0193_parse_unary.txt: -------------------------------------------------------------------------------- 1 | exec pint parse -- '-up' 2 | cmp stdout stdout.txt 3 | ! stderr . 4 | 5 | -- stdout.txt -- 6 | ++ node: -up 7 | UnaryExpr: 8 | * Type: vector 9 | * Op: - 10 | * Expr: up 11 | -------------------------------------------------------------------------------- /docs/checks/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Checks 4 | parent: Documentation 5 | nav_order: 2 6 | has_children: true 7 | --- 8 | 9 | # List of supported checks 10 | 11 | {: .no_toc } 12 | 13 | 1. TOC 14 | {:toc} 15 | -------------------------------------------------------------------------------- /cmd/pint/tests/0061_lint_workers_zero.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --workers=0 --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="--workers flag must be > 0" 7 | -------------------------------------------------------------------------------- /cmd/pint/tests/0062_lint_no_args.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --workers=1 --no-color lint 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="at least one file or directory required" 7 | -------------------------------------------------------------------------------- /cmd/pint/tests/0064_watch_no_path.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6064 glob 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="at least one file or directory required" 7 | -------------------------------------------------------------------------------- /tools/gomajor/go.mod: -------------------------------------------------------------------------------- 1 | module _ 2 | 3 | go 1.25.0 4 | 5 | tool github.com/icholy/gomajor 6 | 7 | require ( 8 | github.com/icholy/gomajor v0.15.0 // indirect 9 | golang.org/x/mod v0.27.0 // indirect 10 | golang.org/x/sync v0.16.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /cmd/pint/tests/0051_watch_severity_invalid.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --min-severity=foo glob bar 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="invalid --min-severity value: unknown severity: foo" 7 | -------------------------------------------------------------------------------- /tools/benchstat/go.mod: -------------------------------------------------------------------------------- 1 | module _ 2 | 3 | go 1.25.0 4 | 5 | tool golang.org/x/perf/cmd/benchstat 6 | 7 | require ( 8 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect 9 | golang.org/x/perf v0.0.0-20251112180420-cfbd823f7301 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /cmd/pint/tests/0044_parse_error.txt: -------------------------------------------------------------------------------- 1 | ! exec pint parse 'sum(foo) by(' 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="1:13: parse error: unclosed left parenthesis" 7 | -------------------------------------------------------------------------------- /cmd/pint/bench/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark data 2 | 3 | A collection of Prometheus rules copied from 4 | [samber/awesome-prometheus-alerts](https://github.com/samber/awesome-prometheus-alerts) 5 | and used to benchmark pint. 6 | 7 | Run `make fetch` first to download and unpack rules into `rules` folder. 8 | -------------------------------------------------------------------------------- /cmd/pint/tests/0002_nothing_to_lint.txt: -------------------------------------------------------------------------------- 1 | mkdir rules 2 | ! exec pint --no-color lint rules 3 | ! stdout . 4 | cmp stderr stderr.txt 5 | 6 | -- stderr.txt -- 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=ERROR msg="Execution completed with error(s)" err="no matching files" 9 | -------------------------------------------------------------------------------- /cmd/pint/tests/0223_watch_rule_files_no_args.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6223 rule_files 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="exactly one argument required with the URI of Prometheus server to query" 7 | -------------------------------------------------------------------------------- /cmd/pint/tests/0224_watch_rule_files_multi_args.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6224 rule_files prom1 prom2 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=ERROR msg="Execution completed with error(s)" err="exactly one argument required with the URI of Prometheus server to query" 7 | -------------------------------------------------------------------------------- /cmd/pint/tests/0106_ci_git_branch_error.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l debug --no-color ci 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=DEBUG msg="Running git command" args=["rev-parse","--abbrev-ref","HEAD"] 7 | level=ERROR msg="Execution completed with error(s)" err="failed to get the name of current branch" 8 | -------------------------------------------------------------------------------- /.github/pint/rules/2.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: test 3 | rules: 4 | - record: up:sum 5 | expr: sum(up) 6 | 7 | - record: up:sum 8 | expr: sum(up{instance!="none", job=~"bob", job=~"^abc", cluster="dev"}) 9 | 10 | - record: up:sum 11 | expr: | 12 | sum(up{instance!="none", job=~"bob", job=~"^abc", cluster="dev"}) 13 | -------------------------------------------------------------------------------- /tools/deadcode/go.mod: -------------------------------------------------------------------------------- 1 | module _ 2 | 3 | go 1.25.0 4 | 5 | tool golang.org/x/tools/cmd/deadcode 6 | 7 | require ( 8 | golang.org/x/mod v0.30.0 // indirect 9 | golang.org/x/sync v0.18.0 // indirect 10 | golang.org/x/sys v0.38.0 // indirect 11 | golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect 12 | golang.org/x/tools v0.39.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /cmd/pint/logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudflare/pint/internal/log" 7 | ) 8 | 9 | func initLogger(level string, noColor bool) error { 10 | l, err := log.ParseLevel(level) 11 | if err != nil { 12 | return fmt.Errorf("'%s' is not a valid log level", level) 13 | } 14 | 15 | log.Setup(l, noColor) 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /docs/_includes/js/custom.js: -------------------------------------------------------------------------------- 1 | window.matchMedia('(prefers-color-scheme: dark)') 2 | .addEventListener('change', event => { 3 | if (event.matches) { 4 | jtd.setTheme('dark'); 5 | } else { 6 | jtd.setTheme('light'); 7 | } 8 | }); 9 | 10 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 11 | jtd.setTheme('dark'); 12 | } 13 | -------------------------------------------------------------------------------- /cmd/pint/bench/Makefile: -------------------------------------------------------------------------------- 1 | REVISION := 9f5c641bddde827d25c0284134c16a5ac3b94503 2 | 3 | .PHONE: fetch 4 | fetch: 5 | curl -sL -o archive.tar.gz https://github.com/samber/awesome-prometheus-alerts/archive/$(REVISION).tar.gz 6 | tar -xf archive.tar.gz 7 | rm -fr rules 8 | mv awesome-prometheus-alerts-$(REVISION)/dist/rules rules 9 | rm -fr awesome-prometheus-alerts-$(REVISION) archive.tar.gz 10 | -------------------------------------------------------------------------------- /cmd/pint/tests/0189_config_bad_loglevel.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l invalid --no-color config 2 | ! stdout . 3 | stderr 'ERROR Execution completed with error\(s\) err="failed to set log level: ''invalid'' is not a valid log level"' 4 | 5 | ! exec pint -l invalid --no-color parse 'foo' 6 | ! stdout . 7 | stderr 'ERROR Execution completed with error\(s\) err="failed to set log level: ''invalid'' is not a valid log level"' -------------------------------------------------------------------------------- /cmd/pint/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/urfave/cli/v3" 8 | ) 9 | 10 | var versionCmd = &cli.Command{ 11 | Name: "version", 12 | Usage: "Print version and exit.", 13 | Action: actionVersion, 14 | } 15 | 16 | func actionVersion(_ context.Context, _ *cli.Command) error { 17 | fmt.Printf("%s (revision: %s)\n", version, commit) 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/config/ci.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type CI struct { 8 | BaseBranch string `hcl:"baseBranch,optional" json:"baseBranch,omitempty"` 9 | MaxCommits int `hcl:"maxCommits,optional" json:"maxCommits,omitempty"` 10 | } 11 | 12 | func (ci CI) validate() error { 13 | if ci.MaxCommits <= 0 { 14 | return errors.New("maxCommits cannot be <= 0") 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | # This Dockerfile is used by goreleaser when releasing a new version. 2 | FROM debian:stable-20251208@sha256:fb368d0a37330ae6039269031552c2d6f5db7dfdad9c6adad026d23be51187d6 3 | RUN apt-get update --yes && \ 4 | apt-get install --no-install-recommends --yes git ca-certificates && \ 5 | rm -rf /var/lib/apt/lists/* 6 | COPY pint-linux-amd64 /usr/local/bin/pint 7 | WORKDIR /code 8 | CMD ["/usr/local/bin/pint"] 9 | -------------------------------------------------------------------------------- /Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | # This Dockerfile is used by goreleaser when releasing a new version. 2 | FROM debian:stable-20251208@sha256:fb368d0a37330ae6039269031552c2d6f5db7dfdad9c6adad026d23be51187d6 3 | RUN apt-get update --yes && \ 4 | apt-get install --no-install-recommends --yes git ca-certificates && \ 5 | rm -rf /var/lib/apt/lists/* 6 | COPY pint-linux-arm64 /usr/local/bin/pint 7 | WORKDIR /code 8 | CMD ["/usr/local/bin/pint"] 9 | -------------------------------------------------------------------------------- /cmd/pint/tests/0013_issue49_1.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color --config not_existed_config.hcl lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=not_existed_config.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \"not_existed_config.hcl\": : Configuration file not found; The configuration file not_existed_config.hcl does not exist." 8 | -------------------------------------------------------------------------------- /cmd/pint/tests/0096_bad_symlink.txt: -------------------------------------------------------------------------------- 1 | mkdir rules 2 | exec ln -s ../bad.yml rules/symlink.yml 3 | 4 | ! exec pint -l debug --no-color lint rules 5 | ! stdout . 6 | cmp stderr stderr.txt 7 | 8 | -- stderr.txt -- 9 | level=INFO msg="Finding all rules to check" paths=["rules"] 10 | level=ERROR msg="Execution completed with error(s)" err="rules/symlink.yml is a symlink but target file cannot be evaluated: lstat rules/../bad.yml: no such file or directory" 11 | -------------------------------------------------------------------------------- /cmd/pint/tests/0026_aggregate_empty_name.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color config 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": empty name regex" 8 | -- .pint.hcl -- 9 | rule { 10 | match { 11 | kind = "recording" 12 | } 13 | aggregate "" { 14 | keep = [ "job" ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/pint/tests/0229_watch_health_endpoint.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | exec pint --no-color watch --interval=5s --listen=127.0.0.1:6229 --pidfile=pint.pid glob rules 4 | 5 | cmp health.txt health 6 | 7 | -- health -- 8 | OK 9 | -- test.sh -- 10 | sleep 3 11 | curl -so health.txt http://127.0.0.1:6229/health 12 | cat pint.pid | xargs kill 13 | 14 | -- rules/0001.yml -- 15 | groups: 16 | - name: foo 17 | rules: 18 | - record: sum:up 19 | expr: sum(up) 20 | -------------------------------------------------------------------------------- /tools/benchstat/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= 2 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= 3 | golang.org/x/perf v0.0.0-20251112180420-cfbd823f7301 h1:qKuLfh5O0Hw3QfGs43tKwsiqL8RV+034WMgSAGWW4js= 4 | golang.org/x/perf v0.0.0-20251112180420-cfbd823f7301/go.mod h1:CObWzdfY9ZrvLE+9Ps2aVQKgF18AM8T2lj7TxN/GIXw= 5 | -------------------------------------------------------------------------------- /cmd/pint/tests/0014_issue49_2.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | -- rules/0001.yaml -- 10 | - record: down 11 | expr: up == 0 12 | 13 | -- .pint.hcl -- 14 | parser { 15 | relaxed = ["rules/.*"] 16 | } 17 | -------------------------------------------------------------------------------- /cmd/pint/tests/0227_watch_pidfile_error.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6227 --pidfile=nonexistent/pint.pid glob rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="open nonexistent/pint.pid: no such file or directory" 8 | -- rules/0001.yml -- 9 | groups: 10 | - name: foo 11 | rules: 12 | - record: sum:up 13 | expr: sum(up) 14 | -- .pint.hcl -- 15 | -------------------------------------------------------------------------------- /.github/workflows/deps.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: read-all 5 | 6 | jobs: 7 | dependency-review: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: "Checkout Repository" 11 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 12 | with: 13 | show-progress: false 14 | 15 | - name: "Dependency Review" 16 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 17 | -------------------------------------------------------------------------------- /cmd/pint/tests/0126_lint_fail_on_invalid.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --fail-on=xxx rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- rules/0001.yml -- 6 | # empty 7 | 8 | -- stderr.txt -- 9 | level=INFO msg="Finding all rules to check" paths=["rules"] 10 | level=INFO msg="Checking Prometheus rules" entries=0 workers=10 online=true 11 | level=INFO msg="No rules found, skipping Prometheus discovery" 12 | level=ERROR msg="Execution completed with error(s)" err="invalid --fail-on value: unknown severity: xxx" 13 | -------------------------------------------------------------------------------- /cmd/pint/tests/0142_keep_firing_for.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | -- rules/0001.yml -- 10 | - alert: Instance Is Down 1 11 | expr: up == 0 12 | keep_firing_for: 5m 13 | 14 | -- .pint.hcl -- 15 | parser { 16 | relaxed = [".*"] 17 | } 18 | -------------------------------------------------------------------------------- /internal/parser/utils/vectorselector.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/cloudflare/pint/internal/parser" 5 | 6 | promParser "github.com/prometheus/prometheus/promql/parser" 7 | ) 8 | 9 | func HasVectorSelector(node *parser.PromQLNode) (vs []promParser.VectorSelector) { 10 | if n, ok := node.Expr.(*promParser.VectorSelector); ok { 11 | vs = append(vs, *n) 12 | } 13 | 14 | for _, child := range node.Children { 15 | vs = append(vs, HasVectorSelector(child)...) 16 | } 17 | 18 | return vs 19 | } 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25.5-alpine@sha256:3587db7cc96576822c606d119729370dbf581931c5f43ac6d3fa03ab4ed85a10 2 | COPY . /src 3 | WORKDIR /src 4 | RUN apk add make git 5 | RUN make 6 | 7 | FROM debian:stable-20251208@sha256:fb368d0a37330ae6039269031552c2d6f5db7dfdad9c6adad026d23be51187d6 8 | RUN apt-get update --yes && \ 9 | apt-get install --no-install-recommends --yes git ca-certificates && \ 10 | rm -rf /var/lib/apt/lists/* 11 | COPY --from=0 /src/pint /usr/local/bin/pint 12 | WORKDIR /code 13 | CMD ["/usr/local/bin/pint"] 14 | -------------------------------------------------------------------------------- /internal/output/color.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import "fmt" 4 | 5 | type Color uint8 6 | 7 | const ( 8 | None Color = 0 9 | Bold Color = 1 10 | Dim Color = 2 11 | Black Color = 90 12 | Red Color = 91 13 | Yellow Color = 93 14 | Blue Color = 94 15 | Magenta Color = 95 16 | Cyan Color = 96 17 | White Color = 97 18 | ) 19 | 20 | func MaybeColor(color Color, disabled bool, s string) string { 21 | if disabled { 22 | return s 23 | } 24 | return fmt.Sprintf("\033[%dm%s\033[0m", color, s) 25 | } 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | debian: 9 | patterns: 10 | - "debian" 11 | - package-ecosystem: "gomod" 12 | directories: 13 | - "/" 14 | - "/tools/benchstat" 15 | - "/tools/betteralign" 16 | - "/tools/golangci-lint" 17 | schedule: 18 | interval: "weekly" 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | -------------------------------------------------------------------------------- /.github/spellcheck/config.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | aspell: 4 | lang: en 5 | default_encoding: utf-8 6 | dictionary: 7 | wordlists: 8 | - .github/spellcheck/wordlist.txt 9 | encoding: utf-8 10 | pipeline: 11 | - pyspelling.filters.markdown: 12 | markdown_extensions: 13 | - pymdownx.superfences 14 | - pyspelling.filters.html: 15 | comments: false 16 | ignores: 17 | - code 18 | - pre 19 | sources: 20 | - "*.md" 21 | - "**/*.md" 22 | -------------------------------------------------------------------------------- /cmd/pint/tests/0228_watch_pidfile_remove_error.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | mkdir piddir 4 | exec pint --no-color watch --interval=5s --listen=127.0.0.1:6228 --pidfile=piddir/pint.pid glob rules 5 | 6 | stderr 'level=ERROR msg="Failed to remove pidfile" err="open piddir: permission denied" path=piddir/pint.pid' 7 | 8 | -- test.sh -- 9 | sleep 3 10 | PID=$(cat piddir/pint.pid) 11 | chmod 000 piddir 12 | kill $PID 13 | sleep 1 14 | chmod 755 piddir 15 | 16 | -- rules/0001.yml -- 17 | groups: 18 | - name: foo 19 | rules: 20 | - record: sum:up 21 | expr: sum(up) 22 | -------------------------------------------------------------------------------- /cmd/pint/tests/0199_json_no_dir.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --json=x/y/z/problems.json rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 8 | level=ERROR msg="Execution completed with error(s)" err="open x/y/z/problems.json: no such file or directory" 9 | -- rules/0001.yml -- 10 | groups: 11 | - name: test 12 | rules: 13 | - alert: Example 14 | expr: up 15 | - alert: Example 16 | expr: sum(xxx) with() 17 | 18 | -------------------------------------------------------------------------------- /cmd/pint/tests/0127_lint_fail_on_fatal_but_got_warning.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint --fail-on=fatal --min-severity=bug rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- rules/0001.yml -- 6 | groups: 7 | - name: foo 8 | rules: 9 | - alert: foo 10 | expr: up{job="xxx"} 11 | 12 | -- stderr.txt -- 13 | level=INFO msg="Finding all rules to check" paths=["rules"] 14 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 15 | level=INFO msg="Problems found" Warning=1 16 | level=INFO msg="1 problem(s) not visible because of --min-severity=bug flag" 17 | -------------------------------------------------------------------------------- /cmd/pint/tests/0206_parser_schema_err.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": unsupported parser schema: bogus" 8 | -- rules/1.yml -- 9 | groups: 10 | - name: foo 11 | partial_response_strategy: bob 12 | rules: 13 | - alert: foo 14 | expr: up == 0 15 | - record: bar 16 | expr: sum(up) 17 | 18 | -- .pint.hcl -- 19 | parser { 20 | schema = "bogus" 21 | } 22 | -------------------------------------------------------------------------------- /cmd/pint/tests/0195_checkstyle_lint_no_dir.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --checkstyle=x/y/z/checkstyle.xml rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 8 | level=ERROR msg="Execution completed with error(s)" err="open x/y/z/checkstyle.xml: no such file or directory" 9 | -- rules/0001.yml -- 10 | groups: 11 | - name: test 12 | rules: 13 | - alert: Example 14 | expr: up 15 | - alert: Example 16 | expr: sum(xxx) with() 17 | 18 | -------------------------------------------------------------------------------- /cmd/pint/tests/0091_lint_min_severity_invalid.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --min-severity=xxx rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 8 | level=ERROR msg="Execution completed with error(s)" err="invalid --min-severity value: unknown severity: xxx" 9 | -- rules/0001.yml -- 10 | groups: 11 | - name: foo 12 | rules: 13 | - alert: foo 14 | expr: rate(errors[2m]) > 0 15 | annotations: 16 | summary: 'error rate: {{ $value }}' 17 | -------------------------------------------------------------------------------- /tools/betteralign/go.mod: -------------------------------------------------------------------------------- 1 | module _ 2 | 3 | go 1.25.0 4 | 5 | tool github.com/dkorunic/betteralign/cmd/betteralign 6 | 7 | require ( 8 | github.com/KimMachineGun/automemlimit v0.7.5 // indirect 9 | github.com/dkorunic/betteralign v0.8.1 // indirect 10 | github.com/google/renameio/v2 v2.0.1 // indirect 11 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 12 | github.com/sirkon/dst v0.26.4 // indirect 13 | go.uber.org/automaxprocs v1.6.0 // indirect 14 | golang.org/x/mod v0.30.0 // indirect 15 | golang.org/x/sync v0.18.0 // indirect 16 | golang.org/x/tools v0.39.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /cmd/pint/tests/0047_parse_4.txt: -------------------------------------------------------------------------------- 1 | exec pint parse 'rate(http_requests_total[5m] offset -1w)' 2 | cmp stdout stdout.txt 3 | ! stderr . 4 | 5 | -- stdout.txt -- 6 | ++ node: rate(http_requests_total[5m] offset -1w) 7 | Call: 8 | * Type: vector 9 | * Func: rate 10 | * Args: http_requests_total[5m] offset -1w 11 | ++ node: http_requests_total[5m] offset -1w 12 | Expressions: 13 | ++ node: http_requests_total[5m] offset -1w 14 | MatrixSelector: 15 | * Type: matrix 16 | * VectorSelector: http_requests_total offset -1w 17 | * Range: 5m0s 18 | -------------------------------------------------------------------------------- /cmd/pint/tests/0089_lint_min_severity_bug.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint --min-severity=bug rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 8 | level=INFO msg="Problems found" Information=1 9 | level=INFO msg="1 problem(s) not visible because of --min-severity=bug flag" 10 | -- rules/0001.yml -- 11 | groups: 12 | - name: foo 13 | rules: 14 | - alert: foo 15 | expr: rate(errors[2m]) > 0 16 | annotations: 17 | summary: 'error rate: {{ $value }}' 18 | -------------------------------------------------------------------------------- /docs/examples/ci.hcl: -------------------------------------------------------------------------------- 1 | # This is an example config to be used when running pint as a CI job 2 | # to validate pull requests. 3 | 4 | parser { 5 | # Check all files inside rules/alerting and rules/recording dirs. 6 | include = ["rules/(alerting|recording)/.+"] 7 | 8 | # Ignore all *.md and *.txt files. 9 | exclude = [".+.md", ".+.txt"] 10 | } 11 | 12 | ci { 13 | # Don't run pint if there are more than 50 commits on current branch. 14 | maxCommits = 50 15 | 16 | # When running 'pint ci' compare current branch with origin/main 17 | # to get the list of modified files. 18 | baseBranch = "origin/main" 19 | } 20 | -------------------------------------------------------------------------------- /docs/examples/templates.hcl: -------------------------------------------------------------------------------- 1 | # Examples of rules using templates. 2 | 3 | # Any alert using non-zero `for` field must also 4 | # have a label named `alert_for` with the value 5 | # equal to the `for` value. 6 | # Alert defined as follows: 7 | # - alert: service_down 8 | # expr: up == 0 9 | # for: 5m 10 | # would fail this check, but this version wouldn't: 11 | # - alert: service_down 12 | # expr: up == 0 13 | # for: 5m 14 | # labels: 15 | # alert_for: 5m 16 | rule { 17 | match { 18 | for = "> 0" 19 | } 20 | 21 | label "alert_for" { 22 | required = true 23 | value = "{{ $for }}" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/config/owners.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type Owners struct { 8 | Allowed []string `hcl:"allowed,optional" json:"allowed,omitempty"` 9 | } 10 | 11 | func (o Owners) validate() error { 12 | for _, pattern := range o.Allowed { 13 | _, err := regexp.Compile(pattern) 14 | if err != nil { 15 | return err 16 | } 17 | } 18 | return nil 19 | } 20 | 21 | func (o Owners) CompileAllowed() (r []*regexp.Regexp) { 22 | if len(o.Allowed) == 0 { 23 | r = append(r, regexp.MustCompile(".*")) 24 | return r 25 | } 26 | r = append(r, MustCompileRegexes(o.Allowed...)...) 27 | return r 28 | } 29 | -------------------------------------------------------------------------------- /.github/pint/rules/1.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: test 3 | rules: 4 | - alert: Service Is Down 5 | expr: up == 0 6 | for: 0s 7 | 8 | - alert: Service Is Down 9 | expr: up{job="abc"} == 0 10 | 11 | - alert: Service Is Missing 12 | expr: absent({job="myjob"}) 13 | for: 0s 14 | 15 | - alert: Everything Is Down 16 | expr: up:sum == 0 17 | 18 | - alert: Dead Code 19 | expr: | 20 | sum(foo or vector(0)) by(name) > 0 21 | 22 | - alert: Service Is Missing 23 | expr: absent({job="myjob"}) 24 | annotations: 25 | summary: | 26 | Service {{ $labels.job }} is not running on {{ $labels.instance }} 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0079_check_promql_series_invalid.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color config 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": .pint.hcl:7,3-6: Unsupported argument; An argument named \"bob\" is not expected here." 8 | -- .pint.hcl -- 9 | prometheus "prom" { 10 | uri = "http://127.0.0.1" 11 | required = true 12 | } 13 | 14 | check "promql/series" { 15 | bob = [ 16 | ".*_error", 17 | ".*_error_.*", 18 | ".*_errors", 19 | ".*_errors_.*", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Check goreleaser config 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | goreleaser-config: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | show-progress: false 21 | 22 | - name: Check config 23 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 24 | with: 25 | args: check -f .goreleaser.yml 26 | -------------------------------------------------------------------------------- /cmd/pint/tests/0191_lint_dup_prom.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules.yml 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": prometheus server name must be unique, found two or more config blocks using \"prom\" name" 8 | -- rules.yml -- 9 | -- .pint.hcl -- 10 | prometheus "prom" { 11 | uri = "http://127.0.0.1:2191" 12 | timeout = "5s" 13 | required = true 14 | } 15 | prometheus "prom" { 16 | uri = "http://127.0.0.1:3191" 17 | timeout = "5s" 18 | required = true 19 | } 20 | -------------------------------------------------------------------------------- /cmd/pint/tests/0021_ignore_all.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color -l debug lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": ignore block must have at least one condition" 8 | -- rules/0001.yml -- 9 | - record: "colo:recording" 10 | expr: sum(foo) without(job) 11 | 12 | - alert: "colo:alerting" 13 | expr: sum(bar) without(job) 14 | 15 | -- .pint.hcl -- 16 | parser { 17 | relaxed = ["rules/.*"] 18 | } 19 | rule { 20 | ignore {} 21 | aggregate ".+" { 22 | keep = [ "job" ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/examples/ignore_error_metrics.hcl: -------------------------------------------------------------------------------- 1 | # Define "prod" Prometheus instance that will only be used for 2 | # rules defined in file matching "alerting/prod/.+" or "recording/prod/.+". 3 | prometheus "prod" { 4 | uri = "https://prod.example.com" 5 | timeout = "30s" 6 | include = [ 7 | "alerting/prod/.+", 8 | "recording/prod/.+", 9 | ] 10 | } 11 | 12 | # Extra global configuration for the promql/series check. 13 | check "promql/series" { 14 | # Don't report missing metrics for any metric with name matching 15 | # one of the regexp matchers below. 16 | ignoreMetrics = [ 17 | ".+_error", 18 | ".+_error_.+", 19 | ".+_errors", 20 | ".+_errors_.+", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /cmd/pint/tests/0225_watch_glob_dup_prometheus.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6225 glob rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": prometheus server name must be unique, found two or more config blocks using \"prom\" name" 8 | -- rules/0001.yml -- 9 | groups: 10 | - name: foo 11 | rules: 12 | - record: sum:up 13 | expr: sum(up) 14 | -- .pint.hcl -- 15 | prometheus "prom" { 16 | uri = "http://localhost:7225" 17 | } 18 | prometheus "prom" { 19 | uri = "http://localhost:8225" 20 | } 21 | -------------------------------------------------------------------------------- /cmd/pint/tests/0046_parse_3.txt: -------------------------------------------------------------------------------- 1 | exec pint parse 'sum(http_requests_total{method="GET"} @ 1609746000)' 2 | cmp stdout stdout.txt 3 | ! stderr . 4 | 5 | -- stdout.txt -- 6 | ++ node: sum(http_requests_total{method="GET"} @ 1609746000.000) 7 | AggregateExpr: 8 | * Type: vector 9 | * Op: sum 10 | * Expr: http_requests_total{method="GET"} @ 1609746000.000 11 | * Param: 12 | * Grouping: [] 13 | * Without: false 14 | ++ node: http_requests_total{method="GET"} @ 1609746000.000 15 | VectorSelector: 16 | * Type: vector 17 | * Name: http_requests_total 18 | * Offset: 0s 19 | * LabelMatchers: [method="GET" __name__="http_requests_total"] 20 | -------------------------------------------------------------------------------- /cmd/pint/tests/0136_annotation_regex_key.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | -- rules/0001.yml -- 10 | - alert: Instance Is Down 1 11 | expr: up == 0 12 | annotations: 13 | annotation_foo: bar 14 | annotation_bar: bar 15 | 16 | -- .pint.hcl -- 17 | parser { 18 | relaxed = [".*"] 19 | } 20 | rule { 21 | annotation "annotation_.*" { 22 | required = true 23 | value = "bar" 24 | severity = "bug" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0226_watch_rule_files_dup_prometheus.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color watch --listen=127.0.0.1:6226 rule_files prom 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": prometheus server name must be unique, found two or more config blocks using \"prom\" name" 8 | -- rules/0001.yml -- 9 | groups: 10 | - name: foo 11 | rules: 12 | - record: sum:up 13 | expr: sum(up) 14 | -- .pint.hcl -- 15 | prometheus "prom" { 16 | uri = "http://localhost:7226" 17 | } 18 | prometheus "prom" { 19 | uri = "http://localhost:8226" 20 | } 21 | -------------------------------------------------------------------------------- /cmd/pint/tests/0205_parser_schema_thanos_ok.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | -- rules/1.yml -- 10 | groups: 11 | - name: foo 12 | partial_response_strategy: warn 13 | rules: 14 | - alert: foo 15 | expr: up == 0 16 | 17 | -- rules/2.yml -- 18 | groups: 19 | - name: foo 20 | partial_response_strategy: abort 21 | rules: 22 | - record: bar 23 | expr: sum(up) 24 | 25 | -- .pint.hcl -- 26 | parser { 27 | schema = "thanos" 28 | } 29 | -------------------------------------------------------------------------------- /docs/checks/ignore/file.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | parent: Checks 4 | grand_parent: Documentation 5 | --- 6 | 7 | # ignore/file 8 | 9 | You will see this check reports for files that are excludes from pint using a 10 | `ignore/file` comment. 11 | See this [page](../../ignoring.md) for details on how such comments work. 12 | 13 | Those reports are informational to make it obvious that pint didn't run any 14 | real checks on given file. 15 | 16 | ## Configuration 17 | 18 | This isn't a real check and it doesn't have any configuration options. 19 | 20 | ## How to enable it 21 | 22 | This isn't a real check and cannot be enabled. 23 | 24 | ## How to disable it 25 | 26 | This isn't a real check and cannot be disabled. 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0059_templated_check_bad_template.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": template: regexp:1:126: executing \"regexp\" at : nil is not a command" 8 | -- rules/0001.yml -- 9 | - alert: Instance Is Down 1 10 | expr: up == 0 11 | 12 | -- .pint.hcl -- 13 | parser { 14 | relaxed = [".*"] 15 | } 16 | rule { 17 | match { 18 | for = "> 0" 19 | } 20 | 21 | annotation "alert_for" { 22 | required = true 23 | value = "{{ nil }}" 24 | severity = "bug" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0081_rulefmt.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 8 | Fatal: This rule is not a valid Prometheus rule: `missing expr key`. (yaml/parse) 9 | ---> rules/strict.yml:4 10 | 4 | - record: foo 11 | ^^^ This rule is not a valid Prometheus rule: `missing expr key`. 12 | 13 | level=INFO msg="Problems found" Fatal=1 14 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 15 | -- rules/strict.yml -- 16 | groups: 17 | - name: foo 18 | rules: 19 | - record: foo 20 | -------------------------------------------------------------------------------- /internal/config/report.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type ReportSettings struct { 10 | Comment string `hcl:"comment" json:"comment"` 11 | Severity string `hcl:"severity" json:"severity"` 12 | } 13 | 14 | func (rs ReportSettings) validate() error { 15 | if rs.Comment == "" { 16 | return errors.New("report comment cannot be empty") 17 | } 18 | 19 | if rs.Severity != "" { 20 | if _, err := checks.ParseSeverity(rs.Severity); err != nil { 21 | return err 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (rs ReportSettings) getSeverity() checks.Severity { 29 | sev, _ := checks.ParseSeverity(rs.Severity) 30 | return sev 31 | } 32 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | var Level = &slog.LevelVar{} 11 | 12 | func Setup(level slog.Level, noColor bool) { 13 | Level.Set(level.Level()) 14 | logger := slog.New(newHandler(os.Stderr, Level.Level(), noColor)) 15 | slog.SetDefault(logger) 16 | } 17 | 18 | func ParseLevel(s string) (slog.Level, error) { 19 | switch strings.ToLower(s) { 20 | case "error": 21 | return slog.LevelError, nil 22 | case "warn": 23 | return slog.LevelWarn, nil 24 | case "info": 25 | return slog.LevelInfo, nil 26 | case "debug": 27 | return slog.LevelDebug, nil 28 | default: 29 | return slog.LevelInfo, fmt.Errorf("%q is not a valid log level", s) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/comments/comments_internal_test.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestParseValueUnknownType(t *testing.T) { 10 | // Test UnknownType case - cannot be reached through normal Parse calls. 11 | val, err := parseValue(UnknownType, "test", 1) 12 | require.NoError(t, err) 13 | require.Nil(t, val) 14 | 15 | // Test InvalidComment case - cannot be reached through normal Parse calls. 16 | val, err = parseValue(InvalidComment, "test", 1) 17 | require.NoError(t, err) 18 | require.Nil(t, val) 19 | 20 | // Test default case - not reachable in practice. 21 | val, err = parseValue(Type(255), "test", 1) 22 | require.NoError(t, err) 23 | require.Nil(t, val) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/pint/tests/0114_config_env_expand_error.txt: -------------------------------------------------------------------------------- 1 | env FOO=BAR 2 | ! exec pint --no-color config 3 | ! stdout . 4 | cmp stderr stderr.txt 5 | 6 | -- stderr.txt -- 7 | level=INFO msg="Loading configuration file" path=.pint.hcl 8 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": .pint.hcl:7,17-29: Unknown variable; There is no variable named \"ENV_AUTH_KEY\"., and 1 other diagnostic(s)" 9 | -- .pint.hcl -- 10 | parser { 11 | relaxed = [".*"] 12 | } 13 | prometheus "prod" { 14 | uri = "http://localhost" 15 | headers = { 16 | X-Auth = "${ENV_AUTH_KEY}" 17 | } 18 | } 19 | rule { 20 | match { 21 | kind = "recording" 22 | } 23 | aggregate ".+" { 24 | keep = [ "${ENV_FOO}" ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0043_watch_cancel.txt: -------------------------------------------------------------------------------- 1 | http slow-response github / 30s 200 {} 2 | http start github 127.0.0.1:7043 3 | 4 | exec bash -x ./test.sh & 5 | 6 | exec pint --no-color watch --interval=1h --listen=127.0.0.1:6043 --pidfile=pint.pid glob rules 7 | ! stdout . 8 | stderr 'level=INFO msg="Shutting down"' 9 | stderr 'level=INFO msg="Waiting for all background tasks to finish"' 10 | stderr 'level=INFO msg="Background worker finished"' 11 | 12 | -- test.sh -- 13 | sleep 3 14 | cat pint.pid | xargs kill 15 | 16 | -- rules/1.yml -- 17 | - record: aggregate 18 | expr: sum(foo) without(job) 19 | 20 | -- .pint.hcl -- 21 | parser { 22 | relaxed = [".*"] 23 | } 24 | prometheus "slow" { 25 | uri = "http://127.0.0.1:7043" 26 | timeout = "2m" 27 | required = true 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | permissions: 12 | contents: read 13 | jobs: 14 | semgrep: 15 | name: semgrep/ci 16 | runs-on: ubuntu-latest 17 | env: 18 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 19 | SEMGREP_URL: https://cloudflare.semgrep.dev 20 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 21 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 22 | container: 23 | image: semgrep/semgrep 24 | steps: 25 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | - run: semgrep ci 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0078_repeated_group.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --require-owner rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=WARN msg="Failed to parse file content" err="duplicated group name" path=rules/strict.yml line=4 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | Fatal: duplicated group name (yaml/parse) 10 | ---> rules/strict.yml:4 11 | 4 | - name: foo 12 | ^^^ duplicated group name 13 | 14 | level=INFO msg="Problems found" Fatal=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/strict.yml -- 17 | groups: 18 | - name: foo 19 | rules: [] 20 | - name: foo 21 | rules: [] 22 | -------------------------------------------------------------------------------- /cmd/pint/tests/0133_tls_certs_bad.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": invalid prometheus TLS configuration: open prom-ca.pem: no such file or directory" 8 | -- rules/1.yml -- 9 | groups: 10 | - name: foo 11 | rules: 12 | - record: aggregate 13 | expr: sum(foo) without(job) 14 | 15 | -- .pint.hcl -- 16 | prometheus "prom" { 17 | uri = "https://127.0.0.1:7133" 18 | failover = [] 19 | timeout = "5s" 20 | required = true 21 | tls { 22 | caCert = "prom-ca.pem" 23 | clientCert = "prom.pem" 24 | clientKey = "prom.key" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/go-mod-tidy.yml: -------------------------------------------------------------------------------- 1 | name: Check go.mod 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | go-mod-tidy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | show-progress: false 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 24 | with: 25 | go-version-file: go.ver 26 | cache: false 27 | 28 | - name: Run go mod tidy 29 | run: go mod tidy 30 | 31 | - name: Check for local changes 32 | run: git diff --exit-code 33 | -------------------------------------------------------------------------------- /cmd/pint/tests/0063_lint_offline.txt: -------------------------------------------------------------------------------- 1 | exec pint --offline --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Configured new Prometheus server" name=disabled uris=1 uptime=up tags=[] include=["^invalid/.+$"] exclude=[] 9 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=false 10 | level=INFO msg="Offline mode, skipping Prometheus discovery" 11 | -- rules/ok.yml -- 12 | - record: sum:foo 13 | expr: sum(foo) 14 | -- .pint.hcl -- 15 | prometheus "disabled" { 16 | uri = "http://127.0.0.1:123" 17 | timeout = "5s" 18 | required = true 19 | include = ["invalid/.+"] 20 | } 21 | parser { 22 | relaxed = [".*"] 23 | } 24 | -------------------------------------------------------------------------------- /internal/config/options/call.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type CallSettings struct { 10 | Key string `hcl:",label" json:"key"` 11 | Selectors []SelectorSettings `hcl:"selector,block" json:"selector,omitempty"` 12 | } 13 | 14 | func (cs CallSettings) Validate() error { 15 | if cs.Key == "" { 16 | return errors.New("call key cannot be empty") 17 | } 18 | 19 | if _, err := checks.NewTemplatedRegexp(cs.Key); err != nil { 20 | return err 21 | } 22 | 23 | if len(cs.Selectors) == 0 { 24 | return errors.New("you must specific at least one `selector` block") 25 | } 26 | 27 | for _, s := range cs.Selectors { 28 | if err := s.Validate(); err != nil { 29 | return err 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/pint/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudflare/pint/internal/config" 9 | 10 | "github.com/urfave/cli/v3" 11 | ) 12 | 13 | var configCmd = &cli.Command{ 14 | Name: "config", 15 | Usage: "Parse and print used config.", 16 | Action: actionConfig, 17 | } 18 | 19 | func actionConfig(_ context.Context, c *cli.Command) (err error) { 20 | err = initLogger(c.String(logLevelFlag), c.Bool(noColorFlag)) 21 | if err != nil { 22 | return fmt.Errorf("failed to set log level: %w", err) 23 | } 24 | 25 | cfg, _, err := config.Load(c.String(configFlag), c.IsSet(configFlag)) 26 | if err != nil { 27 | return fmt.Errorf("failed to load config file %q: %w", c.String(configFlag), err) 28 | } 29 | 30 | fmt.Fprintln(os.Stderr, cfg.String()) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /.github/spellcheck/wordlist.txt: -------------------------------------------------------------------------------- 1 | APIs 2 | automaxprocs 3 | BitBucket 4 | bool 5 | changelog 6 | Changelog 7 | ci 8 | CLI 9 | cloudflare 10 | Cloudflare 11 | config 12 | configs 13 | Deduplicate 14 | deduplicated 15 | dir 16 | durations 17 | endraw 18 | github 19 | gitlab 20 | GitLab 21 | GOGC 22 | golang 23 | GOMAXPROCS 24 | hcl 25 | HCL 26 | hoc 27 | hostname 28 | HTTPS 29 | humanize 30 | io 31 | JSON 32 | linter 33 | liveness 34 | matcher 35 | matchers 36 | md 37 | nav 38 | precomputed 39 | printf 40 | prometheus 41 | promql 42 | PromQL 43 | PRs 44 | prymitive 45 | pushgateway 46 | rulefmt 47 | samber 48 | SNI 49 | symlink 50 | symlinked 51 | symlinks 52 | TeamCity 53 | templated 54 | Thanos 55 | TLS 56 | toc 57 | tokenize 58 | uber 59 | UI 60 | unmarshal 61 | uptime 62 | URI 63 | URIs 64 | utf 65 | UTF 66 | validator 67 | VCS 68 | yaml 69 | YAML 70 | -------------------------------------------------------------------------------- /internal/config/checks.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | 7 | "github.com/cloudflare/pint/internal/checks" 8 | ) 9 | 10 | type Checks struct { 11 | Enabled []string `hcl:"enabled,optional" json:"enabled,omitempty"` 12 | Disabled []string `hcl:"disabled,optional" json:"disabled,omitempty"` 13 | } 14 | 15 | func (c Checks) validate() error { 16 | for _, name := range c.Enabled { 17 | if err := validateCheckName(name); err != nil { 18 | return err 19 | } 20 | } 21 | for _, name := range c.Disabled { 22 | if err := validateCheckName(name); err != nil { 23 | return err 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func validateCheckName(name string) error { 31 | if slices.Contains(checks.CheckNames, name) { 32 | return nil 33 | } 34 | return fmt.Errorf("unknown check name %s", name) 35 | } 36 | -------------------------------------------------------------------------------- /internal/promapi/keylock.go: -------------------------------------------------------------------------------- 1 | package promapi 2 | 3 | import "sync" 4 | 5 | // https://medium.com/@petrlozhkin/kmutex-lock-mutex-by-unique-id-408467659c24 6 | type partitionLocker struct { 7 | c *sync.Cond 8 | l sync.Locker 9 | s map[string]struct{} 10 | } 11 | 12 | func newPartitionLocker(l sync.Locker) *partitionLocker { 13 | return &partitionLocker{c: sync.NewCond(l), l: l, s: make(map[string]struct{})} 14 | } 15 | 16 | func (p *partitionLocker) locked(id string) (ok bool) { 17 | _, ok = p.s[id] 18 | return ok 19 | } 20 | 21 | func (p *partitionLocker) lock(id string) { 22 | p.l.Lock() 23 | defer p.l.Unlock() 24 | for p.locked(id) { 25 | p.c.Wait() 26 | } 27 | p.s[id] = struct{}{} 28 | } 29 | 30 | func (p *partitionLocker) unlock(id string) { 31 | p.l.Lock() 32 | defer p.l.Unlock() 33 | delete(p.s, id) 34 | p.c.Broadcast() 35 | } 36 | -------------------------------------------------------------------------------- /cmd/pint/tests/0005_false_positive.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | -- rules/0001.yml -- 10 | - record: "colo:test1" 11 | expr: topk(6, sum(rate(edgeworker_subrequest_errorCount{cordon="free"}[5m])) BY (zoneId,job)) 12 | - record: "colo:test2" 13 | expr: topk(6, sum(rate(edgeworker_subrequest_errorCount{cordon="free"}[10m])) without (instance)) 14 | 15 | -- .pint.hcl -- 16 | parser { 17 | relaxed = ["rules/.*"] 18 | } 19 | rule { 20 | aggregate ".+" { 21 | keep = [ "job" ] 22 | } 23 | aggregate "colo(?:_.+)?:.+" { 24 | strip = [ "instance" ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0036_ci_basebranch.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec pint --no-color ci 15 | ! stdout . 16 | cmp stderr ../stderr.txt 17 | 18 | -- stderr.txt -- 19 | level=INFO msg="Loading configuration file" path=.pint.hcl 20 | level=INFO msg="Running from base branch, skipping checks" branch=main 21 | -- src/v1.yml -- 22 | - record: rule1 23 | expr: sum(foo) by(job) 24 | - record: rule2 25 | expr: sum(foo) bi(job) 26 | 27 | -- src/.pint.hcl -- 28 | ci { 29 | baseBranch = "main" 30 | } 31 | parser { 32 | relaxed = [".*"] 33 | } 34 | -------------------------------------------------------------------------------- /docs/examples/selectors.hcl: -------------------------------------------------------------------------------- 1 | rule { 2 | # Only match alerting rules. 3 | match { 4 | kind = "alerting" 5 | } 6 | 7 | # All alerts using the up metric must specify the "job" label. 8 | # 9 | # Good: 10 | # 11 | # - alert: TargetDown 12 | # expr: up{job="foo"} == 0 13 | # 14 | # Bad: 15 | # 16 | # - alert: TargetDown 17 | # expr: up 18 | # 19 | selector "up" { 20 | requiredLabels = ["job"] 21 | } 22 | 23 | # All alerts using the absent() or absent_over_time() call must specify 24 | # the "team" label: 25 | # 26 | # Good: 27 | # 28 | # - alert: MetricAbsent 29 | # expr: absent(my_metric{team="foo"}) 30 | # 31 | # Bad: 32 | # 33 | # - alert: MetricAbsent 34 | # expr: absent(my_metric{}) 35 | # 36 | call "absent|absent_over_time" { 37 | selector ".+" { 38 | requiredLabels = ["team"] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0082_ci_base_branch_flag.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec pint --no-color ci --base-branch=main 15 | ! stdout . 16 | cmp stderr ../stderr.txt 17 | 18 | -- stderr.txt -- 19 | level=INFO msg="Loading configuration file" path=.pint.hcl 20 | level=INFO msg="Running from base branch, skipping checks" branch=main 21 | -- src/v1.yml -- 22 | - record: rule1 23 | expr: sum(foo) by(job) 24 | - record: rule2 25 | expr: sum(foo) bi(job) 26 | 27 | -- src/.pint.hcl -- 28 | ci { 29 | baseBranch = "foo" 30 | } 31 | parser { 32 | relaxed = [".*"] 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pint/tests/0077_strict_error_owner.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --require-owner rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 8 | Bug: missing owner (rule/owner) 9 | ---> rules/strict.yml:4-7 -> `foo bar` 10 | 7 | foo: bar 11 | ^^^ `rule/owner` comments are required in all files, please add a `# pint file/owner $owner` somewhere in this file and/or `# pint rule/owner $owner` on top of each rule. 12 | 13 | level=INFO msg="Problems found" Bug=1 14 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 15 | -- rules/strict.yml -- 16 | groups: 17 | - name: foo 18 | rules: 19 | - alert: foo bar 20 | expr: up == 0 21 | annotations: 22 | foo: bar 23 | -------------------------------------------------------------------------------- /cmd/pint/tests/0128_lint_fail_on_warning_only.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --fail-on=warning --min-severity=bug rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- rules/0001.yml -- 6 | groups: 7 | - name: foo 8 | rules: 9 | - alert: foo 10 | expr: up{job="xxx"} 11 | 12 | -- stderr.txt -- 13 | level=INFO msg="Finding all rules to check" paths=["rules"] 14 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 15 | level=WARN msg="You have --min-severity set to a higher severity value than --fail-on, pint might exit with a non-zero code but you won't see the problem that caused it" min-severity=Bug fail-on=Warning 16 | level=INFO msg="Problems found" Warning=1 17 | level=INFO msg="1 problem(s) not visible because of --min-severity=bug flag" 18 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Warning or higher" 19 | -------------------------------------------------------------------------------- /internal/config/rule_name.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/cloudflare/pint/internal/checks" 5 | ) 6 | 7 | type RuleNameSettings struct { 8 | Regex string `hcl:",label" json:"key,omitempty"` 9 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 10 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 11 | } 12 | 13 | func (rs RuleNameSettings) validate() error { 14 | if _, err := checks.NewTemplatedRegexp(rs.Regex); err != nil { 15 | return err 16 | } 17 | 18 | if rs.Severity != "" { 19 | if _, err := checks.ParseSeverity(rs.Severity); err != nil { 20 | return err 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (rs RuleNameSettings) getSeverity(fallback checks.Severity) checks.Severity { 28 | if rs.Severity != "" { 29 | sev, _ := checks.ParseSeverity(rs.Severity) 30 | return sev 31 | } 32 | return fallback 33 | } 34 | -------------------------------------------------------------------------------- /internal/output/ranges.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func FormatLineRangeString(lines []int) string { 11 | ls := make([]int, len(lines)) 12 | copy(ls, lines) 13 | slices.Sort(ls) 14 | 15 | var ranges []string 16 | start := -1 17 | end := -1 18 | for _, l := range ls { 19 | switch { 20 | case start < 0: 21 | start = l 22 | end = l 23 | case l == end+1: 24 | end = l 25 | default: 26 | if start > 0 && end > 0 { 27 | ranges = append(ranges, printRange(start, end)) 28 | } 29 | start = l 30 | end = l 31 | } 32 | } 33 | if start > 0 && end > 0 { 34 | ranges = append(ranges, printRange(start, end)) 35 | } 36 | 37 | return strings.Join(ranges, " ") 38 | } 39 | 40 | func printRange(start, end int) string { 41 | if start == end { 42 | return strconv.Itoa(start) 43 | } 44 | return fmt.Sprintf("%d-%d", start, end) 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: Validate examples 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | examples: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | show-progress: false 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 24 | with: 25 | go-version-file: go.ver 26 | cache: false 27 | 28 | - name: Build binary 29 | run: make 30 | 31 | - name: Verify examples 32 | run: | 33 | export AUTH_KEY=12345 34 | for CFG in docs/examples/* ; do 35 | echo ">>> $CFG" 36 | ./pint -c "$CFG" config 37 | done 38 | -------------------------------------------------------------------------------- /cmd/pint/tests/0138_annoation_regex_key_required.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | Bug: required annotation not set (alerts/annotation) 10 | ---> rules/0001.yml:1-2 -> `Instance Is Down 1` 11 | 2 | expr: up == 0 12 | ^^^ `annotation_.*` annotation is required. 13 | 14 | level=INFO msg="Problems found" Bug=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/0001.yml -- 17 | - alert: Instance Is Down 1 18 | expr: up == 0 19 | 20 | -- .pint.hcl -- 21 | parser { 22 | relaxed = [".*"] 23 | } 24 | rule { 25 | annotation "annotation_.*" { 26 | required = true 27 | severity = "bug" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/pint/tests/0177_rule_name.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | Bug: name not allowed (rule/name) 10 | ---> rules/01.yml:4 -> `foo` 11 | 4 | - alert: foo 12 | ^^ alerting rule name must match `^rec:.+$`. 13 | 14 | level=INFO msg="Problems found" Bug=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/01.yml -- 17 | groups: 18 | - name: foo 19 | rules: 20 | - alert: foo 21 | expr: up == 0 22 | - alert: rec:foo 23 | expr: up == 0 24 | 25 | -- .pint.hcl -- 26 | rule { 27 | name "rec:.+" { 28 | severity = "bug" 29 | comment = "must use rec: prefix" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/pint/tests/0050_watch_severity_fatal.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | exec pint watch --listen=127.0.0.1:6050 --min-severity=fatal --pidfile=pint.pid glob rules 4 | cmp curl.txt metrics.txt 5 | 6 | -- test.sh -- 7 | sleep 5 8 | curl -s http://127.0.0.1:6050/metrics | grep -E '^pint_problem' > curl.txt 9 | cat pint.pid | xargs kill 10 | 11 | -- rules/1.yml -- 12 | - record: broken 13 | expr: foo / count()) 14 | 15 | - record: aggregate 16 | expr: sum(foo) without(job) 17 | 18 | - alert: comparison 19 | expr: foo 20 | 21 | -- .pint.hcl -- 22 | parser { 23 | relaxed = [".*"] 24 | } 25 | rule { 26 | match { 27 | kind = "recording" 28 | } 29 | aggregate ".+" { 30 | keep = [ "job" ] 31 | } 32 | } 33 | 34 | -- metrics.txt -- 35 | pint_problem{filename="rules/1.yml",kind="recording",name="broken",owner="",problem="PromQL syntax error: unexpected right parenthesis ')'",reporter="promql/syntax",severity="fatal"} 1 36 | pint_problems 1 37 | -------------------------------------------------------------------------------- /cmd/pint/tests/0090_lint_min_severity_info.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint --min-severity=info rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 8 | Information: use humanize filters for the results (alerts/template) 9 | ---> rules/0001.yml:5-7 -> `foo` 10 | 5 | expr: rate(errors[2m]) > 0 11 | ^^^^^^^^^^^^^^^^ `rate()` will produce results that are hard to read for humans. 12 | | [...] 13 | 7 | summary: 'error rate: {{ $value }}' 14 | ^^^^^^^ Use one of humanize template functions to make the result more readable. 15 | 16 | level=INFO msg="Problems found" Information=1 17 | -- rules/0001.yml -- 18 | groups: 19 | - name: foo 20 | rules: 21 | - alert: foo 22 | expr: rate(errors[2m]) > 0 23 | annotations: 24 | summary: 'error rate: {{ $value }}' 25 | -------------------------------------------------------------------------------- /internal/config/ci_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCISettings(t *testing.T) { 12 | type testCaseT struct { 13 | err error 14 | conf CI 15 | } 16 | 17 | testCases := []testCaseT{ 18 | { 19 | conf: CI{ 20 | MaxCommits: -5, 21 | }, 22 | err: errors.New("maxCommits cannot be <= 0"), 23 | }, 24 | { 25 | conf: CI{ 26 | MaxCommits: 0, 27 | }, 28 | err: errors.New("maxCommits cannot be <= 0"), 29 | }, 30 | { 31 | conf: CI{ 32 | MaxCommits: 10, 33 | BaseBranch: "main", 34 | }, 35 | }, 36 | } 37 | 38 | for _, tc := range testCases { 39 | t.Run(fmt.Sprintf("%v", tc.conf), func(t *testing.T) { 40 | err := tc.conf.validate() 41 | if err == nil || tc.err == nil { 42 | require.Equal(t, err, tc.err) 43 | } else { 44 | require.EqualError(t, err, tc.err.Error()) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cmd/pint/tests/0146_discovery_filepath_bad_template.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": .pint.hcl:5,14-14: Missing required argument; The argument \"name\" is required, but no definition was found." 8 | -- rules/0001.yml -- 9 | groups: 10 | - name: foo 11 | rules: 12 | - record: sum:up 13 | expr: sum(up) 14 | -- servers/prom1.yml -- 15 | -- servers/prom1.yaml -- 16 | -- servers/prom2.yml -- 17 | -- servers/prom2.yaml -- 18 | -- .pint.hcl -- 19 | discovery { 20 | filepath { 21 | directory = "servers" 22 | match = "(?P\\w+).ya?ml" 23 | template { 24 | uri = "https://{{ $name }}.example.com" 25 | } 26 | } 27 | } 28 | prometheus "prom2" { 29 | uri = "https://unique.example.com" 30 | timeout = "5s" 31 | } 32 | -------------------------------------------------------------------------------- /cmd/pint/tests/0102_prometheus_basic_auth_empty.txt: -------------------------------------------------------------------------------- 1 | http auth-response prometheus /api/v1/status/flags admin pass 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 2 | http auth-response prometheus /api/v1/status/config admin pass 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 3 | http auth-response prometheus /api/v1/query_range admin pass 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 4 | http auth-response prometheus /api/v1/query admin pass 200 {"status":"success","data":{"resultType":"vector","result":[]}} 5 | http start prometheus 127.0.0.1:7102 6 | 7 | ! exec pint -l debug --no-color lint rules 8 | ! stdout . 9 | ! stderr 'admin:pass' 10 | -- rules/1.yml -- 11 | - record: aggregate 12 | expr: sum(foo) without(job) 13 | -- .pint.hcl -- 14 | prometheus "prom" { 15 | uri = "http://admin:pass@127.0.0.1:7102" 16 | failover = [] 17 | timeout = "5s" 18 | required = true 19 | } 20 | parser { 21 | relaxed = [".*"] 22 | } 23 | -------------------------------------------------------------------------------- /tools/gomajor/go.sum: -------------------------------------------------------------------------------- 1 | github.com/icholy/gomajor v0.14.0 h1:qr0eGSMyLcZa7lmSuiplH3kr6j4oH6ET8nkdZqpDcks= 2 | github.com/icholy/gomajor v0.14.0/go.mod h1:oJuk5dppdmnvUQRKsUNelWZ7KmYpvYKVupt/OQ0y9UI= 3 | github.com/icholy/gomajor v0.15.0 h1:/H5vbLaDIZddNKg90OK3bpjhkvlQQDsztFEJdxkv7wE= 4 | github.com/icholy/gomajor v0.15.0/go.mod h1:9i98u5jOn79D5/KHbQxhPI1Nqb+abJw+zFMOPTKSb6E= 5 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 6 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 7 | golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 8 | golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 9 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 10 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 12 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 13 | -------------------------------------------------------------------------------- /internal/checks/rule_report_test.go: -------------------------------------------------------------------------------- 1 | package checks_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | "github.com/cloudflare/pint/internal/promapi" 8 | ) 9 | 10 | func TestReportCheck(t *testing.T) { 11 | testCases := []checkTest{ 12 | { 13 | description: "report passed problem / warning", 14 | content: "- alert: foo\n expr: sum(foo)\n annotations:\n alert: foo\n", 15 | checker: func(_ *promapi.FailoverGroup) checks.RuleChecker { 16 | return checks.NewReportCheck("problem reported", checks.Warning) 17 | }, 18 | prometheus: noProm, 19 | problems: true, 20 | }, 21 | { 22 | description: "report passed problem / info", 23 | content: "- record: foo\n expr: sum(foo)\n", 24 | checker: func(_ *promapi.FailoverGroup) checks.RuleChecker { 25 | return checks.NewReportCheck("problem reported", checks.Information) 26 | }, 27 | prometheus: noProm, 28 | problems: true, 29 | }, 30 | } 31 | runTests(t, testCases) 32 | } 33 | -------------------------------------------------------------------------------- /internal/config/range_query.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type RangeQuerySettings struct { 10 | Max string `hcl:"max" json:"max"` 11 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 12 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 13 | } 14 | 15 | func (s RangeQuerySettings) validate() error { 16 | if s.Max != "" { 17 | dur, err := parseDuration(s.Max) 18 | if err != nil { 19 | return err 20 | } 21 | if dur == 0 { 22 | return errors.New("range_query max value cannot be zero") 23 | } 24 | } 25 | 26 | if s.Severity != "" { 27 | if _, err := checks.ParseSeverity(s.Severity); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func (s RangeQuerySettings) getSeverity(fallback checks.Severity) checks.Severity { 36 | if s.Severity != "" { 37 | sev, _ := checks.ParseSeverity(s.Severity) 38 | return sev 39 | } 40 | return fallback 41 | } 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0074_strict_error.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=WARN msg="Failed to parse group entry" err="invalid group key alert" path=rules/strict.yml line=2 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | Fatal: invalid group key alert (yaml/parse) 10 | ---> rules/strict.yml:2 11 | 2 | - alert: Conntrack_Table_Almost_Full 12 | ^^^ invalid group key alert 13 | 14 | level=INFO msg="Problems found" Fatal=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/strict.yml -- 17 | groups: 18 | - alert: Conntrack_Table_Almost_Full 19 | expr: ((node_nf_conntrack_entries / node_nf_conntrack_entries_limit) * 100) > 75 20 | for: 5m 21 | labels: 22 | component: conntrack 23 | priority: "3" 24 | annotations: 25 | summary: Conntrack table is at {{ $value|humanize }}% 26 | -------------------------------------------------------------------------------- /cmd/pint/tests/0056_prometheus_required.txt: -------------------------------------------------------------------------------- 1 | http response prometheus / 500 Offline 2 | http start prometheus 127.0.0.1:7056 3 | 4 | exec pint -l debug --no-color lint rules 5 | ! stdout . 6 | stderr 'level=ERROR msg="Query returned an error" err="500 Internal Server Error" uri=http://127.0.0.1:7056 query=count\(\\nup\\n\)' 7 | stderr 'level=ERROR msg="Query returned an error" err="500 Internal Server Error" uri=http://127.0.0.1:7056 query=/api/v1/status/config' 8 | stderr 'level=INFO msg="Problems found" Warning=[0-9]+' 9 | 10 | -- rules/1.yaml -- 11 | - record: one 12 | expr: up == 0 13 | labels: 14 | path: a 15 | - record: two 16 | expr: up == 0 17 | labels: 18 | path: a 19 | -- rules/2.yaml -- 20 | - record: one 21 | expr: up == 0 22 | labels: 23 | path: b 24 | - record: two 25 | expr: up == 0 26 | labels: 27 | path: b 28 | 29 | -- .pint.hcl -- 30 | prometheus "prom" { 31 | uri = "http://127.0.0.1:7056" 32 | required = false 33 | } 34 | parser { 35 | relaxed = [".*"] 36 | } 37 | 38 | rule{} 39 | -------------------------------------------------------------------------------- /cmd/pint/tests/0010_syntax_check.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=WARN msg="Failed to parse file content" err="did not find expected '-' indicator" path=rules/1.yaml line=6 9 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 10 | Fatal: did not find expected '-' indicator (yaml/parse) 11 | ---> rules/1.yaml:6 12 | 6 | 13 | ^^^ did not find expected '-' indicator 14 | 15 | level=INFO msg="Problems found" Fatal=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/1.yaml -- 18 | - alert: Good 19 | expr: up == 0 20 | for: 2m 21 | labels: 22 | component: foo 23 | 24 | alert: Bad 25 | expr: up == 0 26 | for: 2m 27 | labels: 28 | component: foo 29 | 30 | -- .pint.hcl -- 31 | parser { 32 | relaxed = ["rules/.*"] 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pint/tests/0175_strict_multi_doc.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=WARN msg="Failed to parse file content" err="multi-document YAML files are not allowed" path=rules/strict.yml line=13 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | Fatal: multi-document YAML files are not allowed (yaml/parse) 10 | ---> rules/strict.yml:13 11 | 13 | --- 12 | ^^^ multi-document YAML files are not allowed 13 | 14 | level=INFO msg="Problems found" Fatal=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/strict.yml -- 17 | --- 18 | groups: 19 | - name: foo 20 | rules: 21 | - record: foo 22 | expr: bar 23 | --- 24 | groups: 25 | - name: foo 26 | rules: 27 | - record: foo 28 | expr: bar 29 | --- 30 | groups: 31 | - name: foo 32 | rules: 33 | - record: foo 34 | expr: bar 35 | -------------------------------------------------------------------------------- /cmd/pint/tests/0092_dir_symlink.txt: -------------------------------------------------------------------------------- 1 | mkdir rules 2 | mkdir rules/src 3 | exec ln -s src rules/dst 4 | exec ln -s rules linked 5 | 6 | exec pint -l debug --no-color lint rules linked rules/src/rule.yaml 7 | ! stdout . 8 | cmp stderr stderr.txt 9 | 10 | -- stderr.txt -- 11 | level=INFO msg="Finding all rules to check" paths=["rules","linked","rules/src/rule.yaml"] 12 | level=DEBUG msg="File parsed" path=rules/src/rule.yaml rules=1 13 | level=DEBUG msg="Glob finder completed" count=1 14 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 15 | level=DEBUG msg="Generated all Prometheus servers" count=0 16 | level=DEBUG msg="Found recording rule" path=rules/src/rule.yaml record=down lines=4-5 state=noop 17 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/src/rule.yaml rule=down 18 | -- rules/src/rule.yaml -- 19 | groups: 20 | - name: foo 21 | rules: 22 | - record: down 23 | expr: up == 0 24 | -------------------------------------------------------------------------------- /tools/deadcode/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 4 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 5 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 6 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 7 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 8 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 9 | golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= 10 | golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 11 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 12 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 13 | -------------------------------------------------------------------------------- /cmd/pint/tests/0181_range_query_max.txt: -------------------------------------------------------------------------------- 1 | env NO_COLOR=1 2 | exec pint --no-color lint --min-severity=info rules 3 | ! stdout . 4 | cmp stderr stderr.txt 5 | 6 | -- stderr.txt -- 7 | level=INFO msg="Loading configuration file" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 10 | Warning: query beyond configured retention (promql/range_query) 11 | ---> rules/0001.yaml:2 -> `Error Rate` 12 | 2 | expr: sum(rate(errors[1h1s])) > 0.5 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `errors[1h1s]` selector is trying to query Prometheus for 1h1s worth of metrics, but 1h is the maximum allowed range query. 14 | 15 | level=INFO msg="Problems found" Warning=1 16 | -- rules/0001.yaml -- 17 | - alert: Error Rate 18 | expr: sum(rate(errors[1h1s])) > 0.5 19 | 20 | - alert: Error Rate 21 | expr: sum(rate(errors[1h])) > 0.5 22 | 23 | -- .pint.hcl -- 24 | parser { 25 | relaxed = [".*"] 26 | } 27 | rule { 28 | range_query { 29 | max = "1h" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/pint/tests/0202_report_recording_rules.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | Bug: problem reported by config rule (rule/report) 10 | ---> rules/1.yml:3-4 -> `bar` 11 | 3 | - record: bar 12 | ^^^ You cannot add any recording rules to this Prometheus server. 13 | 14 | level=INFO msg="Problems found" Bug=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/1.yml -- 17 | - alert: foo 18 | expr: up == 0 19 | - record: bar 20 | expr: sum(up) 21 | 22 | -- .pint.hcl -- 23 | parser { 24 | relaxed = [".*"] 25 | } 26 | rule { 27 | match { 28 | kind = "recording" 29 | } 30 | report { 31 | comment = "You cannot add any recording rules to this Prometheus server." 32 | severity = "bug" 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /cmd/pint/tests/0174_auth_publicURI.txt: -------------------------------------------------------------------------------- 1 | http auth-response prometheus /api/v1/status/flags admin pass 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 2 | http auth-response prometheus /api/v1/status/config admin pass 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 3 | http auth-response prometheus /api/v1/query_range admin pass 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 4 | http auth-response prometheus /api/v1/query admin pass 200 {"status":"success","data":{"resultType":"vector","result":[]}} 5 | http start prometheus 127.0.0.1:7174 6 | 7 | ! exec pint -l debug --no-color lint rules 8 | ! stdout . 9 | ! stderr 'admin:pass' 10 | stderr 'http://admin@127.0.0.1:7174' 11 | -- rules/1.yml -- 12 | - record: aggregate 13 | expr: sum(foo) without(job) 14 | -- .pint.hcl -- 15 | prometheus "prom" { 16 | uri = "http://admin:pass@127.0.0.1:7174" 17 | publicURI = "http://admin@127.0.0.1:7174" 18 | failover = [] 19 | timeout = "5s" 20 | required = true 21 | } 22 | parser { 23 | relaxed = [".*"] 24 | } 25 | -------------------------------------------------------------------------------- /cmd/pint/tests/0204_parser_schema_thanos_err.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=WARN msg="Failed to parse group entry" err="invalid partial_response_strategy value: bob" path=rules/1.yml line=3 9 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 10 | Fatal: invalid partial_response_strategy value: bob (yaml/parse) 11 | ---> rules/1.yml:3 12 | 3 | partial_response_strategy: bob 13 | ^^^ invalid partial_response_strategy value: bob 14 | 15 | level=INFO msg="Problems found" Fatal=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/1.yml -- 18 | groups: 19 | - name: foo 20 | partial_response_strategy: bob 21 | rules: 22 | - alert: foo 23 | expr: up == 0 24 | - record: bar 25 | expr: sum(up) 26 | 27 | -- .pint.hcl -- 28 | parser { 29 | schema = "thanos" 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav_exclude: true 3 | --- 4 | 5 | # pint 6 | 7 | pint is a Prometheus rule linter. 8 | 9 | You can find [online docs](https://cloudflare.github.io/pint/) on GitHub Pages. 10 | 11 | Alternatively you can read raw Markdown documentation [here](/docs/index.md): 12 | 13 | Changelog is kept at [docs/changelog.md](/docs/changelog.md). 14 | 15 | Check [examples](/docs/examples) dir for sample config files. 16 | 17 | ## License 18 | 19 | ```text 20 | Copyright (c) 2021-2025 Cloudflare, Inc. 21 | 22 | Licensed under the Apache License, Version 2.0 (the "License"); 23 | you may not use this file except in compliance with the License. 24 | You may obtain a copy of the License at 25 | 26 | http://www.apache.org/licenses/LICENSE-2.0 27 | 28 | Unless required by applicable law or agreed to in writing, software 29 | distributed under the License is distributed on an "AS IS" BASIS, 30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | See the License for the specific language governing permissions and 32 | limitations under the License. 33 | ``` 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Spellcheck 2 | on: [pull_request] 3 | 4 | permissions: 5 | pages: write 6 | 7 | jobs: 8 | spellcheck: 9 | name: Spellcheck 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 14 | with: 15 | show-progress: false 16 | 17 | - name: Spellcheck 18 | uses: rojopolis/spellcheck-github-actions@16d0338a5a3b5e3111a078029fb9a07a8125053d # 0.55.0 19 | with: 20 | config_path: .github/spellcheck/config.yml 21 | 22 | markdown: 23 | name: Markdownlint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Check out code 27 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 28 | with: 29 | show-progress: false 30 | 31 | - name: Markdownlint 32 | uses: nosborn/github-action-markdown-cli@508d6cefd8f0cc99eab5d2d4685b1d5f470042c1 # v3.5.0 33 | with: 34 | files: . 35 | config_file: ".markdownlint.json" 36 | -------------------------------------------------------------------------------- /cmd/pint/tests/0006_rr_labels.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | Fatal: This rule is not a valid Prometheus rule: `incomplete rule, no alert or record key`. (yaml/parse) 10 | ---> rules/0001.yml:8 11 | 8 | - expr: sum(foo) 12 | ^^^ This rule is not a valid Prometheus rule: `incomplete rule, no alert or record key`. 13 | 14 | level=INFO msg="Problems found" Fatal=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/0001.yml -- 17 | groups: 18 | - name: foo 19 | rules: 20 | - record: "colo:test1" 21 | expr: sum(foo) without(job) 22 | labels: 23 | job: foo 24 | - expr: sum(foo) 25 | labels: 26 | job: foo 27 | -- .pint.hcl -- 28 | rule { 29 | aggregate ".+" { 30 | keep = [ "job" ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/pint/tests/0125_lint_fail_on_warning.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --fail-on=warning rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- rules/0001.yml -- 6 | groups: 7 | - name: foo 8 | rules: 9 | - alert: foo 10 | expr: up{job=~"xxx"} 11 | 12 | -- stderr.txt -- 13 | level=INFO msg="Finding all rules to check" paths=["rules"] 14 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 15 | Warning: always firing alert (alerts/comparison) 16 | ---> rules/0001.yml:5 -> `foo` 17 | 5 | expr: up{job=~"xxx"} 18 | ^^^^^^^^^^^^^^ This query doesn't have any condition and so this alert will always fire if it matches anything. 19 | 20 | Warning: redundant regexp (promql/regexp) 21 | ---> rules/0001.yml:5 -> `foo` 22 | 5 | expr: up{job=~"xxx"} 23 | ^^^^^^^^^^ Unnecessary regexp match on static string `job=~"xxx"`, use `job="xxx"` instead. 24 | 25 | level=INFO msg="Problems found" Warning=2 26 | level=ERROR msg="Execution completed with error(s)" err="found 2 problem(s) with severity Warning or higher" 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0129_tls_cacert_bad.txt: -------------------------------------------------------------------------------- 1 | cert $WORK prom 2 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 3 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 4 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 5 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 6 | http start prometheus 127.0.0.1:7129 $WORK/prom.pem $WORK/prom.key 7 | 8 | ! exec pint -l debug --no-color lint rules 9 | ! stdout . 10 | stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' 11 | 12 | -- rules/1.yml -- 13 | groups: 14 | - name: foo 15 | rules: 16 | - record: aggregate 17 | expr: sum(foo) without(job) 18 | 19 | -- .pint.hcl -- 20 | prometheus "prom" { 21 | uri = "https://127.0.0.1:7129" 22 | failover = [] 23 | timeout = "5s" 24 | required = true 25 | } 26 | -------------------------------------------------------------------------------- /cmd/pint/tests/0137_annotation_regex_key_fail.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 9 | Bug: invalid annotation value (alerts/annotation) 10 | ---> rules/0001.yml:4 -> `Instance Is Down 1` 11 | 4 | annotation_foo: foo 12 | ^^^ `annotation_.*` annotation value must match `^bar$`. 13 | 14 | level=INFO msg="Problems found" Bug=1 15 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 16 | -- rules/0001.yml -- 17 | - alert: Instance Is Down 1 18 | expr: up == 0 19 | annotations: 20 | annotation_foo: foo 21 | annotation_bar: bar 22 | 23 | -- .pint.hcl -- 24 | parser { 25 | relaxed = [".*"] 26 | } 27 | rule { 28 | annotation "annotation_.*" { 29 | required = true 30 | value = "bar" 31 | severity = "bug" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/promapi/errors_test.go: -------------------------------------------------------------------------------- 1 | package promapi 2 | 3 | import ( 4 | "testing" 5 | 6 | v1 "github.com/prometheus/client_golang/api/prometheus/v1" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDecodeErrorType(t *testing.T) { 11 | type testCaseT struct { 12 | input string 13 | expected v1.ErrorType 14 | } 15 | 16 | testCases := []testCaseT{ 17 | {input: "bad_data", expected: v1.ErrBadData}, 18 | {input: "timeout", expected: v1.ErrTimeout}, 19 | {input: "canceled", expected: v1.ErrCanceled}, 20 | {input: "execution", expected: v1.ErrExec}, 21 | {input: "bad_response", expected: v1.ErrBadResponse}, 22 | {input: "server_error", expected: v1.ErrServer}, 23 | {input: "client_error", expected: v1.ErrClient}, 24 | {input: "unknown_type", expected: ErrUnknown}, 25 | {input: "", expected: ErrUnknown}, 26 | {input: "random", expected: ErrUnknown}, 27 | } 28 | 29 | for _, tc := range testCases { 30 | t.Run(tc.input, func(t *testing.T) { 31 | result := decodeErrorType(tc.input) 32 | require.Equal(t, tc.expected, result) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/pint/tests/0165_pint_comment_error.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color lint --min-severity=info rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Finding all rules to check" paths=["rules"] 7 | level=INFO msg="Checking Prometheus rules" entries=3 workers=10 online=true 8 | Warning: invalid comment (pint/comment) 9 | ---> rules/1.yml:4 10 | 4 | # pint ignore/line this line 11 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This comment is not a valid pint control comment: unexpected comment suffix: "this line" 12 | 13 | Information: This file was excluded from pint checks. (ignore/file) 14 | ---> rules/2.yml:4 15 | 4 | # pint ignore/file 16 | ^^^^^^^^^^^^^^^^ This file was excluded from pint checks. 17 | 18 | level=INFO msg="Problems found" Warning=1 Information=1 19 | -- rules/1.yml -- 20 | groups: 21 | - name: g1 22 | rules: 23 | # pint ignore/line this line 24 | - record: up:count 25 | expr: count(up == 1) 26 | -- rules/2.yml -- 27 | groups: 28 | - name: g1 29 | rules: 30 | # pint ignore/file 31 | - record: up:count 32 | expr: count(up == 1) 33 | -------------------------------------------------------------------------------- /cmd/pint/tests/0192_ci_broken_removed_rule.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/rules.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | exec git rm rules.yml 16 | exec git commit -am 'v2' 17 | 18 | exec pint --no-color ci 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=INFO msg="Finding all rules to check on current git branch" base=main 25 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 26 | -- src/rules.yml -- 27 | groups: 28 | - name: foo 29 | rules: 30 | - record: 31 | expr: sum(foo) by(job) 32 | - record: rule2 33 | expr: sum(foo 34 | 35 | -- src/.pint.hcl -- 36 | ci { 37 | baseBranch = "main" 38 | } 39 | parser { 40 | include = ["rules.yml"] 41 | } 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0208_lint_full_path.txt: -------------------------------------------------------------------------------- 1 | exec pint -l debug --no-color lint $WORK/rules 2 | ! stdout . 3 | stderr 'level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true' 4 | ! stderr 'level=INFO msg="No rules found, skipping Prometheus discovery"' 5 | 6 | exec pint -l debug --no-color lint rules 7 | ! stdout . 8 | stderr 'level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true' 9 | ! stderr 'level=INFO msg="No rules found, skipping Prometheus discovery"' 10 | 11 | exec pint -l debug --no-color lint ../*0208*/rules 12 | ! stdout . 13 | stderr 'level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true' 14 | ! stderr 'level=INFO msg="No rules found, skipping Prometheus discovery"' 15 | 16 | -- rules/0001.yml -- 17 | groups: 18 | - name: test1 19 | rules: 20 | - record: "colo:test1" 21 | expr: sum(foo) without(job) 22 | 23 | -- rules/0002.yml -- 24 | groups: 25 | - name: test2 26 | rules: 27 | - record: "colo:test2" 28 | expr: sum(foo) without(job) 29 | 30 | -- .pint.hcl -- 31 | parser { 32 | include = ["(.*/)?rules/.*"] 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pint/tests/0067_relaxed.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=WARN msg="Failed to parse file content" err="top level field must be a groups key, got list" path=rules/strict.yml line=2 9 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 10 | Fatal: top level field must be a groups key, got list (yaml/parse) 11 | ---> rules/strict.yml:2 12 | 2 | - alert: No Owner 13 | ^^^ top level field must be a groups key, got list 14 | 15 | level=INFO msg="Problems found" Fatal=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/strict.yml -- 18 | {%- raw %} # pint ignore/line 19 | - alert: No Owner 20 | expr: up > 0 21 | 22 | -- rules/relaxed.yml -- 23 | {%- raw %} # pint ignore/line 24 | - alert: Owner Set 25 | expr: up{job="foo"} == 0 26 | -- .pint.hcl -- 27 | parser { 28 | relaxed = ["rules/relaxed.*"] 29 | } 30 | -------------------------------------------------------------------------------- /cmd/pint/tests/0184_ci_file_ignore.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/rules.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | exec touch .keep 16 | exec git add .keep 17 | exec git commit -am 'v2' 18 | 19 | exec pint --no-color ci 20 | ! stdout . 21 | cmp stderr ../stderr.txt 22 | 23 | -- stderr.txt -- 24 | level=INFO msg="Loading configuration file" path=.pint.hcl 25 | level=INFO msg="Finding all rules to check on current git branch" base=main 26 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 27 | -- src/rules.yml -- 28 | # pint ignore/file 29 | - record: rule1 30 | expr: sum(foo) by(job) 31 | - record: rule2 32 | expr: sum(foo) 33 | 34 | -- src/.pint.hcl -- 35 | ci { 36 | baseBranch = "main" 37 | } 38 | parser { 39 | relaxed = [".*"] 40 | include = ["rules.yml"] 41 | } 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0190_ci_dup_prom.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/.pint.hcl . 6 | env GIT_AUTHOR_NAME=pint 7 | env GIT_AUTHOR_EMAIL=pint@example.com 8 | env GIT_COMMITTER_NAME=pint 9 | env GIT_COMMITTER_EMAIL=pint@example.com 10 | exec git add . 11 | exec git commit -am 'import rules and config' 12 | 13 | exec git checkout -b v2 14 | exec touch .keep 15 | exec git add .keep 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --no-color ci 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=ERROR msg="Execution completed with error(s)" err="failed to load config file \".pint.hcl\": prometheus server name must be unique, found two or more config blocks using \"prom\" name" 25 | -- src/.pint.hcl -- 26 | ci { 27 | baseBranch = "main" 28 | } 29 | prometheus "prom" { 30 | uri = "http://127.0.0.1:2190" 31 | timeout = "5s" 32 | required = true 33 | } 34 | prometheus "prom" { 35 | uri = "http://127.0.0.1:3190" 36 | timeout = "5s" 37 | required = true 38 | } 39 | -------------------------------------------------------------------------------- /cmd/pint/tests/0041_watch.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | exec pint --no-color -l debug watch --interval=5s --listen=127.0.0.1:6041 --pidfile=pint.pid glob rules 4 | ! stdout . 5 | 6 | stderr 'level=INFO msg="Pidfile created" path=pint.pid' 7 | stderr 'level=INFO msg="Started HTTP server" address=127.0.0.1:6041' 8 | stderr 'level=INFO msg="Will continuously run checks until terminated" interval=5s' 9 | stderr 'level=DEBUG msg="Running checks"' 10 | stderr 'level=ERROR msg="Got an error when running checks" err="no matching files"' 11 | stderr 'level=DEBUG msg="Running checks"' 12 | stderr 'level=ERROR msg="Got an error when running checks" err="no matching files"' 13 | stderr 'level=INFO msg="Shutting down"' 14 | stderr 'level=INFO msg="Waiting for all background tasks to finish"' 15 | stderr 'level=INFO msg="Background worker finished"' 16 | stderr 'level=INFO msg="Pidfile removed" path=pint.pid' 17 | 18 | grep '^pint_check_iterations_total 2$' curl.txt 19 | 20 | -- test.sh -- 21 | sleep 7 22 | curl -so curl.txt http://127.0.0.1:6041/metrics 23 | grep -E '^pint_check_iterations_total ' curl.txt 24 | cat pint.pid | xargs kill 25 | -------------------------------------------------------------------------------- /cmd/pint/tests/0101_prometheus_basic_auth.txt: -------------------------------------------------------------------------------- 1 | http auth-response prometheus /api/v1/status/flags admin pass 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 2 | http auth-response prometheus /api/v1/status/config admin pass 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 3 | http auth-response prometheus /api/v1/metadata admin pass 200 {"status":"success","data":{}} 4 | http auth-response prometheus /api/v1/query_range admin pass 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 5 | http auth-response prometheus /api/v1/query admin pass 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 6 | http start prometheus 127.0.0.1:7101 7 | 8 | exec pint -l debug --no-color lint rules 9 | ! stdout . 10 | ! stderr 'admin:pass' 11 | -- rules/1.yml -- 12 | - record: aggregate 13 | expr: sum(foo) without(job) 14 | -- .pint.hcl -- 15 | prometheus "prom" { 16 | uri = "http://admin:pass@127.0.0.1:7101" 17 | failover = [] 18 | timeout = "5s" 19 | required = true 20 | } 21 | parser { 22 | relaxed = [".*"] 23 | } 24 | -------------------------------------------------------------------------------- /cmd/pint/tests/0038_disable_checks_regex.txt: -------------------------------------------------------------------------------- 1 | exec pint --no-color -d 'alerts/.*' -d 'promql/c.+' lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=3 workers=10 online=true 9 | Warning: required label is being removed via aggregation (promql/aggregate) 10 | ---> rules/0001.yml:6 -> `sum:job` 11 | 6 | expr: sum(foo) 12 | ^^^ Query is using aggregation that removes all labels. 13 | `job` label is required and should be preserved when aggregating all rules. 14 | 15 | level=INFO msg="Problems found" Warning=1 16 | -- rules/0001.yml -- 17 | - alert: default-for 18 | expr: foo > 1 19 | for: 0m 20 | 21 | - record: sum:job 22 | expr: sum(foo) 23 | 24 | - alert: no-comparison 25 | expr: foo 26 | 27 | -- .pint.hcl -- 28 | parser { 29 | relaxed = [".*"] 30 | } 31 | rule { 32 | match { 33 | kind = "recording" 34 | } 35 | aggregate ".+" { 36 | keep = [ "job" ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Go code 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | test-go: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | show-progress: false 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 24 | with: 25 | go-version-file: go.ver 26 | cache: false 27 | 28 | - name: Test 29 | run: make test 30 | 31 | - name: Check for local changes 32 | run: git diff --exit-code 33 | 34 | - name: Report code coverage 35 | uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | files: ./.cover/coverage.out 39 | fail_ci_if_error: true 40 | handle_no_reports_found: true 41 | continue-on-error: true 42 | 43 | -------------------------------------------------------------------------------- /cmd/pint/tests/0147_discovery_filepath_error.txt: -------------------------------------------------------------------------------- 1 | ! exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 10 | level=DEBUG msg="Glob finder completed" count=1 11 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 12 | level=INFO msg="Finding Prometheus servers using file paths" dir=notfound match=^(?P\w+).ya?ml$ 13 | level=ERROR msg="Execution completed with error(s)" err="filepath discovery error: lstat notfound: no such file or directory" 14 | -- rules/0001.yml -- 15 | groups: 16 | - name: foo 17 | rules: 18 | - record: sum:up 19 | expr: sum(up) 20 | -- .pint.hcl -- 21 | discovery { 22 | filepath { 23 | directory = "notfound" 24 | match = "(?P\\w+).ya?ml" 25 | template { 26 | name = "{{ $name }}" 27 | uri = "https://{{ $name }}.example.com" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/pint/tests/0214_gitlab_no_auth_token.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | exec touch rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | 16 | ! exec pint --no-color ci 17 | ! stdout . 18 | cmp stderr ../stderr.txt 19 | 20 | -- stderr.txt -- 21 | level=INFO msg="Loading configuration file" path=.pint.hcl 22 | level=INFO msg="Finding all rules to check on current git branch" base=main 23 | level=INFO msg="Checking Prometheus rules" entries=0 workers=10 online=true 24 | level=INFO msg="No rules found, skipping Prometheus discovery" 25 | level=ERROR msg="Execution completed with error(s)" err="GITLAB_AUTH_TOKEN env variable is required when reporting to GitLab" 26 | -- src/.pint.hcl -- 27 | ci { 28 | baseBranch = "main" 29 | } 30 | repository { 31 | gitlab { 32 | uri = "http://127.0.0.1:6214" 33 | timeout = "30s" 34 | project = "1234" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/pint/tests/0099_symlink_outside_glob.txt: -------------------------------------------------------------------------------- 1 | mkdir rules/strict 2 | exec ln -s ../relaxed/1.yml rules/strict/symlink.yml 3 | 4 | exec pint -l debug --no-color lint rules/relaxed 5 | ! stdout . 6 | cmp stderr stderr.txt 7 | 8 | -- stderr.txt -- 9 | level=INFO msg="Loading configuration file" path=.pint.hcl 10 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 11 | level=INFO msg="Finding all rules to check" paths=["rules/relaxed"] 12 | level=DEBUG msg="File parsed" path=rules/relaxed/1.yml rules=1 13 | level=DEBUG msg="Glob finder completed" count=1 14 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 15 | level=DEBUG msg="Generated all Prometheus servers" count=0 16 | level=DEBUG msg="Found recording rule" path=rules/relaxed/1.yml record=foo lines=1-2 state=noop 17 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/relaxed/1.yml rule=foo 18 | -- rules/relaxed/1.yml -- 19 | - record: foo 20 | expr: up == 0 21 | -- .pint.hcl -- 22 | parser { 23 | relaxed = ["rules/relaxed/.*"] 24 | } 25 | -------------------------------------------------------------------------------- /docs/checks/rule/owner.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | parent: Checks 4 | grand_parent: Documentation 5 | --- 6 | 7 | # rule/owner 8 | 9 | This check can be used to enforce rule ownership comments used by `pint watch` 10 | command when exporting metrics about problems detected in rules. 11 | 12 | If you see this check reports it means that `--require-owner` flag is enabled 13 | for pint and a rule file is missing required ownership comment. 14 | 15 | To set a rule owner add a `# pint file/owner $owner` comment in a file, to set 16 | an owner for all rules in that file. You can also set an owner per rule, by adding 17 | `# pint rule/owner $owner` comment around given rule. 18 | 19 | Example: 20 | 21 | ```yaml 22 | # pint file/owner bob 23 | 24 | - alert: ... 25 | expr: ... 26 | 27 | # pint rule/owner alice 28 | - alert: ... 29 | expr: ... 30 | ``` 31 | 32 | ## Configuration 33 | 34 | This check doesn't have any configuration options. 35 | 36 | ## How to enable it 37 | 38 | This check is enabled only if you pass `--require-owner` flag to `pint lint` 39 | or `pint ci` commands. 40 | 41 | ## How to disable it 42 | 43 | Remove `--require-owner` flag from pint CLI arguments. 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: pint ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.pull_request.head.repo.full_name == 'cloudflare/pint' }} 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | show-progress: false 20 | fetch-depth: 0 21 | 22 | - name: Fetch main branch 23 | run: | 24 | git fetch origin main 25 | git checkout main 26 | git fetch origin $GITHUB_HEAD_REF 27 | git checkout $GITHUB_HEAD_REF -- 28 | 29 | - name: Set up Go 30 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 31 | with: 32 | go-version-file: go.ver 33 | cache: false 34 | 35 | - name: Compile pint 36 | run: make build 37 | 38 | - name: Run pint ci 39 | run: ./pint -l debug -c .github/pint/pint.hcl ci 40 | env: 41 | GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0187_ci_noop_yaml_parse.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/rules.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | exec touch .keep 16 | exec git add .keep 17 | exec git commit -am 'v2' 18 | 19 | exec pint --no-color ci 20 | ! stdout . 21 | cmp stderr ../stderr.txt 22 | 23 | -- stderr.txt -- 24 | level=INFO msg="Loading configuration file" path=.pint.hcl 25 | level=INFO msg="Finding all rules to check on current git branch" base=main 26 | level=WARN msg="Failed to parse file content" err="top level field must be a groups key, got list" path=rules.yml line=1 27 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 28 | -- src/rules.yml -- 29 | - record: rule1 30 | expr: sum(foo) by(job) 31 | - record: rule2 32 | expr: sum(foo) 33 | 34 | -- src/.pint.hcl -- 35 | ci { 36 | baseBranch = "main" 37 | } 38 | parser { 39 | include = ["rules.yml"] 40 | } 41 | -------------------------------------------------------------------------------- /cmd/pint/tests/0004_fail_invalid_yaml.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=WARN msg="Failed to parse file content" err="did not find expected key" path=rules/bad.yaml line=4 9 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 10 | Fatal: did not find expected key (yaml/parse) 11 | ---> rules/bad.yaml:4 12 | 4 | 13 | ^^^ did not find expected key 14 | 15 | Fatal: PromQL syntax error (promql/syntax) 16 | ---> rules/ok.yml:5 -> `sum:missing` 17 | 5 | expr: sum(foo[5m) 18 | ^ unclosed left bracket 19 | 20 | level=INFO msg="Problems found" Fatal=2 21 | level=ERROR msg="Execution completed with error(s)" err="found 2 problem(s) with severity Bug or higher" 22 | -- rules/ok.yml -- 23 | groups: 24 | - name: foo 25 | rules: 26 | - record: sum:missing 27 | expr: sum(foo[5m) 28 | 29 | -- rules/bad.yaml -- 30 | xxx: 31 | xxx: 32 | xxx: 33 | 34 | - xx 35 | - yyy 36 | 37 | -- .pint.hcl -- 38 | parser { 39 | relaxed = [".*"] 40 | } 41 | -------------------------------------------------------------------------------- /cmd/pint/tests/0182_range_query_custom_severity.txt: -------------------------------------------------------------------------------- 1 | env NO_COLOR=1 2 | ! exec pint --no-color lint --min-severity=info rules 3 | ! stdout . 4 | cmp stderr stderr.txt 5 | 6 | -- stderr.txt -- 7 | level=INFO msg="Loading configuration file" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 10 | Bug: query beyond configured retention (promql/range_query) 11 | ---> rules/0001.yaml:2 -> `Error Rate` 12 | 2 | expr: sum(rate(errors[1h1s])) > 0.5 13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `errors[1h1s]` selector is trying to query Prometheus for 1h1s worth of metrics, but 1h is the maximum allowed range query. 14 | 15 | level=INFO msg="Problems found" Bug=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/0001.yaml -- 18 | - alert: Error Rate 19 | expr: sum(rate(errors[1h1s])) > 0.5 20 | 21 | - alert: Error Rate 22 | expr: sum(rate(errors[1h])) > 0.5 23 | 24 | -- .pint.hcl -- 25 | parser { 26 | relaxed = [".*"] 27 | } 28 | rule { 29 | range_query { 30 | max = "1h" 31 | severity = "bug" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pint/tests/0185_state_empty.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/rules.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | exec touch .keep 16 | exec git add .keep 17 | exec git commit -am 'v2' 18 | 19 | exec pint --no-color ci 20 | ! stdout . 21 | cmp stderr ../stderr.txt 22 | 23 | -- stderr.txt -- 24 | level=INFO msg="Loading configuration file" path=.pint.hcl 25 | level=INFO msg="Finding all rules to check on current git branch" base=main 26 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 27 | -- src/rules.yml -- 28 | - record: rule1 29 | expr: sum(foo) by(job) 30 | - record: rule2 31 | expr: sum(foo) 32 | 33 | -- src/.pint.hcl -- 34 | ci { 35 | baseBranch = "main" 36 | } 37 | parser { 38 | relaxed = [".*"] 39 | include = ["rules.yml"] 40 | } 41 | rule { 42 | aggregate ".+" { 43 | keep = [ "job" ] 44 | severity = "bug" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/pint/tests/0169_watch_rule_files_noprom.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color -l debug watch --interval=5s --listen=127.0.0.1:6169 --pidfile=pint.pid rule_files prom 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Configured new Prometheus server" name=foo uris=1 uptime=up tags=[] include=[] exclude=[] 9 | level=DEBUG msg="Starting query workers" name=foo uri=http://localhost:7169 workers=16 10 | level=ERROR msg="Execution completed with error(s)" err="no Prometheus named \"prom\" configured in pint" 11 | -- test.sh -- 12 | sleep 7 13 | mv more/*.yaml rules/ 14 | sleep 7 15 | cat pint.pid | xargs kill 16 | 17 | -- rules/1.yaml -- 18 | groups: 19 | - name: g1 20 | rules: 21 | - alert: DownAlert1 22 | expr: up == 0 23 | -- rules/2.yaml -- 24 | groups: 25 | - name: g2 26 | rules: 27 | - alert: DownAlert2 28 | expr: up == 0 29 | -- more/3.yaml -- 30 | groups: 31 | - name: g2 32 | rules: 33 | - alert: DownAlert2 34 | expr: up == 0 35 | -- .pint.hcl -- 36 | prometheus "foo" { 37 | uri = "http://localhost:7169" 38 | } 39 | -------------------------------------------------------------------------------- /cmd/pint/tests/0215_github_no_auth_token.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | exec touch rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | 16 | ! exec pint --no-color ci 17 | ! stdout . 18 | cmp stderr ../stderr.txt 19 | 20 | -- stderr.txt -- 21 | level=INFO msg="Loading configuration file" path=.pint.hcl 22 | level=INFO msg="Finding all rules to check on current git branch" base=main 23 | level=INFO msg="Checking Prometheus rules" entries=0 workers=10 online=true 24 | level=INFO msg="No rules found, skipping Prometheus discovery" 25 | level=ERROR msg="Execution completed with error(s)" err="GITHUB_AUTH_TOKEN env variable is required when reporting to GitHub" 26 | -- src/.pint.hcl -- 27 | ci { 28 | baseBranch = "main" 29 | } 30 | repository { 31 | github { 32 | baseuri = "http://127.0.0.1:6215" 33 | uploaduri = "http://127.0.0.1:6215" 34 | owner = "cloudflare" 35 | repo = "pint" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/config/options/selector.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type SelectorSettings struct { 10 | Key string `hcl:",label" json:"key"` 11 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 12 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 13 | RequiredLabels []string `hcl:"requiredLabels" json:"requiredLabels"` 14 | } 15 | 16 | func (ss SelectorSettings) Validate() error { 17 | if ss.Key == "" { 18 | return errors.New("selector key cannot be empty") 19 | } 20 | 21 | if _, err := checks.NewTemplatedRegexp(ss.Key); err != nil { 22 | return err 23 | } 24 | 25 | if ss.Severity != "" { 26 | if _, err := checks.ParseSeverity(ss.Severity); err != nil { 27 | return err 28 | } 29 | } 30 | 31 | if len(ss.RequiredLabels) == 0 { 32 | return errors.New("requiredLabels cannot be empty") 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (ss SelectorSettings) GetSeverity(fallback checks.Severity) checks.Severity { 39 | if ss.Severity != "" { 40 | sev, _ := checks.ParseSeverity(ss.Severity) 41 | return sev 42 | } 43 | return fallback 44 | } 45 | -------------------------------------------------------------------------------- /cmd/pint/tests/0131_tls_cacert_bad_skipVerify.txt: -------------------------------------------------------------------------------- 1 | cert $WORK prom 2 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 3 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 4 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 5 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 6 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 7 | http start prometheus 127.0.0.1:7131 $WORK/prom.pem $WORK/prom.key 8 | 9 | exec pint -l debug --no-color lint rules 10 | ! stdout . 11 | ! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' 12 | 13 | -- rules/1.yml -- 14 | groups: 15 | - name: foo 16 | rules: 17 | - record: aggregate 18 | expr: sum(foo) without(job) 19 | 20 | -- .pint.hcl -- 21 | prometheus "prom" { 22 | uri = "https://127.0.0.1:7131" 23 | failover = [] 24 | timeout = "5s" 25 | required = true 26 | tls { 27 | skipVerify = true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/pint/tests/0153_ci_bitbucket_missing_token.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --no-color -l debug ci 19 | ! stdout . 20 | stderr 'level=ERROR msg="Execution completed with error\(s\)" err="BITBUCKET_AUTH_TOKEN env variable is required when reporting to BitBucket"' 21 | 22 | -- src/v1.yml -- 23 | - alert: rule1 24 | expr: sum(foo) by(job) > 0 25 | 26 | -- src/v2.yml -- 27 | - alert: rule1 28 | expr: sum(foo) by(job) > 0 29 | - alert: rule2 30 | expr: >- 31 | sum(foo) 32 | by(job) > 0 33 | 34 | -- src/.pint.hcl -- 35 | parser { 36 | relaxed = [".*"] 37 | } 38 | ci { 39 | baseBranch = "main" 40 | } 41 | repository { 42 | bitbucket { 43 | uri = "http://127.0.0.1:6153" 44 | timeout = "10s" 45 | project = "prometheus" 46 | repository = "rules" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cmd/pint/tests/0203_parser_schema_prom_err.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=WARN msg="Failed to parse group entry" err="partial_response_strategy is only valid when parser is configured to use the Thanos rule schema" path=rules/1.yml line=3 9 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 10 | Fatal: partial_response_strategy is only valid when parser is configured to use the Thanos rule schema (yaml/parse) 11 | ---> rules/1.yml:3 12 | 3 | partial_response_strategy: warn 13 | ^^^ partial_response_strategy is only valid when parser is configured to use the Thanos rule schema 14 | 15 | level=INFO msg="Problems found" Fatal=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/1.yml -- 18 | groups: 19 | - name: foo 20 | partial_response_strategy: warn 21 | rules: 22 | - alert: foo 23 | expr: up == 0 24 | - record: bar 25 | expr: sum(up) 26 | 27 | -- .pint.hcl -- 28 | parser { 29 | schema = "prometheus" 30 | } 31 | -------------------------------------------------------------------------------- /cmd/pint/tests/0097_rule_file_symlink_error.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | exec ln -s xxx.yml symlink.yml 7 | cp ../src/.pint.hcl . 8 | env GIT_AUTHOR_NAME=pint 9 | env GIT_AUTHOR_EMAIL=pint@example.com 10 | env GIT_COMMITTER_NAME=pint 11 | env GIT_COMMITTER_EMAIL=pint@example.com 12 | exec git add . 13 | exec git commit -am 'import rules and config' 14 | 15 | exec git checkout -b v2 16 | cp ../src/v2.yml rules.yml 17 | exec git commit -am 'v2' 18 | 19 | ! exec pint -l debug -d promql/series --no-color ci 20 | ! stdout . 21 | stderr 'level=ERROR msg="Execution completed with error\(s\)" err="symlink.yml is a symlink but target file cannot be evaluated: lstat xxx.yml: no such file or directory"' 22 | 23 | -- src/v1.yml -- 24 | groups: 25 | - name: foo 26 | rules: 27 | - alert: rule1 28 | expr: rate(errors_total[5m]) > 0 29 | - alert: rule2 30 | expr: rate(errors_total[5m]) > 0 31 | 32 | -- src/v2.yml -- 33 | groups: 34 | - name: foo 35 | rules: 36 | - alert: rule1 37 | expr: rate(errors_total[2m]) > 0 38 | - alert: rule2 39 | expr: rate(errors_total[2m]) > 0 40 | 41 | -- src/.pint.hcl -- 42 | ci { 43 | baseBranch = "main" 44 | } 45 | 46 | -------------------------------------------------------------------------------- /cmd/pint/tests/0180_parser_exclude_md.txt: -------------------------------------------------------------------------------- 1 | exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 10 | level=DEBUG msg="File path is in the exclude list" path=rules/README.md exclude=["^.*.md$","^.pint.hcl$"] 11 | level=DEBUG msg="Glob finder completed" count=1 12 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 13 | level=DEBUG msg="Generated all Prometheus servers" count=0 14 | level=DEBUG msg="Found recording rule" path=rules/0001.yml record=ok lines=1-2 state=noop 15 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/0001.yml rule=ok 16 | -- rules/0001.yml -- 17 | - record: ok 18 | expr: sum(foo) without(job) 19 | -- rules/README.md -- 20 | This is a readme file 21 | `foo` 22 | -- .pint.hcl -- 23 | parser { 24 | relaxed = [".*"] 25 | exclude = [".*.md"] 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0130_tls_ca_good.txt: -------------------------------------------------------------------------------- 1 | cert $WORK prom 2 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 3 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 4 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 5 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 6 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 7 | http start prometheus 127.0.0.1:7130 $WORK/prom.pem $WORK/prom.key 8 | 9 | exec pint -l debug --no-color lint rules 10 | ! stdout . 11 | ! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' 12 | 13 | -- rules/1.yml -- 14 | groups: 15 | - name: foo 16 | rules: 17 | - record: aggregate 18 | expr: sum(foo) without(job) 19 | 20 | -- .pint.hcl -- 21 | prometheus "prom" { 22 | uri = "https://127.0.0.1:7130" 23 | failover = [] 24 | timeout = "5s" 25 | required = true 26 | tls { 27 | serverName = "prom" 28 | caCert = "prom-ca.pem" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/pint/tests/0217_github_missing_pr_number.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | exec touch rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | 16 | env GITHUB_AUTH_TOKEN=1 17 | ! exec pint --no-color ci 18 | ! stdout . 19 | cmp stderr ../stderr.txt 20 | 21 | -- stderr.txt -- 22 | level=INFO msg="Loading configuration file" path=.pint.hcl 23 | level=INFO msg="Finding all rules to check on current git branch" base=main 24 | level=INFO msg="Checking Prometheus rules" entries=0 workers=10 online=true 25 | level=INFO msg="No rules found, skipping Prometheus discovery" 26 | level=ERROR msg="Execution completed with error(s)" err="GITHUB_PULL_REQUEST_NUMBER env variable is required when reporting to GitHub" 27 | -- src/.pint.hcl -- 28 | ci { 29 | baseBranch = "main" 30 | } 31 | repository { 32 | github { 33 | baseuri = "http://127.0.0.1:6217" 34 | uploaduri = "http://127.0.0.1:6217" 35 | owner = "cloudflare" 36 | repo = "pint" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/checks/promql_syntax_test.go: -------------------------------------------------------------------------------- 1 | package checks_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | "github.com/cloudflare/pint/internal/promapi" 8 | ) 9 | 10 | func newSyntaxCheck(_ *promapi.FailoverGroup) checks.RuleChecker { 11 | return checks.NewSyntaxCheck() 12 | } 13 | 14 | func TestSyntaxCheck(t *testing.T) { 15 | testCases := []checkTest{ 16 | { 17 | description: "valid recording rule", 18 | content: "- record: foo\n expr: sum(foo)\n", 19 | checker: newSyntaxCheck, 20 | prometheus: noProm, 21 | }, 22 | { 23 | description: "valid alerting rule", 24 | content: "- alert: foo\n expr: sum(foo)\n", 25 | checker: newSyntaxCheck, 26 | prometheus: noProm, 27 | }, 28 | { 29 | description: "no arguments for aggregate expression provided", 30 | content: "- record: foo\n expr: sum(\n", 31 | checker: newSyntaxCheck, 32 | prometheus: noProm, 33 | problems: true, 34 | }, 35 | { 36 | description: "unclosed left parenthesis", 37 | content: "- record: foo\n expr: sum(foo) by(", 38 | checker: newSyntaxCheck, 39 | prometheus: noProm, 40 | problems: true, 41 | }, 42 | } 43 | runTests(t, testCases) 44 | } 45 | -------------------------------------------------------------------------------- /internal/config/aggregate.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type AggregateSettings struct { 10 | Name string `hcl:",label" json:"name"` 11 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 12 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 13 | Keep []string `hcl:"keep,optional" json:"keep,omitempty"` 14 | Strip []string `hcl:"strip,optional" json:"strip,omitempty"` 15 | } 16 | 17 | func (ag AggregateSettings) validate() error { 18 | if ag.Name == "" { 19 | return errors.New("empty name regex") 20 | } 21 | 22 | if ag.Severity != "" { 23 | if _, err := checks.ParseSeverity(ag.Severity); err != nil { 24 | return err 25 | } 26 | } 27 | 28 | if _, err := checks.NewTemplatedRegexp(ag.Name); err != nil { 29 | return err 30 | } 31 | 32 | if len(ag.Keep) == 0 && len(ag.Strip) == 0 { 33 | return errors.New("must specify keep or strip list") 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (ag AggregateSettings) getSeverity(fallback checks.Severity) checks.Severity { 40 | if ag.Severity != "" { 41 | sev, _ := checks.ParseSeverity(ag.Severity) 42 | return sev 43 | } 44 | return fallback 45 | } 46 | -------------------------------------------------------------------------------- /cmd/pint/tests/0179_parser_exclude.txt: -------------------------------------------------------------------------------- 1 | exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 10 | level=DEBUG msg="File path is in the exclude list" path=rules/0002.yml exclude=["^rules/0002.yml$","^.pint.hcl$"] 11 | level=DEBUG msg="Glob finder completed" count=1 12 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 13 | level=DEBUG msg="Generated all Prometheus servers" count=0 14 | level=DEBUG msg="Found recording rule" path=rules/0001.yml record=ok lines=1-2 state=noop 15 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/0001.yml rule=ok 16 | -- rules/0001.yml -- 17 | - record: ok 18 | expr: sum(foo) without(job) 19 | -- rules/0002.yml -- 20 | - record: failing 21 | expr: sum() 22 | -- .pint.hcl -- 23 | parser { 24 | relaxed = [".*"] 25 | exclude = ["rules/0002.yml"] 26 | } 27 | -------------------------------------------------------------------------------- /docs/checks/pint/comment.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | parent: Checks 4 | grand_parent: Documentation 5 | --- 6 | 7 | # pint/comment 8 | 9 | You will only ever see this check reporting problems if you have a file 10 | with a comment that looks like one of pint control comments 11 | (see [page](../../ignoring.md)) but pint cannot parse it. 12 | 13 | This might be, for example, when you're trying to disable some checks 14 | for a specific rule using `# pint disable ...`, but the comment is not 15 | "touching" any rule and so pint cannot apply it. 16 | 17 | Valid rule specific comments: 18 | 19 | ```yaml 20 | # pint disable promql/series(my_metric) 21 | - record: foo 22 | expr: sum(my_metric) without(instance) 23 | 24 | - record: foo 25 | # pint disable promql/series(my_metric) 26 | expr: sum(my_metric) without(instance) 27 | ``` 28 | 29 | Invalid comment that's not attached to any rule: 30 | 31 | ```yaml 32 | # pint disable promql/series(my_metric) 33 | 34 | - record: foo 35 | expr: sum(my_metric) without(instance) 36 | ``` 37 | 38 | ## Configuration 39 | 40 | This check doesn't have any configuration options. 41 | 42 | ## How to enable it 43 | 44 | This check is enabled by default. 45 | 46 | ## How to disable it 47 | 48 | You cannot disable this check. 49 | -------------------------------------------------------------------------------- /internal/config/reject.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/cloudflare/pint/internal/checks" 5 | ) 6 | 7 | type RejectSettings struct { 8 | Regex string `hcl:",label" json:"key,omitempty"` 9 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 10 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 11 | LabelKeys bool `hcl:"label_keys,optional" json:"label_keys,omitempty"` 12 | LabelValues bool `hcl:"label_values,optional" json:"label_values,omitempty"` 13 | AnnotationKeys bool `hcl:"annotation_keys,optional" json:"annotation_keys,omitempty"` 14 | AnnotationValues bool `hcl:"annotation_values,optional" json:"annotation_values,omitempty"` 15 | } 16 | 17 | func (rs RejectSettings) validate() error { 18 | if _, err := checks.NewTemplatedRegexp(rs.Regex); err != nil { 19 | return err 20 | } 21 | 22 | if rs.Severity != "" { 23 | if _, err := checks.ParseSeverity(rs.Severity); err != nil { 24 | return err 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func (rs RejectSettings) getSeverity(fallback checks.Severity) checks.Severity { 32 | if rs.Severity != "" { 33 | sev, _ := checks.ParseSeverity(rs.Severity) 34 | return sev 35 | } 36 | return fallback 37 | } 38 | -------------------------------------------------------------------------------- /internal/config/range_query_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRangeQuerySettings(t *testing.T) { 12 | type testCaseT struct { 13 | err error 14 | conf RangeQuerySettings 15 | } 16 | 17 | testCases := []testCaseT{ 18 | { 19 | conf: RangeQuerySettings{ 20 | Max: "foo", 21 | }, 22 | err: errors.New(`not a valid duration string: "foo"`), 23 | }, 24 | { 25 | conf: RangeQuerySettings{ 26 | Max: "0h", 27 | }, 28 | err: errors.New("range_query max value cannot be zero"), 29 | }, 30 | { 31 | conf: RangeQuerySettings{ 32 | Max: "1d", 33 | Severity: "bag", 34 | }, 35 | err: errors.New("unknown severity: bag"), 36 | }, 37 | { 38 | conf: RangeQuerySettings{ 39 | Max: "1d", 40 | Severity: "warning", 41 | }, 42 | }, 43 | { 44 | conf: RangeQuerySettings{}, 45 | }, 46 | } 47 | 48 | for _, tc := range testCases { 49 | t.Run(fmt.Sprintf("%v", tc.conf), func(t *testing.T) { 50 | err := tc.conf.validate() 51 | if err == nil || tc.err == nil { 52 | require.Equal(t, err, tc.err) 53 | } else { 54 | require.EqualError(t, err, tc.err.Error()) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/reporter/json.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | ) 8 | 9 | func NewJSONReporter(output io.Writer) JSONReporter { 10 | return JSONReporter{output: output} 11 | } 12 | 13 | type JSONReporter struct { 14 | output io.Writer 15 | } 16 | 17 | type JSONReport struct { 18 | Path string `json:"path"` 19 | Owner string `json:"owner,omitempty"` 20 | Reporter string `json:"reporter"` 21 | Problem string `json:"problem"` 22 | Details string `json:"details,omitempty"` 23 | Severity string `json:"severity"` 24 | Lines []int `json:"lines"` 25 | } 26 | 27 | func (jr JSONReporter) Submit(_ context.Context, summary Summary) (err error) { 28 | reports := summary.Reports() 29 | out := make([]JSONReport, 0, len(reports)) 30 | 31 | for _, report := range reports { 32 | out = append(out, JSONReport{ 33 | Path: report.Path.Name, 34 | Owner: report.Owner, 35 | Reporter: report.Problem.Reporter, 36 | Problem: report.Problem.Summary, 37 | Details: report.Problem.Details, 38 | Severity: report.Problem.Severity.String(), 39 | Lines: report.Problem.Lines.Expand(), 40 | }) 41 | } 42 | 43 | enc := json.NewEncoder(jr.output) 44 | enc.SetIndent("", " ") 45 | return enc.Encode(out) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/pint/tests/0132_tls_certs.txt: -------------------------------------------------------------------------------- 1 | cert $WORK prom 2 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 3 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 4 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 5 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 6 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 7 | http start prometheus 127.0.0.1:7132 $WORK/prom.pem $WORK/prom.key 8 | 9 | exec pint -l debug --no-color lint rules 10 | ! stdout . 11 | ! stderr 'tls: failed to verify certificate: x509: certificate signed by unknown authority' 12 | 13 | -- rules/1.yml -- 14 | groups: 15 | - name: foo 16 | rules: 17 | - record: aggregate 18 | expr: sum(foo) without(job) 19 | 20 | -- .pint.hcl -- 21 | prometheus "prom" { 22 | uri = "https://127.0.0.1:7132" 23 | failover = [] 24 | timeout = "5s" 25 | required = true 26 | tls { 27 | caCert = "prom-ca.pem" 28 | clientCert = "prom.pem" 29 | clientKey = "prom.key" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/config/__snapshots__/027.snap: -------------------------------------------------------------------------------- 1 | 2 | [TestGetChecksForRule/two_checks_enabled_via_config - 1] 3 | title: two checks enabled via config 4 | config: |- 5 | { 6 | "ci": { 7 | "baseBranch": "master", 8 | "maxCommits": 20 9 | }, 10 | "parser": {}, 11 | "repository": {}, 12 | "checks": { 13 | "enabled": [ 14 | "promql/syntax", 15 | "alerts/count" 16 | ] 17 | }, 18 | "owners": {}, 19 | "prometheus": [ 20 | { 21 | "name": "prom1", 22 | "uri": "http://localhost", 23 | "timeout": "1s", 24 | "uptime": "up", 25 | "include": [ 26 | "rules.yml" 27 | ], 28 | "concurrency": 16, 29 | "rateLimit": 100, 30 | "required": false 31 | } 32 | ], 33 | "rules": [ 34 | { 35 | "alerts": { 36 | "range": "1h", 37 | "step": "1m", 38 | "resolve": "5m" 39 | } 40 | } 41 | ] 42 | } 43 | entry: 44 | path: 45 | name: rules.yml 46 | symlinktarget: rules.yml 47 | filecomments: [] 48 | rulecomments: [] 49 | checks: 50 | - promql/syntax 51 | - alerts/count(prom1) 52 | 53 | --- 54 | -------------------------------------------------------------------------------- /internal/config/rule_link.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/cloudflare/pint/internal/checks" 5 | ) 6 | 7 | type RuleLinkSettings struct { 8 | Regex string `hcl:",label" json:"key,omitempty"` 9 | URI string `hcl:"uri,optional" json:"uri,omitempty"` 10 | Timeout string `hcl:"timeout,optional" json:"timeout,omitempty"` 11 | Headers map[string]string `hcl:"headers,optional" json:"headers,omitempty"` 12 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 13 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 14 | } 15 | 16 | func (s RuleLinkSettings) validate() error { 17 | if _, err := checks.NewTemplatedRegexp(s.Regex); err != nil { 18 | return err 19 | } 20 | 21 | if s.Timeout != "" { 22 | if _, err := parseDuration(s.Timeout); err != nil { 23 | return err 24 | } 25 | } 26 | 27 | if s.Severity != "" { 28 | if _, err := checks.ParseSeverity(s.Severity); err != nil { 29 | return err 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (s RuleLinkSettings) getSeverity(fallback checks.Severity) checks.Severity { 37 | if s.Severity != "" { 38 | sev, _ := checks.ParseSeverity(s.Severity) 39 | return sev 40 | } 41 | return fallback 42 | } 43 | -------------------------------------------------------------------------------- /internal/config/check.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/cloudflare/pint/internal/checks" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/hashicorp/hcl/v2/gohcl" 11 | ) 12 | 13 | type Check struct { 14 | Body hcl.Body `hcl:",remain" json:"-"` 15 | Name string `hcl:",label" json:"name"` 16 | } 17 | 18 | func (c Check) MarshalJSON() ([]byte, error) { 19 | s, err := c.Decode() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return json.MarshalIndent(s, "", " ") 24 | } 25 | 26 | func (c Check) Decode() (s CheckSettings, err error) { 27 | switch c.Name { 28 | case checks.SeriesCheckName: 29 | s = &checks.PromqlSeriesSettings{} 30 | case checks.RegexpCheckName: 31 | s = &checks.PromqlRegexpSettings{} 32 | default: 33 | return nil, fmt.Errorf("unknown check %q", c.Name) 34 | } 35 | 36 | if diag := gohcl.DecodeBody(c.Body, nil, s); diag != nil && diag.HasErrors() { 37 | return nil, diag 38 | } 39 | if err = s.Validate(); err != nil { 40 | return nil, err 41 | } 42 | return s, nil 43 | } 44 | 45 | func (c Check) validate() error { 46 | s, err := c.Decode() 47 | if err != nil { 48 | return err 49 | } 50 | return s.Validate() 51 | } 52 | 53 | type CheckSettings interface { 54 | Validate() error 55 | } 56 | -------------------------------------------------------------------------------- /cmd/pint/tests/0065_ci_include.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/.pint.hcl . 6 | env GIT_AUTHOR_NAME=pint 7 | env GIT_AUTHOR_EMAIL=pint@example.com 8 | env GIT_COMMITTER_NAME=pint 9 | env GIT_COMMITTER_EMAIL=pint@example.com 10 | exec git add . 11 | exec git commit -am 'import rules and config' 12 | 13 | exec git checkout -b v1 14 | cp ../src/a.yml a.yml 15 | exec git add a.yml 16 | exec git commit -am 'v1' 17 | 18 | exec git checkout -b v2 19 | cp ../src/b.yml b.yml 20 | exec git add b.yml 21 | exec git commit -am 'v2' 22 | 23 | exec pint -l debug --no-color ci 24 | ! stdout . 25 | stderr 'level=DEBUG msg="Got branch information" base=main current=v2' 26 | stderr 'level=DEBUG msg="Git file change" change=A path=a.yml commit=.*' 27 | stderr 'level=DEBUG msg="Git file change" change=A path=b.yml commit=.*' 28 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=a.yml' 29 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=b.yml' 30 | 31 | -- src/a.yml -- 32 | - record: rule1 33 | expr: sum(foo) bi() 34 | -- src/b.yml -- 35 | - record: rule1 36 | expr: sum(foo) bi() 37 | -- src/.pint.hcl -- 38 | ci { 39 | baseBranch = "main" 40 | } 41 | parser { 42 | include = ["xxx"] 43 | relaxed = [".*"] 44 | } 45 | -------------------------------------------------------------------------------- /cmd/pint/tests/0139_ci_exclude.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/.pint.hcl . 6 | env GIT_AUTHOR_NAME=pint 7 | env GIT_AUTHOR_EMAIL=pint@example.com 8 | env GIT_COMMITTER_NAME=pint 9 | env GIT_COMMITTER_EMAIL=pint@example.com 10 | exec git add . 11 | exec git commit -am 'import rules and config' 12 | 13 | exec git checkout -b v1 14 | cp ../src/a.yml a.yml 15 | exec git add a.yml 16 | exec git commit -am 'v1' 17 | 18 | exec git checkout -b v2 19 | cp ../src/b.yml b.yml 20 | exec git add b.yml 21 | exec git commit -am 'v2' 22 | 23 | exec pint -l debug --no-color ci 24 | ! stdout . 25 | stderr 'level=DEBUG msg="Got branch information" base=main current=v2' 26 | stderr 'level=DEBUG msg="Git file change" change=A path=a.yml commit=.*' 27 | stderr 'level=DEBUG msg="Git file change" change=A path=b.yml commit=.*' 28 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=a.yml' 29 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=b.yml' 30 | 31 | -- src/a.yml -- 32 | - record: rule1 33 | expr: sum(foo) bi() 34 | -- src/b.yml -- 35 | - record: rule1 36 | expr: sum(foo) bi() 37 | -- src/.pint.hcl -- 38 | ci { 39 | baseBranch = "main" 40 | } 41 | parser { 42 | exclude = [".*.yml"] 43 | relaxed = [".*"] 44 | } 45 | -------------------------------------------------------------------------------- /cmd/pint/tests/0154_ci_discovery_error.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=master . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --no-color -l debug ci 19 | ! stdout . 20 | stderr 'level=ERROR msg="Execution completed with error\(s\)" err="filepath discovery error: lstat notfound: no such file or directory"' 21 | 22 | -- src/v1.yml -- 23 | - alert: rule1 24 | expr: sum(foo) by(job) > 0 25 | 26 | -- src/v2.yml -- 27 | - alert: rule1 28 | expr: sum(foo) by(job) > 0 29 | - alert: rule2 30 | expr: >- 31 | sum(foo) 32 | by(job) > 0 33 | 34 | -- rules/0001.yml -- 35 | groups: 36 | - name: foo 37 | rules: 38 | - record: sum:up 39 | expr: sum(up) 40 | 41 | -- .pint.hcl -- 42 | discovery { 43 | filepath { 44 | directory = "notfound" 45 | match = "(?P\\w+).ya?ml" 46 | template { 47 | name = "{{ $name }}" 48 | uri = "https://{{ $name }}.example.com" 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /cmd/pint/tests/0216_github_invalid_pr_number.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | exec touch rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | 16 | env GITHUB_AUTH_TOKEN=1 17 | env GITHUB_PULL_REQUEST_NUMBER=abc 18 | ! exec pint --no-color ci 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=INFO msg="Finding all rules to check on current git branch" base=main 25 | level=INFO msg="Checking Prometheus rules" entries=0 workers=10 online=true 26 | level=INFO msg="No rules found, skipping Prometheus discovery" 27 | level=ERROR msg="Execution completed with error(s)" err="got not a valid number via GITHUB_PULL_REQUEST_NUMBER: strconv.Atoi: parsing \"abc\": invalid syntax" 28 | -- src/.pint.hcl -- 29 | ci { 30 | baseBranch = "main" 31 | } 32 | repository { 33 | github { 34 | baseuri = "http://127.0.0.1:6216" 35 | uploaduri = "http://127.0.0.1:6216" 36 | owner = "cloudflare" 37 | repo = "pint" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/examples/labels.hcl: -------------------------------------------------------------------------------- 1 | # This example shows how to enforce labels where the label value itself can 2 | # be a string with one or more sub-strings in it. 3 | # For example when you have a 'components' label that can be any of these: 4 | # components: 'db' 5 | # components: 'api' 6 | # components: 'proxy' 7 | # components: 'db api' 8 | # components: 'db api proxy' 9 | # components: 'proxy api db' 10 | # components: 'proxy db' 11 | 12 | rule { 13 | # Only run these checks on alerting rules. 14 | # Ignore recording rules. 15 | match { 16 | kind = "alerting" 17 | } 18 | label "components" { 19 | # Every alerting rule must have this label set. 20 | required = true 21 | # If any alerting rule fails our check pint will report his as a 'Bug' 22 | # severity problem, which will fail (exit with non-zero exit code) 23 | # when running 'pint lint' or 'pint ci'. 24 | # Set it to 'warning' if you don't want to fail pint runs. 25 | severity = "bug" 26 | # Split label value into sub-strings using the 'token' regexp. 27 | # \w is an alias for [0-9A-Za-z_] match. 28 | # Notice that we must escape '\' in HCL config files. 29 | token = "\\w+" 30 | # This is the list of allowed values. 31 | values = [ 32 | "db", 33 | "api", 34 | "proxy", 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/config/reject_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRejectSettings(t *testing.T) { 12 | type testCaseT struct { 13 | err error 14 | conf RejectSettings 15 | } 16 | 17 | testCases := []testCaseT{ 18 | { 19 | conf: RejectSettings{ 20 | Regex: "foo", 21 | Severity: "bug", 22 | }, 23 | }, 24 | { 25 | conf: RejectSettings{}, 26 | }, 27 | { 28 | conf: RejectSettings{ 29 | Regex: "foo.++", 30 | }, 31 | err: errors.New("error parsing regexp: invalid nested repetition operator: `++`"), 32 | }, 33 | { 34 | conf: RejectSettings{ 35 | Regex: "{{nil}}", 36 | }, 37 | err: errors.New(`template: regexp:1:125: executing "regexp" at : nil is not a command`), 38 | }, 39 | { 40 | conf: RejectSettings{ 41 | Regex: "foo", 42 | Severity: "bugx", 43 | }, 44 | err: errors.New("unknown severity: bugx"), 45 | }, 46 | } 47 | 48 | for _, tc := range testCases { 49 | t.Run(fmt.Sprintf("%v", tc.conf), func(t *testing.T) { 50 | err := tc.conf.validate() 51 | if err == nil || tc.err == nil { 52 | require.Equal(t, err, tc.err) 53 | } else { 54 | require.EqualError(t, err, tc.err.Error()) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pint/tests/0001_match_path.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | Bug: required label is being removed via aggregation (promql/aggregate) 10 | ---> rules/0002.yml:2 -> `colo:test2` 11 | 2 | expr: sum(foo) without(job) 12 | ^^^ Query is using aggregation with `without(job)`, all labels included inside `without(...)` will be removed from the results. 13 | `job` label is required and should be preserved when aggregating all rules. 14 | 15 | level=INFO msg="Problems found" Bug=1 16 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 17 | -- rules/0001.yml -- 18 | - record: "colo:test1" 19 | expr: sum(foo) without(job) 20 | -- rules/0002.yml -- 21 | - record: "colo:test2" 22 | expr: sum(foo) without(job) 23 | -- .pint.hcl -- 24 | parser { 25 | relaxed = [".*"] 26 | } 27 | rule { 28 | match { 29 | path = "rules/0002.yml" 30 | } 31 | aggregate ".+" { 32 | severity = "bug" 33 | keep = [ "job" ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/checks/rule_report_test.snap: -------------------------------------------------------------------------------- 1 | 2 | [TestReportCheck/report_passed_problem_/_info - 1] 3 | - description: report passed problem / info 4 | content: | 5 | - record: foo 6 | expr: sum(foo) 7 | output: | 8 | 1 | - record: foo 9 | ^^^ problem reported 10 | problem: 11 | reporter: rule/report 12 | summary: problem reported by config rule 13 | details: "" 14 | diagnostics: 15 | - message: problem reported 16 | firstcolumn: 1 17 | lastcolumn: 3 18 | kind: 0 19 | lines: 20 | first: 1 21 | last: 2 22 | severity: 0 23 | anchor: 0 24 | 25 | --- 26 | 27 | [TestReportCheck/report_passed_problem_/_warning - 1] 28 | - description: report passed problem / warning 29 | content: | 30 | - alert: foo 31 | expr: sum(foo) 32 | annotations: 33 | alert: foo 34 | output: | 35 | 1 | - alert: foo 36 | ^^^ problem reported 37 | problem: 38 | reporter: rule/report 39 | summary: problem reported by config rule 40 | details: "" 41 | diagnostics: 42 | - message: problem reported 43 | firstcolumn: 1 44 | lastcolumn: 3 45 | kind: 0 46 | lines: 47 | first: 1 48 | last: 4 49 | severity: 1 50 | anchor: 0 51 | 52 | --- 53 | -------------------------------------------------------------------------------- /internal/output/humanize.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func HumanizeDuration(d time.Duration) string { 11 | weeks := int64(d.Hours() / (7 * 24)) 12 | days := int64(math.Mod(d.Hours(), 7*24) / 24) 13 | hours := int64(math.Mod(d.Hours(), 24)) 14 | minutes := int64(math.Mod(d.Minutes(), 60)) 15 | seconds := int64(math.Mod(d.Seconds(), 60)) 16 | ms := int64(math.Mod(float64(d.Milliseconds()), 1000)) 17 | 18 | chunks := []struct { 19 | singularName string 20 | amount int64 21 | }{ 22 | {"w", weeks}, 23 | {"d", days}, 24 | {"h", hours}, 25 | {"m", minutes}, 26 | {"s", seconds}, 27 | {"ms", ms}, 28 | } 29 | 30 | parts := []string{} 31 | 32 | for _, chunk := range chunks { 33 | if chunk.amount > 0 { 34 | parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.singularName)) 35 | } 36 | } 37 | 38 | if len(parts) == 0 { 39 | return "0" 40 | } 41 | 42 | return strings.Join(parts, "") 43 | } 44 | 45 | func HumanizeBytes(b int) string { 46 | const unit = 1024 47 | if b < unit { 48 | return fmt.Sprintf("%dB", b) 49 | } 50 | div, exp := int64(unit), 0 51 | for n := b / unit; n >= unit; n /= unit { 52 | div *= unit 53 | exp++ 54 | } 55 | return fmt.Sprintf("%.1f%ciB", 56 | float64(b)/float64(div), "KMGTPE"[exp]) 57 | } 58 | -------------------------------------------------------------------------------- /docs/checks/promql/syntax.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | parent: Checks 4 | grand_parent: Documentation 5 | --- 6 | 7 | # promql/syntax 8 | 9 | This is the most basic check that will report any syntax errors in a PromQL 10 | query on any rule. 11 | 12 | ## Configuration 13 | 14 | This check doesn't have any configuration options. 15 | 16 | ## How to enable it 17 | 18 | This check is enabled by default. 19 | 20 | ## How to disable it 21 | 22 | You can disable this check globally by adding this config block: 23 | 24 | ```js 25 | checks { 26 | disabled = ["promql/syntax"] 27 | } 28 | ``` 29 | 30 | You can also disable it for all rules inside given file by adding 31 | a comment anywhere in that file. Example: 32 | 33 | ```yaml 34 | # pint file/disable promql/syntax 35 | ``` 36 | 37 | Or you can disable it per rule by adding a comment to it. Example: 38 | 39 | ```yaml 40 | # pint disable promql/syntax 41 | ``` 42 | 43 | ## How to snooze it 44 | 45 | You can disable this check until given time by adding a comment to it. Example: 46 | 47 | ```yaml 48 | # pint snooze $TIMESTAMP promql/syntax 49 | ``` 50 | 51 | Where `$TIMESTAMP` is either use [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) 52 | formatted or `YYYY-MM-DD`. 53 | Adding this comment will disable `promql/syntax` *until* `$TIMESTAMP`, after that 54 | check will be re-enabled. 55 | -------------------------------------------------------------------------------- /cmd/pint/tests/0104_file_ignore_prom.txt: -------------------------------------------------------------------------------- 1 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 2 | http response prometheus / 502 Bad Gateway 3 | http start prometheus 127.0.0.1:7104 4 | 5 | ! exec pint --no-color lint rules 6 | ! stdout . 7 | stderr 'level=ERROR msg="Query returned an error" err="502 Bad Gateway" uri=http://127.0.0.1:7104 query=/api/v1/status/config' 8 | stderr 'level=ERROR msg="Query returned an error" err="502 Bad Gateway" uri=http://127.0.0.1:7104 query=/api/v1/status/flags' 9 | stderr 'level=ERROR msg="Query returned an error" err="502 Bad Gateway" uri=http://127.0.0.1:7104 query=count\(\\nfoo\\n\)' 10 | stderr 'level=INFO msg="Problems found" Bug=3' 11 | -- rules/0001.yml -- 12 | # This should skip all online checks 13 | # pint file/disable promql/series 14 | # pint file/disable promql/rate 15 | # 16 | # pint file/disable alerts/count 17 | # pint file/disable promql/range_query 18 | # 19 | 20 | - record: "colo:test1" 21 | expr: sum(foo) without(job) 22 | 23 | -- rules/0002.yml -- 24 | - record: "colo:test2" 25 | expr: sum(foo) without(job) 26 | 27 | # pint file/disable rule/duplicate 28 | 29 | -- .pint.hcl -- 30 | prometheus "prom" { 31 | uri = "http://127.0.0.1:7104" 32 | failover = [] 33 | timeout = "5s" 34 | required = true 35 | } 36 | parser { 37 | relaxed = [".*"] 38 | } 39 | -------------------------------------------------------------------------------- /cmd/pint/tests/0140_ci_include_exclude.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/.pint.hcl . 6 | env GIT_AUTHOR_NAME=pint 7 | env GIT_AUTHOR_EMAIL=pint@example.com 8 | env GIT_COMMITTER_NAME=pint 9 | env GIT_COMMITTER_EMAIL=pint@example.com 10 | exec git add . 11 | exec git commit -am 'import rules and config' 12 | 13 | exec git checkout -b v1 14 | cp ../src/a.yml a.yml 15 | exec git add a.yml 16 | exec git commit -am 'v1' 17 | 18 | exec git checkout -b v2 19 | cp ../src/b.yml b.yml 20 | exec git add b.yml 21 | exec git commit -am 'v2' 22 | 23 | exec pint -l debug --no-color ci 24 | ! stdout . 25 | stderr 'level=DEBUG msg="Got branch information" base=main current=v2' 26 | stderr 'level=DEBUG msg="Git file change" change=A path=a.yml commit=.*' 27 | stderr 'level=DEBUG msg="Git file change" change=A path=b.yml commit=.*' 28 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=a.yml' 29 | stderr 'level=DEBUG msg="Skipping file due to include/exclude rules" path=b.yml' 30 | 31 | -- src/a.yml -- 32 | - record: rule1 33 | expr: sum(foo) bi() 34 | -- src/b.yml -- 35 | - record: rule1 36 | expr: sum(foo) bi() 37 | -- src/.pint.hcl -- 38 | ci { 39 | baseBranch = "main" 40 | } 41 | parser { 42 | include = [".*.yml"] 43 | exclude = [".*.yml"] 44 | relaxed = [".*"] 45 | } 46 | -------------------------------------------------------------------------------- /internal/config/checks_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestChecksSettings(t *testing.T) { 13 | type testCaseT struct { 14 | err error 15 | conf Checks 16 | } 17 | 18 | testCases := []testCaseT{ 19 | { 20 | conf: Checks{}, 21 | }, 22 | { 23 | conf: Checks{ 24 | Enabled: []string{"foo"}, 25 | }, 26 | err: errors.New("unknown check name foo"), 27 | }, 28 | { 29 | conf: Checks{ 30 | Disabled: []string{"foo"}, 31 | }, 32 | err: errors.New("unknown check name foo"), 33 | }, 34 | { 35 | conf: Checks{ 36 | Enabled: []string{"promql/syntax"}, 37 | Disabled: []string{"promql/syntax"}, 38 | }, 39 | }, 40 | } 41 | 42 | for _, tc := range testCases { 43 | t.Run(fmt.Sprintf("%v", tc.conf), func(t *testing.T) { 44 | err := tc.conf.validate() 45 | if err == nil || tc.err == nil { 46 | require.Equal(t, err, tc.err) 47 | } else { 48 | require.EqualError(t, err, tc.err.Error()) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestCheckMarshalJSONError(t *testing.T) { 55 | c := Check{Name: "invalid"} 56 | _, err := json.Marshal(c) 57 | require.EqualError(t, err, `json: error calling MarshalJSON for type config.Check: unknown check "invalid"`) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pint/tests/0178_parser_include.txt: -------------------------------------------------------------------------------- 1 | exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=DEBUG msg="File path is in the include list" path=rules/0001.yml include=["^rules/0001.yml$"] 10 | level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 11 | level=DEBUG msg="File path is not allowed" path=rules/0002.yml include=["^rules/0001.yml$"] exclude=["^.pint.hcl$"] 12 | level=DEBUG msg="Glob finder completed" count=1 13 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 14 | level=DEBUG msg="Generated all Prometheus servers" count=0 15 | level=DEBUG msg="Found recording rule" path=rules/0001.yml record=ok lines=1-2 state=noop 16 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/0001.yml rule=ok 17 | -- rules/0001.yml -- 18 | - record: ok 19 | expr: sum(foo) without(job) 20 | -- rules/0002.yml -- 21 | - record: failing 22 | expr: sum() 23 | -- .pint.hcl -- 24 | parser { 25 | relaxed = [".*"] 26 | include = ["rules/0001.yml"] 27 | } 28 | -------------------------------------------------------------------------------- /cmd/pint/tests/0220_lint_min_severity_fatal_ok.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --min-severity=fatal rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=3 workers=10 online=true 9 | level=WARN msg="You have --min-severity set to a higher severity value than --fail-on, pint might exit with a non-zero code but you won't see the problem that caused it" min-severity=Fatal fail-on=Bug 10 | level=INFO msg="Problems found" Bug=1 Warning=1 Information=1 11 | level=INFO msg="3 problem(s) not visible because of --min-severity=fatal flag" 12 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 13 | -- rules/0001.yml -- 14 | groups: 15 | - name: foo 16 | rules: 17 | - alert: info 18 | expr: info == 0 19 | - alert: warning 20 | expr: warning == 0 21 | - alert: bug 22 | expr: bug == 0 23 | 24 | -- .pint.hcl -- 25 | rule { 26 | selector "info" { 27 | requiredLabels = ["xxx"] 28 | severity = "info" 29 | } 30 | selector "warning" { 31 | requiredLabels = ["xxx"] 32 | severity = "warning" 33 | } 34 | selector "bug" { 35 | requiredLabels = ["xxx"] 36 | severity = "bug" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | golangci-lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | with: 20 | show-progress: false 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 24 | with: 25 | go-version-file: go.ver 26 | cache-dependency-path: tools/golangci-lint/go.sum 27 | 28 | - name: Lint code 29 | run: make lint 30 | 31 | format: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Check out code 35 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 36 | with: 37 | show-progress: false 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 41 | with: 42 | go-version-file: go.ver 43 | cache-dependency-path: tools/golangci-lint/go.sum 44 | 45 | - name: Format code 46 | run: make format 47 | 48 | - name: Check for local changes 49 | run: git diff --exit-code 50 | -------------------------------------------------------------------------------- /cmd/pint/tests/0120_ci_fail_on_invalid.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | env GITHUB_AUTH_TOKEN=12345 19 | env GITHUB_PULL_REQUEST_NUMBER=1 20 | ! exec pint -l debug --offline --no-color ci --fail-on=bogus 21 | ! stdout . 22 | stderr 'level=ERROR msg="Execution completed with error\(s\)" err="invalid --fail-on value: unknown severity: bogus"' 23 | 24 | -- src/v1.yml -- 25 | - alert: rule1 26 | expr: sum(foo) by(job) 27 | - alert: rule2 28 | expr: sum(foo) by(job) 29 | for: 0s 30 | 31 | -- src/v2.yml -- 32 | - alert: rule1 33 | expr: sum(foo) by(job) 34 | for: 0s 35 | - alert: rule2 36 | expr: sum(foo) by(job) 37 | for: 0s 38 | 39 | -- src/.pint.hcl -- 40 | ci { 41 | baseBranch = "main" 42 | } 43 | parser { 44 | relaxed = [".*"] 45 | } 46 | repository { 47 | github { 48 | baseuri = "http://127.0.0.1:6120" 49 | uploaduri = "http://127.0.0.1:6120" 50 | owner = "cloudflare" 51 | repo = "pint" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/pint/tests/0166_invalid_label.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | Fatal: This rule is not a valid Prometheus rule: `invalid annotation name: {{ $value }}`. (yaml/parse) 10 | ---> rules/1.yaml:7 11 | 7 | "{{ $value }}": "down" 12 | ^^^ This rule is not a valid Prometheus rule: `invalid annotation name: {{ $value }}`. 13 | 14 | Fatal: This rule is not a valid Prometheus rule: `invalid label name: {{ $value }}`. (yaml/parse) 15 | ---> rules/1.yaml:11 16 | 11 | "{{ $value }}": "down" 17 | ^^^ This rule is not a valid Prometheus rule: `invalid label name: {{ $value }}`. 18 | 19 | level=INFO msg="Problems found" Fatal=2 20 | level=ERROR msg="Execution completed with error(s)" err="found 2 problem(s) with severity Bug or higher" 21 | -- rules/1.yaml -- 22 | groups: 23 | - name: g1 24 | rules: 25 | - alert: Service Down 26 | expr: up == 0 27 | annotations: 28 | "{{ $value }}": "down" 29 | - alert: Service Down 30 | expr: up == 0 31 | labels: 32 | "{{ $value }}": "down" 33 | 34 | -- .pint.hcl -- 35 | parser { 36 | names = "legacy" 37 | } 38 | -------------------------------------------------------------------------------- /internal/config/__snapshots__/029.snap: -------------------------------------------------------------------------------- 1 | 2 | [TestGetChecksForRule/rule_with_ignore_block_/_match - 1] 3 | title: rule with ignore block / match 4 | config: |- 5 | { 6 | "ci": { 7 | "baseBranch": "master", 8 | "maxCommits": 20 9 | }, 10 | "parser": {}, 11 | "repository": {}, 12 | "checks": { 13 | "enabled": [ 14 | "promql/syntax", 15 | "alerts/count" 16 | ] 17 | }, 18 | "owners": {}, 19 | "prometheus": [ 20 | { 21 | "name": "prom1", 22 | "uri": "http://localhost", 23 | "timeout": "1s", 24 | "uptime": "up", 25 | "include": [ 26 | "rules.yml" 27 | ], 28 | "concurrency": 16, 29 | "rateLimit": 100, 30 | "required": false 31 | } 32 | ], 33 | "rules": [ 34 | { 35 | "ignore": [ 36 | { 37 | "path": "rules.yml" 38 | } 39 | ], 40 | "alerts": { 41 | "range": "1h", 42 | "step": "1m", 43 | "resolve": "5m" 44 | } 45 | } 46 | ] 47 | } 48 | entry: 49 | path: 50 | name: rules.yml 51 | symlinktarget: rules.yml 52 | filecomments: [] 53 | rulecomments: [] 54 | checks: 55 | - promql/syntax 56 | 57 | --- 58 | -------------------------------------------------------------------------------- /docs/checks/alerts/for.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | parent: Checks 4 | grand_parent: Documentation 5 | --- 6 | 7 | # alerts/for 8 | 9 | This check will warn if an alert rule uses invalid `for` or `keep_firing_for` 10 | value or if it passes default value that can be removed to simplify rule. 11 | 12 | ## Configuration 13 | 14 | This check doesn't have any configuration options. 15 | 16 | ## How to enable it 17 | 18 | This check is enabled by default. 19 | 20 | ## How to disable it 21 | 22 | You can disable this check globally by adding this config block: 23 | 24 | ```js 25 | checks { 26 | disabled = ["alerts/for"] 27 | } 28 | ``` 29 | 30 | You can also disable it for all rules inside given file by adding 31 | a comment anywhere in that file. Example: 32 | 33 | ```yaml 34 | # pint file/disable alerts/for 35 | ``` 36 | 37 | Or you can disable it per rule by adding a comment to it. Example: 38 | 39 | ```yaml 40 | # pint disable alerts/for 41 | ``` 42 | 43 | ## How to snooze it 44 | 45 | You can disable this check until given time by adding a comment to it. Example: 46 | 47 | ```yaml 48 | # pint snooze $TIMESTAMP alerts/for 49 | ``` 50 | 51 | Where `$TIMESTAMP` is either use [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) 52 | formatted or `YYYY-MM-DD`. 53 | Adding this comment will disable `alerts/for` *until* `$TIMESTAMP`, after that 54 | check will be re-enabled. 55 | -------------------------------------------------------------------------------- /docs/examples/discovery.hcl: -------------------------------------------------------------------------------- 1 | # Example with Prometheus server discovery. 2 | 3 | discovery { 4 | 5 | # filepath discovery will generate Prometheus servers from files on disk. 6 | # We define a regexp and any file or directory path matching that regexp will 7 | # generate a new Prometheus server. 8 | filepath { 9 | # Directory to scan for files. 10 | directory = "/etc/prometheus/servers" 11 | 12 | # Regexp rule to match, with capture groups to store variables. 13 | match = "(?P\\w+).yaml" 14 | 15 | # Use variables from the regex to generate a new Prometheus configuration block. 16 | template { 17 | name = "prometheus-{{ $name }}" # We can use 'name' regexp capture group as $name. 18 | uri = "https://prometheus-{{ $name }}.example.com" 19 | failover = [ "https://prometheus-{{ $name }}-backup.example.com" ] 20 | headers = { 21 | "X-Auth": "secret", 22 | "X-User": "bob" 23 | "X-Cluster": "{{ $name }}" 24 | } 25 | timeout = "30s" 26 | } 27 | 28 | template { 29 | name = "prometheus-clone-{{ $name }}" 30 | uri = "https://{{ $name }}.example.com" 31 | failover = [ "https://{{ $name }}-backup.example.com" ] 32 | headers = { 33 | "X-Auth": "secret", 34 | "X-User": "bob", 35 | } 36 | timeout = "30s" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/pint/tests/0029_ci_too_many_commits.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | 16 | cp ../src/v2.yml rules.yml 17 | exec git commit -am 'v2' 18 | 19 | cp ../src/v1.yml rules.yml 20 | exec git commit -am 'recert to v1' 21 | 22 | cp ../src/v2.yml rules.yml 23 | exec git commit -am 'v2' 24 | 25 | ! exec pint --no-color ci 26 | ! stdout . 27 | cmp stderr ../stderr.txt 28 | 29 | -- stderr.txt -- 30 | level=INFO msg="Loading configuration file" path=.pint.hcl 31 | level=INFO msg="Finding all rules to check on current git branch" base=main 32 | level=ERROR msg="Execution completed with error(s)" err="number of commits to check (3) is higher than maxCommits (2), exiting" 33 | -- src/v1.yml -- 34 | - record: rule1 35 | expr: sum(foo) by(job) 36 | - record: rule2 37 | expr: sum(foo) bi(job) 38 | 39 | -- src/v2.yml -- 40 | - record: rule1 41 | expr: sum(bar) by(job) 42 | - record: rule2 43 | expr: sum(bar) by(job) 44 | 45 | -- src/.pint.hcl -- 46 | ci { 47 | maxCommits = 2 48 | baseBranch = "main" 49 | } 50 | parser { 51 | relaxed = [".*"] 52 | } 53 | -------------------------------------------------------------------------------- /cmd/pint/tests/0111_snooze.txt: -------------------------------------------------------------------------------- 1 | exec pint -l debug --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 8 | level=INFO msg="Finding all rules to check" paths=["rules"] 9 | level=DEBUG msg="File parsed" path=rules/0001.yml rules=1 10 | level=DEBUG msg="Glob finder completed" count=1 11 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 12 | level=DEBUG msg="Generated all Prometheus servers" count=0 13 | level=DEBUG msg="Found recording rule" path=rules/0001.yml record=sum:job lines=2-3 state=noop 14 | level=DEBUG msg="Check snoozed by comment" check=promql/aggregate(job:true) match=promql/aggregate until="2099-11-28T10:24:18Z" 15 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/0001.yml rule=sum:job 16 | -- rules/0001.yml -- 17 | # pint snooze 2099-11-28T10:24:18Z promql/aggregate 18 | - record: sum:job 19 | expr: sum(foo) 20 | 21 | -- .pint.hcl -- 22 | parser { 23 | relaxed = [".*"] 24 | } 25 | rule { 26 | match { 27 | kind = "recording" 28 | } 29 | aggregate ".+" { 30 | keep = [ "job" ] 31 | severity = "bug" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/config/__snapshots__/028.snap: -------------------------------------------------------------------------------- 1 | 2 | [TestGetChecksForRule/rule_with_ignore_block_/_mismatch - 1] 3 | title: rule with ignore block / mismatch 4 | config: |- 5 | { 6 | "ci": { 7 | "baseBranch": "master", 8 | "maxCommits": 20 9 | }, 10 | "parser": {}, 11 | "repository": {}, 12 | "checks": { 13 | "enabled": [ 14 | "promql/syntax", 15 | "alerts/count" 16 | ] 17 | }, 18 | "owners": {}, 19 | "prometheus": [ 20 | { 21 | "name": "prom1", 22 | "uri": "http://localhost", 23 | "timeout": "1s", 24 | "uptime": "up", 25 | "include": [ 26 | "rules.yml" 27 | ], 28 | "concurrency": 16, 29 | "rateLimit": 100, 30 | "required": false 31 | } 32 | ], 33 | "rules": [ 34 | { 35 | "ignore": [ 36 | { 37 | "path": "foo.xml" 38 | } 39 | ], 40 | "alerts": { 41 | "range": "1h", 42 | "step": "1m", 43 | "resolve": "5m" 44 | } 45 | } 46 | ] 47 | } 48 | entry: 49 | path: 50 | name: rules.yml 51 | symlinktarget: rules.yml 52 | filecomments: [] 53 | rulecomments: [] 54 | checks: 55 | - promql/syntax 56 | - alerts/count(prom1) 57 | 58 | --- 59 | -------------------------------------------------------------------------------- /cmd/pint/tests/0194_checkstyle_lint.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint --min-severity=info --checkstyle=checkstyle.xml rules 2 | cmp checkstyle.xml checkstyle_expected.xml 3 | 4 | -- rules/0001.yml -- 5 | groups: 6 | - name: test 7 | rules: 8 | - alert: Example 9 | expr: up 10 | - alert: Example 11 | expr: sum(xxx) with() 12 | -- checkstyle_expected.xml -- 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /cmd/pint/tests/0055_prometheus_failover.txt: -------------------------------------------------------------------------------- 1 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 2 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 3 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[]}} 4 | http start prometheus 127.0.0.1:7055 5 | 6 | ! exec pint --no-color lint rules 7 | ! stdout . 8 | stderr 'level=ERROR msg="Query returned an error" err="Post \\"http://127.0.0.1:1055/api/v1/query\\": dial tcp 127.0.0.1:1055: connect: connection refused" uri=http://127.0.0.1:1055 query=count\(\\nfoo\\n\)' 9 | stderr 'level=ERROR msg="Query returned an error" err="failed to query Prometheus config: Get \\"http://127.0.0.1:1055/api/v1/status/config\\": dial tcp 127.0.0.1:1055: connect: connection refused" uri=http://127.0.0.1:1055 query=/api/v1/status/config' 10 | ! stderr 'query="count\(\\nfoo offset ' 11 | stderr '`prom` Prometheus server at http://127.0.0.1:7055 didn''t have any series for `foo` metric in the last 1w.' 12 | 13 | -- rules/1.yml -- 14 | - record: aggregate 15 | expr: sum(foo) without(job) 16 | 17 | -- .pint.hcl -- 18 | prometheus "prom" { 19 | uri = "http://127.0.0.1:1055" 20 | failover = ["http://127.0.0.1:7055"] 21 | timeout = "5s" 22 | required = true 23 | } 24 | parser { 25 | relaxed = [".*"] 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pint/tests/0058_templated_check.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=3 workers=10 online=true 9 | Bug: required annotation not set (alerts/annotation) 10 | ---> rules/0001.yml:4-6 -> `Instance Is Down 2` 11 | 6 | for: 5m 12 | ^^ `alert_for` annotation is required. 13 | 14 | Bug: invalid annotation value (alerts/annotation) 15 | ---> rules/0001.yml:12 -> `Instance Is Down 3` 16 | 12 | alert_for: 4m 17 | ^^ `alert_for` annotation value must match `^{{ $for }}$`. 18 | 19 | level=INFO msg="Problems found" Bug=2 20 | level=ERROR msg="Execution completed with error(s)" err="found 2 problem(s) with severity Bug or higher" 21 | -- rules/0001.yml -- 22 | - alert: Instance Is Down 1 23 | expr: up == 0 24 | 25 | - alert: Instance Is Down 2 26 | expr: up == 0 27 | for: 5m 28 | 29 | - alert: Instance Is Down 3 30 | expr: up == 0 31 | for: 5m 32 | annotations: 33 | alert_for: 4m 34 | 35 | -- .pint.hcl -- 36 | parser { 37 | relaxed = [".*"] 38 | } 39 | rule { 40 | match { 41 | for = "> 0" 42 | } 43 | 44 | annotation "alert_for" { 45 | required = true 46 | value = "{{ $for }}" 47 | severity = "bug" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/pint/tests/0162_ci_deleted_dependency.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/alert.yml alert.yml 6 | exec ln -s alert.yml symlink.yml 7 | cp ../src/v1.yml rules.yml 8 | cp ../src/.pint.hcl . 9 | env GIT_AUTHOR_NAME=pint 10 | env GIT_AUTHOR_EMAIL=pint@example.com 11 | env GIT_COMMITTER_NAME=pint 12 | env GIT_COMMITTER_EMAIL=pint@example.com 13 | exec git add . 14 | exec git commit -am 'import rules and config' 15 | 16 | exec git checkout -b v2 17 | cp ../src/v2.yml rules.yml 18 | exec git commit -am 'v2' 19 | 20 | exec pint -l error --offline --no-color ci 21 | ! stdout . 22 | cmp stderr ../stderr.txt 23 | 24 | -- stderr.txt -- 25 | Warning: rule results used by another rule (rule/dependency) 26 | ---> rules.yml:4 (deleted) -> `up:sum` 27 | 28 | -- src/alert.yml -- 29 | groups: 30 | - name: g1 31 | rules: 32 | - alert: Alert 33 | expr: 'up:sum == 0' 34 | annotations: 35 | summary: 'Service is down' 36 | labels: 37 | cluster: dev 38 | -- src/v1.yml -- 39 | groups: 40 | - name: g1 41 | rules: 42 | - record: up:sum 43 | expr: sum(up) 44 | -- src/v2.yml -- 45 | groups: 46 | - name: g1 47 | rules: [] 48 | -- src/.pint.hcl -- 49 | ci { 50 | baseBranch = "main" 51 | } 52 | parser { 53 | include = [".+.yml"] 54 | } 55 | prometheus "prom" { 56 | uri = "http://127.0.0.1:7162" 57 | timeout = "5s" 58 | required = true 59 | } 60 | -------------------------------------------------------------------------------- /cmd/pint/tests/0201_json_ci_no_dir.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --offline --no-color ci --json=x/y/z/report.json 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=INFO msg="Finding all rules to check on current git branch" base=main 25 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=false 26 | level=INFO msg="Offline mode, skipping Prometheus discovery" 27 | level=ERROR msg="Execution completed with error(s)" err="open x/y/z/report.json: no such file or directory" 28 | -- src/v1.yml -- 29 | - alert: rule1 30 | expr: sum(foo) by(job) 31 | - alert: rule2 32 | expr: sum(foo) by(job) 33 | for: 0s 34 | 35 | -- src/v2.yml -- 36 | - alert: rule1 37 | expr: sum(foo) by(job) 38 | for: 0s 39 | - alert: rule2 40 | expr: sum(foo) by(job) 41 | for: 0s 42 | 43 | -- src/.pint.hcl -- 44 | ci { 45 | baseBranch = "main" 46 | } 47 | parser { 48 | relaxed = [".*"] 49 | } 50 | -------------------------------------------------------------------------------- /internal/reporter/summary_test.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewSummary(t *testing.T) { 12 | summary := NewSummary(nil) 13 | require.Empty(t, summary.Reports()) 14 | } 15 | 16 | func TestSummaryMarkCheckDisabled(t *testing.T) { 17 | summary := NewSummary(nil) 18 | summary.MarkCheckDisabled("prom", "config", []string{"check"}) 19 | details := summary.GetPrometheusDetails() 20 | require.Len(t, details, 1) 21 | require.Equal(t, "prom", details[0].Name) 22 | require.Len(t, details[0].DisabledChecks, 1) 23 | require.Equal(t, "config", details[0].DisabledChecks[0].API) 24 | require.Len(t, details[0].DisabledChecks[0].Checks, 1) 25 | require.Equal(t, "check", details[0].DisabledChecks[0].Checks[0]) 26 | } 27 | 28 | func TestSummaryReport(t *testing.T) { 29 | summary := NewSummary(nil) 30 | summary.Report(Report{}) 31 | require.Len(t, summary.Reports(), 1) 32 | } 33 | 34 | func TestSummarySortReports(t *testing.T) { 35 | summary := NewSummary([]Report{ 36 | {Problem: checks.Problem{Severity: checks.Bug}}, 37 | {Problem: checks.Problem{Severity: checks.Warning}}, 38 | }) 39 | summary.SortReports() 40 | reports := summary.Reports() 41 | require.Equal(t, checks.Warning, reports[0].Problem.Severity) 42 | require.Equal(t, checks.Bug, reports[1].Problem.Severity) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/pint/tests/0158_lint_teamcity.txt: -------------------------------------------------------------------------------- 1 | env NO_COLOR=1 2 | ! exec pint --no-color lint --min-severity=info --teamcity rules 3 | ! stdout . 4 | cmp stderr stderr.txt 5 | 6 | -- stderr.txt -- 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 9 | ##teamcity[testSuiteStarted name='alerts/comparison'] 10 | ##teamcity[testSuiteStarted name='Warning'] 11 | ##teamcity[testStarted name='rules/0001.yml:5'] 12 | ##teamcity[testStdErr name='rules/0001.yml:5' out='always firing alert'] 13 | ##teamcity[testFinished name='rules/0001.yml:5'] 14 | ##teamcity[testSuiteFinished name='Warning'] 15 | ##teamcity[testSuiteFinished name='alerts/comparison'] 16 | ##teamcity[testSuiteStarted name='promql/syntax'] 17 | ##teamcity[testSuiteStarted name='Fatal'] 18 | ##teamcity[testStarted name='rules/0001.yml:7'] 19 | ##teamcity[testFailed name='rules/0001.yml:7' message='' details='PromQL syntax error'] 20 | ##teamcity[testFinished name='rules/0001.yml:7'] 21 | ##teamcity[testSuiteFinished name='Fatal'] 22 | ##teamcity[testSuiteFinished name='promql/syntax'] 23 | level=INFO msg="Problems found" Fatal=1 Warning=1 24 | level=ERROR msg="Execution completed with error(s)" err="found 1 problem(s) with severity Bug or higher" 25 | -- rules/0001.yml -- 26 | groups: 27 | - name: test 28 | rules: 29 | - alert: Example 30 | expr: up 31 | - alert: Example 32 | expr: sum(xxx) with() 33 | -------------------------------------------------------------------------------- /cmd/pint/tests/0121_rule_for.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=4 workers=10 online=true 9 | Bug: duration required (rule/for) 10 | ---> rules/0001.yml:6 -> `3m` [+1 duplicates] 11 | 6 | for: 3m 12 | ^^ This alert rule must have a `for` field with a minimum duration of 5m. 13 | 14 | Bug: duration too long (rule/for) 15 | ---> rules/0001.yml:9 -> `13m` 16 | 9 | for: 13m 17 | ^^^ This alert rule must have a `for` field with a maximum duration of 10m. 18 | 19 | level=INFO msg="Some problems are duplicated between rules and all the duplicates were hidden, pass `--show-duplicates` to see them" total=3 duplicates=1 shown=2 20 | level=INFO msg="Problems found" Bug=3 21 | level=ERROR msg="Execution completed with error(s)" err="found 3 problem(s) with severity Bug or higher" 22 | -- rules/0001.yml -- 23 | - alert: ok 24 | expr: up == 0 25 | for: 5m 26 | - alert: 3m 27 | expr: up == 0 28 | for: 3m 29 | - alert: 13m 30 | expr: up == 0 31 | for: 13m 32 | - alert: none 33 | expr: up == 0 34 | 35 | -- .pint.hcl -- 36 | parser { 37 | relaxed = [".*"] 38 | } 39 | rule { 40 | for { 41 | severity = "bug" 42 | min = "5m" 43 | max = "10m" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | 7 | "github.com/cloudflare/pint/internal/log" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestParseLevel(t *testing.T) { 13 | type testCaseT struct { 14 | s string 15 | err string 16 | level slog.Level 17 | } 18 | 19 | testCases := []testCaseT{ 20 | {s: "xxx", level: slog.LevelInfo, err: `"xxx" is not a valid log level`}, 21 | {s: "err", level: slog.LevelInfo, err: `"err" is not a valid log level`}, 22 | {s: "DEB", level: slog.LevelInfo, err: `"DEB" is not a valid log level`}, 23 | {s: "error", level: slog.LevelError}, 24 | {s: "Error", level: slog.LevelError}, 25 | {s: "ERROR", level: slog.LevelError}, 26 | {s: "warn", level: slog.LevelWarn}, 27 | {s: "Warn", level: slog.LevelWarn}, 28 | {s: "WARN", level: slog.LevelWarn}, 29 | {s: "info", level: slog.LevelInfo}, 30 | {s: "Info", level: slog.LevelInfo}, 31 | {s: "INFO", level: slog.LevelInfo}, 32 | {s: "debug", level: slog.LevelDebug}, 33 | {s: "Debug", level: slog.LevelDebug}, 34 | {s: "DEBUG", level: slog.LevelDebug}, 35 | } 36 | 37 | for _, tc := range testCases { 38 | t.Run(tc.s, func(t *testing.T) { 39 | l, err := log.ParseLevel(tc.s) 40 | if tc.err != "" { 41 | require.EqualError(t, err, tc.err) 42 | } else { 43 | require.NoError(t, err) 44 | require.Equal(t, tc.level, l) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cmd/pint/tests/0124_ci_base_branch_flag.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --no-color ci --base-branch=main 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=INFO msg="Finding all rules to check on current git branch" base=main 25 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 26 | level=INFO msg="Problems found" Fatal=1 27 | Fatal: PromQL syntax error (promql/syntax) 28 | ---> rules.yml:2 -> `rule1` 29 | 2 | expr: sum(foo) bi(job) 30 | ^^ unexpected identifier "bi" 31 | 32 | level=ERROR msg="Execution completed with error(s)" err="problems found" 33 | -- src/v1.yml -- 34 | - record: rule1 35 | expr: sum(foo) by(job) 36 | - record: rule2 37 | expr: sum(foo) bi(job) 38 | 39 | -- src/v2.yml -- 40 | - record: rule1 41 | expr: sum(foo) bi(job) 42 | - record: rule2 43 | expr: sum(foo) bi(job) 44 | 45 | -- src/.pint.hcl -- 46 | parser { 47 | relaxed = [".*"] 48 | } 49 | -------------------------------------------------------------------------------- /cmd/pint/tests/0197_checkstyle_ci_no_dir.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --offline --no-color ci --checkstyle=x/y/z/checkstyle.xml 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- src/v1.yml -- 23 | - alert: rule1 24 | expr: sum(foo) by(job) 25 | - alert: rule2 26 | expr: sum(foo) by(job) 27 | for: 0s 28 | 29 | -- src/v2.yml -- 30 | - alert: rule1 31 | expr: sum(foo) by(job) 32 | for: 0s 33 | - alert: rule2 34 | expr: sum(foo) by(job) 35 | for: 0s 36 | 37 | -- src/.pint.hcl -- 38 | ci { 39 | baseBranch = "main" 40 | } 41 | parser { 42 | relaxed = [".*"] 43 | } 44 | 45 | -- stderr.txt -- 46 | level=INFO msg="Loading configuration file" path=.pint.hcl 47 | level=INFO msg="Finding all rules to check on current git branch" base=main 48 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=false 49 | level=INFO msg="Offline mode, skipping Prometheus discovery" 50 | level=ERROR msg="Execution completed with error(s)" err="open x/y/z/checkstyle.xml: no such file or directory" 51 | -------------------------------------------------------------------------------- /cmd/pint/tests/0172_rule_dependency_symlink_delete.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | mkdir rules1 rules2 6 | cp ../src/alert.yml rules1/alert.yml 7 | cp ../src/record.yml rules1/record.yml 8 | exec ln -s ../rules1/alert.yml rules2/alert.yml 9 | exec ln -s ../rules1/record.yml rules2/record.yml 10 | cp ../src/.pint.hcl . 11 | env GIT_AUTHOR_NAME=pint 12 | env GIT_AUTHOR_EMAIL=pint@example.com 13 | env GIT_COMMITTER_NAME=pint 14 | env GIT_COMMITTER_EMAIL=pint@example.com 15 | exec git add . 16 | exec git commit -am 'import rules and config' 17 | 18 | exec git checkout -b v2 19 | exec git rm -fr rules2 20 | exec git commit -am 'v2' 21 | 22 | exec pint -l error --offline --no-color ci 23 | ! stdout . 24 | cmp stderr ../stderr.txt 25 | 26 | -- stderr.txt -- 27 | -- src/alert.yml -- 28 | groups: 29 | - name: g1 30 | rules: 31 | - alert: Alert 32 | expr: 'up:sum == 0' 33 | annotations: 34 | summary: 'Service is down' 35 | -- src/record.yml -- 36 | groups: 37 | - name: g1 38 | rules: 39 | - record: up:sum 40 | expr: sum(up) 41 | -- src/.pint.hcl -- 42 | ci { 43 | baseBranch = "main" 44 | } 45 | prometheus "prom1" { 46 | uri = "http://127.0.0.1:7172/1" 47 | timeout = "5s" 48 | required = true 49 | include = ["rules1/.*"] 50 | } 51 | prometheus "prom2" { 52 | uri = "http://127.0.0.1:7172/2" 53 | timeout = "5s" 54 | required = true 55 | include = ["rules2/.*"] 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark Go code 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | benchmark: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 17 | with: 18 | show-progress: false 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 22 | with: 23 | go-version-file: go.ver 24 | cache: false 25 | 26 | - name: Fetch test rules 27 | run: make -C cmd/pint/bench fetch 28 | 29 | - name: Benchmark PR branch 30 | run: | 31 | make benchmark | tee new.txt 32 | 33 | - name: Benchmark main branch 34 | run: | 35 | git fetch origin main 36 | git reset --hard FETCH_HEAD 37 | make benchmark | tee old.txt 38 | 39 | - name: Diff benchmarks 40 | run: | 41 | git reset --hard ${GITHUB_SHA} 42 | make benchmark-diff 43 | 44 | - name: Report 45 | if: ${{ github.event.pull_request.head.repo.full_name == 'cloudflare/pint' }} 46 | uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 47 | with: 48 | file-path: benchstat.txt 49 | comment-tag: benchstat 50 | -------------------------------------------------------------------------------- /cmd/pint/tests/0027_ci_branch.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/v1.yml rules.yml 6 | cp ../src/.pint.hcl . 7 | env GIT_AUTHOR_NAME=pint 8 | env GIT_AUTHOR_EMAIL=pint@example.com 9 | env GIT_COMMITTER_NAME=pint 10 | env GIT_COMMITTER_EMAIL=pint@example.com 11 | exec git add . 12 | exec git commit -am 'import rules and config' 13 | 14 | exec git checkout -b v2 15 | cp ../src/v2.yml rules.yml 16 | exec git commit -am 'v2' 17 | 18 | ! exec pint --no-color ci 19 | ! stdout . 20 | cmp stderr ../stderr.txt 21 | 22 | -- stderr.txt -- 23 | level=INFO msg="Loading configuration file" path=.pint.hcl 24 | level=INFO msg="Finding all rules to check on current git branch" base=main 25 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 26 | level=INFO msg="Problems found" Fatal=1 27 | Fatal: PromQL syntax error (promql/syntax) 28 | ---> rules.yml:2 -> `rule1` 29 | 2 | expr: sum(foo) bi(job) 30 | ^^ unexpected identifier "bi" 31 | 32 | level=ERROR msg="Execution completed with error(s)" err="problems found" 33 | -- src/v1.yml -- 34 | - record: rule1 35 | expr: sum(foo) by(job) 36 | - record: rule2 37 | expr: sum(foo) bi(job) 38 | 39 | -- src/v2.yml -- 40 | - record: rule1 41 | expr: sum(foo) bi(job) 42 | - record: rule2 43 | expr: sum(foo) bi(job) 44 | 45 | -- src/.pint.hcl -- 46 | ci { 47 | baseBranch = "main" 48 | } 49 | parser { 50 | relaxed = [".*"] 51 | } 52 | -------------------------------------------------------------------------------- /internal/promapi/metrics.go: -------------------------------------------------------------------------------- 1 | package promapi 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | var ( 11 | prometheusQueriesRunning = prometheus.NewGaugeVec( 12 | prometheus.GaugeOpts{ 13 | Name: "pint_prometheus_queries_running", 14 | Help: "Total number of in-flight prometheus queries.", 15 | }, 16 | []string{"name", "endpoint"}, 17 | ) 18 | prometheusQueriesTotal = prometheus.NewCounterVec( 19 | prometheus.CounterOpts{ 20 | Name: "pint_prometheus_queries_total", 21 | Help: "Total number of all prometheus queries.", 22 | }, 23 | []string{"name", "endpoint"}, 24 | ) 25 | prometheusQueryErrorsTotal = prometheus.NewCounterVec( 26 | prometheus.CounterOpts{ 27 | Name: "pint_prometheus_query_errors_total", 28 | Help: "Total number of failed prometheus queries.", 29 | }, 30 | []string{"name", "endpoint", "reason"}, 31 | ) 32 | ) 33 | 34 | func RegisterMetrics(reg *prometheus.Registry) { 35 | reg.MustRegister(prometheusQueriesRunning) 36 | reg.MustRegister(prometheusQueriesTotal) 37 | reg.MustRegister(prometheusQueryErrorsTotal) 38 | } 39 | 40 | func errReason(err error) string { 41 | var neterr net.Error 42 | if ok := errors.As(err, &neterr); ok && neterr.Timeout() { 43 | return "connection/timeout" 44 | } 45 | 46 | var e1 APIError 47 | if ok := errors.As(err, &e1); ok { 48 | return "api/" + string(e1.ErrorType) 49 | } 50 | 51 | return "connection/error" 52 | } 53 | -------------------------------------------------------------------------------- /cmd/pint/tests/0105_too_many_samples.txt: -------------------------------------------------------------------------------- 1 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 2 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 30s\n"}} 3 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 4 | http response prometheus /api/v1/query_range 400 {"status":"error","errorType":"execution","error":"query processing would load too many samples into memory in query execution"} 5 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[]}} 6 | http start prometheus 127.0.0.1:7105 7 | 8 | ! exec pint --no-color lint rules 9 | ! stdout . 10 | stderr 'level=ERROR msg="Query returned an error" err="query processing would load too many samples into memory in query execution" uri=http://127.0.0.1:7105 query=count\(\\nup\\n\)' 11 | stderr 'level=WARN msg="Cannot detect Prometheus uptime gaps" err="execution: query processing would load too many samples into memory in query execution" name=prom' 12 | 13 | -- rules/1.yaml -- 14 | - record: one 15 | expr: up == 0 16 | - record: two 17 | expr: up == 0 18 | -- rules/2.yaml -- 19 | - record: one 20 | expr: up == 0 21 | - record: two 22 | expr: up == 0 23 | 24 | -- .pint.hcl -- 25 | prometheus "prom" { 26 | uri = "http://127.0.0.1:7105" 27 | required = false 28 | } 29 | parser { 30 | relaxed = [".*"] 31 | } 32 | 33 | rule{} 34 | -------------------------------------------------------------------------------- /cmd/pint/tests/0168_watch_rule_files.txt: -------------------------------------------------------------------------------- 1 | http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"rule_files:\n - rules/*\n"}} 2 | http response prometheus /api/v1/metadata 200 {"status":"success","data":{}} 3 | http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}} 4 | http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}} 5 | http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1666873962.795,"1"]}]}} 6 | http start prometheus 127.0.0.1:7168 7 | 8 | exec bash -x ./test.sh & 9 | 10 | exec pint --no-color -l debug watch --interval=5s --listen=127.0.0.1:6168 --pidfile=pint.pid rule_files prom 11 | ! stdout . 12 | 13 | stderr 'level=DEBUG msg="Glob finder completed" count=2' 14 | stderr 'level=DEBUG msg="Glob finder completed" count=3' 15 | 16 | -- test.sh -- 17 | sleep 7 18 | mv more/*.yaml rules/ 19 | sleep 7 20 | cat pint.pid | xargs kill 21 | 22 | -- rules/1.yaml -- 23 | groups: 24 | - name: g1 25 | rules: 26 | - alert: DownAlert1 27 | expr: up == 0 28 | -- rules/2.yaml -- 29 | groups: 30 | - name: g2 31 | rules: 32 | - alert: DownAlert2 33 | expr: up == 0 34 | -- more/3.yaml -- 35 | groups: 36 | - name: g2 37 | rules: 38 | - alert: DownAlert2 39 | expr: up == 0 40 | -- .pint.hcl -- 41 | prometheus "prom" { 42 | uri = "http://localhost:7168" 43 | } -------------------------------------------------------------------------------- /cmd/pint/tests/0049_watch_severity_warning.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | exec pint watch --listen=127.0.0.1:6049 --min-severity=warning --pidfile=pint.pid glob rules 4 | cmp curl.txt metrics.txt 5 | 6 | -- test.sh -- 7 | sleep 5 8 | curl -s http://127.0.0.1:6049/metrics | grep -E '^pint_problem' > curl.txt 9 | cat pint.pid | xargs kill 10 | 11 | -- rules/1.yml -- 12 | - record: broken 13 | expr: foo / count()) 14 | 15 | - record: aggregate 16 | expr: sum(foo) without(job) 17 | 18 | - alert: comparison 19 | expr: foo 20 | 21 | -- .pint.hcl -- 22 | parser { 23 | relaxed = [".*"] 24 | } 25 | rule { 26 | match { 27 | kind = "recording" 28 | } 29 | aggregate ".+" { 30 | keep = [ "job" ] 31 | } 32 | } 33 | 34 | -- metrics.txt -- 35 | pint_problem{filename="rules/1.yml",kind="alerting",name="comparison",owner="",problem="always firing alert: This query doesn't have any condition and so this alert will always fire if it matches anything.",reporter="alerts/comparison",severity="warning"} 1 36 | pint_problem{filename="rules/1.yml",kind="recording",name="aggregate",owner="",problem="required label is being removed via aggregation: `job` label is required and should be preserved when aggregating all rules.",reporter="promql/aggregate",severity="warning"} 1 37 | pint_problem{filename="rules/1.yml",kind="recording",name="broken",owner="",problem="PromQL syntax error: unexpected right parenthesis ')'",reporter="promql/syntax",severity="fatal"} 1 38 | pint_problems 3 39 | -------------------------------------------------------------------------------- /internal/config/annotation.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type AnnotationSettings struct { 10 | Key string `hcl:",label" json:"key"` 11 | Token string `hcl:"token,optional" json:"token,omitempty"` 12 | Value string `hcl:"value,optional" json:"value,omitempty"` 13 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 14 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 15 | Values []string `hcl:"values,optional" json:"values,omitempty"` 16 | Required bool `hcl:"required,optional" json:"required,omitempty"` 17 | } 18 | 19 | func (as AnnotationSettings) validate() error { 20 | if as.Key == "" { 21 | return errors.New("annotation key cannot be empty") 22 | } 23 | 24 | if _, err := checks.NewTemplatedRegexp(as.Key); err != nil { 25 | return err 26 | } 27 | 28 | if _, err := checks.NewRawTemplatedRegexp(as.Token); err != nil { 29 | return err 30 | } 31 | 32 | if _, err := checks.NewTemplatedRegexp(as.Value); err != nil { 33 | return err 34 | } 35 | 36 | if as.Severity != "" { 37 | if _, err := checks.ParseSeverity(as.Severity); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (as AnnotationSettings) getSeverity(fallback checks.Severity) checks.Severity { 46 | if as.Severity != "" { 47 | sev, _ := checks.ParseSeverity(as.Severity) 48 | return sev 49 | } 50 | return fallback 51 | } 52 | -------------------------------------------------------------------------------- /cmd/pint/tests/0048_watch_limit.txt: -------------------------------------------------------------------------------- 1 | exec bash -x ./test.sh & 2 | 3 | exec pint watch --listen=127.0.0.1:6048 --max-problems=2 --min-severity=info --pidfile=pint.pid glob rules 4 | cmp curl.txt metrics.txt 5 | 6 | -- test.sh -- 7 | sleep 5 8 | curl -s http://127.0.0.1:6048/metrics | grep -E '^pint_problem' > curl.txt 9 | cat pint.pid | xargs kill 10 | 11 | -- rules/1.yml -- 12 | groups: 13 | - name: foo 14 | rules: 15 | - record: broken 16 | expr: foo / count()) 17 | - record: aggregate 18 | expr: sum(foo) without(job) 19 | - alert: comparison1 20 | expr: foo 21 | - alert: comparison2 22 | expr: bar 23 | - record: bad:join:1 24 | expr: foo{job="a"} / foo{job="b"} 25 | - record: bad:join:2 26 | expr: foo{job="b"} / foo{job="a"} 27 | 28 | -- .pint.hcl -- 29 | rule { 30 | match { 31 | kind = "recording" 32 | } 33 | aggregate ".+" { 34 | keep = [ "job" ] 35 | } 36 | } 37 | 38 | -- metrics.txt -- 39 | pint_problem{filename="rules/1.yml",kind="alerting",name="comparison1",owner="",problem="always firing alert: This query doesn't have any condition and so this alert will always fire if it matches anything.",reporter="alerts/comparison",severity="warning"} 1 40 | pint_problem{filename="rules/1.yml",kind="recording",name="aggregate",owner="",problem="required label is being removed via aggregation: `job` label is required and should be preserved when aggregating all rules.",reporter="promql/aggregate",severity="warning"} 1 41 | pint_problems 3 42 | -------------------------------------------------------------------------------- /cmd/pint/tests/0060_ci_noop.txt: -------------------------------------------------------------------------------- 1 | mkdir testrepo 2 | cd testrepo 3 | exec git init --initial-branch=main . 4 | 5 | cp ../src/.pint.hcl . 6 | env GIT_AUTHOR_NAME=pint 7 | env GIT_AUTHOR_EMAIL=pint@example.com 8 | env GIT_COMMITTER_NAME=pint 9 | env GIT_COMMITTER_EMAIL=pint@example.com 10 | exec git add . 11 | exec git commit -am 'import rules and config' 12 | 13 | exec git checkout -b v1 14 | cp ../src/a.yml a.yml 15 | exec git add a.yml 16 | exec git commit -am 'v1' 17 | 18 | exec git checkout -b v2 19 | cp ../src/b.yml b.yml 20 | exec git add b.yml 21 | exec git commit -am 'v2' 22 | 23 | exec git checkout -b v3 24 | exec git rm a.yml 25 | exec git commit -am 'v3' 26 | 27 | ! exec pint --no-color ci 28 | ! stdout . 29 | cmp stderr ../stderr.txt 30 | 31 | -- stderr.txt -- 32 | level=INFO msg="Loading configuration file" path=.pint.hcl 33 | level=INFO msg="Finding all rules to check on current git branch" base=main 34 | level=INFO msg="Checking Prometheus rules" entries=1 workers=10 online=true 35 | level=INFO msg="Problems found" Fatal=1 36 | Fatal: PromQL syntax error (promql/syntax) 37 | ---> b.yml:2 -> `rule1` 38 | 2 | expr: sum(foo) bi() 39 | ^^ unexpected identifier "bi" 40 | 41 | level=ERROR msg="Execution completed with error(s)" err="problems found" 42 | -- src/a.yml -- 43 | - record: rule1 44 | expr: sum(foo) bi() 45 | -- src/b.yml -- 46 | - record: rule1 47 | expr: sum(foo) bi() 48 | -- src/.pint.hcl -- 49 | ci { 50 | baseBranch = "main" 51 | } 52 | parser { 53 | relaxed = [".*"] 54 | } 55 | -------------------------------------------------------------------------------- /cmd/pint/tests/0095_rulefmt_symlink.txt: -------------------------------------------------------------------------------- 1 | mkdir rules/strict 2 | exec ln -s ../relaxed/1.yml rules/strict/symlink.yml 3 | 4 | exec pint -l debug --no-color lint rules 5 | ! stdout . 6 | cmp stderr stderr.txt 7 | 8 | -- stderr.txt -- 9 | level=INFO msg="Loading configuration file" path=.pint.hcl 10 | level=DEBUG msg="Adding pint config to the parser exclude list" path=.pint.hcl 11 | level=INFO msg="Finding all rules to check" paths=["rules"] 12 | level=DEBUG msg="File parsed" path=rules/relaxed/1.yml rules=1 13 | level=DEBUG msg="File parsed" path=rules/strict/symlink.yml rules=1 14 | level=DEBUG msg="Glob finder completed" count=2 15 | level=INFO msg="Checking Prometheus rules" entries=2 workers=10 online=true 16 | level=DEBUG msg="Generated all Prometheus servers" count=0 17 | level=DEBUG msg="Found recording rule" path=rules/relaxed/1.yml record=foo lines=1-2 state=noop 18 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/relaxed/1.yml rule=foo 19 | level=DEBUG msg="Found recording rule" path=rules/strict/symlink.yml record=foo lines=1-2 state=noop 20 | level=DEBUG msg="Configured checks for rule" enabled=["promql/syntax","alerts/for","alerts/comparison","alerts/template","promql/fragile","promql/regexp","promql/impossible"] path=rules/strict/symlink.yml rule=foo 21 | -- rules/relaxed/1.yml -- 22 | - record: foo 23 | expr: up == 0 24 | -- .pint.hcl -- 25 | parser { 26 | relaxed = ["rules/relaxed/.*"] 27 | } 28 | -------------------------------------------------------------------------------- /internal/config/for.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/cloudflare/pint/internal/checks" 8 | ) 9 | 10 | type ForSettings struct { 11 | Min string `hcl:"min,optional" json:"min,omitempty"` 12 | Max string `hcl:"max,optional" json:"max,omitempty"` 13 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 14 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 15 | } 16 | 17 | func (fs ForSettings) validate() error { 18 | if fs.Severity != "" { 19 | if _, err := checks.ParseSeverity(fs.Severity); err != nil { 20 | return err 21 | } 22 | } 23 | if fs.Min != "" { 24 | if _, err := parseDuration(fs.Min); err != nil { 25 | return err 26 | } 27 | } 28 | if fs.Max != "" { 29 | if _, err := parseDuration(fs.Max); err != nil { 30 | return err 31 | } 32 | } 33 | if fs.Min == "" && fs.Max == "" { 34 | return errors.New("must set either min or max option, or both") 35 | } 36 | return nil 37 | } 38 | 39 | func (fs ForSettings) getSeverity(fallback checks.Severity) checks.Severity { 40 | if fs.Severity != "" { 41 | sev, _ := checks.ParseSeverity(fs.Severity) 42 | return sev 43 | } 44 | return fallback 45 | } 46 | 47 | func (fs ForSettings) resolve() (severity checks.Severity, minFor, maxFor time.Duration) { 48 | severity = fs.getSeverity(checks.Bug) 49 | if fs.Min != "" { 50 | minFor, _ = parseDuration(fs.Min) 51 | } 52 | if fs.Max != "" { 53 | maxFor, _ = parseDuration(fs.Max) 54 | } 55 | return severity, minFor, maxFor 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | concurrency: 18 | group: pages-${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | pages: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 27 | with: 28 | show-progress: false 29 | 30 | - name: Setup Pages 31 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 32 | 33 | - name: Build with Jekyll 34 | uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13 35 | with: 36 | source: ./docs 37 | destination: ./_site 38 | 39 | - name: Upload artifact 40 | if: github.event_name != 'pull_request' 41 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 42 | 43 | deploy: 44 | if: github.event_name != 'pull_request' 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | needs: pages 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 54 | -------------------------------------------------------------------------------- /cmd/pint/tests/0143_keep_firing_for.txt: -------------------------------------------------------------------------------- 1 | ! exec pint --no-color lint rules 2 | ! stdout . 3 | cmp stderr stderr.txt 4 | 5 | -- stderr.txt -- 6 | level=INFO msg="Loading configuration file" path=.pint.hcl 7 | level=INFO msg="Finding all rules to check" paths=["rules"] 8 | level=INFO msg="Checking Prometheus rules" entries=4 workers=10 online=true 9 | Bug: duration required (rule/for) 10 | ---> rules/0001.yml:6 -> `3m` [+1 duplicates] 11 | 6 | keep_firing_for: 3m 12 | ^^ This alert rule must have a `keep_firing_for` field with a minimum duration of 5m. 13 | 14 | Bug: duration too long (rule/for) 15 | ---> rules/0001.yml:9 -> `13m` 16 | 9 | keep_firing_for: 13m 17 | ^^^ This alert rule must have a `keep_firing_for` field with a maximum duration of 10m. 18 | 19 | level=INFO msg="Some problems are duplicated between rules and all the duplicates were hidden, pass `--show-duplicates` to see them" total=3 duplicates=1 shown=2 20 | level=INFO msg="Problems found" Bug=3 21 | level=ERROR msg="Execution completed with error(s)" err="found 3 problem(s) with severity Bug or higher" 22 | -- rules/0001.yml -- 23 | - alert: ok 24 | expr: up == 0 25 | keep_firing_for: 5m 26 | - alert: 3m 27 | expr: up == 0 28 | keep_firing_for: 3m 29 | - alert: 13m 30 | expr: up == 0 31 | keep_firing_for: 13m 32 | - alert: none 33 | expr: up == 0 34 | 35 | -- .pint.hcl -- 36 | parser { 37 | relaxed = [".*"] 38 | } 39 | rule { 40 | keep_firing_for { 41 | severity = "bug" 42 | min = "5m" 43 | max = "10m" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/config/cost_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCostSettings(t *testing.T) { 12 | type testCaseT struct { 13 | err error 14 | conf CostSettings 15 | } 16 | 17 | testCases := []testCaseT{ 18 | { 19 | conf: CostSettings{}, 20 | }, 21 | { 22 | conf: CostSettings{ 23 | MaxSeries: -1, 24 | Severity: "bug", 25 | }, 26 | err: errors.New("maxSeries value must be >= 0"), 27 | }, 28 | { 29 | conf: CostSettings{ 30 | Severity: "foo", 31 | }, 32 | err: errors.New("unknown severity: foo"), 33 | }, 34 | { 35 | conf: CostSettings{ 36 | MaxPeakSamples: -1, 37 | }, 38 | err: errors.New("maxPeakSamples value must be >= 0"), 39 | }, 40 | { 41 | conf: CostSettings{ 42 | MaxTotalSamples: -1, 43 | }, 44 | err: errors.New("maxTotalSamples value must be >= 0"), 45 | }, 46 | { 47 | conf: CostSettings{ 48 | MaxEvaluationDuration: "1abc", 49 | }, 50 | err: errors.New(`unknown unit "abc" in duration "1abc"`), 51 | }, 52 | { 53 | conf: CostSettings{ 54 | MaxEvaluationDuration: "5m", 55 | Severity: "warning", 56 | }, 57 | }, 58 | } 59 | 60 | for _, tc := range testCases { 61 | t.Run(fmt.Sprintf("%v", tc.conf), func(t *testing.T) { 62 | err := tc.conf.validate() 63 | if err == nil || tc.err == nil { 64 | require.Equal(t, err, tc.err) 65 | } else { 66 | require.EqualError(t, err, tc.err.Error()) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/config/alerts.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudflare/pint/internal/checks" 7 | ) 8 | 9 | type AlertsSettings struct { 10 | Range string `hcl:"range" json:"range"` 11 | Step string `hcl:"step" json:"step"` 12 | Resolve string `hcl:"resolve" json:"resolve"` 13 | Comment string `hcl:"comment,optional" json:"comment,omitempty"` 14 | Severity string `hcl:"severity,optional" json:"severity,omitempty"` 15 | MinCount int `hcl:"minCount,optional" json:"minCount,omitempty"` 16 | } 17 | 18 | func (as AlertsSettings) validate() error { 19 | if as.Range != "" { 20 | if _, err := parseDuration(as.Range); err != nil { 21 | return err 22 | } 23 | } 24 | if as.Step != "" { 25 | if _, err := parseDuration(as.Step); err != nil { 26 | return err 27 | } 28 | } 29 | if as.Resolve != "" { 30 | if _, err := parseDuration(as.Resolve); err != nil { 31 | return err 32 | } 33 | } 34 | if as.MinCount < 0 { 35 | return fmt.Errorf("minCount cannot be < 0, got %d", as.MinCount) 36 | } 37 | if as.Severity != "" { 38 | sev, err := checks.ParseSeverity(as.Severity) 39 | if err != nil { 40 | return err 41 | } 42 | if as.MinCount <= 0 && sev > checks.Information { 43 | return fmt.Errorf("cannot set serverity to %q when minCount is 0", as.Severity) 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (as AlertsSettings) getSeverity(fallback checks.Severity) checks.Severity { 50 | if as.Severity != "" { 51 | sev, _ := checks.ParseSeverity(as.Severity) 52 | return sev 53 | } 54 | return fallback 55 | } 56 | --------------------------------------------------------------------------------