diff --git a/.github/workflows/generate-go-docs.yaml b/.github/workflows/generate-go-docs.yaml index fc44e8e09..59cca7239 100644 --- a/.github/workflows/generate-go-docs.yaml +++ b/.github/workflows/generate-go-docs.yaml @@ -31,7 +31,7 @@ jobs: GOPRIVATE: github.com/smartcontractkit/generate-go-function-docs run: | git config --global url."https://x-access-token:${{ steps.setup-github-token-read.outputs.access-token }}@github.com/".insteadOf "https://github.com/" - go install github.com/smartcontractkit/generate-go-function-docs@v0.0.1 + go install github.com/smartcontractkit/generate-go-function-docs@v0.0.2 go install github.com/jmank88/gomods@v0.1.3 go install golang.org/x/tools/gopls@latest diff --git a/.github/workflows/wasp-test-benchspy.yml b/.github/workflows/wasp-test-benchspy.yml new file mode 100644 index 000000000..f2107d396 --- /dev/null +++ b/.github/workflows/wasp-test-benchspy.yml @@ -0,0 +1,27 @@ +name: WASP's BenchSpy Go Tests +on: [push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + defaults: + run: + working-directory: wasp + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + src: + - 'wasp/benchspy/**' + - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 + if: steps.changes.outputs.src == 'true' + with: + nix_path: nixpkgs=channel:nixos-unstable + - name: Run tests + if: steps.changes.outputs.src == 'true' + run: |- + nix develop -c make test_benchspy_race diff --git a/.github/workflows/wasp-test.yml b/.github/workflows/wasp-test.yml index 34d395137..b438bd4c7 100644 --- a/.github/workflows/wasp-test.yml +++ b/.github/workflows/wasp-test.yml @@ -8,7 +8,7 @@ jobs: defaults: run: working-directory: wasp - runs-on: ubuntu-latest + runs-on: ubuntu22.04-16cores-64GB steps: - uses: actions/checkout@v3 - uses: dorny/paths-filter@v3 diff --git a/.gitignore b/.gitignore index 522e640ef..4073b8808 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ artifacts/ # Output of the go coverage tool, specifically when used with LiteIDE *.out +cover.html # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.nancy-ignore b/.nancy-ignore index 29171ebf5..e43ddaec9 100644 --- a/.nancy-ignore +++ b/.nancy-ignore @@ -12,4 +12,5 @@ CVE-2024-24786 # CWE-835 Loop with Unreachable Exit Condition ('Infinite Loop') CVE-2024-32972 # CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustion') [still not fixed, not even in v1.13.8] CVE-2023-42319 # CWE-noinfo: lol... go-ethereum v1.13.8 again CVE-2024-10086 # Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') -CVE-2024-51744 # CWE-755: Improper Handling of Exceptional Conditions \ No newline at end of file +CVE-2024-51744 # CWE-755: Improper Handling of Exceptional Conditions +CVE-2024-45338 # CWE-770: Allocation of Resources Without Limits or Throttling \ No newline at end of file diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0ea63e9e9..ee63e75b7 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -69,6 +69,23 @@ - [Profile](./libs/wasp/components/profile.md) - [Sampler](./libs/wasp/components/sampler.md) - [Schedule](./libs/wasp/components/schedule.md) + - [BenchSpy](./libs/wasp/benchspy/overview.md) + - [Getting started](./libs/wasp/benchspy/getting_started.md) + - [Your first test](./libs/wasp/benchspy/first_test.md) + - [Simplest metrics](./libs/wasp/benchspy/simplest_metrics.md) + - [Standard Loki metrics](./libs/wasp/benchspy/loki_std.md) + - [Custom Loki metrics](./libs/wasp/benchspy/loki_custom.md) + - [Standard Prometheus metrics](./libs/wasp/benchspy/prometheus_std.md) + - [Custom Prometheus metrics](./libs/wasp/benchspy/prometheus_custom.md) + - [To Loki or not to Loki?](./libs/wasp/benchspy/loki_dillema.md) + - [Real world example](./libs/wasp/benchspy/real_world.md) + - [Reports](./libs/wasp/benchspy/reports/overview.md) + - [Standard Report](./libs/wasp/benchspy/reports/standard_report.md) + - [Adding new QueryExecutor](./libs/wasp/benchspy/reports/new_executor.md) + - [Adding new standard load metric]() + - [Adding new standard resource metric]() + - [Defining a new report](./libs/wasp/benchspy/reports/new_report.md) + - [Adding new storage]() - [How to](./libs/wasp/how-to/overview.md) - [Start local observability stack](./libs/wasp/how-to/start_local_observability_stack.md) - [Try it out quickly](./libs/wasp/how-to/run_included_tests.md) diff --git a/book/src/libs/wasp/benchspy/first_test.md b/book/src/libs/wasp/benchspy/first_test.md new file mode 100644 index 000000000..5703290ef --- /dev/null +++ b/book/src/libs/wasp/benchspy/first_test.md @@ -0,0 +1,114 @@ +# BenchSpy - Your First Test + +Let's start with the simplest case, which doesn't require any part of the observability stack—only `WASP` and the application you are testing. +`BenchSpy` comes with built-in `QueryExecutors`, each of which also has predefined metrics that you can use. One of these executors is the `DirectQueryExecutor`, which fetches metrics directly from `WASP` generators, +which means you can run it with Loki. + +> [!NOTE] +> Not sure whether to use `Loki` or `Direct` query executors? [Read this!](./loki_dillema.md) + +## Test Overview + +Our first test will follow this logic: +- Run a simple load test. +- Generate a performance report and store it. +- Run the load test again. +- Generate a new report and compare it to the previous one. + +We'll use very simplified assertions for this example and expect the performance to remain unchanged. + +### Step 1: Define and Run a Generator + +Let's start by defining and running a generator that uses a mocked service: + +```go +gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), +}) +require.NoError(t, err) +gen.Run(true) +``` + +### Step 2: Generate a Baseline Performance Report + +With load data available, let's generate a baseline performance report and store it in local storage: + +```go +baseLineReport, err := benchspy.NewStandardReport( + // random hash, this should be the commit or hash of the Application Under Test (AUT) + "v1.0.0", + // use built-in queries for an executor that fetches data directly from the WASP generator + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + // WASP generators + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") + +fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +fetchErr := baseLineReport.FetchData(fetchCtx) +require.NoError(t, fetchErr, "failed to fetch data for baseline report") + +path, storeErr := baseLineReport.Store() +require.NoError(t, storeErr, "failed to store baseline report", path) +``` + +> [!NOTE] +> There's a lot to unpack here, and you're encouraged to read more about the built-in `QueryExecutors` and the standard metrics they provide as well as about the `StandardReport` [here](./reports/standard_report.md). +> +> For now, it's enough to know that the standard metrics provided by `StandardQueryExecutor_Direct` include: +> - Median latency +> - P95 latency (95th percentile) +> - Max latency +> - Error rate + +### Step 3: Run the Test Again and Compare Reports + +With the baseline report ready, let's run the load test again. This time, we'll use a wrapper function to automatically load the previous report, generate a new one, and ensure they are comparable. + +```go +// define a new generator using the same config values +newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), +}) +require.NoError(t, err) + +// run the load +newGen.Run(true) + +fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +// currentReport is the report that we just created (baseLineReport) +currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + // commit or tag of the new application version + "v2.0.0", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithGenerators(newGen), +) +require.NoError(t, err, "failed to fetch current report or load the previous one") +``` + +> [!NOTE] +> In a real-world case, once you've generated the first report, you should only need to use the `benchspy.FetchNewStandardReportAndLoadLatestPrevious` function. + +### What's Next? + +Now that we have two reports, how do we ensure that the application's performance meets expectations? +Find out in the [next chapter](./simplest_metrics.md). \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/getting_started.md b/book/src/libs/wasp/benchspy/getting_started.md new file mode 100644 index 000000000..9f5152ff5 --- /dev/null +++ b/book/src/libs/wasp/benchspy/getting_started.md @@ -0,0 +1,14 @@ +# BenchSpy - Getting Started + +The following examples assume you have access to the following applications: +- Grafana +- Loki +- Prometheus + +> [!NOTE] +> The easiest way to run these locally is by using CTFv2's [observability stack](../../../framework/observability/observability_stack.md). +> Be sure to install the `CTF CLI` first, as described in the [CTFv2 Getting Started](../../../framework/getting_started.md) guide. + +Since BenchSpy is tightly coupled with WASP, we highly recommend that you [get familiar with it first](../overview.md) if you haven't already. + +Ready? [Let's get started!](./first_test.md) \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/loki_custom.md b/book/src/libs/wasp/benchspy/loki_custom.md new file mode 100644 index 000000000..97f9d1efe --- /dev/null +++ b/book/src/libs/wasp/benchspy/loki_custom.md @@ -0,0 +1,47 @@ +# BenchSpy - Custom Loki Metrics + +In this chapter, we’ll explore how to use custom `LogQL` queries in the performance report. For this more advanced use case, we’ll manually compose the performance report. + +The load generation part is the same as in the standard Loki metrics example and will be skipped. + +## Defining Custom Metrics + +Let’s define two illustrative metrics: +- **`vu_over_time`**: The rate of virtual users generated by WASP, using a 10-second window. +- **`responses_over_time`**: The number of AUT's responses, using a 1-second window. + +```go +lokiQueryExecutor := benchspy.NewLokiQueryExecutor( + map[string]string{ + "vu_over_time": fmt.Sprintf("max_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"stats\", gen_name=~\"%s\"} | json | unwrap current_instances [10s]) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + "responses_over_time": fmt.Sprintf("sum(count_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} [1s])) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + }, + gen.Cfg.LokiConfig, +) +``` + +> [!NOTE] +> These `LogQL` queries use the standard labels that `WASP` applies when sending data to Loki. + +## Creating a `StandardReport` with Custom Queries + +Now, let’s create a `StandardReport` using our custom queries: + +```go +baseLineReport, err := benchspy.NewStandardReport( + "v1.0.0", + // notice the different functional option used to pass Loki executor with custom queries + benchspy.WithQueryExecutors(lokiQueryExecutor), + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") +``` + +## Wrapping Up + +The rest of the code remains unchanged, except for the names of the metrics being asserted. You can find the full example [here](...). + +Now it’s time to look at the last of the bundled `QueryExecutors`. Proceed to the [next chapter to read about Prometheus](./prometheus_std.md). + +> [!NOTE] +> You can find the full example [here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/loki_query_executor/loki_query_executor_test.go). \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/loki_dillema.md b/book/src/libs/wasp/benchspy/loki_dillema.md new file mode 100644 index 000000000..cf076eba4 --- /dev/null +++ b/book/src/libs/wasp/benchspy/loki_dillema.md @@ -0,0 +1,39 @@ +# BenchSpy - To Loki or Not to Loki? + +You might be wondering whether to use the `Loki` or `Direct` query executor if all you need are basic latency metrics. + +## Rule of Thumb + +You should opt for the `Direct` query executor if all you need is a single number, such as the median latency or error rate, and you're not interested in: +- Comparing time series directly, +- Examining minimum or maximum values over time, or +- Performing advanced calculations on raw data, + +## Why Choose `Direct`? + +The `Direct` executor returns a single value for each standard metric using the same raw data that Loki would use. It accesses data stored in the `WASP` generator, which is later pushed to Loki. + +This means you can: +- Run your load test without a Loki instance. +- Avoid calculating metrics like the median, 95th percentile latency, or error ratio yourself. + +By using `Direct`, you save resources and simplify the process when advanced analysis isn't required. + +> [!WARNING] +> Metrics calculated by the two query executors may differ slightly due to differences in their data processing and calculation methods: +> - **`Direct` QueryExecutor**: This method processes all individual data points from the raw dataset, ensuring that every value is taken into account for calculations like averages, percentiles, or other statistics. It provides the most granular and precise results but may also be more sensitive to outliers and noise in the data. +> - **`Loki` QueryExecutor**: This method aggregates data using a default window size of 10 seconds. Within each window, multiple raw data points are combined (e.g., through averaging, summing, or other aggregation functions), which reduces the granularity of the dataset. While this approach can improve performance and reduce noise, it also smooths the data, which may obscure outliers or small-scale variability. + +> #### Why This Matters for Percentiles: +> Percentiles, such as the 95th percentile (p95), are particularly sensitive to the granularity of the input data: +> - In the **`Direct` QueryExecutor**, the p95 is calculated across all raw data points, capturing the true variability of the dataset, including any extreme values or spikes. +> - In the **`Loki` QueryExecutor**, the p95 is calculated over aggregated data (i.e. using the 10-second window). As a result, the raw values within each window are smoothed into a single representative value, potentially lowering or altering the calculated p95. For example, an outlier that would significantly affect the p95 in the `Direct` calculation might be averaged out in the `Loki` window, leading to a slightly lower percentile value. + +> #### Direct caveats: +> - **buffer limitations:** `WASP` generator use a [StringBuffer](https://github.com/smartcontractkit/chainlink-testing-framework/blob/main/wasp/buffer.go) with fixed size to store the responses. Once full capacity is reached +> oldest entries are replaced with incoming ones. The size of the buffer can be set in generator's config. By default, it is limited to 50k entries to lower resource consumption and potential OOMs. +> +> - **sampling:** `WASP` generators support optional sampling of successful responses. It is disabled by deafult, but if you do enable it, then the calculations would no longer be done over a full dataset. + +> #### Key Takeaway: +> The difference arises because `Direct` prioritizes precision by using raw data, while `Loki` prioritizes efficiency and scalability by using aggregated data. When interpreting results, it’s essential to consider how the smoothing effect of `Loki` might impact the representation of variability or extremes in the dataset. This is especially important for metrics like percentiles, where such details can significantly influence the outcome. \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/loki_std.md b/book/src/libs/wasp/benchspy/loki_std.md new file mode 100644 index 000000000..51fa91211 --- /dev/null +++ b/book/src/libs/wasp/benchspy/loki_std.md @@ -0,0 +1,139 @@ +# BenchSpy - Standard Loki Metrics + +> [!WARNING] +> This example assumes you have access to Loki and Grafana instances. If you don't, learn how to launch them using CTFv2's [observability stack](../../../framework/observability/observability_stack.md). + +In this example, our Loki workflow will differ from the previous one in just a few details: +- The generator will include a Loki configuration. +- The standard query executor type will be `benchspy.StandardQueryExecutor_Loki`. +- All results will be cast to `[]string`. +- We'll calculate medians for all metrics. + +Ready? + +## Step 1: Define a New Load Generator + +Let's start by defining a new load generator: + +```go +label := "benchspy-std" + +gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + // read Loki config from environment + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu", + // set unique labels + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), +}) +require.NoError(t, err) +``` + +## Step 2: Run the Generator and Save the Baseline Report + +```go +gen.Run(true) + +baseLineReport, err := benchspy.NewStandardReport( + "v1.0.0", + // notice the different standard query executor type + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") + +fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +fetchErr := baseLineReport.FetchData(fetchCtx) +require.NoError(t, fetchErr, "failed to fetch data for baseline report") + +path, storeErr := baseLineReport.Store() +require.NoError(t, storeErr, "failed to store baseline report", path) +``` + +## Step 3: Skip to Metrics Comparison +Since the next steps are very similar to those in the first test, we’ll skip them and go straight to metrics comparison. + +By default, the `LokiQueryExecutor` returns results as the `[]string` data type. Let’s use dedicated convenience functions to cast them from `interface{}` to string slices: + +```go +allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) +allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + +require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty") +require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty") + +currentAsStringSlice := allCurrentAsStringSlice[gen.Cfg.GenName] +previousAsStringSlice := allPreviousAsStringSlice[gen.Cfg.GenName] +``` + +An explanation is needed here: this function separates metrics for each generator, hence it returns a `map[string]map[string][]string`. Let's break it down: +- outer map's key is generator name +- inner map's key is metric name and the value is a series of measurements +In our case there's only a single generator, but in a complex test there might be a few. + +## Step 4: Compare Metrics + +Now, let’s compare metrics. Since we have `[]string`, we’ll first convert it to `[]float64`, calculate the median, and ensure the difference between the averages is less than 1%. Again, this is just an example—you should decide the best way to validate your metrics. Here we are explicitly aggregating them using an average to get a single number representation of each metric, but for your case a median or percentile or yet some other aggregate might be more appropriate. + +```go +var compareAverages = func(t *testing.T, metricName string, currentAsStringSlice, previousAsStringSlice map[string][]string, maxPrecentageDiff float64) { + require.NotEmpty(t, currentAsStringSlice[metricName], "%s results were missing from current report", metricName) + require.NotEmpty(t, previousAsStringSlice[metricName], "%s results were missing from previous report", metricName) + + currentFloatSlice, err := benchspy.StringSliceToFloat64Slice(currentAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + currentMedian, err := stats.Mean(currentFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + previousFloatSlice, err := benchspy.StringSliceToFloat64Slice(previousAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + previousMedian, err := stats.Mean(previousFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + var diffPrecentage float64 + if previousMedian != 0.0 && currentMedian != 0.0 { + diffPrecentage = (currentMedian - previousMedian) / previousMedian * 100 + } else if previousMedian == 0.0 && currentMedian == 0.0 { + diffPrecentage = 0.0 + } else { + diffPrecentage = 100.0 + } + assert.LessOrEqual(t, math.Abs(diffPrecentage), maxPrecentageDiff, "%s medians are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage)) +} + +compareAverages( + t, + string(benchspy.MedianLatency), + currentAsStringSlice, + previousAsStringSlice, + 1.0, +) +compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice, previousAsStringSlice, 1.0) +compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice, previousAsStringSlice, 1.0) +compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice, previousAsStringSlice, 1.0) +``` + +> [!WARNING] +> Standard Loki metrics are all calculated using a 10 seconds moving window, which results in smoothing of values due to aggregation. +> To learn what that means in details, please refer to [To Loki or Not to Loki](./loki_dillema.md) chapter. +> +> Also, due to the HTTP API endpoint used, namely the `query_range`, all query results **are always returned as a slice**. Execution of **instant queries** +> that return a single data point is currently **not supported**. + +## What’s Next? + +In this example, we used standard metrics, which are the same as in the first test. Now, [let’s explore how to use your custom LogQL queries](./loki_custom.md). + +> [!NOTE] +> You can find the full example [here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/loki_query_executor/loki_query_executor_test.go). \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/overview.md b/book/src/libs/wasp/benchspy/overview.md new file mode 100644 index 000000000..51687eed3 --- /dev/null +++ b/book/src/libs/wasp/benchspy/overview.md @@ -0,0 +1,19 @@ +# BenchSpy + +BenchSpy (short for Benchmark Spy) is a [WASP](../overview.md)-coupled tool designed for easy comparison of various performance metrics. + +## Key Features +- **Three built-in data sources**: + - `Loki` + - `Prometheus` + - `Direct` +- **Standard/pre-defined metrics** for each data source. +- **Ease of extensibility** with custom metrics. +- **Ability to load the latest performance report** based on Git history. + +BenchSpy does not include any built-in comparison logic beyond ensuring that performance reports are comparable (e.g., they measure the same metrics in the same way), offering complete freedom to the user for interpretation and analysis. + +## Why could you need it? +`BenchSpy` was created with two main goals in mind: +* **measuring application performance programmatically**, +* **finding performance-related changes or regression issues between different commits or releases**. \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/prometheus_custom.md b/book/src/libs/wasp/benchspy/prometheus_custom.md new file mode 100644 index 000000000..b9ca06c44 --- /dev/null +++ b/book/src/libs/wasp/benchspy/prometheus_custom.md @@ -0,0 +1,93 @@ +# BenchSpy - Custom Prometheus Metrics + +Similar to what we did with Loki, we can use custom metrics with Prometheus. + +Most of the code remains the same as in the previous example. However, the differences begin with the need to manually create a `PrometheusQueryExecutor` with our custom queries: + +```go +// No need to pass the name regex pattern, as we provide it directly in the queries +// Remeber that you are free to use any other matching values or labels or none at all +promConfig := benchspy.NewPrometheusConfig() + +customPrometheus, err := benchspy.NewPrometheusQueryExecutor( + map[string]string{ + // Scalar value + "95p_cpu_all_containers": "scalar(quantile(0.95, rate(container_cpu_usage_seconds_total{name=~\"node[^0]\"}[5m])) * 100)", + // Matrix value + "cpu_rate_by_container": "rate(container_cpu_usage_seconds_total{name=~\"node[^0]\"}[1m])[30m:1m]", + }, + promConfig, +) +``` + +## Passing Custom Queries to the Report + +Next, pass the custom queries as a query executor: + +```go +baseLineReport, err := benchspy.NewStandardReport( + "91ee9e3c903d52de12f3d0c1a07ac3c2a6d141fb", + // notice the different functional option used to pass Prometheus executor with custom queries + benchspy.WithQueryExecutors(customPrometheus), + benchspy.WithGenerators(gen), + // notice that no Prometehus config is passed here +) +require.NoError(t, err, "failed to create baseline report") +``` + +> [!NOTE] +> When using custom Prometheus queries, you don’t need to pass the `PrometheusConfig` to `NewStandardReport()`, as the URL already been set during the creation of the `PrometheusQueryExecutor`. + +## Fetching and Casting Metrics + +Fetching the current and previous reports remains unchanged, as does casting Prometheus metrics to their specific types: + +```go +currentAsValues := benchspy.MustAllPrometheusResults(currentReport) +previousAsValues := benchspy.MustAllPrometheusResults(previousReport) + +assert.Equal(t, len(currentAsValues), len(previousAsValues), "number of metrics in results should be the same") +``` + +## Handling Different Data Types + +Here’s where things differ. While all standard query results are instances of `model.Vector`, the two custom queries introduce new types: +- `model.Matrix` +- `*model.Scalar` + +These differences are reflected in the further casting process before accessing the final metrics: + +```go +current95CPUUsage := currentAsValues["95p_cpu_all_containers"] +previous95CPUUsage := previousAsValues["95p_cpu_all_containers"] + +assert.Equal(t, current95CPUUsage.Type(), previous95CPUUsage.Type(), "types of metrics should be the same") +assert.IsType(t, current95CPUUsage, &model.Scalar{}, "current metric should be a scalar") + +currentCPUByContainer := currentAsValues["cpu_rate_by_container"] +previousCPUByContainer := previousAsValues["cpu_rate_by_container"] + +assert.Equal(t, currentCPUByContainer.Type(), previousCPUByContainer.Type(), "types of metrics should be the same") +assert.IsType(t, currentCPUByContainer, model.Matrix{}, "current metric should be a scalar") + +current95CPUUsageAsMatrix := currentCPUByContainer.(model.Matrix) +previous95CPUUsageAsMatrix := currentCPUByContainer.(model.Matrix) + +assert.Equal(t, len(current95CPUUsageAsMatrix), len(previous95CPUUsageAsMatrix), "number of samples in matrices should be the same") +``` + +> [!WARNING] +> When casting to Prometheus' final types, it’s crucial to remember the distinction between pointer and value receivers: +> +> **Pointer receivers**: +> - `*model.String` +> - `*model.Scalar` +> +> **Value receivers**: +> - `model.Vector` +> - `model.Matrix` + +And that's it! You know all you need to know to unlock the full power of `BenchSpy`! + +> [!NOTE] +> You can find the full example [here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/prometheus_query_executor/prometheus_query_executor_test.go). \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/prometheus_std.md b/book/src/libs/wasp/benchspy/prometheus_std.md new file mode 100644 index 000000000..b5b0bb440 --- /dev/null +++ b/book/src/libs/wasp/benchspy/prometheus_std.md @@ -0,0 +1,116 @@ +# BenchSpy - Standard Prometheus Metrics + +Now that we've seen how to query and assert load-related metrics, let's explore how to query and assert on resource usage by our Application Under Test (AUT). + +If you're unsure why this is important, consider the following situation: the p95 latency of a new release matches the previous version, but memory consumption is 34% higher. Not ideal, right? + +## Step 1: Prometheus Configuration + +Since `WASP` has no built-in integration with `Prometheus`, we need to pass its configuration separately: + +```go +promConfig := benchspy.NewPrometheusConfig("node[^0]") +``` + +This constructor loads the URL from the environment variable `PROMETHEUS_URL` and adds a single regex pattern to match containers **by name**. In this case, it excludes the bootstrap Chainlink node (named `node0` in the `CTFv2` stack). + +> [!WARNING] +> This example assumes that you have both the observability stack and basic node set running. +> If you have the [CTF CLI](../../../framework/getting_started.md), you can start it by running: `ctf b ns`. + +> [!NOTE] +> Matching containers **by name** should work both for most k8s and Docker setups using `CTFv2` observability stack. + +## Step 2: Fetching and Storing a Baseline Report + +As in previous examples, we'll use built-in Prometheus metrics and fetch and store a baseline report: + +```go +baseLineReport, err := benchspy.NewStandardReport( + "91ee9e3c903d52de12f3d0c1a07ac3c2a6d141fb", + // notice the different standard query executor type + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Prometheus), + benchspy.WithPrometheusConfig(promConfig), + // Required to calculate test time range based on generator start/end times. + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") + +fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +fetchErr := baseLineReport.FetchData(fetchCtx) +require.NoError(t, fetchErr, "failed to fetch baseline report") + +path, storeErr := baseLineReport.Store() +require.NoError(t, storeErr, "failed to store baseline report", path) +``` + +> [!NOTE] +> Standard metrics for Prometheus differ from those used by `Loki` or `Direct` query executors. +> Prometheus metrics focus on resource usage by the AUT, while `Loki`/`Direct` metrics measure load characteristics. +> +> Standard Prometheus metrics include: +> - `median_cpu_usage` +> - `median_mem_usage` +> - `max_cpu_usage` +> - `p95_cpu_usage` +> - `p95_mem_usage` +> - `max_mem_usage` +> +> These are calculated at the **container level**, based on total usage (user + system). + +## Step 3: Handling Prometheus Result Types + +Unlike Loki and Generator, Prometheus results can have various data types: +- `scalar` +- `string` +- `vector` +- `matrix` + +This makes asserting results a bit more complex. + +### Converting Results to `model.Value` + +First, convert results to the `model.Value` interface using convenience functions: + +```go +currentAsValues := benchspy.MustAllPrometheusResults(currentReport) +previousAsValues := benchspy.MustAllPrometheusResults(previousReport) +``` + +### Casting to Specific Types + +Next, determine the data type returned by your query and cast it accordingly: + +```go +// Fetch a single metric +currentMedianCPUUsage := currentAsValues[string(benchspy.MedianCPUUsage)] +previousMedianCPUUsage := previousAsValues[string(benchspy.MedianCPUUsage)] + +assert.Equal(t, currentMedianCPUUsage.Type(), previousMedianCPUUsage.Type(), "types of metrics should be the same") + +// In this case, we know the query returns a Vector +currentMedianCPUUsageVector := currentMedianCPUUsage.(model.Vector) +previousMedianCPUUsageVector := previousMedianCPUUsage.(model.Vector) +``` + +Since these metrics are not related to load generation, the convenience function a `map[string](model.Value)`, where key is resource metric name. + +> [!WARNING] +> All standard Prometheus metrics bundled with `BenchSpy` return `model.Vector`. +> However, if you use custom queries, you must manually verify their return types. + +## Skipping Assertions for Resource Usage + +We skip the assertion part because, unless you're comparing resource usage under stable loads, significant differences between reports are likely. +For example: +- The first report might be generated right after the node set starts. +- The second report might be generated after the node set has been running for some time. + +## What’s Next? + +In the next chapter, we’ll [explore custom Prometheus queries](./prometheus_custom.md). + +> [!NOTE] +> You can find the full example [here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/prometheus_query_executor/prometheus_query_executor_test.go). \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/real_world.md b/book/src/libs/wasp/benchspy/real_world.md new file mode 100644 index 000000000..ae6af9d62 --- /dev/null +++ b/book/src/libs/wasp/benchspy/real_world.md @@ -0,0 +1,109 @@ +# BenchSpy - Real-World Example + +Now that we've covered all possible usages, you might wonder how to write a test that compares performance between different releases of your application. Here’s a practical example to guide you through the process. + +## Typical Workflow + +1. Write a performance test. +2. At the end of the test, generate a performance report, store it, and commit it to Git. +3. Modify the test to fetch both the latest report and create a new one. +4. Write assertions to validate your performance metrics. + +--- + +## Writing the Performance Test + +We'll use a simple mock for the application under test. This mock waits for `50 ms` before returning a 200 response code. + +```go +generator, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), +}) +require.NoError(t, err) +generator.Run(true) +``` + +--- + +## Generating the First Report + +Here, we'll generate a performance report for version `v1.0.0` using the `Direct` query executor. The report will be saved to a custom directory named `test_reports`. This report will later be used to compare the performance of new versions. + +```go +fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +baseLineReport, err := benchspy.NewStandardReport( + "v1.0.0", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithReportDirectory("test_reports"), + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") + +fetchErr := baseLineReport.FetchData(fetchCtx) +require.NoError(t, fetchErr, "failed to fetch data for original report") + +path, storeErr := baseLineReport.Store() +require.NoError(t, storeErr, "failed to store current report", path) +``` + +--- + +## Modifying Report Generation + +With the baseline report for `v1.0.0` stored, we'll modify the test to support future releases. The code from the previous step will change as follows: + +```go +currentVersion := os.Getenv("CURRENT_VERSION") +require.NotEmpty(t, currentVersion, "No current version provided") + +fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) +defer cancelFn() + +currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + currentVersion, + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithReportDirectory("test_reports"), + benchspy.WithGenerators(generator), +) +require.NoError(t, err, "failed to fetch current report or load the previous one") +``` + +This function fetches the current report (for version passed as environment variable `CURRENT_VERSION`) while loading the latest stored report from the `test_reports` directory. + +--- + +## Adding Assertions + +Let’s assume you want to ensure that none of the performance metrics degrade by more than **1%** between releases (and that error rate has not changed at all). Here's how you can write assertions using a convenient function for the `Direct` query executor: + +```go +hasErrors, errors := benchspy.CompareDirectWithThresholds( + 1.0, // Max 1% worse median latency + 1.0, // Max 1% worse p95 latency + 1.0, // Max 1% worse maximum latency + 0.0, // No increase in error rate + currentReport, previousReport) +require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors)) +``` + +--- + +## Conclusion + +You’re now ready to use `BenchSpy` to ensure that your application’s performance does not degrade below your specified thresholds! + +> [!NOTE] +> [Here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/direct_query_executor/direct_query_real_case.go) you can find an example test where performance has degraded significantly, +> because mock's latency has been increased from `50ms` to `60ms`. +> +> **This test passes because it is designed to expect performance degradation. Of course, in a real application, your goal should be to prevent such regressions.** 😊 \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/reports/new_executor.md b/book/src/libs/wasp/benchspy/reports/new_executor.md new file mode 100644 index 000000000..a14af5a19 --- /dev/null +++ b/book/src/libs/wasp/benchspy/reports/new_executor.md @@ -0,0 +1,67 @@ +# BenchSpy - Adding a New QueryExecutor + +# QueryExecutor interface + +As mentioned earlier, the `StandardReport` supports three different data sources: +- `Direct` +- `Loki` +- `Prometheus` + +Each of these implements the `QueryExecutor` interface: + +```go +type QueryExecutor interface { + // Kind returns the type of the QueryExecutor + Kind() string + // Validate checks if the QueryExecutor has all the necessary data and configuration to execute the queries + Validate() error + // Execute executes the queries and populates the QueryExecutor with the results + Execute(ctx context.Context) error + // Results returns the results of the queries, where the key is the query name and the value is the result + Results() map[string]interface{} + // IsComparable checks whether both QueryExecutors can be compared (e.g., they have the same type, queries, etc.), + // and returns an error if any differences are found + IsComparable(other QueryExecutor) error + // TimeRange sets the time range for the queries + TimeRange(startTime, endTime time.Time) +} +``` + +When creating a new `QueryExecutor`, most functions in this interface are straightforward. Below, we focus on two that may require additional explanation. + +--- + +## `Kind` + +The `Kind` function should return a unique string identifier for your `QueryExecutor`. This identifier is crucial because `StandardReport` uses it when unmarshalling JSON files with stored reports. + +Additionally, you need to extend the `StandardReport.UnmarshalJSON` function to support your new executor. + +> [!NOTE] +> If your `QueryExecutor` includes interfaces, `interface{}` or `any` types, or fields that should not or cannot be serialized, ensure you implement custom `MarshalJSON` and `UnmarshalJSON` functions. Existing executors can provide useful examples. + +--- + +## `TimeRange` + +The `TimeRange` method is called by `StandardReport` just before invoking `Execute()` for each executor. This method sets the time range for queries (required for Loki and Prometheus). + +By default, `StandardReport` calculates the test time range automatically by analyzing the schedules of all generators and determining the earliest start time and latest end time. This eliminates the need for manual calculations. + +--- + +With these details in mind, you should have a clear path to implementing your own `QueryExecutor` and integrating it seamlessly with `BenchSpy`'s `StandardReport`. + +# NamedGenerator interface + +Executors that query load generation metrics should also implement this simple interface: +```go +type NamedGenerator interface { + // GeneratorName returns the name of the generator + GeneratorName() string +} +``` + +It is used primarly, when casting results from `map[string]interface{}` to target type, while splitting them between different generators. + +Currently, this interface is implemented by `Direct` and `Loki` exectors, but not by `Prometheus`. \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/reports/new_report.md b/book/src/libs/wasp/benchspy/reports/new_report.md new file mode 100644 index 000000000..655e6989d --- /dev/null +++ b/book/src/libs/wasp/benchspy/reports/new_report.md @@ -0,0 +1,74 @@ +# Defining a New Report + +Each `BenchSpy` report must implement the `Reporter` interface, which handles three primary responsibilities: +- **Storage and retrieval** (`Storer` interface) +- **Data fetching** (`DataFetcher` interface) +- **Comparison** (`Comparator` interface) + +--- + +## Reporter Interface +### Definition +```go +type Reporter interface { + Storer + DataFetcher + Comparator +} +``` + +The comparison of actual performance data should not be part of the report itself. It should be done independently, ideally using Go's `require` and `assert` statements. + +--- + +## Storer Interface +### Definition +```go +type Storer interface { + // Store stores the report in persistent storage and returns the path to it, or an error + Store() (string, error) + // Load loads the report from persistent storage based on the test name and commit/tag, or returns an error + Load(testName, commitOrTag string) error + // LoadLatest loads the latest report from persistent storage for the given test name, or returns an error + LoadLatest(testName string) error +} +``` + +### Usage +- If storing reports locally under Git satisfies your requirements, you can reuse the `LocalStorage` implementation of `Storer`. +- If you need to store reports in S3 or a database, you will need to implement the interface yourself. + +--- + +## DataFetcher Interface +### Definition +```go +type DataFetcher interface { + // FetchData populates the report with data from the test + FetchData(ctx context.Context) error +} +``` + +### Purpose +This interface is solely responsible for fetching data from the data source and populating the report with results. + +--- + +## Comparator Interface +### Definition +```go +type Comparator interface { + // IsComparable checks whether two reports can be compared (e.g., test configuration, app resources, queries, and metrics are identical), + // and returns an error if any differences are found + IsComparable(otherReport Reporter) error +} +``` + +### Purpose +This interface ensures that both reports are comparable by verifying: +- Both use generators with identical configurations (e.g., load type, load characteristics). +- Both reports feature the same data sources and queries. + +--- + +This design provides flexibility and composability, allowing you to store, fetch, and compare reports in a way that fits your specific requirements. \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/reports/overview.md b/book/src/libs/wasp/benchspy/reports/overview.md new file mode 100644 index 000000000..bcb6ef3c9 --- /dev/null +++ b/book/src/libs/wasp/benchspy/reports/overview.md @@ -0,0 +1,5 @@ +# BenchSpy - Reports + +`BenchSpy` was designed with composability in mind. It includes a `StandardReport` implementation that should cover the majority of use cases while also allowing for the creation of fully customized reports with ease. + +Learn more about the `StandardReport` in the [next chapter](./standard_report.md). diff --git a/book/src/libs/wasp/benchspy/reports/standard_report.md b/book/src/libs/wasp/benchspy/reports/standard_report.md new file mode 100644 index 000000000..b716ffdda --- /dev/null +++ b/book/src/libs/wasp/benchspy/reports/standard_report.md @@ -0,0 +1,150 @@ +# BenchSpy - Standard Report + +`StandardReport` supports three types of data sources: +- `Direct` +- `Loki` +- `Prometheus` + +Each allows you to use either pre-defined metrics or create custom ones. + +--- + +## Pre-defined (Standard) Metrics + +### Direct and Loki + +Both query executors focus on the characteristics of the load generated by `WASP`. Their datasets are nearly identical because the `Direct` executor queries load-specific data before it is sent to Loki. The `Loki` executor, however, offers richer querying options (via `LogQL`) and access to the actual load profile (rather than the configured one). + +Predefined metrics for both include: +- Median latency +- 95th percentile latency +- Max latency +- Error rate + +**Latency** is the round-trip time from sending a request to receiving a response from the Application Under Test (AUT). + +**Error rate** is the ratio of failed responses to total responses. This includes requests that timed out or returned errors from `Gun` or `Vu` implementations. + +--- + +### Prometheus + +Prometheus metrics focus on resource consumption by the AUT rather than load generation. These metrics include: +- Median CPU usage +- 95th percentile CPU usage +- Max CPU usage +- Median memory usage +- 95th percentile memory usage +- Max memory usage + +Prometheus queries focus on **total** consumption, which combines the usage of the underlying system and your application. + +--- + +### How to Use Pre-defined Metrics + +To use pre-defined metrics, call the `NewStandardReport` method: + +```go +report, err := benchspy.NewStandardReport( + "v1.1.1", + // Query executor types for which standard metrics should be generated + benchspy.WithStandardQueries( + benchspy.StandardQueryExecutor_Prometheus, + benchspy.StandardQueryExecutor_Loki, + ), + // Prometheus configuration is required if using standard Prometheus metrics + benchspy.WithPrometheusConfig(benchspy.NewPrometheusConfig("node[^0]")), + // WASP generators + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create the report") +``` + +--- + +## Custom Metrics + +### WASP Generator + +Since `WASP` stores AUT responses in each generator, you can create custom metrics based on this data. For example, to calculate the timeout ratio: + +```go +var generator *wasp.Generator + +var timeouts = func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + if len(responses.Data) == 0 { + return 0, nil + } + + timeoutCount := 0.0 + inTimeCount := 0.0 + for _, response := range responses.Data { + if response.Timeout { + timeoutCount++ + } else { + inTimeCount++ + } + } + + return timeoutCount / (timeoutCount + inTimeCount), nil +} + +directExecutor, err := NewDirectQueryExecutor(generator, map[string]DirectQueryFn{ + "timeout_ratio": timeouts, +}) +require.NoError(t, err, "failed to create Direct Query Executor") +``` + +--- + +### Loki + +Creating custom `LogQL` queries is even simpler. Use `NewLokiQueryExecutor` with a map of desired queries: + +```go +var generator *wasp.Generator + +lokiQueryExecutor := benchspy.NewLokiQueryExecutor( + map[string]string{ + "responses_over_time": fmt.Sprintf("sum(count_over_time({my_label=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} [5s])) by (node_id, gen_name)", label, gen.Cfg.GenName), + }, + generator.Cfg.LokiConfig, +) +``` + +> [!NOTE] +> To write effective `LogQL` queries for `WASP`, familiarize yourself with generator labeling and the `test_data_types` used by `WASP`. + +--- + +### Prometheus + +Adding custom `PromQL` queries is equally straightforward: + +```go +promConfig := benchspy.NewPrometheusConfig() + +prometheusExecutor, err := benchspy.NewPrometheusQueryExecutor( + map[string]string{ + "cpu_rate_by_container": "rate(container_cpu_usage_seconds_total{name=~\"chainlink.*\"}[5m])[30m:1m]", + }, + promConfig, +) +require.NoError(t, err) +``` + +--- + +### Using Custom Queries with `StandardReport` + +To use custom queries in a `StandardReport`, pass the custom `QueryExecutors` created above using the `WithQueryExecutors` option instead of the `StandardQueryExecutorType`: + +```go +report, err := benchspy.NewStandardReport( + "v1.2.3", + benchspy.WithQueryExecutors(directExecutor, lokiQueryExecutor, prometheusExecutor), + benchspy.WithGenerators(gen), +) +require.NoError(t, err, "failed to create baseline report") +``` \ No newline at end of file diff --git a/book/src/libs/wasp/benchspy/simplest_metrics.md b/book/src/libs/wasp/benchspy/simplest_metrics.md new file mode 100644 index 000000000..4669d2b28 --- /dev/null +++ b/book/src/libs/wasp/benchspy/simplest_metrics.md @@ -0,0 +1,57 @@ +# BenchSpy - Simplest Metrics + +As mentioned earlier, `BenchSpy` doesn't include any built-in comparison logic. It's up to you to decide how to compare metrics, as there are various ways to approach it and different data formats returned by queries. + +For example, if your query returns a time series, you could: +- Compare each data point in the time series individually. +- Compare aggregates like averages, medians, or min/max values of the time series. + +## Working with Built-in `QueryExecutors` +Each built-in `QueryExecutor` returns a different data type, and we use the `interface{}` type to reflect this. Since `Direct` executor always returns `float64` we have added a convenience function +that checks whether any of the standard metrics has **degraded** more than the threshold. If the performance has improved, no error will be returned. + +```go +hasErrors, errors := benchspy.CompareDirectWithThresholds( + // maximum differences in percentages for: + 1.0, // median latency + 1.0, // p95 latency + 1.0, // max latency + 1.0, // error rate + currentReport, + previousReport, +) +require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors)) +``` + +If there are errors they will be returned as `map[string][]errors`, where key is the name of a generator. + +> [!NOTE] +> Both `Direct` and `Loki` query executors support following standard performance metrics out of the box: +> - `median_latency` +> - `p95_latency` +> - `max_latency` +> - `error_rate` + +The function also prints a table with the differences between two reports, regardless whether they were meaningful: +```bash +Generator: vu1 +============== ++-------------------------+---------+---------+---------+ +| METRIC | V1 | V2 | DIFF % | ++-------------------------+---------+---------+---------+ +| median_latency | 50.1300 | 50.1179 | -0.0242 | ++-------------------------+---------+---------+---------+ +| 95th_percentile_latency | 50.7387 | 50.7622 | 0.0463 | ++-------------------------+---------+---------+---------+ +| max_latency | 55.7195 | 51.7248 | -7.1692 | ++-------------------------+---------+---------+---------+ +| error_rate | 0.0000 | 0.0000 | 0.0000 | ++-------------------------+---------+---------+---------+ +``` + +## Wrapping Up + +And that's it! You've written your first test that uses `WASP` to generate load and `BenchSpy` to ensure that the median latency, 95th percentile latency, max latency and error rate haven't changed significantly between runs. You accomplished this without even needing a Loki instance. But what if you wanted to leverage the power of `LogQL`? We'll explore that in the [next chapter](./loki_std.md). + +> [!NOTE] +> You can find the full example [here](https://github.com/smartcontractkit/chainlink-testing-framework/tree/main/wasp/examples/benchspy/direct_query_executor/direct_query_executor_test.go). \ No newline at end of file diff --git a/lib/client/loki.go b/lib/client/loki.go index 344dbf82f..5e538a622 100644 --- a/lib/client/loki.go +++ b/lib/client/loki.go @@ -171,7 +171,25 @@ func (lc *LokiClient) extractRawLogEntries(lokiResp LokiResponse) []LokiLogEntry for _, result := range lokiResp.Data.Result { for _, entry := range result.Values { - timestamp := entry[0].(string) + if len(entry) != 2 { + lc.Logger.Error().Interface("Log entry", entry).Msgf("Error parsing log entry. Expected 2 elements, got %d", len(entry)) + continue + } + var timestamp string + if entry[0] == nil { + lc.Logger.Error().Msg("Error parsing timestamp. Entry at index 0, that should be a timestamp, is nil") + continue + } + if timestampString, ok := entry[0].(string); ok { + timestamp = timestampString + } else if timestampInt, ok := entry[0].(int); ok { + timestamp = fmt.Sprintf("%d", timestampInt) + } else if timestampFloat, ok := entry[0].(float64); ok { + timestamp = fmt.Sprintf("%f", timestampFloat) + } else { + lc.Logger.Error().Msgf("Error parsing timestamp. Expected string, int, or float64, got %T", entry[0]) + continue + } logLine := entry[1].(string) logEntries = append(logEntries, LokiLogEntry{ Timestamp: timestamp, diff --git a/wasp/.gitignore b/wasp/.gitignore index 12f57355e..5e4d56469 100644 --- a/wasp/.gitignore +++ b/wasp/.gitignore @@ -2,7 +2,7 @@ bin/ .vscode/ .idea/ .direnv/ - +performance_reports/ k3dvolume/ .private.env .envrc.ci diff --git a/wasp/Makefile b/wasp/Makefile index fc3260502..d594aa256 100644 --- a/wasp/Makefile +++ b/wasp/Makefile @@ -6,6 +6,10 @@ test: test_race: go test -v -race -count 1 `go list ./... | grep -v examples` -run TestSmoke +.PHONY: test_benchspy_race +test_benchspy_race: + @./scripts/run_benchspy_tests.sh + .PHONY: test_bench test_bench: go test -bench=. -benchmem -count 1 -run=^# diff --git a/wasp/benchspy/TO_DO.md b/wasp/benchspy/TO_DO.md new file mode 100644 index 000000000..825d9bee4 --- /dev/null +++ b/wasp/benchspy/TO_DO.md @@ -0,0 +1,9 @@ +Known things to do: +- [ ] add logger +- [x] add unit tests for prometheus +- [x] add wasp test for prometheus only +- [ ] add e2e OCRv2 test with CTFv2 +- [x] write documentation +- [ ] add report builder (?) +- [x] add wrapper function for executing some code and then creating a report +- [ ] add helper method for a profile what would create a report based on all generators? \ No newline at end of file diff --git a/wasp/benchspy/basic.go b/wasp/benchspy/basic.go new file mode 100644 index 000000000..f466ab33f --- /dev/null +++ b/wasp/benchspy/basic.go @@ -0,0 +1,186 @@ +package benchspy + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" +) + +// BasicData is the basic data that is required for a report, common to all reports +type BasicData struct { + TestName string `json:"test_name"` + CommitOrTag string `json:"commit_or_tag"` + + // Test metrics + TestStart time.Time `json:"test_start_timestamp"` + TestEnd time.Time `json:"test_end_timestamp"` + + // all, generator settings, including segments + GeneratorConfigs map[string]*wasp.Config `json:"generator_configs"` +} + +// MustNewBasicData creates a new BasicData instance from a commit or tag. +// It panics if the creation fails, ensuring that the caller receives a valid instance. +func MustNewBasicData(commitOrTag string, generators ...*wasp.Generator) BasicData { + b, err := NewBasicData(commitOrTag, generators...) + if err != nil { + panic(err) + } + + return *b +} + +// NewBasicData creates a new BasicData instance using the provided commit or tag and a list of generators. +// It ensures that at least one generator is provided and that it is associated with a testing.T instance. +// This function is essential for initializing test data configurations in a structured manner. +func NewBasicData(commitOrTag string, generators ...*wasp.Generator) (*BasicData, error) { + if len(generators) == 0 { + return nil, errors.New("at least one generator is required") + } + + if generators[0].Cfg.T == nil { + return nil, errors.New("generators are not associated with a testing.T instance. Please set it as generator.Cfg.T and try again") + } + + b := &BasicData{ + TestName: generators[0].Cfg.T.Name(), + CommitOrTag: commitOrTag, + GeneratorConfigs: make(map[string]*wasp.Config), + } + + for _, g := range generators { + b.GeneratorConfigs[g.Cfg.GenName] = g.Cfg + } + + timeErr := b.FillStartEndTimes() + if timeErr != nil { + return nil, timeErr + } + + return b, nil +} + +// FillStartEndTimes calculates the earliest start time and latest end time from generator schedules. +// It updates the BasicData instance with these times, ensuring all segments have valid start and end times. +func (b *BasicData) FillStartEndTimes() error { + earliestTime := time.Now() + var latestTime time.Time + + for _, cfg := range b.GeneratorConfigs { + if len(cfg.Schedule) == 0 { + return fmt.Errorf("schedule is empty for generator %s", cfg.GenName) + } + + for _, segment := range cfg.Schedule { + if segment.StartTime.IsZero() { + return fmt.Errorf("start time is missing in one of the segments belonging to generator %s. Did that generator run?", cfg.GenName) + } + if segment.StartTime.Before(earliestTime) { + earliestTime = segment.StartTime + } + if segment.EndTime.IsZero() { + return fmt.Errorf("end time is missing in one of the segments belonging to generator %s. Did that generator finish running?", cfg.GenName) + } + if segment.EndTime.After(latestTime) { + latestTime = segment.EndTime + } + } + } + + b.TestStart = earliestTime + b.TestEnd = latestTime + + return nil +} + +// Validate checks the integrity of the BasicData fields, ensuring that the test start and end times are set, +// and that at least one generator configuration is provided. It returns an error if any of these conditions are not met. +func (b *BasicData) Validate() error { + if b.TestStart.IsZero() { + return errors.New("test start time is missing. We cannot query Loki without a time range. Please set it and try again") + } + if b.TestEnd.IsZero() { + return errors.New("test end time is missing. We cannot query Loki without a time range. Please set it and try again") + } + + if len(b.GeneratorConfigs) == 0 { + return errors.New("generator configs are missing. At least one is required. Please set them and try again") + } + + return nil +} + +// IsComparable checks if two BasicData instances have the same configuration settings. +// It validates the count, presence, and equivalence of generator configurations, +// returning an error if any discrepancies are found. This function is useful for ensuring +// consistency between data reports before processing or comparison. +func (b *BasicData) IsComparable(otherData BasicData) error { + // are all configs present? do they have the same schedule type? do they have the same segments? is call timeout the same? is rate limit timeout the same? + if len(b.GeneratorConfigs) != len(otherData.GeneratorConfigs) { + return fmt.Errorf("generator configs count is different. Expected %d, got %d", len(b.GeneratorConfigs), len(otherData.GeneratorConfigs)) + } + + for name1, cfg1 := range b.GeneratorConfigs { + if cfg2, ok := otherData.GeneratorConfigs[name1]; !ok { + return fmt.Errorf("generator config %s is missing from the other report", name1) + } else { + if err := compareGeneratorConfigs(cfg1, cfg2); err != nil { + return err + } + } + } + + return nil +} + +func compareGeneratorConfigs(cfg1, cfg2 *wasp.Config) error { + if cfg1.GenName != cfg2.GenName { + return fmt.Errorf("generator names are different. Expected %s, got %s", cfg1.GenName, cfg2.GenName) + } + if cfg1.LoadType != cfg2.LoadType { + return fmt.Errorf("load types are different. Expected %s, got %s", cfg1.LoadType, cfg2.LoadType) + } + + if len(cfg1.Schedule) != len(cfg2.Schedule) { + return fmt.Errorf("schedules are different. Expected %d, got %d", len(cfg1.Schedule), len(cfg2.Schedule)) + } + + var areSegmentsEqual = func(segment1, segment2 *wasp.Segment) bool { + return segment1.From == segment2.From && segment1.Duration == segment2.Duration && segment1.Type == segment2.Type + } + + for i, segment1 := range cfg1.Schedule { + segment2 := cfg2.Schedule[i] + if segment1 == nil { + return fmt.Errorf("segment at index %d is nil in the current report", i) + } + if segment2 == nil { + return fmt.Errorf("segment at index %d is nil in the other report", i) + } + if !areSegmentsEqual(segment1, segment2) { + return fmt.Errorf("segments at index %d are different. Expected %s segment(s), got %s segment(s)", i, mustMarshallSegment(segment1), mustMarshallSegment(segment2)) + } + } + + if cfg1.CallTimeout != cfg2.CallTimeout { + return fmt.Errorf("call timeouts are different. Expected %s, got %s", cfg1.CallTimeout, cfg2.CallTimeout) + } + + if cfg1.RateLimitUnitDuration != cfg2.RateLimitUnitDuration { + return fmt.Errorf("rate limit unit durations are different. Expected %s, got %s", cfg1.RateLimitUnitDuration, cfg2.RateLimitUnitDuration) + } + + return nil +} + +func mustMarshallSegment(segment *wasp.Segment) string { + segmentBytes, err := json.MarshalIndent(segment, "", " ") + if err != nil { + panic(err) + } + + return string(segmentBytes) +} diff --git a/wasp/benchspy/basic_test.go b/wasp/benchspy/basic_test.go new file mode 100644 index 000000000..0d7b45086 --- /dev/null +++ b/wasp/benchspy/basic_test.go @@ -0,0 +1,475 @@ +package benchspy + +import ( + "testing" + "time" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBenchSpy_NewBasicData(t *testing.T) { + testName := t.Name() + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{{From: 1, Duration: time.Hour, StartTime: time.Now().Add(-time.Hour), EndTime: time.Now()}}, + }, + } + + tests := []struct { + name string + commitOrTag string + generators []*wasp.Generator + wantErr bool + errMsg string + }{ + { + name: "valid basic data", + commitOrTag: "abc123", + generators: []*wasp.Generator{gen}, + wantErr: false, + }, + { + name: "no generators", + commitOrTag: "abc123", + generators: []*wasp.Generator{}, + wantErr: true, + errMsg: "at least one generator is required", + }, + { + name: "generator without testing.T", + commitOrTag: "abc123", + generators: []*wasp.Generator{{Cfg: &wasp.Config{GenName: "gen1"}}}, + wantErr: true, + errMsg: "generators are not associated with a testing.T instance", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bd, err := NewBasicData(tt.commitOrTag, tt.generators...) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + assert.Equal(t, tt.commitOrTag, bd.CommitOrTag) + assert.Equal(t, testName, bd.TestName) + assert.Len(t, bd.GeneratorConfigs, len(tt.generators)) + }) + } +} + +func TestBenchSpy_MustNewBasicData(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {From: 1, Duration: time.Hour, StartTime: time.Now().Add(-time.Hour), EndTime: time.Now()}, + }, + }, + } + + t.Run("successful creation", func(t *testing.T) { + assert.NotPanics(t, func() { + bd := MustNewBasicData("abc123", gen) + assert.Equal(t, "abc123", bd.CommitOrTag) + }) + }) + + t.Run("panics on error", func(t *testing.T) { + assert.Panics(t, func() { + MustNewBasicData("abc123") // no generators + }) + }) +} + +func TestBenchSpy_BasicData_FillStartEndTimes(t *testing.T) { + now := time.Now() + + t.Run("successful fill", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {StartTime: now, EndTime: now.Add(time.Hour)}, + {StartTime: now.Add(2 * time.Hour), EndTime: now.Add(3 * time.Hour)}, + }, + }, + } + + bd, err := NewBasicData("abc123", gen) + require.NoError(t, err) + + err = bd.FillStartEndTimes() + require.NoError(t, err) + assert.Equal(t, now, bd.TestStart) + assert.Equal(t, now.Add(3*time.Hour), bd.TestEnd) + }) + + t.Run("error on missing start time", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {EndTime: now.Add(-time.Hour), Type: wasp.SegmentType_Plain, From: 1, Duration: time.Hour}, + }, + }, + } + + bd, err := NewBasicData("abc123", gen) + require.Nil(t, bd) + require.Error(t, err) + assert.Contains(t, err.Error(), "start time is missing") + }) + + t.Run("error on missing end time", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {StartTime: now.Add(time.Hour)}, + }, + }, + } + + bd, err := NewBasicData("abc123", gen) + require.Nil(t, bd) + require.Error(t, err) + assert.Contains(t, err.Error(), "end time is missing") + }) + + t.Run("error on empty schedule", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + }, + } + + bd, err := NewBasicData("abc123", gen) + require.Nil(t, bd) + require.Error(t, err) + assert.Contains(t, err.Error(), "schedule is empty for generator gen1") + }) +} + +func TestBenchSpy_BasicData_FillStartEndTimes_MultipleGenerators(t *testing.T) { + baseTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + gens []*wasp.Generator + wantStart time.Time + wantEnd time.Time + wantErr bool + errMsg string + }{ + { + name: "multiple generators with different schedules", + gens: []*wasp.Generator{ + { + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(2 * time.Hour)}, + }, + }, + }, + { + Cfg: &wasp.Config{ + T: t, + GenName: "gen2", + Schedule: []*wasp.Segment{ + {StartTime: baseTime.Add(time.Hour), EndTime: baseTime.Add(3 * time.Hour)}, + }, + }, + }, + }, + wantStart: baseTime, + wantEnd: baseTime.Add(3 * time.Hour), + wantErr: false, + }, + { + name: "multiple segments in one generator", + gens: []*wasp.Generator{ + { + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + Schedule: []*wasp.Segment{ + {StartTime: baseTime.Add(2 * time.Hour), EndTime: baseTime.Add(3 * time.Hour)}, + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + }, + }, + }, + wantStart: baseTime, + wantEnd: baseTime.Add(3 * time.Hour), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bd, err := NewBasicData("test-commit", tt.gens...) + require.NoError(t, err) + + err = bd.FillStartEndTimes() + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + return + } + + require.NoError(t, err) + }) + } +} + +func TestBenchSpy_TestBasicData_Validate(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "gen1", + }, + } + + tests := []struct { + name string + bd *BasicData + wantErr bool + errMsg string + }{ + { + name: "valid data", + bd: &BasicData{ + TestStart: time.Now(), + TestEnd: time.Now().Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"gen1": gen.Cfg}, + }, + wantErr: false, + }, + { + name: "missing start time", + bd: &BasicData{ + TestEnd: time.Now().Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"gen1": gen.Cfg}, + }, + wantErr: true, + errMsg: "test start time is missing", + }, + { + name: "missing generator configs", + bd: &BasicData{ + TestStart: time.Now(), + TestEnd: time.Now().Add(time.Hour), + }, + wantErr: true, + errMsg: "generator configs are missing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.bd.Validate() + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + }) + } +} + +func TestBenchSpy_BasicData_IsComparable(t *testing.T) { + baseConfig := &wasp.Config{ + T: t, + GenName: "gen1", + LoadType: wasp.VU, + Schedule: []*wasp.Segment{ + {From: 1, Duration: time.Hour}, + }, + CallTimeout: time.Second, + } + + tests := []struct { + name string + bd1 BasicData + bd2 BasicData + wantErr bool + errMsg string + }{ + { + name: "identical configs", + bd1: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + }}, + bd2: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + }}, + wantErr: false, + }, + { + name: "different generator count", + bd1: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + }}, + bd2: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + "gen2": baseConfig, + }}, + wantErr: true, + errMsg: "generator configs count is different", + }, + { + name: "other report is missing a generator #1", + bd1: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + }}, + bd2: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen2": baseConfig, + }}, + wantErr: true, + errMsg: "generator config gen1 is missing from the other report", + }, + { + name: "other report is missing a generator #2", + bd1: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen1": baseConfig, + "gen2": baseConfig, + }}, + bd2: BasicData{GeneratorConfigs: map[string]*wasp.Config{ + "gen2": baseConfig, + "gen3": baseConfig, + }}, + wantErr: true, + errMsg: "generator config gen1 is missing from the other report", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.bd1.IsComparable(tt.bd2) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + }) + } +} + +func TestBenchSpy_compareGeneratorConfigs(t *testing.T) { + baseConfig := &wasp.Config{ + GenName: "gen", + LoadType: wasp.VU, + Schedule: []*wasp.Segment{ + {From: 1, Duration: time.Hour, Type: wasp.SegmentType_Plain}, + }, + CallTimeout: time.Second, + } + + t.Run("identical configs", func(t *testing.T) { + err := compareGeneratorConfigs(baseConfig, baseConfig) + require.NoError(t, err) + }) + + t.Run("different generator names", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.GenName = "gen2" + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "generator names are different. Expected gen, got gen") + }) + + t.Run("different segment load", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.Schedule = []*wasp.Segment{ + {From: 2, Duration: time.Hour, Type: wasp.SegmentType_Plain}, + } + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "segments at index 0 are different") + }) + + t.Run("different segment duration", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.Schedule = []*wasp.Segment{ + {From: 1, Duration: time.Minute, Type: wasp.SegmentType_Plain}, + } + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "segments at index 0 are different") + }) + + t.Run("different segment type", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.Schedule = []*wasp.Segment{ + {From: 1, Duration: time.Hour, Type: wasp.SegmentType_Steps}, + } + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "segments at index 0 are different") + }) + + t.Run("different segment count", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.Schedule = []*wasp.Segment{ + {From: 2, Duration: time.Hour}, + {From: 3, Duration: time.Hour}, + } + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "schedules are different. Expected 1, got 2") + }) + + t.Run("different load types", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.LoadType = wasp.RPS + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "load types are different") + }) + + t.Run("different callTimeout", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.CallTimeout = time.Minute + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "call timeouts are different. Expected 1s, got 1m0s") + }) + + t.Run("different rate limit duration", func(t *testing.T) { + cfg2 := *baseConfig + cfg2.RateLimitUnitDuration = time.Minute + err := compareGeneratorConfigs(baseConfig, &cfg2) + require.Error(t, err) + assert.Contains(t, err.Error(), "rate limit unit durations are different. Expected 0s, got 1m0s") + }) +} + +func TestBenchSpy_mustMarshallSegment(t *testing.T) { + segment := &wasp.Segment{ + From: 1, + Duration: time.Hour, + } + + t.Run("successful marshal", func(t *testing.T) { + assert.NotPanics(t, func() { + result := mustMarshallSegment(segment) + assert.Contains(t, result, `"from": 1`) + }) + }) +} diff --git a/wasp/benchspy/direct.go b/wasp/benchspy/direct.go new file mode 100644 index 000000000..abb980c67 --- /dev/null +++ b/wasp/benchspy/direct.go @@ -0,0 +1,304 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/montanaflynn/stats" + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" +) + +type DirectQueryFn = func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) + +type DirectQueryExecutor struct { + KindName string `json:"kind"` + Generator *wasp.Generator `json:"generator_config"` + Queries map[string]DirectQueryFn `json:"queries"` + QueryResults map[string]interface{} `json:"query_results"` +} + +// NewStandardDirectQueryExecutor creates a new DirectQueryExecutor configured for standard queries. +// It initializes the executor and generates the necessary queries, returning the executor or an error if the process fails. +func NewStandardDirectQueryExecutor(generator *wasp.Generator) (*DirectQueryExecutor, error) { + g := &DirectQueryExecutor{ + KindName: string(StandardQueryExecutor_Direct), + } + + queries, err := g.generateStandardQueries() + if err != nil { + return nil, err + } + + return NewDirectQueryExecutor(generator, queries) +} + +// NewDirectQueryExecutor creates a new DirectQueryExecutor with the specified generator and query functions. +// It initializes the executor with a kind name and prepares a map for query results, enabling efficient query execution. +func NewDirectQueryExecutor(generator *wasp.Generator, queries map[string]DirectQueryFn) (*DirectQueryExecutor, error) { + g := &DirectQueryExecutor{ + KindName: string(StandardQueryExecutor_Direct), + Generator: generator, + Queries: queries, + QueryResults: make(map[string]interface{}), + } + + return g, nil +} + +// GeneratorName returns the name of the generator associated with the query executor. +// It is useful for identifying and categorizing results based on their generator type. +func (g *DirectQueryExecutor) GeneratorName() string { + return g.Generator.Cfg.GenName +} + +// Results returns the query results as a map of string keys to interface{} values. +// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions. +func (g *DirectQueryExecutor) Results() map[string]interface{} { + return g.QueryResults +} + +// Kind returns the type of the query executor as a string. +// It is useful for identifying the specific implementation of a query executor in a collection. +func (l *DirectQueryExecutor) Kind() string { + return l.KindName +} + +// IsComparable checks if the given QueryExecutor is of the same type and has comparable configurations. +// It returns an error if the types do not match or if the configurations are not comparable. +func (g *DirectQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { + otherType := reflect.TypeOf(otherQueryExecutor) + + if otherType != reflect.TypeOf(g) { + return fmt.Errorf("expected type %s, got %s", reflect.TypeOf(g), otherType) + } + + otherGeneratorQueryExecutor := otherQueryExecutor.(*DirectQueryExecutor) + + if compareGeneratorConfigs(g.Generator.Cfg, otherGeneratorQueryExecutor.Generator.Cfg) != nil { + return errors.New("generators are not comparable") + } + + return g.compareQueries(otherGeneratorQueryExecutor.Queries) +} + +func (l *DirectQueryExecutor) compareQueries(other map[string]DirectQueryFn) error { + this := l.Queries + if len(this) != len(other) { + return fmt.Errorf("queries count is different. Expected %d, got %d", len(this), len(other)) + } + + for name1 := range this { + if _, ok := other[name1]; !ok { + return fmt.Errorf("query %s is missing from the other report", name1) + } + } + + return nil +} + +// Validate checks if the query executor is properly configured. +// It ensures that a generator is set and at least one query is provided. +// Returns an error if validation fails, helping to prevent execution issues. +func (g *DirectQueryExecutor) Validate() error { + if g.Generator == nil { + return errors.New("generator is not set") + } + + if len(g.Queries) == 0 { + return errors.New("at least one query is needed") + } + + return nil +} + +// Execute runs the defined queries using the data from the generator. +// It validates the generator's data and aggregates responses before executing each query. +// This function is essential for processing and retrieving results from multiple queries concurrently. +func (g *DirectQueryExecutor) Execute(_ context.Context) error { + if g.Generator == nil { + return errors.New("generator is not set") + } + + for queryName, queryFunction := range g.Queries { + if g.Generator.GetData() == nil { + return fmt.Errorf("generator %s has no data", g.Generator.Cfg.GenName) + } + length := len(g.Generator.GetData().FailResponses.Data) + len(g.Generator.GetData().OKData.Data) + allResponses := wasp.NewSliceBuffer[*wasp.Response](length) + + for _, response := range g.Generator.GetData().OKResponses.Data { + allResponses.Append(response) + } + + for _, response := range g.Generator.GetData().FailResponses.Data { + allResponses.Append(response) + } + + if len(allResponses.Data) == 0 { + return fmt.Errorf("no responses found for generator %s", g.Generator.Cfg.GenName) + } + + results, queryErr := queryFunction(allResponses) + if queryErr != nil { + return queryErr + } + + g.QueryResults[queryName] = results + } + + return nil +} + +// TimeRange ensures that the query executor operates within the specified time range. +// It is a no-op for executors that already have responses stored in the correct time range. +func (g *DirectQueryExecutor) TimeRange(_, _ time.Time) { + // nothing to do here, since all responses stored in the generator are already in the right time range +} + +func (g *DirectQueryExecutor) generateStandardQueries() (map[string]DirectQueryFn, error) { + standardQueries := make(map[string]DirectQueryFn) + + for _, metric := range StandardLoadMetrics { + query, err := g.standardQuery(metric) + if err != nil { + return nil, err + } + standardQueries[string(metric)] = query + } + + return standardQueries, nil +} + +func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) (DirectQueryFn, error) { + var responsesToDurationFn = func(responses *wasp.SliceBuffer[*wasp.Response]) []float64 { + var asMiliDuration []float64 + for _, response := range responses.Data { + // get duration as nanoseconds and convert to milliseconds in order to not lose precision + // otherwise, the duration will be rounded to the nearest millisecond + asMiliDuration = append(asMiliDuration, float64(response.Duration.Nanoseconds())/1_000_000) + } + + return asMiliDuration + } + + var calculateFailureRateFn = func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + if len(responses.Data) == 0 { + return 0, nil + } + + failedCount := 0.0 + successfulCount := 0.0 + for _, response := range responses.Data { + if response.Failed || response.Timeout { + failedCount = failedCount + 1 + } else { + successfulCount = successfulCount + 1 + } + } + + return failedCount / (failedCount + successfulCount), nil + } + + switch standardMetric { + case MedianLatency: + medianFn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + return stats.Median(responsesToDurationFn(responses)) + } + return medianFn, nil + case Percentile95Latency: + p95Fn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + return stats.Percentile(responsesToDurationFn(responses), 95) + } + return p95Fn, nil + case MaxLatency: + maxFn := func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + return stats.Max(responsesToDurationFn(responses)) + } + return maxFn, nil + case ErrorRate: + return calculateFailureRateFn, nil + default: + return nil, fmt.Errorf("unsupported standard metric %s", standardMetric) + } +} + +// MarshalJSON customizes the JSON representation of the DirectQueryExecutor. +// It serializes only the relevant fields, including query names and results, +// making it suitable for efficient data transmission and storage. +func (g *DirectQueryExecutor) MarshalJSON() ([]byte, error) { + // we need custom marshalling to only include query names, since the functions are not serializable + type QueryExecutor struct { + Kind string `json:"kind"` + Generator interface{} `json:"generator_config"` + Queries []string `json:"queries"` + QueryResults map[string]interface{} `json:"query_results"` + } + + return json.Marshal(&QueryExecutor{ + Kind: g.KindName, + Generator: func() interface{} { + if g.Generator != nil { + return g.Generator.Cfg + } + return nil + }(), + Queries: func() []string { + keys := make([]string, 0, len(g.Queries)) + for k := range g.Queries { + keys = append(keys, k) + } + return keys + }(), + QueryResults: g.QueryResults, + }) +} + +// UnmarshalJSON decodes JSON data into a DirectQueryExecutor instance. +// It populates the executor's fields, including queries and results, +// enabling seamless integration of JSON configurations into the executor's structure. +func (g *DirectQueryExecutor) UnmarshalJSON(data []byte) error { + // helper struct with QueryExecutors as json.RawMessage and QueryResults as map[string]interface{} + // and as actual types + type Alias DirectQueryExecutor + var raw struct { + Alias + GeneratorCfg wasp.Config `json:"generator_config"` + Queries []json.RawMessage `json:"queries"` + QueryResults map[string]interface{} `json:"query_results"` + } + + // unmarshal into the helper struct to populate other fields automatically + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + queries := make(map[string]DirectQueryFn) + + // unmarshall only query names + for _, rawQuery := range raw.Queries { + var queryName string + if err := json.Unmarshal(rawQuery, &queryName); err != nil { + return err + } + + queries[queryName] = nil + } + + // convert map[string]interface{} to map[string]actualType + convertedTypes, conversionErr := convertQueryResults(raw.QueryResults) + if conversionErr != nil { + return conversionErr + } + + *g = DirectQueryExecutor(raw.Alias) + g.Queries = queries + g.QueryResults = convertedTypes + g.Generator = &wasp.Generator{ + Cfg: &raw.GeneratorCfg, + } + return nil +} diff --git a/wasp/benchspy/direct_test.go b/wasp/benchspy/direct_test.go new file mode 100644 index 000000000..ca23d76cf --- /dev/null +++ b/wasp/benchspy/direct_test.go @@ -0,0 +1,389 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBenchSpy_NewDirectQueryExecutor(t *testing.T) { + t.Run("success case", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + GenName: "my_gen", + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: time.Minute, + }, + }, + }, + } + executor, err := NewStandardDirectQueryExecutor(gen) + assert.NoError(t, err) + assert.Equal(t, "direct", executor.KindName) + assert.Equal(t, gen, executor.Generator) + assert.NotEmpty(t, executor.Queries) + assert.NotNil(t, executor.QueryResults) + + err = executor.Validate() + assert.NoError(t, err) + }) + + t.Run("nil generator", func(t *testing.T) { + executor, err := NewStandardDirectQueryExecutor(nil) + assert.NoError(t, err) + + err = executor.Validate() + assert.Error(t, err) + }) +} + +func TestBenchSpy_DirectQueryExecutor_Results(t *testing.T) { + expected := map[string]interface{}{ + "test": "result", + } + executor := &DirectQueryExecutor{ + QueryResults: expected, + } + assert.Equal(t, expected, executor.Results()) +} + +func TestBenchSpy_DirectQueryExecutor_IsComparable(t *testing.T) { + baseGen := &wasp.Generator{ + Cfg: &wasp.Config{ + GenName: "my_gen", + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: time.Minute, + }, + }, + }, + } + + t.Run("same configs", func(t *testing.T) { + exec1, _ := NewStandardDirectQueryExecutor(baseGen) + exec2, _ := NewStandardDirectQueryExecutor(baseGen) + assert.NoError(t, exec1.IsComparable(exec2)) + }) + + t.Run("different query count", func(t *testing.T) { + exec1, _ := NewStandardDirectQueryExecutor(baseGen) + exec2, _ := NewStandardDirectQueryExecutor(baseGen) + exec2.Queries = map[string]DirectQueryFn{"test": nil} + + assert.Error(t, exec1.IsComparable(exec2)) + }) + + t.Run("different queries", func(t *testing.T) { + exec1, _ := NewStandardDirectQueryExecutor(baseGen) + exec2, _ := NewStandardDirectQueryExecutor(baseGen) + exec2.Queries = map[string]DirectQueryFn{"test": nil, "test2": nil, "test3": nil} + + assert.Error(t, exec1.IsComparable(exec2)) + }) + + t.Run("different configs", func(t *testing.T) { + exec1, _ := NewStandardDirectQueryExecutor(baseGen) + differentGen := &wasp.Generator{ + Cfg: &wasp.Config{ + GenName: "my_gen", + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 2, + Duration: time.Minute, + }, + }, + }, + } + exec2, _ := NewStandardDirectQueryExecutor(differentGen) + assert.Error(t, exec1.IsComparable(exec2)) + }) + + t.Run("different types", func(t *testing.T) { + exec1, _ := NewStandardDirectQueryExecutor(baseGen) + exec2 := &LokiQueryExecutor{} + assert.Error(t, exec1.IsComparable(exec2)) + }) +} + +func TestBenchSpy_DirectQueryExecutor_Validate(t *testing.T) { + t.Run("valid case", func(t *testing.T) { + executor, _ := NewStandardDirectQueryExecutor(&wasp.Generator{ + Cfg: &wasp.Config{}, + }) + assert.NoError(t, executor.Validate()) + }) + + t.Run("missing generator", func(t *testing.T) { + executor := &DirectQueryExecutor{ + Queries: map[string]DirectQueryFn{"test": nil}, + } + assert.Error(t, executor.Validate()) + }) + + t.Run("empty queries", func(t *testing.T) { + executor := &DirectQueryExecutor{ + Generator: &wasp.Generator{}, + Queries: map[string]DirectQueryFn{}, + } + assert.Error(t, executor.Validate()) + }) +} + +type fakeGun struct { + maxSuccesses int + successCounter int + maxFailures int + failureCounter int + successMutex sync.Mutex + failureMutex sync.Mutex + + schedule *wasp.Segment +} + +func (f *fakeGun) Call(l *wasp.Generator) *wasp.Response { + now := time.Now() + f.successMutex.Lock() + defer f.successMutex.Unlock() + if f.successCounter < f.maxSuccesses { + f.successCounter++ + d := time.Duration(150) * time.Millisecond + time.Sleep(d) + finishedAt := time.Now() + return &wasp.Response{ + StartedAt: &now, + Duration: d, + FinishedAt: &finishedAt, + Failed: false, + Data: fmt.Sprintf("success[%d]", f.successCounter), + } + } + + f.failureMutex.Lock() + defer f.failureMutex.Unlock() + if f.failureCounter < f.maxFailures { + f.failureCounter++ + d := time.Duration(200) * time.Millisecond + time.Sleep(d) + finishedAt := time.Now() + return &wasp.Response{ + StartedAt: &now, + Duration: d, + FinishedAt: &finishedAt, + Failed: true, + Data: fmt.Sprintf("failure[%d]", f.failureCounter), + } + } + + panic(fmt.Sprintf("fakeGun.Call called too many times (%d vs %d). Expected maxFailures: %d. Expected maxSuccesses: %d. do adjust your settings (sum of maxSuccesses and maxFilure should be greater than the duration of the segment in seconds)", f.maxFailures+f.maxSuccesses, f.maxFailures+f.maxSuccesses+1, f.maxFailures, f.maxSuccesses)) +} + +func TestBenchSpy_DirectQueryExecutor_Execute(t *testing.T) { + t.Run("success case with mixed responses", func(t *testing.T) { + cfg := &wasp.Config{ + GenName: "my_gen", + LoadType: wasp.RPS, + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: 5 * time.Second, + }, + }, + } + + fakeGun := &fakeGun{ + maxSuccesses: 4, + maxFailures: 3, + schedule: cfg.Schedule[0], + } + + cfg.Gun = fakeGun + + gen, err := wasp.NewGenerator(cfg) + require.NoError(t, err) + + gen.Run(true) + + require.Equal(t, 4, len(gen.GetData().OKResponses.Data), "expected 4 successful responses") + require.GreaterOrEqual(t, len(gen.GetData().FailResponses.Data), 2, "expected >=2 failed responses") + + actualFailures := len(gen.GetData().FailResponses.Data) + + executor, err := NewStandardDirectQueryExecutor(gen) + assert.NoError(t, err) + + err = executor.Execute(context.Background()) + assert.NoError(t, err) + + results := executor.Results() + assert.NotEmpty(t, results) + + // 4 responses with ~150ms latency (150ms sleep + some execution overhead) + // and 2-3 responses with ~200ms latency (200ms sleep + some execution overhead) + // expected median latency: (150ms, 151ms> + resultsAsFloats, err := ResultsAs(0.0, executor, string(MedianLatency), string(Percentile95Latency), string(ErrorRate)) + assert.NoError(t, err) + require.Equal(t, 3, len(resultsAsFloats)) + require.InDelta(t, 151.0, resultsAsFloats[string(MedianLatency)], 1.0) + + // since we have 2-3 responses with 200-201ms latency, the 95th percentile should be (200ms, 201ms> + require.InDelta(t, 201.0, resultsAsFloats[string(Percentile95Latency)], 1.0) + + errorRate, exists := resultsAsFloats[string(ErrorRate)] + assert.True(t, exists) + + // error rate is the number of failures divided by the total number of responses + expectedErrorRate := float64(actualFailures) / (float64(fakeGun.maxSuccesses) + float64(actualFailures)) + assert.Equal(t, expectedErrorRate, errorRate) + }) + + t.Run("all responses failed", func(t *testing.T) { + cfg := &wasp.Config{ + GenName: "my_gen", + LoadType: wasp.RPS, + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: 5 * time.Second, + }, + }, + } + + fakeGun := &fakeGun{ + maxSuccesses: 0, + maxFailures: 10, + schedule: cfg.Schedule[0], + } + + cfg.Gun = fakeGun + + gen, err := wasp.NewGenerator(cfg) + assert.NoError(t, err) + + gen.Run(true) + + require.Equal(t, 0, len(gen.GetData().OKResponses.Data), "expected 0 successful responses") + require.GreaterOrEqual(t, len(gen.GetData().FailResponses.Data), 5, "expected >=5 failed responses") + + executor, err := NewStandardDirectQueryExecutor(gen) + assert.NoError(t, err) + + err = executor.Execute(context.Background()) + assert.NoError(t, err) + + results := executor.Results() + assert.NotEmpty(t, results) + + errorRate, exists := results[string(ErrorRate)] + assert.True(t, exists) + assert.Equal(t, 1.0, errorRate) + }) + + t.Run("no responses", func(t *testing.T) { + cfg := &wasp.Config{ + GenName: "my_gen", + LoadType: wasp.RPS, + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: 5 * time.Second, + }, + }, + } + + fakeGun := &fakeGun{ + maxSuccesses: 0, + maxFailures: 0, + schedule: cfg.Schedule[0], + } + + cfg.Gun = fakeGun + gen, err := wasp.NewGenerator(cfg) + require.NoError(t, err) + + executor, _ := NewStandardDirectQueryExecutor(gen) + err = executor.Execute(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no responses found for generator") + }) +} + +func TestBenchSpy_DirectQueryExecutor_MarshalJSON(t *testing.T) { + t.Run("marshal/unmarshal round trip", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + GenName: "my_gen", + Schedule: []*wasp.Segment{ + { + Type: "plain", + From: 1, + Duration: time.Minute, + }, + }, + }, + } + original, _ := NewStandardDirectQueryExecutor(gen) + original.QueryResults["test"] = 2.0 + original.QueryResults["test2"] = 12.1 + + original.Queries = map[string]DirectQueryFn{ + "test": func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + return 2.0, nil + }, + "test2": func(responses *wasp.SliceBuffer[*wasp.Response]) (float64, error) { + return 12.1, nil + }, + } + + data, err := json.Marshal(original) + assert.NoError(t, err) + + var recovered DirectQueryExecutor + err = json.Unmarshal(data, &recovered) + assert.NoError(t, err) + + assert.Equal(t, original.KindName, recovered.KindName) + assert.Equal(t, original.QueryResults, recovered.QueryResults) + assert.Equal(t, len(original.Queries), len(recovered.Queries)) + }) + + t.Run("marshal with nil generator", func(t *testing.T) { + executor := &DirectQueryExecutor{} + data, err := json.Marshal(executor) + assert.NoError(t, err) + assert.Contains(t, string(data), `"generator_config":null`) + }) + + t.Run("unmarshal invalid JSON", func(t *testing.T) { + var executor DirectQueryExecutor + err := json.Unmarshal([]byte(`{invalid json}`), &executor) + assert.Error(t, err) + }) +} + +func TestBenchSpy_DirectQueryExecutor_TimeRange(t *testing.T) { + executor := &DirectQueryExecutor{} + start := time.Now() + end := start.Add(time.Hour) + + // TimeRange should not modify the executor + executor.TimeRange(start, end) + assert.NotPanics(t, func() { + executor.TimeRange(start, end) + }) +} diff --git a/wasp/benchspy/log.go b/wasp/benchspy/log.go new file mode 100644 index 000000000..f4295af64 --- /dev/null +++ b/wasp/benchspy/log.go @@ -0,0 +1,32 @@ +package benchspy + +import ( + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + LogLevelEnvVar = "BENCHSPY_LOG_LEVEL" +) + +var ( + L zerolog.Logger +) + +func init() { + initDefaultLogging() +} + +func initDefaultLogging() { + lvlStr := os.Getenv(LogLevelEnvVar) + if lvlStr == "" { + lvlStr = "info" + } + lvl, err := zerolog.ParseLevel(lvlStr) + if err != nil { + panic(err) + } + L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(lvl) +} diff --git a/wasp/benchspy/loki.go b/wasp/benchspy/loki.go new file mode 100644 index 000000000..d6204f968 --- /dev/null +++ b/wasp/benchspy/loki.go @@ -0,0 +1,299 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-testing-framework/lib/client" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "golang.org/x/sync/errgroup" +) + +// all metrics, but error rate are calculated over a 10s interval +var ( + Loki_MedianQuery = `quantile_over_time(0.5, {branch=~"%s", commit=~"%s", go_test_name=~"%s", test_data_type=~"responses", gen_name=~"%s"} | json| unwrap duration [10s]) by (go_test_name, gen_name) / 1e6` + Loki_95thQuery = `quantile_over_time(0.95, {branch=~"%s", commit=~"%s", go_test_name=~"%s", test_data_type=~"responses", gen_name=~"%s"} | json| unwrap duration [10s]) by (go_test_name, gen_name) / 1e6` + Loki_MaxQuery = `max(max_over_time({branch=~"%s", commit=~"%s", go_test_name=~"%s", test_data_type=~"responses", gen_name=~"%s"} | json| unwrap duration [10s]) by (go_test_name, gen_name) / 1e6)` + Loki_ErrorRate = `sum(max_over_time({branch=~"%s", commit=~"%s", go_test_name=~"%s", test_data_type=~"stats", gen_name=~"%s"} | json| unwrap failed [%s]) by (node_id, go_test_name, gen_name)) by (__stream_shard__)` +) + +// NewLokiQueryExecutor creates a new LokiQueryExecutor instance. +// It initializes the executor with the specified generator name, queries, and Loki configuration. +// This function is useful for setting up a query executor to interact with Loki for log data retrieval. +func NewLokiQueryExecutor(generatorName string, queries map[string]string, lokiConfig *wasp.LokiConfig) *LokiQueryExecutor { + return &LokiQueryExecutor{ + KindName: string(StandardQueryExecutor_Loki), + GeneratorNameString: generatorName, + Queries: queries, + Config: lokiConfig, + QueryResults: make(map[string]interface{}), + } +} + +type LokiQueryExecutor struct { + KindName string `json:"kind"` + GeneratorNameString string `json:"generator_name"` + + // Test metrics + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + + // a map of name to query template, ex: "average cpu usage": "avg(rate(cpu_usage_seconds_total[5m]))" + Queries map[string]string `json:"queries"` + // can be anything, avg RPS, amount of errors, 95th percentile of CPU utilization, etc + QueryResults map[string]interface{} `json:"query_results"` + + Config *wasp.LokiConfig `json:"-"` +} + +// GeneratorName returns the name of the generator associated with the LokiQueryExecutor. +// It is useful for identifying the source of results in reports or logs. +func (l *LokiQueryExecutor) GeneratorName() string { + return l.GeneratorNameString +} + +// Results returns the query results as a map of string to interface{}. +// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions. +func (l *LokiQueryExecutor) Results() map[string]interface{} { + return l.QueryResults +} + +// Kind returns the type of the query executor as a string. +// It is used to identify the specific kind of query executor in various operations. +func (l *LokiQueryExecutor) Kind() string { + return l.KindName +} + +// IsComparable checks if the given QueryExecutor is of the same type as the current instance. +// It compares the queries of both executors to ensure they are equivalent in structure and content. +// This function is useful for validating compatibility between different query executors. +func (l *LokiQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { + otherType := reflect.TypeOf(otherQueryExecutor) + + if otherType != reflect.TypeOf(l) { + return fmt.Errorf("expected type %s, got %s", reflect.TypeOf(l), otherType) + } + + otherAsLoki := otherQueryExecutor.(*LokiQueryExecutor) + if l.GeneratorNameString != otherAsLoki.GeneratorNameString { + return fmt.Errorf("generator name is different. Expected %s, got %s", l.GeneratorNameString, otherAsLoki.GeneratorNameString) + } + + return l.compareQueries(otherQueryExecutor.(*LokiQueryExecutor).Queries) +} + +// Validate checks if the LokiQueryExecutor has valid queries and configuration. +// It returns an error if no queries are set or if the configuration is missing, +// ensuring that the executor is ready for execution. +func (l *LokiQueryExecutor) Validate() error { + if len(l.Queries) == 0 { + return errors.New("there are no Loki queries, there's nothing to fetch. Please set them and try again") + } + if l.Config == nil { + return errors.New("loki config is missing. Please set it and try again") + } + if l.GeneratorNameString == "" { + return errors.New("generator name is missing. Please set it and try again") + } + + return nil +} + +// Execute runs the configured Loki queries concurrently and collects the results. +// It requires a valid configuration and handles basic authentication if provided. +// The function returns an error if any query execution fails or if the configuration is missing. +func (l *LokiQueryExecutor) Execute(ctx context.Context) error { + var basicAuth client.LokiBasicAuth + + if l.Config == nil { + return errors.New("loki config is missing. Please set it and try again") + } + + if l.Config.BasicAuth != "" { + splitAuth := strings.Split(l.Config.BasicAuth, ":") + if len(splitAuth) == 2 { + basicAuth = client.LokiBasicAuth{ + Login: splitAuth[0], + Password: splitAuth[1], + } + } + } + + l.QueryResults = make(map[string]interface{}) + resultCh := make(chan map[string][]string, len(l.Queries)) + errGroup, errCtx := errgroup.WithContext(ctx) + + for name, query := range l.Queries { + errGroup.Go(func() error { + queryParams := client.LokiQueryParams{ + Query: query, + StartTime: l.StartTime, + EndTime: l.EndTime, + Limit: 1000, //TODO make this configurable + } + + parsedLokiUrl, err := url.Parse(l.Config.URL) + if err != nil { + return errors.Wrapf(err, "failed to parse Loki URL %s", l.Config.URL) + } + + lokiUrl := parsedLokiUrl.Scheme + "://" + parsedLokiUrl.Host + lokiClient := client.NewLokiClient(lokiUrl, l.Config.TenantID, basicAuth, queryParams) + + rawLogs, err := lokiClient.QueryLogs(errCtx) + if err != nil { + return errors.Wrapf(err, "failed to query logs for %s", name) + } + + resultMap := make(map[string][]string) + for _, log := range rawLogs { + resultMap[name] = append(resultMap[name], log.Log) + } + + select { + case resultCh <- resultMap: + return nil + case <-errCtx.Done(): + return errCtx.Err() // Allows goroutine to exit if timeout occurs + } + }) + } + + if err := errGroup.Wait(); err != nil { + return errors.Wrap(err, "failed to execute Loki queries") + } + + for i := 0; i < len(l.Queries); i++ { + result := <-resultCh + for name, logs := range result { + l.QueryResults[name] = logs + } + } + + return nil +} + +func (l *LokiQueryExecutor) compareQueries(other map[string]string) error { + this := l.Queries + if len(this) != len(other) { + return fmt.Errorf("queries count is different. Expected %d, got %d", len(this), len(other)) + } + + for name1, query1 := range this { + if query2, ok := other[name1]; !ok { + return fmt.Errorf("query %s is missing from the other report", name1) + } else { + if query1 != query2 { + return fmt.Errorf("query %s is different. Expected %s, got %s", name1, query1, query2) + } + } + } + + return nil +} + +// TimeRange sets the start and end time for the Loki query execution. +// This function is essential for defining the time window of the data to be fetched. +func (l *LokiQueryExecutor) TimeRange(start, end time.Time) { + l.StartTime = start + l.EndTime = end +} + +// UnmarshalJSON parses the JSON-encoded data and populates the LokiQueryExecutor fields. +// It converts the query results from a generic map to a specific type map, enabling type-safe access to the results. +func (l *LokiQueryExecutor) UnmarshalJSON(data []byte) error { + // helper struct with QueryResults map[string]interface{} + type Alias LokiQueryExecutor + var raw struct { + Alias + QueryResults map[string]interface{} `json:"query_results"` + } + + // unmarshal into the helper struct to populate other fields automatically + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // convert map[string]interface{} to map[string]actualType + convertedTypes, conversionErr := convertQueryResults(raw.QueryResults) + if conversionErr != nil { + return conversionErr + } + + *l = LokiQueryExecutor(raw.Alias) + l.QueryResults = convertedTypes + return nil +} + +// NewStandardMetricsLokiExecutor creates a LokiQueryExecutor configured with standard metrics queries. +// It generates queries based on provided test parameters and time range, returning the executor or an error if query generation fails. +func NewStandardMetricsLokiExecutor(lokiConfig *wasp.LokiConfig, testName, generatorName, branch, commit string, startTime, endTime time.Time) (*LokiQueryExecutor, error) { + lq := &LokiQueryExecutor{ + KindName: string(StandardQueryExecutor_Loki), + GeneratorNameString: generatorName, + Config: lokiConfig, + QueryResults: make(map[string]interface{}), + } + + standardQueries, queryErr := lq.generateStandardQueries(testName, generatorName, branch, commit, startTime, endTime) + if queryErr != nil { + return nil, queryErr + } + + lq.Queries = standardQueries + + return lq, nil +} + +func (l *LokiQueryExecutor) standardQuery(standardMetric StandardLoadMetric, testName, generatorName, branch, commit string, startTime, endTime time.Time) (string, error) { + // if we decide to include only plain segments for the calculation, we we will need to execute this function for each of them + // and then aggregate the results + switch standardMetric { + case MedianLatency: + return fmt.Sprintf(Loki_MedianQuery, branch, commit, testName, generatorName), nil + case Percentile95Latency: + return fmt.Sprintf(Loki_95thQuery, branch, commit, testName, generatorName), nil + case MaxLatency: + return fmt.Sprintf(Loki_MaxQuery, branch, commit, testName, generatorName), nil + case ErrorRate: + queryRange := calculateTimeRange(startTime, endTime) + return fmt.Sprintf(Loki_ErrorRate, branch, commit, testName, generatorName, queryRange), nil + default: + return "", fmt.Errorf("unsupported standard metric %s", standardMetric) + } +} + +func (l *LokiQueryExecutor) generateStandardQueries(testName, generatorName, branch, commit string, startTime, endTime time.Time) (map[string]string, error) { + standardQueries := make(map[string]string) + + for _, metric := range StandardLoadMetrics { + query, err := l.standardQuery(metric, testName, generatorName, branch, commit, startTime, endTime) + if err != nil { + return nil, err + } + standardQueries[string(metric)] = query + } + + return standardQueries, nil +} + +func calculateTimeRange(startTime, endTime time.Time) string { + totalSeconds := int(endTime.Sub(startTime).Seconds()) + + var rangeStr string + if totalSeconds%3600 == 0 { // Exact hours + rangeStr = fmt.Sprintf("%dh", totalSeconds/3600) + } else if totalSeconds%60 == 0 { // Exact minutes + rangeStr = fmt.Sprintf("%dm", totalSeconds/60) + } else { // Use seconds for uneven intervals + rangeStr = fmt.Sprintf("%ds", totalSeconds) + } + + return rangeStr +} diff --git a/wasp/benchspy/loki_test.go b/wasp/benchspy/loki_test.go new file mode 100644 index 000000000..c9aae3af0 --- /dev/null +++ b/wasp/benchspy/loki_test.go @@ -0,0 +1,252 @@ +package benchspy + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/stretchr/testify/assert" +) + +func TestBenchSpy_NewLokiQueryExecutor(t *testing.T) { + queries := map[string]string{ + "query1": "test query 1", + "query2": "test query 2", + } + config := &wasp.LokiConfig{ + URL: "http://localhost:3100", + TenantID: "test", + BasicAuth: "user:pass", + } + + executor := NewLokiQueryExecutor("some_generator", queries, config) + assert.Equal(t, "loki", executor.KindName) + assert.Equal(t, queries, executor.Queries) + assert.Equal(t, config, executor.Config) + assert.NotNil(t, executor.QueryResults) +} + +func TestBenchSpy_LokiQueryExecutor_Results(t *testing.T) { + executor := &LokiQueryExecutor{ + QueryResults: map[string]interface{}{ + "test": []string{"result1", "result2"}, + }, + } + results := executor.Results() + assert.Equal(t, executor.QueryResults, results) +} + +type anotherQueryExecutor struct{} + +func (a *anotherQueryExecutor) Kind() string { + return "another" +} + +func (a *anotherQueryExecutor) Validate() error { + return nil +} +func (a *anotherQueryExecutor) Execute(_ context.Context) error { + return nil +} +func (a *anotherQueryExecutor) Results() map[string]interface{} { + return nil +} +func (a *anotherQueryExecutor) IsComparable(_ QueryExecutor) error { + return nil +} +func (a *anotherQueryExecutor) TimeRange(_, _ time.Time) { + +} + +func TestBenchSpy_LokiQueryExecutor_IsComparable(t *testing.T) { + executor1 := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q1": "query1"}, + } + executor2 := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q1": "query2"}, + } + executor3 := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q2": "query1"}, + } + executor4 := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q1": "query1", "q2": "query2"}, + } + executor5 := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q1": "query1", "q3": "query3"}, + } + executor6 := &LokiQueryExecutor{ + GeneratorNameString: "other", + Queries: map[string]string{"q1": "query1"}, + } + + t.Run("same queries", func(t *testing.T) { + err := executor1.IsComparable(executor1) + assert.NoError(t, err) + }) + + t.Run("different generator names", func(t *testing.T) { + err := executor1.IsComparable(executor6) + assert.Error(t, err) + }) + + t.Run("same queries, different names", func(t *testing.T) { + err := executor1.IsComparable(executor3) + assert.Error(t, err) + }) + + t.Run("same names, different queries", func(t *testing.T) { + err := executor1.IsComparable(executor2) + assert.Error(t, err) + }) + + t.Run("different types", func(t *testing.T) { + invalidExector := &anotherQueryExecutor{} + + err := executor1.IsComparable(invalidExector) + assert.Error(t, err) + }) + + t.Run("different query count", func(t *testing.T) { + err := executor1.IsComparable(executor4) + assert.Error(t, err) + }) + + t.Run("missing query", func(t *testing.T) { + err := executor4.IsComparable(executor5) + assert.Error(t, err) + }) +} + +func TestBenchSpy_LokiQueryExecutor_Validate(t *testing.T) { + t.Run("valid configuration", func(t *testing.T) { + executor := &LokiQueryExecutor{ + GeneratorNameString: "generator", + Queries: map[string]string{"q1": "query1"}, + Config: &wasp.LokiConfig{}, + } + err := executor.Validate() + assert.NoError(t, err) + }) + + t.Run("missing generator name", func(t *testing.T) { + executor := &LokiQueryExecutor{ + Config: &wasp.LokiConfig{}, + } + err := executor.Validate() + assert.Error(t, err) + }) + + t.Run("missing queries", func(t *testing.T) { + executor := &LokiQueryExecutor{ + Config: &wasp.LokiConfig{}, + } + err := executor.Validate() + assert.Error(t, err) + }) + + t.Run("missing config", func(t *testing.T) { + executor := &LokiQueryExecutor{ + Queries: map[string]string{"q1": "query1"}, + } + err := executor.Validate() + assert.Error(t, err) + }) +} + +func TestBenchSpy_LokiQueryExecutor_Execute(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/loki/api/v1/query_range", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{ + "data": { + "result": [ + { + "stream": {"namespace": "test"}, + "values": [["1234567890", "Log message 1"]] + } + ] + } + }`)) + assert.NoError(t, err) + })) + defer mockServer.Close() + + executor := &LokiQueryExecutor{ + Queries: map[string]string{"test_query": "test"}, + Config: &wasp.LokiConfig{ + URL: mockServer.URL, + TenantID: "test", + BasicAuth: "user:pass", + }, + StartTime: time.Now().Add(-1 * time.Hour), + EndTime: time.Now(), + } + + err := executor.Execute(context.Background()) + assert.NoError(t, err) + assert.Contains(t, executor.QueryResults, "test_query") + + asStringSlice, ok := executor.QueryResults["test_query"].([]string) + assert.True(t, ok) + + assert.Equal(t, "Log message 1", asStringSlice[0]) +} + +func TestBenchSpy_LokiQueryExecutor_TimeRange(t *testing.T) { + executor := &LokiQueryExecutor{} + start := time.Now().Add(-1 * time.Hour) + end := time.Now() + + executor.TimeRange(start, end) + assert.Equal(t, start, executor.StartTime) + assert.Equal(t, end, executor.EndTime) +} + +func TestBenchSpy_NewStandardMetricsLokiExecutor(t *testing.T) { + config := &wasp.LokiConfig{ + URL: "http://localhost:3100", + TenantID: "test", + BasicAuth: "user:pass", + } + testName := "test" + genName := "generator" + branch := "main" + commit := "abc123" + start := time.Now().Add(-1 * time.Hour) + end := time.Now() + + executor, err := NewStandardMetricsLokiExecutor(config, testName, genName, branch, commit, start, end) + assert.NoError(t, err) + assert.NotNil(t, executor) + assert.Equal(t, "loki", executor.KindName) + assert.Len(t, executor.Queries, len(StandardLoadMetrics)) +} + +func TestBenchSpy_CalculateTimeRange(t *testing.T) { + tests := []struct { + name string + duration time.Duration + want string + }{ + {"exact hours", 2 * time.Hour, "2h"}, + {"exact minutes", 30 * time.Minute, "30m"}, + {"seconds", 45 * time.Second, "45s"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := time.Now() + end := start.Add(tt.duration) + got := calculateTimeRange(start, end) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/wasp/benchspy/metrics.go b/wasp/benchspy/metrics.go new file mode 100644 index 000000000..57c12ab0c --- /dev/null +++ b/wasp/benchspy/metrics.go @@ -0,0 +1,19 @@ +package benchspy + +import ( + "strconv" +) + +// StringSliceToFloat64Slice converts a slice of strings to a slice of float64 values. +// It returns an error if any string cannot be parsed as a float64, making it useful for data conversion tasks. +func StringSliceToFloat64Slice(s []string) ([]float64, error) { + numbers := make([]float64, len(s)) + for i, str := range s { + var err error + numbers[i], err = strconv.ParseFloat(str, 64) + if err != nil { + return nil, err + } + } + return numbers, nil +} diff --git a/wasp/benchspy/metrics_test.go b/wasp/benchspy/metrics_test.go new file mode 100644 index 000000000..a092b5bb1 --- /dev/null +++ b/wasp/benchspy/metrics_test.go @@ -0,0 +1,41 @@ +package benchspy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBenchSpy_StringSliceToFloat64Slice(t *testing.T) { + t.Run("valid conversion", func(t *testing.T) { + input := []string{"1.0", "2.5", "3.14"} + expected := []float64{1.0, 2.5, 3.14} + + result, err := StringSliceToFloat64Slice(input) + assert.NoError(t, err) + assert.Equal(t, expected, result) + }) + + t.Run("invalid number format", func(t *testing.T) { + input := []string{"1.0", "invalid", "3.14"} + result, err := StringSliceToFloat64Slice(input) + assert.Error(t, err) + assert.Nil(t, result) + }) + + t.Run("empty slice", func(t *testing.T) { + input := []string{} + result, err := StringSliceToFloat64Slice(input) + assert.NoError(t, err) + assert.Empty(t, result) + }) + + t.Run("scientific notation", func(t *testing.T) { + input := []string{"1e-10", "2e5"} + expected := []float64{1e-10, 2e5} + + result, err := StringSliceToFloat64Slice(input) + assert.NoError(t, err) + assert.Equal(t, expected, result) + }) +} diff --git a/wasp/benchspy/prometheus.go b/wasp/benchspy/prometheus.go new file mode 100644 index 000000000..19174df34 --- /dev/null +++ b/wasp/benchspy/prometheus.go @@ -0,0 +1,348 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "os" + "reflect" + "time" + + "github.com/pkg/errors" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + + "github.com/smartcontractkit/chainlink-testing-framework/lib/client" +) + +// all of them are calculated over 5 minutes intervals (rate query), that are later sampled every 10 seconds over %s duration (quantile_over_time query) +var ( + Prometheus_MedianCPU = `quantile_over_time(0.5, rate(container_cpu_usage_seconds_total{name=~"%s"}[5m])[%s:10s]) * 100` + Prometheus_P95CPU = `quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{name=~"%s"}[5m])[%s:10s]) * 100` + Prometheus_MaxCPU = `max(max_over_time(rate(container_cpu_usage_seconds_total{name=~"%s"}[5m])[%s:10s]) * 100)` + Prometheus_MedianMem = `quantile_over_time(0.5, rate(container_memory_usage_bytes{name=~"%s"}[5m])[%s:10s]) * 100` + Prometheus_P95Mem = `quantile_over_time(0.95, rate(container_memory_usage_bytes{name=~"%s"}[5m])[%s:10s]) * 100` + Prometheus_MaxMem = `max(max_over_time(rate(container_memory_usage_bytes{name=~"%s"}[5m])[%s:10s]) * 100)` +) + +type PrometheusConfig struct { + Url string + NameRegexPatterns []string +} + +const PrometheusUrlEnvVar = "PROMETHEUS_URL" + +// NewPrometheusConfig creates a new PrometheusConfig instance with the specified name regex patterns. +// It retrieves the Prometheus URL from the environment and is used to configure query execution for Prometheus data sources. +func NewPrometheusConfig(nameRegexPatterns ...string) *PrometheusConfig { + return &PrometheusConfig{ + Url: os.Getenv(PrometheusUrlEnvVar), + NameRegexPatterns: nameRegexPatterns, + } +} + +var WithoutPrometheus *PrometheusConfig = nil + +type PrometheusQueryExecutor struct { + KindName string `json:"kind"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + client v1.API `json:"-"` + Queries map[string]string `json:"queries"` + QueryResults map[string]interface{} `json:"query_results"` + warnings map[string]v1.Warnings `json:"-"` +} + +// NewPrometheusQueryExecutor creates a new PrometheusResourceReporter, url should include basic auth if needed +func NewPrometheusQueryExecutor(queries map[string]string, config *PrometheusConfig) (*PrometheusQueryExecutor, error) { + c, err := client.NewPrometheusClient(config.Url) + if err != nil { + return nil, errors.Wrapf(err, "failed to create Prometheus client") + } + + return &PrometheusQueryExecutor{ + KindName: string(StandardQueryExecutor_Prometheus), + client: c, + Queries: queries, + QueryResults: make(map[string]interface{}), + }, nil +} + +// NewStandardPrometheusQueryExecutor creates a PrometheusQueryExecutor with standard queries +// based on the provided time range and configuration. It simplifies the process of generating +// queries for Prometheus, making it easier to integrate Prometheus data into reports. +func NewStandardPrometheusQueryExecutor(startTime, endTime time.Time, config *PrometheusConfig) (*PrometheusQueryExecutor, error) { + p := &PrometheusQueryExecutor{} + + standardQueries := make(map[string]string) + for _, nameRegexPattern := range config.NameRegexPatterns { + queries, queryErr := p.generateStandardQueries(nameRegexPattern, startTime, endTime) + if queryErr != nil { + return nil, errors.Wrapf(queryErr, "failed to generate standard queries for %s", nameRegexPattern) + } + + for name, query := range queries { + standardQueries[name] = query + } + } + + return NewPrometheusQueryExecutor(standardQueries, config) +} + +// Execute runs the defined Prometheus queries concurrently, collecting results and warnings. +// It returns an error if any query fails, allowing for efficient data retrieval in reporting tasks. +func (r *PrometheusQueryExecutor) Execute(ctx context.Context) error { + for name, query := range r.Queries { + result, warnings, queryErr := r.client.Query(ctx, query, r.EndTime) + if queryErr != nil { + return errors.Wrapf(queryErr, "failed to query Prometheus for %s", name) + } + + if len(warnings) > 0 { + r.warnings[name] = warnings + } + + r.QueryResults[name] = result + } + + return nil +} + +// Results returns the query results as a map of string to interface{}. +// It allows users to access the results of executed queries, facilitating data retrieval and manipulation. +func (r *PrometheusQueryExecutor) Results() map[string]interface{} { + return r.QueryResults +} + +// Kind returns the type of the query executor as a string. +// It is used to identify the specific kind of executor in a collection of query executors. +func (l *PrometheusQueryExecutor) Kind() string { + return l.KindName +} + +// Validate checks the PrometheusQueryExecutor for a valid client and ensures that at least one query is provided. +// It returns an error if the client is nil or no queries are specified, helping to ensure proper configuration before execution. +func (r *PrometheusQueryExecutor) Validate() error { + if r.client == nil { + return errors.New("prometheus client is nil") + } + + if len(r.Queries) == 0 { + return errors.New("no queries provided") + } + + return nil +} + +// IsComparable checks if the provided QueryExecutor is of the same type as the receiver. +// It returns an error if the types do not match, ensuring type safety for query comparisons. +func (r *PrometheusQueryExecutor) IsComparable(other QueryExecutor) error { + otherType := reflect.TypeOf(other) + if otherType != reflect.TypeOf(r) { + return fmt.Errorf("expected type %s, got %s", reflect.TypeOf(r), otherType) + } + + asPrometheusResourceReporter := other.(*PrometheusQueryExecutor) + + return r.compareQueries(asPrometheusResourceReporter.Queries) +} + +func (r *PrometheusQueryExecutor) compareQueries(other map[string]string) error { + this := r.Queries + if len(this) != len(other) { + return fmt.Errorf("queries count is different. Expected %d, got %d", len(this), len(other)) + } + + for name1, query1 := range this { + if query2, ok := other[name1]; !ok { + return fmt.Errorf("query %s is missing from the other report", name1) + } else { + if query1 != query2 { + return fmt.Errorf("query %s is different. Expected %s, got %s", name1, query1, query2) + } + } + } + + return nil +} + +// Warnings returns a map of warnings encountered during query execution. +// This function is useful for retrieving any issues that may have arisen, +// allowing users to handle or log them appropriately. +func (r *PrometheusQueryExecutor) Warnings() map[string]v1.Warnings { + return r.warnings +} + +// MustResultsAsValue retrieves the query results as a map of metric names to their corresponding values. +// It ensures that the results are in a consistent format, making it easier to work with metrics in subsequent operations. +func (r *PrometheusQueryExecutor) MustResultsAsValue() map[string]model.Value { + results := make(map[string]model.Value) + for name, result := range r.QueryResults { + var val model.Value + switch v := result.(type) { + case model.Matrix: + // model.Matrix implements model.Value with value receivers + val = v + case *model.Matrix: + val = v + case model.Vector: + // model.Vector implements model.Value with value receivers + val = v + case *model.Vector: + val = v + case model.Scalar: + scalar := v + // *model.Scalar implements model.Value + val = &scalar + case *model.Scalar: + val = v + case model.String: + str := v + // *model.String implements model.Value + val = &str + case *model.String: + val = v + default: + panic(fmt.Sprintf("Unknown result type: %T", result)) + } + results[name] = val + } + return results +} + +// TimeRange sets the start and end time for the Prometheus query execution. +// This function is essential for defining the time window for data retrieval, ensuring accurate and relevant results. +func (r *PrometheusQueryExecutor) TimeRange(startTime, endTime time.Time) { + r.StartTime = startTime + r.EndTime = endTime +} + +func (r *PrometheusQueryExecutor) standardQuery(metric StandardResourceMetric, nameRegexPattern string, startTime, endTime time.Time) (string, error) { + duration := calculateTimeRange(startTime, endTime) + switch metric { + case MedianCPUUsage: + return fmt.Sprintf(Prometheus_MedianCPU, nameRegexPattern, duration), nil + case P95CPUUsage: + return fmt.Sprintf(Prometheus_P95CPU, nameRegexPattern, duration), nil + case MaxCPUUsage: + return fmt.Sprintf(Prometheus_MaxCPU, nameRegexPattern, duration), nil + case MedianMemUsage: + return fmt.Sprintf(Prometheus_MedianMem, nameRegexPattern, duration), nil + case P95MemUsage: + return fmt.Sprintf(Prometheus_P95Mem, nameRegexPattern, duration), nil + case MaxMemUsage: + return fmt.Sprintf(Prometheus_MaxMem, nameRegexPattern, duration), nil + default: + return "", fmt.Errorf("unsupported standard metric %s", metric) + } +} + +func (r *PrometheusQueryExecutor) generateStandardQueries(nameRegexPattern string, startTime, endTime time.Time) (map[string]string, error) { + standardQueries := make(map[string]string) + + for _, metric := range StandardResourceMetrics { + query, err := r.standardQuery(metric, nameRegexPattern, startTime, endTime) + if err != nil { + return nil, err + } + standardQueries[string(metric)] = query + } + + return standardQueries, nil +} + +type TypedMetric struct { + Value model.Value `json:"value"` + MetricType string `json:"metric_type"` +} + +// MarshalJSON customizes the JSON representation of PrometheusQueryExecutor. +// It includes only essential fields: Kind, Queries, and simplified QueryResults. +// This function is useful for serializing the executor's state in a concise format. +func (g *PrometheusQueryExecutor) MarshalJSON() ([]byte, error) { + // we need custom marshalling to only include some parts of the metrics + type QueryExecutor struct { + Kind string `json:"kind"` + Queries map[string]string `json:"queries"` + QueryResults map[string]TypedMetric `json:"query_results"` + } + + q := &QueryExecutor{ + Kind: g.KindName, + Queries: g.Queries, + QueryResults: func() map[string]TypedMetric { + simplifiedMetrics := make(map[string]TypedMetric) + for name, value := range g.MustResultsAsValue() { + simplifiedMetrics[name] = TypedMetric{ + MetricType: value.Type().String(), + Value: value, + } + } + return simplifiedMetrics + }(), + } + + return json.Marshal(q) +} + +// UnmarshalJSON decodes JSON data into a PrometheusQueryExecutor instance. +// It populates the QueryResults field with appropriately typed metrics, +// enabling easy access to the results of Prometheus queries. +func (r *PrometheusQueryExecutor) UnmarshalJSON(data []byte) error { + // helper struct with QueryResults map[string]interface{} + type Alias PrometheusQueryExecutor + var raw struct { + Alias + QueryResults map[string]json.RawMessage `json:"query_results"` + } + + // unmarshal into the helper struct to populate other fields automatically + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + var convertedQueryResults = make(map[string]interface{}) + + for name, rawResult := range raw.QueryResults { + var rawTypedMetric struct { + MetricType string `json:"metric_type"` + Value json.RawMessage `json:"value"` + } + if err := json.Unmarshal(rawResult, &rawTypedMetric); err != nil { + return errors.Wrapf(err, "failed to unmarshal query result for %s", name) + } + + switch rawTypedMetric.MetricType { + case "scalar": + var scalar model.Scalar + if err := json.Unmarshal(rawTypedMetric.Value, &scalar); err != nil { + return errors.Wrapf(err, "failed to unmarshal scalar value for %s", name) + } + convertedQueryResults[name] = &scalar + case "vector": + var vector model.Vector + if err := json.Unmarshal(rawTypedMetric.Value, &vector); err != nil { + return errors.Wrapf(err, "failed to unmarshal vector value for %s", name) + } + convertedQueryResults[name] = vector + case "matrix": + var matrix model.Matrix + if err := json.Unmarshal(rawTypedMetric.Value, &matrix); err != nil { + return errors.Wrapf(err, "failed to unmarshal matrix value for %s", name) + } + convertedQueryResults[name] = matrix + case "string": + var str model.String + if err := json.Unmarshal(rawTypedMetric.Value, &str); err != nil { + return errors.Wrapf(err, "failed to unmarshal string value for %s", name) + } + convertedQueryResults[name] = &str + default: + return fmt.Errorf("unknown metric type %s", rawTypedMetric.MetricType) + } + } + + *r = PrometheusQueryExecutor(raw.Alias) + r.QueryResults = convertedQueryResults + return nil +} diff --git a/wasp/benchspy/prometheus_test.go b/wasp/benchspy/prometheus_test.go new file mode 100644 index 000000000..024e18f76 --- /dev/null +++ b/wasp/benchspy/prometheus_test.go @@ -0,0 +1,453 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBenchSpy_NewPrometheusQueryExecutor(t *testing.T) { + t.Run("successful creation", func(t *testing.T) { + queries := map[string]string{"test": "query"} + + executor, err := NewPrometheusQueryExecutor(queries, &PrometheusConfig{Url: "http://localhost:9090"}) + + require.NoError(t, err) + assert.NotNil(t, executor) + assert.Equal(t, queries, executor.Queries) + }) +} + +func TestBenchSpy_NewStandardPrometheusQueryExecutor(t *testing.T) { + t.Run("successful creation", func(t *testing.T) { + startTime := time.Now().Add(-1 * time.Hour) + endTime := time.Now() + + executor, err := NewStandardPrometheusQueryExecutor(startTime, endTime, &PrometheusConfig{Url: "http://localhost:9090", NameRegexPatterns: []string{"test.*"}}) + + require.NoError(t, err) + assert.NotNil(t, executor) + assert.NotEmpty(t, executor.Queries) + assert.Equal(t, 6, len(executor.Queries)) + + queries := []string{} + for name := range executor.Queries { + queries = append(queries, name) + } + + assert.Contains(t, queries, string(MedianCPUUsage)) + assert.Contains(t, queries, string(MedianMemUsage)) + assert.Contains(t, queries, string(P95MemUsage)) + assert.Contains(t, queries, string(P95CPUUsage)) + }) +} + +func TestBenchSpy_PrometheusQueryExecutor_Execute(t *testing.T) { + t.Run("successful execution", func(t *testing.T) { + // Create test data + expectedQuery := "rate(test_metric[5m])" + expectedTime := time.Now() + expectedValue := &model.Vector{ + &model.Sample{ + Timestamp: model.Time(expectedTime.Unix()), + Value: 42, + }, + } + + // Create mock client with custom query behavior + mockClient := &mockPrometheusClient{ + queryFn: func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) { + assert.Equal(t, expectedQuery, query) + return expectedValue, nil, nil + }, + } + + executor := &PrometheusQueryExecutor{ + client: mockClient, + Queries: map[string]string{"test_metric": expectedQuery}, + warnings: make(map[string]v1.Warnings), + QueryResults: make(map[string]interface{}), + StartTime: expectedTime.Add(-1 * time.Hour), + EndTime: expectedTime, + } + + err := executor.Execute(context.Background()) + + require.NoError(t, err) + assert.NotEmpty(t, executor.QueryResults) + assert.Contains(t, executor.QueryResults, "test_metric") + + // Verify the stored result + result, ok := executor.QueryResults["test_metric"] + require.True(t, ok) + assert.Equal(t, expectedValue, result) + }) + + t.Run("handles warnings", func(t *testing.T) { + // Create test data + expectedQuery := "rate(test_metric[5m])" + expectedTime := time.Now() + expectedWarnings := v1.Warnings{"warning1", "warning2"} + expectedValue := &model.Vector{ + &model.Sample{ + Timestamp: model.Time(expectedTime.Unix()), + Value: 42, + }, + } + + // Create mock client that returns warnings + mockClient := &mockPrometheusClient{ + queryFn: func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) { + return expectedValue, expectedWarnings, nil + }, + } + + executor := &PrometheusQueryExecutor{ + client: mockClient, + Queries: map[string]string{"test_metric": expectedQuery}, + warnings: make(map[string]v1.Warnings), + QueryResults: make(map[string]interface{}), + StartTime: expectedTime.Add(-1 * time.Hour), + EndTime: expectedTime, + } + + err := executor.Execute(context.Background()) + + // Verify execution + require.NoError(t, err) + assert.NotEmpty(t, executor.QueryResults) + assert.Contains(t, executor.QueryResults, "test_metric") + + // Verify warnings were stored + assert.Contains(t, executor.Warnings(), "test_metric") + assert.Equal(t, expectedWarnings, executor.Warnings()["test_metric"]) + + // Verify result was still stored despite warnings + result, ok := executor.QueryResults["test_metric"] + require.True(t, ok) + assert.Equal(t, expectedValue, result) + }) + + t.Run("handles query error", func(t *testing.T) { + mockClient := &mockPrometheusClient{ + queryFn: func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) { + return nil, nil, fmt.Errorf("query failed") + }, + } + + executor := &PrometheusQueryExecutor{ + client: mockClient, + Queries: map[string]string{"test_metric": "invalid_query"}, + warnings: make(map[string]v1.Warnings), + QueryResults: make(map[string]interface{}), + StartTime: time.Now().Add(-1 * time.Hour), + EndTime: time.Now(), + } + + err := executor.Execute(context.Background()) + require.Error(t, err) + assert.Empty(t, executor.QueryResults) + }) +} + +func TestBenchSpy_PrometheusQueryExecutor_Validate(t *testing.T) { + t.Run("valid configuration", func(t *testing.T) { + executor := &PrometheusQueryExecutor{ + client: &mockPrometheusClient{}, + Queries: map[string]string{"test": "query"}, + StartTime: time.Now(), + EndTime: time.Now(), + } + + err := executor.Validate() + + require.NoError(t, err) + }) + + t.Run("missing client", func(t *testing.T) { + executor := &PrometheusQueryExecutor{ + Queries: map[string]string{"test": "query"}, + StartTime: time.Now(), + EndTime: time.Now(), + } + + err := executor.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "client is nil") + }) + + t.Run("empty queries", func(t *testing.T) { + executor := &PrometheusQueryExecutor{ + client: &mockPrometheusClient{}, + StartTime: time.Now(), + EndTime: time.Now(), + } + + err := executor.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "no queries provided") + }) +} + +func TestBenchSpy_PrometheusQueryExecutor_IsComparable(t *testing.T) { + t.Run("same queries", func(t *testing.T) { + queries := map[string]string{"test": "query"} + executor1 := &PrometheusQueryExecutor{Queries: queries} + executor2 := &PrometheusQueryExecutor{Queries: queries} + + err := executor1.IsComparable(executor2) + + require.NoError(t, err) + }) + + t.Run("different query count", func(t *testing.T) { + executor1 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + "test2": "query2", + }, + } + executor2 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + }, + } + + err := executor1.IsComparable(executor2) + + require.Error(t, err) + assert.Contains(t, err.Error(), "queries count is different") + }) + + t.Run("missing query", func(t *testing.T) { + executor1 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + "test2": "query2", + }, + } + executor2 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + "test3": "query3", + }, + } + + err := executor1.IsComparable(executor2) + + require.Error(t, err) + assert.Contains(t, err.Error(), "query test2 is missing") + }) + + t.Run("different query content", func(t *testing.T) { + executor1 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + "test2": "query2", + }, + } + executor2 := &PrometheusQueryExecutor{ + Queries: map[string]string{ + "test1": "query1", + "test2": "different_query", + }, + } + + err := executor1.IsComparable(executor2) + + require.Error(t, err) + assert.Contains(t, err.Error(), "query test2 is different") + }) + + t.Run("different types", func(t *testing.T) { + executor := &PrometheusQueryExecutor{} + other := &mockQueryExecutor{ + kindName: "mock", + queries: make(map[string]string), + results: make(map[string]interface{}), + } + + err := executor.IsComparable(other) + + require.Error(t, err) + assert.Contains(t, err.Error(), "expected type") + }) +} + +func TestBenchSpy_PrometheusQueryExecutor_JSONMarshalling(t *testing.T) { + t.Run("marshal and unmarshal", func(t *testing.T) { + original := &PrometheusQueryExecutor{ + KindName: "prometheus", + Queries: map[string]string{ + "test_metric": "rate(test[5m])", + "test_metric2": "histogram_quantile(0.95, test[5m])", + }, + QueryResults: map[string]interface{}{ + "test_metric": &model.Vector{ + &model.Sample{ + Timestamp: model.Time(time.Now().Unix()), + Value: 42, + }, + }, + }, + } + + // Marshal to JSON + data, err := json.Marshal(original) + require.NoError(t, err) + + // Verify JSON contains expected fields + jsonStr := string(data) + assert.Contains(t, jsonStr, "prometheus") + assert.Contains(t, jsonStr, "test_metric") + assert.Contains(t, jsonStr, "rate(test[5m])") + + // Unmarshal back + var decoded PrometheusQueryExecutor + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + // Verify fields + assert.Equal(t, original.KindName, decoded.KindName) + assert.Equal(t, original.Queries, decoded.Queries) + assert.Equal(t, len(original.QueryResults), len(decoded.QueryResults)) + + // Verify unexported fields are not marshalled + assert.Zero(t, decoded.StartTime) + assert.Zero(t, decoded.EndTime) + assert.Nil(t, decoded.client) + }) +} + +type mockQueryExecutor struct { + kindName string + queries map[string]string + results map[string]interface{} +} + +func (m *mockQueryExecutor) Execute(ctx context.Context) error { + return nil +} + +func (r *mockQueryExecutor) IsComparable(other QueryExecutor) error { + return nil +} + +func (m *mockQueryExecutor) Validate() error { + return nil +} + +func (m *mockQueryExecutor) Kind() string { + return m.kindName +} + +func (r *mockQueryExecutor) TimeRange(startTime, endTime time.Time) { +} + +func (m *mockQueryExecutor) Results() map[string]interface{} { + return m.results +} + +// Mock Prometheus client for testing +type mockPrometheusClient struct { + // Add test control fields + queryFn func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) +} + +// Default query implementation +func (m *mockPrometheusClient) Query(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) { + if m.queryFn != nil { + return m.queryFn(ctx, query, ts, opts...) + } + return &model.Vector{}, v1.Warnings{}, nil +} + +// Minimal implementations for remaining interface methods +func (m *mockPrometheusClient) Alerts(ctx context.Context) (v1.AlertsResult, error) { + return v1.AlertsResult{}, nil +} + +func (m *mockPrometheusClient) AlertManagers(ctx context.Context) (v1.AlertManagersResult, error) { + return v1.AlertManagersResult{}, nil +} + +func (m *mockPrometheusClient) CleanTombstones(ctx context.Context) error { + return nil +} + +func (m *mockPrometheusClient) Config(ctx context.Context) (v1.ConfigResult, error) { + return v1.ConfigResult{}, nil +} + +func (m *mockPrometheusClient) DeleteSeries(ctx context.Context, matches []string, startTime, endTime time.Time) error { + return nil +} + +func (m *mockPrometheusClient) Flags(ctx context.Context) (v1.FlagsResult, error) { + return v1.FlagsResult{}, nil +} + +func (m *mockPrometheusClient) LabelNames(ctx context.Context, matches []string, startTime, endTime time.Time, opts ...v1.Option) ([]string, v1.Warnings, error) { + return []string{}, v1.Warnings{}, nil +} + +func (m *mockPrometheusClient) LabelValues(ctx context.Context, label string, matches []string, startTime, endTime time.Time, opts ...v1.Option) (model.LabelValues, v1.Warnings, error) { + return model.LabelValues{}, v1.Warnings{}, nil +} + +func (m *mockPrometheusClient) QueryRange(ctx context.Context, query string, r v1.Range, opts ...v1.Option) (model.Value, v1.Warnings, error) { + return &model.Vector{}, v1.Warnings{}, nil +} + +func (m *mockPrometheusClient) QueryExemplars(ctx context.Context, query string, startTime, endTime time.Time) ([]v1.ExemplarQueryResult, error) { + return []v1.ExemplarQueryResult{}, nil +} + +func (m *mockPrometheusClient) Buildinfo(ctx context.Context) (v1.BuildinfoResult, error) { + return v1.BuildinfoResult{}, nil +} + +func (m *mockPrometheusClient) Runtimeinfo(ctx context.Context) (v1.RuntimeinfoResult, error) { + return v1.RuntimeinfoResult{}, nil +} + +func (m *mockPrometheusClient) Series(ctx context.Context, matches []string, startTime, endTime time.Time, opts ...v1.Option) ([]model.LabelSet, v1.Warnings, error) { + return []model.LabelSet{}, v1.Warnings{}, nil +} + +func (m *mockPrometheusClient) Snapshot(ctx context.Context, skipHead bool) (v1.SnapshotResult, error) { + return v1.SnapshotResult{}, nil +} + +func (m *mockPrometheusClient) Rules(ctx context.Context) (v1.RulesResult, error) { + return v1.RulesResult{}, nil +} + +func (m *mockPrometheusClient) Targets(ctx context.Context) (v1.TargetsResult, error) { + return v1.TargetsResult{}, nil +} + +func (m *mockPrometheusClient) TargetsMetadata(ctx context.Context, matchTarget, metric, limit string) ([]v1.MetricMetadata, error) { + return []v1.MetricMetadata{}, nil +} + +func (m *mockPrometheusClient) Metadata(ctx context.Context, metric, limit string) (map[string][]v1.Metadata, error) { + return map[string][]v1.Metadata{}, nil +} + +func (m *mockPrometheusClient) TSDB(ctx context.Context, opts ...v1.Option) (v1.TSDBResult, error) { + return v1.TSDBResult{}, nil +} + +func (m *mockPrometheusClient) WalReplay(ctx context.Context) (v1.WalReplayStatus, error) { + return v1.WalReplayStatus{}, nil +} diff --git a/wasp/benchspy/report.go b/wasp/benchspy/report.go new file mode 100644 index 000000000..7f8ae0444 --- /dev/null +++ b/wasp/benchspy/report.go @@ -0,0 +1,713 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" + "github.com/prometheus/common/model" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "golang.org/x/sync/errgroup" +) + +// StandardReport is a report that contains all the necessary data for a performance test +type StandardReport struct { + BasicData + LocalStorage + QueryExecutors []QueryExecutor `json:"query_executors"` +} + +// Store saves the report to local storage as a JSON file. +// It returns the absolute path of the stored file and any error encountered. +func (b *StandardReport) Store() (string, error) { + return b.LocalStorage.Store(b.TestName, b.CommitOrTag, b) +} + +// Load retrieves a report based on the specified test name and commit or tag. +// It utilizes local storage to find and decode the corresponding report file, +// ensuring that the report is available for further processing or analysis. +func (b *StandardReport) Load(testName, commitOrTag string) error { + return b.LocalStorage.Load(testName, commitOrTag, b) +} + +// LoadLatest retrieves the most recent report for the specified test name from local storage. +// It returns an error if the report cannot be loaded, enabling users to access historical test data efficiently. +func (b *StandardReport) LoadLatest(testName string) error { + return b.LocalStorage.Load(testName, "", b) +} + +// ResultsAs retrieves and casts results from a query executor to a specified type. +// It returns a map of query names to their corresponding results, or an error if casting fails. +func ResultsAs[Type any](newType Type, queryExecutor QueryExecutor, queryNames ...string) (map[string]Type, error) { + results := make(map[string]Type) + + var toTypeOrErr = func(result interface{}, queryName string) error { + if asType, ok := result.(Type); ok { + results[queryName] = asType + } else { + return fmt.Errorf("failed to cast result to type %T. It's actual type is: %T", newType, result) + } + + return nil + } + + if len(queryNames) > 0 { + for _, queryName := range queryNames { + if result, ok := queryExecutor.Results()[queryName]; ok { + if err := toTypeOrErr(result, queryName); err != nil { + return nil, err + } + } + } + } else { + for queryName, result := range queryExecutor.Results() { + if err := toTypeOrErr(result, queryName); err != nil { + return nil, err + } + } + } + + return results, nil +} + +type LokiResultsByGenerator map[string]map[string][]string + +// MustAllLokiResults retrieves and aggregates results from all Loki query executors in a StandardReport. +// It panics if any query execution fails, ensuring that only successful results are returned. +func MustAllLokiResults(sr *StandardReport) LokiResultsByGenerator { + results := make(LokiResultsByGenerator) + + for _, queryExecutor := range sr.QueryExecutors { + if strings.EqualFold(queryExecutor.Kind(), string(StandardQueryExecutor_Loki)) { + singleResult, err := ResultsAs([]string{}, queryExecutor) + if err != nil { + panic(err) + } + + asNamedGenerator := queryExecutor.(NamedGenerator) + results[asNamedGenerator.GeneratorName()] = singleResult + } + } + + return results +} + +type DirectResultsByGenerator map[string]map[string]float64 + +// MustAllDirectResults extracts and returns all direct results from a given StandardReport. +// It panics if any result extraction fails, ensuring that only valid results are processed. +func MustAllDirectResults(sr *StandardReport) DirectResultsByGenerator { + results := make(DirectResultsByGenerator) + + for _, queryExecutor := range sr.QueryExecutors { + if strings.EqualFold(queryExecutor.Kind(), string(StandardQueryExecutor_Direct)) { + singleResult, err := ResultsAs(0.0, queryExecutor) + if err != nil { + panic(err) + } + + asNamedGenerator := queryExecutor.(NamedGenerator) + results[asNamedGenerator.GeneratorName()] = singleResult + } + } + + return results +} + +// MustAllPrometheusResults retrieves all Prometheus query results from a StandardReport. +// It returns a map of query names to their corresponding model.Values, ensuring type safety. +// This function is useful for aggregating and accessing Prometheus metrics efficiently. +func MustAllPrometheusResults(sr *StandardReport) map[string]model.Value { + results := make(map[string]model.Value) + + for _, queryExecutor := range sr.QueryExecutors { + if strings.EqualFold(queryExecutor.Kind(), string(StandardQueryExecutor_Prometheus)) { + for queryName, result := range queryExecutor.Results() { + if asValue, ok := result.(model.Value); ok { + results[queryName] = asValue + } + } + } + } + + return results +} + +func calculateDiffPercentage(current, previous float64) float64 { + var diffPrecentage float64 + if previous != 0.0 && current != 0.0 { + diffPrecentage = (current - previous) / previous * 100 + } else if previous == 0.0 && current == 0.0 { + diffPrecentage = 0.0 + } else { + diffPrecentage = 100.0 + } + + return diffPrecentage +} + +// CompareDirectWithThresholds evaluates the current and previous reports against specified thresholds. +// It checks for significant differences in metrics and returns any discrepancies found, aiding in performance analysis. +func CompareDirectWithThresholds(medianThreshold, p95Threshold, maxThreshold, errorRateThreshold float64, currentReport, previousReport *StandardReport) (bool, map[string][]error) { + allCurrentResults := MustAllDirectResults(currentReport) + allPreviousResults := MustAllDirectResults(previousReport) + + var compareValues = func( + metricName, generatorName string, + maxDiffPercentage float64, + ) error { + if _, ok := allCurrentResults[generatorName]; !ok { + return fmt.Errorf("generator %s results were missing from current report", generatorName) + } + + if _, ok := allPreviousResults[generatorName]; !ok { + return fmt.Errorf("generator %s results were missing from previous report", generatorName) + } + + currentForGenerator := allCurrentResults[generatorName] + previousForGenerator := allPreviousResults[generatorName] + + if _, ok := currentForGenerator[metricName]; !ok { + return fmt.Errorf("%s metric results were missing from current report for generator %s", metricName, generatorName) + } + + if _, ok := previousForGenerator[metricName]; !ok { + return fmt.Errorf("%s metric results were missing from previous report for generator %s", metricName, generatorName) + } + + currentMetric := currentForGenerator[metricName] + previousMetric := previousForGenerator[metricName] + + diffPrecentage := calculateDiffPercentage(currentMetric, previousMetric) + if diffPrecentage > maxDiffPercentage { + return fmt.Errorf("%s is %.4f%% different, which is higher than the threshold %.4f%%", metricName, diffPrecentage, maxDiffPercentage) + } + + return nil + } + + errors := make(map[string][]error) + + for _, genCfg := range currentReport.GeneratorConfigs { + if err := compareValues(string(MedianLatency), genCfg.GenName, medianThreshold); err != nil { + errors[genCfg.GenName] = append(errors[genCfg.GenName], err) + } + + if err := compareValues(string(Percentile95Latency), genCfg.GenName, p95Threshold); err != nil { + errors[genCfg.GenName] = append(errors[genCfg.GenName], err) + } + + if err := compareValues(string(MaxLatency), genCfg.GenName, maxThreshold); err != nil { + errors[genCfg.GenName] = append(errors[genCfg.GenName], err) + } + + if err := compareValues(string(ErrorRate), genCfg.GenName, errorRateThreshold); err != nil { + errors[genCfg.GenName] = append(errors[genCfg.GenName], err) + } + } + + PrintStandardDirectMetrics(currentReport, previousReport) + + return len(errors) > 0, errors +} + +// PrintStandardDirectMetrics outputs a comparison of direct metrics between two reports. +// It displays the current and previous values along with the percentage difference for each metric, +// helping users to quickly assess performance changes across different generator configurations. +func PrintStandardDirectMetrics(currentReport, previousReport *StandardReport) { + currentResults := MustAllDirectResults(currentReport) + previousResults := MustAllDirectResults(previousReport) + + for _, genCfg := range currentReport.GeneratorConfigs { + generatorName := genCfg.GenName + table := tablewriter.NewWriter(os.Stderr) + table.SetHeader([]string{"Metric", previousReport.CommitOrTag, currentReport.CommitOrTag, "Diff %"}) + + for _, metricName := range StandardLoadMetrics { + metricString := string(metricName) + diff := calculateDiffPercentage(currentResults[generatorName][metricString], previousResults[generatorName][metricString]) + table.Append([]string{metricString, fmt.Sprintf("%.4f", previousResults[genCfg.GenName][metricString]), fmt.Sprintf("%.4f", currentResults[genCfg.GenName][metricString]), fmt.Sprintf("%.4f", diff)}) + } + + table.SetBorder(true) + table.SetRowLine(true) + table.SetAlignment(tablewriter.ALIGN_LEFT) + + title := "Generator: " + generatorName + fmt.Println(title) + fmt.Println(strings.Repeat("=", len(title))) + + table.Render() + } +} + +// FetchData retrieves data for the report within the specified time range. +// It validates the time range and executes queries in parallel, returning any errors encountered during execution. +func (b *StandardReport) FetchData(ctx context.Context) error { + if b.TestStart.IsZero() || b.TestEnd.IsZero() { + return errors.New("start and end times are not set") + } + + errGroup, errCtx := errgroup.WithContext(ctx) + for _, queryExecutor := range b.QueryExecutors { + errGroup.Go(func() error { + // feature: PLAIN SEGEMENT ONLY + // go over all schedules and execute the code below only for ones with type "plain" + // and then concatenate that data and return that; if parallelizing then we should first + // create a slice of plain segments and then, when sending results over channel include the index, + // so that we can concatenate them in the right order + queryExecutor.TimeRange(b.TestStart, b.TestEnd) + + // in case someone skipped helper functions and didn't set the start and end times + if validateErr := queryExecutor.Validate(); validateErr != nil { + return validateErr + } + + if execErr := queryExecutor.Execute(errCtx); execErr != nil { + return execErr + } + + return nil + }) + } + + if err := errGroup.Wait(); err != nil { + return err + } + + return nil +} + +// IsComparable checks if the current report can be compared with another report. +// It validates the type of the other report and ensures that their basic data and query executors are comparable. +// This function is useful for verifying report consistency before performing further analysis. +func (b *StandardReport) IsComparable(otherReport Reporter) error { + if _, ok := otherReport.(*StandardReport); !ok { + return fmt.Errorf("expected type %s, got %T", "*StandardReport", otherReport) + } + + asStandardReport := otherReport.(*StandardReport) + + basicErr := b.BasicData.IsComparable(asStandardReport.BasicData) + if basicErr != nil { + return basicErr + } + + for _, queryExecutor := range b.QueryExecutors { + queryErr := queryExecutor.IsComparable(queryExecutor) + if queryErr != nil { + return queryErr + } + } + + return nil +} + +type standardReportConfig struct { + executorTypes []StandardQueryExecutorType + generators []*wasp.Generator + prometheusConfig *PrometheusConfig + queryExecutors []QueryExecutor + reportDirectory string +} + +type StandardReportOption func(*standardReportConfig) + +// WithStandardQueries sets the executor types for a standard report configuration. +// It allows users to specify which types of query executors to use, enabling customization +// of report generation based on their requirements. +func WithStandardQueries(executorTypes ...StandardQueryExecutorType) StandardReportOption { + return func(c *standardReportConfig) { + c.executorTypes = executorTypes + } +} + +// WithGenerators sets the generators for the standard report configuration. +// It allows users to specify custom generator instances to be included in the report. +func WithGenerators(generators ...*wasp.Generator) StandardReportOption { + return func(c *standardReportConfig) { + c.generators = generators + } +} + +// WithPrometheusConfig sets the Prometheus configuration for the standard report. +// It returns a StandardReportOption that can be used to customize report generation. +func WithPrometheusConfig(prometheusConfig *PrometheusConfig) StandardReportOption { + return func(c *standardReportConfig) { + c.prometheusConfig = prometheusConfig + } +} + +// WithReportDirectory sets the directory for storing report files. +// This function is useful for configuring the output location of reports +// generated by the standard reporting system. +func WithReportDirectory(reportDirectory string) StandardReportOption { + return func(c *standardReportConfig) { + c.reportDirectory = reportDirectory + } +} + +// WithQueryExecutors sets the query executors for a standard report configuration. +// It allows customization of how queries are executed, enhancing report generation flexibility. +func WithQueryExecutors(queryExecutors ...QueryExecutor) StandardReportOption { + return func(c *standardReportConfig) { + c.queryExecutors = queryExecutors + } +} + +func (c *standardReportConfig) validate() error { + if len(c.executorTypes) == 0 && len(c.queryExecutors) == 0 { + return errors.New("no standard executor types and no custom query executors are provided. At least one is needed") + } + + hasPrometehus := false + for _, t := range c.executorTypes { + if t == StandardQueryExecutor_Prometheus { + hasPrometehus = true + if c.prometheusConfig == WithoutPrometheus { + return errors.New("prometheus as query executor type is set, but prometheus config is not provided") + } + } + } + + if len(c.generators) == 0 { + return errors.New("generators are not set, at least one is required") + } + + if c.prometheusConfig != WithoutPrometheus { + if !hasPrometehus { + return errors.New("prometheus config is set, but query executor type is not set to prometheus") + } + + if c.prometheusConfig.Url == "" { + return errors.New("prometheus url is not set") + } + if len(c.prometheusConfig.NameRegexPatterns) == 0 { + return errors.New("prometheus name regex patterns are not set. At least one pattern is needed to match containers by name") + } + } + + return nil +} + +// NewStandardReport creates a new StandardReport based on the provided commit or tag and options. +// It initializes necessary data and query executors, ensuring all configurations are validated. +// This function is essential for generating reports that require specific data sources and execution strategies. +func NewStandardReport(commitOrTag string, opts ...StandardReportOption) (*StandardReport, error) { + config := standardReportConfig{} + for _, opt := range opts { + opt(&config) + } + + basicData, basicErr := NewBasicData(commitOrTag, config.generators...) + if basicErr != nil { + var generatorNames string + for _, g := range config.generators { + generatorNames += g.Cfg.GenName + ", " + } + return nil, errors.Wrapf(basicErr, "failed to create basic data for generators %s", generatorNames) + } + + configErr := config.validate() + if configErr != nil { + return nil, configErr + } + + basicValidateErr := basicData.Validate() + if basicValidateErr != nil { + return nil, basicValidateErr + } + + queryExecutors, initErr := initStandardLoadExecutors(config, basicData) + if initErr != nil { + return nil, errors.Wrap(initErr, "failed to initialize standard query executors") + } + + if len(config.queryExecutors) > 0 { + queryExecutors = append(queryExecutors, config.queryExecutors...) + } + + prometheusExecutors, promErr := initPrometheusQueryExecutor(config, basicData) + if promErr != nil { + return nil, errors.Wrap(promErr, "failed to initialize prometheus query executor") + } + + queryExecutors = append(queryExecutors, prometheusExecutors...) + + sr := &StandardReport{ + BasicData: *basicData, + QueryExecutors: queryExecutors, + } + + if config.reportDirectory != "" { + sr.LocalStorage.Directory = config.reportDirectory + } + + return sr, nil +} + +func initPrometheusQueryExecutor(config standardReportConfig, basicData *BasicData) ([]QueryExecutor, error) { + var queryExecutors []QueryExecutor + if config.prometheusConfig != WithoutPrometheus { + // not ideal, but we want to follow the same pattern as with other executors + for _, n := range config.prometheusConfig.NameRegexPatterns { + prometheusExecutor, prometheusErr := NewStandardPrometheusQueryExecutor(basicData.TestStart, basicData.TestEnd, NewPrometheusConfig(config.prometheusConfig.Url, n)) + if prometheusErr != nil { + return nil, errors.Wrapf(prometheusErr, "failed to create Prometheus executor for name patterns: %s", strings.Join(config.prometheusConfig.NameRegexPatterns, ", ")) + } + validateErr := prometheusExecutor.Validate() + if validateErr != nil { + return nil, errors.Wrapf(validateErr, "failed to Prometheus executor for for name patterns: %s", strings.Join(config.prometheusConfig.NameRegexPatterns, ", ")) + } + queryExecutors = append(queryExecutors, prometheusExecutor) + } + } + + return queryExecutors, nil +} + +func initStandardLoadExecutors(config standardReportConfig, basicData *BasicData) ([]QueryExecutor, error) { + var queryExecutors []QueryExecutor + if len(config.executorTypes) != 0 { + for _, g := range config.generators { + for _, exType := range config.executorTypes { + if exType != StandardQueryExecutor_Prometheus { + executor, executorErr := initStandardQueryExecutor(exType, basicData, g) + if executorErr != nil { + return nil, errors.Wrapf(executorErr, "failed to create standard %s query executor for generator %s", exType, g.Cfg.GenName) + } + + validateErr := executor.Validate() + if validateErr != nil { + return nil, errors.Wrapf(validateErr, "failed to validate queries for generator %s", g.Cfg.GenName) + } + + queryExecutors = append(queryExecutors, executor) + } + } + } + } + + return queryExecutors, nil +} + +func initStandardQueryExecutor(kind StandardQueryExecutorType, basicData *BasicData, g *wasp.Generator) (QueryExecutor, error) { + switch kind { + case StandardQueryExecutor_Loki: + if !generatorHasLabels(g) { + return nil, fmt.Errorf("generator %s is missing branch or commit labels", g.Cfg.GenName) + } + executor, executorErr := NewStandardMetricsLokiExecutor(g.Cfg.LokiConfig, basicData.TestName, g.Cfg.GenName, g.Cfg.Labels["branch"], g.Cfg.Labels["commit"], basicData.TestStart, basicData.TestEnd) + if executorErr != nil { + return nil, errors.Wrapf(executorErr, "failed to create standard Loki query executor for generator %s", g.Cfg.GenName) + } + return executor, nil + case StandardQueryExecutor_Direct: + executor, executorErr := NewStandardDirectQueryExecutor(g) + if executorErr != nil { + return nil, errors.Wrapf(executorErr, "failed to create standard generator query executor for generator %s", g.Cfg.GenName) + } + return executor, nil + default: + return nil, fmt.Errorf("unknown standard query executor type: %s", kind) + } +} + +func generatorHasLabels(g *wasp.Generator) bool { + return g.Cfg.Labels["branch"] != "" && g.Cfg.Labels["commit"] != "" +} + +// UnmarshalJSON decodes JSON data into a StandardReport struct. +// It populates the QueryExecutors and ResourceFetchers fields, +// allowing for dynamic handling of JSON structures in reports. +func (s *StandardReport) UnmarshalJSON(data []byte) error { + // helper struct with QueryExecutors as json.RawMessage + type Alias StandardReport + var raw struct { + Alias + QueryExecutors []json.RawMessage `json:"query_executors"` + ResourceFetchers []json.RawMessage `json:"resource_fetchers"` + } + + // unmarshal into the helper struct to populate other fields automatically + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + queryExecutors, queryErr := unmarshallQueryExecutors(raw.QueryExecutors) + if queryErr != nil { + return queryErr + } + + *s = StandardReport(raw.Alias) + s.QueryExecutors = queryExecutors + return nil +} + +func unmarshallQueryExecutors(raw []json.RawMessage) ([]QueryExecutor, error) { + var queryExecutors []QueryExecutor + + // manually decide, which QueryExecutor implementation to use based on the "kind" field + for _, rawExecutor := range raw { + var typeIndicator struct { + Kind string `json:"kind"` + } + if err := json.Unmarshal(rawExecutor, &typeIndicator); err != nil { + return nil, err + } + + // each new implementation of QueryExecutor might need a custom JSON unmarshaller + // especially if it's using interface{} fields and when unmarhsalling you would like them + // to have actual types (e.g. []string instead of []interface{}) as that will help + // with type safety and readability + var executor QueryExecutor + switch typeIndicator.Kind { + case "loki": + executor = &LokiQueryExecutor{} + case "direct": + executor = &DirectQueryExecutor{} + case "prometheus": + executor = &PrometheusQueryExecutor{} + default: + return nil, fmt.Errorf("unknown query executor type: %s\nIf you added a new query executor make sure to add a custom JSON unmarshaller to StandardReport.UnmarshalJSON()", typeIndicator.Kind) + } + + if unmarshalErr := json.Unmarshal(rawExecutor, executor); unmarshalErr != nil { + return nil, unmarshalErr + } + + queryExecutors = append(queryExecutors, executor) + } + + return queryExecutors, nil +} + +func convertQueryResults(results map[string]interface{}) (map[string]interface{}, error) { + converted := make(map[string]interface{}) + + var convertToStringSlice = func(v []interface{}, key string) { + strSlice := make([]string, len(v)) + allConverted := true + for i, elem := range v { + str, ok := elem.(string) + if !ok { + // return original slice if we can't convert, because its composed of different types + converted[key] = v + allConverted = false + break + } + strSlice[i] = str + } + if allConverted { + converted[key] = strSlice + } + } + + var convertToIntSlice = func(v []interface{}, key string) { + intSlice := make([]int, len(v)) + allConverted := true + for i, elem := range v { + num, ok := elem.(int) + if !ok { + // return original slice if we can't convert, because its composed of different types + converted[key] = v + allConverted = false + break + } + intSlice[i] = num + } + if allConverted { + converted[key] = intSlice + } + } + + var convertToFloatSlice = func(v []interface{}, key string) { + floatSlice := make([]float64, len(v)) + allConverted := true + for i, elem := range v { + f, ok := elem.(float64) + if !ok { + // return original slice if we can't convert, because its composed of different types + converted[key] = v + allConverted = false + break + } + floatSlice[i] = f + } + if allConverted { + converted[key] = floatSlice + } + } + + for key, value := range results { + switch v := value.(type) { + case string, int, float64: + converted[key] = v + case []interface{}: + if len(v) == 0 { + converted[key] = v + continue + } + // Convert first element to determine slice type + switch v[0].(type) { + case string: + convertToStringSlice(v, key) + case int: + convertToIntSlice(v, key) + case float64: + convertToFloatSlice(v, key) + default: + // do nothing if it's not a type we can convert + converted[key] = v + } + default: + // do nothing if it's not a type we can convert + converted[key] = v + } + } + return converted, nil +} + +// FetchNewStandardReportAndLoadLatestPrevious creates a new standard report for a given commit or tag, +// loads the latest previous report, and checks their comparability. +// It returns the new report, the previous report, and any error encountered during the process. +func FetchNewStandardReportAndLoadLatestPrevious(ctx context.Context, newCommitOrTag string, newReportOpts ...StandardReportOption) (*StandardReport, *StandardReport, error) { + newReport, err := NewStandardReport(newCommitOrTag, newReportOpts...) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to create new report for commit or tag %s", newCommitOrTag) + } + + config := standardReportConfig{} + for _, opt := range newReportOpts { + opt(&config) + } + + var localStorage LocalStorage + + if config.reportDirectory != "" { + localStorage.Directory = config.reportDirectory + } + + previousReport := &StandardReport{ + LocalStorage: localStorage, + } + + if err = previousReport.LoadLatest(newReport.TestName); err != nil { + return nil, nil, errors.Wrapf(err, "failed to load latest report for test %s", newReport.TestName) + } + + if err = newReport.FetchData(ctx); err != nil { + return nil, nil, errors.Wrapf(err, "failed to fetch data for new report") + } + + err = newReport.IsComparable(previousReport) + if err != nil { + return nil, nil, errors.Wrapf(err, "new report is not comparable to previous report") + } + + return newReport, previousReport, nil +} diff --git a/wasp/benchspy/report_test.go b/wasp/benchspy/report_test.go new file mode 100644 index 000000000..7e07c5216 --- /dev/null +++ b/wasp/benchspy/report_test.go @@ -0,0 +1,1703 @@ +package benchspy + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/prometheus/common/model" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var lokiConfig = &wasp.LokiConfig{ + URL: "http://localhost:3100", + TenantID: "test", + BasicAuth: "user:pass", +} + +func TestBenchSpy_NewStandardReport(t *testing.T) { + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + LokiConfig: lokiConfig, + }, + } + + t.Run("successful creation (loki)", func(t *testing.T) { + report, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + assert.NotNil(t, report) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &LokiQueryExecutor{}, report.QueryExecutors[0]) + }) + + t.Run("successful creation (generator)", func(t *testing.T) { + report, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Direct), WithGenerators(basicGen)) + require.NoError(t, err) + assert.NotNil(t, report) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &DirectQueryExecutor{}, report.QueryExecutors[0]) + }) + + t.Run("missing branch label", func(t *testing.T) { + invalidGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + }, + } + _, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(invalidGen)) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing branch or commit labels") + }) + + t.Run("missing commit label", func(t *testing.T) { + invalidGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + }, + } + _, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(invalidGen)) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing branch or commit labels") + }) + + t.Run("missing loki config", func(t *testing.T) { + gen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + // no loki config + }, + } + + report, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(gen)) + require.Nil(t, report) + require.Error(t, err) + assert.Contains(t, err.Error(), "loki config is missing") + }) + + t.Run("nil generator", func(t *testing.T) { + _, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki)) + require.Error(t, err) + }) +} + +func TestBenchSpy_NewStandardReportWithPrometheus(t *testing.T) { + baseTime := time.Now() + promConfig := &PrometheusConfig{ + Url: "http://localhost:9090", + NameRegexPatterns: []string{"node"}, + } + + validGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + LokiConfig: lokiConfig, + }, + } + + t.Run("successful prometheus creation", func(t *testing.T) { + report, err := NewStandardReport("test-commit", + WithStandardQueries(StandardQueryExecutor_Prometheus, StandardQueryExecutor_Loki), + WithGenerators(validGen), + WithPrometheusConfig(promConfig)) + require.NoError(t, err) + assert.NotNil(t, report) + assert.Equal(t, 2, len(report.QueryExecutors)) + assert.IsType(t, &LokiQueryExecutor{}, report.QueryExecutors[0]) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[1]) + }) + + t.Run("successful prometheus creation (multiple name regex)", func(t *testing.T) { + multiPromConfig := &PrometheusConfig{ + Url: "http://localhost:9090", + NameRegexPatterns: []string{"node", "eth"}, + } + + report, err := NewStandardReport("test-commit", + WithStandardQueries(StandardQueryExecutor_Prometheus), + WithGenerators(validGen), + WithPrometheusConfig(multiPromConfig)) + require.NoError(t, err) + assert.NotNil(t, report) + require.Equal(t, 2, len(report.QueryExecutors)) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[0]) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[1]) + firstAsProm := report.QueryExecutors[0].(*PrometheusQueryExecutor) + assert.Equal(t, 6, len(firstAsProm.Queries)) + secondAsProm := report.QueryExecutors[0].(*PrometheusQueryExecutor) + assert.Equal(t, 6, len(secondAsProm.Queries)) + }) + + t.Run("invalid prometheus config (mising url)", func(t *testing.T) { + invalidPromConfig := &PrometheusConfig{ + NameRegexPatterns: []string{"node"}, + } + + _, err := NewStandardReport("test-commit", + WithStandardQueries(StandardQueryExecutor_Prometheus), + WithGenerators(validGen), + WithPrometheusConfig(invalidPromConfig), + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "prometheus url is not set") + }) + + t.Run("invalid prometheus config (mising name regex)", func(t *testing.T) { + invalidPromConfig := &PrometheusConfig{ + Url: "http://localhost:9090", + } + + _, err := NewStandardReport("test-commit", + WithStandardQueries(StandardQueryExecutor_Prometheus), + WithGenerators(validGen), + WithPrometheusConfig(invalidPromConfig), + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "prometheus name regex patterns are not set") + }) +} + +func TestBenchSpy_StandardReport_FetchData_WithMockExecutors(t *testing.T) { + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + }, + } + ctx := context.Background() + + t.Run("successful parallel execution", func(t *testing.T) { + // Create mock executors that simulate successful execution + exec1 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return nil }, + ValidateFn: func() error { return nil }, + } + exec2 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return nil }, + ValidateFn: func() error { return nil }, + } + + report := &StandardReport{ + BasicData: BasicData{ + TestStart: baseTime, + TestEnd: baseTime.Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"basic": basicGen.Cfg}, + }, + QueryExecutors: []QueryExecutor{exec1, exec2}, + } + + err := report.FetchData(ctx) + require.NoError(t, err) + assert.True(t, exec1.ExecuteCalled) + assert.True(t, exec2.ExecuteCalled) + }) + + t.Run("one executor fails execution", func(t *testing.T) { + exec1 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return nil }, + ValidateFn: func() error { return nil }, + } + exec2 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return fmt.Errorf("execution failed") }, + ValidateFn: func() error { return nil }, + } + + report := &StandardReport{ + BasicData: BasicData{ + TestStart: baseTime, + TestEnd: baseTime.Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"basic": basicGen.Cfg}, + }, + QueryExecutors: []QueryExecutor{exec1, exec2}, + } + + err := report.FetchData(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "execution failed") + }) + + t.Run("context cancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(ctx) + + exec1 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { + cancel() + time.Sleep(100 * time.Millisecond) + return nil + }, + ValidateFn: func() error { return nil }, + } + exec2 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { + <-ctx.Done() + return ctx.Err() + }, + ValidateFn: func() error { return nil }, + } + + report := &StandardReport{ + BasicData: BasicData{ + TestStart: baseTime, + TestEnd: baseTime.Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"basic": basicGen.Cfg}, + }, + QueryExecutors: []QueryExecutor{exec1, exec2}, + } + + err := report.FetchData(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("executor returns error", func(t *testing.T) { + exec1 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return nil }, + ValidateFn: func() error { return nil }, + } + exec2 := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return fmt.Errorf("execution failed") }, + ValidateFn: func() error { return nil }, + } + + report := &StandardReport{ + BasicData: BasicData{ + TestStart: baseTime, + TestEnd: baseTime.Add(time.Hour), + GeneratorConfigs: map[string]*wasp.Config{"basic": basicGen.Cfg}, + }, + QueryExecutors: []QueryExecutor{exec1, exec2}, + } + + err := report.FetchData(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), "execution failed") + }) +} + +// MockQueryExecutor implements QueryExecutor interface for testing +type MockQueryExecutor struct { + ExecuteFn func(context.Context) error + ValidateFn func() error + TimeRangeFn func(time.Time, time.Time) + ValidateCalled bool + ExecuteCalled bool + ResultsFn func() map[string]interface{} + KindFn func() string + GeneratorNameFn func() string +} + +func (m *MockQueryExecutor) GeneratorName() string { + return m.GeneratorNameFn() +} + +func (m *MockQueryExecutor) Kind() string { + return m.KindFn() +} + +func (m *MockQueryExecutor) Execute(ctx context.Context) error { + m.ExecuteCalled = true + return m.ExecuteFn(ctx) +} + +func (m *MockQueryExecutor) Validate() error { + m.ValidateCalled = true + return m.ValidateFn() +} + +func (m *MockQueryExecutor) TimeRange(start, end time.Time) { + if m.TimeRangeFn != nil { + m.TimeRangeFn(start, end) + } +} + +func (m *MockQueryExecutor) IsComparable(other QueryExecutor) error { + return nil +} + +func (m *MockQueryExecutor) Results() map[string]interface{} { + return m.ResultsFn() +} + +func TestBenchSpy_StandardReport_UnmarshalJSON(t *testing.T) { + t.Run("valid loki executor", func(t *testing.T) { + jsonData := `{ + "test_name": "test1", + "commit_or_tag": "abc123", + "query_executors": [{ + "kind": "loki", + "generator_name": "some generator", + "queries": { + "test query": "some query" + }, + "query_results": { + "test query": ["1", "2", "3"] + } + }] + }` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.NoError(t, err) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &LokiQueryExecutor{}, report.QueryExecutors[0]) + asLoki := report.QueryExecutors[0].(*LokiQueryExecutor) + assert.NotNil(t, asLoki.Queries["test query"]) + assert.Equal(t, "some query", asLoki.Queries["test query"]) + assert.Equal(t, 1, len(report.QueryExecutors[0].Results())) + assert.IsType(t, []string{}, report.QueryExecutors[0].Results()["test query"]) + asStringSlice, err := ResultsAs([]string{}, report.QueryExecutors[0], "test query") + require.NoError(t, err) + assert.Equal(t, []string{"1", "2", "3"}, asStringSlice["test query"]) + }) + + t.Run("valid direct executor", func(t *testing.T) { + jsonData := `{ + "test_name": "test1", + "commit_or_tag": "abc123", + "query_executors": [{ + "kind": "direct", + "generator_config": { + "generator_name": "test_generator", + "load_type": "vu_schedule", + "schedule": [ + { + "from": 10, + "duration": 15000000000, + "type": "plain", + "time_start": "2024-12-18T12:26:01.578938+01:00", + "time_end": "2024-12-18T12:26:16.579713+01:00" + } + ], + "rate_limit_unit_duration": 1000000000, + "call_timeout": 100000000 + }, + "queries": [ + "test generator query" + ], + "query_results": { + "test generator query": 1.0 + } + }] + }` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.NoError(t, err) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &DirectQueryExecutor{}, report.QueryExecutors[0]) + asGenerator := report.QueryExecutors[0].(*DirectQueryExecutor) + + assert.Equal(t, 1, len(asGenerator.Queries)) + _, keyExists := asGenerator.Queries["test generator query"] + assert.True(t, keyExists, "map should contain the key") + assert.Nil(t, asGenerator.Queries["test generator query"]) + assert.Equal(t, 1, len(asGenerator.Results())) + assert.IsType(t, 0.0, asGenerator.Results()["test generator query"]) + directResults := MustAllDirectResults(&report) + require.NoError(t, err) + assert.Len(t, directResults["test_generator"], 1) + assert.Equal(t, 1.0, directResults["test_generator"]["test generator query"]) + }) + + t.Run("valid prometheus executor (vector)", func(t *testing.T) { + jsonData := `{ + "test_name": "test1", + "commit_or_tag": "abc123", + "query_executors": [{ + "kind": "prometheus", + "queries": { + "rate": "rate(test_metric[5m])" + }, + "query_results": { + "rate": { + "value": [{ + "metric": { + "container_label_framework": "ctf", + "container_label_logging": "promtail", + "container_label_org_opencontainers_image_created": "2024-10-10T15:34:11Z", + "container_label_org_opencontainers_image_description": "\"node of the decentralized oracle network, bridging on and off-chain computation\"", + "container_label_org_opencontainers_image_licenses": "\"MIT\"", + "container_label_org_opencontainers_image_ref_name": "ubuntu", + "container_label_org_opencontainers_image_revision": "5ebb63266ca697f0649633641bbccb436c2c18bb", + "container_label_org_opencontainers_image_source": "\"https://github.com/smartcontractkit/chainlink\"", + "container_label_org_opencontainers_image_title": "chainlink", + "container_label_org_opencontainers_image_url": "\"https://github.com/smartcontractkit/chainlink\"", + "container_label_org_opencontainers_image_version": "2.17.0", + "container_label_org_testcontainers": "true", + "container_label_org_testcontainers_lang": "go", + "container_label_org_testcontainers_reap": "true", + "container_label_org_testcontainers_sessionId": "0e438f13ded27fcd3f85123134091358f9ce5c575c0b14a6f3c8998b4d2e7d14", + "container_label_org_testcontainers_version": "0.34.0", + "cpu": "total", + "id": "/docker/26c2319b0e3f5c4f6103b15c9e656fad1635356c39222d5cbd1076d4d49a1375", + "image": "public.ecr.aws/chainlink/chainlink:v2.17.0-arm64", + "instance": "cadvisor:8080", + "job": "cadvisor", + "name": "node3" + }, + "value": [ + 1733919891.792, + "0.39449004082983885" + ] + }], + "metric_type": "vector" + } + } + }] +}` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.NoError(t, err) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[0]) + asProm := report.QueryExecutors[0].(*PrometheusQueryExecutor) + assert.NotNil(t, asProm.Queries["rate"]) + assert.Equal(t, "rate(test_metric[5m])", asProm.Queries["rate"]) + assert.Equal(t, 1, len(report.QueryExecutors[0].Results())) + + asValue := asProm.MustResultsAsValue() + + assert.IsType(t, model.Vector{}, asValue["rate"]) + asVector := asValue["rate"].(model.Vector) + + assert.Equal(t, 1, len(asVector)) + assert.Equal(t, 0.39449004082983885, float64(asVector[0].Value)) + }) + + t.Run("valid prometheus executor (matrix)", func(t *testing.T) { + jsonData := `{ + "test_name":"test1", + "commit_or_tag":"abc123", + "query_executors":[ + { + "kind":"prometheus", + "queries":{ + "rate":"rate(test_metric[5m])" + }, + "query_results":{ + "rate":{ + "value":[{ + "metric": { + "container_label_framework": "ctf", + "container_label_logging": "promtail", + "container_label_org_opencontainers_image_created": "2024-10-10T15:34:11Z", + "container_label_org_opencontainers_image_description": "\"node of the decentralized oracle network, bridging on and off-chain computation\"", + "container_label_org_opencontainers_image_licenses": "\"MIT\"", + "container_label_org_opencontainers_image_ref_name": "ubuntu", + "container_label_org_opencontainers_image_revision": "5ebb63266ca697f0649633641bbccb436c2c18bb", + "container_label_org_opencontainers_image_source": "\"https://github.com/smartcontractkit/chainlink\"", + "container_label_org_opencontainers_image_title": "chainlink", + "container_label_org_opencontainers_image_url": "\"https://github.com/smartcontractkit/chainlink\"", + "container_label_org_opencontainers_image_version": "2.17.0", + "container_label_org_testcontainers": "true", + "container_label_org_testcontainers_lang": "go", + "container_label_org_testcontainers_reap": "true", + "container_label_org_testcontainers_sessionId": "0e438f13ded27fcd3f85123134091358f9ce5c575c0b14a6f3c8998b4d2e7d14", + "container_label_org_testcontainers_version": "0.34.0", + "cpu": "total", + "id": "/docker/a2f7ab689d98d06f941732a3c04eae867ce56de687d87c7fd1bb1ac8c36a415a", + "image": "public.ecr.aws/chainlink/chainlink:v2.17.0-arm64", + "instance": "cadvisor:8080", + "job": "cadvisor", + "name": "node1" + }, + "values":[[ + 1734010920, + "0.004647525277233765" + ]] + }], + "metric_type":"matrix" + } + } + } + ] + }` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.NoError(t, err) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[0]) + asProm := report.QueryExecutors[0].(*PrometheusQueryExecutor) + assert.NotNil(t, asProm.Queries["rate"]) + assert.Equal(t, "rate(test_metric[5m])", asProm.Queries["rate"]) + assert.Equal(t, 1, len(report.QueryExecutors[0].Results())) + + asValue := asProm.MustResultsAsValue() + + assert.IsType(t, model.Matrix{}, asValue["rate"]) + asMatrix := asValue["rate"].(model.Matrix) + + assert.Equal(t, 1, len(asMatrix)) + assert.Equal(t, 1, len(asMatrix[0].Values)) + assert.Equal(t, 0.004647525277233765, float64(asMatrix[0].Values[0].Value)) + }) + + t.Run("valid prometheus executor (scalar)", func(t *testing.T) { + jsonData := `{ + "test_name": "test1", + "commit_or_tag": "abc123", + "query_executors": [{ + "kind": "prometheus", + "queries": { + "rate": "scalar(quantile(0.95, rate(container_cpu_usage_seconds_total{}[5m])) * 100)" + }, + "query_results": { + "rate": { + "value": [ + 1734012682.065, + "0.4631138973188853" + ], + "metric_type": "scalar" + } + } + }] +}` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.NoError(t, err) + assert.Equal(t, 1, len(report.QueryExecutors)) + assert.IsType(t, &PrometheusQueryExecutor{}, report.QueryExecutors[0]) + asProm := report.QueryExecutors[0].(*PrometheusQueryExecutor) + assert.NotNil(t, asProm.Queries["rate"]) + assert.Equal(t, "scalar(quantile(0.95, rate(container_cpu_usage_seconds_total{}[5m])) * 100)", asProm.Queries["rate"]) + assert.Equal(t, 1, len(report.QueryExecutors[0].Results())) + + asValue := asProm.MustResultsAsValue() + + assert.IsType(t, &model.Scalar{}, asValue["rate"]) + asScalar := asValue["rate"].(*model.Scalar) + + assert.Equal(t, 0.4631138973188853, float64(asScalar.Value)) + }) + + t.Run("unknown executor type", func(t *testing.T) { + jsonData := `{ + "test_name": "test1", + "commit_or_tag": "abc123", + "query_executors": [{ + "kind": "unknown", + "query": "test query" + }] + }` + + var report StandardReport + err := json.Unmarshal([]byte(jsonData), &report) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown query executor type") + }) +} + +func TestBenchSpy_StandardReport_FetchData(t *testing.T) { + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + }, + } + + t.Run("valid fetch", func(t *testing.T) { + basicGen.Cfg.LokiConfig = lokiConfig + report, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + + mockExec := &MockQueryExecutor{ + ExecuteFn: func(ctx context.Context) error { return nil }, + ValidateFn: func() error { return nil }, + } + report.QueryExecutors = []QueryExecutor{mockExec} + + err = report.FetchData(context.Background()) + require.NoError(t, err) + assert.True(t, mockExec.ExecuteCalled) + }) +} + +func TestBenchSpy_StandardReport_IsComparable(t *testing.T) { + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + LokiConfig: lokiConfig, + }, + } + + t.Run("matching reports", func(t *testing.T) { + report1, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + report2, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + + err = report1.IsComparable(report2) + require.NoError(t, err) + }) + + t.Run("different report types", func(t *testing.T) { + report1, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + + // Create a mock reporter that implements Reporter interface + mockReport := &MockReport{} + + err = report1.IsComparable(mockReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected type *StandardReport") + }) + + t.Run("different executors", func(t *testing.T) { + report1, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + + // Create second report with different executor + diffGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen-2", // different name + Labels: map[string]string{ + "branch": "main", + "commit": "def456", // different commit + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(2 * time.Hour)}, // different duration + }, + LokiConfig: lokiConfig, + }, + } + report2, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(diffGen)) + require.NoError(t, err) + + err = report1.IsComparable(report2) + require.Error(t, err) + }) +} + +// MockReport implements Reporter interface for testing +type MockReport struct{} + +func (m *MockReport) FetchData(ctx context.Context) error { return nil } +func (m *MockReport) Store() (string, error) { return "", nil } +func (m *MockReport) Load(testName, commitOrTag string) error { return nil } +func (m *MockReport) LoadLatest(testName string) error { return nil } +func (m *MockReport) IsComparable(other Reporter) error { return nil } +func (m *MockReport) FetchResources(ctx context.Context) error { return nil } + +func TestBenchSpy_StandardReport_Store_Load(t *testing.T) { + // Setup test directory with git repo + tmpDir := t.TempDir() + + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour)}, + }, + LokiConfig: lokiConfig, + }, + } + + t.Run("store and load", func(t *testing.T) { + report, err := NewStandardReport("test-commit", WithStandardQueries(StandardQueryExecutor_Loki), WithGenerators(basicGen)) + require.NoError(t, err) + + storage := LocalStorage{Directory: tmpDir} + report.LocalStorage = storage + + _, err = report.Store() + require.NoError(t, err) + + loadedReport := &StandardReport{LocalStorage: storage} + err = loadedReport.Load(report.TestName, report.CommitOrTag) + require.NoError(t, err) + + assert.Equal(t, report.TestName, loadedReport.TestName) + assert.Equal(t, report.CommitOrTag, loadedReport.CommitOrTag) + }) +} + +func TestBenchSpy_StandardReport_ResultsAs(t *testing.T) { + t.Run("successful type conversion for float64", func(t *testing.T) { + mockExecutor := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": float64(123.45), + "query2": float64(678.90), + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + } + + results, err := ResultsAs(float64(0), mockExecutor) + require.NoError(t, err) + assert.Equal(t, float64(123.45), results["query1"]) + assert.Equal(t, float64(678.90), results["query2"]) + }) + + t.Run("successful type conversion with specific query names", func(t *testing.T) { + mockExecutor := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": "result1", + "query2": "result2", + "query3": "result3", + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + } + + results, err := ResultsAs("", mockExecutor, "query1", "query3") + require.NoError(t, err) + assert.Equal(t, "result1", results["query1"]) + assert.Equal(t, "result3", results["query3"]) + assert.Empty(t, results["query2"]) + }) + + t.Run("failed type conversion", func(t *testing.T) { + mockExecutor := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": "result1", + "query2": 2, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + } + + results, err := ResultsAs("", mockExecutor, "query1", "query2") + require.Error(t, err) + require.Nil(t, results) + require.Contains(t, err.Error(), "failed to cast result to type string") + }) +} + +func TestBenchSpy_ConvertQueryResults(t *testing.T) { + t.Run("successful type conversions", func(t *testing.T) { + type customType struct{} + + t.Run("int conversion", func(t *testing.T) { + input := map[string]interface{}{ + "int_val": 123, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, 0, result["int_val"]) + assert.Equal(t, 123, result["int_val"]) + }) + + t.Run("string conversion", func(t *testing.T) { + input := map[string]interface{}{ + "str_val": "test string", + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, "", result["str_val"]) + assert.Equal(t, "test string", result["str_val"]) + }) + + t.Run("float64 conversion", func(t *testing.T) { + input := map[string]interface{}{ + "float_val": 123.45, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, float64(0), result["float_val"]) + assert.Equal(t, 123.45, result["float_val"]) + }) + + t.Run("[]int conversion", func(t *testing.T) { + input := map[string]interface{}{ + "int_slice": []interface{}{1, 2, 3}, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, []int{}, result["int_slice"]) + assert.Equal(t, []int{1, 2, 3}, result["int_slice"]) + }) + + t.Run("[]string conversion", func(t *testing.T) { + input := map[string]interface{}{ + "str_slice": []interface{}{"a", "b", "c"}, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, []string{}, result["str_slice"]) + assert.Equal(t, []string{"a", "b", "c"}, result["str_slice"]) + }) + + t.Run("float64 slice conversion", func(t *testing.T) { + input := map[string]interface{}{ + "float_slice": []interface{}{1.1, 2.2, 3.3}, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, []float64{}, result["float_slice"]) + assert.Equal(t, []float64{1.1, 2.2, 3.3}, result["float_slice"]) + }) + + t.Run("no conversion (not all are int)", func(t *testing.T) { + input := map[string]interface{}{ + "interface_slice": []interface{}{1, "a", 2.2}, + "other": customType{}, + } + + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, customType{}, result["other"]) + assert.Equal(t, customType{}, result["other"]) + assert.Equal(t, []interface{}{1, "a", 2.2}, result["interface_slice"]) + assert.IsType(t, []interface{}{}, result["interface_slice"]) + }) + + t.Run("no conversion (not all are string)", func(t *testing.T) { + input := map[string]interface{}{ + "interface_slice": []interface{}{"a", 2.2}, + } + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.Equal(t, []interface{}{"a", 2.2}, result["interface_slice"]) + assert.IsType(t, []interface{}{}, result["interface_slice"]) + }) + + t.Run("no conversion (not all are float64)", func(t *testing.T) { + input := map[string]interface{}{ + "interface_slice": []interface{}{1.2, "a", 2}, + } + + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.Equal(t, []interface{}{1.2, "a", 2}, result["interface_slice"]) + assert.IsType(t, []interface{}{}, result["interface_slice"]) + }) + + t.Run("no conversion (custom type)", func(t *testing.T) { + input := map[string]interface{}{ + "other": customType{}, + } + + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, customType{}, result["other"]) + assert.Equal(t, customType{}, result["other"]) + }) + + t.Run("partial", func(t *testing.T) { + input := map[string]interface{}{ + "other": customType{}, + "str_slice": []interface{}{"a", "b", "c"}, + } + + result, err := convertQueryResults(input) + require.NoError(t, err) + assert.IsType(t, customType{}, result["other"]) + assert.Equal(t, customType{}, result["other"]) + assert.IsType(t, []string{}, result["str_slice"]) + assert.Equal(t, []string{"a", "b", "c"}, result["str_slice"]) + }) + }) + + t.Run("error cases", func(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + result, err := convertQueryResults(nil) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{}, result) + }) + }) +} + +func TestBenchSpy_MustAllResults(t *testing.T) { + t.Run("MustAllLokiResults", func(t *testing.T) { + mockLokiExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": []string{"log1", "log2"}, + "query2": []string{"log3", "log4"}, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockLokiExec}, + } + + results := MustAllLokiResults(sr) + assert.Equal(t, 1, len(results)) + assert.Equal(t, 2, len(results["generator"])) + assert.Equal(t, []string{"log1", "log2"}, results["generator"]["query1"]) + assert.Equal(t, []string{"log3", "log4"}, results["generator"]["query2"]) + }) + + t.Run("MustAllLokiResults - two executors", func(t *testing.T) { + firstMockLokiExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": []string{"log1", "log2"}, + "query2": []string{"log3", "log4"}, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + secondMockLokiExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query3": []string{"log5", "log6"}, + "query4": []string{"log7", "log8"}, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + GeneratorNameFn: func() string { return "generator2" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{firstMockLokiExec, secondMockLokiExec}, + } + + results := MustAllLokiResults(sr) + assert.Equal(t, 2, len(results)) + assert.Equal(t, 2, len(results["generator"])) + assert.Equal(t, 2, len(results["generator2"])) + assert.Equal(t, []string{"log1", "log2"}, results["generator"]["query1"]) + assert.Equal(t, []string{"log3", "log4"}, results["generator"]["query2"]) + assert.Equal(t, []string{"log5", "log6"}, results["generator2"]["query3"]) + assert.Equal(t, []string{"log7", "log8"}, results["generator2"]["query4"]) + }) + + t.Run("MustAllDirectResults", func(t *testing.T) { + mockGenExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": 1.0, + "query2": 2.0, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Direct) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockGenExec}, + } + + results := MustAllDirectResults(sr) + assert.Equal(t, 1, len(results)) + assert.Equal(t, 2, len(results["generator"])) + assert.Equal(t, 1.0, results["generator"]["query1"]) + assert.Equal(t, 2.0, results["generator"]["query2"]) + }) + + t.Run("MustAllPrometheusResults", func(t *testing.T) { + vector1 := model.Vector{ + &model.Sample{ + Value: 1.23, + }, + } + vector2 := model.Vector{ + &model.Sample{ + Value: 4.56, + }, + } + + mockPromExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": vector1, + "query2": vector2, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Prometheus) + }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockPromExec}, + } + + results := MustAllPrometheusResults(sr) + assert.Equal(t, 2, len(results)) + assert.Equal(t, model.Vector(vector1), results["query1"].(model.Vector)) + assert.Equal(t, model.Vector(vector2), results["query2"].(model.Vector)) + }) + + t.Run("MustAllLokiResults panics on wrong type", func(t *testing.T) { + mockExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": 123, // wrong type + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockExec}, + } + + assert.Panics(t, func() { + MustAllLokiResults(sr) + }) + }) + + t.Run("MustAllGeneratorResults panics on wrong type", func(t *testing.T) { + mockExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "query1": []string{"wrong", "type"}, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Direct) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockExec}, + } + + assert.Panics(t, func() { + MustAllDirectResults(sr) + }) + }) + + t.Run("Results from mixed executors", func(t *testing.T) { + mockLokiExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "loki_query": []string{"log1", "log2"}, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Loki) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + mockGenExec := &MockQueryExecutor{ + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + "gen_query": 1.0, + } + }, + KindFn: func() string { + return string(StandardQueryExecutor_Direct) + }, + GeneratorNameFn: func() string { return "generator" }, + } + + sr := &StandardReport{ + QueryExecutors: []QueryExecutor{mockLokiExec, mockGenExec}, + } + + lokiResults := MustAllLokiResults(sr) + assert.Equal(t, 1, len(lokiResults)) + assert.Equal(t, 1, len(lokiResults["generator"])) + assert.Equal(t, []string{"log1", "log2"}, lokiResults["generator"]["loki_query"]) + + directResults := MustAllDirectResults(sr) + assert.Equal(t, 1, len(directResults)) + assert.Equal(t, 1, len(directResults["generator"])) + assert.Equal(t, 1.0, directResults["generator"]["gen_query"]) + }) +} + +func TestBenchSpy_FetchNewReportAndLoadLatestPrevious(t *testing.T) { + baseTime := time.Now() + basicGen := &wasp.Generator{ + Cfg: &wasp.Config{ + T: t, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour), From: 1, Duration: 2 * time.Second}, + }, + LokiConfig: lokiConfig, + }, + } + + t.Run("successful execution", func(t *testing.T) { + tmpDir := t.TempDir() + + cfg := &wasp.Config{ + T: t, + LoadType: wasp.RPS, + GenName: "test-gen", + Labels: map[string]string{ + "branch": "main", + "commit": "abc123", + }, + Schedule: []*wasp.Segment{ + {StartTime: baseTime, EndTime: baseTime.Add(time.Hour), From: 1, Duration: 2 * time.Second, Type: wasp.SegmentType_Plain}, + }, + } + + fakeGun := &fakeGun{ + maxSuccesses: 4, + maxFailures: 3, + schedule: cfg.Schedule[0], + } + + cfg.Gun = fakeGun + + gen, err := wasp.NewGenerator(cfg) + require.NoError(t, err) + + gen.Run(true) + + prevReport, err := NewStandardReport("a7fc5826a572c09f8b93df3b9f674113372ce924", + WithStandardQueries(StandardQueryExecutor_Direct), + WithGenerators(gen), + WithReportDirectory(tmpDir)) + require.NoError(t, err) + _, err = prevReport.Store() + require.NoError(t, err) + + newReport, prevLoadedReport, err := FetchNewStandardReportAndLoadLatestPrevious( + context.Background(), + "new-commit", + WithStandardQueries(StandardQueryExecutor_Direct), + WithGenerators(gen), + WithReportDirectory(tmpDir), + ) + require.NoError(t, err) + + assert.NotNil(t, newReport) + assert.NotNil(t, prevLoadedReport) + assert.Equal(t, "a7fc5826a572c09f8b93df3b9f674113372ce924", prevLoadedReport.CommitOrTag) + }) + + t.Run("no previous report", func(t *testing.T) { + tmpDir := t.TempDir() + + basicGen.Cfg.T = t + newReport, prevReport, err := FetchNewStandardReportAndLoadLatestPrevious( + context.Background(), + "new-commit-7", + WithStandardQueries(StandardQueryExecutor_Direct), + WithGenerators(basicGen), + WithReportDirectory(tmpDir), + ) + + assert.Error(t, err) + assert.Nil(t, newReport) + assert.Nil(t, prevReport) + }) +} +func TestBenchSpy_CompareDirectWithThresholds(t *testing.T) { + t.Run("metrics within thresholds", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 100.0, + string(Percentile95Latency): 200.0, + string(MaxLatency): 300.0, + string(ErrorRate): 1.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 105.0, // 5% increase + string(Percentile95Latency): 210.0, // 5% increase + string(MaxLatency): 315.0, // 5% increase + string(ErrorRate): 1.05, // 5% increase + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + assert.False(t, failed) + assert.Empty(t, errs) + }) + + t.Run("one metric exceed thresholds", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 100.0, + string(Percentile95Latency): 200.0, + string(MaxLatency): 300.0, + string(ErrorRate): 1.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 150.0, // 50% increase + string(Percentile95Latency): 200.0, // no increase + string(MaxLatency): 300.0, // no increase + string(ErrorRate): 1.0, // no increase + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 1.0, 1.0, 1.0, currentReport, previousReport) + assert.True(t, failed) + assert.Len(t, errs, 1) + assert.Len(t, errs["test-gen"], 1) + for _, err := range errs["test-gen"] { + assert.Contains(t, err.Error(), "different, which is higher than the threshold") + } + }) + + t.Run("all metrics exceed thresholds", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 100.0, + string(Percentile95Latency): 200.0, + string(MaxLatency): 300.0, + string(ErrorRate): 1.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 150.0, // 50% increase + string(Percentile95Latency): 300.0, // 50% increase + string(MaxLatency): 450.0, // 50% increase + string(ErrorRate): 2.0, // 100% increase + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + assert.True(t, failed) + assert.Len(t, errs, 1) + assert.Len(t, errs["test-gen"], 4) + for _, err := range errs["test-gen"] { + assert.Contains(t, err.Error(), "different, which is higher than the threshold") + } + }) + + t.Run("handle zero values", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 0.0, + string(Percentile95Latency): 0.0, + string(MaxLatency): 0.0, + string(ErrorRate): 0.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 0.0, + string(Percentile95Latency): 0.0, + string(MaxLatency): 0.0, + string(ErrorRate): 0.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + assert.False(t, failed) + assert.Empty(t, errs) + }) + + t.Run("handle missing metrics", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 100.0, + // missing other metrics + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 105.0, + // missing other metrics + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + assert.True(t, failed) + assert.Len(t, errs, 1) + assert.Len(t, errs["test-gen"], 3) // Should have errors for missing P95, Max, and Error Rate + for _, err := range errs["test-gen"] { + assert.Contains(t, err.Error(), "results were missing") + } + }) + + t.Run("handle zero to non-zero transition", func(t *testing.T) { + previousReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 0.0, + string(Percentile95Latency): 0.0, + string(MaxLatency): 0.0, + string(ErrorRate): 0.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + currentReport := &StandardReport{ + BasicData: BasicData{ + GeneratorConfigs: map[string]*wasp.Config{ + "test-gen": { + GenName: "test-gen", + }, + }, + }, + QueryExecutors: []QueryExecutor{ + &MockQueryExecutor{ + KindFn: func() string { return string(StandardQueryExecutor_Direct) }, + ResultsFn: func() map[string]interface{} { + return map[string]interface{}{ + string(MedianLatency): 100.0, + string(Percentile95Latency): 200.0, + string(MaxLatency): 300.0, + string(ErrorRate): 1.0, + } + }, + GeneratorNameFn: func() string { return "test-gen" }, + }, + }, + } + + failed, errs := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + assert.True(t, failed) + assert.Len(t, errs, 1) + assert.Len(t, errs["test-gen"], 4) + for _, err := range errs["test-gen"] { + assert.Contains(t, err.Error(), "100.0000% different") + } + }) +} + +func TestBenchSpy_Standard_Direct_Metrics_Two_Generators_E2E(t *testing.T) { + p := wasp.NewProfile() + + p.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu1", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 5*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + })) + + p.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu2", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 7*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 60 * time.Millisecond, + }), + })) + + _, runErr := p.Run(true) + require.NoError(t, runErr) + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + previousReport, err := NewStandardReport( + "v1", + WithStandardQueries(StandardQueryExecutor_Direct), + WithGenerators(p.Generators[0], p.Generators[1]), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchErr := previousReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + p2 := wasp.NewProfile() + p2.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu1", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 5*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + })) + + p2.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu2", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 7*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 60 * time.Millisecond, + }), + })) + + _, runErr = p2.Run(true) + require.NoError(t, runErr) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, err := NewStandardReport( + "v2", + WithStandardQueries(StandardQueryExecutor_Direct), + WithGenerators(p2.Generators[0], p2.Generators[1]), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchErr = currentReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + hasErrors, errors := CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors)) +} diff --git a/wasp/benchspy/storage.go b/wasp/benchspy/storage.go new file mode 100644 index 000000000..b93c85b78 --- /dev/null +++ b/wasp/benchspy/storage.go @@ -0,0 +1,197 @@ +package benchspy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +const DEFAULT_DIRECTORY = "performance_reports" + +type LocalStorage struct { + Directory string `json:"directory"` +} + +func (l *LocalStorage) defaultDirectoryIfEmpty() { + if l.Directory == "" { + l.Directory = DEFAULT_DIRECTORY + } +} + +func (l *LocalStorage) cleanTestName(testName string) string { + // nested tests might contain slashes, replace them with underscores + return strings.ReplaceAll(testName, "/", "_") +} + +// Store saves a test report as a JSON file in local storage. +// It organizes reports by test name and commit/tag, ensuring easy retrieval and management. +// Returns the absolute path of the stored report or an error if the operation fails. +func (l *LocalStorage) Store(testName, commitOrTag string, report interface{}) (string, error) { + l.defaultDirectoryIfEmpty() + asJson, err := json.MarshalIndent(report, "", " ") + if err != nil { + return "", err + } + + if _, err := os.Stat(l.Directory); os.IsNotExist(err) { + if err := os.MkdirAll(l.Directory, 0755); err != nil { + return "", errors.Wrapf(err, "failed to create directory %s", l.Directory) + } + } + + cleanTestName := l.cleanTestName(testName) + reportFilePath := filepath.Join(l.Directory, fmt.Sprintf("%s-%s.json", cleanTestName, commitOrTag)) + reportFile, err := os.Create(reportFilePath) + if err != nil { + return "", errors.Wrapf(err, "failed to create file %s", reportFilePath) + } + defer func() { _ = reportFile.Close() }() + + reader := bytes.NewReader(asJson) + _, err = io.Copy(reportFile, reader) + if err != nil { + return "", errors.Wrapf(err, "failed to write to file %s", reportFilePath) + } + + abs, err := filepath.Abs(reportFilePath) + if err != nil { + return reportFilePath, nil + } + + return abs, nil +} + +// Load retrieves a report from local storage based on the specified test name and optional commit or tag. +// It decodes the report into the provided interface, enabling users to access stored test results. +func (l *LocalStorage) Load(testName, commitOrTag string, report interface{}) error { + l.defaultDirectoryIfEmpty() + if testName == "" { + return errors.New("test name is empty. Please set it and try again") + } + + cleanTestName := l.cleanTestName(testName) + + var ref string + if commitOrTag == "" { + var refErr error + ref, refErr = l.findRef(cleanTestName) + if refErr != nil { + return refErr + } + } else { + ref = commitOrTag + } + + reportFilePath := filepath.Join(l.Directory, fmt.Sprintf("%s-%s.json", cleanTestName, ref)) + + reportFile, err := os.Open(reportFilePath) + if err != nil { + return errors.Wrapf(err, "failed to open file %s", reportFilePath) + } + + decoder := json.NewDecoder(reportFile) + if err := decoder.Decode(report); err != nil { + return errors.Wrapf(err, "failed to decode file %s", reportFilePath) + } + + return nil +} + +func (l *LocalStorage) findAllGitlikeReferences(cleanTestName string, entries []fs.DirEntry) ([]string, error) { + var refs []string + for _, entry := range entries { + if !entry.IsDir() && strings.Contains(entry.Name(), cleanTestName) { + parts := strings.Split(entry.Name(), "-") + if len(parts) == 2 { + ref := strings.TrimSuffix(parts[len(parts)-1], ".json") + refs = append(refs, ref) + } else { + return nil, errors.Errorf("invalid file name: %s. Expected: %s-.json", entry.Name(), cleanTestName) + } + } + } + + return refs, nil +} + +func (l *LocalStorage) findLatestGitRef(refs []string) (string, error) { + var ref string + // Find git root + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + cmd.Dir = l.Directory + out, err := cmd.Output() + if err != nil { + return "", errors.Wrap(err, "failed to find git root") + } + gitRoot := strings.TrimSpace(string(out)) + + // Resolve all refs to commit hashes + resolvedRefs := make(map[string]string) + for _, ref := range refs { + cmd = exec.Command("git", "rev-parse", ref) + cmd.Dir = gitRoot + if out, err := cmd.Output(); err == nil { + resolvedRefs[ref] = strings.TrimSpace(string(out)) + } + } + + // Find latest among resolved commits + var commitRefs []string + for _, hash := range resolvedRefs { + commitRefs = append(commitRefs, hash) + } + + args := append([]string{"rev-list", "--topo-order", "--date-order", "--max-count=1"}, commitRefs...) + cmd = exec.Command("git", args...) + cmd.Dir = gitRoot + out, err = cmd.Output() + if err != nil { + return "", errors.Wrap(err, "failed to find latest reference") + } + latestCommit := strings.TrimSpace(string(out)) + + // Find original ref for this commit + foundOriginal := false + for origRef, hash := range resolvedRefs { + if hash == latestCommit { + ref = origRef + foundOriginal = true + break + } + } + + if !foundOriginal { + return "", fmt.Errorf("no file found for latest commit %s. This should never happen", latestCommit) + } + + return ref, nil +} + +func (l *LocalStorage) findRef(cleanTestName string) (string, error) { + entries, err := os.ReadDir(l.Directory) + if err != nil { + return "", errors.Wrap(err, "failed to read storage directory") + } + + refs, refErr := l.findAllGitlikeReferences(cleanTestName, entries) + if refErr != nil { + return "", refErr + } + + switch len(refs) { + case 0: + return "", fmt.Errorf("no reports found in directory %s", l.Directory) + case 1: + return refs[0], nil + default: + return l.findLatestGitRef(refs) + } +} diff --git a/wasp/benchspy/storage_test.go b/wasp/benchspy/storage_test.go new file mode 100644 index 000000000..627aed489 --- /dev/null +++ b/wasp/benchspy/storage_test.go @@ -0,0 +1,535 @@ +package benchspy + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testReport struct { + Data string `json:"data"` +} + +func TestBenchSpy_LocalStorage_Load(t *testing.T) { + tempDir := t.TempDir() + + storage := &LocalStorage{ + Directory: tempDir, + } + + // Create test data + sampleReport := testReport{Data: "test data"} + reportJSON, err := json.MarshalIndent(sampleReport, "", " ") + require.NoError(t, err) + + t.Run("successful load with specific commit", func(t *testing.T) { + // Setup + commitID := "abc123" + fileName := filepath.Join(tempDir, "test-abc123.json") + require.NoError(t, os.WriteFile(fileName, reportJSON, 0644)) + + // Test + var loadedReport testReport + err := storage.Load("test", commitID, &loadedReport) + require.NoError(t, err) + assert.Equal(t, sampleReport.Data, loadedReport.Data) + }) + + t.Run("error when test name is empty", func(t *testing.T) { + var loadedReport testReport + err := storage.Load("", "abc123", &loadedReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "test name is empty") + }) + + t.Run("error when file doesn't exist", func(t *testing.T) { + var loadedReport testReport + err := storage.Load("nonexistent", "abc123", &loadedReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to open file") + }) + + t.Run("error with invalid JSON", func(t *testing.T) { + // Setup + commitID := "def456" + fileName := filepath.Join(tempDir, "test-def456.json") + require.NoError(t, os.WriteFile(fileName, []byte("invalid json"), 0644)) + + // Test + var loadedReport testReport + err := storage.Load("test", commitID, &loadedReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode file") + }) + + t.Run("error when no reports found in directory", func(t *testing.T) { + // Setup empty directory + emptyDir := t.TempDir() + storage := &LocalStorage{ + Directory: emptyDir, + } + + // Attempt to load from empty directory + var loadedReport testReport + err := storage.Load("test", "", &loadedReport) + + // Verify error + require.Error(t, err) + assert.Contains(t, err.Error(), "no reports found in directory") + }) + + t.Run("error when no commits found for latest commit", func(t *testing.T) { + // Setup git repo in temp dir + gitDir := t.TempDir() + cmd := exec.Command("git", "init") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Create two reports + fileName := filepath.Join(gitDir, "test-abc123.json") + require.NoError(t, os.WriteFile(fileName, reportJSON, 0644)) + + fileName = filepath.Join(gitDir, "test-abc1234.json") + require.NoError(t, os.WriteFile(fileName, reportJSON, 0644)) + + // Configure git for test + configCmd := exec.Command("git", "config", "user.email", "test@example.com") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + configCmd = exec.Command("git", "config", "user.name", "Test User") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + storage := &LocalStorage{ + Directory: gitDir, + } + var loadedReport testReport + err := storage.Load("test", "", &loadedReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to find latest reference") + }) + + t.Run("loads latest git tag", func(t *testing.T) { + // Setup git repo in temp dir + gitDir := t.TempDir() + cmd := exec.Command("git", "init") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Configure git for test + configCmd := exec.Command("git", "config", "user.email", "test@example.com") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + configCmd = exec.Command("git", "config", "user.name", "Test User") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + // Disable commit signing + configCmd = exec.Command("git", "config", "--local", "commit.gpgsign", "false") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + // Create test files and tag them + storage := &LocalStorage{Directory: gitDir} + + // First tag with older data + oldReport := testReport{Data: "old data v1.0.0"} + _, err := storage.Store("test", "v1.0.0", oldReport) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "first") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "tag", "v1.0.0") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Second tag with newer data + newReport := testReport{Data: "new data v2.0.0"} + _, err = storage.Store("test", "v2.0.0", newReport) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "second") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "tag", "v2.0.0") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Test loading latest tag - should get v2.0.0 data + var loadedReport testReport + err = storage.Load("test", "", &loadedReport) + require.NoError(t, err) + assert.Equal(t, newReport, loadedReport) + }) + + t.Run("loads latest commit", func(t *testing.T) { + // Setup git repo in temp dir + gitDir := t.TempDir() + cmd := exec.Command("git", "init") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Configure git for test + configCmd := exec.Command("git", "config", "user.email", "test@example.com") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + configCmd = exec.Command("git", "config", "user.name", "Test User") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + // Disable commit signing + configCmd = exec.Command("git", "config", "--local", "commit.gpgsign", "false") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + // Create test files and commit them + storage := &LocalStorage{Directory: gitDir} + + // First commit with older data + oldCommitData := testReport{Data: "old commit data"} + _, err := storage.Store("test", "commit1", oldCommitData) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "first commit") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Get actual commit hash + cmd = exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = gitDir + commitHash1, err := cmd.Output() + require.NoError(t, err) + + // Rename file to use actual commit hash + oldPath := filepath.Join(gitDir, "test-commit1.json") + newPath := filepath.Join(gitDir, fmt.Sprintf("test-%s.json", strings.TrimSpace(string(commitHash1)))) + require.NoError(t, os.Rename(oldPath, newPath)) + + // Second commit with newer data + newCommitData := testReport{Data: "new commit data"} + _, err = storage.Store("test", "commit2", newCommitData) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "second commit") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Get actual commit hash and rename file + cmd = exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = gitDir + commitHash2, err := cmd.Output() + require.NoError(t, err) + oldPath = filepath.Join(gitDir, "test-commit2.json") + newPath = filepath.Join(gitDir, fmt.Sprintf("test-%s.json", strings.TrimSpace(string(commitHash2)))) + require.NoError(t, os.Rename(oldPath, newPath)) + + // Test loading latest commit - should get newest data + var loadedReport testReport + err = storage.Load("test", "", &loadedReport) + require.NoError(t, err) + assert.Equal(t, newCommitData, loadedReport) + }) + + t.Run("prefers newer commits over latest tag", func(t *testing.T) { + // Setup git repo in temp dir + gitDir := t.TempDir() + cmd := exec.Command("git", "init") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Configure git for test + configCmd := exec.Command("git", "config", "user.email", "test@example.com") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + configCmd = exec.Command("git", "config", "user.name", "Test User") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + configCmd = exec.Command("git", "config", "--local", "commit.gpgsign", "false") + configCmd.Dir = gitDir + require.NoError(t, configCmd.Run()) + + storage := &LocalStorage{Directory: gitDir} + + // First tagged commit + v1Data := testReport{Data: "v1.0.0 data"} + _, err := storage.Store("test", "v1.0.0", v1Data) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "first tagged commit") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "tag", "v1.0.0") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Second tagged commit + v2Data := testReport{Data: "v2.0.0 data"} + _, err = storage.Store("test", "v2.0.0", v2Data) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "second tagged commit") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "tag", "v2.0.0") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Third untagged commit (newer than v2.0.0) + newerData := testReport{Data: "newer untagged commit data"} + _, err = storage.Store("test", "HEAD", newerData) + require.NoError(t, err) + cmd = exec.Command("git", "add", ".") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "newer untagged commit") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + cmd = exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = gitDir + commitHash, err := cmd.Output() + require.NoError(t, err) + oldPath := filepath.Join(gitDir, "test-HEAD.json") + newPath := filepath.Join(gitDir, fmt.Sprintf("test-%s.json", strings.TrimSpace(string(commitHash)))) + require.NoError(t, os.Rename(oldPath, newPath)) + + // Test loading - should newer commit + var loadedReport testReport + err = storage.Load("test", "", &loadedReport) + require.NoError(t, err) + assert.Equal(t, newerData, loadedReport) + }) +} + +func TestBenchSpy_LocalStorage_Store(t *testing.T) { + tempDir := t.TempDir() + + storage := &LocalStorage{ + Directory: tempDir, + } + + sampleReport := testReport{Data: "test data"} + + t.Run("successful store with valid data", func(t *testing.T) { + filePath, err := storage.Store("test", "abc123", sampleReport) + require.NoError(t, err) + assert.Contains(t, filePath, "test-abc123.json") + + // Verify file contents + data, err := os.ReadFile(filePath) + require.NoError(t, err) + var savedReport testReport + require.NoError(t, json.Unmarshal(data, &savedReport)) + assert.Equal(t, sampleReport.Data, savedReport.Data) + }) + + t.Run("error with invalid JSON marshaling", func(t *testing.T) { + invalidReport := make(chan int) // channels can't be marshaled + _, err := storage.Store("test", "abc123", invalidReport) + require.Error(t, err) + }) + + t.Run("error with invalid directory permissions", func(t *testing.T) { + // Create read-only directory + readOnlyDir := filepath.Join(tempDir, "readonly") + require.NoError(t, os.MkdirAll(readOnlyDir, 0444)) + + storage := &LocalStorage{ + Directory: readOnlyDir, + } + _, err := storage.Store("test", "abc123", sampleReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create file") + }) + + t.Run("error with directory creation", func(t *testing.T) { + // Try to create directory in a non-existent parent + storage := &LocalStorage{ + Directory: "/nonexistent/path/reports", + } + _, err := storage.Store("test", "abc123", sampleReport) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create directory") + }) + + t.Run("uses default directory when empty", func(t *testing.T) { + storage := &LocalStorage{} // empty directory + _, err := storage.Store("test", "abc123", sampleReport) + require.NoError(t, err) + + // Verify file was created in default directory + _, err = os.Stat(filepath.Join(DEFAULT_DIRECTORY, "test-abc123.json")) + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.RemoveAll(DEFAULT_DIRECTORY) + }) + }) + + t.Run("handles special characters in test name and commit", func(t *testing.T) { + // Use URL-safe special characters instead + filePath, err := storage.Store("test-with_special.chars", "commit-2.0_RC1", sampleReport) + require.NoError(t, err) + + // Verify file exists + _, err = os.Stat(filePath) + require.NoError(t, err) + }) + + t.Run("error with write permissions", func(t *testing.T) { + // Create directory with read-only permission after creation + restrictedDir := filepath.Join(tempDir, "restricted") + require.NoError(t, os.MkdirAll(restrictedDir, 0755)) + require.NoError(t, os.Chmod(restrictedDir, 0444)) + + storage := &LocalStorage{ + Directory: restrictedDir, + } + _, err := storage.Store("test", "abc123", sampleReport) + require.Error(t, err) + + // Restore permissions for cleanup + t.Cleanup(func() { _ = os.Chmod(restrictedDir, 0755) }) + }) +} +func TestBenchSpy_LocalStorage_Load_GitEdgeCases(t *testing.T) { + gitDir := t.TempDir() + + // Init git repo + cmd := exec.Command("git", "init") + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + + // Configure git + for _, config := range [][]string{ + {"user.email", "test@example.com"}, + {"user.name", "Test User"}, + {"commit.gpgsign", "false"}, + } { + cmd := exec.Command("git", "config", "--local", config[0], config[1]) + cmd.Dir = gitDir + require.NoError(t, cmd.Run()) + } + + storage := &LocalStorage{Directory: gitDir} + + t.Run("works with invalid git ref (1 file)", func(t *testing.T) { + testData := testReport{Data: "test"} + _, err := storage.Store("test", "invalid##ref", testData) + require.NoError(t, err) + + var report testReport + err = storage.Load("test", "", &report) + require.NoError(t, err) + }) + + t.Run("error with non-existent commit", func(t *testing.T) { + var report testReport + err := storage.Load("test", "nonexistentcommit", &report) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to open file") + }) + + t.Run("error with invalid git ref (2 files)", func(t *testing.T) { + // Create 2 files with invalid ref + testData := testReport{Data: "test"} + _, err := storage.Store("test", "invalid##ref", testData) + require.NoError(t, err) + + testData = testReport{Data: "test"} + _, err = storage.Store("test", "invalid##ref2", testData) + require.NoError(t, err) + + var report testReport + err = storage.Load("test", "", &report) + require.Error(t, err) + }) + + t.Run("error when git command fails", func(t *testing.T) { + // Create 2 invalid files and break git repo + require.NoError(t, os.RemoveAll(filepath.Join(gitDir, ".git"))) + + testData := testReport{Data: "test"} + _, err := storage.Store("test", "invalid##ref", testData) + require.NoError(t, err) + + testData = testReport{Data: "test"} + _, err = storage.Store("test", "invalid##ref2", testData) + require.NoError(t, err) + + var report testReport + err = storage.Load("test", "", &report) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to find git root") + }) +} + +func TestBenchSpy_LocalStorage_Load_PathEdgeCases(t *testing.T) { + t.Run("loads with relative directory path", func(t *testing.T) { + relDir := "./test_reports" + t.Cleanup(func() { _ = os.RemoveAll(relDir) }) + + storage := &LocalStorage{Directory: relDir} + testData := testReport{Data: "test"} + + // Store and load with relative path + _, err := storage.Store("test", "ref", testData) + require.NoError(t, err) + + var loaded testReport + err = storage.Load("test", "ref", &loaded) + require.NoError(t, err) + assert.Equal(t, testData, loaded) + }) + + t.Run("loads with absolute directory path", func(t *testing.T) { + absDir, err := filepath.Abs(t.TempDir()) + require.NoError(t, err) + + storage := &LocalStorage{Directory: absDir} + testData := testReport{Data: "test"} + + _, err = storage.Store("test", "ref", testData) + require.NoError(t, err) + + var loaded testReport + err = storage.Load("test", "ref", &loaded) + require.NoError(t, err) + assert.Equal(t, testData, loaded) + }) + + t.Run("handles directory with special characters", func(t *testing.T) { + specialDir := filepath.Join(t.TempDir(), "test dir with spaces!") + require.NoError(t, os.MkdirAll(specialDir, 0755)) + + storage := &LocalStorage{Directory: specialDir} + testData := testReport{Data: "test"} + + _, err := storage.Store("test", "ref", testData) + require.NoError(t, err) + + var loaded testReport + err = storage.Load("test", "ref", &loaded) + require.NoError(t, err) + assert.Equal(t, testData, loaded) + }) +} diff --git a/wasp/benchspy/types.go b/wasp/benchspy/types.go new file mode 100644 index 000000000..e44d4579e --- /dev/null +++ b/wasp/benchspy/types.go @@ -0,0 +1,83 @@ +package benchspy + +import ( + "context" + "time" +) + +type Storer interface { + // Store stores the report in a persistent storage and returns the path to it, or an error + Store() (string, error) + // Load loads the report from a persistent storage and returns it, or an error + Load(testName, commitOrTag string) error + // LoadLatest loads the latest report from a persistent storage and returns it, or an error + LoadLatest(testName string) error +} + +type DataFetcher interface { + // Fetch populates the report with the data from the test + FetchData(ctx context.Context) error +} + +type Comparator interface { + // IsComparable checks whether both reports can be compared (e.g. test config is the same, app's resources are the same, queries or metrics used are the same, etc.), and an error if any difference is found + IsComparable(otherReport Reporter) error +} + +type Reporter interface { + Storer + DataFetcher + Comparator +} + +type QueryExecutor interface { + // Kind returns the type of the QueryExecutor + Kind() string + // Validate checks if the QueryExecutor has all the necessary data and configuration to execute the queries + Validate() error + // Execute executes the queries and populates the QueryExecutor with the results + Execute(ctx context.Context) error + // Results returns the results of the queries, where key is the name of the query and value is the result + Results() map[string]interface{} + // IsComparable checks whether both QueryExecutors can be compared (e.g. they have the same type, queries are the same, etc.), and returns an error (if any difference is found) + IsComparable(other QueryExecutor) error + // TimeRange sets the time range for the queries + TimeRange(startTime, endTime time.Time) +} + +type NamedGenerator interface { + // GeneratorName returns the name of the generator + GeneratorName() string +} + +type StandardQueryExecutorType string + +const ( + StandardQueryExecutor_Loki StandardQueryExecutorType = "loki" + StandardQueryExecutor_Direct StandardQueryExecutorType = "direct" + StandardQueryExecutor_Prometheus StandardQueryExecutorType = "prometheus" +) + +type StandardLoadMetric string + +const ( + MedianLatency StandardLoadMetric = "median_latency" + Percentile95Latency StandardLoadMetric = "95th_percentile_latency" + MaxLatency StandardLoadMetric = "max_latency" + ErrorRate StandardLoadMetric = "error_rate" +) + +var StandardLoadMetrics = []StandardLoadMetric{MedianLatency, Percentile95Latency, MaxLatency, ErrorRate} + +type StandardResourceMetric string + +const ( + MedianCPUUsage StandardResourceMetric = "median_cpu_usage" + MedianMemUsage StandardResourceMetric = "median_mem_usage" + P95CPUUsage StandardResourceMetric = "p95_cpu_usage" + MaxCPUUsage StandardResourceMetric = "max_cpu_usage" + P95MemUsage StandardResourceMetric = "p95_mem_usage" + MaxMemUsage StandardResourceMetric = "max_mem_usage" +) + +var StandardResourceMetrics = []StandardResourceMetric{MedianCPUUsage, MedianMemUsage, P95CPUUsage, P95MemUsage, MaxCPUUsage, MaxMemUsage} diff --git a/wasp/examples/benchspy/direct_query_executor/direct_query_executor_test.go b/wasp/examples/benchspy/direct_query_executor/direct_query_executor_test.go new file mode 100644 index 000000000..9c8536e0f --- /dev/null +++ b/wasp/examples/benchspy/direct_query_executor/direct_query_executor_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test can be run without external dependencies +func TestBenchSpy_Standard_Direct_Metrics(t *testing.T) { + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen.Run(true) + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + baseLineReport, err := benchspy.NewStandardReport( + "v1", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + // currentReport is the report that we just created (baseLineReport) + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v2", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + + // make sure that previous report is the same as the baseline report + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + hasErrors, errors := benchspy.CompareDirectWithThresholds(1.0, 1.0, 1.0, 1.0, currentReport, previousReport) + require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors)) +} + +func TestBenchSpy_Standard_Direct_Metrics_Two_Generators(t *testing.T) { + gen1, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu1", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen2, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu2", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 20*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 60 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen1.Run(false) + gen2.Run(true) + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + baseLineReport, err := benchspy.NewStandardReport( + "v1", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithGenerators(gen1, gen2), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen1, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu1", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen2, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu2", + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 20*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 60 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen1.Run(false) + newGen2.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + // currentReport is the report that we just created (baseLineReport) + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v2", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithGenerators(newGen1, newGen2), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + + // make sure that previous report is the same as the baseline report + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + hasErrors, errors := benchspy.CompareDirectWithThresholds(10.0, 10.0, 10.0, 10.0, currentReport, previousReport) + require.False(t, hasErrors, fmt.Sprintf("errors found: %v", errors)) +} diff --git a/wasp/examples/benchspy/direct_query_executor/direct_query_real_case_test.go b/wasp/examples/benchspy/direct_query_executor/direct_query_real_case_test.go new file mode 100644 index 000000000..8719c79db --- /dev/null +++ b/wasp/examples/benchspy/direct_query_executor/direct_query_real_case_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test can be run without external dependencies +// it demontsrates a case, where performance degradation was found between two releases +func TestBenchSpy_Standard_Direct_Metrics_RealCase(t *testing.T) { + // uncomment the code below and comment the rest + // to generate the v1.0.0 report + + // generator, err := wasp.NewGenerator(&wasp.Config{ + // T: t, + // GenName: "vu", + // CallTimeout: 100 * time.Millisecond, + // LoadType: wasp.VU, + // Schedule: wasp.Plain(10, 15*time.Second), + // VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + // // notice lower latency + // CallSleep: 50 * time.Millisecond, + // }), + // }) + // require.NoError(t, err) + + // generator.Run(true) + + // fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + // defer cancelFn() + + // baseLineReport, err := benchspy.NewStandardReport( + // "v1.0.0", + // benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + // benchspy.WithGenerators(generator), + // ) + // require.NoError(t, err, "failed to create baseline report") + + // fetchErr := baseLineReport.FetchData(fetchCtx) + // require.NoError(t, fetchErr, "failed to fetch data for original report") + + // path, storeErr := baseLineReport.Store() + // require.NoError(t, storeErr, "failed to store current report", path) + + // comment this part, when generating the v1.0.0 report + generator, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + // increase latency by 10ms + CallSleep: 60 * time.Millisecond, + }), + }) + require.NoError(t, err) + generator.Run(true) + + currentVersion := os.Getenv("CURRENT_VERSION") + require.NotEmpty(t, currentVersion, "No current version provided") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + currentVersion, + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct), + benchspy.WithReportDirectory("test_reports"), + benchspy.WithGenerators(generator), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + + hasErrors, errors := benchspy.CompareDirectWithThresholds(1.0, 1.0, 1.0, 1.0, currentReport, previousReport) + require.True(t, hasErrors, "Found no errors, but expected some") + require.Equal(t, 3, len(errors), "Expected 3 errors, got %d", len(errors)) + + expectedErrors := []benchspy.StandardLoadMetric{benchspy.MedianLatency, benchspy.Percentile95Latency, benchspy.MaxLatency} + var foundErrors []benchspy.StandardLoadMetric + + for _, e := range errors[generator.Cfg.GenName] { + for _, expected := range expectedErrors { + if strings.Contains(e.Error(), string(expected)) { + foundErrors = append(foundErrors, expected) + break + } + } + } + + require.EqualValues(t, expectedErrors, foundErrors, "Expected errors not found") +} diff --git a/wasp/examples/benchspy/direct_query_executor/test_reports/TestBenchSpy_Standard_Direct_Metrics_RealCase-v1.0.0.json b/wasp/examples/benchspy/direct_query_executor/test_reports/TestBenchSpy_Standard_Direct_Metrics_RealCase-v1.0.0.json new file mode 100644 index 000000000..0a21cb969 --- /dev/null +++ b/wasp/examples/benchspy/direct_query_executor/test_reports/TestBenchSpy_Standard_Direct_Metrics_RealCase-v1.0.0.json @@ -0,0 +1,56 @@ +{ + "test_name": "TestBenchSpy_Standard_Direct_Metrics_RealCase", + "commit_or_tag": "v1.0.0", + "test_start_timestamp": "2024-12-18T12:26:01.578938+01:00", + "test_end_timestamp": "2024-12-18T12:26:16.579713+01:00", + "generator_configs": { + "vu": { + "generator_name": "vu", + "load_type": "vu_schedule", + "schedule": [ + { + "from": 10, + "duration": 15000000000, + "type": "plain", + "time_start": "2024-12-18T12:26:01.578938+01:00", + "time_end": "2024-12-18T12:26:16.579713+01:00" + } + ], + "rate_limit_unit_duration": 1000000000, + "call_timeout": 100000000 + } + }, + "directory": "test_reports", + "query_executors": [ + { + "kind": "direct", + "generator_config": { + "generator_name": "vu", + "load_type": "vu_schedule", + "schedule": [ + { + "from": 10, + "duration": 15000000000, + "type": "plain", + "time_start": "2024-12-18T12:26:01.578938+01:00", + "time_end": "2024-12-18T12:26:16.579713+01:00" + } + ], + "rate_limit_unit_duration": 1000000000, + "call_timeout": 100000000 + }, + "queries": [ + "median_latency", + "95th_percentile_latency", + "max_latency", + "error_rate" + ], + "query_results": { + "95th_percentile_latency": 51.0081875, + "error_rate": 0, + "max_latency": 52.136167, + "median_latency": 50.425604 + } + } + ] +} \ No newline at end of file diff --git a/wasp/examples/benchspy/loki_query_executor/loki_query_executor_test.go b/wasp/examples/benchspy/loki_query_executor/loki_query_executor_test.go new file mode 100644 index 000000000..998a53102 --- /dev/null +++ b/wasp/examples/benchspy/loki_query_executor/loki_query_executor_test.go @@ -0,0 +1,344 @@ +package main + +import ( + "context" + "fmt" + "math" + "testing" + "time" + + "github.com/montanaflynn/stats" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test requires CTFv2 observability stack to be running +func TestBenchSpy_Standard_Loki_Metrics(t *testing.T) { + label := "benchspy-std" + + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen.Run(true) + + baseLineReport, err := benchspy.NewStandardReport( + "v1", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v2", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) + allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + + require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty") + require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty") + + currentAsStringSlice := allCurrentAsStringSlice[gen.Cfg.GenName] + previousAsStringSlice := allPreviousAsStringSlice[gen.Cfg.GenName] + + compareAverages(t, string(benchspy.MedianLatency), currentAsStringSlice, previousAsStringSlice, 1.0) + compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice, previousAsStringSlice, 1.0) + compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice, previousAsStringSlice, 1.0) + compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice, previousAsStringSlice, 1.0) +} + +// this test requires CTFv2 observability stack to be running +func TestBenchSpy_Custom_Loki_Metrics(t *testing.T) { + label := "benchspy-custom" + + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen.Run(true) + + lokiQueryExecutor := benchspy.NewLokiQueryExecutor( + gen.Cfg.GenName, + map[string]string{ + "vu_over_time": fmt.Sprintf("max_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"stats\", gen_name=~\"%s\"} | json | unwrap current_instances [10s]) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + "responses_over_time": fmt.Sprintf("sum(count_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} [1s])) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + }, + gen.Cfg.LokiConfig, + ) + + baseLineReport, err := benchspy.NewStandardReport( + "v1", + benchspy.WithQueryExecutors(lokiQueryExecutor), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v2", + benchspy.WithQueryExecutors(lokiQueryExecutor), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) + allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + + require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty") + require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty") + + currentAsStringSlice := allCurrentAsStringSlice[gen.Cfg.GenName] + previousAsStringSlice := allPreviousAsStringSlice[gen.Cfg.GenName] + + compareAverages(t, "vu_over_time", currentAsStringSlice, previousAsStringSlice, 1.0) + compareAverages(t, "responses_over_time", currentAsStringSlice, previousAsStringSlice, 1.0) +} + +var compareAverages = func(t *testing.T, metricName string, currentAsStringSlice, previousAsStringSlice map[string][]string, maxPrecentageDiff float64) { + require.NotEmpty(t, currentAsStringSlice[metricName], "%s results were missing from current report", metricName) + require.NotEmpty(t, previousAsStringSlice[metricName], "%s results were missing from previous report", metricName) + + currentFloatSlice, err := benchspy.StringSliceToFloat64Slice(currentAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + currentMedian, err := stats.Mean(currentFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + previousFloatSlice, err := benchspy.StringSliceToFloat64Slice(previousAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + previousMedian, err := stats.Mean(previousFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + var diffPrecentage float64 + if previousMedian != 0.0 && currentMedian != 0.0 { + diffPrecentage = (currentMedian - previousMedian) / previousMedian * 100 + } else if previousMedian == 0.0 && currentMedian == 0.0 { + diffPrecentage = 0.0 + } else { + diffPrecentage = 100.0 + } + assert.LessOrEqual(t, math.Abs(diffPrecentage), maxPrecentageDiff, "%s medians are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage)) +} + +func TestBenchSpy_Standard_Loki_Metrics_Two_Generators(t *testing.T) { + label := "benchspy-std" + + p1 := wasp.NewProfile() + p1.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu1", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 12*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + })) + + p1.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu2", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(7, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 48 * time.Millisecond, + }), + })) + + _, err := p1.Run(true) + require.NoError(t, err) + + baseLineReport, err := benchspy.NewStandardReport( + "c2cf545d733eef8bad51d685fcb302e277d7ca14", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(p1.Generators[0], p1.Generators[1]), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch data for original report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + p2 := wasp.NewProfile() + p2.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu1", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 200 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(10, 12*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + })) + + p2.Add(wasp.NewGenerator(&wasp.Config{ + T: t, + LokiConfig: wasp.NewEnvLokiConfig(), + GenName: "vu2", + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(7, 15*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 48 * time.Millisecond, + }), + })) + + _, err = p2.Run(true) + require.NoError(t, err) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "c2cf545d733eef8bad51d685fcb302e277d7ca15", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(p2.Generators[0], p2.Generators[1]), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) + allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + + require.Equal(t, 2, len(allCurrentAsStringSlice), "current report doesn't have 2 loki results") + require.Equal(t, 2, len(allPreviousAsStringSlice), "previous report doesn't have 2 loki results") + + currentAsStringSlice_vu1 := allCurrentAsStringSlice[p1.Generators[0].Cfg.GenName] + previousAsStringSlice_vu1 := allPreviousAsStringSlice[p2.Generators[0].Cfg.GenName] + + compareAverages(t, string(benchspy.MedianLatency), currentAsStringSlice_vu1, previousAsStringSlice_vu1, 10.0) + compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice_vu1, previousAsStringSlice_vu1, 10.0) + compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice_vu1, previousAsStringSlice_vu1, 10.0) + compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice_vu1, previousAsStringSlice_vu1, 10.0) + + currentAsStringSlice_vu2 := allCurrentAsStringSlice[p1.Generators[1].Cfg.GenName] + previousAsStringSlice_vu2 := allPreviousAsStringSlice[p2.Generators[1].Cfg.GenName] + + compareAverages(t, string(benchspy.MedianLatency), currentAsStringSlice_vu2, previousAsStringSlice_vu2, 10.0) + compareAverages(t, string(benchspy.Percentile95Latency), currentAsStringSlice_vu2, previousAsStringSlice_vu2, 10.0) + compareAverages(t, string(benchspy.MaxLatency), currentAsStringSlice_vu2, previousAsStringSlice_vu2, 10.0) + compareAverages(t, string(benchspy.ErrorRate), currentAsStringSlice_vu2, previousAsStringSlice_vu2, 10.0) +} diff --git a/wasp/examples/benchspy/prometheus_query_executor/prometheus_query_executor_test.go b/wasp/examples/benchspy/prometheus_query_executor/prometheus_query_executor_test.go new file mode 100644 index 000000000..453f925f0 --- /dev/null +++ b/wasp/examples/benchspy/prometheus_query_executor/prometheus_query_executor_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test requires CTFv2 node_set with observability stack to be running +func TestBenchSpy_Standard_Prometheus_Metrics(t *testing.T) { + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen.Run(true) + + // exclude bootstrap node + promConfig := benchspy.NewPrometheusConfig("node[^0]") + + baseLineReport, err := benchspy.NewStandardReport( + "91ee9e3c903d52de12f3d0c1a07ac3c2a6d141fb", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Prometheus), + benchspy.WithPrometheusConfig(promConfig), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch current report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "2d1fa3532656c51991c0212afce5f80d2914e34f", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Prometheus), + benchspy.WithPrometheusConfig(promConfig), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + currentAsValues := benchspy.MustAllPrometheusResults(currentReport) + previousAsValues := benchspy.MustAllPrometheusResults(previousReport) + + assert.Equal(t, len(currentAsValues), len(previousAsValues), "number of metrics in results should be the same") + assert.Equal(t, 6, len(currentAsValues), "there should be 6 metrics in the report") + + for _, metric := range benchspy.StandardResourceMetrics { + assert.NotEmpty(t, currentAsValues[string(metric)], "current report should contain metric %s", metric) + assert.NotEmpty(t, previousAsValues[string(metric)], "previous report should contain metric %s", metric) + } + + currentMedianCPUUsage := currentAsValues[string(benchspy.MedianCPUUsage)] + previousMedianCPUUsage := previousAsValues[string(benchspy.MedianCPUUsage)] + + assert.Equal(t, currentMedianCPUUsage.Type(), previousMedianCPUUsage.Type(), "types of metrics should be the same") + + currentMedianCPUUsageVector := currentMedianCPUUsage.(model.Vector) + previousMedianCPUUsageVector := previousMedianCPUUsage.(model.Vector) + + assert.Equal(t, len(currentMedianCPUUsageVector), len(previousMedianCPUUsageVector), "number of samples in vectors should be the same") + + // here we could compare actual values, but most likely they will be very different and the test will fail +} + +// this test requires CTFv2 node_set with observability stack to be running +func TestBenchSpy_Custom_Prometheus_Metrics(t *testing.T) { + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + gen.Run(true) + + // no need to not pass name regexp pattern + // we provide them directly in custom queries + promConfig := benchspy.NewPrometheusConfig() + + customPrometheus, err := benchspy.NewPrometheusQueryExecutor( + map[string]string{ + // scalar value + "95p_cpu_all_containers": "scalar(quantile(0.95, rate(container_cpu_usage_seconds_total{name=~\"node[^0]\"}[5m])) * 100)", + // matrix value + "cpu_rate_by_container": "rate(container_cpu_usage_seconds_total{name=~\"node[^0]\"}[1m])[30m:1m]", + }, + promConfig, + ) + require.NoError(t, err) + + baseLineReport, err := benchspy.NewStandardReport( + "91ee9e3c903d52de12f3d0c1a07ac3c2a6d141fb", + benchspy.WithQueryExecutors(customPrometheus), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create baseline report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch current report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "2d1fa3532656c51991c0212afce5f80d2914e340", + benchspy.WithQueryExecutors(customPrometheus), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + currentAsValues := benchspy.MustAllPrometheusResults(currentReport) + previousAsValues := benchspy.MustAllPrometheusResults(previousReport) + + assert.Equal(t, len(currentAsValues), len(previousAsValues), "number of metrics in results should be the same") + + current95CPUUsage := currentAsValues["95p_cpu_all_containers"] + previous95CPUUsage := previousAsValues["95p_cpu_all_containers"] + + assert.Equal(t, current95CPUUsage.Type(), previous95CPUUsage.Type(), "types of metrics should be the same") + assert.IsType(t, current95CPUUsage, &model.Scalar{}, "current metric should be a scalar") + + currentCPUByContainer := currentAsValues["cpu_rate_by_container"] + previousCPUByContainer := previousAsValues["cpu_rate_by_container"] + + assert.Equal(t, currentCPUByContainer.Type(), previousCPUByContainer.Type(), "types of metrics should be the same") + assert.IsType(t, currentCPUByContainer, model.Matrix{}, "current metric should be a scalar") + + current95CPUUsageAsMatrix := currentCPUByContainer.(model.Matrix) + previous95CPUUsageAsMatrix := currentCPUByContainer.(model.Matrix) + + assert.Equal(t, len(current95CPUUsageAsMatrix), len(previous95CPUUsageAsMatrix), "number of samples in matrices should be the same") + + // here we could compare actual values, but most likely they will be very different and the test will fail +} diff --git a/wasp/examples/benchspy/two_query_executors/loki_and_direct_comparison_test.go b/wasp/examples/benchspy/two_query_executors/loki_and_direct_comparison_test.go new file mode 100644 index 000000000..589acc9f9 --- /dev/null +++ b/wasp/examples/benchspy/two_query_executors/loki_and_direct_comparison_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "fmt" + "math" + "strconv" + "testing" + "time" + + "github.com/montanaflynn/stats" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test requires CTFv2 observability stack to be running +func TestBenchSpy_Standard_Direct_And_Loki_Metrics(t *testing.T) { + label := "benchspy-direct-loki" + + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + + gen.Run(true) + + baseLineReport, err := benchspy.NewStandardReport( + "91ee9e3c903d52de12f3d0c1a07ac3c2a6d141fb", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Direct, benchspy.StandardQueryExecutor_Loki), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch current report") + + currentAsLokiSlices := benchspy.MustAllLokiResults(baseLineReport)[gen.Cfg.GenName] + currentAsDirectFloats := benchspy.MustAllDirectResults(baseLineReport)[gen.Cfg.GenName] + + require.NotEmpty(t, currentAsLokiSlices[string(benchspy.MedianLatency)], "%s results were missing for loki", string(benchspy.MedianLatency)) + require.NotEmpty(t, currentAsDirectFloats[string(benchspy.MedianLatency)], "%s results were missing for direct", string(benchspy.MedianLatency)) + + var compareValues = func(t *testing.T, metricName string, lokiFloat, directFloat, maxDiffPrecentage float64) { + var diffPrecentage float64 + if lokiFloat != 0.0 && directFloat != 0.0 { + diffPrecentage = (directFloat - lokiFloat) / lokiFloat * 100 + } else if lokiFloat == 0.0 && directFloat == 0.0 { + diffPrecentage = 0.0 + } else { + diffPrecentage = 100.0 + } + assert.LessOrEqual(t, math.Abs(diffPrecentage), maxDiffPrecentage, "%s are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage)) + } + + lokiFloatSlice, err := benchspy.StringSliceToFloat64Slice(currentAsLokiSlices[string(benchspy.MedianLatency)]) + require.NoError(t, err, "failed to convert %s results to float64 slice", string(benchspy.MedianLatency)) + lokiMedian, err := stats.Median(lokiFloatSlice) + require.NoError(t, err, "failed to calculate median for loki %s results", string(benchspy.MedianLatency)) + + compareValues(t, string(benchspy.MedianLatency), lokiMedian, currentAsDirectFloats[string(benchspy.MedianLatency)], 1.0) + + lokip95, err := stats.Percentile(lokiFloatSlice, 95) + require.NoError(t, err, "failed to calculate 95th percentile for loki %s results", string(benchspy.Percentile95Latency)) + + // here the max diff is 1.5% because of higher impact of data aggregation in loki + compareValues(t, string(benchspy.Percentile95Latency), lokip95, currentAsDirectFloats[string(benchspy.Percentile95Latency)], 1.5) + + lokiErrorRate := 0 + for _, v := range currentAsLokiSlices[string(benchspy.ErrorRate)] { + asInt, err := strconv.Atoi(v) + require.NoError(t, err) + lokiErrorRate += int(asInt) + } + + lokiErrorRate = lokiErrorRate / len(currentAsLokiSlices[string(benchspy.ErrorRate)]) + compareValues(t, string(benchspy.ErrorRate), float64(lokiErrorRate), currentAsDirectFloats[string(benchspy.ErrorRate)], 1.0) +} diff --git a/wasp/examples/benchspy/two_query_executors/two_query_executors_test.go b/wasp/examples/benchspy/two_query_executors/two_query_executors_test.go new file mode 100644 index 000000000..1fcbd930b --- /dev/null +++ b/wasp/examples/benchspy/two_query_executors/two_query_executors_test.go @@ -0,0 +1,244 @@ +package main + +import ( + "context" + "fmt" + "math" + "testing" + "time" + + "github.com/montanaflynn/stats" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/wasp/benchspy" +) + +// this test requires CTFv2 node_set with observability stack to be running +func TestBenchSpy_Standard_Prometheus_And_Loki_Metrics(t *testing.T) { + label := "benchspy-two-query-executors" + + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + + gen.Run(true) + + baseLineReport, err := benchspy.NewStandardReport( + "v1.0.0", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Prometheus, benchspy.StandardQueryExecutor_Loki), + benchspy.WithPrometheusConfig(benchspy.NewPrometheusConfig("node[^0]")), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch current report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v1.1.0", + benchspy.WithStandardQueries(benchspy.StandardQueryExecutor_Prometheus, benchspy.StandardQueryExecutor_Loki), + benchspy.WithPrometheusConfig(benchspy.NewPrometheusConfig("node[^0]")), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) + allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + + require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty") + require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty") + + currentAsLokiSlices := allCurrentAsStringSlice[gen.Cfg.GenName] + previousAsLokiSlices := allPreviousAsStringSlice[gen.Cfg.GenName] + + compareMedian(t, string(benchspy.MedianLatency), currentAsLokiSlices, previousAsLokiSlices) + + currentPromValues := benchspy.MustAllPrometheusResults(currentReport) + previousPromValues := benchspy.MustAllPrometheusResults(previousReport) + + assert.Equal(t, len(currentPromValues), len(previousPromValues), "number of metrics in results should be the same") + + currentMedianCPUUsage := currentPromValues[string(benchspy.MedianCPUUsage)] + previousMedianCPUUsage := previousPromValues[string(benchspy.MedianCPUUsage)] + + assert.Equal(t, currentMedianCPUUsage.Type(), previousMedianCPUUsage.Type(), "types of metrics should be the same") + + currentMedianCPUUsageVector := currentMedianCPUUsage.(model.Vector) + previousMedianCPUUsageVector := previousMedianCPUUsage.(model.Vector) + + assert.Equal(t, len(currentMedianCPUUsageVector), len(previousMedianCPUUsageVector), "number of samples in vectors should be the same") + + // here we could compare actual values, but most likely they will be very different and the test will fail +} + +// this test requires CTFv2 observability stack to be running +func TestBenchSpy_Two_Loki_Executors(t *testing.T) { + label := "benchspy-two-loki-executors" + + gen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + + gen.Run(true) + + firstLokiQueryExecutor := benchspy.NewLokiQueryExecutor( + gen.Cfg.GenName, + map[string]string{ + "vu_over_time": fmt.Sprintf("max_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"stats\", gen_name=~\"%s\"} | json | unwrap current_instances [10s]) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + }, + gen.Cfg.LokiConfig, + ) + + secondLokiQueryExecutor := benchspy.NewLokiQueryExecutor( + gen.Cfg.GenName, + map[string]string{ + "responses_over_time": fmt.Sprintf("sum(count_over_time({branch=~\"%s\", commit=~\"%s\", go_test_name=~\"%s\", test_data_type=~\"responses\", gen_name=~\"%s\"} [1s])) by (node_id, go_test_name, gen_name)", label, label, t.Name(), gen.Cfg.GenName), + }, + gen.Cfg.LokiConfig, + ) + + baseLineReport, err := benchspy.NewStandardReport( + "v1.0.0", + benchspy.WithQueryExecutors(firstLokiQueryExecutor, secondLokiQueryExecutor), + benchspy.WithGenerators(gen), + ) + require.NoError(t, err, "failed to create original report") + + fetchCtx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + fetchErr := baseLineReport.FetchData(fetchCtx) + require.NoError(t, fetchErr, "failed to fetch current report") + + path, storeErr := baseLineReport.Store() + require.NoError(t, storeErr, "failed to store current report", path) + + newGen, err := wasp.NewGenerator(&wasp.Config{ + T: t, + GenName: "vu", + CallTimeout: 100 * time.Millisecond, + LoadType: wasp.VU, + Schedule: wasp.Plain(1, 10*time.Second), + VU: wasp.NewMockVU(&wasp.MockVirtualUserConfig{ + CallSleep: 50 * time.Millisecond, + }), + Labels: map[string]string{ + "branch": label, + "commit": label, + }, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + + newGen.Run(true) + + fetchCtx, cancelFn = context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + currentReport, previousReport, err := benchspy.FetchNewStandardReportAndLoadLatestPrevious( + fetchCtx, + "v1.1.0", + benchspy.WithQueryExecutors(firstLokiQueryExecutor, secondLokiQueryExecutor), + benchspy.WithGenerators(newGen), + ) + require.NoError(t, err, "failed to fetch current report or load the previous one") + require.Equal(t, baseLineReport.CommitOrTag, previousReport.CommitOrTag, "current report should be the same as the original report") + + allCurrentAsStringSlice := benchspy.MustAllLokiResults(currentReport) + allPreviousAsStringSlice := benchspy.MustAllLokiResults(previousReport) + + require.NotEmpty(t, allCurrentAsStringSlice, "current report is empty") + require.NotEmpty(t, allPreviousAsStringSlice, "previous report is empty") + + currentAsLokiSlices := allCurrentAsStringSlice[gen.Cfg.GenName] + previousAsLokiSlices := allPreviousAsStringSlice[gen.Cfg.GenName] + + compareMedian(t, "vu_over_time", currentAsLokiSlices, previousAsLokiSlices) + compareMedian(t, "responses_over_time", currentAsLokiSlices, previousAsLokiSlices) + + // here we could compare actual values, but most likely they will be very different and the test will fail +} + +var compareMedian = func(t *testing.T, metricName string, currentAsStringSlice, previousAsStringSlice map[string][]string) { + require.NotEmpty(t, currentAsStringSlice[metricName], "%s results were missing from current report", metricName) + require.NotEmpty(t, previousAsStringSlice[metricName], "%s results were missing from previous report", metricName) + + currentFloatSlice, err := benchspy.StringSliceToFloat64Slice(currentAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + currentMedian, err := stats.Median(currentFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + previousFloatSlice, err := benchspy.StringSliceToFloat64Slice(previousAsStringSlice[metricName]) + require.NoError(t, err, "failed to convert %s results to float64 slice", metricName) + previousMedian, err := stats.Median(previousFloatSlice) + require.NoError(t, err, "failed to calculate median for %s results", metricName) + + var diffPrecentage float64 + if previousMedian != 0.0 && currentMedian != 0.0 { + diffPrecentage = (currentMedian - previousMedian) / previousMedian * 100 + } else if previousMedian == 0.0 && currentMedian == 0.0 { + diffPrecentage = 0.0 + } else { + diffPrecentage = 100.0 + } + assert.LessOrEqual(t, math.Abs(diffPrecentage), 1.0, "%s medians are more than 1% different", metricName, fmt.Sprintf("%.4f", diffPrecentage)) +} diff --git a/wasp/examples/go.mod b/wasp/examples/go.mod index 390f8edb1..549961745 100644 --- a/wasp/examples/go.mod +++ b/wasp/examples/go.mod @@ -1,63 +1,102 @@ module github.com/smartcontractkit/chainlink-testing-framework/wasp-tests -go 1.22.5 +go 1.23.3 replace github.com/smartcontractkit/chainlink-testing-framework/wasp => ../ require ( github.com/K-Phoen/grabana v0.22.2 github.com/go-resty/resty/v2 v2.15.3 - github.com/smartcontractkit/chainlink-testing-framework/wasp v0.4.9 + github.com/montanaflynn/stats v0.7.1 + github.com/prometheus/common v0.60.0 + github.com/smartcontractkit/chainlink-testing-framework/wasp v1.50.1 github.com/stretchr/testify v1.9.0 go.uber.org/ratelimit v0.3.1 nhooyr.io/websocket v1.8.10 ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/K-Phoen/sdk v0.12.4 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.45.25 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/constructs-go/constructs/v10 v10.4.2 // indirect + github.com/aws/jsii-runtime-go v1.104.0 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b // indirect github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect + github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/coder/websocket v1.8.12 // indirect - github.com/coreos/go-semver v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.3.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect @@ -76,14 +115,19 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v41 v41.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gosimple/slug v1.13.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/grafana/dskit v0.0.0-20231120170505-765e343eda4f // indirect @@ -92,6 +136,7 @@ require ( github.com/grafana/loki v1.6.2-0.20231215164305-b51b7d7b5503 // indirect github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 // indirect github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/consul/api v1.28.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -106,7 +151,9 @@ require ( github.com/hashicorp/serf v0.10.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -115,91 +162,134 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/miekg/dns v1.1.56 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/spdystream v0.4.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/alertmanager v0.26.0 // indirect - github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sercand/kuberesolver/v5 v5.1.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.20-0.20241211181913-305088e7925a // indirect github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/testcontainers/testcontainers-go v0.34.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/x448/float16 v0.8.4 // indirect - go.etcd.io/etcd/api/v3 v3.5.7 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect - go.etcd.io/etcd/client/v3 v3.5.7 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.etcd.io/etcd/api/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect go.opentelemetry.io/collector/pdata v1.0.0-rcv0015 // indirect go.opentelemetry.io/collector/semconv v0.81.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect + go.uber.org/zap v1.27.0 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.26.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.31.2 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/apimachinery v0.31.2 // indirect + k8s.io/cli-runtime v0.31.2 // indirect k8s.io/client-go v0.31.2 // indirect + k8s.io/component-base v0.31.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kubectl v0.31.2 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.17.2 // indirect + sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/wasp/examples/go.sum b/wasp/examples/go.sum index f4f6fb063..94fc4ee72 100644 --- a/wasp/examples/go.sum +++ b/wasp/examples/go.sum @@ -30,7 +30,13 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= @@ -39,6 +45,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybI github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= @@ -66,11 +74,13 @@ github.com/K-Phoen/grabana v0.22.2 h1:tMiSvcKHnDbXi3IgBCax2+sg5qL6x0G6wMURHgjGDa github.com/K-Phoen/grabana v0.22.2/go.mod h1:TbgU7jM55UlExzQyzu6AMiSUXr9jiaXmCu9AN28WXHk= github.com/K-Phoen/sdk v0.12.4 h1:j2EYuBJm3zDTD0fGKACVFWxAXtkR0q5QzfVqxmHSeGQ= github.com/K-Phoen/sdk v0.12.4/go.mod h1:qmM0wO23CtoDux528MXPpYvS4XkRWkWX6rvX9Za8EVU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -100,12 +110,46 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4= github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= +github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 h1:Rrqru2wYkKQCS2IM5/JrgKUQIoNTqA6y/iuxkjzxC6M= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2/go.mod h1:QuCURO98Sqee2AXmqDNxKXYFm2OEDAVAPApMqO0Vqnc= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/constructs-go/constructs/v10 v10.4.2 h1:+hDLTsFGLJmKIn0Dg20vWpKBrVnFrEWYgTEY5UiTEG8= +github.com/aws/constructs-go/constructs/v10 v10.4.2/go.mod h1:cXsNCKDV+9eR9zYYfwy6QuE4uPFp6jsq6TtH1MwBx9w= +github.com/aws/jsii-runtime-go v1.104.0 h1:651Sh6J2FtatfnVzlOQ3/Ye1WWPAseZ6E/tSQxEKdSI= +github.com/aws/jsii-runtime-go v1.104.0/go.mod h1:7ZmQXxV0AAhhvv/GaHX4n6zbgA1tSRVdnQYAJbIhXHk= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -113,8 +157,16 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= +github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -123,12 +175,20 @@ github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdS github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5 h1:rvc39Ol6z3MvaBzXkxFC6Nfsnixq/dRypushKDd7Nc0= +github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5/go.mod h1:R/pdNYDYFQk+tuuOo7QES1kkv6OLmp5ze2XBZQIVffM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f h1:onZ3oc6l1Gz8pVpQ0c1U1Cb11kIMoDb3xtEy/iZbYZM= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -144,29 +204,50 @@ github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0Tx github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/digitalocean/godo v1.99.0 h1:gUHO7n9bDaZFWvbzOum4bXE0/09ZuYA9yA8idQHX57E= github.com/digitalocean/godo v1.99.0/go.mod h1:SsS2oXo2rznfM/nORlZ/6JaUJZFhmKTib1YhopUc8NA= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -183,17 +264,31 @@ github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnv github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= +github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -202,6 +297,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -220,6 +317,11 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= @@ -269,7 +371,10 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= @@ -315,6 +420,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -357,9 +464,12 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= +github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -374,9 +484,11 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -408,6 +520,11 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= @@ -462,6 +579,8 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hetznercloud/hcloud-go/v2 v2.0.0 h1:Sg1DJ+MAKvbYAqaBaq9tPbwXBS2ckPIaMtVdUjKu+4g= github.com/hetznercloud/hcloud-go/v2 v2.0.0/go.mod h1:4iUG2NG8b61IAwNx6UsMWQ6IfIf/i1RsG0BbsKAyR5Q= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -469,12 +588,16 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ionos-cloud/sdk-go/v6 v6.1.8 h1:493wE/BkZxJf7x79UCE0cYGPZoqQcPiEBALvt7uVGY0= github.com/ionos-cloud/sdk-go/v6 v6.1.8/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -520,8 +643,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linode/linodego v1.19.0 h1:n4WJrcr9+30e9JGZ6DI0nZbm5SdAj1kSwvvt/998YUw= github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0Uu5HNWJgQ= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -545,6 +678,11 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -557,6 +695,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -565,6 +705,22 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= +github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -572,27 +728,37 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= @@ -608,8 +774,10 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -625,6 +793,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/alertmanager v0.26.0 h1:uOMJWfIwJguc3NaM3appWNbbrh6G/OjvaHMk22aBBYc= github.com/prometheus/alertmanager v0.26.0/go.mod h1:rVcnARltVjavgVaNnmevxK7kOn7IZavyf0KNgHkbEpU= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -632,8 +802,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -660,6 +830,11 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510 h1:6ksZ7t1hNOzGPPs8DK7SvXQf6UfWzi+W5Z7PCBl8gx4= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510/go.mod h1:UC0TwJiF90m2T3iYPQBKnGu8gv3s55dF/EgpTq8gyvo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -668,6 +843,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.20 h1:a9hSJdJcd16e0HoMsnFvaHvxB3pxSD+SC7+CISp7xY0= @@ -676,6 +853,16 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -683,6 +870,10 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.20-0.20241211181913-305088e7925a h1:mUidQlqrBDqQ06eCFgRQOSt96m76P6SibVkduHQJCVA= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.20-0.20241211181913-305088e7925a/go.mod h1:ag7LEgejsVtPXaUNkcoFPpAoDkl1J8V2HSbqVUxfEtk= github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 h1:VIxK8u0Jd0Q/VuhmsNm6Bls6Tb31H/sA3A/rbc5hnhg= github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0/go.mod h1:lyAu+oMXdNUzEDScj2DXB2IueY+SDXPPfyl/kb63tMM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -695,6 +886,8 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -717,7 +910,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -738,6 +939,8 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -746,12 +949,14 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.etcd.io/etcd/api/v3 v3.5.7 h1:sbcmosSVesNrWOJ58ZQFitHMdncusIifYcrBfwrlJSY= -go.etcd.io/etcd/api/v3 v3.5.7/go.mod h1:9qew1gCdDDLu+VwmeG+iFpL+QlpHTo7iubavdVDgCAA= -go.etcd.io/etcd/client/pkg/v3 v3.5.7 h1:y3kf5Gbp4e4q7egZdn5T7W9TSHUvkClN6u+Rq9mEOmg= -go.etcd.io/etcd/client/pkg/v3 v3.5.7/go.mod h1:o0Abi1MK86iad3YrWhgUsbGx1pmTS+hrORWc2CamuhY= -go.etcd.io/etcd/client/v3 v3.5.7 h1:u/OhpiuCgYY8awOHlhIhmGIGpxfBU/GZBUP3m/3/Iz4= -go.etcd.io/etcd/client/v3 v3.5.7/go.mod h1:sOWmj9DZUMyAngS7QQwCyAXXAL6WhgTOPLNS/NabQgw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= +go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= +go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= +go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= +go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= @@ -766,14 +971,24 @@ go.opentelemetry.io/collector/pdata v1.0.0-rcv0015 h1:8PzrQFk3oKiT1Sd5EmNEcagdMy go.opentelemetry.io/collector/pdata v1.0.0-rcv0015/go.mod h1:I1PqyHJlsXjANC73tp43nDId7/jiv82NoZZ6uS0xdwM= go.opentelemetry.io/collector/semconv v0.81.0 h1:lCYNNo3powDvFIaTPP2jDKIrBiV1T92NK4QgL/aHYXw= go.opentelemetry.io/collector/semconv v0.81.0/go.mod h1:TlYPtzvsXyHOgr5eATi43qEMqwSmIziivJB2uctKswo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -782,8 +997,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -798,11 +1013,12 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -813,8 +1029,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -836,8 +1052,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -879,8 +1095,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -921,6 +1137,7 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -946,6 +1163,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -954,27 +1172,32 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -986,8 +1209,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1041,12 +1264,14 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1069,6 +1294,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1140,6 +1366,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -1160,6 +1388,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1169,14 +1399,22 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= +k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= +k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.31.2 h1:gTxbvRkMBwvTSAlobiTVqsH6S8Aa1aGyBcu5xYLsn8M= +k8s.io/kubectl v0.31.2/go.mod h1:EyASYVU6PY+032RrTh5ahtSOMgoDRIux9V1JLKtG5xM= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= @@ -1186,8 +1424,16 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g= +sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0= +sigs.k8s.io/kustomize/kyaml v0.17.1 h1:TnxYQxFXzbmNG6gOINgGWQt09GghzgTP6mIurOgrLCQ= +sigs.k8s.io/kustomize/kyaml v0.17.1/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/wasp/go.mod b/wasp/go.mod index dd923d734..652769d30 100644 --- a/wasp/go.mod +++ b/wasp/go.mod @@ -1,6 +1,6 @@ module github.com/smartcontractkit/chainlink-testing-framework/wasp -go 1.22.5 +go 1.23 require ( github.com/K-Phoen/grabana v0.22.2 @@ -33,16 +33,16 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.0 // indirect; indirectf + github.com/coreos/go-semver v0.3.1 // indirect; indirectf github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -108,10 +108,10 @@ require ( github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/alertmanager v0.26.0 // indirect - github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97 // indirect @@ -124,29 +124,29 @@ require ( github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/ugorji/go/codec v1.2.12 // indirect - go.etcd.io/etcd/api/v3 v3.5.7 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect - go.etcd.io/etcd/client/v3 v3.5.7 // indirect + go.etcd.io/etcd/api/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect + go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.29.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/protobuf v1.34.2 // indirect @@ -161,55 +161,147 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) +replace github.com/smartcontractkit/chainlink-testing-framework/lib => ../lib + require ( github.com/coder/websocket v1.8.12 github.com/grafana/pyroscope-go v1.1.2 + github.com/montanaflynn/stats v0.7.1 + github.com/olekukonko/tablewriter v0.0.5 + github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.18 github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/constructs-go/constructs/v10 v10.4.2 // indirect + github.com/aws/jsii-runtime-go v1.104.0 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.3.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect + github.com/fatih/camelcase v1.0.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-github/v41 v41.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/grafana/gomemcache v0.0.0-20231023152154-6947259a0586 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/huandu/xstrings v1.3.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect - github.com/onsi/gomega v1.33.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/spdystream v0.4.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/onsi/ginkgo/v2 v2.20.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sercand/kuberesolver/v5 v5.1.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/testcontainers/testcontainers-go v0.34.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opentelemetry.io/collector/pdata v1.0.0-rcv0015 // indirect go.opentelemetry.io/collector/semconv v0.81.0 // indirect + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.8.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect + k8s.io/cli-runtime v0.31.2 // indirect + k8s.io/component-base v0.31.2 // indirect + k8s.io/kubectl v0.31.2 // indirect + sigs.k8s.io/controller-runtime v0.19.0 // indirect + sigs.k8s.io/kustomize/api v0.17.2 // indirect + sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect ) retract [v1.999.0-test-release, v1.999.999-test-release] diff --git a/wasp/go.sum b/wasp/go.sum index 4afeab084..8ff66e0d5 100644 --- a/wasp/go.sum +++ b/wasp/go.sum @@ -30,7 +30,13 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 h1:8q4SaHjFsClSvuVne0ID/5Ka8u3fcIHyqkLjcFpNRHQ= @@ -39,6 +45,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybI github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= @@ -66,11 +74,13 @@ github.com/K-Phoen/grabana v0.22.2 h1:tMiSvcKHnDbXi3IgBCax2+sg5qL6x0G6wMURHgjGDa github.com/K-Phoen/grabana v0.22.2/go.mod h1:TbgU7jM55UlExzQyzu6AMiSUXr9jiaXmCu9AN28WXHk= github.com/K-Phoen/sdk v0.12.4 h1:j2EYuBJm3zDTD0fGKACVFWxAXtkR0q5QzfVqxmHSeGQ= github.com/K-Phoen/sdk v0.12.4/go.mod h1:qmM0wO23CtoDux528MXPpYvS4XkRWkWX6rvX9Za8EVU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -101,12 +111,46 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4= github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ= +github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 h1:Rrqru2wYkKQCS2IM5/JrgKUQIoNTqA6y/iuxkjzxC6M= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2/go.mod h1:QuCURO98Sqee2AXmqDNxKXYFm2OEDAVAPApMqO0Vqnc= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/constructs-go/constructs/v10 v10.4.2 h1:+hDLTsFGLJmKIn0Dg20vWpKBrVnFrEWYgTEY5UiTEG8= +github.com/aws/constructs-go/constructs/v10 v10.4.2/go.mod h1:cXsNCKDV+9eR9zYYfwy6QuE4uPFp6jsq6TtH1MwBx9w= +github.com/aws/jsii-runtime-go v1.104.0 h1:651Sh6J2FtatfnVzlOQ3/Ye1WWPAseZ6E/tSQxEKdSI= +github.com/aws/jsii-runtime-go v1.104.0/go.mod h1:7ZmQXxV0AAhhvv/GaHX4n6zbgA1tSRVdnQYAJbIhXHk= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -114,8 +158,16 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= +github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -124,12 +176,20 @@ github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdS github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g= github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5 h1:rvc39Ol6z3MvaBzXkxFC6Nfsnixq/dRypushKDd7Nc0= +github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5/go.mod h1:R/pdNYDYFQk+tuuOo7QES1kkv6OLmp5ze2XBZQIVffM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f h1:onZ3oc6l1Gz8pVpQ0c1U1Cb11kIMoDb3xtEy/iZbYZM= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240709130330-9f4feec7553f/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -145,29 +205,50 @@ github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0Tx github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/digitalocean/godo v1.99.0 h1:gUHO7n9bDaZFWvbzOum4bXE0/09ZuYA9yA8idQHX57E= github.com/digitalocean/godo v1.99.0/go.mod h1:SsS2oXo2rznfM/nORlZ/6JaUJZFhmKTib1YhopUc8NA= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -184,18 +265,32 @@ github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnv github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= +github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -204,6 +299,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -222,6 +319,11 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= @@ -271,7 +373,10 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= @@ -318,6 +423,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -360,9 +467,12 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= +github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -377,9 +487,11 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -411,6 +523,11 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= @@ -465,6 +582,8 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hetznercloud/hcloud-go/v2 v2.0.0 h1:Sg1DJ+MAKvbYAqaBaq9tPbwXBS2ckPIaMtVdUjKu+4g= github.com/hetznercloud/hcloud-go/v2 v2.0.0/go.mod h1:4iUG2NG8b61IAwNx6UsMWQ6IfIf/i1RsG0BbsKAyR5Q= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -472,12 +591,16 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ionos-cloud/sdk-go/v6 v6.1.8 h1:493wE/BkZxJf7x79UCE0cYGPZoqQcPiEBALvt7uVGY0= github.com/ionos-cloud/sdk-go/v6 v6.1.8/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -524,8 +647,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linode/linodego v1.19.0 h1:n4WJrcr9+30e9JGZ6DI0nZbm5SdAj1kSwvvt/998YUw= github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0Uu5HNWJgQ= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -549,6 +682,11 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -561,6 +699,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -569,6 +709,22 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= +github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -576,27 +732,37 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e h1:4cPxUYdgaGzZIT5/j0IfqOrrXmq6bG8AwvwisMXpdrg= github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo= github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w= @@ -612,8 +778,10 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -629,6 +797,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/alertmanager v0.26.0 h1:uOMJWfIwJguc3NaM3appWNbbrh6G/OjvaHMk22aBBYc= github.com/prometheus/alertmanager v0.26.0/go.mod h1:rVcnARltVjavgVaNnmevxK7kOn7IZavyf0KNgHkbEpU= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -636,8 +806,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -664,6 +834,11 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510 h1:6ksZ7t1hNOzGPPs8DK7SvXQf6UfWzi+W5Z7PCBl8gx4= github.com/prometheus/prometheus v0.47.2-0.20231010075449-4b9c19fe5510/go.mod h1:UC0TwJiF90m2T3iYPQBKnGu8gv3s55dF/EgpTq8gyvo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -672,6 +847,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.20 h1:a9hSJdJcd16e0HoMsnFvaHvxB3pxSD+SC7+CISp7xY0= @@ -680,6 +857,16 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -687,6 +874,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 h1:VIxK8u0Jd0Q/VuhmsNm6Bls6Tb31H/sA3A/rbc5hnhg= github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0/go.mod h1:lyAu+oMXdNUzEDScj2DXB2IueY+SDXPPfyl/kb63tMM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -699,6 +888,8 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -721,7 +912,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -742,6 +941,8 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -750,12 +951,14 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.etcd.io/etcd/api/v3 v3.5.7 h1:sbcmosSVesNrWOJ58ZQFitHMdncusIifYcrBfwrlJSY= -go.etcd.io/etcd/api/v3 v3.5.7/go.mod h1:9qew1gCdDDLu+VwmeG+iFpL+QlpHTo7iubavdVDgCAA= -go.etcd.io/etcd/client/pkg/v3 v3.5.7 h1:y3kf5Gbp4e4q7egZdn5T7W9TSHUvkClN6u+Rq9mEOmg= -go.etcd.io/etcd/client/pkg/v3 v3.5.7/go.mod h1:o0Abi1MK86iad3YrWhgUsbGx1pmTS+hrORWc2CamuhY= -go.etcd.io/etcd/client/v3 v3.5.7 h1:u/OhpiuCgYY8awOHlhIhmGIGpxfBU/GZBUP3m/3/Iz4= -go.etcd.io/etcd/client/v3 v3.5.7/go.mod h1:sOWmj9DZUMyAngS7QQwCyAXXAL6WhgTOPLNS/NabQgw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= +go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= +go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= +go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= +go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= @@ -770,14 +973,24 @@ go.opentelemetry.io/collector/pdata v1.0.0-rcv0015 h1:8PzrQFk3oKiT1Sd5EmNEcagdMy go.opentelemetry.io/collector/pdata v1.0.0-rcv0015/go.mod h1:I1PqyHJlsXjANC73tp43nDId7/jiv82NoZZ6uS0xdwM= go.opentelemetry.io/collector/semconv v0.81.0 h1:lCYNNo3powDvFIaTPP2jDKIrBiV1T92NK4QgL/aHYXw= go.opentelemetry.io/collector/semconv v0.81.0/go.mod h1:TlYPtzvsXyHOgr5eATi43qEMqwSmIziivJB2uctKswo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -786,8 +999,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -802,11 +1015,12 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -820,8 +1034,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -844,8 +1058,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -887,8 +1101,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -929,6 +1143,7 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -954,6 +1169,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -962,27 +1178,32 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -994,8 +1215,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1051,12 +1272,14 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -1083,6 +1306,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1154,6 +1378,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -1174,6 +1400,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1183,14 +1411,22 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= +k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= +k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.31.2 h1:gTxbvRkMBwvTSAlobiTVqsH6S8Aa1aGyBcu5xYLsn8M= +k8s.io/kubectl v0.31.2/go.mod h1:EyASYVU6PY+032RrTh5ahtSOMgoDRIux9V1JLKtG5xM= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= @@ -1198,8 +1434,16 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g= +sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0= +sigs.k8s.io/kustomize/kyaml v0.17.1 h1:TnxYQxFXzbmNG6gOINgGWQt09GghzgTP6mIurOgrLCQ= +sigs.k8s.io/kustomize/kyaml v0.17.1/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/wasp/schedule.go b/wasp/schedule.go index d630b752b..4654b66f5 100644 --- a/wasp/schedule.go +++ b/wasp/schedule.go @@ -18,6 +18,7 @@ func Plain(from int64, duration time.Duration) []*Segment { { From: from, Duration: duration, + Type: SegmentType_Plain, }, } } @@ -33,6 +34,7 @@ func Steps(from, increase int64, steps int, duration time.Duration) []*Segment { segments = append(segments, &Segment{ From: newFrom, Duration: perStepDuration, + Type: SegmentType_Steps, }) } return segments diff --git a/wasp/schedule_test.go b/wasp/schedule_test.go index b7f82a175..b5932721f 100644 --- a/wasp/schedule_test.go +++ b/wasp/schedule_test.go @@ -22,14 +22,17 @@ func TestSmokeSchedules(t *testing.T) { { From: 10, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, { From: 20, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, { From: 30, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, }, }, @@ -40,14 +43,17 @@ func TestSmokeSchedules(t *testing.T) { { From: 100, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, { From: 90, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, { From: 80, Duration: 10 * time.Second, + Type: SegmentType_Steps, }, }, }, @@ -58,6 +64,7 @@ func TestSmokeSchedules(t *testing.T) { { From: 1, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, }, }, @@ -71,10 +78,12 @@ func TestSmokeSchedules(t *testing.T) { { From: 200, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, { From: 300, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, }, }, @@ -89,18 +98,22 @@ func TestSmokeSchedules(t *testing.T) { { From: 1, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, { From: 100, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, { From: 1, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, { From: 100, Duration: 1 * time.Second, + Type: SegmentType_Plain, }, }, }, diff --git a/wasp/scripts/run_benchspy_tests.sh b/wasp/scripts/run_benchspy_tests.sh new file mode 100755 index 000000000..bbd0bbf3b --- /dev/null +++ b/wasp/scripts/run_benchspy_tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +go test -v -race -coverprofile cover.out -count 1 `go list ./... | grep -v examples | grep benchspy` -run TestBenchSpy +coverage=$(go tool cover -func=cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') +if [ -z "$coverage" ]; then + echo "Error: Could not determine test coverage"; + exit 1 +fi + +if [[ $(echo "$coverage < 85" | bc -l) -eq 1 ]]; then + echo "Test coverage $coverage% is below minimum 85%" + exit 1 +fi +echo "Test coverage: $coverage%" \ No newline at end of file diff --git a/wasp/stat.go b/wasp/stat.go deleted file mode 100644 index 3d24589a6..000000000 --- a/wasp/stat.go +++ /dev/null @@ -1,48 +0,0 @@ -package wasp - -import ( - linuxproc "github.com/c9s/goprocinfo/linux" - "github.com/pbnjay/memory" - "github.com/rs/zerolog/log" - "runtime" - "sync" - "time" -) - -var ( - ResourcesThresholdCheckInterval = 5 * time.Second - // CPUIdleThresholdPercentage is default CPU idle threshold - CPUIdleThresholdPercentage = 20 - // MEMFreeThresholdPercentage is default MEM free threshold - MEMFreeThresholdPercentage = 0 -) - -var once = &sync.Once{} - -// CPUCheckLoop continuously monitors CPU idle and memory free percentages. -// It terminates the application if resource thresholds are exceeded. -// Use it to ensure the system operates within defined resource limits. -func CPUCheckLoop() { - once.Do(func() { - //nolint - if runtime.GOOS == "linux" { - go func() { - for { - time.Sleep(ResourcesThresholdCheckInterval) - stat, err := linuxproc.ReadStat("/proc/stat") - if err != nil { - log.Fatal().Err(err).Send() - } - s := stat.CPUStatAll - cpuPerc := float64((s.Idle * 100) / (s.User + s.Nice + s.System + s.Idle + s.IOWait + s.IRQ + s.SoftIRQ + s.Guest + s.GuestNice)) - log.Debug().Float64("CPUIdle", cpuPerc).Msg("Checking CPU load") - freeMemPerc := float64(memory.FreeMemory()*100) / float64(memory.TotalMemory()) - log.Debug().Float64("FreeMEM", freeMemPerc).Msg("Free memory percentage") - if cpuPerc <= float64(CPUIdleThresholdPercentage) || freeMemPerc <= float64(MEMFreeThresholdPercentage) { - log.Fatal().Msgf("Resources threshold was triggered, CPUIdle: %.2f, FreeMEM: %.2f", cpuPerc, freeMemPerc) - } - } - }() - } - }) -} diff --git a/wasp/wasp.go b/wasp/wasp.go index bc96ab2ef..919f22727 100644 --- a/wasp/wasp.go +++ b/wasp/wasp.go @@ -38,6 +38,7 @@ var ( ErrTeardown = errors.New("generator request teardown error") ErrStartFrom = errors.New("from must be > 0") ErrInvalidSegmentDuration = errors.New("SegmentDuration must be defined") + ErrMissingSegmentType = errors.New("Segment Type myst be set") ErrNoGun = errors.New("rps load scheduleSegments selected but gun implementation is nil") ErrNoVU = errors.New("vu load scheduleSegments selected but vu implementation is nil") ErrInvalidLabels = errors.New("invalid Loki labels, labels should be [a-z][A-Z][0-9] and _") @@ -104,10 +105,20 @@ const ( VU ScheduleType = "vu_schedule" ) +type SegmentType string + +const ( + SegmentType_Plain SegmentType = "plain" + SegmentType_Steps SegmentType = "steps" +) + // Segment load test schedule segment type Segment struct { - From int64 - Duration time.Duration + From int64 `json:"from"` + Duration time.Duration `json:"duration"` + Type SegmentType `json:"type"` + StartTime time.Time `json:"time_start"` + EndTime time.Time `json:"time_end"` } // Validate checks that the Segment has a valid starting point and duration. @@ -120,29 +131,33 @@ func (ls *Segment) Validate() error { if ls.Duration == 0 { return ErrInvalidSegmentDuration } + if ls.Type == "" { + return ErrMissingSegmentType + } + return nil } // Config is for shared load test data and configuration type Config struct { - T *testing.T - GenName string - LoadType ScheduleType - Labels map[string]string - LokiConfig *LokiConfig - Schedule []*Segment - RateLimitUnitDuration time.Duration - CallResultBufLen int - StatsPollInterval time.Duration - CallTimeout time.Duration - SetupTimeout time.Duration - TeardownTimeout time.Duration - FailOnErr bool - Gun Gun - VU VirtualUser - Logger zerolog.Logger - SharedData interface{} - SamplerConfig *SamplerConfig + T *testing.T `json:"-"` + GenName string `json:"generator_name"` + LoadType ScheduleType `json:"load_type"` + Labels map[string]string `json:"-"` + LokiConfig *LokiConfig `json:"-"` + Schedule []*Segment `json:"schedule"` + RateLimitUnitDuration time.Duration `json:"rate_limit_unit_duration"` + CallResultBufLen int `json:"-"` + StatsPollInterval time.Duration `json:"-"` + CallTimeout time.Duration `json:"call_timeout"` + SetupTimeout time.Duration `json:"-"` + TeardownTimeout time.Duration `json:"-"` + FailOnErr bool `json:"-"` + Gun Gun `json:"-"` + VU VirtualUser `json:"-"` + Logger zerolog.Logger `json:"-"` + SharedData interface{} `json:"-"` + SamplerConfig *SamplerConfig `json:"-"` // calculated fields duration time.Duration // only available in cluster mode @@ -202,7 +217,6 @@ type Stats struct { CurrentSegment atomic.Int64 `json:"current_schedule_segment"` SamplesRecorded atomic.Int64 `json:"samples_recorded"` SamplesSkipped atomic.Int64 `json:"samples_skipped"` - RunStarted atomic.Bool `json:"runStarted"` RunPaused atomic.Bool `json:"runPaused"` RunStopped atomic.Bool `json:"runStopped"` RunFailed atomic.Bool `json:"runFailed"` @@ -231,6 +245,7 @@ type Generator struct { Log zerolog.Logger labels model.LabelSet rl atomic.Pointer[ratelimit.Limiter] + rpsLoopOnce *sync.Once scheduleSegments []*Segment currentSegmentMu *sync.Mutex currentSegment *Segment @@ -305,6 +320,7 @@ func NewGenerator(cfg *Config) (*Generator, error) { dataCancel: dataCancel, gun: cfg.Gun, vu: cfg.VU, + rpsLoopOnce: &sync.Once{}, Responses: NewResponses(rch), ResponsesChan: rch, labels: ls, @@ -330,43 +346,26 @@ func NewGenerator(cfg *Config) (*Generator, error) { return nil, err } } - CPUCheckLoop() return g, nil } -// runExecuteLoop initiates the generator's execution loop based on the configured load type. -// It manages request pacing for RPS or handles virtual users for load testing scenarios. -func (g *Generator) runExecuteLoop() { - g.currentSegment = g.scheduleSegments[0] - g.stats.LastSegment.Store(int64(len(g.scheduleSegments))) - switch g.Cfg.LoadType { - case RPS: - g.ResponsesWaitGroup.Add(1) - // we run pacedCall controlled by stats.CurrentRPS - go func() { - for { - select { - case <-g.ResponsesCtx.Done(): - g.ResponsesWaitGroup.Done() - g.Log.Info().Msg("RPS generator has stopped") - return - default: - g.pacedCall() - } +// runGunLoop runs the generator's Gun loop +// It manages request pacing for RPS after the first segment is loaded. +func (g *Generator) runGunLoop() { + g.ResponsesWaitGroup.Add(1) + // we run pacedCall controlled by stats.CurrentRPS + go func() { + for { + select { + case <-g.ResponsesCtx.Done(): + g.ResponsesWaitGroup.Done() + g.Log.Info().Msg("RPS generator has stopped") + return + default: + g.pacedCall() } - }() - case VU: - g.currentSegmentMu.Lock() - g.stats.CurrentVUs.Store(g.currentSegment.From) - g.currentSegmentMu.Unlock() - // we start all vus once - vus := g.stats.CurrentVUs.Load() - for i := 0; i < int(vus); i++ { - inst := g.vu.Clone(g) - g.runVU(inst) - g.vus = append(g.vus, inst) } - } + }() } // runSetupWithTimeout executes the VirtualUser's setup within the configured timeout. @@ -463,7 +462,6 @@ func (g *Generator) runVU(vu VirtualUser) { // It returns true when all segments have been handled, signaling the scheduler to terminate. func (g *Generator) processSegment() bool { defer func() { - g.stats.RunStarted.Store(true) g.Log.Info(). Int64("Segment", g.stats.CurrentSegment.Load()). Int64("VUs", g.stats.CurrentVUs.Load()). @@ -477,11 +475,16 @@ func (g *Generator) processSegment() bool { g.currentSegment = g.scheduleSegments[g.stats.CurrentSegment.Load()] g.currentSegmentMu.Unlock() g.stats.CurrentSegment.Add(1) + g.currentSegment.StartTime = time.Now() switch g.Cfg.LoadType { case RPS: newRateLimit := ratelimit.New(int(g.currentSegment.From), ratelimit.Per(g.Cfg.RateLimitUnitDuration), ratelimit.WithoutSlack) g.rl.Store(&newRateLimit) g.stats.CurrentRPS.Store(g.currentSegment.From) + // start Gun loop once, in next segments we control it using g.rl ratelimiter + g.rpsLoopOnce.Do(func() { + g.runGunLoop() + }) case VU: oldVUs := g.stats.CurrentVUs.Load() newVUs := g.currentSegment.From @@ -511,6 +514,8 @@ func (g *Generator) processSegment() bool { // runScheduleLoop initiates an asynchronous loop that processes scheduling segments and monitors for completion signals. // It enables the generator to handle load distribution seamlessly in the background. func (g *Generator) runScheduleLoop() { + g.currentSegment = g.scheduleSegments[0] + g.stats.LastSegment.Store(int64(len(g.scheduleSegments))) go func() { for { select { @@ -527,6 +532,7 @@ func (g *Generator) runScheduleLoop() { return } time.Sleep(g.currentSegment.Duration) + g.currentSegment.EndTime = time.Now() } } }() @@ -604,11 +610,7 @@ func (g *Generator) collectVUResults() { // handling timeouts and storing the response. // It ensures requests adhere to the generator's configuration and execution state. func (g *Generator) pacedCall() { - if !g.Stats().RunStarted.Load() { - return - } - l := *g.rl.Load() - l.Take() + (*g.rl.Load()).Take() if g.stats.RunPaused.Load() { return } @@ -651,7 +653,6 @@ func (g *Generator) Run(wait bool) (interface{}, bool) { g.sendStatsToLoki() } g.runScheduleLoop() - g.runExecuteLoop() g.collectVUResults() if wait { return g.Wait() @@ -679,7 +680,6 @@ func (g *Generator) Stop() (interface{}, bool) { if g.stats.RunStopped.Load() { return nil, true } - g.stats.RunStarted.Store(false) g.stats.RunStopped.Store(true) g.stats.RunFailed.Store(true) g.Log.Warn().Msg("Graceful stop") @@ -693,9 +693,9 @@ func (g *Generator) Wait() (interface{}, bool) { g.ResponsesWaitGroup.Wait() g.stats.Duration = g.Cfg.duration.Nanoseconds() g.stats.CurrentTimeUnit = g.Cfg.RateLimitUnitDuration.Nanoseconds() + g.dataCancel() + g.dataWaitGroup.Wait() if g.Cfg.LokiConfig != nil { - g.dataCancel() - g.dataWaitGroup.Wait() g.stopLokiStream() } return g.GetData(), g.stats.RunFailed.Load() diff --git a/wasp/wasp_bench_test.go b/wasp/wasp_bench_test.go index bdbbfc55c..88b66f137 100644 --- a/wasp/wasp_bench_test.go +++ b/wasp/wasp_bench_test.go @@ -21,7 +21,7 @@ func BenchmarkPacedCall(b *testing.B) { Gun: NewMockGun(&MockGunConfig{}), }) require.NoError(b, err) - gen.runExecuteLoop() + gen.runGunLoop() b.ResetTimer() for i := 0; i < b.N; i++ { gen.pacedCall() diff --git a/wasp/wasp_test.go b/wasp/wasp_test.go index f396455fe..537bdeb9f 100644 --- a/wasp/wasp_test.go +++ b/wasp/wasp_test.go @@ -449,13 +449,13 @@ func TestSmokeStaticRPSSchedulePrecision(t *testing.T) { require.NoError(t, err) _, failed := gen.Run(true) require.Equal(t, false, failed) - require.GreaterOrEqual(t, gen.Stats().Success.Load(), int64(980)) + require.GreaterOrEqual(t, gen.Stats().Success.Load(), int64(950)) require.LessOrEqual(t, gen.Stats().Success.Load(), int64(1010)) require.Equal(t, gen.Stats().Failed.Load(), int64(0)) require.Equal(t, gen.Stats().CallTimeout.Load(), int64(0)) okData, _, failResponses := convertResponsesData(gen) - require.GreaterOrEqual(t, len(okData), 980) + require.GreaterOrEqual(t, len(okData), 950) require.LessOrEqual(t, len(okData), 1010) require.Empty(t, failResponses) require.Empty(t, gen.Errors()) @@ -475,14 +475,14 @@ func TestSmokeCustomUnitPrecision(t *testing.T) { _, failed := gen.Run(true) require.Equal(t, false, failed) stats := gen.Stats() - require.GreaterOrEqual(t, stats.Success.Load(), int64(4970)) + require.GreaterOrEqual(t, stats.Success.Load(), int64(4950)) require.LessOrEqual(t, stats.Success.Load(), int64(5010)) require.Equal(t, stats.Failed.Load(), int64(0)) require.Equal(t, stats.CallTimeout.Load(), int64(0)) require.Equal(t, stats.CurrentTimeUnit, gen.Cfg.RateLimitUnitDuration.Nanoseconds()) okData, _, failResponses := convertResponsesData(gen) - require.GreaterOrEqual(t, len(okData), 4970) + require.GreaterOrEqual(t, len(okData), 4950) require.LessOrEqual(t, len(okData), 5010) require.Empty(t, failResponses) require.Empty(t, gen.Errors()) @@ -501,13 +501,13 @@ func TestSmokeStaticRPSScheduleIsNotBlocking(t *testing.T) { require.NoError(t, err) _, failed := gen.Run(true) require.Equal(t, false, failed) - require.GreaterOrEqual(t, gen.Stats().Success.Load(), int64(980)) + require.GreaterOrEqual(t, gen.Stats().Success.Load(), int64(950)) require.LessOrEqual(t, gen.Stats().Success.Load(), int64(1010)) require.Equal(t, gen.Stats().Failed.Load(), int64(0)) require.Equal(t, gen.Stats().CallTimeout.Load(), int64(0)) okData, _, failResponses := convertResponsesData(gen) - require.GreaterOrEqual(t, len(okData), 980) + require.GreaterOrEqual(t, len(okData), 950) require.LessOrEqual(t, len(okData), 1010) require.Empty(t, failResponses) require.Empty(t, gen.Errors()) @@ -676,10 +676,10 @@ func TestSmokeVUsIncrease(t *testing.T) { okData, okResponses, failResponses := convertResponsesData(gen) require.GreaterOrEqual(t, okResponses[0].Duration, 50*time.Millisecond) - require.GreaterOrEqual(t, len(okResponses), 147) - require.GreaterOrEqual(t, len(okData), 147) + require.GreaterOrEqual(t, len(okResponses), 140) + require.GreaterOrEqual(t, len(okData), 140) require.Equal(t, okResponses[0].Data.(string), "successCallData") - require.Equal(t, okResponses[147].Data.(string), "successCallData") + require.Equal(t, okResponses[140].Data.(string), "successCallData") require.Empty(t, failResponses) require.Empty(t, gen.Errors()) } @@ -706,10 +706,10 @@ func TestSmokeVUsDecrease(t *testing.T) { okData, okResponses, failResponses := convertResponsesData(gen) require.GreaterOrEqual(t, okResponses[0].Duration, 50*time.Millisecond) - require.GreaterOrEqual(t, len(okResponses), 147) - require.GreaterOrEqual(t, len(okData), 147) + require.GreaterOrEqual(t, len(okResponses), 140) + require.GreaterOrEqual(t, len(okData), 140) require.Equal(t, okResponses[0].Data.(string), "successCallData") - require.Equal(t, okResponses[147].Data.(string), "successCallData") + require.Equal(t, okResponses[140].Data.(string), "successCallData") require.Empty(t, failResponses) require.Empty(t, gen.Errors()) }