From d28594d2f97bcd148f31f4661d4febc8e18ceb80 Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:28:41 +0100 Subject: [PATCH 01/23] SecretsManager: Limit of 24kiB for raw input for secure value (#107403) * SecureValues: Add limit of 24kiB for raw input Co-authored-by: Matheus Macabu * Fix lint --------- Co-authored-by: Matheus Macabu --- pkg/apis/secret/v0alpha1/secure_value.go | 2 ++ pkg/apis/secret/v0alpha1/zz_generated.openapi.go | 3 ++- pkg/registry/apis/secret/contracts/secure_value.go | 3 +++ .../apis/secret/reststorage/secure_value_rest.go | 7 +++++++ .../secret/reststorage/secure_value_rest_test.go | 12 ++++++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/apis/secret/v0alpha1/secure_value.go b/pkg/apis/secret/v0alpha1/secure_value.go index 86212f39c02..26a68263568 100644 --- a/pkg/apis/secret/v0alpha1/secure_value.go +++ b/pkg/apis/secret/v0alpha1/secure_value.go @@ -59,7 +59,9 @@ type SecureValueSpec struct { // The raw value is only valid for write. Read/List will always be empty. // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // Minimum and maximum lengths in bytes. // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 Value ExposedSecureValue `json:"value,omitempty"` // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go index 836db012cb0..9bb5f4753e5 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go @@ -641,8 +641,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac }, "value": { SchemaProps: spec.SchemaProps{ - Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.", + Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. Minimum and maximum lengths in bytes.", MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](24576), Type: []string{"string"}, Format: "", }, diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index 25508f16569..ca3f66f991d 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -8,6 +8,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) +// The maximum size of a secure value in bytes when written as raw input. +const SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES = 24576 // 24 KiB + type DecryptSecureValue struct { Keeper *string Ref string diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest.go b/pkg/registry/apis/secret/reststorage/secure_value_rest.go index 824a77869f7..0aed12aac53 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest.go @@ -245,6 +245,13 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss } // General validations. + if len(sv.Spec.Value) > contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES { + errs = append( + errs, + field.TooLong(field.NewPath("spec", "value"), len(sv.Spec.Value), contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES), + ) + } + if errs := validateDecrypters(sv.Spec.Decrypters, decryptersAllowList); len(errs) > 0 { return errs } diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go index 113faf6f0aa..1cde8d63816 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go @@ -4,12 +4,14 @@ import ( "fmt" "maps" "slices" + "strings" "testing" "github.com/stretchr/testify/require" "k8s.io/apiserver/pkg/admission" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" ) func TestValidateSecureValue(t *testing.T) { @@ -50,6 +52,16 @@ func TestValidateSecureValue(t *testing.T) { require.Len(t, errs, 1) require.Equal(t, "spec", errs[0].Field) }) + + t.Run("`value` cannot exceed 24576 bytes", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1)) + sv.Spec.Ref = nil + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 1) + require.Equal(t, "spec.value", errs[0].Field) + }) }) t.Run("when updating a securevalue", func(t *testing.T) { From dec2df83790cc32a6dfb7cf805d43d5b20a1d135 Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:37:49 -0500 Subject: [PATCH 02/23] Docs: Updated the Graphite data source docs (#105426) * initial edits * edits to config doc * query editor updates * ran prettier * updates * made more updates * final edits * ran prettier, updated some descriptions * a few more quick edits * one more definition --- docs/sources/datasources/graphite/_index.md | 125 +++++------- .../datasources/graphite/configure/index.md | 179 ++++++++++++++++++ .../graphite/query-editor/index.md | 90 ++++----- .../graphite/template-variables/index.md | 167 +++++++++------- 4 files changed, 372 insertions(+), 189 deletions(-) create mode 100644 docs/sources/datasources/graphite/configure/index.md diff --git a/docs/sources/datasources/graphite/_index.md b/docs/sources/datasources/graphite/_index.md index 993f40b127d..af2320fe73c 100644 --- a/docs/sources/datasources/graphite/_index.md +++ b/docs/sources/datasources/graphite/_index.md @@ -2,7 +2,7 @@ aliases: - ../data-sources/graphite/ - ../features/datasources/graphite/ -description: Guide for using Graphite in Grafana +description: Introduction to the Graphite data source in Grafana. keywords: - grafana - graphite @@ -46,6 +46,36 @@ refs: destination: /docs/grafana//administration/data-source-management/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//administration/data-source-management/ + transformations: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/query-transform-data/transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/ + alerting: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/ + visualizations: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/visualizations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/ + variables: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/variables/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/ + annotate-visualizations: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/ + set-up-grafana-monitoring: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/set-up-grafana-monitoring/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/set-up-grafana-monitoring/ --- # Graphite data source @@ -54,87 +84,30 @@ Grafana includes built-in support for Graphite. This topic explains options, variables, querying, and other features specific to the Graphite data source, which include its feature-rich query editor. For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:data-source-management). -Only users with the organization administrator role can add data sources. Once you've added the Graphite data source, you can [configure it](#configure-the-data-source) so that your Grafana instance's users can create queries in its [query editor](query-editor/) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore). {{< docs/play title="Graphite: Sample Website Dashboard" url="https://play.grafana.org/d/000000003/" >}} -## Configure the data source - -To configure basic settings for the data source, complete the following steps: - -1. Click **Connections** in the left-side menu. -1. Under Your connections, click **Data sources**. -1. Enter `Graphite` in the search bar. -1. Click **Graphite**. - - The **Settings** tab of the data source is displayed. - -1. Set the data source's basic configuration options: - - | Name | Description | - | ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | - | **Name** | Sets the name you use to refer to the data source in panels and queries. | - | **Default** | Sets whether the data source is pre-selected for new panels. You can set only one default data source per organization. | - | **URL** | Sets the HTTP protocol, IP, and port of your graphite-web or graphite-api installation. | - | **Auth** | For details, refer to [Configure Authentication](ref:configure-authentication). | - | **Basic Auth** | Enables basic authentication to the data source. | - | **User** | Sets the user name for basic authentication. | - | **Password** | Sets the password for basic authentication. | - | **Custom HTTP Headers** | Click **Add header** to add a custom HTTP header. | - | **Header** | Defines the custom header name. | - | **Value** | Defines the custom header value. | - -You can also configure settings specific to the Graphite data source: - -| Name | Description | -| ----------- | -------------------------------------------------------------------------------------------------------- | -| **Version** | Select your version of Graphite. If you are using Grafana Cloud Graphite, this should be set to `1.1.x`. | -| **Type** | Select your type of Graphite. If you are using Grafana Cloud Graphite, this should be set to `Default`. | - -### Integrate with Loki - -When you change the data source selection in [Explore](ref:explore), Graphite queries are converted to Loki queries. -Grafana extracts Loki label names and values from the Graphite queries according to mappings provided in the Graphite data source configuration. -Queries using tags with `seriesByTags()` are also transformed without any additional setup. - -### Provision the data source - -You can define and configure the data source in YAML files as part of Grafana's provisioning system. -For more information about provisioning, and for lists of common configuration options and JSON data options, refer to [Provisioning data sources](ref:provisioning-data-sources). - -#### Provisioning example - -```yaml -apiVersion: 1 - -datasources: - - name: Graphite - type: graphite - access: proxy - url: http://localhost:8080 - jsonData: - graphiteVersion: '1.1' -``` - -## Query the data source - -Grafana includes a Graphite-specific query editor to help you build queries. -The query editor helps you quickly navigate the metric space, add functions, and change function parameters. -It can handle all types of Graphite queries, including complex nested queries through the use of query references. - -For details, refer to the [query editor documentation](query-editor/). - -## Use template variables - -Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. -Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard. -Grafana refers to such variables as template variables. - -For details, see the [template variables documentation](template-variables/). +Grafana exposes metrics for Graphite on the `/metrics` endpoint. +For detailed instructions, refer to [Internal Grafana metrics](ref:internal-grafana-metrics). ## Get Grafana metrics into Graphite Grafana exposes metrics for Graphite on the `/metrics` endpoint. -For detailed instructions, refer to [Internal Grafana metrics](ref:internal-grafana-metrics). +Refer to [Internal Grafana metrics](ref:set-up-grafana-monitoring) for more information. + +## Graphite and Loki integration + +When you change the data source selection in [Explore](ref:explore), Graphite queries are converted to Loki queries. +Grafana extracts Loki label names and values from the Graphite queries according to mappings provided in the Graphite data source configuration. Grafana automatically transforms queries using tags with `seriesByTags()` without requiring additional setup. + +## Get the most out of the data source + +After installing and configuring the Graphite data source you can: + +- Create a wide variety of [visualizations](ref:visualizations) +- Configure and use [templates and variables](ref:variables) +- Add [transformations](ref:transformations) +- Add [annotations](ref:annotate-visualizations) +- Set up [alerting](ref:alerting) diff --git a/docs/sources/datasources/graphite/configure/index.md b/docs/sources/datasources/graphite/configure/index.md new file mode 100644 index 00000000000..d7e03817c45 --- /dev/null +++ b/docs/sources/datasources/graphite/configure/index.md @@ -0,0 +1,179 @@ +--- +aliases: + - ../data-sources/graphite/ + - ../datasources/graphite/ + - ../features/datasources/graphite/ +description: This document provides instructions for configuring the Graphite data source. +keywords: + - grafana + - graphite + - guide +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Configure +title: Configure the Graphite data source +weight: 100 +refs: + explore: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//explore/ + provisioning-data-sources: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/provisioning/#data-sources + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/provisioning/#data-sources + internal-grafana-metrics: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/set-up-grafana-monitoring/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/set-up-grafana-monitoring/ + build-dashboards: + - pattern: /docs/grafana/ + destination: /docs/grafana//dashboards/build-dashboards/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//dashboards/build-dashboards/ + configure-authentication: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-security/configure-authentication/ + data-source-management: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/data-source-management/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/data-source-management/ + private-data-source-connect: + - pattern: /docs/grafana/ + destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + - pattern: /docs/grafana-cloud/ + destination: docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/ + configure-pdc: + - pattern: /docs/grafana/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc +--- + +# Configure the Graphite data source + +This document provides instructions for configuring the Graphite data source and explains available configuration options. For general information on managing data sources, refer to [Data source management](ref:data-source-management). + +## Before you begin + +- You must have the `Organization administrator` role to configure the Graphite data source. + Organization administrators can also [configure the data source via YAML](#provision-the-data-source) with the Grafana provisioning system. + +- Grafana comes with a built-in Graphite data source plugin, eliminating the need to install a plugin. + +- Familiarize yourself with your Graphite security configuration and gather any necessary security certificates and client keys. + +## Add the Graphite data source + +To configure basic settings for the data source, complete the following steps: + +1. Click **Connections** in the left-side menu. +1. Click **Add new connection** +1. Type `Graphite` in the search bar. +1. Select the **Graphite data source**. +1. Click **Add new data source** in the upper right. + +Grafana takes you to the **Settings** tab, where you will set up your Graphite configuration. + +## Configuration options in the UI + +Following is a list of configuration options for Graphite. + +| Setting | Description | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| **Name** | The display name for the data source. This is how you'll reference it in panels and queries.
Examples: `graphite-1`, `graphite-metrics`. | +| **Default** | When enabled, sets this data source as the default for dashboard panels. It will be automatically selected when creating new panels. | + +**HTTP:** + +| Setting | Description | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **URL** | Sets the HTTP protocol, IP, and port of your `graphite-web` or `graphite-api` installation.
Since the access method is set to *Server*, the URL must be accessible from the Grafana backend. | +| **Allowed cookies**| By default, Grafana removes forwarded cookies. Specify cookie names here to allow them to be forwarded to the data source. | +| **Timeout** | Sets the HTTP request timeout in seconds. | + +**Auth:** + +| **Setting** | **Description** | +|------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| **Basic Auth** | Toggle on to enable basic authentication to the data source. | +|   **User** | Sets the username used for basic authentication. | +|   **Password** | Enter the password used for basic authentication. | +| **With Credentials** | Toggle on to include cookies and authentication headers in cross-origin requests. | +| **TLS Client Auth** | Toggle on to enable TLS client authentication (both server and client are verified). | +|   **ServerName** | The server name used to verify the hostname on the certificate returned by the server. | +|   **Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. | +|   **Client Key** | Private key used to encrypt communication between the client and server. Also generated by a CA or self-signed. | +| **With CA Cert** | Toggle on to authenticate with a CA certificate. | +|   **CA Cert** | CA certificate used to validate the server certificate. | +| **Skip TLS Verify** | Toggle on to bypass TLS certificate validation. Not recommended unless necessary or for testing purposes. | +| **Forward OAuth Identity** | Toggle on to forward the user's upstream OAuth identity to the data source. Grafana includes the access token in the request. | + +**Custom HTTP Headers:** + +Pass along additional information and metadata about the request or response. + +| **Setting** | **Description** | +|-------------|--------------------------------------------------------------------------------------------------| +| **Header** | Add a custom header. This allows custom headers to be passed based on the needs of your Graphite instance. | +| **Value** | The value of the header. | + +**Graphite details:** + +| **Setting** | **Description** | +|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Version** | Select your Graphite version from the drop-down. This controls which functions are available in the Graphite query editor. Use `1.1.x` for Grafana Cloud Graphite. | +| **Graphite backend type**| Select the Graphite backend type. Choosing `Metrictank` enables additional features like query processing metadata. (`Metrictank` is a multi-tenant time series engine compatible with Graphite.) Use `Default` for Grafana Cloud Graphite. | +| **Rollup indicator** | Toggle on to display an info icon in panel headers when data aggregation (rollup) occurs. Only available when `Metrictank` is selected. | + +**Label mappings:** + +Label mappings are the rules you define to tell Grafana how to pull pieces of the Graphite metric path into Loki labels when switching data sources. They are currently only supported between Graphite and Loki queries. + +When you change your data source from Graphite to Loki, your queries are automatically mapped based on the rules you define. To create a mapping, specify the full path of the metric and replace the nodes you want to map with label names, using parentheses. The corresponding label values are extracted from your Graphite query during the data source switch. + +Grafana automatically maps all Graphite tags to labels, even if you haven’t defined explicit mappings. When using matching patterns with `{}`(e.g., `metric.{a,b}.value`), Grafana converts them to Loki’s regular expression matching syntax. If your queries include functions, Graphite extracts the relevant metrics and tags, then matches them against your mappings. + +| **Graphite Query** | **Mapped to Loki Query** | +| -------------------------------------------------------- | -------------------------------- | +| `alias(servers.west.001.cpu,1,2)` | `{cluster="west", server="001"}` | +| `alias(servers.*.{001,002}.*,1,2)` | `{server=~"(001,002)"}` | +| `interpolate(seriesByTag('foo=bar', 'server=002'), inf)` | `{foo="bar", server="002"}` | + + +| **Setting** | **Description** | +|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Private data source connect** | _Only for Grafana Cloud users._ Establishes a private, secured connection between a Grafana Cloud stack and data sources within a private network. Use the drop-down to locate the PDC URL. For setup instructions, refer to [Private data source connect (PDC)](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). Click **Manage private data source connect** to open your PDC connection page and view your configuration details. +| + +After configuring your Graphite data source options, click **Save & test** at the bottom to test the connection. You should see a confirmation dialog box that says: + +**Data source is working** + +## Provision the data source + +You can define and configure the data source in YAML files as part of the Grafana provisioning system. +For more information about provisioning, and for lists of common configuration options and JSON data options, refer to [Provisioning data sources](ref:provisioning-data-sources). + +Example Graphite YAML provisioning file: + +```yaml +apiVersion: 1 + +datasources: + - name: Graphite + type: graphite + access: proxy + url: http://localhost:8080 + jsonData: + graphiteVersion: '1.1' +``` diff --git a/docs/sources/datasources/graphite/query-editor/index.md b/docs/sources/datasources/graphite/query-editor/index.md index f185c62aafd..82d702c445f 100644 --- a/docs/sources/datasources/graphite/query-editor/index.md +++ b/docs/sources/datasources/graphite/query-editor/index.md @@ -1,7 +1,7 @@ --- aliases: - ../../data-sources/graphite/query-editor/ -description: Guide for using the Graphite data source's query editor +description: Guide for using the Graphite data source query editor. keywords: - grafana - microsoft @@ -41,45 +41,53 @@ refs: Grafana includes a Graphite-specific query editor to help you build queries. The query editor helps you quickly navigate the metric space, add functions, and change function parameters. -It can handle all types of Graphite queries, including complex nested queries through the use of query references. +It supports a variety of Graphite queries, including complex nested queries, through the use of query references. For general documentation on querying data sources in Grafana, see [Query and transform data](ref:query-transform-data). -## View the raw query +## Query editor elements -To see the raw text of the query that Grafana sends to Graphite, click the **Toggle text edit mode** (pencil) icon. +The query editor consists of the following elements: + +- **Series** - A series in Graphite is a unique time-series dataset, represented by a specific metric name and timestamped values. Click **select metric** to select a metric from the drop-down. + +- **Functions** - Graphite uses functions to manipulate data. Click the **+ sign** to view a list of functions in the drop-down. You can create a query with multiple functions. + +To view the raw query, click the **Pencil icon** in the upper right. Click the **Pencil icon** again to continue adding series and functions. ## Choose metrics to query -Click **Select metric** to navigate the metric space. -Once you begin, you can use the mouse or keyboard arrow keys. -You can also select a wildcard and still continue. +Click **Select metric** to browse the available metrics. You can navigate using your mouse or arrow keys. You can also select a wildcard. {{< figure src="/static/img/docs/graphite/graphite-query-editor-still.png" animated-gif="/static/img/docs/graphite/graphite-query-editor.gif" >}} ## Functions -Click the plus icon next to **Function** to add a function. You can search for the function or select it from the menu. Once -a function is selected, it will be added and your focus will be in the text box of the first parameter. +Click the **+ sign** next to **Function** to add a function from the drop-down. You can also search by typing the first few letters of the function name. -- To edit or change a parameter, click on it and it will turn into a text box. -- To delete a function, click the function name followed by the x icon. +After selecting a function, Grafana adds it to your query and automatically places your cursor in the first parameter field. + +To edit a parameter, click it to open an editable text box. + +To remove a function simply click on it, then click the **X icon** that appears above it. {{< figure src="/static/img/docs/graphite/graphite-functions-still.png" animated-gif="/static/img/docs/graphite/graphite-functions-demo.gif" >}} -Some functions like aliasByNode support an optional second argument. To add an argument, hover your mouse over the first argument and then click the `+` symbol that appears. To remove the second optional parameter, click on it and leave it blank and the editor will remove it. +Some functions like `aliasByNode` support an optional second argument. To add this argument, hover your mouse over the argument and a dialog box appears. To remove the second optional parameter, click on it to delete it. -To learn more, refer to [Graphite's documentation on functions](https://graphite.readthedocs.io/en/latest/functions.html). +Refer to [Functions](https://graphite.readthedocs.io/en/latest/functions.html) in the Graphite documentation for more information. -{{< admonition type="warning" >}} -Some functions take a second argument that may be a function that returns a series. If you are adding a second argument that is a function, it is suggested to use a series reference from a second query instead of the function itself. The query editor does not currently support parsing of a second argument that is a function when switching between the query editor and the code editor. -{{< /admonition >}} +{{% admonition type="warning" %}} +Some functions accept a second argument, which can itself be another function that returns a series. If you need to add a second argument that is a function, Grafana recommends using a series reference from a second query instead of embedding the function directly. + +Currently, the query editor does not support parsing a second function argument when switching between the query builder and the code editor. +{{% /admonition %}} ### Sort labels -If you have the same labels on multiple graphs, they are both sorted differently and use different colors. +If the same labels appear on multiple graphs, they may be sorted differently and assigned different colors. -To avoid this and consistently order labels by name, use the `sortByName()` function. +To ensure consistent sorting and coloring, use the `sortByName()` function to order labels alphabetically. ### Modify the metric name in my tables or charts @@ -91,60 +99,52 @@ Grafana consolidates all Graphite metrics so that Graphite doesn't return more d By default, Grafana consolidates data points using the `avg` function. To control how Graphite consolidates metrics, use the Graphite `consolidateBy()` function. -{{< admonition type="note" >}} -Legend summary values (max, min, total) can't all be correct at the same time because they are calculated client-side by Grafana. -Depending on your consolidation function, only one or two can be correct at the same time. -{{< /admonition >}} +{{% admonition type="note" %}} +Grafana calculates legend summary values like `max`, `min`, and `total` on the client side, after data has been calculated. +Depending on the consolidation function used, only one or two of these values may be accurate at the same time. +{{% /admonition %}} ### Combine time series To combine time series, click **Combine** in the **Functions** list. -### Select and explor data with tags +### Select and explore data with tags -In Graphite, _everything_ is a tag. +In Graphite, everything is a tag. When exploring data, previously selected tags filter the remaining result set. To select data, use the `seriesByTag` function, which takes tag expressions (`=`, `!=`, `=~`, `!=~`) to filter timeseries. The Grafana query builder does this for you automatically when you select a tag. -{{< admonition type="note" >}} -The regular expression search can be slow on high-cardinality tags, so try to use other tags to reduce the scope first. -To help reduce the results, start by filtering on a particular name or namespace. -{{< /admonition >}} +{{% admonition type="note" %}} +Regular expression searches can be slow on high-cardinality tags, so try to use other tags to reduce the scope first. To help reduce the results, start by filtering on a particular name or namespace. +{{% /admonition %}} -## Nest queries +## Nested queries -You can reference a query by the "letter" of its row, similar to a spreadsheet. +Grafana lets you reference one query from another using its query letter, similar to how cell references work in a spreadsheet. -If you add a second query to a graph, you can reference the first query by entering `#A`. -This helps you build compounded queries. +For example, if you add a second query and want to build on the results of query A, you can reference it using #A. + +This approach allows you to build compound or nested queries, making your panels more flexible and easier to manage. ## Use wildcards to make fewer queries -To view multiple time series plotted on the same graph, use wildcards in your search to return all of the matching time series in one query. +To display multiple time series on the same graph, use wildcards in your query to return all matching series at once. -For example, to see how the CPU is being utilized on a machine, you can create a graph and use the single query `cpu.percent.*.g` to retrieve all time series that match that pattern. -This is more efficient than adding a query for each time series, such as `cpu.percent.user.g`, `cpu.percent.system.g`, and so on, which results in many queries to the data source. +For example, to monitor CPU utilization across a variety of metrics, you can use a single query like `cpu.percent.*.g` to retrieve all matching time series. +This approach is more efficient than writing separate queries for each series, such as `cpu.percent.user.g`, `cpu.percent.system.g`, and others, which would result in multiple queries to the data source. ## Apply annotations -[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs. -You can add annotation queries in the Dashboard menu's Annotations view. +[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs. You can add annotation queries in the dashboard menu's **Annotations** view. Graphite supports two ways to query annotations: - A regular metric query, using the `Graphite query` textbox. - A Graphite events query, using the `Graphite event tags` textbox with a tag, wildcard, or empty value -## Get Grafana metrics into Graphite - -Grafana exposes metrics for Graphite on the `/metrics` endpoint. -For detailed instructions, refer to [Internal Grafana metrics](ref:set-up-grafana-monitoring). - ## Integration with Loki -Graphite queries get converted to Loki queries when the data source selection changes in Explore. Loki label names and values are extracted from the Graphite queries according to mappings information provided in Graphite data source configuration. Queries using tags with `seriesByTags()` are also transformed without any additional setup. - -Refer to the Graphite data source settings for more details. +When you change the data source to Loki in Explore, your Graphite queries are automatically converted to Loki queries. Loki label names and values are extracted based on the mapping information defined in your Graphite data source configuration. Grafana automatically transforms queries that use tags with `seriesByTags()` without requiring additional setup. diff --git a/docs/sources/datasources/graphite/template-variables/index.md b/docs/sources/datasources/graphite/template-variables/index.md index 67b6cbaf94f..cb1633225cb 100644 --- a/docs/sources/datasources/graphite/template-variables/index.md +++ b/docs/sources/datasources/graphite/template-variables/index.md @@ -1,7 +1,7 @@ --- aliases: - ../../data-sources/graphite/template-variables/ -description: Guide for using template variables when querying the Graphite data source +description: Guide for using template variables when querying the Graphite data source. keywords: - grafana - graphite @@ -37,122 +37,153 @@ refs: # Graphite template variables Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables. -Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard. +Grafana lists these variables in drop-down selection boxes at the top of the dashboard to help you change the data displayed in your dashboard. Grafana refers to such variables as template variables. For an introduction to templating and template variables, refer to the [Templating](ref:variables) and [Add and manage variables](ref:add-template-variables) documentation. -## Select a query type +To view an example templated dashboard, refer to [Graphite Templated Nested dashboard](https://play.grafana.org/d/cvDFGseGz/graphite-templated-nested). -There are three query types for Graphite template variables +## Use query variables -| Query Type | Description | -| ----------------- | ------------------------------------------------------------------------------- | -| Default Query | Use functions such as `tags()`, `tag_values()`, `expand()` and metrics. | -| Value Query | Returns all the values for a query that includes a metric and function. | -| Metric Name Query | Returns all the names for a query that includes a metric and function. | +With Graphite data sources, you can only create query variables. Grafana supports three specific query types for Graphite-based variables: + +| Query type | Description | Example usage | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| **Default query** | Allows you to dynamically list metrics, nodes, or tag values using Graphite functions. | `tag_values(apps.*.requests.count, app)` | +| **Value query** | Returns all the values for a query that includes a metric and function. | `tag_values(apps.*.status.*, status)` | +| **Metric name query** | Returns all the names for a query that includes a metric and function. | `apps.*.requests.count` | + +### Choose a variable syntax + +The Graphite data source supports two variable syntaxes for use in the **Query** field. + +![Variable syntax example](/static/img/docs/v2/templated_variable_parameter.png) + +Grafana allows two ways to reference variables in a query: + +| **Syntax** | **Example** | +| ------------ | ---------------------------------------- | +| `$varname` | `apps.frontend.$server.requests.count` | +| `${varname}` | `apps.frontend.${server}.requests.count` | + +- **Shorthand syntax (`$varname`)** is convenient for simple paths but doesn't work when the variable is adjacent to characters (e.g., `cpu$coreLoad`). +- **Full syntax (`${varname}`)** is more flexible and works in any part of the string, including embedded within words. + +Choose the format that best fits the structure of your Graphite metric path. ## Use tag variables -To create a variable using tag values, use the Grafana functions `tags` and `tag_values`. +Grafana supports tag-based variables for Graphite, allowing you to dynamically populate drop-downs based on tag keys and values in your metric series. To do this, use the Graphite functions `tags()` and `tag_values()` in your variable queries. -| Query | Description | -| --------------------------------------- | -------------------------------------------------------------------------------------------------- | -| `tags()` | Returns all tags. | -| `tags(server=~backend\*)` | Returns only tags that occur in series matching the filter expression. | -| `tag_values(server)` | Returns tag values for the specified tag. | -| `tag_values(server, server=~backend\*)` | Returns filtered tag values that occur for the specified tag in series matching those expressions. | +| Query | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------ | +| `tags()` | Returns a list of all tag keys in the Graphite database. | +| `tags(server=~backend\*)` | Returns tag keys only from series that match the provided filter expression. | +| `tag_values(server)` | Returns all values for the specified tag key. | +| `tag_values(server, server=~backend\*)` | Returns tag values for a given key, filtered to only those that appear in matching series. | -Multiple filter expressions and expressions can contain other variables. For example: +You can use multiple filter expressions, and those expressions can include other Grafana variables. For example: ``` tag_values(server, server=~backend\*, app=~${apps:regex}) ``` +This query returns all server tag values from series where the `server` tag matches backend\* and the `app` tag matches the regex-filtered values from another variable ${apps}. + For details, refer to the [Graphite docs on the autocomplete API for tags](http://graphite.readthedocs.io/en/latest/tags.html#auto-complete-support). -### Use multi-value variables in tag queries +**Using regular expression formatting and the equal tilde operator `=~`:** -Multi-value variables in tag queries use the advanced formatting syntax for variables: `{var:regex}`. -Non-tag queries use the default glob formatting for multi-value variables. - -#### Tag expression example - -**Using regex formatting and the Equal Tilde operator, `=~`:** - -```text +``` server=~${servers:regex} ``` +This query tells Grafana to format the selected values in the `servers` variable as a regular expression (e.g., (`server1`|`server2`) if two servers are selected). + For more information, refer to [Advanced variable format options](ref:variable-syntax-advanced-variable-format-options). +### Filter with multiple expressions + +When using multi-value variables in tag queries, append `${var:regex}` to the variable name to apply regex formatting. + +``` +tag_values(server, app=~${apps:regex}) +``` + +This query returns only series where the app tag matches the selected values in $`{apps}`, formatted as a regular expression. `=~` is the regular expression operator + +Non-tag queries use the default `glob` formatting for multi-value variables. + ## Use other query variables -When writing queries, use the metric find type of query. +When writing queries, use the **metric find** query type to retrieve dynamic values. -For example, a query like `prod.servers.*` fills the variable with all possible values that exist in the wildcard position. +For example, the query `prod.servers.*` populates the variable with all values that exist at the wildcard position (\*). -The results contain all possible values occurring only at the last level of the query. -To get full metric names matching the query, use the `expand` function: `expand(*.servers.*)`. +Note that the results include only the values found at the last level of the query path. + +To return full metric paths that match your query, use the expand() function: + +``` +expand(*.servers.*). +``` ### Compare expanded and non-expanded metric search results -The expanded query returns the full names of matching metrics. -In combination with regular expressions, you can use it to extract any part of the metric name. -By contrast, a non-expanded query returns only the last part of the metric name, and doesn't let you extract other parts of metric names. +When querying Graphite metrics in Grafana, you can choose between using an **expanded** or **non-expanded** query: -Given these example metrics: +- **Expanded queries** (using the `expand()` function) return the **full metric paths** that match your query. +- **Non-expanded queries** return only the **last segment** of each matching metric path, which limits your ability to extract or filter based on deeper parts of the metric name. + +Expanded queries are especially useful when working with regular expressions to match or extract specific parts of the metric path. + +Suppose your Graphite database contains the following metrics: - `prod.servers.001.cpu` - `prod.servers.002.cpu` - `test.servers.001.cpu` -These examples demonstrate how expanded and non-expanded queries can fetch specific parts of the metrics name: +The following table illustrates the difference between expanded and non-expanded queries: -| Non-expanded query | Results | Expanded query | Expanded results | -| ------------------ | ---------- | ------------------------- | ---------------------------------------------------------------- | -| `*` | prod, test | `expand(*)` | prod, test | -| `*.servers` | servers | `expand(*.servers)` | prod.servers, test.servers | -| `test.servers` | servers | `expand(test.servers)` | test.servers | -| `*.servers.*` | 001,002 | `expand(*.servers.*)` | prod.servers.001, prod.servers.002, test.servers.001 | -| `test.servers.*` | 001 | `expand(test.servers.*)` | test.servers.001 | -| `*.servers.*.cpu` | cpu | `expand(*.servers.*.cpu)` | prod.servers.001.cpu, prod.servers.002.cpu, test.servers.001.cpu | +| **Non-expanded query** | **Results** | **Expanded query** | **Expanded results** | +| ---------------------- | -------------- | ------------------------- | ---------------------------------------------------------------------- | +| `*` | `prod`, `test` | `expand(*)` | `prod`, `test` | +| `*.servers` | `servers` | `expand(*.servers)` | `prod.servers`, `test.servers` | +| `test.servers` | `servers` | `expand(test.servers)` | `test.servers` | +| `*.servers.*` | `001`, `002` | `expand(*.servers.*)` | `prod.servers.001`, `prod.servers.002`, `test.servers.001` | +| `test.servers.*` | `001` | `expand(test.servers.*)` | `test.servers.001` | +| `*.servers.*.cpu` | `cpu` | `expand(*.servers.*.cpu)` | `prod.servers.001.cpu`, `prod.servers.002.cpu`, `test.servers.001.cpu` | -The non-expanded query is the same as an expanded query, with a regex matching the last part of the name. +{{% admonition type="note" %}} +A non-expanded query query works like an expanded query but returns only the final segment of each matched metric. +{{% /admonition %}} -You can also create nested variables that use other variables in their definition. -For example, `apps.$app.servers.*` uses the variable `$app` in its query definition. +Grafana also supports **nested variables**, which allow you to reference other variables in a query. -### Use `__searchFilter` to filter query variable results +For example: -You can use `__searchFilter` in the query field to filter the query result based on what the user types in the dropdown select box. -The default value for `__searchFilter` is `*` if you've not entered anything, and `` when used as part of a regular expression. +``` +apps.$app.servers.* +``` -#### Search filter example +This query uses the selected value of the `$app` variable to dynamically filter the metric path. The variable `$app` contains one or more application names and `servers.*` matches all servers for the given application. -To use `__searchFilter` as part of the query field to enable searching for `server` while the user types in the dropdown select box: +### Filter query variable results with `__searchFilter` -Query +Grafana provides the variable `__searchFilter`, which you can use to dynamically filter query results based on what the user types into the variable drop-down. +When the drop-down is empty or blank, `__searchFilter` defaults to `*`, which means it returns all possible values. If you type a string, Grafana replaces `__searchFilter` with that input. -```bash +To use `__searchFilter` as part of the query field to enable searching for `server` while the user types in the drop-down select box: + +Query: + +``` apps.$app.servers.$__searchFilter ``` -TagValues +TagValues: -```bash +``` tag_values(server, server=~${__searchFilter:regex}) ``` - -## Choose a variable syntax - -![variable](/static/img/docs/v2/templated_variable_parameter.png) - -The Graphite data source supports two variable syntaxes for use in the **Query** field: - -- `$`, for example `apps.frontend.$server.requests.count`, which is easier to read and write but does not allow you to use a variable in the middle of a word. -- `${varname}`, for example `apps.frontend.${server}.requests.count`, to use in expressions like `my.server${serverNumber}.count`. - -### Templated dashboard example - -To view an example templated dashboard, refer to [Graphite Templated Nested dashboard](https://play.grafana.org/d/cvDFGseGz/graphite-templated-nested). From 38e1f900e7df8013c0bdddfa02a493170666bc93 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Mon, 30 Jun 2025 21:38:26 -0500 Subject: [PATCH 03/23] TableNG: Specialize cell root and cell content renderers (#107427) --- .../datasources/graphite/configure/index.md | 68 ++--- .../graphite/template-variables/index.md | 8 +- .../src/components/Table/TableNG/TableNG.tsx | 261 +++++++++--------- .../src/components/Table/TableNG/utils.ts | 15 +- 4 files changed, 176 insertions(+), 176 deletions(-) diff --git a/docs/sources/datasources/graphite/configure/index.md b/docs/sources/datasources/graphite/configure/index.md index d7e03817c45..ab384c99560 100644 --- a/docs/sources/datasources/graphite/configure/index.md +++ b/docs/sources/datasources/graphite/configure/index.md @@ -88,52 +88,52 @@ Grafana takes you to the **Settings** tab, where you will set up your Graphite c Following is a list of configuration options for Graphite. -| Setting | Description | -|-------------|-----------------------------------------------------------------------------------------------------------------------------------------| -| **Name** | The display name for the data source. This is how you'll reference it in panels and queries.
Examples: `graphite-1`, `graphite-metrics`. | -| **Default** | When enabled, sets this data source as the default for dashboard panels. It will be automatically selected when creating new panels. | +| Setting | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | The display name for the data source. This is how you'll reference it in panels and queries.
Examples: `graphite-1`, `graphite-metrics`. | +| **Default** | When enabled, sets this data source as the default for dashboard panels. It will be automatically selected when creating new panels. | **HTTP:** -| Setting | Description | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **URL** | Sets the HTTP protocol, IP, and port of your `graphite-web` or `graphite-api` installation.
Since the access method is set to *Server*, the URL must be accessible from the Grafana backend. | -| **Allowed cookies**| By default, Grafana removes forwarded cookies. Specify cookie names here to allow them to be forwarded to the data source. | -| **Timeout** | Sets the HTTP request timeout in seconds. | +| Setting | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **URL** | Sets the HTTP protocol, IP, and port of your `graphite-web` or `graphite-api` installation.
Since the access method is set to _Server_, the URL must be accessible from the Grafana backend. | +| **Allowed cookies** | By default, Grafana removes forwarded cookies. Specify cookie names here to allow them to be forwarded to the data source. | +| **Timeout** | Sets the HTTP request timeout in seconds. | **Auth:** -| **Setting** | **Description** | -|------------------------------|----------------------------------------------------------------------------------------------------------------------------------| -| **Basic Auth** | Toggle on to enable basic authentication to the data source. | -|   **User** | Sets the username used for basic authentication. | -|   **Password** | Enter the password used for basic authentication. | -| **With Credentials** | Toggle on to include cookies and authentication headers in cross-origin requests. | -| **TLS Client Auth** | Toggle on to enable TLS client authentication (both server and client are verified). | -|   **ServerName** | The server name used to verify the hostname on the certificate returned by the server. | -|   **Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. | -|   **Client Key** | Private key used to encrypt communication between the client and server. Also generated by a CA or self-signed. | -| **With CA Cert** | Toggle on to authenticate with a CA certificate. | -|   **CA Cert** | CA certificate used to validate the server certificate. | -| **Skip TLS Verify** | Toggle on to bypass TLS certificate validation. Not recommended unless necessary or for testing purposes. | -| **Forward OAuth Identity** | Toggle on to forward the user's upstream OAuth identity to the data source. Grafana includes the access token in the request. | +| **Setting** | **Description** | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| **Basic Auth** | Toggle on to enable basic authentication to the data source. | +|   **User** | Sets the username used for basic authentication. | +|   **Password** | Enter the password used for basic authentication. | +| **With Credentials** | Toggle on to include cookies and authentication headers in cross-origin requests. | +| **TLS Client Auth** | Toggle on to enable TLS client authentication (both server and client are verified). | +|   **ServerName** | The server name used to verify the hostname on the certificate returned by the server. | +|   **Client Cert** | Client certificate generated by a Certificate Authority (CA) or self-signed. | +|   **Client Key** | Private key used to encrypt communication between the client and server. Also generated by a CA or self-signed. | +| **With CA Cert** | Toggle on to authenticate with a CA certificate. | +|   **CA Cert** | CA certificate used to validate the server certificate. | +| **Skip TLS Verify** | Toggle on to bypass TLS certificate validation. Not recommended unless necessary or for testing purposes. | +| **Forward OAuth Identity** | Toggle on to forward the user's upstream OAuth identity to the data source. Grafana includes the access token in the request. | **Custom HTTP Headers:** Pass along additional information and metadata about the request or response. -| **Setting** | **Description** | -|-------------|--------------------------------------------------------------------------------------------------| +| **Setting** | **Description** | +| ----------- | ---------------------------------------------------------------------------------------------------------- | | **Header** | Add a custom header. This allows custom headers to be passed based on the needs of your Graphite instance. | -| **Value** | The value of the header. | +| **Value** | The value of the header. | **Graphite details:** -| **Setting** | **Description** | -|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Version** | Select your Graphite version from the drop-down. This controls which functions are available in the Graphite query editor. Use `1.1.x` for Grafana Cloud Graphite. | -| **Graphite backend type**| Select the Graphite backend type. Choosing `Metrictank` enables additional features like query processing metadata. (`Metrictank` is a multi-tenant time series engine compatible with Graphite.) Use `Default` for Grafana Cloud Graphite. | -| **Rollup indicator** | Toggle on to display an info icon in panel headers when data aggregation (rollup) occurs. Only available when `Metrictank` is selected. | +| **Setting** | **Description** | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Version** | Select your Graphite version from the drop-down. This controls which functions are available in the Graphite query editor. Use `1.1.x` for Grafana Cloud Graphite. | +| **Graphite backend type** | Select the Graphite backend type. Choosing `Metrictank` enables additional features like query processing metadata. (`Metrictank` is a multi-tenant time series engine compatible with Graphite.) Use `Default` for Grafana Cloud Graphite. | +| **Rollup indicator** | Toggle on to display an info icon in panel headers when data aggregation (rollup) occurs. Only available when `Metrictank` is selected. | **Label mappings:** @@ -149,10 +149,10 @@ Grafana automatically maps all Graphite tags to labels, even if you haven’t de | `alias(servers.*.{001,002}.*,1,2)` | `{server=~"(001,002)"}` | | `interpolate(seriesByTag('foo=bar', 'server=002'), inf)` | `{foo="bar", server="002"}` | +| **Setting** | **Description** | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Private data source connect** | _Only for Grafana Cloud users._ Establishes a private, secured connection between a Grafana Cloud stack and data sources within a private network. Use the drop-down to locate the PDC URL. For setup instructions, refer to [Private data source connect (PDC)](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). Click **Manage private data source connect** to open your PDC connection page and view your configuration details. | -| **Setting** | **Description** | -|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Private data source connect** | _Only for Grafana Cloud users._ Establishes a private, secured connection between a Grafana Cloud stack and data sources within a private network. Use the drop-down to locate the PDC URL. For setup instructions, refer to [Private data source connect (PDC)](ref:private-data-source-connect) and [Configure PDC](ref:configure-pdc). Click **Manage private data source connect** to open your PDC connection page and view your configuration details. | After configuring your Graphite data source options, click **Save & test** at the bottom to test the connection. You should see a confirmation dialog box that says: diff --git a/docs/sources/datasources/graphite/template-variables/index.md b/docs/sources/datasources/graphite/template-variables/index.md index cb1633225cb..b79feb5e3d0 100644 --- a/docs/sources/datasources/graphite/template-variables/index.md +++ b/docs/sources/datasources/graphite/template-variables/index.md @@ -48,11 +48,11 @@ To view an example templated dashboard, refer to [Graphite Templated Nested dash With Graphite data sources, you can only create query variables. Grafana supports three specific query types for Graphite-based variables: -| Query type | Description | Example usage | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| Query type | Description | Example usage | +| --------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------- | | **Default query** | Allows you to dynamically list metrics, nodes, or tag values using Graphite functions. | `tag_values(apps.*.requests.count, app)` | -| **Value query** | Returns all the values for a query that includes a metric and function. | `tag_values(apps.*.status.*, status)` | -| **Metric name query** | Returns all the names for a query that includes a metric and function. | `apps.*.requests.count` | +| **Value query** | Returns all the values for a query that includes a metric and function. | `tag_values(apps.*.status.*, status)` | +| **Metric name query** | Returns all the names for a query that includes a metric and function. | `apps.*.requests.count` | ### Choose a variable syntax diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index d2e38be4f58..625a4b4b106 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -1,7 +1,7 @@ import 'react-data-grid/lib/styles.css'; import { css, cx } from '@emotion/css'; import { Property } from 'csstype'; -import { Key, useLayoutEffect, useMemo, useState } from 'react'; +import { Key, ReactNode, useLayoutEffect, useMemo, useState } from 'react'; import { Cell, CellRendererProps, @@ -22,7 +22,7 @@ import { MenuItem } from '../../Menu/MenuItem'; import { Pagination } from '../../Pagination/Pagination'; import { PanelContext, usePanelContext } from '../../PanelChrome'; import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector'; -import { CellColors } from '../types'; +import { CellColors, TableCellDisplayMode } from '../types'; import { HeaderCell } from './Cells/HeaderCell'; import { RowExpander } from './Cells/RowExpander'; @@ -55,6 +55,8 @@ import { getCellOptions, } from './utils'; +type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; + export function TableNG(props: TableNGProps) { const { cellHeight, @@ -137,10 +139,6 @@ export function TableNG(props: TableNGProps) { // vt scrollbar accounting for column auto-sizing const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]); - const visibleFieldsByDisplayName: Record = useMemo( - () => visibleFields.reduce((acc, f) => ({ ...acc, [getDisplayName(f)]: f }), {}), - [visibleFields] - ); const availableWidth = useMemo( () => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width), [width, hasNestedFrames] @@ -175,11 +173,6 @@ export function TableNG(props: TableNGProps) { [data, enableSharedCrosshair, expandedRows, panelContext] ); - const renderCell = useMemo( - () => renderCellFactory(columnTypes, applyToRowBgFn, rowHeight, textWraps, theme, visibleFieldsByDisplayName), - [columnTypes, applyToRowBgFn, rowHeight, textWraps, theme, visibleFieldsByDisplayName] - ); - const commonDataGridProps = useMemo( () => ({ @@ -240,9 +233,19 @@ export function TableNG(props: TableNGProps) { ] ); - const columns = useMemo((): TableColumn[] => { - const columnsFromFields = (f: Field[], w: number[]): TableColumn[] => - f.map((field, i): TableColumn => { + interface Schema { + columns: TableColumn[]; + cellRootRenderers: Record; + } + + const { columns, cellRootRenderers } = useMemo(() => { + const fromFields = (f: Field[], widths: number[]) => { + const result: Schema = { + columns: [], + cellRootRenderers: {}, + }; + + f.forEach((field, i) => { const justifyContent = getTextAlign(field); const footerStyles = getFooterStyles(justifyContent); const displayName = getDisplayName(field); @@ -253,7 +256,7 @@ export function TableNG(props: TableNGProps) { const cellInspect = Boolean(field.config.custom?.inspect); const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null); const showActions = cellInspect || showFilters; - const width = w[i]; + const width = widths[i]; const frame = data; // helps us avoid string cx and emotion per-cell @@ -265,54 +268,99 @@ export function TableNG(props: TableNGProps) { ) : undefined; - return { + const cellType = cellOptions.type; + const fieldType = columnTypes[displayName]; + const shouldWrap = textWraps[displayName]; + const shouldOverflow = shouldTextOverflow(fieldType, cellType, shouldWrap, cellInspect); + + let lastRowIdx = -1; + let _rowHeight = 0; + + // this fires first + const renderCellRoot = (key: Key, props: CellRendererProps): ReactNode => { + const rowIdx = props.row.__index; + const value = props.row[props.column.key]; + + // meh, this should be cached by the renderRow() call? + if (rowIdx !== lastRowIdx) { + _rowHeight = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; + lastRowIdx = rowIdx; + } + + let colors: CellColors; + + if (applyToRowBgFn != null) { + colors = applyToRowBgFn(props.rowIdx); + } else if (cellType !== TableCellDisplayMode.Auto) { + const displayValue = field.display!(value); // this fires here to get colors, then again to get rendered value? + colors = getCellColors(theme, cellOptions, displayValue); + } else { + colors = {}; + } + + const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, colors); + + return ( + + ); + }; + + result.cellRootRenderers[displayName] = renderCellRoot; + + // this fires second + const renderCellContent = (props: RenderCellProps): JSX.Element => { + const rowIdx = props.row.__index; + const value = props.row[props.column.key]; + + // TODO: defer until click? + const actions = getActions?.(frame, field, props.row.__index, replaceVariables); + + return ( + <> + {renderFieldCell({ + actions, + cellOptions, + frame, + field, + height, + justifyContent, + rowIdx, + theme, + value, + width, + cellInspect, + showFilters, + })} + {showActions && ( + + )} + + ); + }; + + const column: TableColumn = { field, key: displayName, name: displayName, width, headerCellClass, - renderCell: (props: RenderCellProps): JSX.Element => { - // TODO: once per row - const height = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; - // TODO: defer until click? - const actions = getActions?.(frame, field, props.row.__index, replaceVariables); - - const rowIdx = props.row.__index; - const value = props.row[displayName]; - - return ( - <> - {renderFieldCell({ - actions, - cellOptions, - frame, - field, - height, - justifyContent, - rowIdx, - theme, - value, - width, - cellInspect, - showFilters, - })} - {showActions && ( - - )} - - ); - }, + renderCell: renderCellContent, renderHeaderCell: ({ column, sortDirection }): JSX.Element => ( {footerCalcs[i]}; }, }; + + result.columns.push(column); }); - const result: TableColumn[] = columnsFromFields(visibleFields, widths); + return result; + }; + + const result = fromFields(visibleFields, widths); // handle nested frames rendering from here. if (!hasNestedFrames) { @@ -356,13 +409,17 @@ export function TableNG(props: TableNGProps) { } const renderRow = renderRowFactory(firstNestedData.fields, panelContext, expandedRows, enableSharedCrosshair); - const expandedColumns = columnsFromFields( + const { columns: nestedColumns, cellRootRenderers: nestedCellRootRenderers } = fromFields( firstNestedData.fields, computeColWidths(firstNestedData.fields, availableWidth) ); + const renderCellRoot: CellRootRenderer = (key, props) => nestedCellRootRenderers[props.column.key](key, props); + + result.cellRootRenderers.expanded = (key, props) => ; + // If we have nested frames, we need to add a column for the row expansion - result.unshift({ + result.columns.unshift({ key: 'expanded', name: '', field: { @@ -372,16 +429,16 @@ export function TableNG(props: TableNGProps) { values: [], }, cellClass(row) { - if (Number(row.__depth) !== 0) { + if (row.__depth !== 0) { return styles.cellNested; } return; }, colSpan(args) { - return args.type === 'ROW' && Number(args.row.__depth) === 1 ? data.fields.length : 1; + return args.type === 'ROW' && args.row.__depth === 1 ? data.fields.length : 1; }, renderCell: ({ row }) => { - if (Number(row.__depth) === 0) { + if (row.__depth === 0) { return ( {...commonDataGridProps} className={cx(styles.grid, styles.gridNested)} - columns={expandedColumns} + columns={nestedColumns} rows={expandedRecords} - renderers={{ renderRow, renderCell }} + renderers={{ renderRow, renderCell: renderCellRoot }} /> ); }, @@ -433,7 +490,6 @@ export function TableNG(props: TableNGProps) { onCellFilterAdded, panelContext, replaceVariables, - renderCell, rows, rowHeight, setFilter, @@ -443,6 +499,10 @@ export function TableNG(props: TableNGProps) { theme, visibleFields, widths, + applyToRowBgFn, + columnTypes, + height, + textWraps, ]); // invalidate columns on every structureRev change. this supports width editing in the fieldConfig. @@ -454,6 +514,10 @@ export function TableNG(props: TableNGProps) { const displayedEnd = pageRangeEnd; const numRows = sortedRows.length; + const renderCellRoot: CellRootRenderer = (key, props) => { + return cellRootRenderers[props.column.key](key, props); + }; + return ( <> @@ -471,7 +535,7 @@ export function TableNG(props: TableNGProps) { } : null } - renderers={{ renderRow, renderCell }} + renderers={{ renderRow, renderCell: renderCellRoot }} /> {enablePagination && ( @@ -538,11 +602,11 @@ const renderRowFactory = ) => (key: React.Key, props: RenderRowProps): React.ReactNode => { const { row } = props; - const rowIdx = Number(row.__index); + const rowIdx = row.__index; const isExpanded = !!expandedRows[rowIdx]; // Don't render non expanded child rows - if (Number(row.__depth) === 1 && !isExpanded) { + if (row.__depth === 1 && !isExpanded) { return null; } @@ -573,63 +637,6 @@ const renderRowFactory = return ; }; -/** - * passed to the top-level `renderCell` prop on DataGrid. This applies all per-cell styles. - */ -const renderCellFactory = - ( - columnTypes: Record, - applyToRowBgFn: ((rowIdx: number) => CellColors) | undefined, - rowHeight: number | ((row: TableRow) => number), - textWraps: Record, - theme: GrafanaTheme2, - visibleFieldsByDisplayName: Record - ) => - (key: Key, props: CellRendererProps) => { - const displayName = props.column.key; - const field = visibleFieldsByDisplayName[displayName]; - - // exit early if we fail to look up the field from the column key. - if (!field) { - return ; - } - - const cellOptions = getCellOptions(field); - const cellType = cellOptions.type; - const value = props.row[props.column.key]; - - const colors: CellColors = (() => { - if (applyToRowBgFn) { - return applyToRowBgFn(props.rowIdx); - } - const displayValue = field.display?.(value); - if (displayValue && cellOptions) { - return getCellColors(theme, cellOptions, displayValue); - } - return {}; - })(); - - const rh = typeof rowHeight === 'function' ? rowHeight(props.row) : rowHeight; - const shouldOverflow = shouldTextOverflow( - displayName, - columnTypes, - textWraps[getDisplayName(field)], - field, - cellType - ); - const shouldWrap = textWraps[displayName] ?? false; - const cellStyle = getCellStyles(theme, field, rh, shouldWrap, shouldOverflow, colors); - - return ( - - ); - }; - const getGridStyles = ( theme: GrafanaTheme2, { enablePagination, noHeader }: { enablePagination?: boolean; noHeader?: boolean } diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index d65b1cd9ff5..79afab8b076 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -74,21 +74,14 @@ export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCell * Returns true if text overflow handling should be applied to the cell. */ export function shouldTextOverflow( - key: string, - columnTypes: ColumnTypes, + fieldType: FieldType, + cellType: TableCellDisplayMode, textWrap: boolean, - field: Field, - cellType: TableCellDisplayMode + cellInspect: boolean ): boolean { - const cellInspect = field.config?.custom?.inspect ?? false; - // Tech debt: Technically image cells are of type string, which is misleading (kinda?) // so we need to ensure we don't apply overflow hover states fo type image - if (textWrap || cellInspect || cellType === TableCellDisplayMode.Image || columnTypes[key] !== FieldType.string) { - return false; - } - - return true; + return fieldType === FieldType.string && cellType !== TableCellDisplayMode.Image && !textWrap && !cellInspect; } /** From 24138bde7002b7bbc4097e2ffaed406bb1128e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 1 Jul 2025 06:10:57 +0200 Subject: [PATCH 04/23] Plugin Extension: improves mutation logs (#107370) * Plugin Extension: improves mutation logs * chore: removes feature toggle * chore: rename function --- .../src/types/featureToggles.gen.ts | 4 -- pkg/services/featuremgmt/registry.go | 9 ---- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 -- pkg/services/featuremgmt/toggles_gen.json | 3 +- .../components/DataSourcePluginSettings.tsx | 4 +- .../extensions/usePluginComponent.test.tsx | 4 +- .../extensions/usePluginComponents.test.tsx | 2 +- .../plugins/extensions/utils.test.tsx | 53 ++++++------------- .../app/features/plugins/extensions/utils.tsx | 17 +++--- 10 files changed, 32 insertions(+), 69 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index c94a3165b08..fd21c30aa12 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -976,10 +976,6 @@ export interface FeatureToggles { */ alertingBulkActionsInUI?: boolean; /** - * Use proxy-based read-only objects for plugin extensions instead of deep cloning - */ - extensionsReadOnlyProxy?: boolean; - /** * Registers AuthZ /apis endpoint */ kubernetesAuthzApis?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 85fc598fad4..9e99c99bb3a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1675,15 +1675,6 @@ var ( HideFromDocs: true, Expression: "true", // enabled by default }, - { - Name: "extensionsReadOnlyProxy", - Description: "Use proxy-based read-only objects for plugin extensions instead of deep cloning", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - HideFromAdminPage: true, - HideFromDocs: true, - FrontendOnly: true, - }, { Name: "kubernetesAuthzApis", Description: "Registers AuthZ /apis endpoint", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 2278cb1fb50..cd2d6706a08 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -219,7 +219,6 @@ multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false, alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true -extensionsReadOnlyProxy,experimental,@grafana/plugins-platform-backend,false,false,true kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index fbd6e7ab88a..7125fa63ebd 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -887,10 +887,6 @@ const ( // Enables the alerting bulk actions in the UI FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI" - // FlagExtensionsReadOnlyProxy - // Use proxy-based read-only objects for plugin extensions instead of deep cloning - FlagExtensionsReadOnlyProxy = "extensionsReadOnlyProxy" - // FlagKubernetesAuthzApis // Registers AuthZ /apis endpoint FlagKubernetesAuthzApis = "kubernetesAuthzApis" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index dda8e2005c4..052b3b22280 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1110,7 +1110,8 @@ "metadata": { "name": "extensionsReadOnlyProxy", "resourceVersion": "1750434297879", - "creationTimestamp": "2025-05-06T04:55:23Z" + "creationTimestamp": "2025-05-06T04:55:23Z", + "deletionTimestamp": "2025-06-30T08:24:11Z" }, "spec": { "description": "Use proxy-based read-only objects for plugin extensions instead of deep cloning", diff --git a/public/app/features/datasources/components/DataSourcePluginSettings.tsx b/public/app/features/datasources/components/DataSourcePluginSettings.tsx index 0dfd48bb5bf..80098048692 100644 --- a/public/app/features/datasources/components/DataSourcePluginSettings.tsx +++ b/public/app/features/datasources/components/DataSourcePluginSettings.tsx @@ -1,7 +1,7 @@ import { createElement, PureComponent } from 'react'; import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data'; -import { readOnlyCopy } from 'app/features/plugins/extensions/utils'; +import { writableProxy } from 'app/features/plugins/extensions/utils'; import { GenericDataSourcePlugin } from '../types'; @@ -34,7 +34,7 @@ export class DataSourcePluginSettings extends PureComponent {
{plugin.components.ConfigEditor && createElement(plugin.components.ConfigEditor, { - options: readOnlyCopy(dataSource), + options: writableProxy(dataSource), onOptionsChange: this.onModelChanged, })}
diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 2e8e92f7321..266a9f602c0 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -380,8 +380,8 @@ describe('usePluginComponent()', () => { // Should not throw an error if it mutates the props expect(() => render(Component && )).not.toThrow(); - // Should log a warning - expect(log.warning).toHaveBeenCalledWith('Attempted to mutate object property "c"', { + // Should log an error in dev mode + expect(log.error).toHaveBeenCalledWith('Attempted to mutate object property "c"', { stack: expect.any(String), }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index c2ab7ec6845..99ebda0eef5 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -266,7 +266,7 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render()).not.toThrow(); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "foo4"`, { + expect(log.error).toHaveBeenCalledWith(`Attempted to mutate object property "foo4"`, { stack: expect.any(String), }); diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index e1edf9bd3c8..b41ec1c99f0 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -21,8 +21,7 @@ import { getAppPluginDependencies, getExtensionPointPluginMeta, getMutationObserverProxy, - readOnlyCopy, - isReadOnlyProxy, + writableProxy, isMutationObserverProxy, } from './utils'; @@ -401,7 +400,7 @@ describe('Plugin Extensions / Utils', () => { expect(proxy.a).toBe('b'); }); - it('should be possible to set new values, but logs a warning', () => { + it('should be possible to set new values, but logs a debug message', () => { const obj: { a: string; b?: string } = { a: 'a' }; const proxy = getMutationObserverProxy(obj); @@ -412,7 +411,7 @@ describe('Plugin Extensions / Utils', () => { }); }).not.toThrow(); - expect(log.warning).toHaveBeenCalledWith(`Attempted to define object property "b"`, { + expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "b"`, { stack: expect.any(String), }); @@ -440,12 +439,11 @@ describe('Plugin Extensions / Utils', () => { }); }); - describe('readOnlyCopy()', () => { + describe('writableProxy()', () => { const originalEnv = config.buildInfo.env; beforeEach(() => { jest.spyOn(console, 'warn').mockImplementation(); - config.featureToggles.extensionsReadOnlyProxy = false; }); afterEach(() => { @@ -454,34 +452,19 @@ describe('Plugin Extensions / Utils', () => { }); it('should return the same value for primitive types', () => { - expect(readOnlyCopy(1)).toBe(1); - expect(readOnlyCopy('a')).toBe('a'); - expect(readOnlyCopy(true)).toBe(true); - expect(readOnlyCopy(false)).toBe(false); - expect(readOnlyCopy(null)).toBe(null); - expect(readOnlyCopy(undefined)).toBe(undefined); - }); - - it('should return a read-only proxy of the original object if the feature flag is enabled', () => { - config.featureToggles.extensionsReadOnlyProxy = true; - - const obj = { a: 'a' }; - const copy = readOnlyCopy(obj); - - expect(copy).not.toBe(obj); - expect(copy.a).toBe('a'); - expect(isReadOnlyProxy(copy)).toBe(true); - expect(() => { - copy.a = 'b'; - }).toThrow(TypeError); + expect(writableProxy(1)).toBe(1); + expect(writableProxy('a')).toBe('a'); + expect(writableProxy(true)).toBe(true); + expect(writableProxy(false)).toBe(false); + expect(writableProxy(null)).toBe(null); + expect(writableProxy(undefined)).toBe(undefined); }); it('should return a writable deep-copy of the original object in dev mode', () => { - config.featureToggles.extensionsReadOnlyProxy = false; config.buildInfo.env = 'development'; const obj = { a: 'a' }; - const copy = readOnlyCopy(obj); + const copy = writableProxy(obj); expect(copy).not.toBe(obj); expect(copy.a).toBe('a'); @@ -498,11 +481,10 @@ describe('Plugin Extensions / Utils', () => { }); it('should return a writable deep-copy of the original object in production mode', () => { - config.featureToggles.extensionsReadOnlyProxy = false; config.buildInfo.env = 'production'; const obj = { a: 'a' }; - const copy = readOnlyCopy(obj); + const copy = writableProxy(obj); expect(copy).not.toBe(obj); expect(copy.a).toBe('a'); @@ -519,11 +501,10 @@ describe('Plugin Extensions / Utils', () => { }); it('should allow freezing the object in production mode', () => { - config.featureToggles.extensionsReadOnlyProxy = false; config.buildInfo.env = 'production'; const obj = { a: 'a', b: { c: 'c' } }; - const copy = readOnlyCopy(obj); + const copy = writableProxy(obj); expect(() => { Object.freeze(copy); @@ -534,7 +515,7 @@ describe('Plugin Extensions / Utils', () => { expect(Object.isFrozen(copy.b)).toBe(true); expect(copy.b).toEqual({ c: 'c' }); - expect(log.warning).toHaveBeenCalledWith(`Attempted to define object property "a"`, { + expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "a"`, { stack: expect.any(String), }); }); @@ -687,7 +668,7 @@ describe('Plugin Extensions / Utils', () => { expect(screen.getByText('Version: 1.0.0')).toBeVisible(); }); - it('should not be possible to mutate the props in development mode, but it logs a warning', async () => { + it('should not be possible to mutate the props in development mode, but it logs an error', async () => { config.buildInfo.env = 'development'; const pluginId = 'grafana-worldmap-panel'; const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); @@ -700,8 +681,8 @@ describe('Plugin Extensions / Utils', () => { expect(await screen.findByText('Hello Grafana!')).toBeVisible(); // Logs a warning - expect(log.warning).toHaveBeenCalledTimes(1); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "c"`, { + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith(`Attempted to mutate object property "c"`, { stack: expect.any(String), }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 72f17ee6974..423308dbe63 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -85,7 +85,7 @@ export const wrapWithPluginContext = (pluginId: string, Component: React.Com return ( - + ); }; @@ -230,24 +230,27 @@ export function getMutationObserverProxy(obj: T, _log: Extensi } const cache = new WeakMap(); + const logFunction = isGrafanaDevMode() ? _log.error.bind(_log) : _log.warning.bind(_log); // should show error during local development return new Proxy(obj, { deleteProperty(target, prop) { - _log.warning(`Attempted to delete object property "${String(prop)}"`, { + logFunction(`Attempted to delete object property "${String(prop)}"`, { stack: new Error().stack ?? '', }); Reflect.deleteProperty(target, prop); return true; }, defineProperty(target, prop, descriptor) { - _log.warning(`Attempted to define object property "${String(prop)}"`, { + // because immer (used by RTK) calls Object.isFrozen and Object.freeze we know that defineProperty will be called + // behind the scenes as well so we only log message with debug level to minimize the noise and false positives + _log.debug(`Attempted to define object property "${String(prop)}"`, { stack: new Error().stack ?? '', }); Reflect.defineProperty(target, prop, descriptor); return true; }, set(target, prop, newValue) { - _log.warning(`Attempted to mutate object property "${String(prop)}"`, { + logFunction(`Attempted to mutate object property "${String(prop)}"`, { stack: new Error().stack ?? '', }); Reflect.set(target, prop, newValue); @@ -285,16 +288,12 @@ export function getMutationObserverProxy(obj: T, _log: Extensi }); } -export function readOnlyCopy(value: T, _log: ExtensionsLog = log): T { +export function writableProxy(value: T, _log: ExtensionsLog = log): T { // Primitive types are read-only by default if (!value || typeof value !== 'object') { return value; } - if (config.featureToggles.extensionsReadOnlyProxy) { - return getReadOnlyProxy(value); - } - // Default: we return a proxy of a deep-cloned version of the original object, which logs warnings when mutation is attempted return getMutationObserverProxy(cloneDeep(value), _log); } From 3a38832ff60a826eb06f54f7239ad9c831a2594d Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Tue, 1 Jul 2025 09:11:17 +0200 Subject: [PATCH 05/23] Alerting: Fix group interval override when adding new rules (#107324) * Fix group interval override when adding new rules to existing groups * Fix lint errors * Update tests snapshots * Update tests snapshots * Fix GrafanaGroupLoader --- .../useMoveRuleFromRuleGroup.test.tsx.snap | 3 ++ .../useUpdateRuleInRuleGroup.test.tsx.snap | 3 +- .../hooks/ruleGroup/useProduceNewRuleGroup.ts | 15 ++++-- .../useUpdateRuleInRuleGroup.test.tsx | 12 ++--- .../alerting/unified/mocks/grafanaRulerApi.ts | 2 +- .../mocks/server/handlers/grafanaRuler.ts | 8 +-- .../mocks/server/handlers/mimirRuler.ts | 5 ++ .../unified/reducers/ruler/ruleGroups.ts | 2 + .../rule-editor/RuleEditorCloudRules.test.tsx | 50 ++++++++++++++++++- .../RuleEditorGrafanaRules.test.tsx | 45 ++++++++++++++++- .../unified/rule-list/GrafanaGroupLoader.tsx | 4 +- public/test/helpers/alertingRuleEditor.tsx | 1 + 12 files changed, 130 insertions(+), 20 deletions(-) diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap index d5ff767a76f..f88931de9b6 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useMoveRuleFromRuleGroup.test.tsx.snap @@ -4,6 +4,7 @@ exports[`Moving a Data source managed rule should move a rule in a namespace to [ { "body": { + "interval": "1m", "name": "group-1", "rules": [ { @@ -49,6 +50,7 @@ exports[`Moving a Data source managed rule should move a rule in an existing gro [ { "body": { + "interval": "1m", "name": "entirely new group name", "rules": [ { @@ -190,6 +192,7 @@ exports[`Moving a Grafana managed rule should move a rule from an existing group [ { "body": { + "interval": "1m", "name": "empty-group", "rules": [ { diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleInRuleGroup.test.tsx.snap b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleInRuleGroup.test.tsx.snap index 1b0143c619e..6afe36981f9 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleInRuleGroup.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/ruleGroup/__snapshots__/useUpdateRuleInRuleGroup.test.tsx.snap @@ -4,6 +4,7 @@ exports[`Updating a Data source managed rule should be able to move a rule if ta [ { "body": { + "interval": "1m", "name": "a new group", "rules": [ { @@ -144,7 +145,7 @@ exports[`Updating a Grafana managed rule should move a rule in to another group [ { "body": { - "interval": "1m", + "interval": "5m", "name": "grafana-group-2", "rules": [ { diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts index ddbbc0e2401..c5e0eb9faf7 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useProduceNewRuleGroup.ts @@ -6,7 +6,7 @@ import { PostableRulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../../api/alertRuleApi'; import { featureDiscoveryApi } from '../../api/featureDiscoveryApi'; import { notFoundToNullOrThrow } from '../../api/util'; -import { ruleGroupReducer } from '../../reducers/ruler/ruleGroups'; +import { addRuleAction, ruleGroupReducer } from '../../reducers/ruler/ruleGroups'; import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../rule-editor/formDefaults'; import { getDatasourceAPIUid } from '../../utils/datasource'; @@ -62,10 +62,15 @@ export function useProduceNewRuleGroup() { .catch(notFoundToNullOrThrow); const initialRuleGroupDefinition = latestRuleGroupDefinition ?? createBlankRuleGroup(groupName); - const newRuleGroupDefinition = actions.reduce( - (ruleGroup, action) => ruleGroupReducer(ruleGroup, action), - initialRuleGroupDefinition - ); + const newRuleGroupDefinition = actions.reduce((ruleGroup, action) => { + // This is a workaround to ensure that the interval is set correctly when adding a rule to an existing rule group. + // The interval is set to default for DMA rules even for existing rule groups with a non-default interval. + // We no longer allow setting the interval for existing groups, but still allow that when you create a new rule group. + if (latestRuleGroupDefinition && addRuleAction.match(action)) { + action.payload.interval = latestRuleGroupDefinition.interval; + } + return ruleGroupReducer(ruleGroup, action); + }, initialRuleGroupDefinition); return { newRuleGroupDefinition, rulerConfig }; }; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleInRuleGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleInRuleGroup.test.tsx index 2324ed363db..739e57a86f6 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleInRuleGroup.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleInRuleGroup.test.tsx @@ -9,8 +9,8 @@ import { PostableRuleDTO } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; import { - grafanaRulerGroupName, - grafanaRulerGroupName2, + grafanaRulerGroup, + grafanaRulerGroup2, grafanaRulerNamespace, grafanaRulerRule, } from '../../mocks/grafanaRulerApi'; @@ -41,7 +41,7 @@ describe('Updating a Grafana managed rule', () => { const ruleGroupID: RuleGroupIdentifier = { dataSourceName: GRAFANA_RULES_SOURCE_NAME, - groupName: grafanaRulerGroupName, + groupName: grafanaRulerGroup.name, namespaceName: grafanaRulerNamespace.uid, }; @@ -71,13 +71,13 @@ describe('Updating a Grafana managed rule', () => { const ruleGroupID: RuleGroupIdentifier = { dataSourceName: GRAFANA_RULES_SOURCE_NAME, - groupName: grafanaRulerGroupName, + groupName: grafanaRulerGroup.name, namespaceName: grafanaRulerNamespace.uid, }; const targetRuleGroupID: RuleGroupIdentifier = { dataSourceName: GRAFANA_RULES_SOURCE_NAME, - groupName: grafanaRulerGroupName2, + groupName: grafanaRulerGroup2.name, namespaceName: grafanaRulerNamespace.uid, }; @@ -110,7 +110,7 @@ describe('Updating a Grafana managed rule', () => { it('should fail if the rule does not exist in the group', async () => { const ruleGroupID: RuleGroupIdentifier = { dataSourceName: GRAFANA_RULES_SOURCE_NAME, - groupName: grafanaRulerGroupName, + groupName: grafanaRulerGroup.name, namespaceName: grafanaRulerNamespace.uid, }; diff --git a/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts b/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts index 13eb4185823..18c690e7678 100644 --- a/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts +++ b/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts @@ -70,7 +70,7 @@ export const grafanaRulerGroup: RulerRuleGroupDTO = { export const grafanaRulerGroup2: RulerRuleGroupDTO = { name: grafanaRulerGroupName2, - interval: '1m', + interval: '5m', rules: [grafanaRulerRule], }; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts index 6177a07618d..43dd6761400 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts @@ -71,15 +71,17 @@ export const rulerRuleGroupHandler = (options?: HandlerOptions) => { return options.response; } - // This mimic API response as closely as possible. - // Invalid folderUid returns 403 but invalid group will return 202 with empty list of rules - // This should be fixed soon to return 404 instead of 202 const namespace = rulerTestDb.getNamespace(folderUid); if (!namespace) { return new HttpResponse(null, { status: 403 }); } const matchingGroup = rulerTestDb.getGroup(folderUid, groupName); + + if (!matchingGroup) { + return new HttpResponse({ message: 'group does not exist' }, { status: 404 }); + } + return HttpResponse.json({ name: groupName, interval: matchingGroup?.interval, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/mimirRuler.ts b/public/app/features/alerting/unified/mocks/server/handlers/mimirRuler.ts index 6dbdff31031..a0bee5efaf3 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/mimirRuler.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/mimirRuler.ts @@ -53,6 +53,11 @@ export const rulerRuleGroupHandler = (options?: HandlerOptions) => { } const matchingGroup = namespace.find((group) => group.name === groupName); + + if (!matchingGroup) { + return HttpResponse.json({ message: 'group does not exist' }, { status: 404 }); + } + return HttpResponse.json({ name: groupName, interval: matchingGroup?.interval, diff --git a/public/app/features/alerting/unified/reducers/ruler/ruleGroups.ts b/public/app/features/alerting/unified/reducers/ruler/ruleGroups.ts index b8d91042b2c..82224e02d65 100644 --- a/public/app/features/alerting/unified/reducers/ruler/ruleGroups.ts +++ b/public/app/features/alerting/unified/reducers/ruler/ruleGroups.ts @@ -9,6 +9,8 @@ import { hashRulerRule } from '../../utils/rule-id'; import { isCloudRuleIdentifier, isGrafanaRuleIdentifier, rulerRuleType } from '../../utils/rules'; // rule-scoped actions +// TOOD The interval field only make sense when adding a rule to a new rule group. +// We need to split these into distinct actions and introduce a separete addNewRuleGroupAction. export const addRuleAction = createAction<{ rule: PostableRuleDTO; groupName?: string; interval?: string }>( 'ruleGroup/rules/add' ); diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx index 185a6b69d32..ac0668721cb 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx @@ -8,7 +8,7 @@ import { AccessControlAction } from 'app/types'; import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; -import { GROUP_3, NAMESPACE_2 } from '../mocks/mimirRulerApi'; +import { GROUP_3, GROUP_4, NAMESPACE_2 } from '../mocks/mimirRulerApi'; import { mimirDataSource } from '../mocks/server/configure'; import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; import { captureRequests, serializeRequests } from '../mocks/server/events'; @@ -86,4 +86,52 @@ describe('RuleEditor cloud', () => { const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + + it('should keep existing rule interval duration when attaching new rules', async () => { + const { user } = renderRuleEditor(); + + const removeExpressionsButtons = await screen.findAllByLabelText(/Remove expression/); + expect(removeExpressionsButtons).toHaveLength(2); + + // Needs to wait for feature discovery API call to finish - Check if ruler enabled + expect(await screen.findByText('Data source-managed')).toBeInTheDocument(); + + const switchToCloudButton = screen.getByText('Data source-managed'); + expect(switchToCloudButton).toBeInTheDocument(); + expect(switchToCloudButton).toBeEnabled(); + + await user.click(switchToCloudButton); + + //expressions are removed after switching to data-source managed + expect(screen.queryAllByLabelText(/Remove expression/)).toHaveLength(0); + + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toBeInTheDocument(); + + const dataSourceSelect = await ui.inputs.dataSource.find(); + await user.click(dataSourceSelect); + await user.click(screen.getByText(MIMIR_DATASOURCE_UID)); + + await user.type(await ui.inputs.expr.find(), 'up == 1'); + + await user.type(ui.inputs.name.get(), 'my great new rule with 3m interval'); + await clickSelectOption(ui.inputs.namespace.get(), NAMESPACE_2); + await clickSelectOption(ui.inputs.group.get(), GROUP_4); + + await user.type(ui.inputs.annotationValue(0).get(), 'some summary'); + await user.type(ui.inputs.annotationValue(1).get(), 'some description'); + + // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed + await user.click(ui.buttons.addLabel.get()); + + // save and check what was sent to backend + const capture = captureRequests(); + await user.click(ui.buttons.save.get()); + const requests = await capture; + + const serializedRequests = await serializeRequests(requests); + const saveRequest = serializedRequests.find((req) => req.method === 'POST'); + + expect(saveRequest).toBeDefined(); + expect(saveRequest?.body).toMatchObject({ interval: '3m' }); + }); }); diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index f6267271e56..fedb898f955 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -11,7 +11,7 @@ import { DashboardSearchItemType } from 'app/features/search/types'; import { AccessControlAction } from 'app/types'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; -import { grafanaRulerGroup, grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { grafanaRulerGroup, grafanaRulerGroup2, grafanaRulerRule } from '../mocks/grafanaRulerApi'; import { setFolderResponse } from '../mocks/server/configure'; import { captureRequests, serializeRequests } from '../mocks/server/events'; import { setupDataSources } from '../testSetup/datasources'; @@ -140,4 +140,47 @@ describe('RuleEditor grafana managed rules', () => { const serializedRequests = await serializeRequests(requests); expect(serializedRequests).toMatchSnapshot(); }); + + it('should keep existing group interval when creating new rule in existing group', async () => { + const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/')); + + const { user } = renderRuleEditor(); + + await user.type(await ui.inputs.name.find(), 'my great new rule'); + await user.click(await screen.findByRole('button', { name: /select folder/i })); + await user.click(await screen.findByLabelText(/folder a/i)); + + // Select the existing group with 5m interval + const groupInput = await ui.inputs.group.find(); + await user.click(await byRole('combobox').find(groupInput)); + await clickSelectOption(groupInput, grafanaRulerGroup2.name); + await user.type(ui.inputs.annotationValue(1).get(), 'some description'); + + // Set pending period to none (0s) to avoid validation errors + const pendingPeriodInput = await ui.inputs.pendingPeriod.find(); + await user.clear(pendingPeriodInput); + await user.type(pendingPeriodInput, '0s'); + + await user.click(ui.buttons.save.get()); + + expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully'); + const requests = await capture; + const serializedRequests = await serializeRequests(requests); + + // Verify that the existing group's 5m interval is preserved + const saveRequest = serializedRequests.find((req) => req.method === 'POST'); + expect(saveRequest).toBeDefined(); + expect(saveRequest?.body).toMatchObject({ + name: grafanaRulerGroup2.name, + interval: '5m', // The existing group's interval should be preserved + rules: expect.arrayContaining([ + expect.objectContaining({ + annotations: expect.objectContaining({ + description: 'some description', + }), + for: '0s', + }), + ]), + }); + }); }); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index e8c024d4504..c092a748388 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -71,7 +71,7 @@ export function GrafanaGroupLoader({ ); } - if (!rulerResponse || !promResponse) { + if (!rulerResponse && !promResponse) { return ( - {rulerResponse.rules.map((rulerRule) => { + {rulerResponse?.rules.map((rulerRule) => { const promRule = matches.get(rulerRule); if (!promRule) { diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx index 108eadceeed..19aab67df84 100644 --- a/public/test/helpers/alertingRuleEditor.tsx +++ b/public/test/helpers/alertingRuleEditor.tsx @@ -31,6 +31,7 @@ export const ui = { folderContainer: byTestId(selectors.components.FolderPicker.containerV2), namespace: byTestId('namespace-picker'), group: byTestId('group-picker'), + pendingPeriod: byRole('textbox', { name: /^pending period/i }), annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`), annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`), labelKey: (idx: number) => byTestId(`label-key-${idx}`), From b46e305cb2f22f67f962ba24c73aeac41801dcba Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:40:26 +0100 Subject: [PATCH 06/23] SecureValues: Remove actor prefix from decrypters (#107433) Co-authored-by: Matheus Macabu --- .../secret/reststorage/secure_value_rest.go | 32 +++++++--- .../reststorage/secure_value_rest_test.go | 64 ++++++++++--------- .../secure-value-default-generate.yaml | 4 +- .../testdata/secure-value-generate.yaml | 4 +- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest.go b/pkg/registry/apis/secret/reststorage/secure_value_rest.go index 0aed12aac53..7ea856509ea 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest.go @@ -13,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/request" @@ -308,7 +309,7 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro return errs } -// validateDecrypters validates that (if populated) the `decrypters` must match "actor_{name}" and must be unique. +// validateDecrypters validates that (if populated) the `decrypters` must be unique. func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList { errs := make(field.ErrorList, 0) @@ -326,8 +327,17 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru decrypterNames := make(map[string]struct{}, 0) for i, decrypter := range decrypters { + decrypter = strings.TrimSpace(decrypter) + if decrypter == "" { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"), + ) + + continue + } + // Allow List: decrypters must match exactly and be in the allowed list to be able to decrypt. - // This means an allow list item should have the format "actor_{name}" and not just "{name}". if len(decryptersAllowList) > 0 { if _, exists := decryptersAllowList[decrypter]; !exists { errs = append( @@ -341,17 +351,19 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru continue } - actor, name, found := strings.Cut(strings.TrimSpace(decrypter), "_") - if !found || actor != "actor" || name == "" { - errs = append( - errs, - field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "a decrypter must have the format `actor_{name}`"), - ) + // Use the same validation as labels for the decrypters. + if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 { + for _, verr := range verrs { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr), + ) + } continue } - if _, exists := decrypterNames[name]; exists { + if _, exists := decrypterNames[decrypter]; exists { errs = append( errs, field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"), @@ -360,7 +372,7 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru continue } - decrypterNames[name] = struct{}{} + decrypterNames[decrypter] = struct{}{} } return errs diff --git a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go index 1cde8d63816..1656ca7a1fc 100644 --- a/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go +++ b/pkg/registry/apis/secret/reststorage/secure_value_rest_test.go @@ -22,7 +22,7 @@ func TestValidateSecureValue(t *testing.T) { Description: "description", Value: "value", Keeper: &keeper, - Decrypters: []string{"actor_app1", "actor_app2"}, + Decrypters: []string{"app1", "app2"}, }, } @@ -187,8 +187,8 @@ func TestValidateSecureValue(t *testing.T) { Description: "description", Ref: &ref, Decrypters: []string{ - "actor_app1", - "actor_app1", + "app1", + "app1", }, }, } @@ -198,33 +198,8 @@ func TestValidateSecureValue(t *testing.T) { require.Equal(t, "spec.decrypters.[1]", errs[0].Field) }) - t.Run("`decrypters` must match the expected format", func(t *testing.T) { - ref := "ref" - sv := &secretv0alpha1.SecureValue{ - Spec: secretv0alpha1.SecureValueSpec{ - Description: "description", Ref: &ref, - - Decrypters: []string{ - "app1", - "_app1", - "actr_app1", - "actor_ ", - "actor_", - }, - }, - } - - errs := ValidateSecureValue(sv, nil, admission.Create, nil) - require.Len(t, errs, len(sv.Spec.Decrypters)) - - for i, err := range errs { - require.Equal(t, fmt.Sprintf("spec.decrypters.[%d]", i), err.Field) - require.Contains(t, err.Error(), "a decrypter must have the format `actor_{name}`") - } - }) - t.Run("when set, the `decrypters` must be one of the allowed in the allow list", func(t *testing.T) { - allowList := map[string]struct{}{"actor_app1": {}, "actor_app2": {}} + allowList := map[string]struct{}{"app1": {}, "app2": {}} decrypters := slices.Collect(maps.Keys(allowList)) t.Run("no matches, returns an error", func(t *testing.T) { @@ -233,7 +208,7 @@ func TestValidateSecureValue(t *testing.T) { Spec: secretv0alpha1.SecureValueSpec{ Description: "description", Ref: &ref, - Decrypters: []string{"actor_app3"}, + Decrypters: []string{"app3"}, }, } @@ -284,10 +259,37 @@ func TestValidateSecureValue(t *testing.T) { }) }) + t.Run("`decrypters` must be a valid label value", func(t *testing.T) { + decrypters := []string{ + "", // invalid + "is/this/valid", // invalid + "is this valid", // invalid + "is.this.valid", + "is-this-valid", + "is_this_valid", + "0isthisvalid9", + "isthisvalid9", + "0isthisvalid", + "isthisvalid", + } + + ref := "ref" + sv := &secretv0alpha1.SecureValue{ + Spec: secretv0alpha1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: decrypters, + }, + } + + errs := ValidateSecureValue(sv, nil, admission.Create, nil) + require.Len(t, errs, 3) + }) + t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) { decrypters := make([]string, 0, 64+1) for i := 0; i < 64+1; i++ { - decrypters = append(decrypters, fmt.Sprintf("actor_app%d", i)) + decrypters = append(decrypters, fmt.Sprintf("app%d", i)) } ref := "ref" diff --git a/pkg/tests/apis/secret/testdata/secure-value-default-generate.yaml b/pkg/tests/apis/secret/testdata/secure-value-default-generate.yaml index 5bc9a368723..dec2cd8611b 100644 --- a/pkg/tests/apis/secret/testdata/secure-value-default-generate.yaml +++ b/pkg/tests/apis/secret/testdata/secure-value-default-generate.yaml @@ -11,5 +11,5 @@ spec: description: This is a secret value: this is super duper secure decrypters: - - actor_k6 - - actor_synthetic-monitoring + - k6 + - synthetic-monitoring diff --git a/pkg/tests/apis/secret/testdata/secure-value-generate.yaml b/pkg/tests/apis/secret/testdata/secure-value-generate.yaml index 2743a32650b..158052350c9 100644 --- a/pkg/tests/apis/secret/testdata/secure-value-generate.yaml +++ b/pkg/tests/apis/secret/testdata/secure-value-generate.yaml @@ -12,5 +12,5 @@ spec: keeper: my-keeper-1 value: super duper secure decrypters: - - actor_k6 - - actor_synthetic-monitoring + - k6 + - synthetic-monitoring From bd140613672cda9ca191be8d96b8ea842194881d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 1 Jul 2025 09:58:44 +0200 Subject: [PATCH 07/23] Plugin Extensions: adds source and id to mutation logs (#107385) * Plugin Extensions: adds source and id to mutation logs * chore: fix broken tests * chore: updates after PR feedback --- .../components/DataSourcePluginSettings.tsx | 2 +- .../extensions/usePluginComponent.test.tsx | 18 +- .../extensions/usePluginComponents.test.tsx | 18 +- .../plugins/extensions/utils.test.tsx | 228 ++++++++++++------ .../app/features/plugins/extensions/utils.tsx | 46 +++- 5 files changed, 215 insertions(+), 97 deletions(-) diff --git a/public/app/features/datasources/components/DataSourcePluginSettings.tsx b/public/app/features/datasources/components/DataSourcePluginSettings.tsx index 80098048692..78062064bfc 100644 --- a/public/app/features/datasources/components/DataSourcePluginSettings.tsx +++ b/public/app/features/datasources/components/DataSourcePluginSettings.tsx @@ -34,7 +34,7 @@ export class DataSourcePluginSettings extends PureComponent {
{plugin.components.ConfigEditor && createElement(plugin.components.ConfigEditor, { - options: writableProxy(dataSource), + options: writableProxy(dataSource, { source: 'datasource', pluginId: plugin.meta?.id }), onOptionsChange: this.onModelChanged, })}
diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 266a9f602c0..9ce151c2200 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -381,9 +381,12 @@ describe('usePluginComponent()', () => { expect(() => render(Component && )).not.toThrow(); // Should log an error in dev mode - expect(log.error).toHaveBeenCalledWith('Attempted to mutate object property "c"', { - stack: expect.any(String), - }); + expect(log.error).toHaveBeenCalledWith( + 'Attempted to mutate object property "c" from extension with id myorg-extensions-app', + { + stack: expect.any(String), + } + ); }); it('should pass a writable copy of the props (in production mode)', async () => { @@ -434,8 +437,11 @@ describe('usePluginComponent()', () => { expect(() => render(Component && )).not.toThrow(); // Should log a warning - expect(log.warning).toHaveBeenCalledWith('Attempted to mutate object property "c"', { - stack: expect.any(String), - }); + expect(log.warning).toHaveBeenCalledWith( + 'Attempted to mutate object property "c" from extension with id myorg-extensions-app', + { + stack: expect.any(String), + } + ); }); }); diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index 99ebda0eef5..dc8b84a7287 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -266,9 +266,12 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render()).not.toThrow(); - expect(log.error).toHaveBeenCalledWith(`Attempted to mutate object property "foo4"`, { - stack: expect.any(String), - }); + expect(log.error).toHaveBeenCalledWith( + `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app`, + { + stack: expect.any(String), + } + ); // Check if the original property hasn't been changed expect(originalFoo.foo2.foo3.foo4).toBe('bar'); @@ -327,9 +330,12 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render()).not.toThrow(); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "foo4"`, { - stack: expect.any(String), - }); + expect(log.warning).toHaveBeenCalledWith( + `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app`, + { + stack: expect.any(String), + } + ); // Check if the original property hasn't been changed expect(originalFoo.foo2.foo3.foo4).toBe('bar'); diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index b41ec1c99f0..2755d98d064 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -7,7 +7,6 @@ import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { log } from './logs/log'; -import { resetLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, @@ -30,17 +29,17 @@ jest.mock('app/features/plugins/pluginSettings', () => ({ getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }), })); -jest.mock('./logs/log', () => { - const { createLogMock } = jest.requireActual('./logs/testUtils'); - const original = jest.requireActual('./logs/log'); - - return { - ...original, - log: createLogMock(), - }; -}); - describe('Plugin Extensions / Utils', () => { + beforeEach(() => { + jest.spyOn(log, 'error').mockImplementation(() => {}); + jest.spyOn(log, 'warning').mockImplementation(() => {}); + jest.spyOn(log, 'debug').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + describe('deepFreeze()', () => { test('should not fail when called with primitive values', () => { // Although the type system doesn't allow to call it with primitive values, it can happen that the plugin just ignores these errors. @@ -386,69 +385,146 @@ describe('Plugin Extensions / Utils', () => { }); describe('getMutationObserverProxy()', () => { - it('should not be possible to modify values in proxied object, but logs a warning', () => { - const proxy = getMutationObserverProxy({ a: 'a' }); - - expect(() => { - proxy.a = 'b'; - }).not.toThrow(); - - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "a"`, { - stack: expect.any(String), + describe('in development mode', () => { + beforeEach(() => { + config.buildInfo.env = 'development'; }); - expect(proxy.a).toBe('b'); - }); + it('should be possible to modify values in proxied object, but logs an error', () => { + const proxy = getMutationObserverProxy({ a: 'a' }, { pluginId: 'myorg-cool-datasource', source: 'datasource' }); - it('should be possible to set new values, but logs a debug message', () => { - const obj: { a: string; b?: string } = { a: 'a' }; - const proxy = getMutationObserverProxy(obj); + expect(() => { + proxy.a = 'b'; + }).not.toThrow(); - expect(() => { - Object.defineProperty(proxy, 'b', { - value: 'b', - writable: false, + expect(log.error).toHaveBeenCalledWith( + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + { + stack: expect.any(String), + } + ); + + expect(proxy.a).toBe('b'); + }); + + it('should be possible to call defineProperty, but logs a debug message', () => { + const obj: { a: string; b?: string } = { a: 'a' }; + const proxy = getMutationObserverProxy(obj, { pluginId: 'myorg-cool-extension' }); + + expect(() => { + Object.defineProperty(proxy, 'b', { + value: 'b', + writable: false, + }); + }).not.toThrow(); + + expect(log.debug).toHaveBeenCalledWith( + `Attempted to define object property "b" from extension with id myorg-cool-extension`, + { + stack: expect.any(String), + } + ); + + expect(proxy.b).toBe('b'); + }); + + it('should be possible to delete properties, but logs an error', () => { + const proxy = getMutationObserverProxy({ + a: { + c: 'c', + }, + b: 'b', }); - }).not.toThrow(); - expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "b"`, { - stack: expect.any(String), + expect(() => { + // @ts-ignore - This is to test the logic + delete proxy.a.c; + }).not.toThrow(); + + expect(log.error).toHaveBeenCalledWith( + `Attempted to delete object property "c" from extension with id unknown`, + { + stack: expect.any(String), + } + ); + + expect(proxy.a.c).toBeUndefined(); }); - - expect(proxy.b).toBe('b'); }); - it('should be possible to delete properties, but logs a warning', () => { - const proxy = getMutationObserverProxy({ - a: { - c: 'c', - }, - b: 'b', + describe('in production mode', () => { + beforeEach(() => { + config.buildInfo.env = 'production'; }); - expect(() => { - // @ts-ignore - This is to test the logic - delete proxy.a.c; - }).not.toThrow(); + it('should be possible to modify values in proxied object, but logs a warning', () => { + const proxy = getMutationObserverProxy({ a: 'a' }, { pluginId: 'myorg-cool-datasource', source: 'datasource' }); - expect(log.warning).toHaveBeenCalledWith(`Attempted to delete object property "c"`, { - stack: expect.any(String), + expect(() => { + proxy.a = 'b'; + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith( + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + { + stack: expect.any(String), + } + ); + + expect(proxy.a).toBe('b'); }); - expect(proxy.a.c).toBeUndefined(); + it('should be possible to call defineProperty, but logs a debug message', () => { + const obj: { a: string; b?: string } = { a: 'a' }; + const proxy = getMutationObserverProxy(obj, { pluginId: 'myorg-cool-extension' }); + + expect(() => { + Object.defineProperty(proxy, 'b', { + value: 'b', + writable: false, + }); + }).not.toThrow(); + + expect(log.debug).toHaveBeenCalledWith( + `Attempted to define object property "b" from extension with id myorg-cool-extension`, + { + stack: expect.any(String), + } + ); + + expect(proxy.b).toBe('b'); + }); + + it('should be possible to delete properties, but logs a warning', () => { + const proxy = getMutationObserverProxy({ + a: { + c: 'c', + }, + b: 'b', + }); + + expect(() => { + // @ts-ignore - This is to test the logic + delete proxy.a.c; + }).not.toThrow(); + + expect(log.warning).toHaveBeenCalledWith( + `Attempted to delete object property "c" from extension with id unknown`, + { + stack: expect.any(String), + } + ); + + expect(proxy.a.c).toBeUndefined(); + }); }); }); describe('writableProxy()', () => { const originalEnv = config.buildInfo.env; - beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(); - }); - afterEach(() => { config.buildInfo.env = originalEnv; - jest.mocked(console.warn).mockClear(); }); it('should return the same value for primitive types', () => { @@ -464,7 +540,7 @@ describe('Plugin Extensions / Utils', () => { config.buildInfo.env = 'development'; const obj = { a: 'a' }; - const copy = writableProxy(obj); + const copy = writableProxy(obj, { source: 'datasource', pluginId: 'myorg-cool-datasource' }); expect(copy).not.toBe(obj); expect(copy.a).toBe('a'); @@ -473,9 +549,12 @@ describe('Plugin Extensions / Utils', () => { copy.a = 'b'; }).not.toThrow(); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "a"`, { - stack: expect.any(String), - }); + expect(log.error).toHaveBeenCalledWith( + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + { + stack: expect.any(String), + } + ); expect(copy.a).toBe('b'); }); @@ -484,7 +563,7 @@ describe('Plugin Extensions / Utils', () => { config.buildInfo.env = 'production'; const obj = { a: 'a' }; - const copy = writableProxy(obj); + const copy = writableProxy(obj, { source: 'datasource', pluginId: 'myorg-cool-datasource' }); expect(copy).not.toBe(obj); expect(copy.a).toBe('a'); @@ -493,9 +572,12 @@ describe('Plugin Extensions / Utils', () => { copy.a = 'b'; }).not.toThrow(); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "a"`, { - stack: expect.any(String), - }); + expect(log.warning).toHaveBeenCalledWith( + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + { + stack: expect.any(String), + } + ); expect(copy.a).toBe('b'); }); @@ -515,7 +597,7 @@ describe('Plugin Extensions / Utils', () => { expect(Object.isFrozen(copy.b)).toBe(true); expect(copy.b).toEqual({ c: 'c' }); - expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "a"`, { + expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "a" from extension with id unknown`, { stack: expect.any(String), }); }); @@ -644,10 +726,6 @@ describe('Plugin Extensions / Utils', () => { ); }; - beforeEach(() => { - resetLogMock(log); - }); - it('should make the plugin context available for the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); @@ -674,17 +752,18 @@ describe('Plugin Extensions / Utils', () => { const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); const props = { a: { b: { c: 'Grafana' } } }; - jest.spyOn(console, 'error').mockImplementation(); - render(); expect(await screen.findByText('Hello Grafana!')).toBeVisible(); // Logs a warning expect(log.error).toHaveBeenCalledTimes(1); - expect(log.error).toHaveBeenCalledWith(`Attempted to mutate object property "c"`, { - stack: expect.any(String), - }); + expect(log.error).toHaveBeenCalledWith( + `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel`, + { + stack: expect.any(String), + } + ); // Not able to mutate the props in dev mode either expect(props.a.b.c).toBe('Grafana'); @@ -702,9 +781,12 @@ describe('Plugin Extensions / Utils', () => { // Logs a warning expect(log.warning).toHaveBeenCalledTimes(1); - expect(log.warning).toHaveBeenCalledWith(`Attempted to mutate object property "c"`, { - stack: expect.any(String), - }); + expect(log.warning).toHaveBeenCalledWith( + `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel`, + { + stack: expect.any(String), + } + ); // Not able to mutate the props in production mode either expect(props.a.b.c).toBe('Grafana'); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 423308dbe63..ec994d81383 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -22,7 +22,7 @@ import appEvents from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; import { OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; -import { ExtensionsLog, log } from './logs/log'; +import { ExtensionsLog, log as baseLog } from './logs/log'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators'; @@ -47,7 +47,7 @@ export function createOpenModalFunction(pluginId: string): PluginExtensionEventH component: wrapWithPluginContext( pluginId, getModalWrapper({ title, body, width, height }), - log + baseLog ), }) ); @@ -85,7 +85,7 @@ export const wrapWithPluginContext = (pluginId: string, Component: React.Com return ( - + ); }; @@ -218,23 +218,35 @@ export function getReadOnlyProxy(obj: T): T { }); } +type MutationSource = 'extension' | 'datasource'; +interface ProxyOptions { + log?: ExtensionsLog; + source?: MutationSource; + pluginId?: string; +} + /** * Returns a proxy that logs any attempted mutation to the original object. * * @param obj The object to observe + * @param options The options for the proxy + * @param options.log The logger to use + * @param options.source The source of the mutation + * @param options.pluginId The id of the plugin that is mutating the object * @returns A new proxy object that logs any attempted mutation to the original object */ -export function getMutationObserverProxy(obj: T, _log: ExtensionsLog = log): T { +export function getMutationObserverProxy(obj: T, options?: ProxyOptions): T { if (!obj || typeof obj !== 'object' || isMutationObserverProxy(obj)) { return obj; } + const { log = baseLog, source = 'extension', pluginId = 'unknown' } = options ?? {}; const cache = new WeakMap(); - const logFunction = isGrafanaDevMode() ? _log.error.bind(_log) : _log.warning.bind(_log); // should show error during local development + const logFunction = isGrafanaDevMode() ? log.error.bind(log) : log.warning.bind(log); // should show error during local development return new Proxy(obj, { deleteProperty(target, prop) { - logFunction(`Attempted to delete object property "${String(prop)}"`, { + logFunction(`Attempted to delete object property "${String(prop)}" from ${source} with id ${pluginId}`, { stack: new Error().stack ?? '', }); Reflect.deleteProperty(target, prop); @@ -243,14 +255,14 @@ export function getMutationObserverProxy(obj: T, _log: Extensi defineProperty(target, prop, descriptor) { // because immer (used by RTK) calls Object.isFrozen and Object.freeze we know that defineProperty will be called // behind the scenes as well so we only log message with debug level to minimize the noise and false positives - _log.debug(`Attempted to define object property "${String(prop)}"`, { + log.debug(`Attempted to define object property "${String(prop)}" from ${source} with id ${pluginId}`, { stack: new Error().stack ?? '', }); Reflect.defineProperty(target, prop, descriptor); return true; }, set(target, prop, newValue) { - logFunction(`Attempted to mutate object property "${String(prop)}"`, { + logFunction(`Attempted to mutate object property "${String(prop)}" from ${source} with id ${pluginId}`, { stack: new Error().stack ?? '', }); Reflect.set(target, prop, newValue); @@ -278,7 +290,7 @@ export function getMutationObserverProxy(obj: T, _log: Extensi if (isObject(value) || isArray(value)) { if (!cache.has(value)) { - cache.set(value, getMutationObserverProxy(value, _log)); + cache.set(value, getMutationObserverProxy(value, { log, source, pluginId })); } return cache.get(value); } @@ -288,14 +300,26 @@ export function getMutationObserverProxy(obj: T, _log: Extensi }); } -export function writableProxy(value: T, _log: ExtensionsLog = log): T { +/** + * Returns a proxy that logs any attempted mutation to the original object. + * + * @param value The object to observe + * @param options The options for the proxy + * @param options.log The logger to use + * @param options.source The source of the mutation + * @param options.pluginId The id of the plugin that is mutating the object + * @returns A new proxy object that logs any attempted mutation to the original object + */ +export function writableProxy(value: T, options?: ProxyOptions): T { // Primitive types are read-only by default if (!value || typeof value !== 'object') { return value; } + const { log = baseLog, source = 'extension', pluginId = 'unknown' } = options ?? {}; + // Default: we return a proxy of a deep-cloned version of the original object, which logs warnings when mutation is attempted - return getMutationObserverProxy(cloneDeep(value), _log); + return getMutationObserverProxy(cloneDeep(value), { log, pluginId, source }); } function isRecord(value: unknown): value is Record { From 2f8c1a3c48aa779f76802cd7962de5fa59847a08 Mon Sep 17 00:00:00 2001 From: Misi Date: Tue, 1 Jul 2025 10:33:10 +0200 Subject: [PATCH 08/23] Auth: Enable improved session handling by default for OAuth and SAML (#107442) Enable improved session handling by default --- .../feature-toggles/index.md | 168 +++++++++--------- .../src/types/featureToggles.gen.ts | 2 + pkg/services/featuremgmt/registry.go | 6 +- pkg/services/featuremgmt/toggles_gen.csv | 4 +- pkg/services/featuremgmt/toggles_gen.json | 24 ++- 5 files changed, 108 insertions(+), 96 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 48a2ca08a92..09f24bcb746 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -22,95 +22,95 @@ For more information about feature release stages, refer to [Release life cycle Most [generally available](https://grafana.com/docs/release-life-cycle/#general-availability) features are enabled by default. You can disable these feature by setting the feature flag to "false" in the configuration. -| Feature toggle name | Description | Enabled by default | -| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | -| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes | -| `featureHighlights` | Highlight Grafana Enterprise features | | -| `correlations` | Correlations page | Yes | -| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | -| `nestedFolders` | Enable folder nesting | Yes | -| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | -| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | -| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | -| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | -| `unifiedRequestLog` | Writes error logs to the request logger | Yes | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | Yes | -| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | -| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | -| `transformationsRedesign` | Enables the transformations redesign | Yes | -| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | -| `dashgpt` | Enable AI powered features in dashboards | Yes | -| `externalCorePlugins` | Allow core plugins to be loaded as external | Yes | -| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | -| `formatString` | Enable format string transformer | Yes | -| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | Yes | -| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | -| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes | -| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | -| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | -| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | -| `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | -| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | -| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | -| `lokiQueryHints` | Enables query hints for Loki | Yes | -| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | -| `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | -| `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes | -| `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | Yes | -| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes | -| `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | Yes | -| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | -| `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes | -| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | -| `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | Yes | -| `pinNavItems` | Enables pinning of nav items | Yes | -| `ssoSettingsLDAP` | Use the new SSO Settings API to configure LDAP | Yes | -| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | -| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes | -| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | Yes | -| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes | -| `pluginsSriChecks` | Enables SRI checks for plugin assets | | -| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | -| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | -| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | -| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | -| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | -| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes | -| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | Yes | -| `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes | -| `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes | -| `unifiedNavbars` | Enables unified navbars | | -| `tabularNumbers` | Use fixed-width numbers globally in the UI | Yes | +| Feature toggle name | Description | Enabled by default | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | +| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes | +| `featureHighlights` | Highlight Grafana Enterprise features | | +| `correlations` | Correlations page | Yes | +| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | +| `nestedFolders` | Enable folder nesting | Yes | +| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | +| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | +| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | +| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | +| `unifiedRequestLog` | Writes error logs to the request logger | Yes | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | Yes | +| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | +| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | +| `transformationsRedesign` | Enables the transformations redesign | Yes | +| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | +| `dashgpt` | Enable AI powered features in dashboards | Yes | +| `externalCorePlugins` | Allow core plugins to be loaded as external | Yes | +| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | +| `formatString` | Enable format string transformer | Yes | +| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | Yes | +| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | +| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes | +| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | +| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | +| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | +| `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | +| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | +| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | +| `lokiQueryHints` | Enables query hints for Loki | Yes | +| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | +| `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | +| `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes | +| `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | Yes | +| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes | +| `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | Yes | +| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | +| `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes | +| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | +| `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | Yes | +| `pinNavItems` | Enables pinning of nav items | Yes | +| `ssoSettingsLDAP` | Use the new SSO Settings API to configure LDAP | Yes | +| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | +| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes | +| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | Yes | +| `improvedExternalSessionHandling` | Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. | Yes | +| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes | +| `pluginsSriChecks` | Enables SRI checks for plugin assets | | +| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | +| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | +| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | +| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | +| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | +| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes | +| `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. | Yes | +| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | Yes | +| `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes | +| `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes | +| `unifiedNavbars` | Enables unified navbars | | +| `tabularNumbers` | Use fixed-width numbers globally in the UI | Yes | ## Public preview feature toggles [Public preview](https://grafana.com/docs/release-life-cycle/#public-preview) features are supported by our Support teams, but might be limited to enablement, configuration, and some troubleshooting. -| Feature toggle name | Description | -| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `panelTitleSearch` | Search for dashboards using panel title | -| `grpcServer` | Run the GRPC server | -| `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache | -| `refactorVariablesTimeRange` | Refactor time range variables flow to reduce number of API calls made when query variables are chained | -| `faroDatasourceSelector` | Enable the data source selector within the Frontend Apps section of the Frontend Observability | -| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel | -| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior | -| `reportingRetries` | Enables rendering retries for the reporting feature | -| `externalServiceAccounts` | Automatic service account and token setup for plugins | -| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | -| `pdfTables` | Enables generating table data as PDF in reporting | -| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | -| `regressionTransformation` | Enables regression analysis transformation | -| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | -| `tableNextGen` | Allows access to the new react-data-grid based table component. | -| `improvedExternalSessionHandling` | Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. | -| `enableSCIM` | Enables SCIM support for user and group management | -| `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource | -| `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. | -| `alertRuleRestore` | Enables the alert rule restore feature | -| `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | -| `logsPanelControls` | Enables a control component for the logs panel in Explore | +| Feature toggle name | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `panelTitleSearch` | Search for dashboards using panel title | +| `grpcServer` | Run the GRPC server | +| `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache | +| `refactorVariablesTimeRange` | Refactor time range variables flow to reduce number of API calls made when query variables are chained | +| `faroDatasourceSelector` | Enable the data source selector within the Frontend Apps section of the Frontend Observability | +| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel | +| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior | +| `reportingRetries` | Enables rendering retries for the reporting feature | +| `externalServiceAccounts` | Automatic service account and token setup for plugins | +| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | +| `pdfTables` | Enables generating table data as PDF in reporting | +| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | +| `regressionTransformation` | Enables regression analysis transformation | +| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | +| `tableNextGen` | Allows access to the new react-data-grid based table component. | +| `enableSCIM` | Enables SCIM support for user and group management | +| `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource | +| `alertRuleRestore` | Enables the alert rule restore feature | +| `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | +| `logsPanelControls` | Enables a control component for the logs panel in Explore | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index fd21c30aa12..a68bc7368a9 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -667,6 +667,7 @@ export interface FeatureToggles { alertingQueryAndExpressionsStepMode?: boolean; /** * Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. + * @default true */ improvedExternalSessionHandling?: boolean; /** @@ -803,6 +804,7 @@ export interface FeatureToggles { k8SFolderMove?: boolean; /** * Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. + * @default true */ improvedExternalSessionHandlingSAML?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9e99c99bb3a..c589310337b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1145,7 +1145,8 @@ var ( { Name: "improvedExternalSessionHandling", Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default Owner: identityAccessTeam, AllowSelfServe: true, }, @@ -1367,7 +1368,8 @@ var ( { Name: "improvedExternalSessionHandlingSAML", Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default Owner: identityAccessTeam, AllowSelfServe: true, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index cd2d6706a08..66dc7718f48 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -148,7 +148,7 @@ exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true -improvedExternalSessionHandling,preview,@grafana/identity-access-team,false,false,false +improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false @@ -179,7 +179,7 @@ lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false -improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false +improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false teamHttpHeadersMimir,GA,@grafana/identity-access-team,false,false,false teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 052b3b22280..e73c13bc098 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1399,27 +1399,35 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1750434297879", - "creationTimestamp": "2024-09-17T10:54:39Z" + "resourceVersion": "1751355094344", + "creationTimestamp": "2024-09-17T10:54:39Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC" + } }, "spec": { "description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", - "stage": "preview", + "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true + "allowSelfServe": true, + "expression": "true" } }, { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-01-09T17:02:49Z" + "resourceVersion": "1751355094344", + "creationTimestamp": "2025-01-09T17:02:49Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC" + } }, "spec": { "description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", - "stage": "preview", + "stage": "GA", "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true + "allowSelfServe": true, + "expression": "true" } }, { From 08f3cfbbf76e87efc08f72a5f9c456d517203be9 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:36:35 +0200 Subject: [PATCH 09/23] Jaeger: Decouple backend (#107310) * Jaeger: Decouple backend * Update * Remove core import --- .golangci.yml | 2 + pkg/tsdb/jaeger/jaeger.go | 7 ++-- pkg/tsdb/jaeger/standalone/datasource.go | 39 +++++++++++++++++++ pkg/tsdb/jaeger/standalone/main.go | 23 +++++++++++ .../app/plugins/datasource/jaeger/plugin.json | 1 + 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 pkg/tsdb/jaeger/standalone/datasource.go create mode 100644 pkg/tsdb/jaeger/standalone/main.go diff --git a/.golangci.yml b/.golangci.yml index 4c7cbc8f644..745881e3e2e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -103,6 +103,8 @@ linters: - '**/pkg/tsdb/cloudwatch/**/*' - '**/pkg/tsdb/loki/*' - '**/pkg/tsdb/loki/**/*' + - '**/pkg/tsdb/jaeger/*' + - '**/pkg/tsdb/jaeger/**/*' deny: - pkg: github.com/grafana/grafana/pkg/api desc: Core plugins are not allowed to depend on Grafana core packages diff --git a/pkg/tsdb/jaeger/jaeger.go b/pkg/tsdb/jaeger/jaeger.go index a3f336c6f8d..a5c71c4327a 100644 --- a/pkg/tsdb/jaeger/jaeger.go +++ b/pkg/tsdb/jaeger/jaeger.go @@ -8,10 +8,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" - - "github.com/grafana/grafana/pkg/infra/httpclient" ) var logger = backend.NewLoggerWith("logger", "tsdb.jaeger") @@ -20,7 +19,7 @@ type Service struct { im instancemgmt.InstanceManager } -func ProvideService(httpClientProvider httpclient.Provider) *Service { +func ProvideService(httpClientProvider *httpclient.Provider) *Service { return &Service{ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)), } @@ -36,7 +35,7 @@ type datasourceJSONData struct { } `json:"traceIdTimeParams"` } -func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { +func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { httpClientOptions, err := settings.HTTPClientOptions(ctx) if err != nil { diff --git a/pkg/tsdb/jaeger/standalone/datasource.go b/pkg/tsdb/jaeger/standalone/datasource.go new file mode 100644 index 00000000000..d61de484087 --- /dev/null +++ b/pkg/tsdb/jaeger/standalone/datasource.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + + jaeger "github.com/grafana/grafana/pkg/tsdb/jaeger" +) + +var ( + _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CallResourceHandler = (*Datasource)(nil) +) + +func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &Datasource{ + Service: jaeger.ProvideService(httpclient.NewProvider()), + }, nil +} + +type Datasource struct { + Service *jaeger.Service +} + +func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + return d.Service.QueryData(ctx, req) +} + +func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return d.Service.CallResource(ctx, req, sender) +} + +func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return d.Service.CheckHealth(ctx, req) +} diff --git a/pkg/tsdb/jaeger/standalone/main.go b/pkg/tsdb/jaeger/standalone/main.go new file mode 100644 index 00000000000..cb20b94f200 --- /dev/null +++ b/pkg/tsdb/jaeger/standalone/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + // Start listening to requests sent from Grafana. This call is blocking so + // it won't finish until Grafana shuts down the process or the plugin choose + // to exit by itself using os.Exit. Manage automatically manages life cycle + // of datasource instances. It accepts datasource instance factory as first + // argument. This factory will be automatically called on incoming request + // from Grafana to create different instances of SampleDatasource (per datasource + // ID). When datasource configuration changed Dispose method will be called and + // new datasource instance created using NewSampleDatasource factory. + if err := datasource.Manage("jaeger", NewDatasource, datasource.ManageOpts{}); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/public/app/plugins/datasource/jaeger/plugin.json b/public/app/plugins/datasource/jaeger/plugin.json index 3d4d6802480..cd7169bb666 100644 --- a/public/app/plugins/datasource/jaeger/plugin.json +++ b/public/app/plugins/datasource/jaeger/plugin.json @@ -3,6 +3,7 @@ "name": "Jaeger", "id": "jaeger", "category": "tracing", + "executable": "gpx_jaeger", "backend": true, "metrics": true, From 2294620b2e6ea82ad53393200532138381e66c60 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 1 Jul 2025 10:49:16 +0200 Subject: [PATCH 10/23] Advisor: Include optional more info in failures (#107244) --- apps/advisor/kinds/check.cue | 2 + .../apis/advisor/v0alpha1/check_status_gen.go | 2 + .../apis/advisor/v0alpha1/zz_openapi_gen.go | 7 + apps/advisor/pkg/apis/advisor_manifest.go | 2 +- .../pkg/app/checks/datasourcecheck/check.go | 178 ---------- .../app/checks/datasourcecheck/check_test.go | 3 +- .../datasourcecheck/health_check_step.go | 93 +++++ .../datasourcecheck/missing_plugin_step.go | 77 +++++ .../datasourcecheck/uid_validation_step.go | 50 +++ apps/advisor/pkg/app/checks/utils.go | 18 + .../advisor.grafana.app-v0alpha1.json | 4 + .../clients/advisor/v0alpha1/endpoints.gen.ts | 323 +++++++++++++++++- 12 files changed, 578 insertions(+), 181 deletions(-) create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go create mode 100644 apps/advisor/pkg/app/checks/datasourcecheck/uid_validation_step.go diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index 755561ab615..2ce51faf910 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -40,6 +40,8 @@ check: { itemID: string // Links to actions that can be taken to resolve the failure links: [...#ErrorLink] + // More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. + moreInfo?: string } #Report: { // Number of elements analyzed diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go index 1658881108b..b77811fefc2 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go @@ -27,6 +27,8 @@ type CheckReportFailure struct { ItemID string `json:"itemID"` // Links to actions that can be taken to resolve the failure Links []CheckErrorLink `json:"links"` + // More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. + MoreInfo *string `json:"moreInfo,omitempty"` } // NewCheckReportFailure creates a new CheckReportFailure object. diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go index bf1119a40e6..84e85e7f5ba 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go @@ -205,6 +205,13 @@ func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCal }, }, }, + "moreInfo": { + SchemaProps: spec.SchemaProps{ + Description: "More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.", + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"severity", "stepID", "item", "itemID", "links"}, }, diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 73246227712..6f5d472dc54 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -12,7 +12,7 @@ import ( ) var ( - rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) versionSchemaCheckv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index e5bdc4c4e2e..38846740814 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -3,19 +3,14 @@ package datasourcecheck import ( "context" "errors" - "fmt" - sysruntime "runtime" - "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-plugin-sdk-go/backend" - advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/util" ) const ( @@ -110,179 +105,6 @@ func (c *check) Steps() []checks.Step { } } -type uidValidationStep struct{} - -func (s *uidValidationStep) ID() string { - return UIDValidationStepID -} - -func (s *uidValidationStep) Title() string { - return "UID validation" -} - -func (s *uidValidationStep) Description() string { - return "Checks if the UID of a data source is valid." -} - -func (s *uidValidationStep) Resolution() string { - return "Check the documentation for more information or delete the data source and create a new one." -} - -func (s *uidValidationStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { - ds, ok := i.(*datasources.DataSource) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - // Data source UID validation - err := util.ValidateUID(ds.UID) - if err != nil { - return []advisor.CheckReportFailure{checks.NewCheckReportFailure( - advisor.CheckReportFailureSeverityLow, - s.ID(), - fmt.Sprintf("%s (%s)", ds.Name, ds.UID), - ds.UID, - []advisor.CheckErrorLink{}, - )}, nil - } - return nil, nil -} - -type healthCheckStep struct { - PluginContextProvider pluginContextProvider - PluginClient plugins.Client -} - -func (s *healthCheckStep) Title() string { - return "Health check" -} - -func (s *healthCheckStep) Description() string { - return "Checks if a data source is healthy." -} - -func (s *healthCheckStep) Resolution() string { - return "Go to the data source configuration page and address the issues reported." -} - -func (s *healthCheckStep) ID() string { - return HealthCheckStepID -} - -func (s *healthCheckStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { - ds, ok := i.(*datasources.DataSource) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - - // Health check execution - requester, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) - if err != nil { - if errors.Is(err, plugins.ErrPluginNotRegistered) { - // The plugin is not installed, handle this in the missing plugin step - return nil, nil - } - // Unable to check health check - log.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err) - return nil, nil - } - req := &backend.CheckHealthRequest{ - PluginContext: pCtx, - Headers: map[string]string{}, - } - resp, err := s.PluginClient.CheckHealth(ctx, req) - if err != nil || resp.Status != backend.HealthStatusOk { - if err != nil { - log.Debug("Failed to check health", "datasource_uid", ds.UID, "error", err) - if errors.Is(err, plugins.ErrMethodNotImplemented) || errors.Is(err, plugins.ErrPluginUnavailable) { - // The plugin does not support backend health checks - return nil, nil - } - } else { - log.Debug("Failed to check health", "datasource_uid", ds.UID, "status", resp.Status, "message", resp.Message) - } - return []advisor.CheckReportFailure{checks.NewCheckReportFailure( - advisor.CheckReportFailureSeverityHigh, - s.ID(), - ds.Name, - ds.UID, - []advisor.CheckErrorLink{ - { - Message: "Fix me", - Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), - }, - }, - )}, nil - } - return nil, nil -} - -type missingPluginStep struct { - PluginStore pluginstore.Store - PluginRepo repo.Service - GrafanaVersion string -} - -func (s *missingPluginStep) Title() string { - return "Missing plugin check" -} - -func (s *missingPluginStep) Description() string { - return "Checks if the plugin associated with the data source is installed and available." -} - -func (s *missingPluginStep) Resolution() string { - return "Delete the datasource or install the plugin." -} - -func (s *missingPluginStep) ID() string { - return MissingPluginStepID -} - -func (s *missingPluginStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { - ds, ok := i.(*datasources.DataSource) - if !ok { - return nil, fmt.Errorf("invalid item type %T", i) - } - - _, exists := s.PluginStore.Plugin(ctx, ds.Type) - if !exists { - links := []advisor.CheckErrorLink{ - { - Message: "Delete data source", - Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), - }, - } - plugins, err := s.PluginRepo.GetPluginsInfo(ctx, repo.GetPluginsInfoOptions{ - IncludeDeprecated: true, - Plugins: []string{ds.Type}, - }, repo.NewCompatOpts(s.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH)) - if err != nil { - return nil, err - } - if len(plugins) > 0 { - // Plugin is available in the repo - links = append(links, advisor.CheckErrorLink{ - Message: "View plugin", - Url: fmt.Sprintf("/plugins/%s", ds.Type), - }) - } - // The plugin is not installed - return []advisor.CheckReportFailure{checks.NewCheckReportFailure( - advisor.CheckReportFailureSeverityHigh, - s.ID(), - ds.Name, - ds.UID, - links, - )}, nil - } - return nil, nil -} - type pluginContextProvider interface { GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index d9cb532b9d5..fb02a8892ad 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -107,7 +107,7 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} - mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "test message"}} mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ {ID: 1, Slug: "prometheus", Status: "active"}, }} @@ -125,6 +125,7 @@ func TestCheck_Run(t *testing.T) { assert.NoError(t, err) assert.Len(t, failures, 1) assert.Equal(t, "health-check", failures[0].StepID) + assert.Contains(t, *failures[0].MoreInfo, "test message") }) t.Run("should skip health check when plugin does not support backend health checks", func(t *testing.T) { diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go b/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go new file mode 100644 index 00000000000..c020d0ba82a --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/health_check_step.go @@ -0,0 +1,93 @@ +package datasourcecheck + +import ( + "context" + "errors" + "fmt" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/datasources" +) + +type healthCheckStep struct { + PluginContextProvider pluginContextProvider + PluginClient plugins.Client +} + +func (s *healthCheckStep) Title() string { + return "Health check" +} + +func (s *healthCheckStep) Description() string { + return "Checks if a data source is healthy." +} + +func (s *healthCheckStep) Resolution() string { + return "Go to the data source configuration page and address the issues reported." +} + +func (s *healthCheckStep) ID() string { + return HealthCheckStepID +} + +func (s *healthCheckStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + + // Health check execution + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) + if err != nil { + if errors.Is(err, plugins.ErrPluginNotRegistered) { + // The plugin is not installed, handle this in the missing plugin step + return nil, nil + } + // Unable to check health check + log.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err) + return nil, nil + } + req := &backend.CheckHealthRequest{ + PluginContext: pCtx, + Headers: map[string]string{}, + } + resp, err := s.PluginClient.CheckHealth(ctx, req) + if err != nil || (resp != nil && resp.Status != backend.HealthStatusOk) { + if err != nil { + log.Debug("Failed to check health", "datasource_uid", ds.UID, "error", err) + if errors.Is(err, plugins.ErrMethodNotImplemented) || errors.Is(err, plugins.ErrPluginUnavailable) { + // The plugin does not support backend health checks + return nil, nil + } + } else { + log.Debug("Failed to check health", "datasource_uid", ds.UID, "status", resp.Status, "message", resp.Message) + } + moreInfo := "" + if resp != nil { + moreInfo = fmt.Sprintf("Status: %s\nMessage: %s\nJSONDetails: %s", resp.Status, resp.Message, resp.JSONDetails) + } + return []advisor.CheckReportFailure{checks.NewCheckReportFailureWithMoreInfo( + advisor.CheckReportFailureSeverityHigh, + s.ID(), + ds.Name, + ds.UID, + []advisor.CheckErrorLink{ + { + Message: "Fix me", + Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), + }, + }, + moreInfo, + )}, nil + } + return nil, nil +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go b/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go new file mode 100644 index 00000000000..1d784f0a544 --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/missing_plugin_step.go @@ -0,0 +1,77 @@ +package datasourcecheck + +import ( + "context" + "fmt" + sysruntime "runtime" + + "github.com/grafana/grafana-app-sdk/logging" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type missingPluginStep struct { + PluginStore pluginstore.Store + PluginRepo repo.Service + GrafanaVersion string +} + +func (s *missingPluginStep) Title() string { + return "Missing plugin check" +} + +func (s *missingPluginStep) Description() string { + return "Checks if the plugin associated with the data source is installed and available." +} + +func (s *missingPluginStep) Resolution() string { + return "Delete the datasource or install the plugin." +} + +func (s *missingPluginStep) ID() string { + return MissingPluginStepID +} + +func (s *missingPluginStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + + _, exists := s.PluginStore.Plugin(ctx, ds.Type) + if !exists { + links := []advisor.CheckErrorLink{ + { + Message: "Delete data source", + Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), + }, + } + plugins, err := s.PluginRepo.GetPluginsInfo(ctx, repo.GetPluginsInfoOptions{ + IncludeDeprecated: true, + Plugins: []string{ds.Type}, + }, repo.NewCompatOpts(s.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH)) + if err != nil { + return nil, err + } + if len(plugins) > 0 { + // Plugin is available in the repo + links = append(links, advisor.CheckErrorLink{ + Message: "View plugin", + Url: fmt.Sprintf("/plugins/%s", ds.Type), + }) + } + // The plugin is not installed + return []advisor.CheckReportFailure{checks.NewCheckReportFailureWithMoreInfo( + advisor.CheckReportFailureSeverityHigh, + s.ID(), + ds.Name, + ds.UID, + links, + fmt.Sprintf("Plugin: %s", ds.Type), + )}, nil + } + return nil, nil +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/uid_validation_step.go b/apps/advisor/pkg/app/checks/datasourcecheck/uid_validation_step.go new file mode 100644 index 00000000000..0f183339aba --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/uid_validation_step.go @@ -0,0 +1,50 @@ +package datasourcecheck + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-app-sdk/logging" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/util" +) + +type uidValidationStep struct{} + +func (s *uidValidationStep) ID() string { + return UIDValidationStepID +} + +func (s *uidValidationStep) Title() string { + return "UID validation" +} + +func (s *uidValidationStep) Description() string { + return "Checks if the UID of a data source is valid." +} + +func (s *uidValidationStep) Resolution() string { + return "Check the documentation for more information or delete the data source and create a new one." +} + +func (s *uidValidationStep) Run(ctx context.Context, log logging.Logger, obj *advisor.CheckSpec, i any) ([]advisor.CheckReportFailure, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + // Data source UID validation + err := util.ValidateUID(ds.UID) + if err != nil { + return []advisor.CheckReportFailure{checks.NewCheckReportFailure( + advisor.CheckReportFailureSeverityLow, + s.ID(), + fmt.Sprintf("%s (%s)", ds.Name, ds.UID), + ds.UID, + []advisor.CheckErrorLink{}, + )}, nil + } + return nil, nil +} diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go index 7ca9cc473b4..0abcb8a603b 100644 --- a/apps/advisor/pkg/app/checks/utils.go +++ b/apps/advisor/pkg/app/checks/utils.go @@ -39,6 +39,24 @@ func NewCheckReportFailure( } } +func NewCheckReportFailureWithMoreInfo( + severity advisor.CheckReportFailureSeverity, + stepID string, + item string, + itemID string, + links []advisor.CheckErrorLink, + moreInfo string, +) advisor.CheckReportFailure { + return advisor.CheckReportFailure{ + Severity: severity, + StepID: stepID, + Item: item, + ItemID: itemID, + Links: links, + MoreInfo: &moreInfo, + } +} + func GetNamespace(stackID string) (string, error) { if stackID == "" { return metav1.NamespaceDefault, nil diff --git a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json index 7b2fad7ec8f..1fb2aa6d751 100644 --- a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json @@ -1851,6 +1851,10 @@ ] } }, + "moreInfo": { + "description": "More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.", + "type": "string" + }, "severity": { "description": "Severity of the failure", "type": "string", diff --git a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts index afadfbb5bf3..86ca7fa2a38 100644 --- a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts @@ -1,11 +1,15 @@ import { api } from './baseAPI'; -export const addTagTypes = ['Check', 'CheckType'] as const; +export const addTagTypes = ['API Discovery', 'Check', 'CheckType'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/apis/advisor.grafana.app/v0alpha1/` }), + providesTags: ['API Discovery'], + }), listCheck: build.query({ query: (queryArg) => ({ url: `/checks`, @@ -39,6 +43,29 @@ const injectedRtkApi = api }), invalidatesTags: ['Check'], }), + deletecollectionCheck: build.mutation({ + query: (queryArg) => ({ + url: `/checks`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Check'], + }), getCheck: build.query({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -48,6 +75,20 @@ const injectedRtkApi = api }), providesTags: ['Check'], }), + replaceCheck: build.mutation({ + query: (queryArg) => ({ + url: `/checks/${queryArg.name}`, + method: 'PUT', + body: queryArg.check, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Check'], + }), deleteCheck: build.mutation({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -97,6 +138,81 @@ const injectedRtkApi = api }), providesTags: ['CheckType'], }), + createCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes`, + method: 'POST', + body: queryArg.checkType, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['CheckType'], + }), + deletecollectionCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['CheckType'], + }), + getCheckType: build.query({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['CheckType'], + }), + replaceCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + method: 'PUT', + body: queryArg.checkType, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['CheckType'], + }), + deleteCheckType: build.mutation({ + query: (queryArg) => ({ + url: `/checktypes/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['CheckType'], + }), updateCheckType: build.mutation({ query: (queryArg) => ({ url: `/checktypes/${queryArg.name}`, @@ -116,6 +232,8 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; export type ListCheckApiResponse = /** status 200 OK */ CheckList; export type ListCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -176,6 +294,57 @@ export type CreateCheckApiArg = { fieldValidation?: string; check: Check; }; +export type DeletecollectionCheckApiResponse = /** status 200 OK */ Status; +export type DeletecollectionCheckApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; export type GetCheckApiResponse = /** status 200 OK */ Check; export type GetCheckApiArg = { /** name of the Check */ @@ -183,6 +352,20 @@ export type GetCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; }; +export type ReplaceCheckApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; +export type ReplaceCheckApiArg = { + /** name of the Check */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + check: Check; +}; export type DeleteCheckApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; export type DeleteCheckApiArg = { /** name of the Check */ @@ -261,6 +444,110 @@ export type ListCheckTypeApiArg = { /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ watch?: boolean; }; +export type CreateCheckTypeApiResponse = /** status 200 OK */ + | CheckType + | /** status 201 Created */ CheckType + | /** status 202 Accepted */ CheckType; +export type CreateCheckTypeApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + checkType: CheckType; +}; +export type DeletecollectionCheckTypeApiResponse = /** status 200 OK */ Status; +export type DeletecollectionCheckTypeApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetCheckTypeApiResponse = /** status 200 OK */ CheckType; +export type GetCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; +export type ReplaceCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + checkType: CheckType; +}; +export type DeleteCheckTypeApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteCheckTypeApiArg = { + /** name of the CheckType */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; export type UpdateCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; export type UpdateCheckTypeApiArg = { /** name of the CheckType */ @@ -277,6 +564,38 @@ export type UpdateCheckTypeApiArg = { force?: boolean; patch: Patch; }; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -390,6 +709,8 @@ export type CheckReportFailure = { itemID: string; /** Links to actions that can be taken to resolve the failure */ links: CheckErrorLink[]; + /** More information about the failure */ + moreInfo?: string; /** Severity of the failure */ severity: string; /** Step ID that the failure is associated with */ From 71a4f20770128769905709bc7b1e75cf7b08adeb Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:58:00 +0200 Subject: [PATCH 11/23] Zipkin: Decouple backend (#107312) * Zipkin: Decouple backend * Update * Remove core import --- .golangci.yml | 2 + pkg/tsdb/zipkin/standalone/datasource.go | 39 +++++++++++++++++++ pkg/tsdb/zipkin/standalone/main.go | 23 +++++++++++ pkg/tsdb/zipkin/zipkin.go | 7 ++-- .../app/plugins/datasource/zipkin/plugin.json | 1 + 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 pkg/tsdb/zipkin/standalone/datasource.go create mode 100644 pkg/tsdb/zipkin/standalone/main.go diff --git a/.golangci.yml b/.golangci.yml index 745881e3e2e..5bb4a5e3167 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -103,6 +103,8 @@ linters: - '**/pkg/tsdb/cloudwatch/**/*' - '**/pkg/tsdb/loki/*' - '**/pkg/tsdb/loki/**/*' + - '**/pkg/tsdb/zipkin/*' + - '**/pkg/tsdb/zipkin/**/*' - '**/pkg/tsdb/jaeger/*' - '**/pkg/tsdb/jaeger/**/*' deny: diff --git a/pkg/tsdb/zipkin/standalone/datasource.go b/pkg/tsdb/zipkin/standalone/datasource.go new file mode 100644 index 00000000000..25dea4d46a1 --- /dev/null +++ b/pkg/tsdb/zipkin/standalone/datasource.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + + "github.com/grafana/grafana/pkg/tsdb/zipkin" +) + +var ( + _ backend.QueryDataHandler = (*Datasource)(nil) + _ backend.CheckHealthHandler = (*Datasource)(nil) + _ backend.CallResourceHandler = (*Datasource)(nil) +) + +func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return &Datasource{ + Service: zipkin.ProvideService(httpclient.NewProvider()), + }, nil +} + +type Datasource struct { + Service *zipkin.Service +} + +func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + return d.Service.QueryData(ctx, req) +} + +func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return d.Service.CallResource(ctx, req, sender) +} + +func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return d.Service.CheckHealth(ctx, req) +} diff --git a/pkg/tsdb/zipkin/standalone/main.go b/pkg/tsdb/zipkin/standalone/main.go new file mode 100644 index 00000000000..0666b55e4f2 --- /dev/null +++ b/pkg/tsdb/zipkin/standalone/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "os" + + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func main() { + // Start listening to requests sent from Grafana. This call is blocking so + // it won't finish until Grafana shuts down the process or the plugin choose + // to exit by itself using os.Exit. Manage automatically manages life cycle + // of datasource instances. It accepts datasource instance factory as first + // argument. This factory will be automatically called on incoming request + // from Grafana to create different instances of SampleDatasource (per datasource + // ID). When datasource configuration changed Dispose method will be called and + // new datasource instance created using NewSampleDatasource factory. + if err := datasource.Manage("zipkin", NewDatasource, datasource.ManageOpts{}); err != nil { + log.DefaultLogger.Error(err.Error()) + os.Exit(1) + } +} diff --git a/pkg/tsdb/zipkin/zipkin.go b/pkg/tsdb/zipkin/zipkin.go index e6d77a82caf..0cd79b3f6fa 100644 --- a/pkg/tsdb/zipkin/zipkin.go +++ b/pkg/tsdb/zipkin/zipkin.go @@ -7,10 +7,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" - - "github.com/grafana/grafana/pkg/infra/httpclient" ) var logger = backend.NewLoggerWith("logger", "tsdb.zipkin") @@ -19,7 +18,7 @@ type Service struct { im instancemgmt.InstanceManager } -func ProvideService(httpClientProvider httpclient.Provider) *Service { +func ProvideService(httpClientProvider *httpclient.Provider) *Service { return &Service{ im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)), } @@ -29,7 +28,7 @@ type datasourceInfo struct { ZipkinClient ZipkinClient } -func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { +func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { httpClientOptions, err := settings.HTTPClientOptions(ctx) if err != nil { diff --git a/public/app/plugins/datasource/zipkin/plugin.json b/public/app/plugins/datasource/zipkin/plugin.json index 9f6e8994f17..fe2c6705549 100644 --- a/public/app/plugins/datasource/zipkin/plugin.json +++ b/public/app/plugins/datasource/zipkin/plugin.json @@ -3,6 +3,7 @@ "name": "Zipkin", "id": "zipkin", "category": "tracing", + "executable": "gpx_zipkin", "backend": true, "metrics": true, From 974a2c47f9d83889b28495252444cbe34faab648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:22:55 +0200 Subject: [PATCH 12/23] feat(unified-storage): add qos support for the resource server (#105939) --- pkg/server/distributor_test.go | 13 ++- pkg/server/module_server.go | 11 +- pkg/setting/setting.go | 3 + pkg/setting/setting_unified_storage.go | 5 +- pkg/storage/unified/client.go | 49 ++++++++- pkg/storage/unified/resource/errors.go | 16 +++ pkg/storage/unified/resource/server.go | 139 +++++++++++++++++++++++++ pkg/storage/unified/sql/server.go | 83 +++++++++------ pkg/storage/unified/sql/service.go | 116 +++++++++++++++------ pkg/util/scheduler/queue.go | 19 +++- pkg/util/scheduler/queue_test.go | 4 + pkg/util/scheduler/scheduler_test.go | 12 ++- 12 files changed, 397 insertions(+), 73 deletions(-) diff --git a/pkg/server/distributor_test.go b/pkg/server/distributor_test.go index 6468d4f0f3e..b84c002b756 100644 --- a/pkg/server/distributor_test.go +++ b/pkg/server/distributor_test.go @@ -352,7 +352,18 @@ func createBaselineServer(t *testing.T, dbType, dbConnStr string, testNamespaces require.NoError(t, err) searchOpts, err := search.NewSearchOptions(features, cfg, tracer, docBuilders, nil) require.NoError(t, err) - server, err := sql.NewResourceServer(nil, cfg, tracer, nil, nil, searchOpts, nil, nil, features) + server, err := sql.NewResourceServer(sql.ServerOptions{ + DB: nil, + Cfg: cfg, + Tracer: tracer, + Reg: nil, + AccessClient: nil, + SearchOptions: searchOpts, + StorageMetrics: nil, + IndexMetrics: nil, + Features: features, + QOSQueue: nil, + }) require.NoError(t, err) testUserA := &identity.StaticRequester{ diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 3c141d2e18d..5def1b5c979 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -53,7 +53,16 @@ func NewModule(opts Options, return s, nil } -func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, reg prometheus.Registerer, promGatherer prometheus.Gatherer, license licensing.Licensing) (*ModuleServer, error) { +func newModuleServer(opts Options, + apiOpts api.ServerOptions, + features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + storageMetrics *resource.StorageMetrics, + indexMetrics *resource.BleveIndexMetrics, + reg prometheus.Registerer, + promGatherer prometheus.Gatherer, + license licensing.Licensing, +) (*ModuleServer, error) { rootCtx, shutdownFn := context.WithCancel(context.Background()) s := &ModuleServer{ diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c43d27664be..f14dbc03fc7 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -567,6 +567,9 @@ type Cfg struct { IndexRebuildInterval time.Duration IndexCacheTTL time.Duration EnableSharding bool + QOSEnabled bool + QOSNumberWorker int + QOSMaxSizePerTenant int MemberlistBindAddr string MemberlistAdvertiseAddr string MemberlistAdvertisePort int diff --git a/pkg/setting/setting_unified_storage.go b/pkg/setting/setting_unified_storage.go index 2484d5c1a33..f0881cf95db 100644 --- a/pkg/setting/setting_unified_storage.go +++ b/pkg/setting/setting_unified_storage.go @@ -49,13 +49,16 @@ func (cfg *Cfg) setUnifiedStorageConfig() { } cfg.UnifiedStorage = storageConfig - // Set indexer config for unified storaae + // Set indexer config for unified storage section := cfg.Raw.Section("unified_storage") cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0) cfg.IndexPath = section.Key("index_path").String() cfg.IndexWorkers = section.Key("index_workers").MustInt(10) cfg.IndexMaxBatchSize = section.Key("index_max_batch_size").MustInt(100) cfg.EnableSharding = section.Key("enable_sharding").MustBool(false) + cfg.QOSEnabled = section.Key("qos_enabled").MustBool(false) + cfg.QOSNumberWorker = section.Key("qos_num_worker").MustInt(16) + cfg.QOSMaxSizePerTenant = section.Key("qos_max_size_per_tenant").MustInt(1000) cfg.MemberlistBindAddr = section.Key("memberlist_bind_addr").String() cfg.MemberlistAdvertiseAddr = section.Key("memberlist_advertise_addr").String() cfg.MemberlistAdvertisePort = section.Key("memberlist_advertise_port").MustInt(7946) diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 1d1855d5eee..53942a903bf 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/dskit/flagext" "github.com/grafana/dskit/grpcclient" "github.com/grafana/dskit/middleware" + "github.com/grafana/dskit/services" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" @@ -31,6 +32,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/util/scheduler" ) type Options struct { @@ -49,7 +51,10 @@ type clientMetrics struct { } // This adds a UnifiedStorage client into the wire dependency tree -func ProvideUnifiedStorageClient(opts *Options, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (resource.ResourceClient, error) { +func ProvideUnifiedStorageClient(opts *Options, + storageMetrics *resource.StorageMetrics, + indexMetrics *resource.BleveIndexMetrics, +) (resource.ResourceClient, error) { // See: apiserver.applyAPIServerConfig(cfg, features, o) apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver") client, err := newClient(options.StorageOptions{ @@ -83,6 +88,7 @@ func newClient(opts options.StorageOptions, indexMetrics *resource.BleveIndexMetrics, ) (resource.ResourceClient, error) { ctx := context.Background() + switch opts.StorageType { case options.StorageTypeFile: if opts.DataPath == "" { @@ -146,13 +152,50 @@ func newClient(opts options.StorageOptions, } return client, nil - // Use the local SQL default: searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, indexMetrics) if err != nil { return nil, err } - server, err := sql.NewResourceServer(db, cfg, tracer, reg, authzc, searchOptions, storageMetrics, indexMetrics, features) + + serverOptions := sql.ServerOptions{ + DB: db, + Cfg: cfg, + Tracer: tracer, + Reg: reg, + AccessClient: authzc, + SearchOptions: searchOptions, + StorageMetrics: storageMetrics, + IndexMetrics: indexMetrics, + Features: features, + } + + if cfg.QOSEnabled { + qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg) + queue := scheduler.NewQueue(&scheduler.QueueOptions{ + MaxSizePerTenant: cfg.QOSMaxSizePerTenant, + Registerer: qosReg, + Logger: cfg.Logger, + }) + if err := services.StartAndAwaitRunning(ctx, queue); err != nil { + return nil, fmt.Errorf("failed to start queue: %w", err) + } + scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{ + NumWorkers: cfg.QOSNumberWorker, + Logger: cfg.Logger, + }) + if err != nil { + return nil, fmt.Errorf("failed to create scheduler: %w", err) + } + + err = services.StartAndAwaitRunning(ctx, scheduler) + if err != nil { + return nil, fmt.Errorf("failed to start scheduler: %w", err) + } + serverOptions.QOSQueue = queue + } + + server, err := sql.NewResourceServer(serverOptions) if err != nil { return nil, err } diff --git a/pkg/storage/unified/resource/errors.go b/pkg/storage/unified/resource/errors.go index fde29c71805..903f6e4cff9 100644 --- a/pkg/storage/unified/resource/errors.go +++ b/pkg/storage/unified/resource/errors.go @@ -12,6 +12,7 @@ import ( grpcstatus "google.golang.org/grpc/status" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/scheduler" ) // Package-level errors. @@ -50,6 +51,14 @@ func NewNotFoundError(key *resourcepb.ResourceKey) *resourcepb.ErrorResult { } } +func NewTooManyRequestsError(msg string) *resourcepb.ErrorResult { + return &resourcepb.ErrorResult{ + Message: msg, + Code: http.StatusTooManyRequests, + Reason: string(metav1.StatusReasonTooManyRequests), + } +} + // Convert golang errors to status result errors that can be returned to a client func AsErrorResult(err error) *resourcepb.ErrorResult { if err == nil { @@ -125,3 +134,10 @@ func GetError(res *resourcepb.ErrorResult) error { } return status } + +func HandleQueueError[T any](err error, makeResp func(*resourcepb.ErrorResult) *T) (*T, error) { + if errors.Is(err, scheduler.ErrTenantQueueFull) { + return makeResp(NewTooManyRequestsError("tenant queue is full, please try again later")), nil + } + return makeResp(AsErrorResult(err)), nil +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 6c07ecb401e..6e74bec40cd 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -19,9 +19,20 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" claims "github.com/grafana/authlib/types" + "github.com/grafana/dskit/backoff" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/scheduler" +) + +const ( + // DefaultMaxBackoff is the default maximum backoff duration for enqueue operations. + DefaultMaxBackoff = 1 * time.Second + // DefaultMinBackoff is the default minimum backoff duration for enqueue operations. + DefaultMinBackoff = 100 * time.Millisecond + // DefaultMaxRetries is the default maximum number of retries for enqueue operations. + DefaultMaxRetries = 3 ) // ResourceServer implements all gRPC services @@ -134,6 +145,10 @@ type BlobSupport interface { // TODO? List+Delete? This is for admin access } +type QOSEnqueuer interface { + Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error +} + type BlobConfig struct { // The CDK configuration URL URL string @@ -203,7 +218,11 @@ type ResourceServerOptions struct { IndexMetrics *BleveIndexMetrics + // MaxPageSizeBytes is the maximum size of a page in bytes. MaxPageSizeBytes int + + // QOSQueue is the quality of service queue used to enqueue + QOSQueue QOSEnqueuer } func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { @@ -222,6 +241,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } + if opts.Now == nil { opts.Now = func() int64 { return time.Now().UnixMilli() @@ -233,6 +253,10 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { opts.MaxPageSizeBytes = 1024 * 1024 * 2 } + if opts.QOSQueue == nil { + opts.QOSQueue = scheduler.NewNoopQueue() + } + // Initialize the blob storage blobstore := opts.Blob.Backend if blobstore == nil { @@ -275,6 +299,8 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { storageMetrics: opts.storageMetrics, indexMetrics: opts.IndexMetrics, maxPageSizeBytes: opts.MaxPageSizeBytes, + reg: opts.Reg, + queue: opts.QOSQueue, } if opts.Search.Resources != nil { @@ -321,6 +347,8 @@ type server struct { initErr error maxPageSizeBytes int + reg prometheus.Registerer + queue QOSEnqueuer } // Init implements ResourceServer. @@ -570,6 +598,25 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re return rsp, nil } + var ( + res *resourcepb.CreateResponse + err error + ) + runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) { + res, err = s.create(ctx, user, req) + }) + if runErr != nil { + return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.CreateResponse { + return &resourcepb.CreateResponse{Error: e} + }) + } + + return res, err +} + +func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) { + rsp := &resourcepb.CreateResponse{} + event, e := s.newEvent(ctx, user, req.Key, req.Value, nil) if e != nil { rsp.Error = e @@ -605,6 +652,24 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re return rsp, nil } + var ( + res *resourcepb.UpdateResponse + err error + ) + runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) { + res, err = s.update(ctx, user, req) + }) + if runErr != nil { + return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.UpdateResponse { + return &resourcepb.UpdateResponse{Error: e} + }) + } + + return res, err +} + +func (s *server) update(ctx context.Context, user claims.AuthInfo, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) { + rsp := &resourcepb.UpdateResponse{} latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{ Key: req.Key, }) @@ -654,6 +719,25 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re return rsp, nil } + var ( + res *resourcepb.DeleteResponse + err error + ) + + runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) { + res, err = s.delete(ctx, user, req) + }) + if runErr != nil { + return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.DeleteResponse { + return &resourcepb.DeleteResponse{Error: e} + }) + } + + return res, err +} + +func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) { + rsp := &resourcepb.DeleteResponse{} latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{ Key: req.Key, }) @@ -744,6 +828,23 @@ func (s *server) Read(ctx context.Context, req *resourcepb.ReadRequest) (*resour return &resourcepb.ReadResponse{Error: NewBadRequestError("missing resource")}, nil } + var ( + res *resourcepb.ReadResponse + err error + ) + runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) { + res, err = s.read(ctx, user, req) + }) + if runErr != nil { + return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.ReadResponse { + return &resourcepb.ReadResponse{Error: e} + }) + } + + return res, err +} + +func (s *server) read(ctx context.Context, user claims.AuthInfo, req *resourcepb.ReadRequest) (*resourcepb.ReadResponse, error) { rsp := s.backend.ReadResource(ctx, req) if rsp.Error != nil && rsp.Error.Code == http.StatusNotFound { return &resourcepb.ReadResponse{Error: rsp.Error}, nil @@ -1237,3 +1338,41 @@ func (s *server) GetBlob(ctx context.Context, req *resourcepb.GetBlobRequest) (* } return rsp, nil } + +func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error { + boff := backoff.New(ctx, backoff.Config{ + MinBackoff: DefaultMinBackoff, + MaxBackoff: DefaultMaxBackoff, + MaxRetries: DefaultMaxRetries, + }) + + var ( + wg sync.WaitGroup + err error + ) + wg.Add(1) + wrapped := func(ctx context.Context) { + runnable(ctx) + wg.Done() + } + for boff.Ongoing() { + err = s.queue.Enqueue(ctx, tenantID, wrapped) + if err == nil { + break + } + s.log.Warn("failed to enqueue runnable, retrying", + "maxRetries", DefaultMaxRetries, + "tenantID", tenantID, + "error", err) + boff.Wait() + } + if err != nil { + s.log.Error("failed to enqueue runnable", + "maxRetries", DefaultMaxRetries, + "tenantID", tenantID, + "error", err) + return fmt.Errorf("failed to enqueue runnable for tenant %s: %w", tenantID, err) + } + wg.Wait() + return nil +} diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 3d6b1d5f248..93f55900fd6 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -1,6 +1,7 @@ package sql import ( + "context" "os" "strings" @@ -8,6 +9,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/authlib/types" + "github.com/grafana/dskit/services" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -17,70 +19,85 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" ) +type QOSEnqueueDequeuer interface { + services.Service + Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error + Dequeue(ctx context.Context) (func(ctx context.Context), error) +} + +// ServerOptions contains the options for creating a new ResourceServer +type ServerOptions struct { + DB infraDB.DB + Cfg *setting.Cfg + Tracer trace.Tracer + Reg prometheus.Registerer + AccessClient types.AccessClient + SearchOptions resource.SearchOptions + StorageMetrics *resource.StorageMetrics + IndexMetrics *resource.BleveIndexMetrics + Features featuremgmt.FeatureToggles + QOSQueue QOSEnqueueDequeuer +} + // Creates a new ResourceServer -func NewResourceServer(db infraDB.DB, cfg *setting.Cfg, - tracer trace.Tracer, reg prometheus.Registerer, ac types.AccessClient, - searchOptions resource.SearchOptions, storageMetrics *resource.StorageMetrics, - indexMetrics *resource.BleveIndexMetrics, features featuremgmt.FeatureToggles) (resource.ResourceServer, error) { - apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver") - opts := resource.ResourceServerOptions{ - Tracer: tracer, +func NewResourceServer( + opts ServerOptions, +) (resource.ResourceServer, error) { + apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver") + serverOptions := resource.ResourceServerOptions{ + Tracer: opts.Tracer, Blob: resource.BlobConfig{ URL: apiserverCfg.Key("blob_url").MustString(""), }, - Reg: reg, + Reg: opts.Reg, } - if ac != nil { - opts.AccessClient = resource.NewAuthzLimitedClient(ac, resource.AuthzOptions{Tracer: tracer, Registry: reg}) + if opts.AccessClient != nil { + serverOptions.AccessClient = resource.NewAuthzLimitedClient(opts.AccessClient, resource.AuthzOptions{Tracer: opts.Tracer, Registry: opts.Reg}) } // Support local file blob - if strings.HasPrefix(opts.Blob.URL, "./data/") { - dir := strings.Replace(opts.Blob.URL, "./data", cfg.DataPath, 1) + if strings.HasPrefix(serverOptions.Blob.URL, "./data/") { + dir := strings.Replace(serverOptions.Blob.URL, "./data", opts.Cfg.DataPath, 1) err := os.MkdirAll(dir, 0700) if err != nil { return nil, err } - opts.Blob.URL = "file:///" + dir + serverOptions.Blob.URL = "file:///" + dir } // This is mostly for testing, being able to influence when we paginate // based on the page size during tests. - unifiedStorageCfg := cfg.SectionWithEnvOverrides("unified_storage") + unifiedStorageCfg := opts.Cfg.SectionWithEnvOverrides("unified_storage") maxPageSizeBytes := unifiedStorageCfg.Key("max_page_size_bytes") - opts.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0) + serverOptions.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0) - eDB, err := dbimpl.ProvideResourceDB(db, cfg, tracer) + eDB, err := dbimpl.ProvideResourceDB(opts.DB, opts.Cfg, opts.Tracer) if err != nil { return nil, err } - isHA := isHighAvailabilityEnabled(cfg.SectionWithEnvOverrides("database"), - cfg.SectionWithEnvOverrides("resource_api")) - withPruner := features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner) + isHA := isHighAvailabilityEnabled(opts.Cfg.SectionWithEnvOverrides("database"), + opts.Cfg.SectionWithEnvOverrides("resource_api")) + withPruner := opts.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner) store, err := NewBackend(BackendOptions{ DBProvider: eDB, - Tracer: tracer, - Reg: reg, + Tracer: opts.Tracer, + Reg: opts.Reg, IsHA: isHA, withPruner: withPruner, - storageMetrics: storageMetrics, + storageMetrics: opts.StorageMetrics, }) if err != nil { return nil, err } - opts.Backend = store - opts.Diagnostics = store - opts.Lifecycle = store - opts.Search = searchOptions - opts.IndexMetrics = indexMetrics + serverOptions.Backend = store + serverOptions.Diagnostics = store + serverOptions.Lifecycle = store + serverOptions.Search = opts.SearchOptions + serverOptions.IndexMetrics = opts.IndexMetrics + serverOptions.QOSQueue = opts.QOSQueue - rs, err := resource.NewResourceServer(opts) - if err != nil { - return nil, err - } - - return rs, nil + return resource.NewResourceServer(serverOptions) } // isHighAvailabilityEnabled determines if high availability mode should diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 5309e23f729..c8a2ef8260d 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource/grpc" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/util/scheduler" ) var ( @@ -50,6 +51,11 @@ type UnifiedStorageGrpcService interface { type service struct { *services.BasicService + // Subservices manager + subservices *services.Manager + subservicesWatcher *services.FailureWatcher + hasSubservices bool + cfg *setting.Cfg features featuremgmt.FeatureToggles db infraDB.DB @@ -71,6 +77,9 @@ type service struct { storageRing *ring.Ring lifecycler *ring.BasicLifecycler + + queue QOSEnqueueDequeuer + scheduler *scheduler.Scheduler } func ProvideUnifiedStorageGrpcService( @@ -85,6 +94,7 @@ func ProvideUnifiedStorageGrpcService( storageRing *ring.Ring, memberlistKVConfig kv.Config, ) (UnifiedStorageGrpcService, error) { + var err error tracer := otel.Tracer("unified-storage") // FIXME: This is a temporary solution while we are migrating to the new authn interceptor @@ -95,20 +105,22 @@ func ProvideUnifiedStorageGrpcService( }) s := &service{ - cfg: cfg, - features: features, - stopCh: make(chan struct{}), - authenticator: authn, - tracing: tracer, - db: db, - log: log, - reg: reg, - docBuilders: docBuilders, - storageMetrics: storageMetrics, - indexMetrics: indexMetrics, - storageRing: storageRing, + cfg: cfg, + features: features, + stopCh: make(chan struct{}), + authenticator: authn, + tracing: tracer, + db: db, + log: log, + reg: reg, + docBuilders: docBuilders, + storageMetrics: storageMetrics, + indexMetrics: indexMetrics, + storageRing: storageRing, + subservicesWatcher: services.NewFailureWatcher(), } + subservices := []services.Service{} if cfg.EnableSharding { ringStore, err := kv.NewClient( memberlistKVConfig, @@ -143,15 +155,50 @@ func ProvideUnifiedStorageGrpcService( if err != nil { return nil, fmt.Errorf("failed to initialize storage-ring lifecycler: %s", err) } + subservices = append(subservices, s.lifecycler) + } + + if cfg.QOSEnabled { + qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg) + queue := scheduler.NewQueue(&scheduler.QueueOptions{ + MaxSizePerTenant: cfg.QOSMaxSizePerTenant, + Registerer: qosReg, + }) + scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{ + NumWorkers: cfg.QOSNumberWorker, + Logger: log, + }) + if err != nil { + return nil, fmt.Errorf("failed to create qos scheduler: %s", err) + } + + s.queue = queue + s.scheduler = scheduler + subservices = append(subservices, s.queue, s.scheduler) + } + + if len(subservices) > 0 { + s.hasSubservices = true + s.subservices, err = services.NewManager(subservices...) + if err != nil { + return nil, fmt.Errorf("failed to create subservices manager: %w", err) + } } // This will be used when running as a dskit service - s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName(modules.StorageServer) + s.BasicService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(modules.StorageServer) return s, nil } -func (s *service) start(ctx context.Context) error { +func (s *service) starting(ctx context.Context) error { + if s.hasSubservices { + s.subservicesWatcher.WatchManager(s.subservices) + if err := services.StartManagerAndAwaitHealthy(ctx, s.subservices); err != nil { + return fmt.Errorf("failed to start subservices: %w", err) + } + } + authzClient, err := authz.ProvideStandaloneAuthZClient(s.cfg, s.features, s.tracing) if err != nil { return err @@ -162,7 +209,19 @@ func (s *service) start(ctx context.Context) error { return err } - server, err := NewResourceServer(s.db, s.cfg, s.tracing, s.reg, authzClient, searchOptions, s.storageMetrics, s.indexMetrics, s.features) + serverOptions := ServerOptions{ + DB: s.db, + Cfg: s.cfg, + Tracer: s.tracing, + Reg: s.reg, + AccessClient: authzClient, + SearchOptions: searchOptions, + StorageMetrics: s.storageMetrics, + IndexMetrics: s.indexMetrics, + Features: s.features, + QOSQueue: s.queue, + } + server, err := NewResourceServer(serverOptions) if err != nil { return err } @@ -192,11 +251,6 @@ func (s *service) start(ctx context.Context) error { } if s.cfg.EnableSharding { - err = s.lifecycler.StartAsync(ctx) - if err != nil { - return fmt.Errorf("failed to start the lifecycler: %s", err) - } - s.log.Info("waiting until resource server is JOINING in the ring") lfcCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() @@ -231,15 +285,27 @@ func (s *service) GetAddress() string { func (s *service) running(ctx context.Context) error { select { case err := <-s.stoppedCh: - if err != nil { + if err != nil && !errors.Is(err, context.Canceled) { return err } + case err := <-s.subservicesWatcher.Chan(): + return fmt.Errorf("subservice failure: %w", err) case <-ctx.Done(): close(s.stopCh) } return nil } +func (s *service) stopping(_ error) error { + if s.hasSubservices { + err := services.StopManagerAndAwaitStopped(context.Background(), s.subservices) + if err != nil { + return fmt.Errorf("failed to stop subservices: %w", err) + } + } + return nil +} + type authenticatorWithFallback struct { authenticator func(ctx context.Context) (context.Context, error) fallback func(ctx context.Context) (context.Context, error) @@ -309,14 +375,6 @@ func NewAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, t } } -func (s *service) stopping(err error) error { - if err != nil && !errors.Is(err, context.Canceled) { - s.log.Error("stopping unified storage grpc service", "error", err) - return err - } - return nil -} - func toLifecyclerConfig(cfg *setting.Cfg, logger log.Logger) (ring.BasicLifecyclerConfig, error) { instanceAddr, err := ring.GetInstanceAddr(cfg.MemberlistBindAddr, netutil.PrivateNetworkInterfacesWithFallback([]string{"eth0", "en0"}, logger), logger, true) if err != nil { diff --git a/pkg/util/scheduler/queue.go b/pkg/util/scheduler/queue.go index eceb0bcf38a..b065d92804f 100644 --- a/pkg/util/scheduler/queue.go +++ b/pkg/util/scheduler/queue.go @@ -9,6 +9,8 @@ import ( "github.com/grafana/dskit/services" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/infra/log" ) const ( @@ -82,6 +84,8 @@ func NewNoopQueue() *NoopQueue { type Queue struct { services.Service + logger log.Logger + enqueueChan chan enqueueRequest dequeueChan chan dequeueRequest lenChan chan lenRequest @@ -108,6 +112,7 @@ type Queue struct { type QueueOptions struct { MaxSizePerTenant int Registerer prometheus.Registerer + Logger log.Logger } // NewQueue creates a new Queue and starts its dispatcher goroutine. @@ -116,7 +121,13 @@ func NewQueue(opts *QueueOptions) *Queue { opts.MaxSizePerTenant = DefaultMaxSizePerTenant } + if opts.Logger == nil { + opts.Logger = log.NewNopLogger() + } + q := &Queue{ + logger: opts.Logger, + enqueueChan: make(chan enqueueRequest), dequeueChan: make(chan dequeueRequest), lenChan: make(chan lenRequest), @@ -226,6 +237,8 @@ func (q *Queue) handleLenRequest(req lenRequest) { func (q *Queue) dispatcherLoop(ctx context.Context) error { defer close(q.dispatcherStoppedChan) + q.logger.Info("queue running", "maxSizePerTenant", q.maxSizePerTenant) + for { q.scheduleRoundRobin() @@ -275,7 +288,6 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx select { case q.enqueueChan <- req: err = <-respChan - q.enqueueDuration.Observe(time.Since(start).Seconds()) case <-q.dispatcherStoppedChan: q.discardedRequests.WithLabelValues(tenantID, "dispatcher_stopped").Inc() err = ErrQueueClosed @@ -283,6 +295,7 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx q.discardedRequests.WithLabelValues(tenantID, "context_canceled").Inc() err = ctx.Err() } + q.enqueueDuration.Observe(time.Since(start).Seconds()) return err } @@ -352,6 +365,8 @@ func (q *Queue) ActiveTenantsLen() int { } func (q *Queue) stopping(_ error) error { + q.logger.Info("queue stopping") + q.queueLength.Reset() q.discardedRequests.Reset() for _, tq := range q.tenantQueues { @@ -359,5 +374,7 @@ func (q *Queue) stopping(_ error) error { } q.activeTenants.Init() q.pendingDequeueRequests.Init() + + q.logger.Info("queue stopped") return nil } diff --git a/pkg/util/scheduler/queue_test.go b/pkg/util/scheduler/queue_test.go index e703501747e..1f56603bf3c 100644 --- a/pkg/util/scheduler/queue_test.go +++ b/pkg/util/scheduler/queue_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/grafana/dskit/services" + "github.com/grafana/grafana/pkg/infra/log" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" ) @@ -25,6 +26,9 @@ func QueueOptionsWithDefaults(opts *QueueOptions) *QueueOptions { if opts.Registerer == nil { opts.Registerer = prometheus.NewRegistry() } + if opts.Logger == nil { + opts.Logger = log.New("qos.test") + } return opts } diff --git a/pkg/util/scheduler/scheduler_test.go b/pkg/util/scheduler/scheduler_test.go index f0fa02d6e4c..9d185df39c7 100644 --- a/pkg/util/scheduler/scheduler_test.go +++ b/pkg/util/scheduler/scheduler_test.go @@ -2,6 +2,7 @@ package scheduler import ( "context" + "fmt" "sync" "sync/atomic" "testing" @@ -130,16 +131,16 @@ func TestScheduler(t *testing.T) { t.Run("ProcessItems", func(t *testing.T) { t.Parallel() - q := NewQueue(QueueOptionsWithDefaults(nil)) + q := NewQueue(QueueOptionsWithDefaults(&QueueOptions{MaxSizePerTenant: 1000})) require.NoError(t, services.StartAndAwaitRunning(context.Background(), q)) - const itemCount = 10 + const itemCount = 1000 var processed sync.Map var wg sync.WaitGroup wg.Add(itemCount) scheduler, err := NewScheduler(q, &Config{ - NumWorkers: 2, + NumWorkers: 10, MaxBackoff: 100 * time.Millisecond, Logger: log.New("qos.test"), }) @@ -148,8 +149,11 @@ func TestScheduler(t *testing.T) { for i := 0; i < itemCount; i++ { itemID := i - require.NoError(t, q.Enqueue(context.Background(), "tenant-1", func(_ context.Context) { + tenantIndex := itemID % 10 + tenantID := fmt.Sprintf("tenant-%d", tenantIndex) + require.NoError(t, q.Enqueue(context.Background(), tenantID, func(_ context.Context) { processed.Store(itemID, true) + time.Sleep(10 * time.Millisecond) wg.Done() })) } From 855f133d1eeb587759161b766bba2c1a9afb967f Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 1 Jul 2025 11:55:26 +0200 Subject: [PATCH 13/23] New Logs Panel: fix unwrapped logs display (#107391) * LogLine: remove new lines in unwrapped mode * Displayed fields: fix displayed fields when the body is included * LogLine: prevent overflows from extremely long lines with incorrect width measurement * Update tests * virtualization: test calculateFieldDimensions * getGridTemplateColumns: consider displayed fields to set the grid sizes * LogList: hide overflowing fields in unwrapped mode * processing: extract regex * LogLine: improve hover and selected state * virtualization: strip ansi color codes for measurement * Update tests * LogLine: improve log line pre-resize state * Revert "LogLine: improve log line pre-resize state" This reverts commit a6b4ddded5226cbbb4db1e64823644571e23dfc6. * LogLine: improve hover/active color --- .../logs/components/__mocks__/logRow.ts | 1 + .../logs/components/panel/LogLine.test.tsx | 92 ++++++++++++++++++- .../logs/components/panel/LogLine.tsx | 21 ++++- .../logs/components/panel/LogList.tsx | 15 ++- .../logs/components/panel/processing.test.ts | 49 +++++++++- .../logs/components/panel/processing.ts | 46 +++++++--- .../components/panel/virtualization.test.ts | 55 ++++++++--- .../logs/components/panel/virtualization.ts | 10 +- 8 files changed, 239 insertions(+), 50 deletions(-) diff --git a/public/app/features/logs/components/__mocks__/logRow.ts b/public/app/features/logs/components/__mocks__/logRow.ts index 09aa16998da..9efe2ee97eb 100644 --- a/public/app/features/logs/components/__mocks__/logRow.ts +++ b/public/app/features/logs/components/__mocks__/logRow.ts @@ -46,6 +46,7 @@ export const createLogLine = ( order: LogsSortOrder.Descending, timeZone: 'browser', virtualization: undefined, + wrapLogMessage: true, } ): LogListModel => { const logs = preProcessLogs([createLogRow(overrides)], processOptions); diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx index 6a2152b2e9f..bdbbf74b2f2 100644 --- a/public/app/features/logs/components/panel/LogLine.test.tsx +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -6,7 +6,7 @@ import { CoreApp, createTheme, LogsDedupStrategy, LogsSortOrder } from '@grafana import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { createLogLine } from '../__mocks__/logRow'; -import { getStyles, LogLine, Props } from './LogLine'; +import { getGridTemplateColumns, getStyles, LogLine, Props } from './LogLine'; import { LogListFontSize } from './LogList'; import { LogListContextProvider } from './LogListContext'; import { LogListSearchContext } from './LogListSearchContext'; @@ -36,7 +36,7 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { beforeEach(() => { log = createLogLine( { labels: { place: 'luna' }, entry: `log message 1` }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } ); contextProps.logs = [log]; contextProps.fontSize = fontSize; @@ -226,7 +226,7 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { jest.spyOn(virtualization, 'getTruncationLength').mockReturnValue(5); log = createLogLine( { labels: { place: 'luna' }, entry: `log message 1` }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } ); }); @@ -425,3 +425,89 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { }); }); }); + +describe('getGridTemplateColumns', () => { + test('Gets the template columns for the default visualization mode', () => { + expect( + getGridTemplateColumns( + [ + { + field: 'timestamp', + width: 23, + }, + { + field: 'level', + width: 4, + }, + ], + [] + ) + ).toBe('23px 4px 1fr'); + }); + + test('Gets the template columns when displayed fields are used', () => { + expect( + getGridTemplateColumns( + [ + { + field: 'timestamp', + width: 23, + }, + { + field: 'level', + width: 4, + }, + ], + ['field'] + ) + ).toBe('23px 4px'); + }); + + test('Gets the template columns when displayed fields are used', () => { + expect( + getGridTemplateColumns( + [ + { + field: 'timestamp', + width: 23, + }, + { + field: 'level', + width: 4, + }, + { + field: 'field', + width: 4, + }, + ], + ['field'] + ) + ).toBe('23px 4px 4px'); + }); + + test('Gets the template columns when displayed fields are used', () => { + expect( + getGridTemplateColumns( + [ + { + field: 'timestamp', + width: 23, + }, + { + field: 'level', + width: 4, + }, + { + field: 'field', + width: 4, + }, + { + field: LOG_LINE_BODY_FIELD_NAME, + width: 20, + }, + ], + ['field'] + ) + ).toBe('23px 4px 4px 20px'); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index a8d87abc92f..3e7ac11bc96 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -192,7 +192,7 @@ const LogLineComponent = memo( {/* A button element could be used but in Safari it prevents text selection. Fallback available for a11y in LogLineMenu */} {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
; }; -export function getGridTemplateColumns(dimensions: LogFieldDimension[]) { +export function getGridTemplateColumns(dimensions: LogFieldDimension[], displayedFields: string[]) { const columns = dimensions.map((dimension) => dimension.width).join('px '); - return `${columns}px 1fr`; + const logLineWidth = displayedFields.length > 0 ? '' : ' 1fr'; + return `${columns}px${logLineWidth}`; } export type LogLineStyles = ReturnType; @@ -368,6 +369,8 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali parsedField: theme.colors.text.primary, }; + const hoverColor = tinycolor(theme.colors.background.canvas).darken(4).toRgbString(); + return { logLine: css({ color: tinycolor(theme.colors.text.secondary).setAlpha(0.75).toRgbString(), @@ -379,7 +382,7 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali lineHeight: theme.typography.body.lineHeight, wordBreak: 'break-all', '&:hover': { - background: theme.isDark ? `hsla(0, 0%, 0%, 0.3)` : `hsla(0, 0%, 0%, 0.1)`, + background: hoverColor, }, '&.infinite-scroll': { '&::before': { @@ -440,7 +443,7 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali lineHeight: theme.typography.bodySmall.lineHeight, }), detailsDisplayed: css({ - background: theme.isDark ? `hsla(0, 0%, 0%, 0.5)` : `hsla(0, 0%, 0%, 0.1)`, + background: hoverColor, }), pinnedLogLine: css({ backgroundColor: tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString(), @@ -525,6 +528,9 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER), whiteSpace: 'pre', paddingBottom: theme.spacing(0.75), + '& .field': { + overflow: 'hidden', + }, }), wrappedLogLine: css({ alignSelf: 'flex-start', @@ -537,6 +543,11 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali marginRight: 0, }, }), + fieldsWrapper: css({ + '&:hover': { + background: hoverColor, + }, + }), collapsedLogLine: css({ overflow: 'hidden', }), diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 58a82266bcc..acdbc0dc593 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -248,7 +248,7 @@ const LogListComponent = ({ () => (wrapLogMessage ? [] : virtualization.calculateFieldDimensions(processedLogs, displayedFields)), [displayedFields, processedLogs, virtualization, wrapLogMessage] ); - const styles = useStyles2(getStyles, dimensions, { showTime }); + const styles = useStyles2(getStyles, dimensions, displayedFields, { showTime }); const widthContainer = wrapperRef.current ?? containerElement; const { closePopoverMenu, @@ -283,13 +283,13 @@ const LogListComponent = ({ setProcessedLogs( preProcessLogs( logs, - { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone, virtualization }, + { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone, virtualization, wrapLogMessage }, grammar ) ); virtualization.resetLogLineSizes(); listRef.current?.resetAfterIndex(0); - }, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone, virtualization]); + }, [forceEscape, getFieldLinks, grammar, loading, logs, sortOrder, timeZone, virtualization, wrapLogMessage]); useEffect(() => { listRef.current?.resetAfterIndex(0); @@ -469,13 +469,18 @@ const LogListComponent = ({ ); }; -function getStyles(theme: GrafanaTheme2, dimensions: LogFieldDimension[], { showTime }: { showTime: boolean }) { +function getStyles( + theme: GrafanaTheme2, + dimensions: LogFieldDimension[], + displayedFields: string[], + { showTime }: { showTime: boolean } +) { const columns = showTime ? dimensions : dimensions.filter((_, index) => index > 0); return { logList: css({ '& .unwrapped-log-line': { display: 'grid', - gridTemplateColumns: getGridTemplateColumns(columns), + gridTemplateColumns: getGridTemplateColumns(columns, displayedFields), }, }), logListContainer: css({ diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 72461861e8b..129d97451da 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -79,6 +79,7 @@ describe('preProcessLogs', () => { getFieldLinks, order: LogsSortOrder.Descending, timeZone: 'browser', + wrapLogMessage: true, }); }); @@ -92,9 +93,53 @@ describe('preProcessLogs', () => { entry: `35.191.12.195 - accounts.google.com:test@grafana.com [18/Mar/2025:08:58:38 +0000] 200 "POST /grafana/api/ds/query?ds_type=prometheus&requestId=SQR461 HTTP/1.1" 59460 "https://test.example.com/?orgId=1" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "95.91.240.90, 34.107.247.24"`, logLevel: LogLevel.critical, }); - const logListModel = new LogListModel(logRowModel, { escape: false, timeZone: 'browser ' }); + const logListModel = new LogListModel(logRowModel, { escape: false, timeZone: 'browser ', wrapLogMessage: true }); expect(logListModel).toMatchObject(logRowModel); }); + + test('Unwrapped log lines strip new lines', () => { + const logListModel = createLogLine( + { labels: { place: `lu\nna` }, entry: `log\n message\n 1` }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: false, // unwrapped + } + ); + expect(logListModel.getDisplayedFieldValue('place')).toBe('luna'); + expect(logListModel.body).toBe('log message 1'); + }); + + test('Wrapped log lines do not modify new lines', () => { + const logListModel = createLogLine( + { labels: { place: `lu\nna` }, entry: `log\n message\n 1` }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + } + ); + expect(logListModel.getDisplayedFieldValue('place')).toBe(logListModel.labels['place']); + expect(logListModel.body).toBe(logListModel.raw); + }); + + test('Strips ansi colors for measurement', () => { + const logListModel = createLogLine( + { entry: `log \u001B[31mmessage\u001B[0m 1` }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, + } + ); + expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, false)).toBe( + `log \u001B[31mmessage\u001B[0m 1` + ); + expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, true)).toBe('log message 1'); + }); }); test('Orders logs', () => { @@ -176,7 +221,7 @@ describe('preProcessLogs', () => { entry = new Array(2 * virtualization.getTruncationLength(null)).fill('e').join(''); longLog = createLogLine( { entry, labels: { field: 'value' } }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } ); }); diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index f891f89b097..60e51ca91d4 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,3 +1,4 @@ +import ansicolor from 'ansicolor'; import Prism, { Grammar } from 'prismjs'; import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; @@ -11,6 +12,7 @@ import { generateLogGrammar, generateTextMatchGrammar } from './grammar'; import { LogLineVirtualization } from './virtualization'; const TRUNCATION_DEFAULT_LENGTH = 50000; +const NEWLINES_REGEX = /(\r\n|\n|\r)/g; export class LogListModel implements LogRowModel { collapsed: boolean | undefined = undefined; @@ -47,8 +49,12 @@ export class LogListModel implements LogRowModel { private _fields: FieldDef[] | undefined = undefined; private _getFieldLinks: GetFieldLinksFn | undefined = undefined; private _virtualization?: LogLineVirtualization; + private _wrapLogMessage: boolean; - constructor(log: LogRowModel, { escape, getFieldLinks, grammar, timeZone, virtualization }: PreProcessLogOptions) { + constructor( + log: LogRowModel, + { escape, getFieldLinks, grammar, timeZone, virtualization, wrapLogMessage }: PreProcessLogOptions + ) { // LogRowModel this.datasourceType = log.datasourceType; this.dataFrame = log.dataFrame; @@ -82,6 +88,7 @@ export class LogListModel implements LogRowModel { defaultWithMS: true, }); this._virtualization = virtualization; + this._wrapLogMessage = wrapLogMessage; let raw = log.raw; if (escape && log.hasUnescapedContent) { @@ -95,6 +102,9 @@ export class LogListModel implements LogRowModel { this._body = this.collapsed ? this.raw.substring(0, this._virtualization?.getTruncationLength(null) ?? TRUNCATION_DEFAULT_LENGTH) : this.raw; + if (!this._wrapLogMessage) { + this._body = this._body.replace(NEWLINES_REGEX, ''); + } } return this._body; } @@ -123,25 +133,31 @@ export class LogListModel implements LogRowModel { return checkLogsSampled(this); } - getDisplayedFieldValue(fieldName: string): string { + getDisplayedFieldValue(fieldName: string, stripAnsi = false): string { if (fieldName === LOG_LINE_BODY_FIELD_NAME) { - return this.body; + return stripAnsi ? ansicolor.strip(this.body) : this.body; } + let fieldValue = ''; if (this.labels[fieldName] != null) { - return this.labels[fieldName]; - } - const field = this.fields.find((field) => { - return field.keys[0] === fieldName; - }); + fieldValue = this.labels[fieldName]; + } else { + const field = this.fields.find((field) => { + return field.keys[0] === fieldName; + }); - return field ? field.values.toString() : ''; + fieldValue = field ? field.values.toString() : ''; + } + if (!this._wrapLogMessage) { + return fieldValue.replace(NEWLINES_REGEX, ''); + } + return fieldValue; } updateCollapsedState(displayedFields: string[], container: HTMLDivElement | null) { const lineLength = displayedFields.length > 0 - ? displayedFields.map((field) => this.getDisplayedFieldValue(field)).join('').length - : this.raw.length; + ? displayedFields.map((field) => this.getDisplayedFieldValue(field, true)).join('').length + : this.entry.length; const collapsed = lineLength >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH) ? true @@ -172,15 +188,18 @@ export interface PreProcessOptions { order: LogsSortOrder; timeZone: string; virtualization?: LogLineVirtualization; + wrapLogMessage: boolean; } export const preProcessLogs = ( logs: LogRowModel[], - { escape, getFieldLinks, order, timeZone, virtualization }: PreProcessOptions, + { escape, getFieldLinks, order, timeZone, virtualization, wrapLogMessage }: PreProcessOptions, grammar?: Grammar ): LogListModel[] => { const orderedLogs = sortLogRows(logs, order); - return orderedLogs.map((log) => preProcessLog(log, { escape, getFieldLinks, grammar, timeZone, virtualization })); + return orderedLogs.map((log) => + preProcessLog(log, { escape, getFieldLinks, grammar, timeZone, virtualization, wrapLogMessage }) + ); }; interface PreProcessLogOptions { @@ -189,6 +208,7 @@ interface PreProcessLogOptions { grammar?: Grammar; timeZone: string; virtualization?: LogLineVirtualization; + wrapLogMessage: boolean; } const preProcessLog = (log: LogRowModel, options: PreProcessLogOptions): LogListModel => { return new LogListModel(log, options); diff --git a/public/app/features/logs/components/panel/virtualization.test.ts b/public/app/features/logs/components/panel/virtualization.test.ts index b474332616c..ff1789ad50e 100644 --- a/public/app/features/logs/components/panel/virtualization.test.ts +++ b/public/app/features/logs/components/panel/virtualization.test.ts @@ -3,7 +3,7 @@ import { createTheme, LogsSortOrder } from '@grafana/data'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { createLogLine } from '../__mocks__/logRow'; -import { LogListModel } from './processing'; +import { LogListModel, PreProcessOptions } from './processing'; import { LogLineVirtualization, getLogLineSize, DisplayOptions } from './virtualization'; describe('Virtualization', () => { @@ -28,12 +28,16 @@ describe('Virtualization', () => { hasSampledLogs: false, }; + const preProcessOptions: PreProcessOptions = { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + virtualization, + wrapLogMessage: true, + }; + beforeEach(() => { - log = createLogLine( - { labels: { place: 'luna' }, entry: `log message 1` }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } - ); - //virtualization = new LogLineVirtualization(createTheme(), 'default'); + log = createLogLine({ labels: { place: 'luna' }, entry: `log message 1` }, preProcessOptions); container = document.createElement('div'); jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(CONTAINER_SIZE); LETTER_WIDTH = virtualization.measureTextWidth('e'); @@ -86,7 +90,7 @@ describe('Virtualization', () => { entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''), logLevel: undefined, }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true }, 0); @@ -96,7 +100,7 @@ describe('Virtualization', () => { test('Measures a multi-line log line with level, controls, and displayed time', () => { log = createLogLine( { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( @@ -118,7 +122,7 @@ describe('Virtualization', () => { entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''), logLevel: undefined, }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( @@ -136,7 +140,7 @@ describe('Virtualization', () => { test('Measures displayed fields in a log line with level, controls, and displayed time', () => { log = createLogLine( { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( @@ -154,7 +158,7 @@ describe('Virtualization', () => { test('Measures a multi-line log line with duplicates', () => { log = createLogLine( { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); log.duplicates = 1; @@ -173,7 +177,7 @@ describe('Virtualization', () => { test('Measures a multi-line log line with errors', () => { log = createLogLine( { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( @@ -191,7 +195,7 @@ describe('Virtualization', () => { test('Measures a multi-line sampled log line', () => { log = createLogLine( { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( @@ -214,6 +218,29 @@ describe('Virtualization', () => { }); }); + describe('calculateFieldDimensions', () => { + test('Measures displayed fields including the log line body', () => { + expect(virtualization.calculateFieldDimensions([log], ['place', LOG_LINE_BODY_FIELD_NAME])).toEqual([ + { + field: 'timestamp', + width: 23, + }, + { + field: 'level', + width: 4, + }, + { + field: 'place', + width: 4, + }, + { + field: '___LOG_LINE_BODY___', + width: 13, + }, + ]); + }); + }); + describe('With small font size', () => { const virtualization = new LogLineVirtualization(createTheme(), 'small'); @@ -232,7 +259,7 @@ describe('Virtualization', () => { entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join(''), logLevel: undefined, }, - { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization } + preProcessOptions ); const size = getLogLineSize( diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index fd4a0280fe6..2f9b6b6773a 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -2,8 +2,6 @@ import ansicolor from 'ansicolor'; import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; -import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; - import { LogListFontSize } from './LogList'; import { LogListModel } from './processing'; @@ -195,7 +193,7 @@ export class LogLineVirtualization { levelWidth = Math.round(width); } for (const field of displayedFields) { - width = this.measureTextWidth(logs[i].getDisplayedFieldValue(field)); + width = this.measureTextWidth(logs[i].getDisplayedFieldValue(field, true)); fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field]; } } @@ -210,10 +208,6 @@ export class LogLineVirtualization { }, ]; for (const field in fieldWidths) { - // Skip the log line when it's a displayed field - if (field === LOG_LINE_BODY_FIELD_NAME) { - continue; - } dimensions.push({ field, width: fieldWidths[field], @@ -294,7 +288,7 @@ export function getLogLineSize( textToMeasure += logs[index].displayLevel ?? ''; } for (const field of displayedFields) { - textToMeasure = logs[index].getDisplayedFieldValue(field) + textToMeasure; + textToMeasure = logs[index].getDisplayedFieldValue(field, true) + textToMeasure; } if (!displayedFields.length) { textToMeasure += ansicolor.strip(logs[index].body); From 406923f912f59252366c922038ebcb79c3d20cfd Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Tue, 1 Jul 2025 11:56:27 +0200 Subject: [PATCH 14/23] Chore: Commit wire_gen.go (#107410) * commit wire_gen.go * do not generate code in ci --- .github/workflows/backend-unit-tests.yml | 4 - .github/workflows/go-lint.yml | 1 - .github/workflows/pr-test-integration.yml | 6 - .gitignore | 4 +- pkg/server/wire_gen.go | 1461 +++++++++++++++++++++ 5 files changed, 1462 insertions(+), 14 deletions(-) create mode 100644 pkg/server/wire_gen.go diff --git a/.github/workflows/backend-unit-tests.yml b/.github/workflows/backend-unit-tests.yml index 07663eb8631..1c839432829 100644 --- a/.github/workflows/backend-unit-tests.yml +++ b/.github/workflows/backend-unit-tests.yml @@ -60,8 +60,6 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: go.mod - - name: Generate Go code - run: make gen-go - name: Run unit tests env: SHARD: ${{ matrix.shard }} @@ -109,8 +107,6 @@ jobs: - run: go install github.com/jstemmer/go-junit-report/v2@85bf4716ac1f025f2925510a9f5e9f5bb347c009 # Run code - - name: Generate Go code - run: make gen-go - name: Run unit tests env: SHARD: ${{ matrix.shard }} diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index cdc84874d01..78788f3570c 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -22,7 +22,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: make gen-go - name: golangci-lint uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd with: diff --git a/.github/workflows/pr-test-integration.yml b/.github/workflows/pr-test-integration.yml index 6fae22a591f..76f2068172c 100644 --- a/.github/workflows/pr-test-integration.yml +++ b/.github/workflows/pr-test-integration.yml @@ -41,8 +41,6 @@ jobs: with: go-version-file: go.mod cache: true - - name: Generate Go code - run: make gen-go - name: Run tests env: SHARD: ${{ matrix.shard }} @@ -89,8 +87,6 @@ jobs: cache: true - name: Setup MySQL devenv run: mysql -h 127.0.0.1 -P 3306 -u root -prootpass < devenv/docker/blocks/mysql_tests/setup.sql - - name: Generate Go code - run: make gen-go - name: Run tests env: SHARD: ${{ matrix.shard }} @@ -136,8 +132,6 @@ jobs: cache: true - name: Setup Postgres devenv run: psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql - - name: Generate Go code - run: make gen-go - name: Run tests env: SHARD: ${{ matrix.shard }} diff --git a/.gitignore b/.gitignore index a5ac2a50e44..ccc42239a87 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,7 @@ profile.cov # Extensions /pkg/cmd/grafana-cli/runner/wireexts_enterprise.go /pkg/server/wireexts_enterprise.go +/pkg/server/enterprise_wire_gen.go /pkg/build/cmd/enterprise.go /pkg/extensions/* !/pkg/extensions/.keep @@ -204,9 +205,6 @@ compilation-stats.json # auto generated frontend docs /docs/sources/packages_api -# wire generated files -**/wire_gen.go - # Auto-generated internationalization files public/locales/_build/ public/locales/*/*.js diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go new file mode 100644 index 00000000000..8bed4c0fc67 --- /dev/null +++ b/pkg/server/wire_gen.go @@ -0,0 +1,1461 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run ./pkg/build/wire/cmd/wire/main.go gen -tags "oss" +//go:build !wireinject && !enterprise && !pro + +package server + +import ( + "github.com/google/wire" + httpclient2 "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/api/avatar" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/log/slogadapter" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/infra/serverlock" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/infra/usagestats/service" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" + validator2 "github.com/grafana/grafana/pkg/infra/usagestats/validator" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/login/social/connectors" + "github.com/grafana/grafana/pkg/login/social/socialimpl" + "github.com/grafana/grafana/pkg/middleware/csrf" + "github.com/grafana/grafana/pkg/middleware/loggermw" + "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" + provider2 "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" + manager3 "github.com/grafana/grafana/pkg/plugins/manager" + "github.com/grafana/grafana/pkg/plugins/manager/filestore" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/registry/apis" + "github.com/grafana/grafana/pkg/registry/apis/dashboard" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" + "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/registry/apis/iam" + "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" + "github.com/grafana/grafana/pkg/registry/apis/ofrep" + provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" + query2 "github.com/grafana/grafana/pkg/registry/apis/query" + "github.com/grafana/grafana/pkg/registry/apis/secret" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" + "github.com/grafana/grafana/pkg/registry/apis/userstorage" + "github.com/grafana/grafana/pkg/registry/apps" + advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" + notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" + "github.com/grafana/grafana/pkg/registry/apps/investigations" + "github.com/grafana/grafana/pkg/registry/apps/playlist" + "github.com/grafana/grafana/pkg/registry/backgroundsvcs" + "github.com/grafana/grafana/pkg/registry/usagestatssvcs" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + dualwrite2 "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" + "github.com/grafana/grafana/pkg/services/anonymous/anonimpl" + "github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore" + "github.com/grafana/grafana/pkg/services/anonymous/validator" + "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" + "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/aggregatorrunner" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" + "github.com/grafana/grafana/pkg/services/auth/idimpl" + "github.com/grafana/grafana/pkg/services/auth/jwt" + "github.com/grafana/grafana/pkg/services/authn/authnimpl" + "github.com/grafana/grafana/pkg/services/authz" + "github.com/grafana/grafana/pkg/services/caching" + "github.com/grafana/grafana/pkg/services/cleanup" + "github.com/grafana/grafana/pkg/services/cloudmigration/cloudmigrationimpl" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/correlations" + "github.com/grafana/grafana/pkg/services/dashboardimport" + service9 "github.com/grafana/grafana/pkg/services/dashboardimport/service" + dashboards2 "github.com/grafana/grafana/pkg/services/dashboards" + database2 "github.com/grafana/grafana/pkg/services/dashboards/database" + service5 "github.com/grafana/grafana/pkg/services/dashboards/service" + "github.com/grafana/grafana/pkg/services/dashboardsnapshots" + database4 "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database" + service8 "github.com/grafana/grafana/pkg/services/dashboardsnapshots/service" + "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" + "github.com/grafana/grafana/pkg/services/datasourceproxy" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/guardian" + service7 "github.com/grafana/grafana/pkg/services/datasources/service" + "github.com/grafana/grafana/pkg/services/encryption" + "github.com/grafana/grafana/pkg/services/encryption/provider" + service2 "github.com/grafana/grafana/pkg/services/encryption/service" + "github.com/grafana/grafana/pkg/services/extsvcauth" + registry2 "github.com/grafana/grafana/pkg/services/extsvcauth/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/grpcserver" + "github.com/grafana/grafana/pkg/services/grpcserver/context" + "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" + "github.com/grafana/grafana/pkg/services/hooks" + "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" + "github.com/grafana/grafana/pkg/services/ldap" + api4 "github.com/grafana/grafana/pkg/services/ldap/api" + service10 "github.com/grafana/grafana/pkg/services/ldap/service" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/librarypanels" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/live" + "github.com/grafana/grafana/pkg/services/live/pushhttp" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoimpl" + "github.com/grafana/grafana/pkg/services/loginattempt" + "github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl" + "github.com/grafana/grafana/pkg/services/navtree/navtreeimpl" + "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/services/ngalert/image" + metrics2 "github.com/grafana/grafana/pkg/services/ngalert/metrics" + store2 "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/oauthtoken" + "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" + "github.com/grafana/grafana/pkg/services/plugindashboards" + service6 "github.com/grafana/grafana/pkg/services/plugindashboards/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration" + "github.com/grafana/grafana/pkg/services/pluginsintegration/advisor" + "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetectorsprovider" + "github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector" + "github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore" + "github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" + licensing2 "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/loader" + "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginexternal" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" + service4 "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/renderer" + "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" + "github.com/grafana/grafana/pkg/services/pluginsintegration/serviceregistration" + "github.com/grafana/grafana/pkg/services/preference/prefimpl" + "github.com/grafana/grafana/pkg/services/provisioning" + "github.com/grafana/grafana/pkg/services/publicdashboards" + api2 "github.com/grafana/grafana/pkg/services/publicdashboards/api" + database3 "github.com/grafana/grafana/pkg/services/publicdashboards/database" + "github.com/grafana/grafana/pkg/services/publicdashboards/metric" + service3 "github.com/grafana/grafana/pkg/services/publicdashboards/service" + "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/queryhistory" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/rendering" + search2 "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/search/sort" + "github.com/grafana/grafana/pkg/services/searchV2" + "github.com/grafana/grafana/pkg/services/searchusers" + "github.com/grafana/grafana/pkg/services/searchusers/filters" + "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/secrets/database" + kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" + migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" + "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/secrets/migrator" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" + manager2 "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" + "github.com/grafana/grafana/pkg/services/serviceaccounts/proxy" + "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" + "github.com/grafana/grafana/pkg/services/shorturls" + "github.com/grafana/grafana/pkg/services/shorturls/shorturlimpl" + "github.com/grafana/grafana/pkg/services/signingkeys" + "github.com/grafana/grafana/pkg/services/signingkeys/signingkeysimpl" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/services/ssosettings" + "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" + api3 "github.com/grafana/grafana/pkg/services/star/api" + "github.com/grafana/grafana/pkg/services/star/starimpl" + "github.com/grafana/grafana/pkg/services/stats/statsimpl" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/resolver" + "github.com/grafana/grafana/pkg/services/store/sanitizer" + "github.com/grafana/grafana/pkg/services/supportbundles" + "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" + "github.com/grafana/grafana/pkg/services/tag" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/services/team/teamapi" + "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/temp_user" + "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" + "github.com/grafana/grafana/pkg/services/updatemanager" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/services/validations" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + database5 "github.com/grafana/grafana/pkg/storage/secret/database" + encryption2 "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/metadata" + migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/storage/unified" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor" + "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource" + "github.com/grafana/grafana/pkg/tsdb/grafana-pyroscope-datasource" + "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" + "github.com/grafana/grafana/pkg/tsdb/grafanads" + "github.com/grafana/grafana/pkg/tsdb/graphite" + "github.com/grafana/grafana/pkg/tsdb/influxdb" + "github.com/grafana/grafana/pkg/tsdb/jaeger" + "github.com/grafana/grafana/pkg/tsdb/loki" + "github.com/grafana/grafana/pkg/tsdb/mssql" + "github.com/grafana/grafana/pkg/tsdb/mysql" + "github.com/grafana/grafana/pkg/tsdb/opentsdb" + "github.com/grafana/grafana/pkg/tsdb/parca" + "github.com/grafana/grafana/pkg/tsdb/prometheus" + "github.com/grafana/grafana/pkg/tsdb/tempo" + "github.com/grafana/grafana/pkg/tsdb/zipkin" + "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +import ( + _ "github.com/grafana/grafana/pkg/extensions" +) + +// Injectors from wire.go: + +func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Server, error) { + routeRegisterImpl := routing.ProvideRegister() + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return nil, err + } + inProcBus := bus.ProvideBus(tracingService) + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return nil, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + ossMigrations := migrations.ProvideOSSMigrations(featureToggles) + sqlStore, err := sqlstore.ProvideService(cfg, featureToggles, ossMigrations, inProcBus, tracingService) + if err != nil { + return nil, err + } + kvStore := kvstore.ProvideService(sqlStore) + accessControl := acimpl.ProvideAccessControl(featureToggles) + bundleregistryService := bundleregistry.ProvideService() + usageStats, err := service.ProvideService(cfg, kvStore, routeRegisterImpl, tracingService, accessControl, bundleregistryService) + if err != nil { + return nil, err + } + secretsStoreImpl := database.ProvideSecretsStore(sqlStore) + providerProvider := provider.ProvideEncryptionProvider() + serviceService, err := service2.ProvideEncryptionService(tracingService, providerProvider, usageStats, cfg) + if err != nil { + return nil, err + } + osskmsprovidersService := osskmsproviders.ProvideService(serviceService, cfg, featureToggles) + secretsService, err := manager.ProvideSecretsService(tracingService, secretsStoreImpl, osskmsprovidersService, serviceService, cfg, featureToggles, usageStats) + if err != nil { + return nil, err + } + remoteCache, err := remotecache.ProvideService(cfg, sqlStore, usageStats, secretsService) + if err != nil { + return nil, err + } + ossImpl := setting.ProvideProvider(cfg) + pluginManagementCfg, err := pluginconfig.ProvidePluginManagementConfig(cfg, ossImpl, featureToggles) + if err != nil { + return nil, err + } + pluginInstanceCfg, err := pluginconfig.ProvidePluginInstanceConfig(cfg, ossImpl, featureToggles) + if err != nil { + return nil, err + } + hooksService := hooks.ProvideService() + ossLicensingService := licensing.ProvideService(cfg, hooksService) + licensingService := licensing2.ProvideLicensing(cfg, ossLicensingService) + envVarsProvider := pluginconfig.NewEnvVarsProvider(pluginInstanceCfg, licensingService) + inMemory := registry.ProvideService() + rendererManager, err := renderer.ProvideService(pluginManagementCfg, envVarsProvider, inMemory, tracingService) + if err != nil { + return nil, err + } + renderingService, err := rendering.ProvideService(cfg, featureToggles, remoteCache, rendererManager) + if err != nil { + return nil, err + } + cacheService := localcache.ProvideService() + ossDataSourceRequestValidator := validations.ProvideValidator() + sourcesService := sources.ProvideService(cfg, pluginManagementCfg) + discovery := pipeline.ProvideDiscoveryStage(pluginManagementCfg, inMemory) + keystoreService := keystore.ProvideService(kvStore) + keyRetriever := dynamic.ProvideService(cfg, keystoreService) + keyretrieverService := keyretriever.ProvideService(keyRetriever) + signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + assetpathService := assetpath.ProvideService(pluginManagementCfg, pluginscdnService) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, assetpathService) + unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) + validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) + angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) + angulardetectorsproviderDynamic, err := angulardetectorsprovider.ProvideDynamic(cfg, angularpatternsstoreService) + if err != nil { + return nil, err + } + angularinspectorService, err := angularinspector.ProvideService(angulardetectorsproviderDynamic) + if err != nil { + return nil, err + } + validate := pipeline.ProvideValidationStage(pluginManagementCfg, validation, angularinspectorService) + ossDataSourceRequestURLValidator := validations.ProvideURLValidator() + httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService) + azuremonitorService := azuremonitor.ProvideService(httpclientProvider) + cloudwatchService := cloudwatch.ProvideService() + cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider) + elasticsearchService := elasticsearch.ProvideService(httpclientProvider) + graphiteService := graphite.ProvideService(httpclientProvider, tracingService) + influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles) + tracer := otelTracer() + lokiService := loki.ProvideService(httpclientProvider, tracer) + opentsdbService := opentsdb.ProvideService(httpclientProvider) + prometheusService := prometheus.ProvideService(httpclientProvider) + tempoService := tempo.ProvideService(httpclientProvider) + testdatasourceService := testdatasource.ProvideService() + postgresService := postgres.ProvideService(cfg) + mysqlService := mysql.ProvideService() + mssqlService := mssql.ProvideService(cfg) + entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) + quotaService := quotaimpl.ProvideService(sqlStore, cfg) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + teamService, err := teamimpl.ProvideService(sqlStore, cfg, tracingService) + if err != nil { + return nil, err + } + userService, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamService, cacheService, tracingService, quotaService, bundleregistryService) + if err != nil { + return nil, err + } + actionSetService := resourcepermissions.NewActionSetService() + permissionRegistry := permreg.ProvidePermissionRegistry() + serverLockService := serverlock.ProvideService(sqlStore, tracingService) + acimplService, err := acimpl.ProvideService(cfg, sqlStore, routeRegisterImpl, cacheService, accessControl, userService, actionSetService, featureToggles, tracingService, permissionRegistry, serverLockService) + if err != nil { + return nil, err + } + folderStoreImpl := folderimpl.ProvideStore(sqlStore) + tagimplService := tagimpl.ProvideService(sqlStore) + dashboardsStore, err := database2.ProvideDashboardStore(sqlStore, cfg, featureToggles, tagimplService) + if err != nil { + return nil, err + } + dashboardFolderStoreImpl := folderimpl.ProvideDashboardFolderStore(sqlStore) + publicDashboardStoreImpl := database3.ProvideStore(sqlStore, cfg, featureToggles) + publicDashboardServiceWrapperImpl := service3.ProvideServiceWrapper(publicDashboardStoreImpl) + registerer := metrics.ProvideRegisterer() + apikeyService, err := apikeyimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + contextHandler := grpccontext.ProvideContextHandler(tracingService) + authenticator := interceptors.ProvideAuthenticator(apikeyService, userService, acimplService, contextHandler) + grpcserverProvider, err := grpcserver.ProvideService(cfg, featureToggles, authenticator, tracer, registerer) + if err != nil { + return nil, err + } + client, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + if err != nil { + return nil, err + } + eventualRestConfigProvider := apiserver.ProvideEventualRestConfigProvider() + accessClient, err := authz.ProvideAuthZClient(cfg, featureToggles, grpcserverProvider, tracingService, registerer, sqlStore, acimplService, client, eventualRestConfigProvider) + if err != nil { + return nil, err + } + ossDashboardStats := search.ProvideDashboardStats() + documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) + options := &unified.Options{ + Cfg: cfg, + Features: featureToggles, + DB: sqlStore, + Tracer: tracingService, + Reg: registerer, + Authzc: accessClient, + Docs: documentBuilderSupplier, + } + storageMetrics := resource.ProvideStorageMetrics(registerer) + bleveIndexMetrics := resource.ProvideIndexMetrics(registerer) + resourceClient, err := unified.ProvideUnifiedStorageClient(options, storageMetrics, bleveIndexMetrics) + if err != nil { + return nil, err + } + dualwriteService := dualwrite.ProvideService(featureToggles, registerer, kvStore, cfg) + sortService := sort.ProvideService() + folderimplService := folderimpl.ProvideService(folderStoreImpl, accessControl, inProcBus, dashboardsStore, dashboardFolderStoreImpl, userService, sqlStore, featureToggles, bundleregistryService, publicDashboardServiceWrapperImpl, cfg, registerer, tracer, resourceClient, dualwriteService, sortService, eventualRestConfigProvider) + searchService := searchV2.ProvideService(cfg, sqlStore, entityEventsService, acimplService, tracingService, featureToggles, orgService, userService, folderimplService) + systemUsers := store.ProvideSystemUsersService() + storageService, err := store.ProvideService(sqlStore, featureToggles, cfg, quotaService, systemUsers) + if err != nil { + return nil, err + } + grafanadsService := grafanads.ProvideService(searchService, storageService, featureToggles) + pyroscopeService := pyroscope.ProvideService(httpclientProvider) + parcaService := parca.ProvideService(httpclientProvider) + zipkinService := zipkin.ProvideService(httpclientProvider) + jaegerService := jaeger.ProvideService(httpclientProvider) + corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService) + providerService := provider2.ProvideService(corepluginRegistry) + processService := process.ProvideService() + retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService) + serviceAccountPermissionsService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, retrieverService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + serviceAccountsService, err := manager2.ProvideServiceAccountsService(cfg, usageStats, sqlStore, apikeyService, kvStore, userService, orgService, acimplService, serviceAccountPermissionsService, serverLockService) + if err != nil { + return nil, err + } + extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) + registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) + service11 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) + terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) + if err != nil { + return nil, err + } + errorRegistry := pluginerrs.ProvideErrorTracker() + loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) + pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) + if err != nil { + return nil, err + } + filestoreService := filestore.ProvideService(inMemory) + fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) + folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + dashboardServiceImpl, err := service5.ProvideDashboardServiceImpl(cfg, dashboardsStore, dashboardFolderStoreImpl, featureToggles, folderPermissionsService, accessControl, acimplService, folderimplService, registerer, eventualRestConfigProvider, userService, quotaService, orgService, publicDashboardServiceWrapperImpl, resourceClient, dualwriteService, sortService, serverLockService, kvStore) + if err != nil { + return nil, err + } + pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) + service12 := service6.ProvideService(fileStoreManager, pluginService) + orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) + ssosettingsimplService := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routeRegisterImpl, featureToggles, secretsService, usageStats, registerer, ossImpl, ossLicensingService) + socialService := socialimpl.ProvideService(cfg, featureToggles, usageStats, bundleregistryService, remoteCache, orgRoleMapper, ssosettingsimplService) + loginStore := authinfoimpl.ProvideStore(sqlStore, secretsService) + authinfoimplService := authinfoimpl.ProvideService(loginStore, remoteCache, secretsService) + userAuthTokenService, err := authimpl.ProvideUserAuthTokenService(sqlStore, serverLockService, quotaService, secretsService, cfg, tracingService, featureToggles) + if err != nil { + return nil, err + } + oauthtokenService := oauthtoken.ProvideService(socialService, authinfoimplService, cfg, registerer, serverLockService, tracingService, userAuthTokenService, featureToggles) + ossCachingService := caching.ProvideCachingService() + middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokenService, tracingService, ossCachingService, featureToggles, registerer) + if err != nil { + return nil, err + } + pluginerrsStore := pluginerrs.ProvideStore(errorRegistry) + repoManager, err := repo.ProvideService(pluginManagementCfg) + if err != nil { + return nil, err + } + pluginInstaller := manager3.ProvideInstaller(pluginManagementCfg, inMemory, loaderLoader, repoManager, serviceregistrationService) + ossProvider := guardian.ProvideGuardian() + cacheServiceImpl := service7.ProvideCacheService(cacheService, sqlStore, ossProvider) + shortURLService := shorturlimpl.ProvideService(sqlStore) + queryHistoryService := queryhistory.ProvideService(cfg, sqlStore, routeRegisterImpl, accessControl) + dashboardService := service5.ProvideDashboardService(featureToggles, dashboardServiceImpl) + dashverService := dashverimpl.ProvideService(cfg, sqlStore, dashboardService, dashboardsStore, featureToggles, eventualRestConfigProvider, userService, resourceClient, dualwriteService, sortService) + dashboardSnapshotStore := database4.ProvideStore(sqlStore, cfg) + serviceImpl := service8.ProvideService(dashboardSnapshotStore, secretsService, dashboardService) + dBstore, err := store2.ProvideDBStore(cfg, featureToggles, sqlStore, folderimplService, dashboardService, accessControl, inProcBus) + if err != nil { + return nil, err + } + deleteExpiredService := image.ProvideDeleteExpiredService(dBstore) + tempuserService := tempuserimpl.ProvideService(sqlStore, cfg) + cleanupServiceImpl := annotationsimpl.ProvideCleanupService(sqlStore, cfg) + cleanUpService := cleanup.ProvideService(cfg, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dashboardService, dBstore) + secretsKVStore, err := kvstore2.ProvideService(sqlStore, secretsService) + if err != nil { + return nil, err + } + datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) + requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) + baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) + service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + if err != nil { + return nil, err + } + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + if err != nil { + return nil, err + } + mailer, err := notifications.ProvideSmtpService(cfg) + if err != nil { + return nil, err + } + notificationService, err := notifications.ProvideService(inProcBus, cfg, mailer, tempuserService) + if err != nil { + return nil, err + } + dashboardProvisioningService := service5.ProvideDashboardProvisioningService(featureToggles, dashboardServiceImpl) + receiverPermissionsService, err := ossaccesscontrol.ProvideReceiverPermissionsService(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + if err != nil { + return nil, err + } + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + starService := starimpl.ProvideService(sqlStore) + searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) + queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) + repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) + grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, repositoryImpl, orgService, eventualRestConfigProvider) + if err != nil { + return nil, err + } + gateway := pushhttp.ProvideService(cfg, grafanaLive) + authnimplService := authnimpl.ProvideService(cfg, tracingService, userAuthTokenService, usageStats, registerer, authinfoimplService) + authnAuthenticator := authnimpl.ProvideAuthnServiceAuthenticateOnly(authnimplService) + contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) + logger := loggermw.Provide(cfg, featureToggles) + ngAlert := metrics2.ProvideService() + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + if err != nil { + return nil, err + } + libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService) + libraryPanelService, err := librarypanels.ProvideService(cfg, sqlStore, routeRegisterImpl, libraryElementService, folderimplService) + if err != nil { + return nil, err + } + grafanaService, err := updatemanager.ProvideGrafanaService(cfg, tracingService) + if err != nil { + return nil, err + } + noop := managedplugins.NewNoop() + provisionedpluginsNoop := provisionedplugins.NewNoop() + preinstallImpl := pluginchecker.ProvidePreinstall(cfg) + plugincheckerService := pluginchecker.ProvideService(noop, provisionedpluginsNoop, preinstallImpl) + pluginsService, err := updatemanager.ProvidePluginsService(cfg, pluginstoreService, pluginInstaller, tracingService, featureToggles, plugincheckerService) + if err != nil { + return nil, err + } + ossSearchUserFilter := filters.ProvideOSSSearchUserFilter() + ossService := searchusers.ProvideUsersService(cfg, ossSearchUserFilter, userService) + serviceAccountsProxy, err := proxy.ProvideServiceAccountsProxy(cfg, accessControl, acimplService, featureToggles, serviceAccountPermissionsService, serviceAccountsService, routeRegisterImpl) + if err != nil { + return nil, err + } + pluginassetsService := pluginassets.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) + prefService := prefimpl.ProvideService(sqlStore, cfg) + dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl) + if err != nil { + return nil, err + } + csrfCSRF := csrf.ProvideCSRFFilter(cfg) + playlistService := playlistimpl.ProvideService(sqlStore, tracingService) + secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) + middleware := api2.ProvideMiddleware() + apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) + loginattemptimplService := loginattemptimpl.ProvideService(sqlStore, cfg, serverLockService) + deletionService, err := orgimpl.ProvideDeletionService(sqlStore, cfg, dashboardService, accessControl) + if err != nil { + return nil, err + } + authnService := authnimpl.ProvideAuthnService(authnimplService) + openFeatureService, err := featuremgmt.ProvideOpenFeatureService(cfg) + if err != nil { + return nil, err + } + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService, openFeatureService) + searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) + statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) + gatherer := metrics.ProvideGatherer() + apiAPI := api3.ProvideApi(starService, dashboardService) + anonUserLimitValidatorImpl := validator.ProvideAnonUserLimitValidator() + anonDeviceService := anonimpl.ProvideAnonymousDeviceService(usageStats, authnService, sqlStore, cfg, orgService, serverLockService, accessControl, routeRegisterImpl, anonUserLimitValidatorImpl) + signingkeysimplService, err := signingkeysimpl.ProvideEmbeddedSigningKeysService(sqlStore, secretsService, remoteCache, routeRegisterImpl) + if err != nil { + return nil, err + } + localSigner, err := idimpl.ProvideLocalSigner(signingkeysimplService) + if err != nil { + return nil, err + } + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationService, idimplService) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + if err != nil { + return nil, err + } + validatorService, err := validator2.ProvideService(pluginstoreService) + if err != nil { + return nil, err + } + sandboxService := sandbox.ProvideService(cfg) + advisorService, err := advisor.ProvideService(cfg, eventualRestConfigProvider) + if err != nil { + return nil, err + } + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) + if err != nil { + return nil, err + } + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + if err != nil { + return nil, err + } + metricService, err := metric.ProvideService(publicDashboardStoreImpl, registerer) + if err != nil { + return nil, err + } + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() + aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() + apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) + if err != nil { + return nil, err + } + pluginexternalService, err := pluginexternal.ProvideService(cfg, pluginstoreService) + if err != nil { + return nil, err + } + plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService) + if err != nil { + return nil, err + } + zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) + playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) + investigationsAppProvider := investigations.RegisterApp(cfg) + checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) + alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) + appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) + if err != nil { + return nil, err + } + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + sanitizerProvider := sanitizer.ProvideService(renderingService) + healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) + if err != nil { + return nil, err + } + reflectionService, err := grpcserver.ProvideReflectionService(cfg, grpcserverProvider) + if err != nil { + return nil, err + } + ossGroups := ldap.ProvideGroupsService() + identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) + ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) + apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) + featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) + if err != nil { + return nil, err + } + folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient) + storageBackendImpl := noopstorage.ProvideStorageBackend() + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl) + if err != nil { + return nil, err + } + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + if err != nil { + return nil, err + } + userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) + databaseDatabase := database5.ProvideDatabase(sqlStore) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, featureToggles) + if err != nil { + return nil, err + } + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, featureToggles) + if err != nil { + return nil, err + } + secretDBMigrator := migrator2.NewWithEngine(sqlStore) + secretAPIBuilder, err := secret.RegisterAPIService(featureToggles, cfg, apiserverService, tracingService, secureValueMetadataStorage, keeperMetadataStorage, accessClient, acimplService, secretDBMigrator) + if err != nil { + return nil, err + } + factory := github.ProvideFactory() + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) + v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) + if err != nil { + return nil, err + } + staticFlagEvaluator, err := featuremgmt.ProvideStaticEvaluator(cfg) + if err != nil { + return nil, err + } + ofrepAPIBuilder := ofrep.RegisterAPIService(apiserverService, cfg, staticFlagEvaluator) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, secretAPIBuilder, apiBuilder, ofrepAPIBuilder) + teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + if err != nil { + return nil, err + } + authService, err := jwt.ProvideService(cfg, remoteCache) + if err != nil { + return nil, err + } + ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() + registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokenService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationService) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) + server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, registerer) + if err != nil { + return nil, err + } + return server, nil +} + +func InitializeForTest(t sqlutil.ITestDB, testingT interface { + Cleanup(func()) + mock.TestingT +}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { + routeRegisterImpl := routing.ProvideRegister() + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return nil, err + } + inProcBus := bus.ProvideBus(tracingService) + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return nil, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + ossMigrations := migrations.ProvideOSSMigrations(featureToggles) + sqlStore, err := sqlstore.ProvideServiceForTests(t, cfg, featureToggles, inProcBus, ossMigrations) + if err != nil { + return nil, err + } + kvStore := kvstore.ProvideService(sqlStore) + accessControl := acimpl.ProvideAccessControl(featureToggles) + bundleregistryService := bundleregistry.ProvideService() + usageStats, err := service.ProvideService(cfg, kvStore, routeRegisterImpl, tracingService, accessControl, bundleregistryService) + if err != nil { + return nil, err + } + secretsStoreImpl := database.ProvideSecretsStore(sqlStore) + providerProvider := provider.ProvideEncryptionProvider() + serviceService, err := service2.ProvideEncryptionService(tracingService, providerProvider, usageStats, cfg) + if err != nil { + return nil, err + } + osskmsprovidersService := osskmsproviders.ProvideService(serviceService, cfg, featureToggles) + secretsService, err := manager.ProvideSecretsService(tracingService, secretsStoreImpl, osskmsprovidersService, serviceService, cfg, featureToggles, usageStats) + if err != nil { + return nil, err + } + remoteCache, err := remotecache.ProvideService(cfg, sqlStore, usageStats, secretsService) + if err != nil { + return nil, err + } + ossImpl := setting.ProvideProvider(cfg) + pluginManagementCfg, err := pluginconfig.ProvidePluginManagementConfig(cfg, ossImpl, featureToggles) + if err != nil { + return nil, err + } + pluginInstanceCfg, err := pluginconfig.ProvidePluginInstanceConfig(cfg, ossImpl, featureToggles) + if err != nil { + return nil, err + } + hooksService := hooks.ProvideService() + ossLicensingService := licensing.ProvideService(cfg, hooksService) + licensingService := licensing2.ProvideLicensing(cfg, ossLicensingService) + envVarsProvider := pluginconfig.NewEnvVarsProvider(pluginInstanceCfg, licensingService) + inMemory := registry.ProvideService() + rendererManager, err := renderer.ProvideService(pluginManagementCfg, envVarsProvider, inMemory, tracingService) + if err != nil { + return nil, err + } + renderingService, err := rendering.ProvideService(cfg, featureToggles, remoteCache, rendererManager) + if err != nil { + return nil, err + } + cacheService := localcache.ProvideService() + ossDataSourceRequestValidator := validations.ProvideValidator() + sourcesService := sources.ProvideService(cfg, pluginManagementCfg) + discovery := pipeline.ProvideDiscoveryStage(pluginManagementCfg, inMemory) + keystoreService := keystore.ProvideService(kvStore) + keyRetriever := dynamic.ProvideService(cfg, keystoreService) + keyretrieverService := keyretriever.ProvideService(keyRetriever) + signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService) + pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg) + assetpathService := assetpath.ProvideService(pluginManagementCfg, pluginscdnService) + bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, assetpathService) + unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg) + validation := signature.ProvideValidatorService(unsignedPluginAuthorizer) + angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore) + angulardetectorsproviderDynamic, err := angulardetectorsprovider.ProvideDynamic(cfg, angularpatternsstoreService) + if err != nil { + return nil, err + } + angularinspectorService, err := angularinspector.ProvideService(angulardetectorsproviderDynamic) + if err != nil { + return nil, err + } + validate := pipeline.ProvideValidationStage(pluginManagementCfg, validation, angularinspectorService) + ossDataSourceRequestURLValidator := validations.ProvideURLValidator() + httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService) + azuremonitorService := azuremonitor.ProvideService(httpclientProvider) + cloudwatchService := cloudwatch.ProvideService() + cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider) + elasticsearchService := elasticsearch.ProvideService(httpclientProvider) + graphiteService := graphite.ProvideService(httpclientProvider, tracingService) + influxdbService := influxdb.ProvideService(httpclientProvider, featureToggles) + tracer := otelTracer() + lokiService := loki.ProvideService(httpclientProvider, tracer) + opentsdbService := opentsdb.ProvideService(httpclientProvider) + prometheusService := prometheus.ProvideService(httpclientProvider) + tempoService := tempo.ProvideService(httpclientProvider) + testdatasourceService := testdatasource.ProvideService() + postgresService := postgres.ProvideService(cfg) + mysqlService := mysql.ProvideService() + mssqlService := mssql.ProvideService(cfg) + entityEventsService := store.ProvideEntityEventsService(cfg, sqlStore, featureToggles) + quotaService := quotaimpl.ProvideService(sqlStore, cfg) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + teamService, err := teamimpl.ProvideService(sqlStore, cfg, tracingService) + if err != nil { + return nil, err + } + userService, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamService, cacheService, tracingService, quotaService, bundleregistryService) + if err != nil { + return nil, err + } + actionSetService := resourcepermissions.NewActionSetService() + permissionRegistry := permreg.ProvidePermissionRegistry() + serverLockService := serverlock.ProvideService(sqlStore, tracingService) + acimplService, err := acimpl.ProvideService(cfg, sqlStore, routeRegisterImpl, cacheService, accessControl, userService, actionSetService, featureToggles, tracingService, permissionRegistry, serverLockService) + if err != nil { + return nil, err + } + folderStoreImpl := folderimpl.ProvideStore(sqlStore) + tagimplService := tagimpl.ProvideService(sqlStore) + dashboardsStore, err := database2.ProvideDashboardStore(sqlStore, cfg, featureToggles, tagimplService) + if err != nil { + return nil, err + } + dashboardFolderStoreImpl := folderimpl.ProvideDashboardFolderStore(sqlStore) + publicDashboardStoreImpl := database3.ProvideStore(sqlStore, cfg, featureToggles) + publicDashboardServiceWrapperImpl := service3.ProvideServiceWrapper(publicDashboardStoreImpl) + registerer := metrics.ProvideRegistererForTest() + apikeyService, err := apikeyimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + contextHandler := grpccontext.ProvideContextHandler(tracingService) + authenticator := interceptors.ProvideAuthenticator(apikeyService, userService, acimplService, contextHandler) + grpcserverProvider, err := grpcserver.ProvideService(cfg, featureToggles, authenticator, tracer, registerer) + if err != nil { + return nil, err + } + client, err := authz.ProvideZanzana(cfg, sqlStore, tracingService, featureToggles, registerer) + if err != nil { + return nil, err + } + eventualRestConfigProvider := apiserver.ProvideEventualRestConfigProvider() + accessClient, err := authz.ProvideAuthZClient(cfg, featureToggles, grpcserverProvider, tracingService, registerer, sqlStore, acimplService, client, eventualRestConfigProvider) + if err != nil { + return nil, err + } + ossDashboardStats := search.ProvideDashboardStats() + documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) + options := &unified.Options{ + Cfg: cfg, + Features: featureToggles, + DB: sqlStore, + Tracer: tracingService, + Reg: registerer, + Authzc: accessClient, + Docs: documentBuilderSupplier, + } + storageMetrics := resource.ProvideStorageMetrics(registerer) + bleveIndexMetrics := resource.ProvideIndexMetrics(registerer) + resourceClient, err := unified.ProvideUnifiedStorageClient(options, storageMetrics, bleveIndexMetrics) + if err != nil { + return nil, err + } + dualwriteService := dualwrite.ProvideService(featureToggles, registerer, kvStore, cfg) + sortService := sort.ProvideService() + folderimplService := folderimpl.ProvideService(folderStoreImpl, accessControl, inProcBus, dashboardsStore, dashboardFolderStoreImpl, userService, sqlStore, featureToggles, bundleregistryService, publicDashboardServiceWrapperImpl, cfg, registerer, tracer, resourceClient, dualwriteService, sortService, eventualRestConfigProvider) + searchService := searchV2.ProvideService(cfg, sqlStore, entityEventsService, acimplService, tracingService, featureToggles, orgService, userService, folderimplService) + systemUsers := store.ProvideSystemUsersService() + storageService, err := store.ProvideService(sqlStore, featureToggles, cfg, quotaService, systemUsers) + if err != nil { + return nil, err + } + grafanadsService := grafanads.ProvideService(searchService, storageService, featureToggles) + pyroscopeService := pyroscope.ProvideService(httpclientProvider) + parcaService := parca.ProvideService(httpclientProvider) + zipkinService := zipkin.ProvideService(httpclientProvider) + jaegerService := jaeger.ProvideService(httpclientProvider) + corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService) + providerService := provider2.ProvideService(corepluginRegistry) + processService := process.ProvideService() + retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService) + serviceAccountPermissionsService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, retrieverService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + serviceAccountsService, err := manager2.ProvideServiceAccountsService(cfg, usageStats, sqlStore, apikeyService, kvStore, userService, orgService, acimplService, serviceAccountPermissionsService, serverLockService) + if err != nil { + return nil, err + } + extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) + registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) + service11 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) + terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) + if err != nil { + return nil, err + } + errorRegistry := pluginerrs.ProvideErrorTracker() + loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) + pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) + if err != nil { + return nil, err + } + filestoreService := filestore.ProvideService(inMemory) + fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) + folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + dashboardServiceImpl, err := service5.ProvideDashboardServiceImpl(cfg, dashboardsStore, dashboardFolderStoreImpl, featureToggles, folderPermissionsService, accessControl, acimplService, folderimplService, registerer, eventualRestConfigProvider, userService, quotaService, orgService, publicDashboardServiceWrapperImpl, resourceClient, dualwriteService, sortService, serverLockService, kvStore) + if err != nil { + return nil, err + } + pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) + service12 := service6.ProvideService(fileStoreManager, pluginService) + oauthtokentestService := oauthtokentest.ProvideService() + ossCachingService := caching.ProvideCachingService() + middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokentestService, tracingService, ossCachingService, featureToggles, registerer) + if err != nil { + return nil, err + } + pluginerrsStore := pluginerrs.ProvideStore(errorRegistry) + repoManager, err := repo.ProvideService(pluginManagementCfg) + if err != nil { + return nil, err + } + pluginInstaller := manager3.ProvideInstaller(pluginManagementCfg, inMemory, loaderLoader, repoManager, serviceregistrationService) + ossProvider := guardian.ProvideGuardian() + cacheServiceImpl := service7.ProvideCacheService(cacheService, sqlStore, ossProvider) + userAuthTokenService, err := authimpl.ProvideUserAuthTokenService(sqlStore, serverLockService, quotaService, secretsService, cfg, tracingService, featureToggles) + if err != nil { + return nil, err + } + shortURLService := shorturlimpl.ProvideService(sqlStore) + queryHistoryService := queryhistory.ProvideService(cfg, sqlStore, routeRegisterImpl, accessControl) + dashboardService := service5.ProvideDashboardService(featureToggles, dashboardServiceImpl) + dashverService := dashverimpl.ProvideService(cfg, sqlStore, dashboardService, dashboardsStore, featureToggles, eventualRestConfigProvider, userService, resourceClient, dualwriteService, sortService) + dashboardSnapshotStore := database4.ProvideStore(sqlStore, cfg) + serviceImpl := service8.ProvideService(dashboardSnapshotStore, secretsService, dashboardService) + dBstore, err := store2.ProvideDBStore(cfg, featureToggles, sqlStore, folderimplService, dashboardService, accessControl, inProcBus) + if err != nil { + return nil, err + } + deleteExpiredService := image.ProvideDeleteExpiredService(dBstore) + tempuserService := tempuserimpl.ProvideService(sqlStore, cfg) + cleanupServiceImpl := annotationsimpl.ProvideCleanupService(sqlStore, cfg) + cleanUpService := cleanup.ProvideService(cfg, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dashboardService, dBstore) + secretsKVStore, err := kvstore2.ProvideService(sqlStore, secretsService) + if err != nil { + return nil, err + } + datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) + requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) + baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) + service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + if err != nil { + return nil, err + } + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + if err != nil { + return nil, err + } + mailer, err := notifications.ProvideSmtpService(cfg) + if err != nil { + return nil, err + } + notificationService, err := notifications.ProvideService(inProcBus, cfg, mailer, tempuserService) + if err != nil { + return nil, err + } + dashboardProvisioningService := service5.ProvideDashboardProvisioningService(featureToggles, dashboardServiceImpl) + receiverPermissionsService, err := ossaccesscontrol.ProvideReceiverPermissionsService(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + if err != nil { + return nil, err + } + orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) + ssosettingsimplService := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routeRegisterImpl, featureToggles, secretsService, usageStats, registerer, ossImpl, ossLicensingService) + socialService := socialimpl.ProvideService(cfg, featureToggles, usageStats, bundleregistryService, remoteCache, orgRoleMapper, ssosettingsimplService) + loginStore := authinfoimpl.ProvideStore(sqlStore, secretsService) + authinfoimplService := authinfoimpl.ProvideService(loginStore, remoteCache, secretsService) + oauthtokenService := oauthtoken.ProvideService(socialService, authinfoimplService, cfg, registerer, serverLockService, tracingService, userAuthTokenService, featureToggles) + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + starService := starimpl.ProvideService(sqlStore) + searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) + queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) + repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) + grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, repositoryImpl, orgService, eventualRestConfigProvider) + if err != nil { + return nil, err + } + gateway := pushhttp.ProvideService(cfg, grafanaLive) + authnimplService := authnimpl.ProvideService(cfg, tracingService, userAuthTokenService, usageStats, registerer, authinfoimplService) + authnAuthenticator := authnimpl.ProvideAuthnServiceAuthenticateOnly(authnimplService) + contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) + logger := loggermw.Provide(cfg, featureToggles) + notificationServiceMock := notifications.MockNotificationService() + ngAlert := metrics2.ProvideServiceForTest() + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + if err != nil { + return nil, err + } + libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService) + libraryPanelService, err := librarypanels.ProvideService(cfg, sqlStore, routeRegisterImpl, libraryElementService, folderimplService) + if err != nil { + return nil, err + } + grafanaService, err := updatemanager.ProvideGrafanaService(cfg, tracingService) + if err != nil { + return nil, err + } + noop := managedplugins.NewNoop() + provisionedpluginsNoop := provisionedplugins.NewNoop() + preinstallImpl := pluginchecker.ProvidePreinstall(cfg) + plugincheckerService := pluginchecker.ProvideService(noop, provisionedpluginsNoop, preinstallImpl) + pluginsService, err := updatemanager.ProvidePluginsService(cfg, pluginstoreService, pluginInstaller, tracingService, featureToggles, plugincheckerService) + if err != nil { + return nil, err + } + ossSearchUserFilter := filters.ProvideOSSSearchUserFilter() + ossService := searchusers.ProvideUsersService(cfg, ossSearchUserFilter, userService) + serviceAccountsProxy, err := proxy.ProvideServiceAccountsProxy(cfg, accessControl, acimplService, featureToggles, serviceAccountPermissionsService, serviceAccountsService, routeRegisterImpl) + if err != nil { + return nil, err + } + pluginassetsService := pluginassets.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService) + avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg) + prefService := prefimpl.ProvideService(sqlStore, cfg) + dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl) + if err != nil { + return nil, err + } + csrfCSRF := csrf.ProvideCSRFFilter(cfg) + playlistService := playlistimpl.ProvideService(sqlStore, tracingService) + secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) + publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) + middleware := api2.ProvideMiddleware() + apiApi := api2.ProvideApi(publicDashboardServiceImpl, routeRegisterImpl, accessControl, featureToggles, middleware, cfg, ossLicensingService) + loginattemptimplService := loginattemptimpl.ProvideService(sqlStore, cfg, serverLockService) + deletionService, err := orgimpl.ProvideDeletionService(sqlStore, cfg, dashboardService, accessControl) + if err != nil { + return nil, err + } + authnService := authnimpl.ProvideAuthnService(authnimplService) + openFeatureService, err := featuremgmt.ProvideOpenFeatureService(cfg) + if err != nil { + return nil, err + } + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService, openFeatureService) + searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) + statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) + gatherer := metrics.ProvideGathererForTest(registerer) + apiAPI := api3.ProvideApi(starService, dashboardService) + anonUserLimitValidatorImpl := validator.ProvideAnonUserLimitValidator() + anonDeviceService := anonimpl.ProvideAnonymousDeviceService(usageStats, authnService, sqlStore, cfg, orgService, serverLockService, accessControl, routeRegisterImpl, anonUserLimitValidatorImpl) + signingkeysimplService, err := signingkeysimpl.ProvideEmbeddedSigningKeysService(sqlStore, secretsService, remoteCache, routeRegisterImpl) + if err != nil { + return nil, err + } + localSigner, err := idimpl.ProvideLocalSigner(signingkeysimplService) + if err != nil { + return nil, err + } + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationServiceMock, idimplService) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + if err != nil { + return nil, err + } + validatorService, err := validator2.ProvideService(pluginstoreService) + if err != nil { + return nil, err + } + sandboxService := sandbox.ProvideService(cfg) + advisorService, err := advisor.ProvideService(cfg, eventualRestConfigProvider) + if err != nil { + return nil, err + } + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) + if err != nil { + return nil, err + } + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + if err != nil { + return nil, err + } + metricService, err := metric.ProvideService(publicDashboardStoreImpl, registerer) + if err != nil { + return nil, err + } + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() + aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() + apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) + if err != nil { + return nil, err + } + pluginexternalService, err := pluginexternal.ProvideService(cfg, pluginstoreService) + if err != nil { + return nil, err + } + plugininstallerService, err := plugininstaller.ProvideService(cfg, pluginstoreService, pluginInstaller, registerer, repoManager, featureToggles, plugincheckerService) + if err != nil { + return nil, err + } + zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) + playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) + investigationsAppProvider := investigations.RegisterApp(cfg) + checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) + alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) + appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) + if err != nil { + return nil, err + } + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + sanitizerProvider := sanitizer.ProvideService(renderingService) + healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) + if err != nil { + return nil, err + } + reflectionService, err := grpcserver.ProvideReflectionService(cfg, grpcserverProvider) + if err != nil { + return nil, err + } + ossGroups := ldap.ProvideGroupsService() + identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) + ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) + apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) + featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) + if err != nil { + return nil, err + } + folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient) + storageBackendImpl := noopstorage.ProvideStorageBackend() + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl) + if err != nil { + return nil, err + } + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + if err != nil { + return nil, err + } + userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) + databaseDatabase := database5.ProvideDatabase(sqlStore) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, featureToggles) + if err != nil { + return nil, err + } + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, featureToggles) + if err != nil { + return nil, err + } + secretDBMigrator := migrator2.NewWithEngine(sqlStore) + secretAPIBuilder, err := secret.RegisterAPIService(featureToggles, cfg, apiserverService, tracingService, secureValueMetadataStorage, keeperMetadataStorage, accessClient, acimplService, secretDBMigrator) + if err != nil { + return nil, err + } + factory := github.ProvideFactory() + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) + v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) + if err != nil { + return nil, err + } + staticFlagEvaluator, err := featuremgmt.ProvideStaticEvaluator(cfg) + if err != nil { + return nil, err + } + ofrepAPIBuilder := ofrep.RegisterAPIService(apiserverService, cfg, staticFlagEvaluator) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, secretAPIBuilder, apiBuilder, ofrepAPIBuilder) + teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + if err != nil { + return nil, err + } + authService, err := jwt.ProvideService(cfg, remoteCache) + if err != nil { + return nil, err + } + ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() + registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokentestService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationServiceMock) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, serviceImpl, serviceAccountsProxy, sanitizerProvider, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) + server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, registerer) + if err != nil { + return nil, err + } + testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory) + if err != nil { + return nil, err + } + return testEnv, nil +} + +func InitializeForCLI(cfg *setting.Cfg) (Runner, error) { + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return Runner{}, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + ossMigrations := migrations.ProvideOSSMigrations(featureToggles) + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return Runner{}, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return Runner{}, err + } + inProcBus := bus.ProvideBus(tracingService) + sqlStore, err := sqlstore.ProvideService(cfg, featureToggles, ossMigrations, inProcBus, tracingService) + if err != nil { + return Runner{}, err + } + ossImpl := setting.ProvideProvider(cfg) + providerProvider := provider.ProvideEncryptionProvider() + kvStore := kvstore.ProvideService(sqlStore) + routeRegisterImpl := routing.ProvideRegister() + accessControl := acimpl.ProvideAccessControl(featureToggles) + bundleregistryService := bundleregistry.ProvideService() + usageStats, err := service.ProvideService(cfg, kvStore, routeRegisterImpl, tracingService, accessControl, bundleregistryService) + if err != nil { + return Runner{}, err + } + serviceService, err := service2.ProvideEncryptionService(tracingService, providerProvider, usageStats, cfg) + if err != nil { + return Runner{}, err + } + secretsStoreImpl := database.ProvideSecretsStore(sqlStore) + osskmsprovidersService := osskmsproviders.ProvideService(serviceService, cfg, featureToggles) + secretsService, err := manager.ProvideSecretsService(tracingService, secretsStoreImpl, osskmsprovidersService, serviceService, cfg, featureToggles, usageStats) + if err != nil { + return Runner{}, err + } + secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) + quotaService := quotaimpl.ProvideService(sqlStore, cfg) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return Runner{}, err + } + teamService, err := teamimpl.ProvideService(sqlStore, cfg, tracingService) + if err != nil { + return Runner{}, err + } + cacheService := localcache.ProvideService() + userService, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamService, cacheService, tracingService, quotaService, bundleregistryService) + if err != nil { + return Runner{}, err + } + runner := NewRunner(cfg, sqlStore, ossImpl, serviceService, featureToggles, secretsService, secretsMigrator, userService) + return runner, nil +} + +// InitializeForCLITarget is a simplified set of dependencies for the CLI, used +// by the server target subcommand to launch specific dskit modules. +func InitializeForCLITarget(cfg *setting.Cfg) (ModuleRunner, error) { + ossImpl := setting.ProvideProvider(cfg) + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return ModuleRunner{}, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + moduleRunner := NewModuleRunner(cfg, ossImpl, featureToggles) + return moduleRunner, nil +} + +// InitializeModuleServer is a simplified set of dependencies for the CLI, +// suitable for running background services and targeting dskit modules. +func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*ModuleServer, error) { + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return nil, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + registerer := metrics.ProvideRegisterer() + storageMetrics := resource.ProvideStorageMetrics(registerer) + bleveIndexMetrics := resource.ProvideIndexMetrics(registerer) + gatherer := metrics.ProvideGatherer() + hooksService := hooks.ProvideService() + ossLicensingService := licensing.ProvideService(cfg, hooksService) + moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, ossLicensingService) + if err != nil { + return nil, err + } + return moduleServer, nil +} + +// Initialize the standalone APIServer factory +func InitializeAPIServerFactory() (standalone.APIServerFactory, error) { + apiServerFactory := standalone.ProvideAPIServerFactory() + return apiServerFactory, nil +} + +func InitializeDocumentBuilders(cfg *setting.Cfg) (resource.DocumentBuilderSupplier, error) { + featureManager, err := featuremgmt.ProvideManagerService(cfg) + if err != nil { + return nil, err + } + featureToggles := featuremgmt.ProvideToggles(featureManager) + ossMigrations := migrations.ProvideOSSMigrations(featureToggles) + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return nil, err + } + inProcBus := bus.ProvideBus(tracingService) + sqlStore, err := sqlstore.ProvideService(cfg, featureToggles, ossMigrations, inProcBus, tracingService) + if err != nil { + return nil, err + } + ossDashboardStats := search.ProvideDashboardStats() + documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) + return documentBuilderSupplier, nil +} + +// wire.go: + +func otelTracer() trace.Tracer { + return otel.GetTracerProvider().Tracer("grafana") +} + +var withOTelSet = wire.NewSet( + otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, +) + +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) + +var wireSet = wire.NewSet( + wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), +) + +var wireCLISet = wire.NewSet( + NewRunner, + wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), +) + +var wireTestSet = wire.NewSet( + wireBasicSet, + ProvideTestEnv, metrics.WireSetForTest, sqlstore.ProvideServiceForTests, metrics2.ProvideServiceForTest, notifications.MockNotificationService, wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, oauthtokentest.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtokentest.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), +) From 03fff523b16f3ab338177037dc2ce2ce0938eb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:10:58 +0200 Subject: [PATCH 15/23] Docs: Update render service tracing (#106698) Co-authored-by: jtvdez --- .../setup-grafana/image-rendering/_index.md | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index 23ecd53aaa3..43c45bf19e5 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -365,6 +365,30 @@ RENDERING_DUMPIO=true } ``` +#### Tracing + +{{< admonition type="note" >}} +Tracing is supported in the image renderer v3.12.6 and later. +{{< /admonition >}} + +Set the tracing URL to enable OpenTelemetry Tracing. The default is empty (disabled). +You can also configure the service name that will be set in the traces. The default is `grafana-image-renderer`. + +```bash +RENDERING_TRACING_URL="http://localhost:4318/v1/traces" +``` + +```json +{ + "rendering": { + "tracing": { + "url": "http://localhost:4318/v1/traces", + "serviceName": "grafana-renderer" + } + } +} +``` + #### Custom Chrome/Chromium If you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) @@ -580,21 +604,3 @@ RENDERING_VIEWPORT_PAGE_ZOOM_LEVEL=1 } } ``` - -#### Tracing - -Enable OpenTelemetry Tracing by setting the tracing URL. Default is empty (disabled). - -```bash -RENDERING_TRACING_URL="http://localhost:4318/v1/traces" -``` - -```json -{ - "rendering": { - "tracing": { - "url": "http://localhost:4318/v1/traces" - } - } -} -``` From d76e55371a7b00c8599dbebd9da333c5662912a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 1 Jul 2025 13:25:16 +0200 Subject: [PATCH 16/23] fix(unified-storage): use the provided connection config parameters (#107455) * fix(unified-storage): use the provided connection config parameters * extend tests * make update-workspace --- pkg/server/wire_gen.go | 10 +++--- .../unified/sql/db/dbimpl/db_engine.go | 12 +++++-- pkg/storage/unified/sql/db/dbimpl/util.go | 5 +++ .../unified/sql/db/dbimpl/util_test.go | 34 ++++++++++++++++--- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 8bed4c0fc67..efa495e511f 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -582,7 +582,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService) + libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService, eventualRestConfigProvider, userService) libraryPanelService, err := librarypanels.ProvideService(cfg, sqlStore, routeRegisterImpl, libraryElementService, folderimplService) if err != nil { return nil, err @@ -747,7 +747,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) @@ -1104,7 +1104,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService) + libraryElementService := libraryelements.ProvideService(cfg, sqlStore, routeRegisterImpl, folderimplService, featureToggles, accessControl, dashboardService, eventualRestConfigProvider, userService) libraryPanelService, err := librarypanels.ProvideService(cfg, sqlStore, routeRegisterImpl, libraryElementService, folderimplService) if err != nil { return nil, err @@ -1269,7 +1269,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) @@ -1444,7 +1444,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go index d170982683a..e462be12ed0 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -87,9 +87,10 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { return nil, fmt.Errorf("open database: %w", err) } - engine.SetMaxOpenConns(0) - engine.SetMaxIdleConns(2) - engine.SetConnMaxLifetime(4 * time.Hour) + engine.SetMaxOpenConns(getter.Int("max_open_conns", 0)) + engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) + maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second + engine.SetConnMaxLifetime(maxLifetime) return engine, nil } @@ -188,5 +189,10 @@ func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { return nil, fmt.Errorf("open database: %w", err) } + engine.SetMaxOpenConns(getter.Int("max_open_conns", 0)) + engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) + maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second + engine.SetConnMaxLifetime(maxLifetime) + return engine, nil } diff --git a/pkg/storage/unified/sql/db/dbimpl/util.go b/pkg/storage/unified/sql/db/dbimpl/util.go index da142be7d04..e2838a5a299 100644 --- a/pkg/storage/unified/sql/db/dbimpl/util.go +++ b/pkg/storage/unified/sql/db/dbimpl/util.go @@ -18,6 +18,7 @@ type confGetter interface { Err() error Bool(key string) bool String(key string) string + Int(key string, def int) int } func newConfGetter(ds *setting.DynamicSection, keyPrefix string) confGetter { @@ -52,6 +53,10 @@ func (g *sectionGetter) String(key string) string { return v } +func (g *sectionGetter) Int(key string, def int) int { + return g.ds.Key(g.keyPrefix + key).MustInt(def) +} + // MakeDSN creates a DSN from the given key/value pair. It validates the strings // form valid UTF-8 sequences and escapes values if needed. func MakeDSN(m map[string]string) (string, error) { diff --git a/pkg/storage/unified/sql/db/dbimpl/util_test.go b/pkg/storage/unified/sql/db/dbimpl/util_test.go index 9fff4209e3e..a46801474f3 100644 --- a/pkg/storage/unified/sql/db/dbimpl/util_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/util_test.go @@ -28,11 +28,13 @@ func TestSectionGetter(t *testing.T) { t.Parallel() var ( - key = "the key" - keyBoolTrue = "I'm true" - keyBoolFalse = "not me!" - prefix = "this is some prefix" - val = string(invalidUTF8ByteSequence) + key = "the key" + keyBoolTrue = "I'm true" + keyBoolFalse = "not me!" + keyIntValid = "valid_int" + keyIntMissing = "missing_int" + prefix = "this is some prefix" + val = string(invalidUTF8ByteSequence) ) t.Run("with prefix", func(t *testing.T) { @@ -42,6 +44,8 @@ func TestSectionGetter(t *testing.T) { prefix + key: val, prefix + keyBoolTrue: "YES", prefix + keyBoolFalse: "0", + prefix + keyIntValid: "42", + // Note: keyIntMissing is intentionally not included to test default behavior }, prefix) require.False(t, g.Bool("whatever bool")) @@ -53,6 +57,15 @@ func TestSectionGetter(t *testing.T) { require.True(t, g.Bool(keyBoolTrue)) require.NoError(t, g.Err()) + require.Equal(t, 999, g.Int("whatever int", 999)) + require.NoError(t, g.Err()) + + require.Equal(t, 42, g.Int(keyIntValid, 100)) + require.NoError(t, g.Err()) + + require.Equal(t, 200, g.Int(keyIntMissing, 200)) + require.NoError(t, g.Err()) + require.Empty(t, g.String("whatever string")) require.NoError(t, g.Err()) @@ -68,6 +81,8 @@ func TestSectionGetter(t *testing.T) { key: val, keyBoolTrue: "true", keyBoolFalse: "f", + keyIntValid: "123", + // Note: keyIntMissing is intentionally not included to test default behavior }, "") require.False(t, g.Bool("whatever bool")) @@ -79,6 +94,15 @@ func TestSectionGetter(t *testing.T) { require.True(t, g.Bool(keyBoolTrue)) require.NoError(t, g.Err()) + require.Equal(t, 500, g.Int("whatever int", 500)) + require.NoError(t, g.Err()) + + require.Equal(t, 123, g.Int(keyIntValid, 0)) + require.NoError(t, g.Err()) + + require.Equal(t, 300, g.Int(keyIntMissing, 300)) + require.NoError(t, g.Err()) + require.Empty(t, g.String("whatever string")) require.NoError(t, g.Err()) From d36225990e6c4f24c0cceb179b16a8b9140e43a7 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 1 Jul 2025 13:52:28 +0200 Subject: [PATCH 17/23] Server: Regenerate wire (#107451) From 9a92900ef33db03087d13a15ad44d972707fcd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Tue, 1 Jul 2025 14:06:21 +0200 Subject: [PATCH 18/23] fix(unified-storage): remove 's' in config parameter (#107457) --- pkg/storage/unified/sql/db/dbimpl/db_engine.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go index e462be12ed0..ea638c2a53f 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -87,7 +87,7 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { return nil, fmt.Errorf("open database: %w", err) } - engine.SetMaxOpenConns(getter.Int("max_open_conns", 0)) + engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second engine.SetConnMaxLifetime(maxLifetime) @@ -189,7 +189,7 @@ func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { return nil, fmt.Errorf("open database: %w", err) } - engine.SetMaxOpenConns(getter.Int("max_open_conns", 0)) + engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second engine.SetConnMaxLifetime(maxLifetime) From da7b83c1bbae257893dc1e4cf7268e1ef4db5d54 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 1 Jul 2025 14:19:25 +0200 Subject: [PATCH 19/23] ControlledLogRows: pass filter levels (#107448) --- public/app/features/explore/Logs/Logs.tsx | 1 + .../features/logs/components/ControlledLogRows.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index acaa32584c1..c51cbfb7d4d 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1035,6 +1035,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { logOptionsStorageKey={SETTING_KEY_ROOT} onLogOptionsChange={onLogOptionsChange} hasUnescapedContent={hasUnescapedContent} + filterLevels={filterLevels} />
)} diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index 4ddaeb13648..189ce66dda9 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -7,6 +7,7 @@ import { DataFrame, EventBusSrv, ExploreLogsPanelState, + LogLevel, LogsMetaItem, LogsSortOrder, SplitOpen, @@ -32,6 +33,7 @@ export interface ControlledLogRowsProps extends Omit { logOptionsStorageKey?: string; onLogOptionsChange?: (option: keyof LogListControlOptions, value: string | boolean | string[]) => void; range: TimeRange; + filterLevels?: LogLevel[]; /** Props added for Table **/ visualisationType: LogsVisualisationType; @@ -45,7 +47,14 @@ export interface ControlledLogRowsProps extends Omit { export type LogRowsComponentProps = Omit< ControlledLogRowsProps, - 'app' | 'dedupStrategy' | 'showLabels' | 'showTime' | 'logsSortOrder' | 'prettifyLogMessage' | 'wrapLogMessage' + | 'app' + | 'dedupStrategy' + | 'filterLevels' + | 'showLabels' + | 'showTime' + | 'logsSortOrder' + | 'prettifyLogMessage' + | 'wrapLogMessage' >; export const ControlledLogRows = forwardRef( @@ -53,6 +62,7 @@ export const ControlledLogRows = forwardRef Date: Tue, 1 Jul 2025 08:59:22 -0500 Subject: [PATCH 20/23] Transformations: GA the Regression transformation (#106074) * First draft of removing flag, regenerating flags and content * Add description to catalog entry * fix gdev dashboard --- .../transforms/regression-analysis.json | 123 +++++++++++++----- .../transform-data/index.md | 2 - .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 4 - pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- .../app/features/transformers/docs/content.ts | 2 - .../transformers/regression/regression.ts | 1 + .../transformers/standardTransformers.ts | 2 +- 11 files changed, 93 insertions(+), 57 deletions(-) diff --git a/devenv/dev-dashboards/transforms/regression-analysis.json b/devenv/dev-dashboards/transforms/regression-analysis.json index 03e9a376cad..f9330ff55cf 100644 --- a/devenv/dev-dashboards/transforms/regression-analysis.json +++ b/devenv/dev-dashboards/transforms/regression-analysis.json @@ -39,6 +39,7 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -70,7 +71,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -96,10 +97,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -144,6 +147,7 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -175,7 +179,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -201,10 +205,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -248,6 +254,7 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -279,7 +286,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -305,11 +312,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "10.3.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "time, val\n2023-11-20 12:09:00, 1\n2023-11-20 12:09:02, 2\n2023-11-20 12:09:03, 3\n2023-11-20 12:09:04, 4\n2023-11-20 12:09:05, 5\n2023-11-20 12:09:06, 6\n2023-11-20 12:09:07, 2\n2023-11-20 12:09:08, 3\n2023-11-20 12:09:09, 4\n2023-11-20 12:09:10, 1\n2023-11-20 12:09:11, 2\n2023-11-20 12:09:12, 3\n2023-11-20 12:09:13, 4", @@ -328,6 +336,7 @@ "options": { "conversions": [ { + "dateFormat": "YYYY-MM-DD hh:mm:ss", "destinationType": "time", "targetField": "time" } @@ -374,6 +383,7 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -405,7 +415,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -431,10 +441,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "time, val\n2023-11-20 12:09:00, 1\n2023-11-20 12:09:02, 2\n2023-11-20 12:09:03, 3\n2023-11-20 12:09:04, 4\n2023-11-20 12:09:05, null\n2023-11-20 12:09:06, 6\n2023-11-20 12:09:07, 2\n2023-11-20 12:09:08, null\n2023-11-20 12:09:09, 4\n2023-11-20 12:09:10, 1\n2023-11-20 12:09:11, 2\n2023-11-20 12:09:12, 3\n2023-11-20 12:09:13, 4", @@ -453,6 +465,7 @@ "options": { "conversions": [ { + "dateFormat": "YYYY-MM-DD hh:mm:ss", "destinationType": "time", "targetField": "time" } @@ -499,14 +512,17 @@ "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", + "fillOpacity": 50, "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "pointShape": "circle", "pointSize": { "fixed": 5 }, + "pointStrokeWidth": 1, "scaleDistribution": { "type": "linear" }, @@ -518,7 +534,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -562,22 +578,42 @@ }, "id": 3, "options": { - "dims": { - "frame": 0 - }, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, - "series": [], - "seriesMapping": "auto", + "mapping": "auto", + "series": [ + { + "frame": { + "matcher": { + "id": "byIndex", + "options": 0 + } + }, + "x": { + "matcher": { + "id": "byType", + "options": "number" + } + }, + "y": { + "matcher": { + "id": "byType", + "options": "number" + } + } + } + ], "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "x,y\n1,4\n2,1\n3,2\n4,-2\n5,6\n3,2\n1,7\n3,9\n6,3\n5,-3\n2,-2\n7,15", @@ -654,7 +690,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -677,6 +713,7 @@ "graphMode": "area", "justifyMode": "auto", "orientation": "auto", + "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" @@ -684,10 +721,16 @@ "fields": "", "values": false }, + "showPercentChange": false, "textMode": "auto", "wideLayout": true }, - "pluginVersion": "10.3.0-pre", + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "refId": "A" + } + ], "title": "stat panel", "transformations": [ { @@ -728,6 +771,7 @@ "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, + "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", @@ -758,7 +802,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -809,11 +854,12 @@ "showLegend": true }, "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "10.3.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "x, val\n6,2\n8,1\n10,5\n15,1\n22,10\n", @@ -877,14 +923,17 @@ "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", + "fillOpacity": 50, "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "pointShape": "circle", "pointSize": { "fixed": 5 }, + "pointStrokeWidth": 1, "scaleDistribution": { "type": "linear" }, @@ -895,7 +944,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": 0 }, { "color": "red", @@ -939,37 +989,42 @@ }, "id": 8, "options": { - "dims": { - "exclude": [], - "frame": 0, - "x": "foo" - }, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "mapping": "auto", "series": [ { - "pointColor": {}, - "pointSize": { - "field": "baz", - "fixed": 50.5, - "max": 100, - "min": 1 + "frame": { + "matcher": { + "id": "byIndex", + "options": 0 + } }, - "x": "foo", - "y": "foo" + "x": { + "matcher": { + "id": "byName", + "options": "foo" + } + }, + "y": { + "matcher": { + "id": "byType", + "options": "number" + } + } } ], - "seriesMapping": "auto", "tooltip": { + "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "10.3.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1014,8 +1069,9 @@ "type": "xychart" } ], + "preload": false, "refresh": "", - "schemaVersion": 39, + "schemaVersion": 41, "tags": [ "gdev", "transform" @@ -1031,6 +1087,5 @@ "timezone": "", "title": "Transforms - Regression analysis", "uid": "d2d2bb99-42e4-44b8-b93e-3ad1aae31c6b", - "version": 39, - "weekStart": "" + "version": 1 } \ No newline at end of file diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 8bcc7039269..529c517f2f9 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -1484,8 +1484,6 @@ There are two different models: - **Polynomial regression** - Fits a polynomial function to the data. {{< figure src="/static/img/docs/transformations/polynomial-regression.png" class="docs-image--no-shadow" max-width= "1100px" alt="A time series visualization with a curved line representing the polynomial function" >}} -> **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the `regressionTransformation` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. - [Table panel]: ref:table-panel [Calculation types]: ref:calculation-types [sparkline cell type]: ref:sparkline-cell-type diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 09f24bcb746..da44ded91fe 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -103,7 +103,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | | `pdfTables` | Enables generating table data as PDF in reporting | | `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | -| `regressionTransformation` | Enables regression analysis transformation | | `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | | `tableNextGen` | Allows access to the new react-data-grid based table component. | | `enableSCIM` | Enables SCIM support for user and group management | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a68bc7368a9..e76e1b494ae 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -420,10 +420,6 @@ export interface FeatureToggles { */ tableSharedCrosshair?: boolean; /** - * Enables regression analysis transformation - */ - regressionTransformation?: boolean; - /** * Enables query hints for Loki * @default true */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c589310337b..417d95f0d52 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -697,13 +697,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaDatavizSquad, }, - { - Name: "regressionTransformation", - Description: "Enables regression analysis transformation", - Stage: FeatureStagePublicPreview, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - }, { // this is mainly used as a way to quickly disable query hints as a safeguard for our infrastructure Name: "lokiQueryHints", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 66dc7718f48..bbd8023623b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -92,7 +92,6 @@ logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true -regressionTransformation,preview,@grafana/dataviz-squad,false,false,true lokiQueryHints,GA,@grafana/observability-logs,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7125fa63ebd..47d4562df37 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -379,10 +379,6 @@ const ( // Enables shared crosshair in table panel FlagTableSharedCrosshair = "tableSharedCrosshair" - // FlagRegressionTransformation - // Enables regression analysis transformation - FlagRegressionTransformation = "regressionTransformation" - // FlagLokiQueryHints // Enables query hints for Loki FlagLokiQueryHints = "lokiQueryHints" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e73c13bc098..ce8ab6502f3 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2615,7 +2615,8 @@ "metadata": { "name": "regressionTransformation", "resourceVersion": "1750434297879", - "creationTimestamp": "2023-11-24T14:49:16Z" + "creationTimestamp": "2023-11-24T14:49:16Z", + "deletionTimestamp": "2025-07-01T13:24:02Z" }, "spec": { "description": "Enables regression analysis transformation", diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index dd98a2e4e7a..f9dd0074977 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -1595,8 +1595,6 @@ ${buildImageContent( imageRenderType, 'A time series visualization with a curved line representing the polynomial function' )} - -> **Note:** This transformation is currently in public preview. Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. Enable the \`regressionTransformation\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. `; }, }, diff --git a/public/app/features/transformers/regression/regression.ts b/public/app/features/transformers/regression/regression.ts index 751dbd63b25..b7d532d381c 100644 --- a/public/app/features/transformers/regression/regression.ts +++ b/public/app/features/transformers/regression/regression.ts @@ -38,6 +38,7 @@ export const DEGREES = [ export const RegressionTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.regression, name: 'Regression analysis', + description: 'Create a new data frame containing values predicted by a statistical model.', operator: (options, ctx) => (source) => source.pipe(map((data) => RegressionTransformer.transformer(options, ctx)(data))), transformer: (options, ctx) => { diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index 852c7a7bc34..4f8a5598c75 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -63,9 +63,9 @@ export const getStandardTransformers = (): TransformerRegistryItem[] => { groupingToMatrixTransformRegistryItem, limitTransformRegistryItem, joinByLabelsTransformRegistryItem, + regressionTransformerRegistryItem, partitionByValuesTransformRegistryItem, ...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []), - ...(config.featureToggles.regressionTransformation ? [regressionTransformerRegistryItem] : []), ...(config.featureToggles.groupToNestedTableTransformation ? [groupToNestedTableTransformRegistryItem] : []), formatTimeTransformerRegistryItem, timeSeriesTableTransformRegistryItem, From f09e85c0484250bb3fab30b341e5394b13d2addf Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:15:10 -0400 Subject: [PATCH 21/23] unified-storage: Distributor rename to better reflect that it'll be used for search (#107409) * rename distributor/ring references to "storage-api" to "search-server" --- pkg/modules/dependencies.go | 36 +++++++++---------- pkg/server/module_server.go | 14 ++++---- pkg/server/ring.go | 14 ++++---- ...ibutor.go => search_server_distributor.go} | 8 ++--- ...t.go => search_server_distributor_test.go} | 2 +- ...ibutor.go => search_server_distributor.go} | 8 ++--- 6 files changed, 41 insertions(+), 41 deletions(-) rename pkg/server/{distributor.go => search_server_distributor.go} (63%) rename pkg/server/{distributor_test.go => search_server_distributor_test.go} (99%) rename pkg/storage/unified/resource/{distributor.go => search_server_distributor.go} (95%) diff --git a/pkg/modules/dependencies.go b/pkg/modules/dependencies.go index e413f5248c4..3ffe045ce06 100644 --- a/pkg/modules/dependencies.go +++ b/pkg/modules/dependencies.go @@ -4,25 +4,25 @@ const ( // All includes all modules necessary for Grafana to run as a standalone server All string = "all" - Core string = "core" - MemberlistKV string = "memberlistkv" - GrafanaAPIServer string = "grafana-apiserver" - StorageRing string = "storage-ring" - Distributor string = "distributor" - StorageServer string = "storage-server" - ZanzanaServer string = "zanzana-server" - InstrumentationServer string = "instrumentation-server" - FrontendServer string = "frontend-server" + Core string = "core" + MemberlistKV string = "memberlistkv" + GrafanaAPIServer string = "grafana-apiserver" + SearchServerRing string = "search-server-ring" + SearchServerDistributor string = "search-server-distributor" + StorageServer string = "storage-server" + ZanzanaServer string = "zanzana-server" + InstrumentationServer string = "instrumentation-server" + FrontendServer string = "frontend-server" ) var dependencyMap = map[string][]string{ - MemberlistKV: {InstrumentationServer}, - StorageRing: {InstrumentationServer, MemberlistKV}, - GrafanaAPIServer: {InstrumentationServer}, - StorageServer: {InstrumentationServer, StorageRing}, - ZanzanaServer: {InstrumentationServer}, - Distributor: {InstrumentationServer, MemberlistKV, StorageRing}, - Core: {}, - All: {Core}, - FrontendServer: {}, + MemberlistKV: {InstrumentationServer}, + SearchServerRing: {InstrumentationServer, MemberlistKV}, + GrafanaAPIServer: {InstrumentationServer}, + StorageServer: {InstrumentationServer, SearchServerRing}, + ZanzanaServer: {InstrumentationServer}, + SearchServerDistributor: {InstrumentationServer, MemberlistKV, SearchServerRing}, + Core: {}, + All: {Core}, + FrontendServer: {}, } diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 5def1b5c979..979e993eb7a 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -116,10 +116,10 @@ type ModuleServer struct { promGatherer prometheus.Gatherer registerer prometheus.Registerer - MemberlistKVConfig kv.Config - httpServerRouter *mux.Router - storageRing *ring.Ring - storageRingClientPool *ringclient.Pool + MemberlistKVConfig kv.Config + httpServerRouter *mux.Router + searchServerRing *ring.Ring + searchServerRingClientPool *ringclient.Pool } // init initializes the server and its services. @@ -162,8 +162,8 @@ func (s *ModuleServer) Run() error { }) m.RegisterModule(modules.MemberlistKV, s.initMemberlistKV) - m.RegisterModule(modules.StorageRing, s.initRing) - m.RegisterModule(modules.Distributor, s.initDistributor) + m.RegisterModule(modules.SearchServerRing, s.initSearchServerRing) + m.RegisterModule(modules.SearchServerDistributor, s.initSearchServerDistributor) m.RegisterModule(modules.Core, func() (services.Service, error) { return NewService(s.cfg, s.opts, s.apiOpts) @@ -183,7 +183,7 @@ func (s *ModuleServer) Run() error { if err != nil { return nil, err } - return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.storageRing, s.MemberlistKVConfig) + return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig) }) m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) { diff --git a/pkg/server/ring.go b/pkg/server/ring.go index 1499702a81a..cd89f719f8a 100644 --- a/pkg/server/ring.go +++ b/pkg/server/ring.go @@ -25,7 +25,7 @@ import ( var metricsPrefix = resource.RingName + "_" -func (ms *ModuleServer) initRing() (services.Service, error) { +func (ms *ModuleServer) initSearchServerRing() (services.Service, error) { if !ms.cfg.EnableSharding { return nil, nil } @@ -48,7 +48,7 @@ func (ms *ModuleServer) initRing() (services.Service, error) { return nil, fmt.Errorf("failed to create KV store client: %s", err) } - storageRing, err := ring.NewWithStoreClientAndStrategy( + searchServerRing, err := ring.NewWithStoreClientAndStrategy( toRingConfig(ms.cfg, ms.MemberlistKVConfig), resource.RingName, resource.RingKey, @@ -58,11 +58,11 @@ func (ms *ModuleServer) initRing() (services.Service, error) { logger, ) if err != nil { - return nil, fmt.Errorf("failed to initialize storage-ring ring: %s", err) + return nil, fmt.Errorf("failed to initialize index-server-ring ring: %s", err) } startFn := func(ctx context.Context) error { - err = storageRing.StartAsync(ctx) + err = searchServerRing.StartAsync(ctx) if err != nil { return fmt.Errorf("failed to start the ring: %s", err) } @@ -74,10 +74,10 @@ func (ms *ModuleServer) initRing() (services.Service, error) { return nil } - ms.storageRing = storageRing - ms.storageRingClientPool = pool + ms.searchServerRing = searchServerRing + ms.searchServerRingClientPool = pool - ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(storageRing) + ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(searchServerRing) svc := services.NewIdleService(startFn, nil) diff --git a/pkg/server/distributor.go b/pkg/server/search_server_distributor.go similarity index 63% rename from pkg/server/distributor.go rename to pkg/server/search_server_distributor.go index 79306666822..41eaabd8b28 100644 --- a/pkg/server/distributor.go +++ b/pkg/server/search_server_distributor.go @@ -10,18 +10,18 @@ import ( "go.opentelemetry.io/otel" ) -func (ms *ModuleServer) initDistributor() (services.Service, error) { +func (ms *ModuleServer) initSearchServerDistributor() (services.Service, error) { var ( distributor = &distributorService{} - tracer = otel.Tracer("unified-storage-distributor") + tracer = otel.Tracer("index-server-distributor") err error ) - distributor.grpcHandler, err = resource.ProvideDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.storageRing, ms.storageRingClientPool) + distributor.grpcHandler, err = resource.ProvideSearchDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.searchServerRing, ms.searchServerRingClientPool) if err != nil { return nil, err } - return services.NewBasicService(nil, distributor.running, nil).WithName(modules.Distributor), nil + return services.NewBasicService(nil, distributor.running, nil).WithName(modules.SearchServerDistributor), nil } type distributorService struct { diff --git a/pkg/server/distributor_test.go b/pkg/server/search_server_distributor_test.go similarity index 99% rename from pkg/server/distributor_test.go rename to pkg/server/search_server_distributor_test.go index b84c002b756..3062b1efe8d 100644 --- a/pkg/server/distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -273,7 +273,7 @@ func initDistributorServerForTest(t *testing.T, memberlistPort int) testModuleSe cfg.MemberlistJoinMember = "127.0.0.1:" + strconv.Itoa(memberlistPort) cfg.MemberlistAdvertiseAddr = "127.0.0.1" cfg.MemberlistAdvertisePort = memberlistPort - cfg.Target = []string{modules.Distributor} + cfg.Target = []string{modules.SearchServerDistributor} cfg.InstanceID = "distributor" // does nothing for the distributor but may be useful to debug tests conn, err := grpc.NewClient(cfg.GRPCServer.Address, diff --git a/pkg/storage/unified/resource/distributor.go b/pkg/storage/unified/resource/search_server_distributor.go similarity index 95% rename from pkg/storage/unified/resource/distributor.go rename to pkg/storage/unified/resource/search_server_distributor.go index fc49901d255..4fbb7083aca 100644 --- a/pkg/storage/unified/resource/distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -21,7 +21,7 @@ import ( "google.golang.org/grpc/metadata" ) -func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) { +func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) { var err error grpcHandler, err := grpcserver.ProvideService(cfg, features, nil, tracer, registerer) if err != nil { @@ -29,7 +29,7 @@ func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureTogg } distributorServer := &distributorServer{ - log: log.New("unified-storage-distributor"), + log: log.New("index-server-distributor"), ring: ring, clientPool: ringClientPool, } @@ -73,8 +73,8 @@ func (c *RingClient) RemoteAddress() string { return c.Conn.Target() } -const RingKey = "unified-storage-ring" -const RingName = "unified_storage_ring" +const RingKey = "search-server-ring" +const RingName = "search_server_ring" const RingHeartbeatTimeout = time.Minute const RingNumTokens = 128 From 1620f028b4ad807bf3ccfea4767035973acd75b0 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:27:06 -0500 Subject: [PATCH 22/23] TableNG: Fix click events bubbling up (#107156) --- .../src/components/Table/Cells/DefaultCell.tsx | 5 ++--- .../src/components/Table/Cells/ImageCell.tsx | 11 +++++++---- .../components/Table/Cells/JSONViewCell.tsx | 7 ++----- .../Table/DataLinksActionsTooltip.tsx | 2 +- .../Table/TableNG/Cells/AutoCell.tsx | 4 ++-- .../Table/TableNG/Cells/BarGaugeCell.tsx | 4 ++-- .../Table/TableNG/Cells/ImageCell.tsx | 6 ++---- .../Table/TableNG/Cells/JSONCell.tsx | 4 ++-- .../grafana-ui/src/components/Table/utils.ts | 18 ++++++++++++++++++ 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx index 1c70685e017..00df617e864 100644 --- a/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx @@ -15,6 +15,7 @@ import { getCellColors, getCellOptions, getDataLinksActionsTooltipUtils, + tooltipOnClickHandler, } from '../utils'; export const DefaultCell = (props: TableCellProps) => { @@ -88,9 +89,7 @@ export const DefaultCell = (props: TableCellProps) => { {...rest} className={cellStyle} style={{ ...cellProps.style, cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }} - onClick={({ clientX, clientY }) => { - setTooltipCoords({ clientX, clientY }); - }} + onClick={tooltipOnClickHandler(setTooltipCoords)} > {shouldShowLink ? ( renderSingleLink(links[0], value, getLinkStyle(tableStyles, cellOptions)) diff --git a/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx index 9eb49788371..d0e6fd2b43a 100644 --- a/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/ImageCell.tsx @@ -3,7 +3,12 @@ import { useState } from 'react'; import { getCellLinks } from '../../../utils/table'; import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip'; import { TableCellDisplayMode, TableCellProps } from '../types'; -import { DataLinksActionsTooltipCoords, getCellOptions, getDataLinksActionsTooltipUtils } from '../utils'; +import { + tooltipOnClickHandler, + DataLinksActionsTooltipCoords, + getCellOptions, + getDataLinksActionsTooltipUtils, +} from '../utils'; const DATALINKS_HEIGHT_OFFSET = 10; @@ -37,9 +42,7 @@ export const ImageCell = (props: TableCellProps) => { {...cellProps} className={tableStyles.cellContainer} style={{ ...cellProps.style, cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }} - onClick={({ clientX, clientY }) => { - setTooltipCoords({ clientX, clientY }); - }} + onClick={tooltipOnClickHandler(setTooltipCoords)} > {/* If there are data links/actions, we render them with image */} {/* Otherwise we simply render the image */} diff --git a/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx index e214c600458..e9d94fec7c5 100644 --- a/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/Cells/JSONViewCell.tsx @@ -7,7 +7,7 @@ import { CellActions } from '../CellActions'; import { DataLinksActionsTooltip, renderSingleLink } from '../DataLinksActionsTooltip'; import { TableCellInspectorMode } from '../TableCellInspector'; import { TableCellProps } from '../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../utils'; +import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../utils'; export function JSONViewCell(props: TableCellProps): JSX.Element { const { cell, tableStyles, cellProps, field, row } = props; @@ -37,10 +37,7 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { return (
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} -
setTooltipCoords({ clientX, clientY })} - > +
{shouldShowLink ? ( renderSingleLink(links[0], displayValue) ) : shouldShowTooltip ? ( diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx index 36327a6ca7b..1bd8774479b 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx +++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx @@ -89,7 +89,7 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
e.stopPropagation() })} + {...getReferenceProps()} {...getFloatingProps()} style={floatingStyles} className={styles.tooltipWrapper} diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index a3bcdad4db0..3fdcfc85491 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -8,7 +8,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../../../themes/ThemeContext'; import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; import { TableCellOptions, TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; import { AutoCellProps } from '../types'; import { getCellLinks } from '../utils'; @@ -27,7 +27,7 @@ export default function AutoCell({ value, field, justifyContent, rowIdx, cellOpt // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
setTooltipCoords({ clientX, clientY })} + onClick={tooltipOnClickHandler(setTooltipCoords)} style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }} data-testid={selectors.components.TablePanel.autoCell} > diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx index b386ef29879..710a8c65726 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -5,7 +5,7 @@ import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@g import { BarGauge } from '../../../BarGauge/BarGauge'; import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; import { BarGaugeCellProps } from '../types'; import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils'; @@ -78,7 +78,7 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actio // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
setTooltipCoords({ clientX, clientY })} + onClick={tooltipOnClickHandler(setTooltipCoords)} > {shouldShowLink ? ( renderSingleLink(links[0], renderComponent()) diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx index 348d0aa168c..bc62e266b12 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; import { TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; import { ImageCellProps } from '../types'; import { getCellLinks } from '../utils'; @@ -33,9 +33,7 @@ export const ImageCell = ({ cellOptions, field, height, justifyContent, value, r
{ - setTooltipCoords({ clientX, clientY }); - }} + onClick={tooltipOnClickHandler(setTooltipCoords)} > {shouldShowLink ? ( renderSingleLink(links[0], img) diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx index 025cf19bac3..2506255cb03 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; import { JSONCellProps } from '../types'; import { getCellLinks } from '../utils'; @@ -43,7 +43,7 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSON // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
setTooltipCoords({ clientX, clientY })} + onClick={tooltipOnClickHandler(setTooltipCoords)} style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }} > {shouldShowLink ? ( diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 1f1f9f0c50c..f4c9b072b16 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -775,3 +775,21 @@ export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: Ac return { shouldShowLink, hasMultipleLinksOrActions }; }; + +const shouldTriggerTooltip = (event: React.MouseEvent): boolean => { + return event.target === event.currentTarget; +}; + +/** + * Creates an onClick handler for table cells that only triggers tooltip when clicking directly on the cell + * @param setTooltipCoords - function to set tooltip coordinates + * @returns onClick handler + */ +export const tooltipOnClickHandler = (setTooltipCoords: (coords: DataLinksActionsTooltipCoords) => void) => { + return (event: React.MouseEvent) => { + if (shouldTriggerTooltip(event)) { + const { clientX, clientY } = event; + setTooltipCoords({ clientX, clientY }); + } + }; +}; From 73e2ead04ba812b13d5610c66f2539873dbcd48c Mon Sep 17 00:00:00 2001 From: beejeebus Date: Tue, 1 Jul 2025 12:02:54 -0400 Subject: [PATCH 23/23] Add checksums to SHA256 mismatch error message (#107461) This should make it easier to debug this issue if we see it again. --- pkg/plugins/repo/client.go | 11 ++++++----- pkg/plugins/repo/errors.go | 6 +++--- pkg/plugins/repo/errors_test.go | 6 ++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/plugins/repo/client.go b/pkg/plugins/repo/client.go index 1c4c6496ece..698b61e186d 100644 --- a/pkg/plugins/repo/client.go +++ b/pkg/plugins/repo/client.go @@ -100,7 +100,7 @@ func (c *Client) SendReq(ctx context.Context, url *url.URL, compatOpts CompatOpt return io.ReadAll(bodyReader) } -func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) { +func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, expectedChecksum string, compatOpts CompatOpts) (err error) { // Try handling URL as a local file path first if _, err := os.Stat(pluginURL); err == nil { // TODO re-verify @@ -136,7 +136,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, if err != nil { return } - err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts) + err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts) } else { c.retryCount = 0 failure := fmt.Sprintf("%v", r) @@ -169,7 +169,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, if c.retryCount < 3 { c.retryCount++ c.log.Debug("Failed downloading. Will retry.") - err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts) + err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts) } return err } @@ -187,8 +187,9 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, if err = w.Flush(); err != nil { return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err) } - if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) { - return ErrChecksumMismatch(pluginURL) + computedChecksum := fmt.Sprintf("%x", h.Sum(nil)) + if len(expectedChecksum) > 0 && expectedChecksum != computedChecksum { + return ErrChecksumMismatch(pluginURL, expectedChecksum, computedChecksum) } c.retryCount = 0 diff --git a/pkg/plugins/repo/errors.go b/pkg/plugins/repo/errors.go index c275ce43afc..f25474345b1 100644 --- a/pkg/plugins/repo/errors.go +++ b/pkg/plugins/repo/errors.go @@ -60,7 +60,7 @@ var ( ErrArcNotFoundBase = errutil.NotFound("plugin.archNotFound"). MustTemplate(ErrArcNotFoundMsg, errutil.WithPublic(ErrArcNotFoundMsg)) - ErrChecksumMismatchMsg = "expected SHA256 checksum does not match the downloaded archive ({{.Public.ArchiveURL}}) - please contact security@grafana.com" + ErrChecksumMismatchMsg = "expected SHA256 checksum ({{.Public.ExpectedSHA256}}) does not match the downloaded archive ({{.Public.ArchiveURL}}) computed SHA256 checksum ({{.Public.ComputedSHA256}}) - please contact security@grafana.com" ErrChecksumMismatchBase = errutil.UnprocessableEntity("plugin.checksumMismatch"). MustTemplate(ErrChecksumMismatchMsg, errutil.WithPublic(ErrChecksumMismatchMsg)) @@ -85,8 +85,8 @@ func ErrArcNotFound(pluginID, systemInfo string) error { return ErrArcNotFoundBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "SysInfo": systemInfo}}) } -func ErrChecksumMismatch(archiveURL string) error { - return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL}}) +func ErrChecksumMismatch(archiveURL, expectedSHA256, computedSHA256 string) error { + return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL, "ExpectedSHA256": expectedSHA256, "ComputedSHA256": computedSHA256}}) } func ErrCorePlugin(pluginID string) error { diff --git a/pkg/plugins/repo/errors_test.go b/pkg/plugins/repo/errors_test.go index 7e10ed587b8..59f4b7446df 100644 --- a/pkg/plugins/repo/errors_test.go +++ b/pkg/plugins/repo/errors_test.go @@ -49,11 +49,13 @@ func TestErrorTemplates(t *testing.T) { require.Equal(t, "plugin.archNotFound", base.Public().MessageID) require.Equal(t, "grafana-test-app is not compatible with your system architecture: darwin-amd64", base.Public().Message) - err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download") + expectedChecksum := "abcdef1234567890" + computedChecksum := "abcdef0987654321" + err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download", expectedChecksum, computedChecksum) require.True(t, errors.As(err, base)) require.Equal(t, http.StatusUnprocessableEntity, base.Public().StatusCode) require.Equal(t, "plugin.checksumMismatch", base.Public().MessageID) - require.Equal(t, "expected SHA256 checksum does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) - please contact security@grafana.com", base.Public().Message) + require.Equal(t, "expected SHA256 checksum (abcdef1234567890) does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) computed SHA256 checksum (abcdef0987654321) - please contact security@grafana.com", base.Public().Message) err = ErrCorePlugin("grafana-test-app") require.True(t, errors.As(err, base))