6 |
--------------------------------------------------------------------------------
/docs/data/menu/extra.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | header:
3 | - name: GitHub
4 | ref: https://github.com/o11ydev/oy-toolkit
5 | icon: gdoc_github
6 | external: true
7 | - name: Support
8 | ref: https://o11y.eu/prometheus-support
9 | icon: gdoc_fire
10 | external: true
11 |
--------------------------------------------------------------------------------
/scripts/errcheck_excludes.txt:
--------------------------------------------------------------------------------
1 | // Used in HTTP handlers, any error is handled by the server itself.
2 | (net/http.ResponseWriter).Write
3 |
4 | // Never check for logger errors.
5 | (github.com/go-kit/log.Logger).Log
6 |
7 | // No need to check for errors on server's shutdown.
8 | (*net/http.Server).Shutdown
9 |
--------------------------------------------------------------------------------
/cmd/oy-csv-to-targets/testdata/expected_targets.json:
--------------------------------------------------------------------------------
1 | [{"labels":{"availability_zone":"az1","datacenter":"dc1"},"targets":["prometheus1:9090"]},{"labels":{"availability_zone":"az2","datacenter":"dc1"},"targets":["prometheus2:9090"]},{"labels":{"availability_zone":"az1","datacenter":"dc2"},"targets":["prometheus3:9090"]}]
2 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | (import (let
2 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
3 | in
4 | fetchTarball {
5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
6 | sha256 = lock.nodes.flake-compat.locked.narHash;
7 | }) {src = ./.;})
8 | .defaultNix
9 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | (import (let
2 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
3 | in
4 | fetchTarball {
5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
6 | sha256 = lock.nodes.flake-compat.locked.narHash;
7 | }) {src = ./.;})
8 | .shellNix
9 |
--------------------------------------------------------------------------------
/docs/config.toml:
--------------------------------------------------------------------------------
1 | baseURL = '/'
2 | languageCode = 'en-us'
3 | title = 'O11y toolkit'
4 |
5 | [params]
6 |
7 | geekdocMenuBundle = true
8 | geekdocRepo = "https://github.com/o11ydev/oy-toolkit"
9 |
10 | [params.geekdocContentLicense]
11 | name = "CC BY-SA 4.0"
12 | link = "https://creativecommons.org/licenses/by-sa/4.0/"
13 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: "Test"
2 | on:
3 | pull_request:
4 | jobs:
5 | deploy:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2.4.0
9 | - uses: cachix/install-nix-action@v15
10 | with:
11 | install_url: https://releases.nixos.org/nix/nix-2.13.3/install
12 | - uses: cachix/cachix-action@v12
13 | with:
14 | name: oy-toolkit
15 | - run: nix show-derivation
16 | - run: make lint build packages publish-script documentation
17 |
--------------------------------------------------------------------------------
/docs/content/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | geekdocDescription: Welcome
3 | title: "O11y toolkit"
4 |
5 | ---
6 |
7 | The **O11y toolkit** is a set of utilities that help you debug, augment,
8 | and manage your open source observability stack.
9 |
10 | The toolkit is licensed under the Apache 2 license. You can use it for free.
11 |
12 | ### Available tools
13 |
14 | {{< tools >}}
15 |
16 | ### Sponsor
17 |
18 | This toolkit is sponsored by [O11y](https://o11y.eu). O11y provides
19 | professional [support](https://o11y.eu/support/) and [services](https://o11y.eu/services/)
20 | for your Open Source observability stack.
21 |
--------------------------------------------------------------------------------
/.golangci.goheader.license:
--------------------------------------------------------------------------------
1 | Copyright The o11y toolkit Authors
2 | spdx-license-identifier: apache-2.0
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at:
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | deadline: 5m
3 |
4 | output:
5 | sort-results: true
6 |
7 | linters:
8 | enable:
9 | - gofumpt
10 | - goimports
11 | - revive
12 | - depguard
13 | - goheader
14 |
15 | linters-settings:
16 | depguard:
17 | rules:
18 | main:
19 | deny:
20 | - pkg: sync/atomic
21 | desc: "Use go.uber.org/atomic instead of sync/atomic"
22 | - pkg: github.com/go-kit/kit/log
23 | desc: "Use github.com/go-kit/log instead of github.com/go-kit/kit/log"
24 | goimports:
25 | local-prefixes: github.com/o11ydev/oy-toolkit
26 | gofumpt:
27 | extra-rules: true
28 | goheader:
29 | template-path: .golangci.goheader.license
30 | regexp:
31 | YEAR: 202[2-9]
32 | errcheck:
33 | exclude: scripts/errcheck_excludes.txt
34 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/histogram/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction
2 | # TYPE prometheus_tsdb_compaction_chunk_range histogram
3 | prometheus_tsdb_compaction_chunk_range_bucket{le="100"} 0
4 | prometheus_tsdb_compaction_chunk_range_bucket{le="400"} 0
5 | prometheus_tsdb_compaction_chunk_range_bucket{le="1600"} 0
6 | prometheus_tsdb_compaction_chunk_range_bucket{le="6400"} 0
7 | prometheus_tsdb_compaction_chunk_range_bucket{le="25600"} 7
8 | prometheus_tsdb_compaction_chunk_range_bucket{le="102400"} 7
9 | prometheus_tsdb_compaction_chunk_range_bucket{le="409600"} 1.412839e+06
10 | prometheus_tsdb_compaction_chunk_range_bucket{le="1.6384e+06"} 1.69185e+06
11 | prometheus_tsdb_compaction_chunk_range_bucket{le="6.5536e+06"} 1.691853e+06
12 | prometheus_tsdb_compaction_chunk_range_bucket{le="2.62144e+07"} 1.691853e+06
13 | prometheus_tsdb_compaction_chunk_range_bucket{le="+Inf"} 1.691853e+06
14 | prometheus_tsdb_compaction_chunk_range_sum 6.71393432189e+11
15 | prometheus_tsdb_compaction_chunk_range_count 1.691853e+06
16 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/two_metric_files.out:
--------------------------------------------------------------------------------
1 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
2 | # TYPE node_textfile_mtime_seconds gauge
3 | node_textfile_mtime_seconds{file="fixtures/textfile/two_metric_files/metrics1.prom"} 1
4 | node_textfile_mtime_seconds{file="fixtures/textfile/two_metric_files/metrics2.prom"} 1
5 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
6 | # TYPE node_textfile_scrape_error gauge
7 | node_textfile_scrape_error 0
8 | # HELP testmetric1_1 Metric read from fixtures/textfile/two_metric_files/metrics1.prom
9 | # TYPE testmetric1_1 untyped
10 | testmetric1_1{foo="bar"} 10
11 | # HELP testmetric1_2 Metric read from fixtures/textfile/two_metric_files/metrics1.prom
12 | # TYPE testmetric1_2 untyped
13 | testmetric1_2{foo="baz"} 20
14 | # HELP testmetric2_1 Metric read from fixtures/textfile/two_metric_files/metrics2.prom
15 | # TYPE testmetric2_1 untyped
16 | testmetric2_1{foo="bar"} 30
17 | # HELP testmetric2_2 Metric read from fixtures/textfile/two_metric_files/metrics2.prom
18 | # TYPE testmetric2_2 untyped
19 | testmetric2_2{foo="baz"} 40
20 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/summary_extra_dimension/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP prometheus_rule_evaluation_duration_seconds The duration for a rule to execute.
2 | # TYPE prometheus_rule_evaluation_duration_seconds summary
3 | prometheus_rule_evaluation_duration_seconds{rule_type="alerting",quantile="0.5", handler="foo"} 0.000571464
4 | prometheus_rule_evaluation_duration_seconds{rule_type="alerting",quantile="0.9"} 0.001765451
5 | prometheus_rule_evaluation_duration_seconds{rule_type="alerting",quantile="0.99"} 0.018672076
6 | prometheus_rule_evaluation_duration_seconds_sum{rule_type="alerting"} 214.85081044700146
7 | prometheus_rule_evaluation_duration_seconds_count{rule_type="alerting"} 185209
8 | prometheus_rule_evaluation_duration_seconds{rule_type="recording",quantile="0.5"} 4.3132e-05
9 | prometheus_rule_evaluation_duration_seconds{rule_type="recording",quantile="0.9"} 8.9295e-05
10 | prometheus_rule_evaluation_duration_seconds{rule_type="recording",quantile="0.99"} 0.000193657
11 | prometheus_rule_evaluation_duration_seconds_sum{rule_type="recording"} 185091.01317759082
12 | prometheus_rule_evaluation_duration_seconds_count{rule_type="recording"} 1.0020195e+08
13 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "O11ytools";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | flake-utils.url = "github:numtide/flake-utils";
7 | flake-compat = {
8 | url = "github:edolstra/flake-compat";
9 | flake = false;
10 | };
11 | };
12 |
13 | outputs = {
14 | self,
15 | nixpkgs,
16 | flake-utils,
17 | ...
18 | } @ inputs:
19 | flake-utils.lib.eachDefaultSystem (system: let
20 | pkgs = import nixpkgs {
21 | inherit system;
22 | config = import ./go.nix;
23 | };
24 | in rec {
25 | packages = import ./packages.nix {inherit pkgs;};
26 | defaultPackage = packages.oy-toolkit;
27 | devShell = pkgs.mkShell rec {
28 | buildInputs = [
29 | pkgs.go
30 | pkgs.gofumpt
31 | pkgs.gotestsum
32 | pkgs.gotools
33 | pkgs.golangci-lint
34 | pkgs.git
35 | pkgs.nix
36 | pkgs.skopeo
37 | pkgs.hugo
38 | pkgs.alejandra
39 | pkgs.nfpm
40 | ];
41 | # This variable is needed in our Makefile.
42 | O11Y_NIX_SHELL_ENABLED = "1";
43 | };
44 | });
45 | nixConfig.bash-prompt = "\[nix-develop\]$ ";
46 | }
47 |
--------------------------------------------------------------------------------
/cmd/oy-scrape-jitter/README.md:
--------------------------------------------------------------------------------
1 | # oy-scrape-jitter
2 |
3 | *oy-scrape-jitter* queries a Prometheus server to see how regular scrapes are.
4 | Perfect scrape alignment happens when the distance between all the scrapes are
5 | exactly the same. It enables Prometheus [delta-of-delta](https://github.com/prometheus/prometheus/blob/main/tsdb/docs/bstream.md)
6 | encoding to reduce significantly the size of the blocks.
7 |
8 | ## Example usage
9 |
10 | ```shell
11 | $ ./oy-scrape-jitter --prometheus.url=https://prometheus.demo.do.prometheus.io/ --log.results-only
12 | ts=2022-04-26T12:11:34.659Z caller=main.go:117 level=info msg="overall results" aligned_targets=0 unaligned_targets=10 max_ms=25
13 | ```
14 |
15 | This means that the maximum deviation seen in your scrape jobs is 25ms. You
16 | could set `--scrape.timestamp-tolerance=25ms` to reduce your disk usage over
17 | time, by enabling Prometheus to correct timestamps up to 25ms.
18 |
19 |
20 | ## Prometheus limits
21 |
22 | Prometheus will only apply timestamp tolerance up to 1%. If your scrape interval
23 | is 30s, you can only adjust timestamps up to 300ms. Setting a 500ms tolerance
24 | will have no effects on jobs with a scrape interval lower than 500s, even if the
25 | deviation is tiny.
26 |
27 | ## Plotting the output
28 |
29 | By using `--plot.file=scrape.png`, you can generate a PNG file which shows the
30 | scrape timestamps jitter with an histogram.
31 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/histogram.out:
--------------------------------------------------------------------------------
1 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
2 | # TYPE node_textfile_mtime_seconds gauge
3 | node_textfile_mtime_seconds{file="fixtures/textfile/histogram/metrics.prom"} 1
4 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
5 | # TYPE node_textfile_scrape_error gauge
6 | node_textfile_scrape_error 0
7 | # HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction
8 | # TYPE prometheus_tsdb_compaction_chunk_range histogram
9 | prometheus_tsdb_compaction_chunk_range_bucket{le="100"} 0
10 | prometheus_tsdb_compaction_chunk_range_bucket{le="400"} 0
11 | prometheus_tsdb_compaction_chunk_range_bucket{le="1600"} 0
12 | prometheus_tsdb_compaction_chunk_range_bucket{le="6400"} 0
13 | prometheus_tsdb_compaction_chunk_range_bucket{le="25600"} 7
14 | prometheus_tsdb_compaction_chunk_range_bucket{le="102400"} 7
15 | prometheus_tsdb_compaction_chunk_range_bucket{le="409600"} 1.412839e+06
16 | prometheus_tsdb_compaction_chunk_range_bucket{le="1.6384e+06"} 1.69185e+06
17 | prometheus_tsdb_compaction_chunk_range_bucket{le="6.5536e+06"} 1.691853e+06
18 | prometheus_tsdb_compaction_chunk_range_bucket{le="2.62144e+07"} 1.691853e+06
19 | prometheus_tsdb_compaction_chunk_range_bucket{le="+Inf"} 1.691853e+06
20 | prometheus_tsdb_compaction_chunk_range_sum 6.71393432189e+11
21 | prometheus_tsdb_compaction_chunk_range_count 1.691853e+06
22 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/inconsistent_metrics/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP http_requests_total Total number of HTTP requests made.
2 | # TYPE http_requests_total counter
3 | http_requests_total{code="200",handler="alerts",method="get"} 35
4 | http_requests_total{code="200",handler="config",method="get"} 8
5 | http_requests_total{code="200",method="get", foo="bar"} 325
6 | http_requests_total{code="200",handler="flags",method="get"} 18
7 | http_requests_total{code="200",handler="graph",method="get"} 89
8 | http_requests_total{code="200",method="get", baz="bar"} 93
9 | http_requests_total{code="200",handler="prometheus",method="get"} 17051
10 | http_requests_total{code="200",handler="query",method="get"} 401
11 | http_requests_total{code="200",handler="query_range",method="get"} 15663
12 | http_requests_total{code="200",handler="rules",method="get"} 7
13 | http_requests_total{code="200",handler="series",method="get"} 221
14 | http_requests_total{code="200",handler="static",method="get"} 1647
15 | http_requests_total{code="200",handler="status",method="get"} 12
16 | http_requests_total{code="200",method="get"} 11
17 | http_requests_total{code="206",handler="static",method="get"} 2
18 | http_requests_total{code="400",handler="query_range",method="get"} 40
19 | http_requests_total{code="503",handler="query_range",method="get"} 3
20 |
21 | # HELP go_goroutines Number of goroutines that currently exist.
22 | # TYPE go_goroutines gauge
23 | go_goroutines{foo="bar"} 229
24 | go_goroutines 20
25 |
--------------------------------------------------------------------------------
/cmd/oy-expose/README.md:
--------------------------------------------------------------------------------
1 | # oy-expose
2 |
3 | *oy-expose* reads a metrics file and exposes its content to be scraped by a
4 | Prometheus server.
5 |
6 | This is similar to the [Node Exporter Textfile Collector](https://github.com/prometheus/node_exporter#textfile-collector),
7 | with a few differences:
8 | - oy-expose only exposes on file.
9 | - oy-expose does not embed other collectors.
10 |
11 | ## Example usage
12 |
13 | Let's create a file called "metrics" with the following content:
14 |
15 | ```
16 | maintenance_script_run_timestamp_seconds 1647524557
17 | maintenance_script_return_code 0
18 | ```
19 |
20 | We can run `oy-expose`:
21 |
22 | ```
23 | $ oy-expose --web.disable-exporter-metrics
24 | ```
25 |
26 | And query the metrics:
27 |
28 | ```
29 | $ curl localhost:9099/metrics
30 | # HELP maintenance_script_return_code Metric read from metrics
31 | # TYPE maintenance_script_return_code untyped
32 | maintenance_script_return_code 0
33 | # HELP maintenance_script_run_timestamp_seconds Metric read from metrics
34 | # TYPE maintenance_script_run_timestamp_seconds untyped
35 | maintenance_script_run_timestamp_seconds 1.647524557e+09
36 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
37 | # TYPE node_textfile_mtime_seconds gauge
38 | node_textfile_mtime_seconds{file="metrics"} 1.647530635e+09
39 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
40 | # TYPE node_textfile_scrape_error gauge
41 | node_textfile_scrape_error 0
42 | ```
43 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/summary/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP event_duration_seconds_total Query timings
2 | # TYPE event_duration_seconds_total summary
3 | event_duration_seconds_total{baz="inner_eval",quantile="0.5"} 1.073e-06
4 | event_duration_seconds_total{baz="inner_eval",quantile="0.9"} 1.928e-06
5 | event_duration_seconds_total{baz="inner_eval",quantile="0.99"} 4.35e-06
6 | event_duration_seconds_total_sum{baz="inner_eval"} 1.8652166505091474e+06
7 | event_duration_seconds_total_count{baz="inner_eval"} 1.492355615e+09
8 | event_duration_seconds_total{baz="prepare_time",quantile="0.5"} 4.283e-06
9 | event_duration_seconds_total{baz="prepare_time",quantile="0.9"} 7.796e-06
10 | event_duration_seconds_total{baz="prepare_time",quantile="0.99"} 2.2083e-05
11 | event_duration_seconds_total_sum{baz="prepare_time"} 840923.7919437207
12 | event_duration_seconds_total_count{baz="prepare_time"} 1.492355814e+09
13 | event_duration_seconds_total{baz="result_append",quantile="0.5"} 1.566e-06
14 | event_duration_seconds_total{baz="result_append",quantile="0.9"} 3.223e-06
15 | event_duration_seconds_total{baz="result_append",quantile="0.99"} 6.53e-06
16 | event_duration_seconds_total_sum{baz="result_append"} 4.404109951000078
17 | event_duration_seconds_total_count{baz="result_append"} 1.427647e+06
18 | event_duration_seconds_total{baz="result_sort",quantile="0.5"} 1.847e-06
19 | event_duration_seconds_total{baz="result_sort",quantile="0.9"} 2.975e-06
20 | event_duration_seconds_total{baz="result_sort",quantile="0.99"} 4.08e-06
21 | event_duration_seconds_total_sum{baz="result_sort"} 3.4123187829998307
22 | event_duration_seconds_total_count{baz="result_sort"} 1.427647e+06
23 |
--------------------------------------------------------------------------------
/cmd/oy-expose/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "context"
20 | "os"
21 |
22 | "github.com/go-kit/log/level"
23 | "github.com/prometheus/client_golang/prometheus"
24 |
25 | "github.com/o11ydev/oy-toolkit/util/cmd"
26 | "github.com/o11ydev/oy-toolkit/util/collectors"
27 | "github.com/o11ydev/oy-toolkit/util/http"
28 |
29 | kingpin "github.com/alecthomas/kingpin/v2"
30 | )
31 |
32 | func main() {
33 | textFile := kingpin.Arg("metrics-file", "File to read metrics from.").Default("metrics").String()
34 | logger := cmd.InitCmd("oy-expose")
35 |
36 | collector, err := collectors.NewTextFileCollector(logger, *textFile)
37 | if err != nil {
38 | level.Error(logger).Log("msg", "Error creating file collector", "err", err)
39 | os.Exit(1)
40 | }
41 |
42 | r := prometheus.NewRegistry()
43 | r.MustRegister(collector)
44 | err = http.Serve(context.Background(), logger, r)
45 | if err != nil {
46 | level.Error(logger).Log("err", err)
47 | os.Exit(1)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/different_metric_types/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP events_total this is a test metric
2 | # TYPE events_total counter
3 | events_total{foo="bar"} 10
4 | events_total{foo="baz"} 20
5 |
6 | # HELP event_duration_seconds_total Query timings
7 | # TYPE event_duration_seconds_total summary
8 | event_duration_seconds_total{baz="inner_eval",quantile="0.5"} 1.073e-06
9 | event_duration_seconds_total{baz="inner_eval",quantile="0.9"} 1.928e-06
10 | event_duration_seconds_total{baz="inner_eval",quantile="0.99"} 4.35e-06
11 | event_duration_seconds_total_sum{baz="inner_eval"} 1.8652166505091474e+06
12 | event_duration_seconds_total_count{baz="inner_eval"} 1.492355615e+09
13 | event_duration_seconds_total{baz="prepare_time",quantile="0.5"} 4.283e-06
14 | event_duration_seconds_total{baz="prepare_time",quantile="0.9"} 7.796e-06
15 | event_duration_seconds_total{baz="prepare_time",quantile="0.99"} 2.2083e-05
16 | event_duration_seconds_total_sum{baz="prepare_time"} 840923.7919437207
17 | event_duration_seconds_total_count{baz="prepare_time"} 1.492355814e+09
18 | event_duration_seconds_total{baz="result_append",quantile="0.5"} 1.566e-06
19 | event_duration_seconds_total{baz="result_append",quantile="0.9"} 3.223e-06
20 | event_duration_seconds_total{baz="result_append",quantile="0.99"} 6.53e-06
21 | event_duration_seconds_total_sum{baz="result_append"} 4.404109951000078
22 | event_duration_seconds_total_count{baz="result_append"} 1.427647e+06
23 | event_duration_seconds_total{baz="result_sort",quantile="0.5"} 1.847e-06
24 | event_duration_seconds_total{baz="result_sort",quantile="0.9"} 2.975e-06
25 | event_duration_seconds_total{baz="result_sort",quantile="0.99"} 4.08e-06
26 | event_duration_seconds_total_sum{baz="result_sort"} 3.4123187829998307
27 | event_duration_seconds_total_count{baz="result_sort"} 1.427647e+06
28 |
29 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/summary_extra_dimension.out:
--------------------------------------------------------------------------------
1 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
2 | # TYPE node_textfile_mtime_seconds gauge
3 | node_textfile_mtime_seconds{file="fixtures/textfile/summary_extra_dimension/metrics.prom"} 1
4 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
5 | # TYPE node_textfile_scrape_error gauge
6 | node_textfile_scrape_error 0
7 | # HELP prometheus_rule_evaluation_duration_seconds The duration for a rule to execute.
8 | # TYPE prometheus_rule_evaluation_duration_seconds summary
9 | prometheus_rule_evaluation_duration_seconds{handler="",rule_type="alerting",quantile="0.9"} 0.001765451
10 | prometheus_rule_evaluation_duration_seconds{handler="",rule_type="alerting",quantile="0.99"} 0.018672076
11 | prometheus_rule_evaluation_duration_seconds_sum{handler="",rule_type="alerting"} 214.85081044700146
12 | prometheus_rule_evaluation_duration_seconds_count{handler="",rule_type="alerting"} 185209
13 | prometheus_rule_evaluation_duration_seconds{handler="",rule_type="recording",quantile="0.5"} 4.3132e-05
14 | prometheus_rule_evaluation_duration_seconds{handler="",rule_type="recording",quantile="0.9"} 8.9295e-05
15 | prometheus_rule_evaluation_duration_seconds{handler="",rule_type="recording",quantile="0.99"} 0.000193657
16 | prometheus_rule_evaluation_duration_seconds_sum{handler="",rule_type="recording"} 185091.01317759082
17 | prometheus_rule_evaluation_duration_seconds_count{handler="",rule_type="recording"} 1.0020195e+08
18 | prometheus_rule_evaluation_duration_seconds{handler="foo",rule_type="alerting",quantile="0.5"} 0.000571464
19 | prometheus_rule_evaluation_duration_seconds_sum{handler="foo",rule_type="alerting"} 0
20 | prometheus_rule_evaluation_duration_seconds_count{handler="foo",rule_type="alerting"} 0
21 |
--------------------------------------------------------------------------------
/cmd/oy-csv-to-targets/README.md:
--------------------------------------------------------------------------------
1 | # oy-csv-to-targets
2 |
3 | *oy-csv-to-targets* takes a list of Prometheus targets as a CSV file as input
4 | and produces a JSON file.
5 |
6 | The resulting JSON file can then be used by the `file_sd` discovery mechanism to
7 | dynamically discover and scrape the Prometheus targets. This allows the targets
8 | to be easily managed and updated without having to manually update the
9 | Prometheus configuration file, if your team is not fluent with JSON.
10 |
11 | If no output file is specified, the output goes to stdout.
12 |
13 | ## Example usage
14 |
15 | With the following CSV as an input:
16 |
17 | ```
18 | ,datacenter,availability_zone
19 | prometheus1:9090,dc1,az1
20 | prometheus2:9090,dc1,az2
21 | prometheus3:9090,dc2,az1
22 | ```
23 |
24 | The following command:
25 |
26 | ```
27 | ./oy-csv-to-targets --input.file targets.csv --output.file targets.json
28 | ```
29 |
30 | Would produce the following `targets.json` file:
31 |
32 | ```json
33 | [
34 | {
35 | "labels": {
36 | "availability_zone": "az1",
37 | "datacenter": "dc1"
38 | },
39 | "targets": [
40 | "prometheus1:9090"
41 | ]
42 | },
43 | {
44 | "labels": {
45 | "availability_zone": "az2",
46 | "datacenter": "dc1"
47 | },
48 | "targets": [
49 | "prometheus2:9090"
50 | ]
51 | },
52 | {
53 | "labels": {
54 | "availability_zone": "az1",
55 | "datacenter": "dc2"
56 | },
57 | "targets": [
58 | "prometheus3:9090"
59 | ]
60 | }
61 | ]
62 | ```
63 |
64 | You can then configure your Prometheus as follows
65 |
66 | ```
67 | scrape_configs:
68 | - job_name: prometheus
69 | file_sd_configs:
70 | - files: [targets.json]
71 | ```
72 |
--------------------------------------------------------------------------------
/cmd/oy-scrape-jitter/main_test.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | _ "embed"
20 | "io"
21 | "net/http"
22 | "net/http/httptest"
23 | "os"
24 | "os/exec"
25 | "testing"
26 |
27 | "github.com/stretchr/testify/require"
28 | )
29 |
30 | //go:embed query_result.json
31 | var queryResult string
32 |
33 | var mainPath = os.Args[0]
34 |
35 | func TestMain(m *testing.M) {
36 | for i, arg := range os.Args {
37 | if arg == "-test.main" {
38 | os.Args = append(os.Args[:i], os.Args[i+1:]...)
39 | main()
40 | return
41 | }
42 | }
43 |
44 | exitCode := m.Run()
45 | os.Exit(exitCode)
46 | }
47 |
48 | func TestExpose(t *testing.T) {
49 | if testing.Short() {
50 | t.Skip("skipping test in short mode.")
51 | }
52 |
53 | testServer := httptest.NewServer(
54 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 | require.Equal(t, "/api/v1/query", r.RequestURI)
56 | _, err := io.WriteString(w, queryResult)
57 | require.NoError(t, err)
58 | }),
59 | )
60 | t.Cleanup(testServer.Close)
61 |
62 | run := exec.Command(mainPath, "-test.main", "--prometheus.url="+testServer.URL)
63 | out, err := run.Output()
64 | t.Log(string(out))
65 | require.NoError(t, err)
66 |
67 | require.Contains(t, string(out), "level=info aligned_targets=4 unaligned_targets=6 max_ms=27")
68 | }
69 |
--------------------------------------------------------------------------------
/util/client/client.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package client
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | "github.com/alecthomas/kingpin/v2"
23 | "github.com/prometheus/client_golang/api"
24 | "github.com/prometheus/common/config"
25 | "gopkg.in/yaml.v2"
26 | )
27 |
28 | type Config struct {
29 | url string
30 | file string
31 | }
32 |
33 | func InitCliFlags() *Config {
34 | var c Config
35 | kingpin.Flag("prometheus.url", "URL of the Prometheus server.").Default("http://127.0.0.1:9090").StringVar(&c.url)
36 | kingpin.Flag("client.config", "Path to a HTTP client configuration.").StringVar(&c.file)
37 | return &c
38 | }
39 |
40 | func NewClient(c *Config) (api.Client, error) {
41 | cfg := config.DefaultHTTPClientConfig
42 | if c.file != "" {
43 | dat, err := os.ReadFile(c.file)
44 | if err != nil {
45 | return nil, fmt.Errorf("error reading client config %s: %w", c.file, err)
46 | }
47 | err = yaml.UnmarshalStrict(dat, &cfg)
48 | if err != nil {
49 | return nil, fmt.Errorf("error unmarshaling client config %s: %w", c.file, err)
50 | }
51 | }
52 | rt, err := config.NewRoundTripperFromConfig(cfg, "oy-toolkit")
53 | if err != nil {
54 | return nil, fmt.Errorf("error creating roundtripper: %w", err)
55 | }
56 | return api.NewClient(api.Config{
57 | Address: c.url,
58 | RoundTripper: rt,
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/summary.out:
--------------------------------------------------------------------------------
1 | # HELP event_duration_seconds_total Query timings
2 | # TYPE event_duration_seconds_total summary
3 | event_duration_seconds_total{baz="inner_eval",quantile="0.5"} 1.073e-06
4 | event_duration_seconds_total{baz="inner_eval",quantile="0.9"} 1.928e-06
5 | event_duration_seconds_total{baz="inner_eval",quantile="0.99"} 4.35e-06
6 | event_duration_seconds_total_sum{baz="inner_eval"} 1.8652166505091474e+06
7 | event_duration_seconds_total_count{baz="inner_eval"} 1.492355615e+09
8 | event_duration_seconds_total{baz="prepare_time",quantile="0.5"} 4.283e-06
9 | event_duration_seconds_total{baz="prepare_time",quantile="0.9"} 7.796e-06
10 | event_duration_seconds_total{baz="prepare_time",quantile="0.99"} 2.2083e-05
11 | event_duration_seconds_total_sum{baz="prepare_time"} 840923.7919437207
12 | event_duration_seconds_total_count{baz="prepare_time"} 1.492355814e+09
13 | event_duration_seconds_total{baz="result_append",quantile="0.5"} 1.566e-06
14 | event_duration_seconds_total{baz="result_append",quantile="0.9"} 3.223e-06
15 | event_duration_seconds_total{baz="result_append",quantile="0.99"} 6.53e-06
16 | event_duration_seconds_total_sum{baz="result_append"} 4.404109951000078
17 | event_duration_seconds_total_count{baz="result_append"} 1.427647e+06
18 | event_duration_seconds_total{baz="result_sort",quantile="0.5"} 1.847e-06
19 | event_duration_seconds_total{baz="result_sort",quantile="0.9"} 2.975e-06
20 | event_duration_seconds_total{baz="result_sort",quantile="0.99"} 4.08e-06
21 | event_duration_seconds_total_sum{baz="result_sort"} 3.4123187829998307
22 | event_duration_seconds_total_count{baz="result_sort"} 1.427647e+06
23 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
24 | # TYPE node_textfile_mtime_seconds gauge
25 | node_textfile_mtime_seconds{file="fixtures/textfile/summary/metrics.prom"} 1
26 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
27 | # TYPE node_textfile_scrape_error gauge
28 | node_textfile_scrape_error 0
29 |
--------------------------------------------------------------------------------
/util/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package cmd
17 |
18 | import (
19 | "os"
20 |
21 | kingpin "github.com/alecthomas/kingpin/v2"
22 | "github.com/go-kit/log"
23 | "github.com/go-kit/log/level"
24 | "github.com/prometheus/common/promlog"
25 | "github.com/prometheus/common/promlog/flag"
26 | "github.com/prometheus/common/version"
27 | )
28 |
29 | func InitCmd(name string) log.Logger {
30 | promlogConfig := &promlog.Config{}
31 | flag.AddFlags(kingpin.CommandLine, promlogConfig)
32 | kingpin.Version(version.Print(name))
33 | kingpin.CommandLine.UsageWriter(os.Stdout)
34 | kingpin.HelpFlag.Short('h')
35 | kingpin.Parse()
36 | return newLogger(promlogConfig)
37 | }
38 |
39 | func newLogger(config *promlog.Config) log.Logger {
40 | var l log.Logger
41 | if config.Format != nil && config.Format.String() == "json" {
42 | l = log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
43 | } else {
44 | l = log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
45 | }
46 |
47 | if config.Level != nil {
48 | var lvl level.Option
49 | switch config.Level.String() {
50 | case "debug":
51 | lvl = level.AllowDebug()
52 | case "info":
53 | lvl = level.AllowInfo()
54 | case "warn":
55 | lvl = level.AllowWarn()
56 | case "error":
57 | lvl = level.AllowError()
58 | default:
59 | lvl = level.AllowDebug()
60 | }
61 | l = level.NewFilter(l, lvl)
62 | }
63 | return l
64 | }
65 |
--------------------------------------------------------------------------------
/util/period/period.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package period
17 |
18 | import (
19 | "time"
20 | )
21 |
22 | type Period struct {
23 | Start time.Time
24 | End time.Time
25 | Complete bool
26 | }
27 |
28 | func CalculatePeriods(now time.Time, period string, lookbackTime time.Duration, includeIncompleteRanges bool) []Period {
29 | periods := []Period{}
30 |
31 | // Calculate the start and end times for each period based
32 | // on the specified period string
33 | switch period {
34 | case "monthly":
35 | firstDate := monthStart(now.Add(-lookbackTime), false)
36 | currentMonth := firstDate
37 | for d := 0; currentMonth.Before(now); d++ {
38 | complete := true
39 | end := monthStart(currentMonth, true).Add(-1 * time.Second)
40 | if end.After(now) {
41 | if !includeIncompleteRanges {
42 | break
43 | }
44 | complete = false
45 | end = now
46 | }
47 |
48 | periods = append(periods, Period{
49 | Start: currentMonth,
50 | End: end,
51 | Complete: complete,
52 | })
53 |
54 | currentMonth = monthStart(currentMonth, true)
55 | }
56 | }
57 | return periods
58 | }
59 |
60 | func monthStart(t time.Time, next bool) time.Time {
61 | year := t.Year()
62 | month := int(t.Month())
63 | if next {
64 | if month == 12 {
65 | month = 1
66 | year++
67 | } else {
68 | month++
69 | }
70 | }
71 | return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, t.Location())
72 | }
73 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/inconsistent_metrics.out:
--------------------------------------------------------------------------------
1 | # HELP go_goroutines Number of goroutines that currently exist.
2 | # TYPE go_goroutines gauge
3 | go_goroutines{foo=""} 20
4 | go_goroutines{foo="bar"} 229
5 | # HELP http_requests_total Total number of HTTP requests made.
6 | # TYPE http_requests_total counter
7 | http_requests_total{baz="",code="200",foo="",handler="",method="get"} 11
8 | http_requests_total{baz="",code="200",foo="",handler="alerts",method="get"} 35
9 | http_requests_total{baz="",code="200",foo="",handler="config",method="get"} 8
10 | http_requests_total{baz="",code="200",foo="",handler="flags",method="get"} 18
11 | http_requests_total{baz="",code="200",foo="",handler="graph",method="get"} 89
12 | http_requests_total{baz="",code="200",foo="",handler="prometheus",method="get"} 17051
13 | http_requests_total{baz="",code="200",foo="",handler="query",method="get"} 401
14 | http_requests_total{baz="",code="200",foo="",handler="query_range",method="get"} 15663
15 | http_requests_total{baz="",code="200",foo="",handler="rules",method="get"} 7
16 | http_requests_total{baz="",code="200",foo="",handler="series",method="get"} 221
17 | http_requests_total{baz="",code="200",foo="",handler="static",method="get"} 1647
18 | http_requests_total{baz="",code="200",foo="",handler="status",method="get"} 12
19 | http_requests_total{baz="",code="200",foo="bar",handler="",method="get"} 325
20 | http_requests_total{baz="",code="206",foo="",handler="static",method="get"} 2
21 | http_requests_total{baz="",code="400",foo="",handler="query_range",method="get"} 40
22 | http_requests_total{baz="",code="503",foo="",handler="query_range",method="get"} 3
23 | http_requests_total{baz="bar",code="200",foo="",handler="",method="get"} 93
24 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
25 | # TYPE node_textfile_mtime_seconds gauge
26 | node_textfile_mtime_seconds{file="fixtures/textfile/inconsistent_metrics/metrics.prom"} 1
27 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
28 | # TYPE node_textfile_scrape_error gauge
29 | node_textfile_scrape_error 0
30 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: "Deploy"
2 | on:
3 | push:
4 | branches:
5 | - 'main'
6 | jobs:
7 | deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2.4.0
11 | - uses: cachix/install-nix-action@v15
12 | with:
13 | install_url: https://releases.nixos.org/nix/nix-2.13.3/install
14 | extra_nix_config: |
15 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
16 | - uses: cachix/cachix-action@v12
17 | with:
18 | name: oy-toolkit
19 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
20 | - run: make lint build
21 | name: Lint check
22 | - run: make build
23 | name: Testing and building binaries
24 | - name: Upload binaries to release
25 | uses: svenstaro/upload-release-action@v2
26 | with:
27 | repo_token: ${{ secrets.GITHUB_TOKEN }}
28 | file: result/bin/*
29 | prerelease: true
30 | release_name: latest
31 | tag: ${{ github.ref }}
32 | overwrite: true
33 | file_glob: true
34 | body: "Latest artefacts, built from the main branch."
35 | - name: Building packages
36 | run: make packages
37 | - name: Upload packages to release
38 | uses: svenstaro/upload-release-action@v2
39 | with:
40 | repo_token: ${{ secrets.GITHUB_TOKEN }}
41 | file: result/*
42 | prerelease: true
43 | release_name: latest
44 | tag: ${{ github.ref }}
45 | overwrite: true
46 | file_glob: true
47 | body: "Latest artefacts, built from the main branch."
48 | - run: make publish DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}
49 | name: Building and pushing containers
50 | - run: make publish documentation DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}
51 | name: Building documentation
52 | - name: Deploying documentation
53 | uses: peaceiris/actions-gh-pages@v3
54 | with:
55 | github_token: ${{ secrets.GITHUB_TOKEN }}
56 | publish_dir: ./documentation
57 |
--------------------------------------------------------------------------------
/util/http/http.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package http
17 |
18 | import (
19 | "context"
20 | "net/http"
21 |
22 | "github.com/alecthomas/kingpin/v2"
23 | "github.com/go-kit/log"
24 | "github.com/prometheus/client_golang/prometheus"
25 | "github.com/prometheus/client_golang/prometheus/collectors"
26 | "github.com/prometheus/client_golang/prometheus/promhttp"
27 | "github.com/prometheus/exporter-toolkit/web"
28 | webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
29 | )
30 |
31 | var (
32 | webConfig = webflag.AddFlags(kingpin.CommandLine, ":9099")
33 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
34 | disableExporterMetrics = kingpin.Flag("web.disable-exporter-metrics", "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).").Bool()
35 | )
36 |
37 | func Serve(ctx context.Context, logger log.Logger, r *prometheus.Registry) error {
38 | if !*disableExporterMetrics {
39 | r.MustRegister(
40 | collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
41 | collectors.NewGoCollector(),
42 | )
43 | }
44 |
45 | http.Handle(*metricsPath, promhttp.HandlerFor(r, promhttp.HandlerOpts{}))
46 | srv := &http.Server{}
47 | errCh := make(chan error, 1)
48 | go func() {
49 | errCh <- web.ListenAndServe(srv, webConfig, logger)
50 | }()
51 |
52 | select {
53 | case e := <-errCh:
54 | return e
55 | case <-ctx.Done():
56 | srv.Shutdown(ctx)
57 | return nil
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/docs/content/promqlparser.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: PromQL Parser
3 | ---
4 |
5 | This tool enables you to validate the format of PromQL queries. It also produces
6 | a prettified query.
7 |
8 | ## Usage
9 |
10 | To use this tool, simply paste the content of your query in the
11 | following text area.
12 | Then, click on the "Parse" button.
13 |
14 | You can click on the following button to load a PromQL query:
15 |
16 | {{< unsafe >}}
17 |
18 | {{< /unsafe >}}
19 |
20 | ## Security and privacy
21 |
22 | The input is parsed in your browser and is not sent to our servers. This tool is
23 | based on the official
24 | [prometheus](https://pkg.go.dev/github.com/prometheus/prometheus@main/promql/parser#Prettify) library and is
25 | cross compiled to [WASM](https://webassembly.org/), so that it runs natively in
26 | your browser.
27 |
28 | ## PromQL input
29 |
30 | {{< unsafe >}}
31 |
32 | {{< /unsafe >}}
33 |
34 | {{< hint type=caution title=Loading icon=gdoc_timer >}}
35 | The application is loading. If this warning does not disappear, please make sure
36 | that [your browser supports WASM](https://caniuse.com/wasm) and that javascript
37 | is enabled.
38 | {{< /hint >}}
39 |
40 | {{< unsafe >}}
41 |
42 | {{< /unsafe >}}
43 |
44 | {{< unsafe >}}
45 |
46 |
47 |
63 |
64 |
65 |
66 | {{< /unsafe >}}
67 |
68 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/different_metric_types.out:
--------------------------------------------------------------------------------
1 | # HELP event_duration_seconds_total Query timings
2 | # TYPE event_duration_seconds_total summary
3 | event_duration_seconds_total{baz="inner_eval",quantile="0.5"} 1.073e-06
4 | event_duration_seconds_total{baz="inner_eval",quantile="0.9"} 1.928e-06
5 | event_duration_seconds_total{baz="inner_eval",quantile="0.99"} 4.35e-06
6 | event_duration_seconds_total_sum{baz="inner_eval"} 1.8652166505091474e+06
7 | event_duration_seconds_total_count{baz="inner_eval"} 1.492355615e+09
8 | event_duration_seconds_total{baz="prepare_time",quantile="0.5"} 4.283e-06
9 | event_duration_seconds_total{baz="prepare_time",quantile="0.9"} 7.796e-06
10 | event_duration_seconds_total{baz="prepare_time",quantile="0.99"} 2.2083e-05
11 | event_duration_seconds_total_sum{baz="prepare_time"} 840923.7919437207
12 | event_duration_seconds_total_count{baz="prepare_time"} 1.492355814e+09
13 | event_duration_seconds_total{baz="result_append",quantile="0.5"} 1.566e-06
14 | event_duration_seconds_total{baz="result_append",quantile="0.9"} 3.223e-06
15 | event_duration_seconds_total{baz="result_append",quantile="0.99"} 6.53e-06
16 | event_duration_seconds_total_sum{baz="result_append"} 4.404109951000078
17 | event_duration_seconds_total_count{baz="result_append"} 1.427647e+06
18 | event_duration_seconds_total{baz="result_sort",quantile="0.5"} 1.847e-06
19 | event_duration_seconds_total{baz="result_sort",quantile="0.9"} 2.975e-06
20 | event_duration_seconds_total{baz="result_sort",quantile="0.99"} 4.08e-06
21 | event_duration_seconds_total_sum{baz="result_sort"} 3.4123187829998307
22 | event_duration_seconds_total_count{baz="result_sort"} 1.427647e+06
23 | # HELP events_total this is a test metric
24 | # TYPE events_total counter
25 | events_total{foo="bar"} 10
26 | events_total{foo="baz"} 20
27 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
28 | # TYPE node_textfile_mtime_seconds gauge
29 | node_textfile_mtime_seconds{file="fixtures/textfile/different_metric_types/metrics.prom"} 1
30 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
31 | # TYPE node_textfile_scrape_error gauge
32 | node_textfile_scrape_error 0
33 |
--------------------------------------------------------------------------------
/cmd/oy-csv-to-targets/main_test.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "encoding/json"
20 | "io"
21 | "os"
22 | "os/exec"
23 | "testing"
24 |
25 | "github.com/stretchr/testify/require"
26 | )
27 |
28 | var mainPath = os.Args[0]
29 |
30 | func TestMain(m *testing.M) {
31 | for i, arg := range os.Args {
32 | if arg == "-test.main" {
33 | os.Args = append(os.Args[:i], os.Args[i+1:]...)
34 | main()
35 | return
36 | }
37 | }
38 |
39 | exitCode := m.Run()
40 | os.Exit(exitCode)
41 | }
42 |
43 | func TestCSVToTargets(t *testing.T) {
44 | if testing.Short() {
45 | t.Skip("skipping test in short mode.")
46 | }
47 |
48 | run := exec.Command(mainPath, "-test.main", "--input.file=testdata/targets.csv", "--output.file=testdata/targets.json")
49 |
50 | // Log stderr in case of failure.
51 | stderr, err := run.StderrPipe()
52 | require.NoError(t, err)
53 | go func() {
54 | slurp, _ := io.ReadAll(stderr)
55 | t.Log(string(slurp))
56 | }()
57 |
58 | err = run.Run()
59 | require.NoError(t, err)
60 |
61 | compareJSON(t, "testdata/expected_targets.json", "testdata/targets.json")
62 | }
63 |
64 | func compareJSON(t *testing.T, file1, file2 string) {
65 | data1, err := os.ReadFile(file1)
66 | require.NoError(t, err)
67 |
68 | data2, err := os.ReadFile(file2)
69 | require.NoError(t, err)
70 |
71 | var obj1 interface{}
72 | err = json.Unmarshal(data1, &obj1)
73 | require.NoError(t, err)
74 |
75 | var obj2 interface{}
76 | err = json.Unmarshal(data2, &obj2)
77 | require.NoError(t, err)
78 |
79 | require.Equal(t, obj1, obj2)
80 | }
81 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/histogram_extra_dimension/metrics.prom:
--------------------------------------------------------------------------------
1 | # HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction
2 | # TYPE prometheus_tsdb_compaction_chunk_range histogram
3 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="100"} 0
4 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="400"} 0
5 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="1600"} 0
6 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="6400"} 0
7 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="25600"} 7
8 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="102400"} 7
9 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="409600"} 1.412839e+06
10 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="1.6384e+06"} 1.69185e+06
11 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="6.5536e+06"} 1.691853e+06
12 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="2.62144e+07"} 1.691853e+06
13 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="+Inf"} 1.691853e+06
14 | prometheus_tsdb_compaction_chunk_range_sum{foo="bar"} 6.71393432189e+11
15 | prometheus_tsdb_compaction_chunk_range_count{foo="bar"} 1.691853e+06
16 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="100"} 0
17 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="400"} 0
18 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="1600"} 0
19 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="6400"} 0
20 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="25600"} 7
21 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="102400"} 7
22 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="409600"} 1.412839e+06
23 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="1.6384e+06"} 1.69185e+06
24 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="6.5536e+06"} 1.691853e+06
25 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="2.62144e+07"} 1.691853e+06
26 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="+Inf"} 1.691853e+06
27 | prometheus_tsdb_compaction_chunk_range_sum{foo="baz"} 6.71393432189e+11
28 | prometheus_tsdb_compaction_chunk_range_count{foo="baz"} 1.691853e+06
29 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-compat": {
4 | "flake": false,
5 | "locked": {
6 | "lastModified": 1696426674,
7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
8 | "owner": "edolstra",
9 | "repo": "flake-compat",
10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
11 | "type": "github"
12 | },
13 | "original": {
14 | "owner": "edolstra",
15 | "repo": "flake-compat",
16 | "type": "github"
17 | }
18 | },
19 | "flake-utils": {
20 | "inputs": {
21 | "systems": "systems"
22 | },
23 | "locked": {
24 | "lastModified": 1701680307,
25 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
26 | "owner": "numtide",
27 | "repo": "flake-utils",
28 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
29 | "type": "github"
30 | },
31 | "original": {
32 | "owner": "numtide",
33 | "repo": "flake-utils",
34 | "type": "github"
35 | }
36 | },
37 | "nixpkgs": {
38 | "locked": {
39 | "lastModified": 1702272962,
40 | "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
41 | "owner": "NixOS",
42 | "repo": "nixpkgs",
43 | "rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
44 | "type": "github"
45 | },
46 | "original": {
47 | "owner": "NixOS",
48 | "ref": "nixpkgs-unstable",
49 | "repo": "nixpkgs",
50 | "type": "github"
51 | }
52 | },
53 | "root": {
54 | "inputs": {
55 | "flake-compat": "flake-compat",
56 | "flake-utils": "flake-utils",
57 | "nixpkgs": "nixpkgs"
58 | }
59 | },
60 | "systems": {
61 | "locked": {
62 | "lastModified": 1681028828,
63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
64 | "owner": "nix-systems",
65 | "repo": "default",
66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
67 | "type": "github"
68 | },
69 | "original": {
70 | "owner": "nix-systems",
71 | "repo": "default",
72 | "type": "github"
73 | }
74 | }
75 | },
76 | "root": "root",
77 | "version": 7
78 | }
79 |
--------------------------------------------------------------------------------
/tool-documentation.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs,
3 | tool,
4 | name,
5 | description,
6 | }: let
7 | usage = pkgs.runCommand "${name} --help" {} "${tool}/bin/${name} --help &> $out";
8 | documentation = pkgs.writeTextFile {
9 | name = "${name}-doc.md";
10 | text = let
11 | firstParagraphDescription = builtins.elemAt (builtins.split "\n\n" (builtins.readFile description)) 0;
12 | frontmatterDescription = pkgs.lib.removePrefix "\n*${name}*" firstParagraphDescription;
13 | in ''
14 | ---
15 | title: ${name}
16 | geekdocRepo: "https://github.com/o11ydev/oy-toolkit"
17 | geekdocEditPath: "edit/main/cmd/${name}"
18 | geekdocFilePath: "README.md"
19 | tool: ${name}
20 | description: ${assert frontmatterDescription != firstParagraphDescription; builtins.toJSON frontmatterDescription}
21 | ---
22 |
23 | ${builtins.readFile ./docs/tools-top.md}
24 |
25 | ## Usage
26 |
27 | ```
28 | ${builtins.readFile usage}
29 | ```
30 |
31 | ## Description
32 | ${builtins.readFile description}
33 |
34 | ## Downloading
35 |
36 | {{< tabs "usage" >}}
37 |
38 | {{< tab "linux (wget)" >}}
39 | To execute **${name}** within Linux, run:
40 | ```
41 | wget https://github.com/o11ydev/oy-toolkit/releases/download/main/${name} -O ${name} && chmod +x ${name} && ./${name} --help
42 | ```
43 | {{< /tab >}}
44 |
45 | {{< tab "linux (deb)" >}}
46 | **${name}** is available as a `.deb` package:
47 | https://github.com/o11ydev/oy-toolkit/releases/download/main/${name}.deb
48 | {{< /tab >}}
49 |
50 | {{< tab "linux (yum)" >}}
51 | **${name}** is available as a `.rpm` package:
52 | https://github.com/o11ydev/oy-toolkit/releases/download/main/${name}.rpm
53 | {{< /tab >}}
54 |
55 | {{< tab "docker" >}}
56 | To execute **${name}** with docker, run:
57 | ```
58 | docker run quay.io/o11y/oy-toolkit:${name} --help
59 | ```
60 | {{< /tab >}}
61 |
62 | {{< tab "nix" >}}
63 | To execute **${name}** with nix, run:
64 | ```
65 | nix run github:o11ydev/oy-toolkit#${name} -- --help
66 | ```
67 | {{< /tab >}}
68 |
69 | {{< /tabs >}}
70 | '';
71 | };
72 | in "cat ${documentation} >> $out/${name}.md"
73 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # This variable is overriden by `nix develop`
2 | O11Y_NIX_SHELL_ENABLED ?= 0
3 |
4 | # Command used to run inside a `nix develop` shell.
5 | # HOME is needed for `go build`.
6 | NIX_DEVELOP = nix --extra-experimental-features nix-command develop --extra-experimental-features flakes -i --keep HOME --keep DOCKER_USERNAME --keep DOCKER_PASSWORD --keep DOCKER_REGISTRY --keep DOCKER_REPOSITORY --keep DOCKER_TAG_SUFFIX --keep DOCKER_ORG
7 |
8 | # Docker settings
9 | export DOCKER_ORG ?= o11y
10 | export DOCKER_PASSWORD ?= none
11 | export DOCKER_REGISTRY ?= quay.io
12 | export DOCKER_REPOSITORY ?= oy-toolkit
13 | export DOCKER_TAG_SUFFIX ?=
14 | export DOCKER_USERNAME ?= none
15 |
16 | # This is true if we are in `nix develop` shell.
17 | ifeq ($(O11Y_NIX_SHELL_ENABLED),1)
18 | all: lint build
19 |
20 | # Human friendly way of running tests.
21 | .PHONY: test
22 | test: lint
23 | @gotestsum ./...
24 |
25 | # Build runs tests, we do not need to explicitely add test as a dependency.
26 | .PHONY: build
27 | build: oy-toolkit
28 |
29 | .PHONY: fmt
30 | fmt:
31 | @gofumpt -l -w --extra .
32 | @goimports -w -local github.com/o11ydev/oy-toolkit .
33 | @alejandra -q *.nix
34 |
35 | .PHONY: lint
36 | lint:
37 | @golangci-lint run
38 | @alejandra -q --check *.nix
39 |
40 | oy-%: rebuild
41 | @echo ">> Building oy-$*"
42 | @nix build ".#oy-$*"
43 |
44 | .PHONY: tidy
45 | tidy:
46 | @go mod tidy -compat=1.17 -go=1.17
47 |
48 | # Shortcut to force running go build each time.
49 | .PHONY: rebuild
50 | rebuild:
51 |
52 | .PHONY: publish-script
53 | publish-script:
54 | @echo ">> Creating publishing script"
55 | @nix build ".#publish-script" -o ./publish.sh
56 |
57 | .PHONY: publish
58 | publish: publish-script
59 | @echo ">> Running publishing script"
60 | @bash -eu ./publish.sh
61 |
62 | .PHONY: documentation
63 | documentation:
64 | @echo ">> Generating documentation"
65 | @nix build ".#documentation" -o ./documentation
66 |
67 | .PHONY: packages
68 | packages:
69 | @echo ">> Generating packages"
70 | @nix build ".#nfpmPackages"
71 |
72 | .PHONY: vendorhash
73 | vendorhash:
74 | @go mod tidy -compat=1.21 -go=1.21
75 | @sed -i '/vendorSha256/s@".*"@"$(shell go mod vendor && nix hash path vendor)"@' packages.nix
76 | @rm -r vendor
77 |
78 | # If we are not in a `nix develop` shell, automatically run into it.
79 | else
80 | default:
81 | @$(NIX_DEVELOP) -c $(MAKE)
82 |
83 | %:
84 | @$(NIX_DEVELOP) -c $(MAKE) $*
85 |
86 | shell:
87 | @$(NIX_DEVELOP)
88 | endif
89 |
90 |
--------------------------------------------------------------------------------
/cmd/oy-periodic-queries/cache.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "sync"
20 | "time"
21 |
22 | "github.com/prometheus/common/model"
23 | )
24 |
25 | // Cache represents a simple cache with key-value pairs.
26 | type Cache struct {
27 | mu sync.Mutex
28 | items map[string]cacheItem
29 | timeout time.Duration
30 | }
31 |
32 | type cacheItem struct {
33 | value model.Value
34 | expiry time.Time
35 | }
36 |
37 | // NewCache creates a new cache with the specified timeout duration.
38 | func NewCache(timeout time.Duration) *Cache {
39 | c := &Cache{
40 | items: make(map[string]cacheItem),
41 | timeout: timeout,
42 | }
43 | go c.startCleanup()
44 | return c
45 | }
46 |
47 | // Get retrieves the value associated with the specified key.
48 | func (c *Cache) Get(key string) (model.Value, bool) {
49 | c.mu.Lock()
50 | defer c.mu.Unlock()
51 | item, ok := c.items[key]
52 | if !ok {
53 | return nil, false
54 | }
55 | if time.Now().After(item.expiry) {
56 | return nil, false
57 | }
58 | return item.value, true
59 | }
60 |
61 | // Set adds or updates the value associated with the specified key.
62 | func (c *Cache) Set(key string, value model.Value) {
63 | c.mu.Lock()
64 | defer c.mu.Unlock()
65 | item := cacheItem{
66 | value: value,
67 | expiry: time.Now().Add(c.timeout),
68 | }
69 | c.items[key] = item
70 | }
71 |
72 | // startCleanup periodically removes expired items from the cache.
73 | func (c *Cache) startCleanup() {
74 | ticker := time.NewTicker(c.timeout)
75 | defer ticker.Stop()
76 | for {
77 | <-ticker.C
78 | c.mu.Lock()
79 | maxDelete := len(c.items) / 20
80 | deleted := 0
81 | for key, item := range c.items {
82 | if time.Now().After(item.expiry) {
83 | delete(c.items, key)
84 | deleted++
85 | if deleted > maxDelete {
86 | break
87 | }
88 | }
89 | }
90 | c.mu.Unlock()
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/cmd/oy-periodic-queries/model.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "fmt"
20 | "os"
21 |
22 | "github.com/prometheus/common/model"
23 | "gopkg.in/yaml.v2"
24 | )
25 |
26 | type groupfile struct {
27 | Groups []Group `yaml:"groups"`
28 | }
29 |
30 | type Group struct {
31 | Name string `yaml:"name"`
32 | TimePeriod string `yaml:"time_period"`
33 | Lookback model.Duration `yaml:"lookback"`
34 | Rules []Rule `yaml:"rules"`
35 | IncludeIncompleteRanges bool `yaml:"include_incomplete_ranges"`
36 | }
37 |
38 | type Rule struct {
39 | Record string `yaml:"record"`
40 | Expr string `yaml:"expr"`
41 | Labels map[string]string `yaml:"labels"`
42 | }
43 |
44 | func (g *Group) Validate() error {
45 | if g.TimePeriod != "monthly" {
46 | return fmt.Errorf("Only monthly period is supported, got %q", g.TimePeriod)
47 | }
48 | return nil
49 | }
50 |
51 | func (g *Group) UnmarshalYAML(unmarshal func(interface{}) error) error {
52 | *g = Group{
53 | IncludeIncompleteRanges: true,
54 | }
55 | type plain Group
56 | if err := unmarshal((*plain)(g)); err != nil {
57 | return err
58 | }
59 |
60 | return g.Validate()
61 | }
62 |
63 | func loadFiles(files []string) ([]Group, error) {
64 | g := make([]Group, 0)
65 | for _, file := range files {
66 | data, err := os.ReadFile(file)
67 | if err != nil {
68 | return nil, fmt.Errorf("error reading rule file %s: %w", file, err)
69 | }
70 | gf := groupfile{}
71 | err = yaml.UnmarshalStrict(data, &gf)
72 | if err != nil {
73 | return nil, fmt.Errorf("error unmarshaling rule file %s: %w", file, err)
74 | }
75 | for _, group := range gf.Groups {
76 | if err := group.Validate(); err != nil {
77 | return nil, err
78 | }
79 | g = append(g, group)
80 | }
81 | }
82 | return g, nil
83 | }
84 |
--------------------------------------------------------------------------------
/util/collectors/fixtures/textfile/histogram_extra_dimension.out:
--------------------------------------------------------------------------------
1 | # HELP node_textfile_mtime_seconds Unixtime mtime of textfiles successfully read.
2 | # TYPE node_textfile_mtime_seconds gauge
3 | node_textfile_mtime_seconds{file="fixtures/textfile/histogram_extra_dimension/metrics.prom"} 1
4 | # HELP node_textfile_scrape_error 1 if there was an error opening or reading a file, 0 otherwise
5 | # TYPE node_textfile_scrape_error gauge
6 | node_textfile_scrape_error 0
7 | # HELP prometheus_tsdb_compaction_chunk_range Final time range of chunks on their first compaction
8 | # TYPE prometheus_tsdb_compaction_chunk_range histogram
9 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="100"} 0
10 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="400"} 0
11 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="1600"} 0
12 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="6400"} 0
13 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="25600"} 7
14 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="102400"} 7
15 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="409600"} 1.412839e+06
16 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="1.6384e+06"} 1.69185e+06
17 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="6.5536e+06"} 1.691853e+06
18 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="2.62144e+07"} 1.691853e+06
19 | prometheus_tsdb_compaction_chunk_range_bucket{foo="bar",le="+Inf"} 1.691853e+06
20 | prometheus_tsdb_compaction_chunk_range_sum{foo="bar"} 6.71393432189e+11
21 | prometheus_tsdb_compaction_chunk_range_count{foo="bar"} 1.691853e+06
22 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="100"} 0
23 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="400"} 0
24 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="1600"} 0
25 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="6400"} 0
26 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="25600"} 7
27 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="102400"} 7
28 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="409600"} 1.412839e+06
29 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="1.6384e+06"} 1.69185e+06
30 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="6.5536e+06"} 1.691853e+06
31 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="2.62144e+07"} 1.691853e+06
32 | prometheus_tsdb_compaction_chunk_range_bucket{foo="baz",le="+Inf"} 1.691853e+06
33 | prometheus_tsdb_compaction_chunk_range_sum{foo="baz"} 6.71393432189e+11
34 | prometheus_tsdb_compaction_chunk_range_count{foo="baz"} 1.691853e+06
35 |
--------------------------------------------------------------------------------
/cmd/oy-periodic-queries/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "context"
20 | "os"
21 |
22 | "github.com/go-kit/log/level"
23 | "github.com/prometheus/client_golang/prometheus"
24 |
25 | "github.com/o11ydev/oy-toolkit/util/client"
26 | "github.com/o11ydev/oy-toolkit/util/cmd"
27 | "github.com/o11ydev/oy-toolkit/util/http"
28 |
29 | kingpin "github.com/alecthomas/kingpin/v2"
30 | )
31 |
32 | func main() {
33 | ruleFiles := kingpin.Arg("rule-file", "File to read periodic recording rules from.").Required().Strings()
34 | prefetch := kingpin.Flag("prefetch", "Cache metrics before exposing them. Avoid scrape timeout.").Default("true").Bool()
35 | c := client.InitCliFlags()
36 | logger := cmd.InitCmd("oy-periodic-queries")
37 |
38 | promClient, err := client.NewClient(c)
39 | if err != nil {
40 | level.Error(logger).Log("msg", "Can't create Prometheus client", "err", err)
41 | os.Exit(1)
42 | }
43 |
44 | groups, err := loadFiles(*ruleFiles)
45 | if err != nil {
46 | level.Error(logger).Log("err", err)
47 | os.Exit(1)
48 | }
49 | collector, err := newGroupsCollector(logger, promClient, groups)
50 | if err != nil {
51 | level.Error(logger).Log("err", err)
52 | os.Exit(1)
53 | }
54 |
55 | r := prometheus.NewRegistry()
56 | if !*prefetch {
57 | r.MustRegister(collector)
58 | } else {
59 | periodicQueriesReady := prometheus.NewGauge(prometheus.GaugeOpts{
60 | Name: "periodic_queries_ready",
61 | Help: "Wheter periodic queries have been pre-fetched.",
62 | })
63 | periodicQueriesReady.Set(0)
64 | r.MustRegister(periodicQueriesReady)
65 | ch := make(chan prometheus.Metric)
66 | go func(ch chan prometheus.Metric) {
67 | for i := range ch {
68 | _ = i
69 | }
70 | }(ch)
71 | go func() {
72 | collector.Collect(ch)
73 | close(ch)
74 | r.MustRegister(collector)
75 | periodicQueriesReady.Set(1)
76 | }()
77 | }
78 | err = http.Serve(context.Background(), logger, r)
79 | if err != nil {
80 | level.Error(logger).Log("err", err)
81 | os.Exit(1)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/o11ydev/oy-toolkit
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/alecthomas/kingpin/v2 v2.4.0
7 | github.com/go-kit/log v0.2.1
8 | github.com/prometheus/client_golang v1.17.0
9 | github.com/prometheus/client_model v0.5.0
10 | github.com/prometheus/common v0.45.0
11 | github.com/prometheus/exporter-toolkit v0.11.0
12 | github.com/prometheus/prometheus v0.48.1
13 | github.com/stretchr/testify v1.8.4
14 | golang.org/x/crypto v0.16.0
15 | gonum.org/v1/plot v0.14.0
16 | gopkg.in/yaml.v2 v2.4.0
17 | )
18 |
19 | require (
20 | git.sr.ht/~sbinet/gg v0.5.0 // indirect
21 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
22 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
23 | github.com/beorn7/perks v1.0.1 // indirect
24 | github.com/campoy/embedmd v1.0.0 // indirect
25 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
26 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect
27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
28 | github.com/dennwc/varint v1.0.0 // indirect
29 | github.com/go-fonts/liberation v0.3.2 // indirect
30 | github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea // indirect
31 | github.com/go-logfmt/logfmt v0.6.0 // indirect
32 | github.com/go-pdf/fpdf v0.9.0 // indirect
33 | github.com/gogo/protobuf v1.3.2 // indirect
34 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
35 | github.com/golang/protobuf v1.5.3 // indirect
36 | github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
37 | github.com/jpillora/backoff v1.0.0 // indirect
38 | github.com/json-iterator/go v1.1.12 // indirect
39 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
41 | github.com/modern-go/reflect2 v1.0.2 // indirect
42 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
43 | github.com/pkg/errors v0.9.1 // indirect
44 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
45 | github.com/prometheus/procfs v0.12.0 // indirect
46 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
47 | go.uber.org/atomic v1.11.0 // indirect
48 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
49 | golang.org/x/image v0.14.0 // indirect
50 | golang.org/x/net v0.19.0 // indirect
51 | golang.org/x/oauth2 v0.15.0 // indirect
52 | golang.org/x/sync v0.5.0 // indirect
53 | golang.org/x/sys v0.15.0 // indirect
54 | golang.org/x/text v0.14.0 // indirect
55 | google.golang.org/appengine v1.6.8 // indirect
56 | google.golang.org/protobuf v1.31.0 // indirect
57 | gopkg.in/yaml.v3 v3.0.1 // indirect
58 | )
59 |
--------------------------------------------------------------------------------
/wasm/promqlparser/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | //go:build js && wasm
17 | // +build js,wasm
18 |
19 | package main
20 |
21 | import (
22 | "fmt"
23 | "syscall/js"
24 |
25 | "github.com/prometheus/prometheus/promql/parser"
26 | )
27 |
28 | func main() {
29 | c := make(chan struct{}, 0)
30 |
31 | js.Global().Set("parsepromql", js.FuncOf(parsePromQL))
32 | js.Global().Set("loadexample", js.FuncOf(loadExample))
33 | jsDoc := js.Global().Get("document")
34 | jsDoc.Call("getElementById", "runButton").Set("disabled", false)
35 | jsDoc.Call("getElementById", "exampleButton").Set("disabled", false)
36 | jsDoc.Call("getElementById", "loadingWarning").Get("style").Set("display", "none")
37 |
38 | <-c
39 | }
40 |
41 | func loadExample(this js.Value, args []js.Value) interface{} {
42 | jsDoc := js.Global().Get("document")
43 | res := jsDoc.Call("getElementById", "promqlInput")
44 | res.Set("value", `100 * sum(rate(jaeger_agent_http_server_errors_total[1m])) by (instance, job, namespace) / sum(rate(jaeger_agent_http_server_total[1m])) by (instance, job, namespace)>1`)
45 | return nil
46 | }
47 |
48 | func parsePromQL(this js.Value, args []js.Value) interface{} {
49 | jsDoc := js.Global().Get("document")
50 | res := jsDoc.Call("getElementById", "resultDiv")
51 |
52 | promql := args[0].String()
53 | expr, err := parser.ParseExpr(promql)
54 | if err != nil {
55 | res.Set("innerHTML", fmt.Sprintf(`
56 |
81 | `, expr.Pretty(0)))
82 | return nil
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/oy-expose/main_test.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "io"
20 | "net/http"
21 | "net/http/httptest"
22 | "net/url"
23 | "os"
24 | "os/exec"
25 | "testing"
26 | "time"
27 |
28 | "github.com/stretchr/testify/require"
29 | )
30 |
31 | var mainPath = os.Args[0]
32 |
33 | func TestMain(m *testing.M) {
34 | for i, arg := range os.Args {
35 | if arg == "-test.main" {
36 | os.Args = append(os.Args[:i], os.Args[i+1:]...)
37 | main()
38 | return
39 | }
40 | }
41 |
42 | exitCode := m.Run()
43 | os.Exit(exitCode)
44 | }
45 |
46 | func TestExpose(t *testing.T) {
47 | if testing.Short() {
48 | t.Skip("skipping test in short mode.")
49 | }
50 |
51 | testServer := httptest.NewServer(nil)
52 | testURL := testServer.URL
53 | testServer.Close()
54 | u, err := url.Parse(testURL)
55 | require.NoError(t, err)
56 |
57 | tmpfile, err := os.CreateTemp("", "metrics")
58 | require.NoError(t, err)
59 | defer os.Remove(tmpfile.Name())
60 |
61 | run := exec.Command(mainPath, "-test.main", "--web.listen-address="+u.Host, tmpfile.Name())
62 |
63 | // Log stderr in case of failure.
64 | stderr, err := run.StderrPipe()
65 | require.NoError(t, err)
66 | go func() {
67 | slurp, _ := io.ReadAll(stderr)
68 | t.Log(string(slurp))
69 | }()
70 |
71 | err = run.Start()
72 | require.NoError(t, err)
73 |
74 | done := make(chan error, 1)
75 | go func() { done <- run.Wait() }()
76 | select {
77 | case err := <-done:
78 | t.Errorf("oy-expose should be still running: %v", err)
79 | case <-time.After(1 * time.Second):
80 | }
81 |
82 | resp, err := http.Get(testURL + "/metrics")
83 | require.NoError(t, err)
84 | body, err := io.ReadAll(resp.Body)
85 | require.NoError(t, err)
86 | require.Contains(t, string(body), "node_textfile_scrape_error 0")
87 |
88 | os.Remove(tmpfile.Name())
89 |
90 | resp, err = http.Get(testURL + "/metrics")
91 | require.NoError(t, err)
92 | body, err = io.ReadAll(resp.Body)
93 | require.NoError(t, err)
94 | require.Contains(t, string(body), "node_textfile_scrape_error 1")
95 |
96 | select {
97 | case err := <-done:
98 | t.Errorf("oy-expose should be still running: %v", err)
99 | case <-time.After(5 * time.Second):
100 | require.NoError(t, run.Process.Kill())
101 | <-done
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/util/period/period_test.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package period
17 |
18 | import (
19 | "testing"
20 | "time"
21 |
22 | "github.com/stretchr/testify/require"
23 | )
24 |
25 | func TestCalculatePeriods(t *testing.T) {
26 | testCases := []struct {
27 | name string
28 | period string
29 | now time.Time
30 | lookbackTime time.Duration
31 | includeIncompleteRanges bool
32 | expectedPeriods []Period
33 | }{
34 | {
35 | name: "monthly periods, include incomplete ranges",
36 | period: "monthly",
37 | now: time.Date(2023, 2, 16, 12, 34, 56, 789, time.UTC),
38 | lookbackTime: 3 * 30 * 24 * time.Hour,
39 | includeIncompleteRanges: true,
40 | expectedPeriods: []Period{
41 | {
42 | Start: time.Date(2022, 11, 1, 0, 0, 0, 0, time.UTC),
43 | End: time.Date(2022, 12, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second),
44 | },
45 | {
46 | Start: time.Date(2022, 12, 1, 0, 0, 0, 0, time.UTC),
47 | End: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second),
48 | },
49 | {
50 | Start: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
51 | End: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second),
52 | },
53 | {
54 | Start: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
55 | End: time.Date(2023, 2, 16, 12, 34, 56, 789, time.UTC),
56 | },
57 | },
58 | },
59 | {
60 | name: "monthly periods, include incomplete ranges",
61 | period: "monthly",
62 | now: time.Date(2023, 2, 16, 12, 34, 56, 789, time.UTC),
63 | lookbackTime: 2 * 30 * 24 * time.Hour,
64 | includeIncompleteRanges: false,
65 | expectedPeriods: []Period{
66 | {
67 | Start: time.Date(2022, 12, 1, 0, 0, 0, 0, time.UTC),
68 | End: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second),
69 | },
70 | {
71 | Start: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
72 | End: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second),
73 | },
74 | },
75 | },
76 | }
77 |
78 | for _, tc := range testCases {
79 | t.Run(tc.name, func(t *testing.T) {
80 | periods := CalculatePeriods(tc.now, tc.period, tc.lookbackTime, tc.includeIncompleteRanges)
81 |
82 | require.Equal(t, len(tc.expectedPeriods), len(periods), "number of periods")
83 |
84 | for i := range periods {
85 | for _, p := range periods {
86 | t.Logf("%+v", p)
87 | }
88 | require.Equal(t, tc.expectedPeriods[i].Start, periods[i].Start, "period %d start", i)
89 | require.Equal(t, tc.expectedPeriods[i].End, periods[i].End, "period %d end", i)
90 | }
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/docs/content/metricslint.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: /metrics lint
3 | ---
4 |
5 | This tool enables you to validate the format of Prometheus metrics, and make
6 | sure they can be scraped by a Prometheus server.
7 |
8 | Prometheus supports two exposition formats: the Prometheus text-based exposition
9 | format and [OpenMetrics](https://openmetrics.io). The text-based exposition
10 | format is widespread, and many applications and client libraries supports it.
11 | Additionally, it can be used from scripts, to push metrics to the
12 | [Pushgateway](https://github.com/prometheus/pushgateway) or written to `*.prom` files, to
13 | be collected by the [textfile collectors](https://github.com/prometheus/node_exporter#textfile-collector)
14 | (available in both the [Node Exporter](https://github.com/prometheus/node_exporter) and the
15 | [Windows Exporter](https://github.com/prometheus-community/windows_exporter)).
16 |
17 | Our toolkit also provides [oy-expose](/oy-expose), a standalone tool that can
18 | expose the metrics of a file to be consumed by Prometheus.
19 |
20 | ## Usage
21 |
22 | To use this tool, simply paste the content of your `*.prom` file, the body of
23 | your Pushgateway request, or the output of a `/metrics` HTTP endpoint in the
24 | following text area.
25 | Then, click on the "Lint" button.
26 |
27 | You can click on the following button to load a few metrics:
28 |
29 | {{< unsafe >}}
30 |
31 | {{< /unsafe >}}
32 |
33 | ## Security and privacy
34 |
35 | The input is parsed in your browser and is not sent to our servers. This tool is
36 | based on the official
37 | [client_golang](https://github.com/prometheus/client_golang) library and is
38 | cross compiled to [WASM](https://webassembly.org/), so that it runs natively in
39 | your browser.
40 |
41 | ## Format specification
42 |
43 | This tool uses the [Prometheus text-based exposition
44 | format](https://prometheus.io/docs/instrumenting/exposition_formats/#exposition-formats).
45 | OpenMetrics is not supported yet.
46 |
47 | Everything is run locally from your browser, we do not receive or collect your
48 | metrics.
49 |
50 | ## Command line tool
51 |
52 | This utility behaves like the `promtool check metrics` command, which is
53 | downloadable with [Prometheus](https://prometheus.io/download).
54 |
55 | ## Metrics validation
56 |
57 | {{< unsafe >}}
58 |
59 | {{< /unsafe >}}
60 |
61 | {{< hint type=caution title=Loading icon=gdoc_timer >}}
62 | The application is loading. If this warning does not disappear, please make sure
63 | that [your browser supports WASM](https://caniuse.com/wasm) and that javascript
64 | is enabled.
65 | {{< /hint >}}
66 |
67 | {{< unsafe >}}
68 |
69 | {{< /unsafe >}}
70 |
71 | {{< unsafe >}}
72 |
73 |
74 |
90 |
91 |
92 |
93 | {{< /unsafe >}}
94 |
95 |
--------------------------------------------------------------------------------
/wasm/pwgen/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | //go:build js && wasm
17 | // +build js,wasm
18 |
19 | package main
20 |
21 | import (
22 | "errors"
23 | "fmt"
24 | "strconv"
25 | "syscall/js"
26 |
27 | "golang.org/x/crypto/bcrypt"
28 | "gopkg.in/yaml.v2"
29 | )
30 |
31 | func main() {
32 | c := make(chan struct{}, 0)
33 |
34 | js.Global().Set("generateUsers", js.FuncOf(pwgen))
35 | jsDoc := js.Global().Get("document")
36 | jsDoc.Call("getElementById", "runButton").Set("disabled", false)
37 | jsDoc.Call("getElementById", "loadingWarning").Get("style").Set("display", "none")
38 | js.Global().Call("addUser")
39 |
40 | <-c
41 | }
42 |
43 | func pwgen(this js.Value, args []js.Value) interface{} {
44 | jsDoc := js.Global().Get("document")
45 | res := jsDoc.Call("getElementById", "resultDiv")
46 | users := jsDoc.Call("querySelectorAll", `[name="username"]`)
47 | passwords := jsDoc.Call("querySelectorAll", `[name="password"]`)
48 | cost := jsDoc.Call("querySelectorAll", `[name="cost"]`)
49 | c, err := strconv.ParseInt(cost.Index(0).Get("value").String(), 10, 32)
50 | if err != nil {
51 | mkErr(err)
52 | return nil
53 | }
54 | userspw := make(map[string]string, users.Length())
55 | for i := 0; i < users.Length(); i++ {
56 | user := users.Index(i).Get("value").String()
57 | pw := passwords.Index(i).Get("value").String()
58 | if user == "" {
59 | mkErr(errors.New("username can't be empty"))
60 | return nil
61 | }
62 | if pw == "" {
63 | mkErr(fmt.Errorf("password for %q can't be empty", user))
64 | return nil
65 | }
66 | if _, ok := userspw[user]; ok {
67 | mkErr(fmt.Errorf("duplicate user %q", user))
68 | return nil
69 | }
70 | gpw, err := bcrypt.GenerateFromPassword([]byte(pw), int(c))
71 | if err != nil {
72 | mkErr(err)
73 | return nil
74 | }
75 | userspw[user] = string(gpw)
76 | }
77 |
78 | out, err := yaml.Marshal(struct {
79 | Users map[string]string `yaml:"basic_auth_users"`
80 | }{Users: userspw})
81 | if err != nil {
82 | mkErr(err)
83 | return nil
84 | }
85 |
86 | res.Set("innerHTML", fmt.Sprintf(`
87 |
Web configuration file
88 |
Write the following content as web.yml file and start Prometheus with --web.config.file=web.yml
38 | {{< /unsafe >}}
39 |
40 | {{< hint type=caution title=Loading icon=gdoc_timer >}}
41 | The application is loading. If this warning does not disappear, please make sure
42 | that [your browser supports WASM](https://caniuse.com/wasm) and that javascript
43 | is enabled.
44 | {{< /hint >}}
45 |
46 | {{< unsafe >}}
47 |
123 | The input can be parsed but there are linting issues:
124 |
%s
125 |
126 |
127 | `, pbs))
128 |
129 | return nil
130 | }
131 |
--------------------------------------------------------------------------------
/cmd/oy-csv-to-targets/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "bytes"
20 | "encoding/csv"
21 | "encoding/json"
22 | "fmt"
23 | "io"
24 | "os"
25 | "path/filepath"
26 |
27 | "github.com/alecthomas/kingpin/v2"
28 |
29 | promlabels "github.com/prometheus/prometheus/model/labels"
30 |
31 | "github.com/o11ydev/oy-toolkit/util/cmd"
32 | )
33 |
34 | var (
35 | input = kingpin.Flag("input.file", "Path to a CSV file to use as an input.").PlaceHolder("input.csv").Required().String()
36 | output = kingpin.Flag("output.file", "Path to a json file to use as an output.").PlaceHolder("targets.json").String()
37 | )
38 |
39 | type target struct {
40 | Labels promlabels.Labels `json:"labels"`
41 | Targets []string `json:"targets"`
42 | }
43 |
44 | func main() {
45 | logger := cmd.InitCmd("oy-csv-to-targets")
46 |
47 | err := csvToJSON(*input, *output)
48 | if err != nil {
49 | logger.Log("error", err.Error())
50 | }
51 | }
52 |
53 | // Convert a CSV file to a JSON with Prometheus targets.
54 | func csvToJSON(csvFile, jsonFile string) error {
55 | f, err := os.Open(csvFile)
56 | if err != nil {
57 | return fmt.Errorf("failed to open CSV file: %w", err)
58 | }
59 | defer f.Close()
60 |
61 | r := csv.NewReader(f)
62 |
63 | // Read the first line of the CSV to get the headers (i.e. label names).
64 | headers, err := r.Read()
65 | if err != nil {
66 | return fmt.Errorf("failed to read headers from CSV: %w", err)
67 | }
68 |
69 | targets := []target{}
70 |
71 | // Read the rest of the CSV to get the target values
72 | for {
73 | record, err := r.Read()
74 | if err == io.EOF {
75 | break
76 | }
77 | if err != nil {
78 | return fmt.Errorf("failed to read record from CSV: %w", err)
79 | }
80 |
81 | // Create a map to hold the label values
82 | labels := map[string]string{}
83 |
84 | // Loop through the headers and record values to create the label
85 | // values.
86 | // Ignore the first one.
87 | for i, header := range headers {
88 | if i > 0 {
89 | labels[header] = record[i]
90 | }
91 | }
92 |
93 | // Create a new Prometheus target using the label values and the first
94 | // record value as the target address.
95 | t := target{
96 | Labels: promlabels.FromMap(labels),
97 | Targets: []string{record[0]},
98 | }
99 |
100 | targets = append(targets, t)
101 | }
102 |
103 | b, err := json.Marshal(targets)
104 | if err != nil {
105 | return fmt.Errorf("failed to marshal targets into JSON: %w", err)
106 | }
107 |
108 | // If there is no file, pretty print to stdout.
109 | if jsonFile == "" {
110 | var out bytes.Buffer
111 | err = json.Indent(&out, b, "", " ")
112 | if err != nil {
113 | return fmt.Errorf("failed to pretty print JSON: %w", err)
114 | }
115 |
116 | fmt.Println(out.String())
117 | return nil
118 | }
119 |
120 | // Otherwise, output to the file using a temp file.
121 | // Create a temp file in the same directory as the destination file.
122 | tmpFile, err := os.CreateTemp(filepath.Dir(jsonFile), "tmp")
123 | if err != nil {
124 | return fmt.Errorf("failed to create temp file: %w", err)
125 | }
126 |
127 | _, err = tmpFile.Write(b)
128 | if err != nil {
129 | tmpFile.Close()
130 | os.Remove(tmpFile.Name())
131 | return fmt.Errorf("failed to write to temp file: %w", err)
132 | }
133 |
134 | err = tmpFile.Close()
135 | if err != nil {
136 | os.Remove(tmpFile.Name())
137 | return fmt.Errorf("failed to close temp file: %w", err)
138 | }
139 |
140 | // Rename the temp file to the destination file.
141 | err = os.Rename(tmpFile.Name(), jsonFile)
142 | if err != nil {
143 | os.Remove(tmpFile.Name())
144 | return fmt.Errorf("failed to rename temp file: %w", err)
145 | }
146 |
147 | return nil
148 | }
149 |
--------------------------------------------------------------------------------
/docs/static/custom.css:
--------------------------------------------------------------------------------
1 | /* Light mode theming */
2 | :root,
3 | :root[color-mode="light"] {
4 | --header-background: #ff004b;
5 | --header-font-color: #ffffff;
6 |
7 | --body-background: #ffffff;
8 | --body-font-color: #343a40;
9 |
10 | --mark-color: #ffab00;
11 |
12 | --button-background: #62cb97;
13 | --button-border-color: #4ec58a;
14 |
15 | --link-color: #518169;
16 | --link-color-visited: #c54e8a;
17 |
18 | --code-background: #f5f6f8;
19 | --code-accent-color: #e3e7eb;
20 | --code-accent-color-lite: #eff1f3;
21 |
22 | --code-copy-font-color: #6b7784;
23 | --code-copy-border-color: #adb4bc;
24 | --code-copy-success-color: #00c853;
25 |
26 | --accent-color: #e9ecef;
27 | --accent-color-lite: #f8f9fa;
28 |
29 | --control-icons: #b2bac1;
30 |
31 | --footer-background: #2f333e;
32 | --footer-font-color: #ffffff;
33 | --footer-link-color: #ffcc5c;
34 | --footer-link-color-visited: #ffcc5c;
35 | }
36 | @media (prefers-color-scheme: light) {
37 | :root {
38 | --header-background: #ff004b;
39 | --header-font-color: #ffffff;
40 |
41 | --body-background: #ffffff;
42 | --body-font-color: #343a40;
43 |
44 | --mark-color: #ffab00;
45 |
46 | --button-background: #62cb97;
47 | --button-border-color: #4ec58a;
48 |
49 | --link-color: #518169;
50 | --link-color-visited: #c54e8a;
51 |
52 | --code-background: #f5f6f8;
53 | --code-accent-color: #e3e7eb;
54 | --code-accent-color-lite: #eff1f3;
55 |
56 | --code-copy-font-color: #6b7784;
57 | --code-copy-border-color: #adb4bc;
58 | --code-copy-success-color: #00c853;
59 |
60 | --accent-color: #e9ecef;
61 | --accent-color-lite: #f8f9fa;
62 |
63 | --control-icons: #b2bac1;
64 |
65 | --footer-background: #2f333e;
66 | --footer-font-color: #ffffff;
67 | --footer-link-color: #ffcc5c;
68 | --footer-link-color-visited: #ffcc5c;
69 | }
70 | }
71 |
72 | /* Dark mode theming */
73 | :root[color-mode="dark"] {
74 | --header-background: #ff004b;
75 | --header-font-color: #ffffff;
76 |
77 | --body-background: #343a40;
78 | --body-font-color: #ced3d8;
79 |
80 | --mark-color: #ffab00;
81 |
82 | --button-background: #62cb97;
83 | --button-border-color: #4ec58a;
84 |
85 | --link-color: #7ac29e;
86 | --link-color-visited: #c27a9e;
87 |
88 | --code-background: #2f353a;
89 | --code-accent-color: #262b2f;
90 | --code-accent-color-lite: #2b3035;
91 |
92 | --code-copy-font-color: #adb4bc;
93 | --code-copy-border-color: #808c98;
94 | --code-copy-success-color: #00c853;
95 |
96 | --accent-color: #2b3035;
97 | --accent-color-lite: #2f353a;
98 |
99 | --control-icons: #b2bac1;
100 |
101 | --footer-background: #2f333e;
102 | --footer-font-color: #ffffff;
103 | --footer-link-color: #ffcc5c;
104 | --footer-link-color-visited: #ffcc5c;
105 | }
106 | @media (prefers-color-scheme: dark) {
107 | :root {
108 | --header-background: #ff004b;
109 | --header-font-color: #ffffff;
110 |
111 | --body-background: #343a40;
112 | --body-font-color: #ced3d8;
113 |
114 | --mark-color: #ffab00;
115 |
116 | --button-background: #62cb97;
117 | --button-border-color: #4ec58a;
118 |
119 | --link-color: #7ac29e;
120 | --link-color-visited: #c27a9e;
121 |
122 | --code-background: #2f353a;
123 | --code-accent-color: #262b2f;
124 | --code-accent-color-lite: #2b3035;
125 |
126 | --code-copy-font-color: #adb4bc;
127 | --code-copy-border-color: #808c98;
128 | --code-copy-success-color: #00c853;
129 |
130 | --accent-color: #2b3035;
131 | --accent-color-lite: #2f353a;
132 |
133 | --control-icons: #b2bac1;
134 |
135 | --footer-background: #2f333e;
136 | --footer-font-color: #ffffff;
137 | --footer-link-color: #ffcc5c;
138 | --footer-link-color-visited: #ffcc5c;
139 | }
140 | }
141 |
142 | .gdoc-brand img {
143 | width: 15rem;
144 | height: 3rem;
145 | }
146 |
147 | .gdoc-brand > span > span {
148 | display: none;
149 | }
150 |
151 | .gdoc-header > .container {
152 | padding-top: 0.5rem;
153 | padding-bottom: 0.5rem;
154 | }
155 |
156 | @media screen and (max-width: 600px) {
157 | table.flext thead {
158 | display: none;
159 | }
160 | table.flext td {
161 | display: flex;
162 | }
163 |
164 | table.flext td.flext::before {
165 | content: attr(label);
166 | font-weight: bold;
167 | width: 120px;
168 | min-width: 120px;
169 | }
170 | }
171 |
172 | pre code {
173 | overflow-x:auto;
174 | }
175 |
--------------------------------------------------------------------------------
/cmd/oy-periodic-queries/collector.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "context"
20 | "fmt"
21 | "os"
22 | "strconv"
23 | "strings"
24 | "time"
25 |
26 | "github.com/go-kit/log"
27 | "github.com/go-kit/log/level"
28 |
29 | "github.com/prometheus/client_golang/api"
30 | v1 "github.com/prometheus/client_golang/api/prometheus/v1"
31 | "github.com/prometheus/client_golang/prometheus"
32 | "github.com/prometheus/common/model"
33 |
34 | "github.com/o11ydev/oy-toolkit/util/period"
35 | )
36 |
37 | // newGroupCollector returns a new Collector exposing metrics from rules
38 | // evaluated at periodic intervals.
39 | func newGroupsCollector(logger log.Logger, client api.Client, groups []Group) (prometheus.Collector, error) {
40 | c := &groupsCollector{
41 | groups: groups,
42 | logger: logger,
43 | client: client,
44 | cache: NewCache(24 * time.Hour),
45 | }
46 | return c, nil
47 | }
48 |
49 | type groupsCollector struct {
50 | groups []Group
51 | logger log.Logger
52 | client api.Client
53 | cache *Cache
54 | }
55 |
56 | // Collect implements the Collector interface.
57 | func (c *groupsCollector) Collect(ch chan<- prometheus.Metric) {
58 | var lastErr error
59 |
60 | defer func() {
61 | var val float64 = 1
62 | if lastErr != nil {
63 | level.Error(c.logger).Log("err", lastErr)
64 | val = 0
65 | }
66 | ch <- prometheus.MustNewConstMetric(
67 | prometheus.NewDesc("periodic_queries_success", "", nil, nil),
68 | prometheus.GaugeValue,
69 | val,
70 | )
71 | }()
72 |
73 | metrics := []prometheus.Metric{}
74 | now := time.Now().Local()
75 | for _, g := range c.groups {
76 | for _, p := range period.CalculatePeriods(now, g.TimePeriod, time.Duration(g.Lookback), g.IncludeIncompleteRanges) {
77 | for _, r := range g.Rules {
78 | m, err := QueryRangeToMetric(c.logger, c.cache, p, r.Expr, c.client, r.Record, r.Labels)
79 | if err != nil {
80 | lastErr = err
81 | return
82 | }
83 | metrics = append(metrics, m...)
84 | }
85 | }
86 | }
87 | for _, metric := range metrics {
88 | ch <- metric
89 | }
90 | }
91 |
92 | func (c *groupsCollector) Describe(chan<- *prometheus.Desc) {
93 | }
94 |
95 | func QueryRangeToMetric(logger log.Logger, c *Cache, p period.Period, query string, client api.Client, metricName string, additionalLabels map[string]string) ([]prometheus.Metric, error) {
96 | queryAPI := v1.NewAPI(client)
97 |
98 | pq := replaceForPeriod(p, query)
99 |
100 | var (
101 | result model.Value
102 | err error
103 | ok bool
104 | )
105 |
106 | var found bool
107 | key := fmt.Sprintf("%d/%s", p.End.Unix(), pq)
108 | if p.Complete {
109 | if result, ok = c.Get(key); ok {
110 | found = true
111 | }
112 | }
113 | if !found {
114 | if pq != query {
115 | level.Debug(logger).Log("msg", "replaced query", "period_start", p.Start, "period_end", p.End, "period_complete", p.Complete, "query", query, "new_query", pq)
116 | }
117 | result, _, err = queryAPI.Query(context.Background(), pq, p.End)
118 | if err != nil {
119 | return nil, err
120 | }
121 | if p.Complete {
122 | c.Set(key, result)
123 | }
124 | }
125 |
126 | var metrics []prometheus.Metric
127 | metricLabels := make(prometheus.Labels, len(additionalLabels))
128 | for k, v := range additionalLabels {
129 | metricLabels[k] = replaceForPeriod(p, v)
130 | }
131 |
132 | for _, vec := range result.(model.Vector) {
133 | labels := make(prometheus.Labels, len(vec.Metric)+len(metricLabels))
134 | for k, v := range vec.Metric {
135 | if k == model.MetricNameLabel {
136 | continue
137 | }
138 | labels[string(k)] = string(v)
139 | }
140 | for k, v := range metricLabels {
141 | if k == model.MetricNameLabel {
142 | continue
143 | }
144 | labels[k] = v
145 | }
146 |
147 | sample := prometheus.MustNewConstMetric(
148 | prometheus.NewDesc(metricName, "", nil, labels),
149 | prometheus.UntypedValue,
150 | float64(vec.Value),
151 | )
152 |
153 | metrics = append(metrics, sample)
154 | }
155 |
156 | return metrics, nil
157 | }
158 |
159 | func replaceForPeriod(p period.Period, v string) string {
160 | d := p.End.Sub(p.Start)
161 | replacements := map[string]string{
162 | "RANGE": fmt.Sprintf("%.0fs", d.Seconds()),
163 | }
164 | for i := 0; i <= 100; i++ {
165 | replacements[strconv.Itoa(i)+"_PC_TIMESTAMP"] = fmt.Sprintf("%d", p.Start.Add(time.Duration(i/100)*d).Unix())
166 | replacements[strconv.Itoa(i)+"_PC_RANGE"] = fmt.Sprintf("%.0fs", float64(i)*d.Seconds()/100)
167 | }
168 |
169 | return os.Expand(v, func(s string) string {
170 | if s == "$" {
171 | return "$"
172 | }
173 | if v, ok := replacements[s]; ok {
174 | return v
175 | }
176 | if strings.HasPrefix(s, "_") {
177 | return p.Start.Format(strings.TrimPrefix(s, "-"))
178 | }
179 | return p.End.Format(s)
180 | })
181 | }
182 |
--------------------------------------------------------------------------------
/cmd/oy-scrape-jitter/main.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package main
17 |
18 | import (
19 | "context"
20 | "fmt"
21 | "image/color"
22 | "os"
23 | "time"
24 |
25 | "github.com/alecthomas/kingpin/v2"
26 | "github.com/go-kit/log"
27 | "github.com/go-kit/log/level"
28 | "github.com/prometheus/client_golang/api"
29 | apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
30 | "github.com/prometheus/common/model"
31 | "gonum.org/v1/plot"
32 | "gonum.org/v1/plot/plotter"
33 | "gonum.org/v1/plot/vg"
34 |
35 | "github.com/o11ydev/oy-toolkit/util/client"
36 | "github.com/o11ydev/oy-toolkit/util/cmd"
37 | )
38 |
39 | var (
40 | metric = kingpin.Flag("metric", "Metric to use to determine jitter.").Default("up").String()
41 | qtime = kingpin.Flag("query.timestamp", "Timestamp of the query.").Int64()
42 | png = kingpin.Flag("plot.file", "Path to a file to write an image of the results.").PlaceHolder("file.png").String()
43 | plotLog = kingpin.Flag("plot.log-y", "Use logarithmic Y axis.").Bool()
44 | lookback = kingpin.Flag("lookback", "How much time to look in the past for scrapes.").Default("1h").Duration()
45 | divisor = kingpin.Flag("divisor", "Divisor to use to determine if a scrape is aligned.").Default("1s").Duration()
46 | unalignedOnly = kingpin.Flag("log.unaligned-only", "Only take unaligned targets in logging.").Bool()
47 | plotUnalignedOnly = kingpin.Flag("plot.unaligned-only", "Only take unaligned targets in plot.").Default("true").Bool()
48 | quiet = kingpin.Flag("log.results-only", "Only log final result.").Bool()
49 | )
50 |
51 | func main() {
52 | c := client.InitCliFlags()
53 | logger := cmd.InitCmd("oy-scrape-jitter")
54 |
55 | promClient, err := client.NewClient(c)
56 | if err != nil {
57 | level.Error(logger).Log("msg", "Can't create Prometheus client", "err", err)
58 | os.Exit(1)
59 | }
60 |
61 | analyzeScrapeAlignment(logger, promClient)
62 | }
63 |
64 | func analyzeScrapeAlignment(logger log.Logger, promClient api.Client) {
65 | var plotValues plotter.Values
66 |
67 | tm := time.Now()
68 | if *qtime != 0 {
69 | tm = time.Unix(*qtime, 0)
70 | }
71 |
72 | api := apiv1.NewAPI(promClient)
73 | v, warnings, err := api.Query(context.Background(), fmt.Sprintf("%s[%dms]", *metric, lookback.Milliseconds()), tm)
74 | if err != nil {
75 | level.Error(logger).Log("msg", "Can't query up metrics", "err", err)
76 | os.Exit(1)
77 | }
78 | for w := range warnings {
79 | if err != nil {
80 | level.Warn(logger).Log("msg", w)
81 | }
82 | }
83 |
84 | if v.Type() != model.ValMatrix {
85 | if err != nil {
86 | level.Error(logger).Log("msg", "Wrong return type", "expected", model.ValMatrix, "got", v.Type())
87 | os.Exit(1)
88 | }
89 | }
90 |
91 | result, _ := v.(model.Matrix)
92 | var goodTargets, badTargets int
93 | var maxTarget int64
94 | for _, r := range result {
95 | var good, bad float64
96 | var max int64
97 | var lastTs time.Time
98 | for _, s := range r.Values {
99 | if lastTs.IsZero() {
100 | lastTs = s.Timestamp.Time()
101 | continue
102 | }
103 | diff := s.Timestamp.Time().Sub(lastTs).Milliseconds()
104 | ok := diff%divisor.Milliseconds() == 0
105 | level.Debug(logger).Log("metric", r.Metric.String(), "prev", lastTs, "current", s.Timestamp.Time(), "difference", diff, "aligned", ok)
106 | if ok {
107 | good++
108 | } else {
109 | bad++
110 | if diff%divisor.Milliseconds() > divisor.Milliseconds()/2 {
111 | diff = divisor.Milliseconds() - diff%divisor.Milliseconds()
112 | }
113 | if diff > max {
114 | max = diff % divisor.Milliseconds()
115 | if max > maxTarget {
116 | maxTarget = max
117 | }
118 | }
119 | }
120 | if *png != "" {
121 | if !ok || !*plotUnalignedOnly {
122 | plotValues = append(plotValues, float64(diff%divisor.Milliseconds()))
123 | }
124 | }
125 | lastTs = s.Timestamp.Time()
126 | }
127 |
128 | if (bad != 0 || !*unalignedOnly) && !*quiet {
129 | level.Info(logger).Log("metric", r.Metric.String(), "aligned", good, "unaligned", bad, "max_ms", max, "pc", fmt.Sprintf("%.2f%%", 100*good/(bad+good)))
130 | }
131 |
132 | if bad == 0 {
133 | goodTargets++
134 | } else {
135 | badTargets++
136 | }
137 | }
138 | level.Info(logger).Log("aligned_targets", goodTargets, "unaligned_targets", badTargets, "max_ms", maxTarget)
139 | if *png != "" {
140 | err := makePlot(plotValues, *png)
141 | if err != nil {
142 | level.Error(logger).Log("msg", "Unable to plot data.", "err", err)
143 | }
144 | }
145 | }
146 |
147 | func makePlot(values plotter.Values, out string) error {
148 | p := plot.New()
149 | p.Title.Text = "Scrape timestamps jitter"
150 | p.X.Label.Text = "Timestamp difference (ms)"
151 | p.Y.Label.Text = "Scrapes"
152 | l := plot.NewLegend()
153 | l.Add("https://o11y.tools")
154 | l.Top = true
155 | l.TextStyle.Color = color.RGBA{0, 0, 0, 100}
156 |
157 | hist, err := plotter.NewHist(values, 25)
158 | if err != nil {
159 | return err
160 | }
161 | hist.FillColor = color.RGBA{255, 0, 74, 255}
162 | hist.LogY = *plotLog
163 | p.Add(hist)
164 | p.Legend = l
165 |
166 | if err := p.Save(4*vg.Inch, 3*vg.Inch, out); err != nil {
167 | return err
168 | }
169 | return nil
170 | }
171 |
--------------------------------------------------------------------------------
/cmd/oy-periodic-queries/README.md:
--------------------------------------------------------------------------------
1 | # oy-periodic-queries
2 |
3 | *oy-periodic-queries* is a tool that allows you to evaluate Prometheus
4 | recording rules and export the results as metrics, with defined boundaries such
5 | as monthly or weekly (*Only monthly is implemented at the moment*).
6 |
7 | It uses calendar months.
8 |
9 | With this exporter, you can easily calculate monthly resource usage, or any
10 | other metric that requires periodic evaluation. Simply specify the recording
11 | rule and time boundaries, and the exporter will run the rule and provide you
12 | with accurate, timely metrics.
13 |
14 | In the recording rule and labels, you have access to the following variables, that will be
15 | replaced:
16 |
17 | | Variable | Meaning |
18 | |----------|---------|
19 | | $RANGE | The range of the query, in duration |
20 | | $0_PC_TIMESTAMP | The start time of the query, in unix timestamp |
21 | | $x_PC_TIMESTAMP | The time located a x % of the range, in unix timestamp, x
22 | between 0 and 100 |
23 | | $100_PC_TIMESTAMP | The end time of the query, in unix timestamp |
24 | | $x_PC_RANGE | A duration that represents x % of the current range, x between 0
25 | and 100 |
26 |
27 | Other values in `${}` are formatted with [Go's
28 | Time.Format](https://pkg.go.dev/time#Time.Format), e.g. `"${2006-01}"` is turned
29 | into the month and day of the end of the range. Prepend `_` to have it relative
30 | to the start of the range: `"${_2006-01}"`.
31 |
32 | ## Configuration
33 |
34 | The syntax of a rule file is:
35 |
36 | ```
37 | groups:
38 | [ - ]
39 | ```
40 |
41 | ### ``
42 |
43 | ```
44 | # The name of the group. Must be unique within a file.
45 | name:
46 |
47 | # The time period covered by the metrics.
48 | time_period: [ | default = "monthly" ]
49 |
50 | # The maximum time to look back.
51 | lookback: [ | default = "12w" ]
52 |
53 | rules:
54 | [ - ... ]
55 | ```
56 |
57 | ### ``
58 |
59 | The syntax for recording rules is:
60 |
61 | ```
62 | # The name of the time series to output to. Must be a valid metric name.
63 | record:
64 |
65 | # The PromQL expression to evaluate. Every evaluation cycle this is
66 | # evaluated at the current time, and the result recorded as a new set of
67 | # time series with the metric name as given by 'record'.
68 | expr:
69 |
70 | # Labels to add or overwrite before storing the result.
71 | labels:
72 | [ : ]
73 | ```
74 |
75 | ## Example rule file
76 |
77 | A simple example rules file would be:
78 |
79 | ```
80 | groups:
81 | - name: prometheus network usage
82 | time_period: monthly
83 | lookback: 365d
84 | include_incomplete_ranges: true
85 | rules:
86 | - expr: |
87 | increase(node_network_receive_bytes_total{instance=~"prometheus.*"}[$RANGE])
88 | and last_over_time(node_network_receive_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${1_PC_TIMESTAMP})
89 | and last_over_time(node_network_receive_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${100_PC_TIMESTAMP})
90 | record: prometheus:node_network_receive_bytes_total:monthly
91 | labels:
92 | month: "${2006-01}"
93 | - expr: |
94 | increase(node_network_transmit_bytes_total{instance=~"prometheus.*"}[$RANGE])
95 | and last_over_time(node_network_transmit_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${1_PC_TIMESTAMP})
96 | and last_over_time(node_network_transmit_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${100_PC_TIMESTAMP})
97 | record: prometheus:node_network_transmit_bytes_total:monthly
98 | labels:
99 | month: "${2006-01}"
100 | ```
101 |
102 |
103 | In the queries, the following part is used to only select the metrics that
104 | existed in the beginning and the end of the different months.
105 | ```
106 | and last_over_time(node_network_transmit_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${1_PC_TIMESTAMP})
107 | and last_over_time(node_network_transmit_bytes_total{instance=~"prometheus.*"}[${1_PC_RANGE}] @ ${100_PC_TIMESTAMP})
108 | ```
109 |
110 | Output of /metrics, when launched with `--web.disable-exporter-metrics`:
111 |
112 |
113 | ```
114 | # HELP periodic_rule_queries_success
115 | # TYPE periodic_rule_queries_success gauge
116 | periodic_rule_queries_success 1
117 | # HELP prometheus:node_network_receive_bytes_total:monthly
118 | # TYPE prometheus:node_network_receive_bytes_total:monthly untyped
119 | prometheus:node_network_receive_bytes_total:monthly{device="ens3",environment="o11ylab",instance="prometheus02.example.com",job="node",month="2023-01"} 2.2096172024710458e+11
120 | prometheus:node_network_receive_bytes_total:monthly{device="ens3",environment="o11ylab",instance="prometheus11.example.com",job="node",month="2023-01"} 1.4455463702554254e+11
121 | prometheus:node_network_receive_bytes_total:monthly{device="lo",environment="o11ylab",instance="prometheus02.example.com",job="node",month="2023-01"} 9.635369117702129e+08
122 | prometheus:node_network_receive_bytes_total:monthly{device="lo",environment="o11ylab",instance="prometheus11.example.com",job="node",month="2023-01"} 2.4574080228794675e+09
123 | # HELP prometheus:node_network_transmit_bytes_total:monthly
124 | # TYPE prometheus:node_network_transmit_bytes_total:monthly untyped
125 | prometheus:node_network_transmit_bytes_total:monthly{device="ens3",environment="o11ylab",instance="prometheus02.example.com",job="node",month="2023-01"} 1.8763866363794147e+11
126 | prometheus:node_network_transmit_bytes_total:monthly{device="ens3",environment="o11ylab",instance="prometheus11.example.com",job="node",month="2023-01"} 3.813239344424051e+11
127 | prometheus:node_network_transmit_bytes_total:monthly{device="lo",environment="o11ylab",instance="prometheus02.example.com",job="node",month="2023-01"} 9.635369117702129e+08
128 | prometheus:node_network_transmit_bytes_total:monthly{device="lo",environment="o11ylab",instance="prometheus11.example.com",job="node",month="2023-01"} 2.4574080228794675e+09
129 | ```
130 |
--------------------------------------------------------------------------------
/docs/static/brand.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
97 |
--------------------------------------------------------------------------------
/util/collectors/textfile.go:
--------------------------------------------------------------------------------
1 | // Copyright The o11y toolkit Authors
2 | // spdx-license-identifier: apache-2.0
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at:
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 |
16 | package collectors
17 |
18 | import (
19 | "fmt"
20 | "os"
21 | "sort"
22 | "time"
23 |
24 | "github.com/go-kit/log"
25 | "github.com/go-kit/log/level"
26 | "github.com/prometheus/client_golang/prometheus"
27 | dto "github.com/prometheus/client_model/go"
28 | "github.com/prometheus/common/expfmt"
29 | )
30 |
31 | var mtimeDesc = prometheus.NewDesc(
32 | "node_textfile_mtime_seconds",
33 | "Unixtime mtime of textfiles successfully read.",
34 | []string{"file"},
35 | nil,
36 | )
37 |
38 | type textFileCollector struct {
39 | path string
40 | // Only set for testing to get predictable output.
41 | mtime *float64
42 | logger log.Logger
43 | }
44 |
45 | // NewTextFileCollector returns a new Collector exposing metrics read from files
46 | // in the given textfile directory.
47 | func NewTextFileCollector(logger log.Logger, path string) (prometheus.Collector, error) {
48 | c := &textFileCollector{
49 | path: path,
50 | logger: logger,
51 | }
52 | return c, nil
53 | }
54 |
55 | func convertMetricFamily(metricFamily *dto.MetricFamily, ch chan<- prometheus.Metric, logger log.Logger) {
56 | var valType prometheus.ValueType
57 | var val float64
58 |
59 | allLabelNames := map[string]struct{}{}
60 | for _, metric := range metricFamily.Metric {
61 | labels := metric.GetLabel()
62 | for _, label := range labels {
63 | if _, ok := allLabelNames[label.GetName()]; !ok {
64 | allLabelNames[label.GetName()] = struct{}{}
65 | }
66 | }
67 | }
68 |
69 | for _, metric := range metricFamily.Metric {
70 | if metric.TimestampMs != nil {
71 | level.Warn(logger).Log("msg", "Ignoring unsupported custom timestamp on textfile collector metric", "metric", metric)
72 | }
73 |
74 | labels := metric.GetLabel()
75 | var names []string
76 | var values []string
77 | for _, label := range labels {
78 | names = append(names, label.GetName())
79 | values = append(values, label.GetValue())
80 | }
81 |
82 | for k := range allLabelNames {
83 | present := false
84 | for _, name := range names {
85 | if k == name {
86 | present = true
87 | break
88 | }
89 | }
90 | if !present {
91 | names = append(names, k)
92 | values = append(values, "")
93 | }
94 | }
95 |
96 | metricType := metricFamily.GetType()
97 | switch metricType {
98 | case dto.MetricType_COUNTER:
99 | valType = prometheus.CounterValue
100 | val = metric.Counter.GetValue()
101 |
102 | case dto.MetricType_GAUGE:
103 | valType = prometheus.GaugeValue
104 | val = metric.Gauge.GetValue()
105 |
106 | case dto.MetricType_UNTYPED:
107 | valType = prometheus.UntypedValue
108 | val = metric.Untyped.GetValue()
109 |
110 | case dto.MetricType_SUMMARY:
111 | quantiles := map[float64]float64{}
112 | for _, q := range metric.Summary.Quantile {
113 | quantiles[q.GetQuantile()] = q.GetValue()
114 | }
115 | ch <- prometheus.MustNewConstSummary(
116 | prometheus.NewDesc(
117 | *metricFamily.Name,
118 | metricFamily.GetHelp(),
119 | names, nil,
120 | ),
121 | metric.Summary.GetSampleCount(),
122 | metric.Summary.GetSampleSum(),
123 | quantiles, values...,
124 | )
125 | case dto.MetricType_HISTOGRAM:
126 | buckets := map[float64]uint64{}
127 | for _, b := range metric.Histogram.Bucket {
128 | buckets[b.GetUpperBound()] = b.GetCumulativeCount()
129 | }
130 | ch <- prometheus.MustNewConstHistogram(
131 | prometheus.NewDesc(
132 | *metricFamily.Name,
133 | metricFamily.GetHelp(),
134 | names, nil,
135 | ),
136 | metric.Histogram.GetSampleCount(),
137 | metric.Histogram.GetSampleSum(),
138 | buckets, values...,
139 | )
140 | default:
141 | panic("unknown metric type")
142 | }
143 | if metricType == dto.MetricType_GAUGE || metricType == dto.MetricType_COUNTER || metricType == dto.MetricType_UNTYPED {
144 | ch <- prometheus.MustNewConstMetric(
145 | prometheus.NewDesc(
146 | *metricFamily.Name,
147 | metricFamily.GetHelp(),
148 | names, nil,
149 | ),
150 | valType, val, values...,
151 | )
152 | }
153 | }
154 | }
155 |
156 | func (c *textFileCollector) exportMTimes(mtimes map[string]time.Time, ch chan<- prometheus.Metric) {
157 | if len(mtimes) == 0 {
158 | return
159 | }
160 |
161 | // Export the mtimes of the successful files.
162 | // Sorting is needed for predictable output comparison in tests.
163 | filepaths := make([]string, 0, len(mtimes))
164 | for path := range mtimes {
165 | filepaths = append(filepaths, path)
166 | }
167 | sort.Strings(filepaths)
168 |
169 | for _, path := range filepaths {
170 | mtime := float64(mtimes[path].UnixNano() / 1e9)
171 | if c.mtime != nil {
172 | mtime = *c.mtime
173 | }
174 | ch <- prometheus.MustNewConstMetric(mtimeDesc, prometheus.GaugeValue, mtime, path)
175 | }
176 | }
177 |
178 | func (c *textFileCollector) Describe(chan<- *prometheus.Desc) {
179 | }
180 |
181 | // Collect implements the Collector interface.
182 | func (c *textFileCollector) Collect(ch chan<- prometheus.Metric) {
183 | // Iterate over files and accumulate their metrics, but also track any
184 | // parsing errors so an error metric can be reported.
185 | var errored bool
186 | defer func() {
187 | var errVal float64
188 | if errored {
189 | errVal = 1.0
190 | }
191 | ch <- prometheus.MustNewConstMetric(
192 | prometheus.NewDesc(
193 | "node_textfile_scrape_error",
194 | "1 if there was an error opening or reading a file, 0 otherwise",
195 | nil, nil,
196 | ),
197 | prometheus.GaugeValue, errVal,
198 | )
199 | }()
200 |
201 | mtimes := make(map[string]time.Time)
202 | //f, err := os.Stat(c.path)
203 | //if err != nil && c.path != "" {
204 | // errored = true
205 | // level.Error(c.logger).Log("msg", "failed to read textfile", "path", c.path, "err", err)
206 | // return
207 | //}
208 |
209 | mtime, err := c.processFile(c.path, ch)
210 | if err != nil {
211 | errored = true
212 | level.Error(c.logger).Log("msg", "failed to collect textfile data", "file", c.path, "err", err)
213 | return
214 | }
215 |
216 | mtimes[c.path] = *mtime
217 | c.exportMTimes(mtimes, ch)
218 | }
219 |
220 | // processFile processes a single file, returning its modification time on success.
221 | func (c *textFileCollector) processFile(path string, ch chan<- prometheus.Metric) (*time.Time, error) {
222 | f, err := os.Open(path)
223 | if err != nil {
224 | return nil, fmt.Errorf("failed to open textfile data file %q: %w", path, err)
225 | }
226 | defer f.Close()
227 |
228 | var parser expfmt.TextParser
229 | families, err := parser.TextToMetricFamilies(f)
230 | if err != nil {
231 | return nil, fmt.Errorf("failed to parse textfile data from %q: %w", path, err)
232 | }
233 |
234 | if hasTimestamps(families) {
235 | return nil, fmt.Errorf("textfile %q contains unsupported client-side timestamps, skipping entire file", path)
236 | }
237 |
238 | for _, mf := range families {
239 | if mf.Help == nil {
240 | help := fmt.Sprintf("Metric read from %s", path)
241 | mf.Help = &help
242 | }
243 | }
244 |
245 | for _, mf := range families {
246 | convertMetricFamily(mf, ch, c.logger)
247 | }
248 |
249 | // Only stat the file once it has been parsed and validated, so that
250 | // a failure does not appear fresh.
251 | stat, err := f.Stat()
252 | if err != nil {
253 | return nil, fmt.Errorf("failed to stat %q: %w", path, err)
254 | }
255 |
256 | t := stat.ModTime()
257 | return &t, nil
258 | }
259 |
260 | // hasTimestamps returns true when metrics contain unsupported timestamps.
261 | func hasTimestamps(parsedFamilies map[string]*dto.MetricFamily) bool {
262 | for _, mf := range parsedFamilies {
263 | for _, m := range mf.Metric {
264 | if m.TimestampMs != nil {
265 | return true
266 | }
267 | }
268 | }
269 | return false
270 | }
271 |
--------------------------------------------------------------------------------
/packages.nix:
--------------------------------------------------------------------------------
1 | {pkgs, ...}:
2 | with pkgs; let
3 | basepkg = name:
4 | buildGoModule {
5 | name = name;
6 | src = stdenv.mkDerivation {
7 | name = "gosrc";
8 | srcs = [./go.mod ./go.sum ./cmd ./util ./wasm];
9 | phases = "installPhase";
10 | installPhase = ''
11 | mkdir $out
12 | for src in $srcs; do
13 | for srcFile in $src; do
14 | cp -r $srcFile $out/$(stripHash $srcFile)
15 | done
16 | done
17 | '';
18 | };
19 | CGO_ENABLED = 0;
20 | vendorSha256 = "sha256-LprSLtEiRffPdgnmAzCkh8SMNzua2Z3uuLw5F1S0M9c=";
21 | #vendorSha256 = pkgs.lib.fakeSha256;
22 | subPackages =
23 | if name == "oy-toolkit"
24 | then []
25 | else ["./cmd/${name}"];
26 |
27 | ldflags = [
28 | "-X github.com/prometheus/common/version.Version=${pkgs.lib.removeSuffix "\n" (builtins.readFile ./VERSION)}"
29 | "-X github.com/prometheus/common/version.Branch=n/a"
30 | "-X github.com/prometheus/common/version.Revision=n/a"
31 | "-X github.com/prometheus/common/version.BuildUser=n/a"
32 | "-X github.com/prometheus/common/version.BuildDate=n/a"
33 | ];
34 | };
35 | packageList =
36 | builtins.mapAttrs
37 | (
38 | name: value:
39 | basepkg name
40 | )
41 | (builtins.readDir ./cmd);
42 | wasmpkg = name:
43 | buildGoModule {
44 | name = name;
45 | src = stdenv.mkDerivation {
46 | name = "gosrc";
47 | srcs = [./go.mod ./go.sum ./cmd ./util ./wasm];
48 | phases = "installPhase";
49 | installPhase = ''
50 | mkdir $out
51 | for src in $srcs; do
52 | for srcFile in $src; do
53 | cp -r $srcFile $out/$(stripHash $srcFile)
54 | done
55 | done
56 | '';
57 | };
58 | CGO_ENABLED = 0;
59 | vendorSha256 = "sha256-LprSLtEiRffPdgnmAzCkh8SMNzua2Z3uuLw5F1S0M9c=";
60 | #vendorSha256 = pkgs.lib.fakeSha256;
61 | subPackages = ["wasm/${name}"];
62 | preBuild = ''
63 | export GOOS=js
64 | export GOARCH=wasm
65 | '';
66 | };
67 | wasmList =
68 | builtins.mapAttrs
69 | (
70 | name: value:
71 | wasmpkg name
72 | )
73 | (builtins.readDir ./wasm);
74 | dockerPackageList =
75 | lib.mapAttrs'
76 | (name: value:
77 | lib.nameValuePair
78 | "docker-${name}"
79 | (pkgs.dockerTools.buildImage {
80 | name = name;
81 | tag = "latest";
82 | contents = [pkgs.bashInteractive (builtins.getAttr name packageList)];
83 | config = {
84 | Entrypoint = ["/bin/${name}"];
85 | };
86 | }))
87 | (builtins.readDir ./cmd);
88 | in
89 | lib.recursiveUpdate
90 | (lib.recursiveUpdate packageList dockerPackageList)
91 | rec {
92 | oy-toolkit = basepkg "oy-toolkit";
93 | publish-script = (
94 | stdenv.mkDerivation {
95 | name = "release-script";
96 | phases = "buildPhase";
97 | buildPhase =
98 | pkgs.writeShellScript "publish" ''
99 | ''
100 | + (
101 | pkgs.lib.concatMapStrings (x: "\n" + x)
102 | (
103 | builtins.attrValues (
104 | builtins.mapAttrs
105 | (name: value: ''
106 | echo -e "\n\n## ${name} ##\n" >> $out
107 | echo 'echo ">> ${name}"' >> $out
108 | echo 'skopeo --insecure-policy copy --dest-username "$DOCKER_USERNAME" --dest-password "$DOCKER_PASSWORD" docker-archive://${builtins.getAttr name dockerPackageList} docker://$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_REPOSITORY:${pkgs.lib.removePrefix "docker-" name}$DOCKER_TAG_SUFFIX' >> $out
109 | '')
110 | dockerPackageList
111 | )
112 | )
113 | );
114 | }
115 | );
116 | documentation = (
117 | let
118 | theme = pkgs.fetchzip {
119 | url = "https://github.com/thegeeklab/hugo-geekdoc/releases/download/v0.29.4/hugo-geekdoc.tar.gz";
120 | sha256 = "sha256-Sypg9jBNbW4IeHTqDAq9ZpxgweW1BmFRFjDF51NSg/M=";
121 | stripRoot = false;
122 | };
123 | menu = {
124 | main = [
125 | {
126 | name = "tools";
127 | sub = builtins.map (x: {
128 | name = x;
129 | ref = "/" + x;
130 | }) (builtins.attrNames packageList);
131 | }
132 | {
133 | name = "Configuration";
134 | ref = "/httpclient";
135 | }
136 | {
137 | name = "Web tools";
138 | sub = [
139 | {
140 | name = "/metrics lint";
141 | ref = "/metricslint";
142 | }
143 | {
144 | name = "Password generator";
145 | ref = "/pwgen";
146 | }
147 | {
148 | name = "PromQL parser";
149 | ref = "/promqlparser";
150 | }
151 | {
152 | name = "Mimir resources";
153 | ref = "/mimircalc";
154 | }
155 | ];
156 | }
157 | ];
158 | };
159 | menuFile = pkgs.writeTextFile {
160 | name = "menu";
161 | text = builtins.toJSON menu;
162 | };
163 | wasmFiles = stdenv.mkDerivation {
164 | name = "wasmFiles";
165 | phases = "buildPhase";
166 | buildPhase =
167 | pkgs.writeShellScript "wasmFiles" ''
168 | mkdir $out
169 | ''
170 | + (
171 | pkgs.lib.concatMapStrings (x: "\n" + x)
172 | (
173 | builtins.attrValues (
174 | builtins.mapAttrs
175 | (name: value: ''
176 | cp ${builtins.getAttr name wasmList}/bin/js_wasm/${name} $out/${name}.wasm
177 | '')
178 | wasmList
179 | )
180 | )
181 | );
182 | };
183 | commandDocs = stdenv.mkDerivation {
184 | name = "commandDocs";
185 | phases = "buildPhase";
186 | buildPhase =
187 | pkgs.writeShellScript "commandDocs" ''
188 | mkdir $out
189 | ''
190 | + (
191 | pkgs.lib.concatMapStrings (x: "\n" + x)
192 | (
193 | builtins.attrValues (
194 | builtins.mapAttrs
195 | (name: value: let
196 | description = pkgs.writeTextFile {
197 | name = "description";
198 | text = pkgs.lib.removePrefix "# ${name}\n" (builtins.readFile (./cmd + "/${name}/README.md"));
199 | };
200 | in (import ./tool-documentation.nix {
201 | tool = builtins.getAttr name packageList;
202 | name = name;
203 | description = description;
204 | pkgs = pkgs;
205 | }))
206 | packageList
207 | )
208 | )
209 | );
210 | };
211 | in
212 | stdenv.mkDerivation {
213 | name = "documentation";
214 | src = ./docs;
215 | buildInputs = [pkgs.hugo];
216 | buildPhase = pkgs.writeShellScript "hugo" ''
217 | set -e
218 | cp ${wasmFiles}/* static/
219 | cp ${pkgs.go_1_21}/share/go/misc/wasm/wasm_exec.js static
220 |
221 | mkdir -p data/menu
222 | cp ${menuFile} data/menu/main.yml
223 | cp -r ${commandDocs}/* content
224 | cat data/menu/main.yml
225 | hugo --theme=${theme} -d $out
226 | echo o11y.tools > $out/CNAME
227 | '';
228 | installPhase = "true";
229 | }
230 | );
231 | nfpmPackages = let
232 | npmConfigurations = (
233 | builtins.mapAttrs (name: value:
234 | pkgs.writeTextFile {
235 | name = "npm-config-${name}";
236 | text = builtins.toJSON {
237 | name = name;
238 | arch = "amd64";
239 | version = pkgs.lib.removeSuffix "\n" (builtins.readFile ./VERSION);
240 | maintainer = "Julien Pivotto ";
241 | description = "The o11y toolkit is a collection of tools that are useful to manage and run an observability stack.";
242 | vendor = "o11y";
243 | contents = [
244 | {
245 | src = (builtins.getAttr name packageList) + "/bin/${name}";
246 | dst = "/bin/${name}";
247 | }
248 | ];
249 | };
250 | })
251 | (builtins.readDir ./cmd)
252 | );
253 | in
254 | stdenv.mkDerivation {
255 | name = "gosrc";
256 | buildInputs = [pkgs.nfpm oy-toolkit];
257 | phases = "installPhase";
258 | installPhase =
259 | ''
260 | mkdir $out
261 | ''
262 | + pkgs.lib.concatMapStrings (x: "\n" + x)
263 | (
264 | builtins.attrValues (
265 | builtins.mapAttrs
266 | (name: value: ''
267 | nfpm package --config ${(builtins.getAttr name npmConfigurations)} -p rpm -t $out/${name}.rpm
268 | nfpm package --config ${(builtins.getAttr name npmConfigurations)} -p deb -t $out/${name}.deb
269 | '')
270 | (builtins.readDir ./cmd)
271 | )
272 | );
273 | };
274 | }
275 |
--------------------------------------------------------------------------------
/docs/content/mimircalc.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Mimir resources
3 | ---
4 |
5 | This tool helps estimate the necessary CPU, memory, and disk space for Grafana
6 | Mimir setups. Input details like the number of active series and queries per
7 | second, and the calculator provides resource requirements for each system
8 | component. It is based on [Grafana's Planning Grafana Mimir capacity
9 | requirements](https://grafana.com/docs/mimir/latest/manage/run-production-environment/planning-capacity).
10 |
11 | {{< unsafe >}}
12 |
133 |
152 |
153 |
Total Resource Requirements
154 |
155 |
Total Resources
156 |
CPU: N/A cores
157 |
Memory: N/A GB
158 |
Disk: N/A GB
159 |
160 |
161 |
162 |
Calculated Requirements
163 |
164 |
Distributor
165 |
CPU: N/A cores
166 |
Memory: N/A GB
167 |
168 |
Ingester
169 |
CPU: N/A cores
170 |
Memory: N/A GB
171 |
Disk: N/A GB
172 |
173 |
Query-frontend
174 |
CPU: N/A cores
175 |
Memory: N/A GB
176 |
177 |
Query-scheduler
178 |
CPU: N/A cores
179 |
Memory: N/A GB
180 |
181 |
Querier
182 |
CPU: N/A cores
183 |
Memory: N/A GB
184 |
185 |
Store-gateway
186 |
CPU: N/A cores
187 |
Memory: N/A GB
188 |
Disk: N/A GB
189 |
190 |
Ruler
191 |
CPU: N/A cores
192 |
Memory: N/A GB
193 |
194 |
Compactor
195 |
CPU: N/A cores
196 |
Memory: N/A GB
197 |
Disk: N/A GB
198 |
199 |
Alertmanager
200 |
CPU: N/A cores
201 |
Memory: N/A GB
202 |
203 | {{< /unsafe >}}
204 |
205 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
2 | git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
3 | git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
4 | git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
5 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA=
6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw=
7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
8 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
9 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
10 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
11 | github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk=
12 | github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
13 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
14 | github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
15 | github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
16 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
17 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
18 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
19 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
20 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
21 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
22 | github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4=
23 | github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
24 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
25 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
26 | github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
27 | github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
28 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
29 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
30 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
31 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
32 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36 | github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
37 | github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
38 | github.com/go-fonts/dejavu v0.3.2 h1:3XlHi0JBYX+Cp8n98c6qSoHrxPa4AUKDMKdrh/0sUdk=
39 | github.com/go-fonts/dejavu v0.3.2/go.mod h1:m+TzKY7ZEl09/a17t1593E4VYW8L1VaBXHzFZOIjGEY=
40 | github.com/go-fonts/latin-modern v0.3.2 h1:M+Sq24Dp0ZRPf3TctPnG1MZxRblqyWC/cRUL9WmdaFc=
41 | github.com/go-fonts/latin-modern v0.3.2/go.mod h1:9odJt4NbRrbdj4UAMuLVd4zEukf6aAEKnDaQga0whqQ=
42 | github.com/go-fonts/liberation v0.3.2 h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ=
43 | github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI=
44 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
45 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
46 | github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc=
47 | github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk=
48 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
49 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
50 | github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
51 | github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
52 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
53 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
54 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
55 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
56 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
57 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
58 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
59 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
60 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
61 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
62 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
63 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
64 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
65 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
66 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
67 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
68 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
69 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
70 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
71 | github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww=
72 | github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
73 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
74 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
75 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
76 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
79 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
80 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
81 | github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
82 | github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
83 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
84 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
85 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
86 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
87 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
88 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
89 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
90 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
91 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
94 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
95 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
96 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
97 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
98 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
99 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
100 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
101 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
102 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
103 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
105 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
106 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
107 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
108 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
109 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
110 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
111 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
112 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
113 | github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
114 | github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
115 | github.com/prometheus/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g=
116 | github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q=
117 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
118 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
119 | github.com/prometheus/prometheus v0.48.1 h1:CTszphSNTXkuCG6O0IfpKdHcJkvvnAAE1GbELKS+NFk=
120 | github.com/prometheus/prometheus v0.48.1/go.mod h1:SRw624aMAxTfryAcP8rOjg4S/sHHaetx2lyJJ2nM83g=
121 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
122 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
123 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
124 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
125 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
126 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
127 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
128 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
129 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
130 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
131 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
132 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
133 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
134 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
135 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
136 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
137 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
138 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
139 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
140 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
141 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
142 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
143 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
144 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
145 | golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
146 | golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
147 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
148 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
149 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
150 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
151 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
152 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
153 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
154 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
155 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
156 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
157 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
158 | golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
159 | golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
160 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
161 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
162 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
163 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
164 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
165 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
166 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
168 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
169 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
170 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
171 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
172 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
173 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
174 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
175 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
176 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
177 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
178 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
179 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
180 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
181 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
182 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
183 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
184 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
185 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
186 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
187 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
188 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
189 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
190 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
191 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
192 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
193 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
194 | gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
195 | gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU=
196 | gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
197 | gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
198 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
199 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
200 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
201 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
202 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
203 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
204 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
205 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
206 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
207 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
208 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
209 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
210 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
211 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
212 | honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
213 | rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
214 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
215 |
--------------------------------------------------------------------------------