Merge remote-tracking branch 'origin/main' into guicaulada/unified-secrets

This commit is contained in:
Guilherme Caulada
2022-04-08 14:21:30 -03:00
250 changed files with 4861 additions and 3227 deletions
+13 -9
View File
@@ -901,8 +901,8 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary && gsutil
-m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary || true
&& gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/canary
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -1594,10 +1594,10 @@ steps:
- commands:
- printenv GCP_KEY | base64 -d > /tmp/gcpkey.json
- gcloud auth activate-service-account --key-file=/tmp/gcpkey.json
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest && gsutil
-m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG} &&
gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest || true
&& gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/latest
- gsutil -m rm -r gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG} ||
true && gsutil -m cp -r ./packages/grafana-ui/dist/storybook/* gs://$${PRERELEASE_BUCKET}/artifacts/storybook/${DRONE_TAG}
depends_on:
- build-storybook
- end-to-end-tests-dashboards-suite
@@ -2934,12 +2934,14 @@ steps:
image: byrnedo/alpine-curl:0.1.8
name: grabpl
- commands:
- ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket grafana-prerelease
- ./bin/grabpl artifacts publish --security --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET}
depends_on:
- grabpl
environment:
GCP_KEY:
from_secret: gcp_key
PRERELEASE_BUCKET:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: publish-artifacts
trigger:
@@ -2970,12 +2972,14 @@ steps:
image: byrnedo/alpine-curl:0.1.8
name: grabpl
- commands:
- ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket grafana-prerelease
- ./bin/grabpl artifacts publish --tag ${TAG} --src-bucket $${PRERELEASE_BUCKET}
depends_on:
- grabpl
environment:
GCP_KEY:
from_secret: gcp_key
PRERELEASE_BUCKET:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.1
name: publish-artifacts
trigger:
@@ -4448,6 +4452,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: ec763e70ae08f79845dd4a59c56e6d08ef8e6b39a1f59663aabcc6d9fe758287
hmac: 9b87d4bb17d422973023c6ad1a59980c37832ae60ba6bb0d11ee024ea1e35b1a
...
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+2 -2
View File
@@ -50,7 +50,7 @@ jobs:
example: 7.4.3 or 7.4.3-beta1"
exit 1
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set intermedia variables
id: intermedia
@@ -69,7 +69,7 @@ jobs:
exit 1
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
@@ -11,7 +11,7 @@ jobs:
working-directory: './pr'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
path: './pr'
@@ -51,7 +51,7 @@ jobs:
working-directory: './base'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
path: './base'
ref: ${{ github.event.pull_request.base.ref }}
@@ -92,7 +92,7 @@ jobs:
GITHUB_STEP_NUMBER: 7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Get built packages from pr
uses: actions/download-artifact@v2
@@ -14,7 +14,7 @@ jobs:
ARTIFACT_NAME: 'levitate'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Download artifact'
uses: actions/github-script@v6
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- run: git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync
- name: generate-packages-docs
uses: actions/setup-node@v2.5.1
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions
+27
View File
@@ -1,3 +1,30 @@
<!-- 8.5.0-beta1 START -->
# 8.5.0-beta1 (2022-04-06)
### Features and enhancements
- Add config option to enable/disable reporting. (Enterprise)
- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
- **Caching:** Add separate TTL for resources cache. (Enterprise)
- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
### Bug fixes
- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
### Breaking changes
When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
<!-- 8.5.0-beta1 END -->
<!-- 8.4.5 START -->
# 8.4.5 (2022-03-31)
-11
View File
@@ -374,17 +374,6 @@
"title": "Dashboard list",
"type": "dashlist"
},
{
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 29
},
"id": 30,
"title": "Panel list",
"type": "pluginlist"
},
{
"gridPos": {
"h": 8,
@@ -4,7 +4,7 @@ aliases = ["/docs/grafana/latest/features/dashboard/dashboards/"]
weight = 80
+++
# Dshboard rows
# Dashboard rows
A dashboard row is a logical divider within a dashboard. It is used to group panels together.
+13 -13
View File
@@ -143,23 +143,23 @@ types of template variables.
### Query variable
The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string.
The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
| Query | Description |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
| `{"find": "terms", "field": "@hostname", "size": 1000}` | Returns a list of values for a field using term aggregation. Query will use current dashboard time range as time range for query. |
| `{"find": "terms", "field": "@hostname", "query": '<lucene query>'}` | Returns a list of values for a field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
| Query | Description |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
| `{"find": "terms", "field": "hostname", "query": '<lucene query>'}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
You can use other variables inside the query. Example query definition for a variable named `$host`.
```
{"find": "terms", "field": "@hostname", "query": "@source:$source"}
{"find": "terms", "field": "hostname", "query": "source:$source"}
```
In the above example, we use another variable named `$source` inside the query definition. Whenever you change, via the dropdown, the current value of the ` $source` variable, it will trigger an update of the `$host` variable so it now only contains hostnames filtered by in this case the
`@source` document property.
In the above example, we use another variable named `$source` inside the query definition. Whenever you change, via the dropdown, the current value of the `$source` variable, it will trigger an update of the `$host` variable so it now only contains hostnames filtered by in this case the
`source` document property.
These queries by default return results in term order (which can then be sorted alphabetically or numerically as for any variable).
To produce a list of terms sorted by doc count (a top-N values list), add an `orderBy` property of "doc_count".
@@ -167,22 +167,22 @@ This automatically selects a descending sort; using "asc" with doc_count (a bott
To keep terms in the doc count order, set the variable's Sort dropdown to **Disabled**; you might alternatively still want to use e.g. **Alphabetical** to re-sort them.
```
{"find": "terms", "field": "@hostname", "orderBy": "doc_count"}
{"find": "terms", "field": "hostname", "orderBy": "doc_count"}
```
### Using variables in queries
There are two syntaxes:
- `$<varname>` Example: @hostname:$hostname
- `[[varname]]` Example: @hostname:[[hostname]]
- `$<varname>` Example: hostname:$hostname
- `[[varname]]` Example: hostname:[[hostname]]
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_
options are enabled, Grafana converts the labels from plain text to a lucene compatible condition.
![Query with template variables](/static/img/docs/elasticsearch/elastic-templating-query-7-4.png)
In the above example, we have a lucene query that filters documents based on the `@hostname` property using a variable named `$hostname`. It is also using
In the above example, we have a lucene query that filters documents based on the `hostname` property using a variable named `$hostname`. It is also using
a variable in the _Terms_ group by field input box. This allows you to use a variable to quickly change how the data is grouped.
Example dashboard:
+19 -17
View File
@@ -16,23 +16,25 @@ Grafana includes built-in support for Prometheus. This topic explains options, v
To access Prometheus settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
| Name | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
| `Default` | Default data source that is pre-selected for new panels. |
| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
| `User` | User name for basic authentication. |
| `Password` | Password for basic authentication. |
| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&amp;'. |
| `Label name` | Add the name of the field in the label object. |
| `URL` | If the link is external, then enter the full link URL. You can interpolate the value from the field with `${__value.raw }` macro. |
| `URL Label` | (Optional) Set a custom display label for the link URL. The link label defaults to the full external URL or the name of datasource and is overridden by this setting. |
| `Internal link` | Select if the link is internal or external. In the case of an internal link, a data source selector allows you to select the target data source. Supports tracing data sources only. |
| Name | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
| `Default` | Default data source that is pre-selected for new panels. |
| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
| `User` | User name for basic authentication. |
| `Password` | Password for basic authentication. |
| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&amp;'. |
| **Exemplars configuration** | |
| `Internal link` | Enable this option is you have an internal link. When you enable this option, you will see a data source selector. Select the backend tracing data store for your exemplar data. |
| `Data source` | You will see this option only if you enable `Internal link` option. Select the backend tracing data store for your exemplar data. |
| `URL` | You will see this option only if the `Internal link` option is disabled. Enter the full URL of the external link. You can interpolate the value from the field with `${__value.raw }` macro. |
| `URL Label` | (Optional) add a custom display label to override the value of the `Label name` field. |
| `Label name` | Add a name for the exemplar traceID property. |
## Prometheus query editor
@@ -29,7 +29,7 @@ Your Grafana license includes a maximum number of _Viewer_ and _Editor/Admin_ ac
- An _active user_ is a user who has signed in to Grafana within the last 30 days. This is a rolling window that is updated daily.
- When you reach the number of maximum active viewers or editor/admins, only currently active users can sign in; new users and non-active users cannot sign in when you reach the limit.
- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/ddmins, new editors and admins can still sign in.
- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/admins, new editors and admins can still sign in.
- The number of dashboards that a user can view or edit, and the number of organizations that they can access does not affect the active user count. A user with editor permissions for many dashboards across many different organizations counts as one editor.
- A license limit banner appears to administrators when Grafana reaches its active user limit; editors and viewers do not see the banner.
To change user roles to make better use of your licenses, refer to [Optimize your tiered license](#optimize-your-tiered-license).
@@ -123,7 +123,7 @@ After you apply the token, Grafana Enterprise resets your license and updates th
> If you are running Grafana Enterprise 8.2 or earlier, the license grants you the total number of licensed users _for each user type_.
For example, if your current license includes 60 viewers and 40 dditor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
For example, if your current license includes 60 viewers and 40 editor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
Before you upgrade to Grafana 8.3, ensure that the total number of active users in Grafana does not exceed the number of users in your combined license. If it does, then new users cannot sign in to Grafana 8.3 until the active user count returns below the licensed limit.
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.5.0-beta1]({{< relref "release-notes-8-5-0-beta1" >}})
- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5" >}})
- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4" >}})
- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3" >}})
@@ -0,0 +1,30 @@
+++
title = "Release notes for Grafana 8.5.0-beta1"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.5.0-beta1
### Features and enhancements
- Add config option to enable/disable reporting. (Enterprise)
- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
- **Caching:** Add separate TTL for resources cache. (Enterprise)
- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
### Bug fixes
- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
### Breaking changes
When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
+2 -2
View File
@@ -108,8 +108,8 @@ require (
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.5
gonum.org/v1/gonum v0.9.3
golang.org/x/tools v0.1.9
gonum.org/v1/gonum v0.11.0
google.golang.org/api v0.60.0
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1
+23 -3
View File
@@ -76,6 +76,7 @@ cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg=
github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
@@ -286,7 +287,10 @@ github.com/aerospike/aerospike-client-go v1.27.0/go.mod h1:zj8LBEnWBDOVEIJt8LvaR
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -440,6 +444,7 @@ github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+Wji
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bonitoo-io/go-sql-bigquery v0.3.4-1.4.0/go.mod h1:J4Y6YJm0qTWB9aFziB7cPeSyc6dOZFyJdteSeybVpXQ=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
@@ -892,6 +897,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -905,6 +911,7 @@ github.com/go-kit/kit v0.11.0/go.mod h1:73/6Ixaufkvb5Osvkls8C79vuQ49Ba1rUEUYNSf+
github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -1044,6 +1051,8 @@ github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9G
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -2068,6 +2077,7 @@ github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -2264,6 +2274,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
@@ -2546,6 +2557,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20180630135845-46796da1b0b4/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
@@ -2754,6 +2766,10 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -2782,6 +2798,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -3206,8 +3223,9 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -3220,13 +3238,14 @@ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJ
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
@@ -3520,6 +3539,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
inet.af/netaddr v0.0.0-20210707202901-70468d781e6c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
+1 -1
View File
@@ -1,4 +1,4 @@
{
"stable": "8.4.5",
"testing": "8.4.5"
"testing": "8.5.0-beta1"
}
+1 -1
View File
@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "8.5.0-pre"
"version": "9.0.0-pre"
}
+6 -6
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -109,7 +109,7 @@
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.2.160",
"@swc/helpers": "0.3.8",
"@testing-library/dom": "8.12.0",
"@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
@@ -202,7 +202,7 @@
"jest-canvas-mock": "2.3.1",
"jest-date-mock": "1.0.8",
"jest-fail-on-console": "2.3.0",
"jest-junit": "13.0.0",
"jest-junit": "13.1.0",
"jest-matcher-utils": "27.5.1",
"jest-mock-console": "1.2.3",
"lerna": "^4.0.0",
@@ -237,7 +237,7 @@
"ts-node": "10.7.0",
"typescript": "4.4.4",
"wait-on": "6.0.1",
"webpack": "5.70.0",
"webpack": "5.72.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.9.2",
"webpack-dev-server": "4.7.4",
@@ -276,7 +276,7 @@
"@react-stately/collections": "3.3.7",
"@react-stately/menu": "3.2.6",
"@react-stately/tree": "3.2.3",
"@reduxjs/toolkit": "1.7.2",
"@reduxjs/toolkit": "1.8.0",
"@sentry/browser": "6.19.1",
"@sentry/types": "6.19.1",
"@sentry/utils": "6.19.1",
@@ -396,7 +396,7 @@
"@rushstack/node-core-library": "3.45.1",
"@rushstack/rig-package": "0.3.8",
"@rushstack/ts-command-line": "4.10.7",
"@storybook/react/webpack": "5.70.0",
"@storybook/react/webpack": "5.72.0",
"node-fetch": "2.6.7"
},
"workspaces": {
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -22,7 +22,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.0",
"@grafana/schema": "8.5.0-pre",
"@grafana/schema": "9.0.0-pre",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.28.0",
@@ -47,7 +47,7 @@
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "13.1.3",
"@swc/helpers": "0.3.8",
"@testing-library/dom": "8.12.0",
"@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
@@ -159,7 +159,9 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.number) {
allValues = allValues.concat(field.values.toArray());
allValues = allValues.concat(
field.values.toArray().map((val: number) => Number(val.toFixed(field.config.decimals ?? 0)))
);
}
}
}
@@ -217,7 +219,7 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
unit: undefined,
},
});
if (!config && field.config.unit) {
if (!config && Object.keys(field.config).length) {
config = field.config;
}
}
@@ -481,7 +481,6 @@ export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
intervalMs: number;
maxDataPoints?: number;
range: TimeRange;
reverse?: boolean;
scopedVars: ScopedVars;
targets: TQuery[];
timezone: string;
@@ -41,6 +41,7 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
disable_http_request_histogram?: boolean;
validatedQueries?: boolean;
publicDashboards?: boolean;
lokiLive?: boolean;
swaggerUi?: boolean;
featureHighlights?: boolean;
@@ -52,4 +53,5 @@ export interface FeatureToggles {
alertProvisioning?: boolean;
storageLocalUpload?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
explore2Dashboard?: boolean;
}
@@ -3,7 +3,7 @@ import { locationUtil } from './location';
describe('locationUtil', () => {
const { location } = window;
beforeAll(() => {
beforeEach(() => {
// @ts-ignore
delete window.location;
@@ -21,63 +21,97 @@ describe('locationUtil', () => {
};
});
afterAll(() => {
afterEach(() => {
window.location = location;
});
describe('strip base when appSubUrl configured', () => {
beforeEach(() => {
locationUtil.initialize({
config: { appSubUrl: '/subUrl' } as any,
getVariablesUrlParams: (() => {}) as any,
getTimeRangeForUrl: (() => {}) as any,
describe('stripBaseFromUrl', () => {
describe('when appSubUrl configured', () => {
beforeEach(() => {
locationUtil.initialize({
config: { appSubUrl: '/subUrl' } as any,
getVariablesUrlParams: (() => {}) as any,
getTimeRangeForUrl: (() => {}) as any,
});
});
});
test('relative url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
});
test('relative url with multiple subUrl in path', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('relative url with subdirectory subUrl', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('absolute url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
});
test('absolute url with multiple subUrl in path', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl(
'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
);
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('absolute url with subdirectory subUrl', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
});
});
describe('strip base when appSubUrl not configured', () => {
beforeEach(() => {
locationUtil.initialize({
config: {} as any,
getVariablesUrlParams: (() => {}) as any,
getTimeRangeForUrl: (() => {}) as any,
test('relative url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
});
test('relative url with multiple subUrl in path', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('relative url with subdirectory subUrl', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('absolute url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
});
test('absolute url with multiple subUrl in path', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl(
'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
);
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
});
test('absolute url with subdirectory subUrl', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
});
});
test('relative url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
describe('when appSubUrl not configured', () => {
beforeEach(() => {
locationUtil.initialize({
config: {} as any,
getVariablesUrlParams: (() => {}) as any,
getTimeRangeForUrl: (() => {}) as any,
});
});
test('relative url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
});
test('absolute url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
});
});
test('absolute url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
describe('when origin does not have a port in it', () => {
beforeEach(() => {
window.location = {
...location,
hash: '#hash',
host: 'www.domain.com',
hostname: 'www.domain.com',
href: 'http://www.domain.com/path/b?search=a&b=c&d#hash',
origin: 'http://www.domain.com',
pathname: '/path/b',
port: '',
protocol: 'http:',
search: '?search=a&b=c&d',
};
});
test('relative url', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
});
test('URL with same host, different port', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/subUrl/grafana/');
});
test('URL of a completely different origin', () => {
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.another-domain.com/subUrl/grafana/');
expect(urlWithoutMaster).toBe('http://www.another-domain.com/subUrl/grafana/');
});
});
});
+25 -4
View File
@@ -7,22 +7,43 @@ let grafanaConfig: GrafanaConfig = { appSubUrl: '' } as any;
let getTimeRangeUrlParams: () => RawTimeRange;
let getVariablesUrlParams: (scopedVars?: ScopedVars) => UrlQueryMap;
const maybeParseUrl = (input: string): URL | undefined => {
try {
return new URL(input);
} catch {
return undefined;
}
};
/**
*
* @param url
* @internal
*/
const stripBaseFromUrl = (url: string): string => {
const stripBaseFromUrl = (urlOrPath: string): string => {
// Will only return a URL object if the input is actually a valid URL
const parsedUrl = maybeParseUrl(urlOrPath);
if (parsedUrl) {
// If the input is a URL, and for a different origin that we're on, just bail
// and return it. There's no need to strip anything from it
if (parsedUrl.origin !== window.location.origin) {
return urlOrPath;
}
}
const appSubUrl = grafanaConfig.appSubUrl ?? '';
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
const isAbsoluteUrl = url.startsWith('http');
const isAbsoluteUrl = urlOrPath.startsWith('http');
let segmentToStrip = appSubUrl;
if (!url.startsWith('/') || isAbsoluteUrl) {
if (!urlOrPath.startsWith('/') || isAbsoluteUrl) {
segmentToStrip = `${window.location.origin}${appSubUrl}`;
}
return url.length > 0 && url.indexOf(segmentToStrip) === 0 ? url.slice(segmentToStrip.length - stripExtraChars) : url;
return urlOrPath.length > 0 && urlOrPath.indexOf(segmentToStrip) === 0
? urlOrPath.slice(segmentToStrip.length - stripExtraChars)
: urlOrPath;
};
/**
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -41,14 +41,14 @@
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",
"webpack": "5.70.0"
"webpack": "5.72.0"
},
"types": "src/index.ts",
"dependencies": {
"@babel/core": "7.17.8",
"@babel/preset-env": "7.16.11",
"@cypress/webpack-preprocessor": "5.11.1",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/e2e-selectors": "9.0.0-pre",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "8.2.4",
+5 -5
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "8.5.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/ui": "8.5.0-pre",
"@grafana/data": "9.0.0-pre",
"@grafana/e2e-selectors": "9.0.0-pre",
"@grafana/ui": "9.0.0-pre",
"@sentry/browser": "6.19.1",
"history": "4.10.1",
"lodash": "4.17.21",
@@ -38,7 +38,7 @@
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-commonjs": "21.0.2",
"@rollup/plugin-node-resolve": "13.1.3",
"@testing-library/dom": "8.12.0",
"@testing-library/dom": "8.13.0",
"@testing-library/react": "12.1.4",
"@testing-library/user-event": "^13.5.0",
"@types/angular": "1.8.4",
+3 -2
View File
@@ -119,7 +119,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
this.theme2 = createTheme({ colors: { mode } });
this.theme = this.theme2.v1;
this.bootData = options.bootData;
this.buildInfo = options.buildInfo;
const defaults = {
datasources: {},
@@ -131,7 +130,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
appUrl: '',
appSubUrl: '',
buildInfo: {
version: 'v1.0',
version: '1.0',
commit: '1',
env: 'production',
},
@@ -142,6 +141,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
merge(this, defaults, options);
this.buildInfo = options.buildInfo || defaults.buildInfo;
if (this.dateFormats) {
systemDateFormats.update(this.dateFormats);
}
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -28,10 +28,10 @@
"dependencies": {
"@babel/core": "7.13.14",
"@babel/preset-env": "7.13.12",
"@grafana/data": "8.5.0-pre",
"@grafana/data": "9.0.0-pre",
"@grafana/eslint-config": "2.5.2",
"@grafana/tsconfig": "^1.2.0-rc1",
"@grafana/ui": "8.5.0-pre",
"@grafana/ui": "9.0.0-pre",
"@jest/core": "26.6.3",
"@rushstack/eslint-patch": "1.0.6",
"@types/command-exists": "^1.2.0",
+6 -6
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.7.1",
"@emotion/react": "11.8.2",
"@grafana/aws-sdk": "0.0.35",
"@grafana/data": "8.5.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/schema": "8.5.0-pre",
"@grafana/data": "9.0.0-pre",
"@grafana/e2e-selectors": "9.0.0-pre",
"@grafana/schema": "9.0.0-pre",
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.4",
@@ -115,7 +115,7 @@
"@storybook/react": "6.4.15",
"@storybook/theming": "6.4.15",
"@swc/helpers": "0.3.8",
"@testing-library/dom": "8.12.0",
"@testing-library/dom": "8.13.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "7.0.2",
@@ -175,7 +175,7 @@
"terser-webpack-plugin": "5.3.1",
"ts-loader": "8.0.11",
"typescript": "4.4.4",
"webpack": "5.70.0",
"webpack": "5.72.0",
"webpack-filter-warnings-plugin": "1.2.1"
},
"types": "src/index.ts"
@@ -71,6 +71,7 @@ export const Basic: Story<StoryProps> = ({
name: '',
values: new ArrayVector([1, 2, 3, 4, 3]),
type: FieldType.number,
state: { range: { min: 1, max: 4, delta: 3 } },
config: {},
},
};
@@ -60,7 +60,7 @@ export const CollapsableSection: FC<Props> = ({
{loading ? (
<Spinner className={styles.spinner} />
) : (
<Icon name={open ? 'angle-down' : 'angle-right'} className={styles.icon} />
<Icon name={open ? 'angle-up' : 'angle-down'} className={styles.icon} />
)}
</button>
<div className={styles.label} id={`collapse-label-${id}`}>
@@ -43,6 +43,7 @@ const defaultConfig: GraphFieldConfig = {
axisPlacement: AxisPlacement.Hidden,
};
/** @internal */
export class Sparkline extends PureComponent<SparklineProps, State> {
constructor(props: SparklineProps) {
super(props);
@@ -84,6 +84,7 @@ export {
BigValueJustifyMode,
BigValueTextMode,
} from './BigValue/BigValue';
export { Sparkline } from './Sparkline/Sparkline';
export { Gauge } from './Gauge/Gauge';
export { Graph } from './Graph/Graph';
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "8.5.0-pre",
"version": "9.0.0-pre",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@emotion/css": "11.7.1",
"@grafana/data": "8.5.0-pre",
"@grafana/ui": "8.5.0-pre",
"@grafana/data": "9.0.0-pre",
"@grafana/ui": "9.0.0-pre",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
-28
View File
@@ -1,7 +1,6 @@
package api
import (
"context"
"fmt"
"testing"
@@ -49,11 +48,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
mock := mockstore.NewSQLStoreMock()
adminLogoutUserScenario(t, "Should not be allowed when calling POST on",
"/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: testUserID}
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}, mock)
@@ -182,8 +176,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
bus.ClearBusHandlers()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
@@ -202,8 +194,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
bus.ClearBusHandlers()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
@@ -222,8 +212,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
bus.ClearBusHandlers()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
@@ -241,8 +229,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
}
adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
bus.ClearBusHandlers()
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 412, sc.resp.Code)
@@ -256,8 +242,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := &HTTPServer{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
@@ -284,8 +268,6 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
@@ -312,8 +294,6 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern
func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -343,8 +323,6 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -372,8 +350,6 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route
func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
authInfoService := &logintest.AuthInfoServiceFake{}
@@ -410,8 +386,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
SQLStore: mockstore.NewSQLStoreMock(),
}
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
sc := setupScenarioContext(t, url)
sc.sqlStore = hs.SQLStore
sc.authInfoService = &logintest.AuthInfoServiceFake{}
@@ -430,8 +404,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
Login: loginservice.LoginServiceMock{
+1 -2
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/guardian"
@@ -491,7 +490,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response {
SecureSettings: dto.SecureSettings,
}
if err := bus.Dispatch(c.Req.Context(), cmd); err != nil {
if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil {
if errors.Is(err, models.ErrSmtpNotEnabled) {
return response.Error(412, err.Error(), err)
}
-7
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/search"
@@ -124,18 +123,12 @@ func TestAlertingAPIEndpoint(t *testing.T) {
}
func callPauseAlert(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.PauseAlertCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
func postAlertScenario(t *testing.T, hs *HTTPServer, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.PauseAlertCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
-9
View File
@@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -293,8 +292,6 @@ var fakeAnnoRepo *fakeAnnotationsRepo
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.PostAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -324,8 +321,6 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -354,8 +349,6 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
@@ -385,8 +378,6 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := setupSimpleHTTPServer(nil)
store := sqlstore.InitTestDB(t)
store.Cfg = hs.Cfg
+9 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
var plog = log.New("api")
@@ -417,7 +418,14 @@ func (hs *HTTPServer) registerRoutes() {
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
})
apiRoute.Get("/alert-notifiers", reqEditorRole, routing.Wrap(
var notifiersAuthHandler web.Handler
if hs.Cfg.UnifiedAlerting.IsEnabled() {
notifiersAuthHandler = reqSignedIn
} else {
notifiersAuthHandler = reqEditorRole
}
apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap(
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
)
+3 -5
View File
@@ -53,8 +53,6 @@ func loggedInUserScenario(t *testing.T, desc string, url string, routePattern st
func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
sc := setupScenarioContext(t, url)
sc.sqlStore = sqlStore
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -82,8 +80,6 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url
func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
sc.context = c
@@ -197,7 +193,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, sqlStore)
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy)
loginService := &logintest.LoginServiceFake{}
authenticator := &logintest.AuthenticatorFake{}
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator)
return ctxHdlr
}
-8
View File
@@ -1,7 +1,6 @@
package api
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
@@ -9,7 +8,6 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
@@ -52,12 +50,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
sqlmock.ExpectedDashboardAclInfoList = aclMockResp
sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{}
// we need it here for now for the guadian service to work
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error {
query.Result = aclMockResp
return nil
})
return mockSnapshotResult
}
-20
View File
@@ -957,31 +957,17 @@ func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) {
}
func (hs *HTTPServer) callGetDashboardVersion(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionQuery) error {
query.Result = &models.DashboardVersion{}
return nil
})
sc.handlerFunc = hs.GetDashboardVersion
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionsQuery) error {
query.Result = []*models.DashboardVersionDTO{}
return nil
})
sc.handlerFunc = hs.GetDashboardVersions
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
return nil
})
hs.dashboardService = mockDashboard
sc.handlerFunc = hs.DeleteDashboardByUID
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
@@ -1003,8 +989,6 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) {
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
cfg := setting.NewCfg()
hs := HTTPServer{
Bus: bus.GetBus(),
@@ -1040,8 +1024,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc, sqlmock sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
cfg := setting.NewCfg()
hs := HTTPServer{
Cfg: cfg,
@@ -1076,8 +1058,6 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
cfg := setting.NewCfg()
mockSQLStore := mockstore.NewSQLStoreMock()
hs := HTTPServer{
-3
View File
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
@@ -355,8 +354,6 @@ func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) {
t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
sc := setupScenarioContext(t, ctx.url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
-4
View File
@@ -138,8 +138,6 @@ func callCreateFolder(sc *scenarioContext) {
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.CreateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
Cfg: setting.NewCfg(),
@@ -170,8 +168,6 @@ func callUpdateFolder(sc *scenarioContext) {
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
cmd models.UpdateFolderCommand, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Cfg: setting.NewCfg(),
folderService: folderService,
+1 -1
View File
@@ -507,7 +507,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.pluginMetricsEndpoint)
m.Use(hs.ContextHandler.Middleware)
m.Use(middleware.OrgRedirect(hs.Cfg))
m.Use(middleware.OrgRedirect(hs.Cfg, hs.SQLStore))
m.Use(acmiddleware.LoadPermissionsMiddleware(hs.AccessControl))
// needs to be after context handler
+1 -1
View File
@@ -307,7 +307,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
Id: "serviceaccounts",
Description: "Manage service accounts",
// TODO: change icon to "key-skeleton-alt" when it's available
Icon: "key-skeleton-alt",
Icon: "keyhole-circle",
Url: hs.Cfg.AppSubURL + "/org/serviceaccounts",
})
}
-13
View File
@@ -466,19 +466,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
})
t.Run("When proxying a datasource that has OAuth token pass-through enabled", func(t *testing.T) {
bus.AddHandler("test", func(ctx context.Context, query *models.GetAuthInfoQuery) error {
query.Result = &models.UserAuth{
Id: 1,
UserId: 1,
AuthModule: "generic_oauth",
OAuthAccessToken: "testtoken",
OAuthRefreshToken: "testrefreshtoken",
OAuthTokenType: "Bearer",
OAuthExpiry: time.Now().AddDate(0, 0, 1),
}
return nil
})
ds := &models.DataSource{
Type: "custom-datasource",
Url: "http://host/root/",
-3
View File
@@ -15,7 +15,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -218,8 +217,6 @@ func callGetPluginAsset(sc *scenarioContext) {
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store,
logger log.Logger, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: pluginStore,
-3
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/shorturls"
@@ -54,8 +53,6 @@ func callCreateShortURL(sc *scenarioContext) {
func createShortURLScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CreateShortURLCmd, shortURLService shorturls.Service, fn scenarioFunc) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
defer bus.ClearBusHandlers()
hs := HTTPServer{
Cfg: setting.NewCfg(),
ShortURLService: shortURLService,
-19
View File
@@ -107,25 +107,6 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
}, mock)
loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) {
fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC)
bus.AddHandler("test", func(ctx context.Context, query *models.GetUserByLoginQuery) error {
require.Equal(t, "danlee", query.LoginOrEmail)
query.Result = &models.User{
Id: int64(1),
Email: "daniel@grafana.com",
Name: "Daniel",
Login: "danlee",
Theme: "light",
IsAdmin: true,
OrgId: int64(2),
IsDisabled: false,
Updated: fakeNow,
Created: fakeNow,
}
return nil
})
createUserCmd := models.CreateUserCommand{
Email: fmt.Sprint("admin", "@test.com"),
Name: "admin",
-15
View File
@@ -79,11 +79,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
token := &models.UserToken{Id: 2}
mock := mockstore.NewSQLStoreMock()
revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) {
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
cmd.Result = &models.User{Id: testUserID}
return nil
})
sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) {
return token, nil
}
@@ -153,8 +148,6 @@ func TestUserTokenAPIEndpoint(t *testing.T) {
func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd,
userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -183,8 +176,6 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat
func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -212,8 +203,6 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte
func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
hs := HTTPServer{
Bus: bus.GetBus(),
AuthTokenService: auth.NewFakeUserAuthTokenService(),
@@ -239,8 +228,6 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId
func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64,
token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
@@ -267,8 +254,6 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R
func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) {
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
hs := HTTPServer{
-107
View File
@@ -21,41 +21,17 @@ type Msg interface{}
// ErrHandlerNotFound defines an error if a handler is not found
var ErrHandlerNotFound = errors.New("handler not found")
// TransactionManager defines a transaction interface
type TransactionManager interface {
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
}
// Bus type defines the bus interface structure
type Bus interface {
Dispatch(ctx context.Context, msg Msg) error
Publish(ctx context.Context, msg Msg) error
// InTransaction starts a transaction and store it in the context.
// The caller can then pass a function with multiple DispatchCtx calls that
// all will be executed in the same transaction. InTransaction will rollback if the
// callback returns an error.
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddHandler(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
// SetTransactionManager allows the user to replace the internal
// noop TransactionManager that is responsible for managing
// transactions in `InTransaction`
SetTransactionManager(tm TransactionManager)
}
// InProcBus defines the bus structure
type InProcBus struct {
logger log.Logger
handlers map[string]HandlerFunc
handlersWithCtx map[string]HandlerFunc
listeners map[string][]HandlerFunc
listenersWithCtx map[string][]HandlerFunc
txMng TransactionManager
tracer tracing.Tracer
}
@@ -64,11 +40,6 @@ func ProvideBus(tracer tracing.Tracer) *InProcBus {
return globalBus
}
// InTransaction defines an in transaction function
func (b *InProcBus) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return b.txMng.InTransaction(ctx, fn)
}
// temp stuff, not sure how to handle bus instance, and init yet
var globalBus = New()
@@ -76,11 +47,8 @@ var globalBus = New()
func New() *InProcBus {
bus := &InProcBus{
logger: log.New("bus"),
handlers: make(map[string]HandlerFunc),
handlersWithCtx: make(map[string]HandlerFunc),
listeners: make(map[string][]HandlerFunc),
listenersWithCtx: make(map[string][]HandlerFunc),
txMng: &noopTransactionManager{},
}
bus.tracer = tracing.InitializeForBus()
return bus
@@ -91,46 +59,6 @@ func GetBus() Bus {
return globalBus
}
// SetTransactionManager function assign a transaction manager to the bus.
func (b *InProcBus) SetTransactionManager(tm TransactionManager) {
b.txMng = tm
}
// DispatchCtx function dispatch a message to the bus context.
func (b *InProcBus) Dispatch(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
ctx, span := b.tracer.Start(ctx, "bus - "+msgName)
defer span.End()
span.SetAttributes("msg", msgName, attribute.Key("msg").String(msgName))
withCtx := true
var handler = b.handlersWithCtx[msgName]
if handler == nil {
withCtx = false
handler = b.handlers[msgName]
if handler == nil {
return ErrHandlerNotFound
}
}
var params = []reflect.Value{}
if withCtx {
params = append(params, reflect.ValueOf(ctx))
} else if setting.Env == setting.Dev {
b.logger.Warn("DispatchCtx called with message handler registered using AddHandler and should be changed to use AddHandler", "msgName", msgName)
}
params = append(params, reflect.ValueOf(msg))
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
}
return err.(error)
}
// PublishCtx function publish a message to the bus listener.
func (b *InProcBus) Publish(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
@@ -176,17 +104,6 @@ func callListeners(listeners []HandlerFunc, params []reflect.Value) error {
return nil
}
func (b *InProcBus) AddHandler(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlersWithCtx[queryTypeName] = handler
}
// GetHandlerCtx returns the handler function for the given struct name.
func (b *InProcBus) GetHandlerCtx(name string) HandlerFunc {
return b.handlersWithCtx[name]
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(1).Elem().Name()
@@ -197,36 +114,12 @@ func (b *InProcBus) AddEventListener(handler HandlerFunc) {
b.listenersWithCtx[eventName] = append(b.listenersWithCtx[eventName], handler)
}
// AddHandler attaches a handler function to the global bus context.
// Package level function.
func AddHandler(implName string, handler HandlerFunc) {
globalBus.AddHandler(handler)
}
// AddEventListenerCtx attaches a handler function to the event listener.
// Package level function.
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
}
func Dispatch(ctx context.Context, msg Msg) error {
return globalBus.Dispatch(ctx, msg)
}
func Publish(ctx context.Context, msg Msg) error {
return globalBus.Publish(ctx, msg)
}
func GetHandlerCtx(name string) HandlerFunc {
return globalBus.GetHandlerCtx(name)
}
func ClearBusHandlers() {
globalBus = New()
}
type noopTransactionManager struct{}
func (*noopTransactionManager) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
return fn(ctx)
}
-135
View File
@@ -2,7 +2,6 @@ package bus
import (
"context"
"errors"
"testing"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -14,140 +13,6 @@ type testQuery struct {
Resp string
}
func TestDispatch(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
var invoked bool
bus.AddHandler(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
require.NoError(t, err)
err = bus.Dispatch(context.Background(), &testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatch_NoRegisteredHandler(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
err = bus.Dispatch(context.Background(), &testQuery{})
require.Equal(t, err, ErrHandlerNotFound,
"expected bus to return HandlerNotFound since no handler is registered")
}
func TestDispatch_ContextHandler(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
var invoked bool
bus.AddHandler(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
err = bus.Dispatch(context.Background(), &testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatchCtx(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
var invoked bool
bus.AddHandler(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
err = bus.Dispatch(context.Background(), &testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatchCtx_NoContextHandler(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
var invoked bool
bus.AddHandler(func(ctx context.Context, query *testQuery) error {
invoked = true
return nil
})
err = bus.Dispatch(context.Background(), &testQuery{})
require.NoError(t, err)
require.True(t, invoked, "expected handler to be called")
}
func TestDispatchCtx_NoRegisteredHandler(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
err = bus.Dispatch(context.Background(), &testQuery{})
require.Equal(t, err, ErrHandlerNotFound,
"expected bus to return HandlerNotFound since no handler is registered")
}
func TestQuery(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
want := "hello from handler"
bus.AddHandler(func(ctx context.Context, q *testQuery) error {
q.Resp = want
return nil
})
q := &testQuery{}
err = bus.Dispatch(context.Background(), q)
require.NoError(t, err, "unable to dispatch query")
require.Equal(t, want, q.Resp)
}
func TestQuery_HandlerReturnsError(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
bus.tracer = tracer
bus.AddHandler(func(ctx context.Context, query *testQuery) error {
return errors.New("handler error")
})
err = bus.Dispatch(context.Background(), &testQuery{})
require.Error(t, err, "expected error but got none")
}
func TestEventPublish(t *testing.T) {
bus := New()
tracer, err := tracing.InitializeTracerForTest()
-7
View File
@@ -10,8 +10,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
datasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/setting"
@@ -35,11 +33,6 @@ func TestService(t *testing.T) {
dataSourceService: &datasources.FakeDataSourceService{},
}
bus.AddHandler("test", func(_ context.Context, query *models.GetDataSourceQuery) error {
query.Result = &models.DataSource{Uid: "1", OrgId: 1, Type: "test", JsonData: simplejson.New()}
return nil
})
queries := []Query{
{
RefID: "A",
+3 -2
View File
@@ -2,6 +2,7 @@ package usagestats
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -29,8 +30,8 @@ func (usm *UsageStatsMock) GetUsageReport(ctx context.Context) (Report, error) {
return Report{Metrics: all}, nil
}
func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, _ string) bool {
return true
func (usm *UsageStatsMock) ShouldBeReported(_ context.Context, s string) bool {
return !strings.HasPrefix(s, "unknown")
}
func (usm *UsageStatsMock) RegisterSendReportCallback(_ SendReportCallbackFunc) {}
+18 -68
View File
@@ -2,48 +2,35 @@ package service
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type UsageStats struct {
Cfg *setting.Cfg
SQLStore sqlstore.Store
pluginStore plugins.Store
SocialService social.Service
kvStore *kvstore.NamespacedKVStore
RouteRegister routing.RouteRegister
pluginStore plugins.Store
log log.Logger
oauthProviders map[string]bool
externalMetrics []usagestats.MetricsFunc
concurrentUserStatsCache memoConcurrentUserStats
startTime time.Time
sendReportCallbacks []usagestats.SendReportCallbackFunc
externalMetrics []usagestats.MetricsFunc
sendReportCallbacks []usagestats.SendReportCallbackFunc
}
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store,
socialService social.Service, kvStore kvstore.KVStore, routeRegister routing.RouteRegister,
) *UsageStats {
func ProvideService(cfg *setting.Cfg, pluginStore plugins.Store, kvStore kvstore.KVStore, routeRegister routing.RouteRegister) *UsageStats {
s := &UsageStats{
Cfg: cfg,
SQLStore: sqlStore,
oauthProviders: socialService.GetOAuthProviders(),
RouteRegister: routeRegister,
pluginStore: pluginStore,
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now(),
Cfg: cfg,
RouteRegister: routeRegister,
pluginStore: pluginStore,
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
}
s.registerAPIEndpoints()
@@ -52,8 +39,6 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
}
func (uss *UsageStats) Run(ctx context.Context) error {
uss.updateTotalStats(ctx)
// try to load last sent time from kv store
lastSent := time.Now()
if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil {
@@ -74,10 +59,8 @@ func (uss *UsageStats) Run(ctx context.Context) error {
}
sendReportTicker := time.NewTicker(nextSendInterval)
updateStatsTicker := time.NewTicker(time.Minute * 30)
defer sendReportTicker.Stop()
defer updateStatsTicker.Stop()
for {
select {
@@ -99,54 +82,21 @@ func (uss *UsageStats) Run(ctx context.Context) error {
for _, callback := range uss.sendReportCallbacks {
callback()
}
case <-updateStatsTicker.C:
uss.updateTotalStats(ctx)
case <-ctx.Done():
return ctx.Err()
}
}
}
type memoConcurrentUserStats struct {
stats *concurrentUsersStats
memoized time.Time
}
const concurrentUserStatsCacheLifetime = time.Hour
func (uss *UsageStats) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) {
memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
return uss.concurrentUserStatsCache.stats, nil
}
uss.concurrentUserStatsCache.stats = &concurrentUsersStats{}
err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
rawSQL := `
SELECT
COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
COUNT(1) AS bucket_le_inf
FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
_, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
}
uss.concurrentUserStatsCache.memoized = time.Now()
return uss.concurrentUserStatsCache.stats, nil
}
func (uss *UsageStats) RegisterSendReportCallback(c usagestats.SendReportCallbackFunc) {
uss.sendReportCallbacks = append(uss.sendReportCallbacks, c)
}
func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool {
ds, exists := uss.pluginStore.Plugin(ctx, dsType)
if !exists {
return false
}
return ds.Signature.IsValid() || ds.Signature.IsInternal()
}
-10
View File
@@ -1,10 +0,0 @@
package service
type concurrentUsersStats struct {
BucketLE3 int32 `xorm:"bucket_le_3"`
BucketLE6 int32 `xorm:"bucket_le_6"`
BucketLE9 int32 `xorm:"bucket_le_9"`
BucketLE12 int32 `xorm:"bucket_le_12"`
BucketLE15 int32 `xorm:"bucket_le_15"`
BucketLEInf int32 `xorm:"bucket_le_inf"`
}
+3 -250
View File
@@ -4,17 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"runtime"
"strings"
"time"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
@@ -38,64 +35,7 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
UsageStatsId: uss.GetUsageStatsId(ctx),
}
statsQuery := models.GetSystemStatsQuery{}
if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil {
uss.log.Error("Failed to get system stats", "error", err)
return report, err
}
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
metrics["stats.users.count"] = statsQuery.Result.Users
metrics["stats.admins.count"] = statsQuery.Result.Admins
metrics["stats.editors.count"] = statsQuery.Result.Editors
metrics["stats.viewers.count"] = statsQuery.Result.Viewers
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
metrics["stats.plugins.apps.count"] = uss.appCount(ctx)
metrics["stats.plugins.panels.count"] = uss.panelCount(ctx)
metrics["stats.plugins.datasources.count"] = uss.dataSourceCount(ctx)
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
metrics["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins
metrics["stats.active_editors.count"] = statsQuery.Result.ActiveEditors
metrics["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers
metrics["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions
metrics["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers
metrics["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers
metrics["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins
metrics["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors
metrics["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers
metrics["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
metrics["stats.stars.count"] = statsQuery.Result.Stars
metrics["stats.folders.count"] = statsQuery.Result.Folders
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
metrics["stats.teams.count"] = statsQuery.Result.Teams
metrics["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens
metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
metrics["stats.annotations.count"] = statsQuery.Result.Annotations
metrics["stats.alert_rules.count"] = statsQuery.Result.AlertRules
metrics["stats.library_panels.count"] = statsQuery.Result.LibraryPanels
metrics["stats.library_variables.count"] = statsQuery.Result.LibraryVariables
metrics["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit
metrics["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin
metrics["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit
metrics["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
metrics["stats.api_keys.count"] = statsQuery.Result.APIKeys
ossEditionCount := 1
enterpriseEditionCount := 0
if uss.Cfg.IsEnterprise {
enterpriseEditionCount = 1
ossEditionCount = 0
}
metrics["stats.edition.oss.count"] = ossEditionCount
metrics["stats.edition.enterprise.count"] = enterpriseEditionCount
uss.registerExternalMetrics(ctx, metrics)
uss.gatherMetrics(ctx, metrics)
// must run after registration of external metrics
if v, ok := metrics["stats.valid_license.count"]; ok {
@@ -104,136 +44,10 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
metrics["stats.valid_license.count"] = 0
}
userCount := statsQuery.Result.Users
avgAuthTokensPerUser := statsQuery.Result.AuthTokens
if userCount != 0 {
avgAuthTokensPerUser /= userCount
}
metrics["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
dsStats := models.GetDataSourceStatsQuery{}
if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil {
uss.log.Error("Failed to get datasource stats", "error", err)
return report, err
}
// send counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsOtherCount := 0
for _, dsStat := range dsStats.Result {
if uss.ShouldBeReported(ctx, dsStat.Type) {
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
} else {
dsOtherCount += dsStat.Count
}
}
metrics["stats.ds.other.count"] = dsOtherCount
esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES}
if err := uss.SQLStore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil {
uss.log.Error("Failed to get elasticsearch json data", "error", err)
return report, err
}
for _, data := range esDataSourcesQuery.Result {
esVersion, err := data.JsonData.Get("esVersion").Int()
if err != nil {
continue
}
statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion)
count, _ := metrics[statName].(int64)
metrics[statName] = count + 1
}
metrics["stats.packaging."+uss.Cfg.Packaging+".count"] = 1
metrics["stats.distributor."+uss.Cfg.ReportingDistributor+".count"] = 1
// fetch datasource access stats
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := uss.SQLStore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil {
uss.log.Error("Failed to get datasource access stats", "error", err)
return report, err
}
// send access counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsAccessOtherCount := make(map[string]int64)
for _, dsAccessStat := range dsAccessStats.Result {
if dsAccessStat.Access == "" {
continue
}
access := strings.ToLower(dsAccessStat.Access)
if uss.ShouldBeReported(ctx, dsAccessStat.Type) {
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
} else {
old := dsAccessOtherCount[access]
dsAccessOtherCount[access] = old + dsAccessStat.Count
}
}
for access, count := range dsAccessOtherCount {
metrics["stats.ds_access.other."+access+".count"] = count
}
// get stats about alert notifier usage
anStats := models.GetAlertNotifierUsageStatsQuery{}
if err := uss.SQLStore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil {
uss.log.Error("Failed to get alert notification stats", "error", err)
return report, err
}
for _, stats := range anStats.Result {
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
}
// Add stats about auth configuration
authTypes := map[string]bool{}
authTypes["anonymous"] = uss.Cfg.AnonymousEnabled
authTypes["basic_auth"] = uss.Cfg.BasicAuthEnabled
authTypes["ldap"] = uss.Cfg.LDAPEnabled
authTypes["auth_proxy"] = uss.Cfg.AuthProxyEnabled
for provider, enabled := range uss.oauthProviders {
authTypes["oauth_"+provider] = enabled
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
}
// Get concurrent users stats as histogram
concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx)
if err != nil {
uss.log.Error("Failed to get concurrent users stats", "error", err)
return report, err
}
// Histogram is cumulative and metric name has a postfix of le_"<upper inclusive bound>"
metrics["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3
metrics["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6
metrics["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9
metrics["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12
metrics["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15
metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf
metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds())
return report, nil
}
func (uss *UsageStats) registerExternalMetrics(ctx context.Context, metrics map[string]interface{}) {
func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]interface{}) {
for _, fn := range uss.externalMetrics {
fnMetrics, err := fn(ctx)
if err != nil {
@@ -290,55 +104,6 @@ var sendUsageStats = func(uss *UsageStats, data *bytes.Buffer) {
}()
}
func (uss *UsageStats) updateTotalStats(ctx context.Context) {
if !uss.Cfg.MetricsEndpointEnabled || uss.Cfg.MetricsEndpointDisableTotalStats {
return
}
statsQuery := models.GetSystemStatsQuery{}
if err := uss.SQLStore.GetSystemStats(ctx, &statsQuery); err != nil {
uss.log.Error("Failed to get system stats", "error", err)
return
}
metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards))
metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders))
metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users))
metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers))
metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists))
metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs))
metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers))
metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers))
metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors))
metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors))
metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins))
metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins))
metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
dsStats := models.GetDataSourceStatsQuery{}
if err := uss.SQLStore.GetDataSourceStats(ctx, &dsStats); err != nil {
uss.log.Error("Failed to get datasource stats", "error", err)
return
}
for _, dsStat := range dsStats.Result {
metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count))
}
}
func (uss *UsageStats) ShouldBeReported(ctx context.Context, dsType string) bool {
ds, exists := uss.pluginStore.Plugin(ctx, dsType)
if !exists {
return false
}
return ds.Signature.IsValid() || ds.Signature.IsInternal()
}
func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
anonId, ok, err := uss.kvStore.Get(ctx, "anonymous_id")
if err != nil {
@@ -366,15 +131,3 @@ func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
return anonId
}
func (uss *UsageStats) appCount(ctx context.Context) int {
return len(uss.pluginStore.Plugins(ctx, plugins.App))
}
func (uss *UsageStats) panelCount(ctx context.Context) int {
return len(uss.pluginStore.Plugins(ctx, plugins.Panel))
}
func (uss *UsageStats) dataSourceCount(ctx context.Context) int {
return len(uss.pluginStore.Plugins(ctx, plugins.DataSource))
}
+133 -406
View File
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
@@ -11,18 +12,16 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// This is to ensure that the interface contract is held by the implementation
@@ -37,411 +36,163 @@ func Test_InterfaceContractValidity(t *testing.T) {
}
func TestMetrics(t *testing.T) {
t.Run("When sending usage stats", func(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
const metricName = "stats.test_metric.count"
sqlStore.ExpectedSystemStats = &models.SystemStats{
Dashboards: 1,
Datasources: 2,
Users: 3,
Admins: 31,
Editors: 32,
Viewers: 33,
ActiveUsers: 4,
ActiveAdmins: 21,
ActiveEditors: 22,
ActiveViewers: 23,
ActiveSessions: 24,
DailyActiveUsers: 25,
DailyActiveAdmins: 26,
DailyActiveEditors: 27,
DailyActiveViewers: 28,
DailyActiveSessions: 29,
Orgs: 5,
Playlists: 6,
Alerts: 7,
Stars: 8,
Folders: 9,
DashboardPermissions: 10,
FolderPermissions: 11,
ProvisionedDashboards: 12,
Snapshots: 13,
Teams: 14,
AuthTokens: 15,
DashboardVersions: 16,
Annotations: 17,
AlertRules: 18,
LibraryPanels: 19,
LibraryVariables: 20,
DashboardsViewersCanAdmin: 3,
DashboardsViewersCanEdit: 2,
FoldersViewersCanAdmin: 1,
FoldersViewersCanEdit: 5,
APIKeys: 2,
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{metricName: 1}, nil
})
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) {
origSendUsageStats := sendUsageStats
t.Cleanup(func() {
sendUsageStats = origSendUsageStats
})
statsSent := false
sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) {
statsSent = true
}
setupSomeDataSourcePlugins(t, uss)
uss.Cfg.ReportingEnabled = false
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{
{
Type: models.DS_ES,
Count: 9,
},
{
Type: models.DS_PROMETHEUS,
Count: 10,
},
{
Type: "unknown_ds",
Count: 11,
},
{
Type: "unknown_ds2",
Count: 12,
},
require.False(t, statsSent)
})
t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) {
origCfg := uss.Cfg
t.Cleanup(func() {
uss.Cfg = origCfg
})
uss.Cfg = &setting.Cfg{
ReportingEnabled: true,
BuildVersion: "5.0.0",
AnonymousEnabled: true,
BasicAuthEnabled: true,
LDAPEnabled: true,
AuthProxyEnabled: true,
Packaging: "deb",
ReportingDistributor: "hosted-grafana",
}
sqlStore.ExpectedDataSources = []*models.DataSource{
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
}),
},
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
}),
},
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 70,
}),
},
}
sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{
{
Type: models.DS_ES,
Access: "direct",
Count: 1,
},
{
Type: models.DS_ES,
Access: "proxy",
Count: 2,
},
{
Type: models.DS_PROMETHEUS,
Access: "proxy",
Count: 3,
},
{
Type: "unknown_ds",
Access: "proxy",
Count: 4,
},
{
Type: "unknown_ds2",
Access: "",
Count: 5,
},
{
Type: "unknown_ds3",
Access: "direct",
Count: 6,
},
{
Type: "unknown_ds4",
Access: "direct",
Count: 7,
},
{
Type: "unknown_ds5",
Access: "proxy",
Count: 8,
},
}
sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{
{
Type: "slack",
Count: 1,
},
{
Type: "webhook",
Count: 2,
},
}
uss.SQLStore = sqlStore
createConcurrentTokens(t, uss.SQLStore)
uss.oauthProviders = map[string]bool{
"github": true,
"gitlab": true,
"azuread": true,
"google": true,
"generic_oauth": true,
"grafana_com": true,
}
ch := make(chan httpResp)
ticker := time.NewTicker(2 * time.Second)
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Logf("Fake HTTP handler received an error: %s", err.Error())
ch <- httpResp{
err: err,
}
return
}
require.NoError(t, err, "Failed to read response body, err=%v", err)
t.Logf("Fake HTTP handler received a response")
ch <- httpResp{
responseBuffer: bytes.NewBuffer(buf),
req: r,
}
}))
t.Cleanup(ts.Close)
t.Cleanup(func() {
close(ch)
})
usageStatsURL = ts.URL
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
t.Run("Given reporting not enabled and sending usage stats", func(t *testing.T) {
origSendUsageStats := sendUsageStats
t.Cleanup(func() {
sendUsageStats = origSendUsageStats
})
statsSent := false
sendUsageStats = func(uss *UsageStats, b *bytes.Buffer) {
statsSent = true
}
// Wait for fake HTTP server to receive a request
var resp httpResp
select {
case resp = <-ch:
require.NoError(t, resp.err, "Fake server experienced an error")
case <-ticker.C:
t.Fatalf("Timed out waiting for HTTP request")
}
uss.Cfg.ReportingEnabled = false
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
t.Logf("Received response from fake HTTP server: %+v\n", resp)
require.False(t, statsSent)
})
assert.NotNil(t, resp.req)
t.Run("Given reporting enabled, stats should be gathered and sent to HTTP endpoint", func(t *testing.T) {
origCfg := uss.Cfg
t.Cleanup(func() {
uss.Cfg = origCfg
})
uss.Cfg = &setting.Cfg{
ReportingEnabled: true,
BuildVersion: "5.0.0",
AnonymousEnabled: true,
BasicAuthEnabled: true,
LDAPEnabled: true,
AuthProxyEnabled: true,
Packaging: "deb",
ReportingDistributor: "hosted-grafana",
}
assert.Equal(t, http.MethodPost, resp.req.Method)
assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type"))
ch := make(chan httpResp)
ticker := time.NewTicker(2 * time.Second)
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Logf("Fake HTTP handler received an error: %s", err.Error())
ch <- httpResp{
err: err,
}
return
}
require.NoError(t, err, "Failed to read response body, err=%v", err)
t.Logf("Fake HTTP handler received a response")
ch <- httpResp{
responseBuffer: bytes.NewBuffer(buf),
req: r,
}
}))
t.Cleanup(ts.Close)
t.Cleanup(func() {
close(ch)
})
usageStatsURL = ts.URL
require.NotNil(t, resp.responseBuffer)
err := uss.sendUsageStats(context.Background())
require.NoError(t, err)
j := make(map[string]interface{})
err = json.Unmarshal(resp.responseBuffer.Bytes(), &j)
require.NoError(t, err)
// Wait for fake HTTP server to receive a request
var resp httpResp
select {
case resp = <-ch:
require.NoError(t, resp.err, "Fake server experienced an error")
case <-ticker.C:
t.Fatalf("Timed out waiting for HTTP request")
}
assert.Equal(t, "5_0_0", j["version"])
assert.Equal(t, runtime.GOOS, j["os"])
assert.Equal(t, runtime.GOARCH, j["arch"])
t.Logf("Received response from fake HTTP server: %+v\n", resp)
usageId := uss.GetUsageStatsId(context.Background())
assert.NotEmpty(t, usageId)
assert.NotNil(t, resp.req)
metrics, ok := j["metrics"].(map[string]interface{})
require.True(t, ok)
assert.EqualValues(t, 1, metrics[metricName])
})
}
assert.Equal(t, http.MethodPost, resp.req.Method)
assert.Equal(t, "application/json", resp.req.Header.Get("Content-Type"))
func TestGetUsageReport_IncludesMetrics(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, true)
metricName := "stats.test_metric.count"
require.NotNil(t, resp.responseBuffer)
j, err := simplejson.NewFromReader(resp.responseBuffer)
require.NoError(t, err)
assert.Equal(t, "5_0_0", j.Get("version").MustString())
assert.Equal(t, runtime.GOOS, j.Get("os").MustString())
assert.Equal(t, runtime.GOARCH, j.Get("arch").MustString())
usageId := uss.GetUsageStatsId(context.Background())
assert.NotEmpty(t, usageId)
metrics := j.Get("metrics")
assert.Equal(t, 15, metrics.Get("stats.total_auth_token.count").MustInt())
assert.Equal(t, 2, metrics.Get("stats.api_keys.count").MustInt())
assert.Equal(t, 5, metrics.Get("stats.avg_auth_token_per_user.count").MustInt())
assert.Equal(t, 16, metrics.Get("stats.dashboard_versions.count").MustInt())
assert.Equal(t, 17, metrics.Get("stats.annotations.count").MustInt())
assert.Equal(t, 18, metrics.Get("stats.alert_rules.count").MustInt())
assert.Equal(t, 19, metrics.Get("stats.library_panels.count").MustInt())
assert.Equal(t, 20, metrics.Get("stats.library_variables.count").MustInt())
assert.Equal(t, 0, metrics.Get("stats.live_users.count").MustInt())
assert.Equal(t, 0, metrics.Get("stats.live_clients.count").MustInt())
assert.Equal(t, 9, metrics.Get("stats.ds."+models.DS_ES+".count").MustInt())
assert.Equal(t, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt())
assert.Equal(t, 11+12, metrics.Get("stats.ds.other.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.ds_access."+models.DS_ES+".direct.count").MustInt())
assert.Equal(t, 2, metrics.Get("stats.ds_access."+models.DS_ES+".proxy.count").MustInt())
assert.Equal(t, 3, metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt())
assert.Equal(t, 6+7, metrics.Get("stats.ds_access.other.direct.count").MustInt())
assert.Equal(t, 4+8, metrics.Get("stats.ds_access.other.proxy.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.alert_notifiers.slack.count").MustInt())
assert.Equal(t, 2, metrics.Get("stats.alert_notifiers.webhook.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.anonymous.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.basic_auth.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.ldap.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_github.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_google.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_azuread.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.packaging.deb.count").MustInt())
assert.Equal(t, 1, metrics.Get("stats.distributor.hosted-grafana.count").MustInt())
assert.LessOrEqual(t, 60, metrics.Get("stats.uptime").MustInt())
assert.Greater(t, 70, metrics.Get("stats.uptime").MustInt())
})
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{metricName: 1}, nil
})
t.Run("When updating total stats", func(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = false
report, err := uss.GetUsageReport(context.Background())
require.NoError(t, err, "Expected no error")
sqlStore.ExpectedSystemStats = &models.SystemStats{}
metric := report.Metrics[metricName]
assert.Equal(t, 1, metric)
}
t.Run("When metrics is disabled and total stats is enabled, stats should not be updated", func(t *testing.T) {
uss.Cfg.MetricsEndpointEnabled = false
uss.Cfg.MetricsEndpointDisableTotalStats = false
uss.updateTotalStats(context.Background())
})
func TestRegisterMetrics(t *testing.T) {
const goodMetricName = "stats.test_external_metric.count"
t.Run("When metrics is enabled and total stats is disabled, stats should not be updated", func(t *testing.T) {
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = true
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2}
uss.updateTotalStats(context.Background())
})
t.Run("When metrics is disabled and total stats is disabled, stats should not be updated", func(t *testing.T) {
uss.Cfg.MetricsEndpointEnabled = false
uss.Cfg.MetricsEndpointDisableTotalStats = true
uss.updateTotalStats(context.Background())
})
t.Run("When metrics is enabled and total stats is enabled, stats should be updated", func(t *testing.T) {
uss.Cfg.MetricsEndpointEnabled = true
uss.Cfg.MetricsEndpointDisableTotalStats = false
uss.updateTotalStats(context.Background())
})
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{goodMetricName: 1}, nil
})
t.Run("When registering a metric", func(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
metricName := "stats.test_metric.count"
{
extMetrics, err := uss.externalMetrics[0](context.Background())
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{goodMetricName: 1}, extMetrics)
}
t.Run("Adds a new metric to the external metrics", func(t *testing.T) {
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{metricName: 1}, nil
})
uss.gatherMetrics(context.Background(), metrics)
assert.Equal(t, 1, metrics[goodMetricName])
metrics, err := uss.externalMetrics[0](context.Background())
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{metricName: 1}, metrics)
})
})
t.Run("When getting usage report", func(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, true)
metricName := "stats.test_metric.count"
createConcurrentTokens(t, uss.SQLStore)
t.Run("Should include metrics for concurrent users", func(t *testing.T) {
report, err := uss.GetUsageReport(context.Background())
require.NoError(t, err)
assert.Equal(t, int32(1), report.Metrics["stats.auth_token_per_user_le_3"])
assert.Equal(t, int32(2), report.Metrics["stats.auth_token_per_user_le_6"])
assert.Equal(t, int32(3), report.Metrics["stats.auth_token_per_user_le_9"])
assert.Equal(t, int32(4), report.Metrics["stats.auth_token_per_user_le_12"])
assert.Equal(t, int32(5), report.Metrics["stats.auth_token_per_user_le_15"])
assert.Equal(t, int32(6), report.Metrics["stats.auth_token_per_user_le_inf"])
})
t.Run("Should include external metrics", func(t *testing.T) {
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{metricName: 1}, nil
})
report, err := uss.GetUsageReport(context.Background())
require.NoError(t, err, "Expected no error")
metric := report.Metrics[metricName]
assert.Equal(t, 1, metric)
})
})
t.Run("When registering external metrics", func(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
uss := createService(t, setting.Cfg{}, sqlStore, false)
metrics := map[string]interface{}{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2}
extMetricName := "stats.test_external_metric.count"
t.Run("do not add metrics that return an error when fetched", func(t *testing.T) {
const badMetricName = "stats.test_external_metric_error.count"
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{extMetricName: 1}, nil
return map[string]interface{}{badMetricName: 1}, errors.New("some error")
})
uss.gatherMetrics(context.Background(), metrics)
uss.registerExternalMetrics(context.Background(), metrics)
extErrorMetric := metrics[badMetricName]
extMetric := metrics[goodMetricName]
assert.Equal(t, 1, metrics[extMetricName])
t.Run("When loading a metric results to an error", func(t *testing.T) {
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{extMetricName: 1}, nil
})
extErrorMetricName := "stats.test_external_metric_error.count"
t.Run("Should not add it to metrics", func(t *testing.T) {
uss.RegisterMetricsFunc(func(context.Context) (map[string]interface{}, error) {
return map[string]interface{}{extErrorMetricName: 1}, errors.New("some error")
})
uss.registerExternalMetrics(context.Background(), metrics)
extErrorMetric := metrics[extErrorMetricName]
extMetric := metrics[extMetricName]
require.Nil(t, extErrorMetric, "Invalid metric should not be added")
assert.Equal(t, 1, extMetric)
assert.Len(t, metrics, 3, "Expected only one available metric")
})
})
require.Nil(t, extErrorMetric, "Invalid metric should not be added")
assert.Equal(t, 1, extMetric)
assert.Len(t, metrics, 3, "Expected only one available metric")
})
}
@@ -470,27 +221,6 @@ func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type
return result
}
func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) {
t.Helper()
uss.pluginStore = &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
models.DS_ES: {
Signature: "internal",
},
models.DS_PROMETHEUS: {
Signature: "internal",
},
models.DS_GRAPHITE: {
Signature: "internal",
},
models.DS_MYSQL: {
Signature: "internal",
},
},
}
}
type httpResp struct {
req *http.Request
responseBuffer *bytes.Buffer
@@ -502,14 +232,11 @@ func createService(t *testing.T, cfg setting.Cfg, sqlStore sqlstore.Store, withD
if withDB {
sqlStore = sqlstore.InitTestDB(t)
}
return &UsageStats{
Cfg: &cfg,
SQLStore: sqlStore,
externalMetrics: make([]usagestats.MetricsFunc, 0),
pluginStore: &fakePluginStore{},
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now().Add(-1 * time.Minute),
RouteRegister: routing.NewRouteRegister(),
}
return ProvideService(
&cfg,
&fakePluginStore{},
kvstore.ProvideService(sqlStore),
routing.NewRouteRegister(),
)
}
@@ -0,0 +1,58 @@
package statscollector
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
const concurrentUserStatsCacheLifetime = time.Hour
type concurrentUsersStats struct {
BucketLE3 int32 `xorm:"bucket_le_3"`
BucketLE6 int32 `xorm:"bucket_le_6"`
BucketLE9 int32 `xorm:"bucket_le_9"`
BucketLE12 int32 `xorm:"bucket_le_12"`
BucketLE15 int32 `xorm:"bucket_le_15"`
BucketLEInf int32 `xorm:"bucket_le_inf"`
}
type memoConcurrentUserStats struct {
stats *concurrentUsersStats
memoized time.Time
}
func (s *Service) concurrentUsers(ctx context.Context) (*concurrentUsersStats, error) {
memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
if !s.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
return s.concurrentUserStatsCache.stats, nil
}
s.concurrentUserStatsCache.stats = &concurrentUsersStats{}
err := s.sqlstore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
rawSQL := `
SELECT
COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
COUNT(1) AS bucket_le_inf
FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
_, err := sess.SQL(rawSQL).Get(s.concurrentUserStatsCache.stats)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
}
s.concurrentUserStatsCache.memoized = time.Now()
return s.concurrentUserStatsCache.stats, nil
}
@@ -1,4 +1,4 @@
package service
package statscollector
import (
"context"
@@ -8,21 +8,35 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
func TestConcurrentUsersMetrics(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
uss := &UsageStats{
SQLStore: sqlStore,
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
}
s := createService(t, setting.NewCfg(), sqlStore)
createConcurrentTokens(t, sqlStore)
stats, err := s.collect(context.Background())
require.NoError(t, err)
assert.Equal(t, int32(1), stats["stats.auth_token_per_user_le_3"])
assert.Equal(t, int32(2), stats["stats.auth_token_per_user_le_6"])
assert.Equal(t, int32(3), stats["stats.auth_token_per_user_le_9"])
assert.Equal(t, int32(4), stats["stats.auth_token_per_user_le_12"])
assert.Equal(t, int32(5), stats["stats.auth_token_per_user_le_15"])
assert.Equal(t, int32(6), stats["stats.auth_token_per_user_le_inf"])
}
func TestConcurrentUsersStats(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
s := createService(t, setting.NewCfg(), sqlStore)
createConcurrentTokens(t, sqlStore)
@@ -31,10 +45,10 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
cancel()
})
actualResult, err := uss.GetConcurrentUsersStats(ctx)
actualResult, err := s.concurrentUsers(ctx)
require.NoError(t, err)
expectedCachedResult := &concurrentUsersStats{
expectedResult := &concurrentUsersStats{
BucketLE3: 1,
BucketLE6: 2,
BucketLE9: 3,
@@ -42,14 +56,39 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
BucketLE15: 5,
BucketLEInf: 6,
}
assert.Equal(t, expectedCachedResult, actualResult)
assert.Equal(t, expectedResult, actualResult)
createToken(t, 8, sqlStore)
require.NoError(t, err)
actualResult, err = uss.GetConcurrentUsersStats(ctx)
// Cached, new token is not reflected yet.
actualResult, err = s.concurrentUsers(ctx)
require.NoError(t, err)
assert.Equal(t, expectedCachedResult, actualResult)
assert.Equal(t, expectedResult, actualResult)
// Bust cache
s.concurrentUserStatsCache = memoConcurrentUserStats{}
expectedResult = &concurrentUsersStats{
BucketLE3: 2,
BucketLE6: 3,
BucketLE9: 4,
BucketLE12: 5,
BucketLE15: 6,
BucketLEInf: 7,
}
actualResult, err = s.concurrentUsers(ctx)
require.NoError(t, err)
assert.Equal(t, expectedResult, actualResult)
}
func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) {
t.Helper()
for u := 1; u <= 6; u++ {
for tkn := 1; tkn <= u*3; tkn++ {
createToken(t, u, sqlStore)
}
}
}
func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) {
@@ -83,15 +122,6 @@ func createToken(t *testing.T, uID int, sqlStore sqlstore.Store) {
require.NoError(t, err)
}
func createConcurrentTokens(t *testing.T, sqlStore sqlstore.Store) {
t.Helper()
for u := 1; u <= 6; u++ {
for tkn := 1; tkn <= u*3; tkn++ {
createToken(t, u, sqlStore)
}
}
}
type userAuthToken struct {
UserID int64 `xorm:"user_id"`
AuthToken string
@@ -0,0 +1,321 @@
package statscollector
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
cfg *setting.Cfg
sqlstore sqlstore.Store
plugins plugins.Store
social social.Service
usageStats usagestats.Service
features *featuremgmt.FeatureManager
log log.Logger
startTime time.Time
concurrentUserStatsCache memoConcurrentUserStats
}
func ProvideService(
usagestats usagestats.Service,
cfg *setting.Cfg,
store sqlstore.Store,
social social.Service,
plugins plugins.Store,
features *featuremgmt.FeatureManager,
) *Service {
s := &Service{
cfg: cfg,
sqlstore: store,
plugins: plugins,
social: social,
usageStats: usagestats,
features: features,
startTime: time.Now(),
log: log.New("infra.usagestats.collector"),
}
usagestats.RegisterMetricsFunc(s.collect)
return s
}
func (s *Service) Run(ctx context.Context) error {
s.updateTotalStats(ctx)
updateStatsTicker := time.NewTicker(time.Minute * 30)
defer updateStatsTicker.Stop()
for {
select {
case <-updateStatsTicker.C:
s.updateTotalStats(ctx)
case <-ctx.Done():
return ctx.Err()
}
}
}
func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) {
m := map[string]interface{}{}
statsQuery := models.GetSystemStatsQuery{}
if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil {
s.log.Error("Failed to get system stats", "error", err)
return nil, err
}
m["stats.dashboards.count"] = statsQuery.Result.Dashboards
m["stats.users.count"] = statsQuery.Result.Users
m["stats.admins.count"] = statsQuery.Result.Admins
m["stats.editors.count"] = statsQuery.Result.Editors
m["stats.viewers.count"] = statsQuery.Result.Viewers
m["stats.orgs.count"] = statsQuery.Result.Orgs
m["stats.playlist.count"] = statsQuery.Result.Playlists
m["stats.plugins.apps.count"] = s.appCount(ctx)
m["stats.plugins.panels.count"] = s.panelCount(ctx)
m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx)
m["stats.alerts.count"] = statsQuery.Result.Alerts
m["stats.active_users.count"] = statsQuery.Result.ActiveUsers
m["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins
m["stats.active_editors.count"] = statsQuery.Result.ActiveEditors
m["stats.active_viewers.count"] = statsQuery.Result.ActiveViewers
m["stats.active_sessions.count"] = statsQuery.Result.ActiveSessions
m["stats.monthly_active_users.count"] = statsQuery.Result.MonthlyActiveUsers
m["stats.daily_active_users.count"] = statsQuery.Result.DailyActiveUsers
m["stats.daily_active_admins.count"] = statsQuery.Result.DailyActiveAdmins
m["stats.daily_active_editors.count"] = statsQuery.Result.DailyActiveEditors
m["stats.daily_active_viewers.count"] = statsQuery.Result.DailyActiveViewers
m["stats.daily_active_sessions.count"] = statsQuery.Result.DailyActiveSessions
m["stats.datasources.count"] = statsQuery.Result.Datasources
m["stats.stars.count"] = statsQuery.Result.Stars
m["stats.folders.count"] = statsQuery.Result.Folders
m["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
m["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
m["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
m["stats.snapshots.count"] = statsQuery.Result.Snapshots
m["stats.teams.count"] = statsQuery.Result.Teams
m["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens
m["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
m["stats.annotations.count"] = statsQuery.Result.Annotations
m["stats.alert_rules.count"] = statsQuery.Result.AlertRules
m["stats.library_panels.count"] = statsQuery.Result.LibraryPanels
m["stats.library_variables.count"] = statsQuery.Result.LibraryVariables
m["stats.dashboards_viewers_can_edit.count"] = statsQuery.Result.DashboardsViewersCanEdit
m["stats.dashboards_viewers_can_admin.count"] = statsQuery.Result.DashboardsViewersCanAdmin
m["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit
m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
m["stats.api_keys.count"] = statsQuery.Result.APIKeys
ossEditionCount := 1
enterpriseEditionCount := 0
if s.cfg.IsEnterprise {
enterpriseEditionCount = 1
ossEditionCount = 0
}
m["stats.edition.oss.count"] = ossEditionCount
m["stats.edition.enterprise.count"] = enterpriseEditionCount
userCount := statsQuery.Result.Users
avgAuthTokensPerUser := statsQuery.Result.AuthTokens
if userCount != 0 {
avgAuthTokensPerUser /= userCount
}
m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
dsStats := models.GetDataSourceStatsQuery{}
if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {
s.log.Error("Failed to get datasource stats", "error", err)
return nil, err
}
// send counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsOtherCount := 0
for _, dsStat := range dsStats.Result {
if s.usageStats.ShouldBeReported(ctx, dsStat.Type) {
m["stats.ds."+dsStat.Type+".count"] = dsStat.Count
} else {
dsOtherCount += dsStat.Count
}
}
m["stats.ds.other.count"] = dsOtherCount
esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES}
if err := s.sqlstore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil {
s.log.Error("Failed to get elasticsearch json data", "error", err)
return nil, err
}
for _, data := range esDataSourcesQuery.Result {
esVersion, err := data.JsonData.Get("esVersion").Int()
if err != nil {
continue
}
statName := fmt.Sprintf("stats.ds.elasticsearch.v%d.count", esVersion)
count, _ := m[statName].(int64)
m[statName] = count + 1
}
m["stats.packaging."+s.cfg.Packaging+".count"] = 1
m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1
// fetch datasource access stats
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := s.sqlstore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil {
s.log.Error("Failed to get datasource access stats", "error", err)
return nil, err
}
// send access counters for each data source
// but ignore any custom data sources
// as sending that name could be sensitive information
dsAccessOtherCount := make(map[string]int64)
for _, dsAccessStat := range dsAccessStats.Result {
if dsAccessStat.Access == "" {
continue
}
access := strings.ToLower(dsAccessStat.Access)
if s.usageStats.ShouldBeReported(ctx, dsAccessStat.Type) {
m["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
} else {
old := dsAccessOtherCount[access]
dsAccessOtherCount[access] = old + dsAccessStat.Count
}
}
for access, count := range dsAccessOtherCount {
m["stats.ds_access.other."+access+".count"] = count
}
// get stats about alert notifier usage
anStats := models.GetAlertNotifierUsageStatsQuery{}
if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil {
s.log.Error("Failed to get alert notification stats", "error", err)
return nil, err
}
for _, stats := range anStats.Result {
m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
}
// Add stats about auth configuration
authTypes := map[string]bool{}
authTypes["anonymous"] = s.cfg.AnonymousEnabled
authTypes["basic_auth"] = s.cfg.BasicAuthEnabled
authTypes["ldap"] = s.cfg.LDAPEnabled
authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled
for provider, enabled := range s.social.GetOAuthProviders() {
authTypes["oauth_"+provider] = enabled
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
m["stats.auth_enabled."+authType+".count"] = enabledValue
}
// Get concurrent users stats as histogram
concurrentUsersStats, err := s.concurrentUsers(ctx)
if err != nil {
s.log.Error("Failed to get concurrent users stats", "error", err)
return nil, err
}
// Histogram is cumulative and metric name has a postfix of le_"<upper inclusive bound>"
m["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3
m["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6
m["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9
m["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12
m["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15
m["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf
m["stats.uptime"] = int64(time.Since(s.startTime).Seconds())
featureUsageStats := s.features.GetUsageStats(ctx)
for k, v := range featureUsageStats {
m[k] = v
}
return m, nil
}
func (s *Service) updateTotalStats(ctx context.Context) bool {
if !s.cfg.MetricsEndpointEnabled || s.cfg.MetricsEndpointDisableTotalStats {
return false
}
statsQuery := models.GetSystemStatsQuery{}
if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil {
s.log.Error("Failed to get system stats", "error", err)
return false
}
metrics.MStatTotalDashboards.Set(float64(statsQuery.Result.Dashboards))
metrics.MStatTotalFolders.Set(float64(statsQuery.Result.Folders))
metrics.MStatTotalUsers.Set(float64(statsQuery.Result.Users))
metrics.MStatActiveUsers.Set(float64(statsQuery.Result.ActiveUsers))
metrics.MStatTotalPlaylists.Set(float64(statsQuery.Result.Playlists))
metrics.MStatTotalOrgs.Set(float64(statsQuery.Result.Orgs))
metrics.StatsTotalViewers.Set(float64(statsQuery.Result.Viewers))
metrics.StatsTotalActiveViewers.Set(float64(statsQuery.Result.ActiveViewers))
metrics.StatsTotalEditors.Set(float64(statsQuery.Result.Editors))
metrics.StatsTotalActiveEditors.Set(float64(statsQuery.Result.ActiveEditors))
metrics.StatsTotalAdmins.Set(float64(statsQuery.Result.Admins))
metrics.StatsTotalActiveAdmins.Set(float64(statsQuery.Result.ActiveAdmins))
metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
dsStats := models.GetDataSourceStatsQuery{}
if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {
s.log.Error("Failed to get datasource stats", "error", err)
return true
}
for _, dsStat := range dsStats.Result {
metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count))
}
return true
}
func (s *Service) appCount(ctx context.Context) int {
return len(s.plugins.Plugins(ctx, plugins.App))
}
func (s *Service) panelCount(ctx context.Context) int {
return len(s.plugins.Plugins(ctx, plugins.Panel))
}
func (s *Service) dataSourceCount(ctx context.Context) int {
return len(s.plugins.Plugins(ctx, plugins.DataSource))
}
@@ -0,0 +1,362 @@
package statscollector
import (
"context"
"fmt"
"testing"
"time"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
)
func TestTotalStatsUpdate(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
s := createService(t, setting.NewCfg(), sqlStore)
s.cfg.MetricsEndpointEnabled = true
s.cfg.MetricsEndpointDisableTotalStats = false
sqlStore.ExpectedSystemStats = &models.SystemStats{}
tests := []struct {
MetricsEndpointEnabled bool
MetricsEndpointDisableTotalStats bool
ExpectedUpdate bool
}{
{
MetricsEndpointEnabled: false,
MetricsEndpointDisableTotalStats: false,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: false,
MetricsEndpointDisableTotalStats: true,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: true,
MetricsEndpointDisableTotalStats: true,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: true,
MetricsEndpointDisableTotalStats: false,
ExpectedUpdate: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf(
"metricsEnabled(%v) * totalStatsDisabled(%v) = %v",
tc.MetricsEndpointEnabled,
tc.MetricsEndpointDisableTotalStats,
tc.ExpectedUpdate,
), func(t *testing.T) {
s.cfg.MetricsEndpointEnabled = tc.MetricsEndpointEnabled
s.cfg.MetricsEndpointDisableTotalStats = tc.MetricsEndpointDisableTotalStats
assert.Equal(t, tc.ExpectedUpdate, s.updateTotalStats(context.Background()))
})
}
}
func TestFeatureUsageStats(t *testing.T) {
store := mockstore.NewSQLStoreMock()
mockSystemStats(store)
s := createService(t, setting.NewCfg(), store)
m, err := s.collect(context.Background())
require.NoError(t, err, "Expected no error")
assert.Equal(t, 1, m["stats.features.feature_1.count"])
assert.Equal(t, 1, m["stats.features.feature_2.count"])
}
func TestCollectingUsageStats(t *testing.T) {
sqlStore := mockstore.NewSQLStoreMock()
s := createService(t, &setting.Cfg{
ReportingEnabled: true,
BuildVersion: "5.0.0",
AnonymousEnabled: true,
BasicAuthEnabled: true,
LDAPEnabled: true,
AuthProxyEnabled: true,
Packaging: "deb",
ReportingDistributor: "hosted-grafana",
}, sqlStore)
s.startTime = time.Now().Add(-1 * time.Minute)
mockSystemStats(sqlStore)
setupSomeDataSourcePlugins(t, s)
sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{
{
Type: models.DS_ES,
Count: 9,
},
{
Type: models.DS_PROMETHEUS,
Count: 10,
},
{
Type: "unknown_ds",
Count: 11,
},
{
Type: "unknown_ds2",
Count: 12,
},
}
sqlStore.ExpectedDataSources = []*models.DataSource{
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
}),
},
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
}),
},
{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 70,
}),
},
}
sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{
{
Type: models.DS_ES,
Access: "direct",
Count: 1,
},
{
Type: models.DS_ES,
Access: "proxy",
Count: 2,
},
{
Type: models.DS_PROMETHEUS,
Access: "proxy",
Count: 3,
},
{
Type: "unknown_ds",
Access: "proxy",
Count: 4,
},
{
Type: "unknown_ds2",
Access: "",
Count: 5,
},
{
Type: "unknown_ds3",
Access: "direct",
Count: 6,
},
{
Type: "unknown_ds4",
Access: "direct",
Count: 7,
},
{
Type: "unknown_ds5",
Access: "proxy",
Count: 8,
},
}
sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{
{
Type: "slack",
Count: 1,
},
{
Type: "webhook",
Count: 2,
},
}
createConcurrentTokens(t, sqlStore)
s.social = &mockSocial{
OAuthProviders: map[string]bool{
"github": true,
"gitlab": true,
"azuread": true,
"google": true,
"generic_oauth": true,
"grafana_com": true,
},
}
metrics, err := s.collect(context.Background())
require.NoError(t, err)
assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"])
assert.EqualValues(t, 2, metrics["stats.api_keys.count"])
assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"])
assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"])
assert.EqualValues(t, 17, metrics["stats.annotations.count"])
assert.EqualValues(t, 18, metrics["stats.alert_rules.count"])
assert.EqualValues(t, 19, metrics["stats.library_panels.count"])
assert.EqualValues(t, 20, metrics["stats.library_variables.count"])
assert.EqualValues(t, 9, metrics["stats.ds."+models.DS_ES+".count"])
assert.EqualValues(t, 10, metrics["stats.ds."+models.DS_PROMETHEUS+".count"])
assert.EqualValues(t, 11+12, metrics["stats.ds.other.count"])
assert.EqualValues(t, 1, metrics["stats.ds_access."+models.DS_ES+".direct.count"])
assert.EqualValues(t, 2, metrics["stats.ds_access."+models.DS_ES+".proxy.count"])
assert.EqualValues(t, 3, metrics["stats.ds_access."+models.DS_PROMETHEUS+".proxy.count"])
assert.EqualValues(t, 6+7, metrics["stats.ds_access.other.direct.count"])
assert.EqualValues(t, 4+8, metrics["stats.ds_access.other.proxy.count"])
assert.EqualValues(t, 1, metrics["stats.alert_notifiers.slack.count"])
assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.anonymous.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.basic_auth.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.ldap.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.auth_proxy.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_github.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_gitlab.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_google.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_azuread.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_generic_oauth.count"])
assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_grafana_com.count"])
assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"])
assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"])
assert.InDelta(t, int64(65), metrics["stats.uptime"], 6)
}
func mockSystemStats(sqlStore *mockstore.SQLStoreMock) {
sqlStore.ExpectedSystemStats = &models.SystemStats{
Dashboards: 1,
Datasources: 2,
Users: 3,
Admins: 31,
Editors: 32,
Viewers: 33,
ActiveUsers: 4,
ActiveAdmins: 21,
ActiveEditors: 22,
ActiveViewers: 23,
ActiveSessions: 24,
DailyActiveUsers: 25,
DailyActiveAdmins: 26,
DailyActiveEditors: 27,
DailyActiveViewers: 28,
DailyActiveSessions: 29,
Orgs: 5,
Playlists: 6,
Alerts: 7,
Stars: 8,
Folders: 9,
DashboardPermissions: 10,
FolderPermissions: 11,
ProvisionedDashboards: 12,
Snapshots: 13,
Teams: 14,
AuthTokens: 15,
DashboardVersions: 16,
Annotations: 17,
AlertRules: 18,
LibraryPanels: 19,
LibraryVariables: 20,
DashboardsViewersCanAdmin: 3,
DashboardsViewersCanEdit: 2,
FoldersViewersCanAdmin: 1,
FoldersViewersCanEdit: 5,
APIKeys: 2,
}
}
type mockSocial struct {
social.Service
OAuthProviders map[string]bool
}
func (m *mockSocial) GetOAuthProviders() map[string]bool {
return m.OAuthProviders
}
type fakePluginStore struct {
plugins.Store
plugins map[string]plugins.PluginDTO
}
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := pr.plugins[pluginID]
return p, exists
}
func setupSomeDataSourcePlugins(t *testing.T, s *Service) {
t.Helper()
s.plugins = &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
models.DS_ES: {
Signature: "internal",
},
models.DS_PROMETHEUS: {
Signature: "internal",
},
models.DS_GRAPHITE: {
Signature: "internal",
},
models.DS_MYSQL: {
Signature: "internal",
},
},
}
}
func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
var result []plugins.PluginDTO
for _, v := range pr.plugins {
for _, t := range pluginTypes {
if v.Type == t {
result = append(result, v)
}
}
}
return result
}
func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store) *Service {
t.Helper()
return ProvideService(
&usagestats.UsageStatsMock{},
cfg,
store,
&mockSocial{},
&fakePluginStore{},
featuremgmt.WithFeatures("feature1", "feature2"),
)
}
-2
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
@@ -41,7 +40,6 @@ func ProvideService(store sqlstore.Store, loginService login.Service) *Authentic
store: store,
loginService: loginService,
}
bus.AddHandler("auth", a.AuthenticateUser)
return a
}
+2 -20
View File
@@ -1,11 +1,9 @@
package middleware
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
@@ -42,22 +40,8 @@ func TestMiddlewareBasicAuth(t *testing.T) {
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
const salt = "Salt"
const orgID int64 = 2
bus.AddHandler("grafana-auth", func(ctx context.Context, query *models.LoginUserQuery) error {
t.Log("Handling LoginUserQuery")
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.User = &models.User{
Password: encoded,
Salt: salt,
}
return nil
})
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: orgID, UserId: id}
authHeader := util.GetBasicAuthHeader("myUser", password)
@@ -88,6 +72,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
}, configure)
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
sc.mockSQLStore.ExpectedError = models.ErrUserNotFound
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("user", "password")
sc.exec()
@@ -100,10 +85,7 @@ func TestMiddlewareBasicAuth(t *testing.T) {
}, configure)
middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("user-query", func(ctx context.Context, loginUserQuery *models.GetUserByLoginQuery) error {
return nil
})
sc.mockSQLStore.ExpectedError = models.ErrUserNotFound
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("killa", "gorilla")
sc.exec()
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/setting"
@@ -109,14 +108,6 @@ func TestMiddlewareJWTAuth(t *testing.T) {
}, nil
}
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{UserId: id, OrgId: orgID, Email: myEmail}
bus.AddHandler("upsert-user", func(ctx context.Context, command *models.UpsertUserCommand) error {
command.Result = &models.User{
Id: id,
Name: command.ExternalUser.Name,
Email: command.ExternalUser.Email,
}
return nil
})
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
+4 -5
View File
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
@@ -25,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/contexthandler/authproxy"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
@@ -569,8 +569,6 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
t.Helper()
t.Run(desc, func(t *testing.T) {
t.Cleanup(bus.ClearBusHandlers)
logger := log.New("test")
loginMaxLifetime, err := gtime.ParseDuration("30d")
@@ -602,7 +600,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
sc.sqlStore = ctxHdlr.SQLStore
sc.contextHandler = ctxHdlr
sc.m.Use(ctxHdlr.Middleware)
sc.m.Use(OrgRedirect(sc.cfg))
sc.m.Use(OrgRedirect(sc.cfg, sc.mockSQLStore))
sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService)
sc.jwtAuthService = ctxHdlr.JWTAuthService.(*models.FakeJWTService)
@@ -644,8 +642,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *mockstore.S
authJWTSvc := models.NewFakeJWTService()
tracer, err := tracing.InitializeTracerForTest()
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, mockSQLStore)
authenticator := &logintest.AuthenticatorFake{ExpectedUser: &models.User{}}
require.NoError(t, err)
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy)
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, authenticator)
}
type fakeRenderService struct {
+3 -3
View File
@@ -6,16 +6,16 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
// OrgRedirect changes org and redirects users if the
// querystring `orgId` doesn't match the active org.
func OrgRedirect(cfg *setting.Cfg) web.Handler {
func OrgRedirect(cfg *setting.Cfg, store sqlstore.Store) web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
orgIdValue := req.URL.Query().Get("orgId")
orgId, err := strconv.ParseInt(orgIdValue, 10, 64)
@@ -34,7 +34,7 @@ func OrgRedirect(cfg *setting.Cfg) web.Handler {
}
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
if err := bus.Dispatch(ctx.Req.Context(), &cmd); err != nil {
if err := store.SetUsingOrg(ctx.Req.Context(), &cmd); err != nil {
if ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)
} else {
+1 -8
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
@@ -47,10 +46,6 @@ func TestOrgRedirectMiddleware(t *testing.T) {
middlewareScenario(t, tc.desc, func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
@@ -68,9 +63,7 @@ func TestOrgRedirectMiddleware(t *testing.T) {
middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(ctx context.Context, query *models.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
sc.mockSQLStore.ExpectedSetUsingOrgError = fmt.Errorf("")
sc.mockSQLStore.ExpectedSignedInUser = &models.SignedInUser{OrgId: 1, UserId: 12}
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
+1 -4
View File
@@ -5,7 +5,6 @@ import (
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
@@ -48,8 +47,6 @@ func panicHandler(c *models.ReqContext) {
func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
t.Run(desc, func(t *testing.T) {
defer bus.ClearBusHandlers()
cfg := setting.NewCfg()
cfg.ErrTemplateName = "error-template"
sc := &scenarioContext{
@@ -73,7 +70,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
contextHandler := getContextHandler(t, nil, nil, nil)
sc.m.Use(contextHandler.Middleware)
// mock out gc goroutine
sc.m.Use(OrgRedirect(cfg))
sc.m.Use(OrgRedirect(cfg, sc.mockSQLStore))
sc.defaultHandler = func(c *models.ReqContext) {
sc.context = c
@@ -127,7 +127,6 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"news": {},
"nodeGraph": {},
"piechart": {},
"pluginlist": {},
"stat": {},
"state-timeline": {},
"status-history": {},
-1
View File
@@ -36,7 +36,6 @@ type testfunc func(*testing.T, schema.VersionedCueSchema, []byte, fs.FileInfo, s
// TODO: remove validdir once we can test directly with devenv folder
var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, fn testfunc) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
require.NoError(t, filepath.Walk(validdir, func(path string, d fs.FileInfo, err error) error {
require.NoError(t, err)
-21
View File
@@ -444,27 +444,6 @@
"overrides": []
}
},
{
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 29
},
"id": 30,
"title": "Panel list",
"type": "pluginlist",
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"options": {},
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 8,
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/registry"
@@ -32,9 +33,10 @@ func ProvideBackgroundServiceRegistry(
pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, pm *manager.PluginManager,
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, tracing tracing.Tracer,
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats,
grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService,
metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService,
remoteCache *remotecache.RemoteCache, thumbnailsService thumbs.Service, StorageService store.StorageService,
statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService,
pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService,
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
thumbnailsService thumbs.Service, StorageService store.StorageService,
// Need to make sure these are initialized, is there a better place to put them?
_ *dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider,
@@ -56,11 +58,13 @@ func ProvideBackgroundServiceRegistry(
pluginsUpdateChecker,
metrics,
usageStats,
statsCollector,
tracing,
remoteCache,
secretsService,
StorageService,
thumbnailsService)
thumbnailsService,
)
}
// BackgroundServiceRegistry provides background services.
+3
View File
@@ -5,6 +5,7 @@ package server
import (
"github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/avatar"
@@ -21,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
loginpkg "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
@@ -237,6 +239,7 @@ var wireBasicSet = wire.NewSet(
secretsStore.ProvideService,
avatar.ProvideAvatarCacheServer,
authproxy.ProvideAuthProxy,
statscollector.ProvideService,
)
var wireSet = wire.NewSet(
+1
View File
@@ -30,6 +30,7 @@ type AlertStore interface {
GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error
SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error
GetAlertNotificationUidWithId(context.Context, *models.GetAlertNotificationUidQuery) error
GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error
GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error
SetAlertState(context.Context, *models.SetAlertStateCommand) error
+4
View File
@@ -65,6 +65,10 @@ func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models.
return nil
}
func (a *AlertStoreMock) GetAlertNotificationUidWithId(c context.Context, query *models.GetAlertNotificationUidQuery) error {
return nil
}
func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error {
if a.getAlertNotificationsWithUidToSend != nil {
return a.getAlertNotificationsWithUidToSend(c, cmd)
+4 -2
View File
@@ -22,13 +22,15 @@ type DashAlertExtractor interface {
type DashAlertExtractorService struct {
datasourcePermissionsService permissions.DatasourcePermissionsService
datasourceService datasources.DataSourceService
alertStore AlertStore
log log.Logger
}
func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService) *DashAlertExtractorService {
func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService, alertStore AlertStore) *DashAlertExtractorService {
return &DashAlertExtractorService{
datasourcePermissionsService: datasourcePermissionsService,
datasourceService: datasourceService,
alertStore: alertStore,
log: log.New("alerting.extractor"),
}
}
@@ -231,7 +233,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json
alert.Settings = jsonAlert
// validate
_, err = NewRuleFromDBAlert(ctx, alert, logTranslationFailures)
_, err = NewRuleFromDBAlert(ctx, e.alertStore, alert, logTranslationFailures)
if err != nil {
return nil, err
}
+4 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -36,7 +37,8 @@ func TestAlertRuleExtraction(t *testing.T) {
}
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService)
store := mockstore.NewSQLStoreMock()
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store)
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
dashJSON, err := simplejson.NewJson(json)
@@ -312,7 +314,7 @@ func TestFilterPermissionsErrors(t *testing.T) {
dsPermissions := permissions.NewMockDatasourcePermissionService()
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService)
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, nil)
tc := []struct {
name string
-9
View File
@@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/bus"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/imguploader"
@@ -205,14 +204,6 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
return nil
}
bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error {
return nil
})
bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error {
return nil
})
setting.AlertingNotificationTimeout = 30 * time.Second
scenarioCtx := &scenarioContext{
+1 -1
View File
@@ -38,7 +38,7 @@ func (arr *defaultRuleReader) fetch(ctx context.Context) []*Rule {
res := make([]*Rule, 0)
for _, ruleDef := range cmd.Result {
if model, err := NewRuleFromDBAlert(ctx, ruleDef, false); err != nil {
if model, err := NewRuleFromDBAlert(ctx, arr.sqlStore, ruleDef, false); err != nil {
arr.log.Error("Could not build alert model for rule", "ruleId", ruleDef.Id, "error", err)
} else {
res = append(res, model)
+6 -7
View File
@@ -9,7 +9,6 @@ import (
"strconv"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
@@ -145,7 +144,7 @@ func getForValue(rawFor string) (time.Duration, error) {
// NewRuleFromDBAlert maps a db version of
// alert to an in-memory version.
func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {
func NewRuleFromDBAlert(ctx context.Context, store AlertStore, ruleDef *models.Alert, logTranslationFailures bool) (*Rule, error) {
model := &Rule{}
model.ID = ruleDef.Id
model.OrgID = ruleDef.OrgId
@@ -170,7 +169,7 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err == nil {
uid, err := translateNotificationIDToUID(ctx, id, ruleDef.OrgId)
uid, err := translateNotificationIDToUID(ctx, store, id, ruleDef.OrgId)
if err != nil {
if !errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID) {
logger.Error("Failed to translate notification id to uid", "error", err.Error(), "dashboardId", model.DashboardID, "alert", model.Name, "panelId", model.PanelID, "notificationId", id)
@@ -211,8 +210,8 @@ func NewRuleFromDBAlert(ctx context.Context, ruleDef *models.Alert, logTranslati
return model, nil
}
func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (string, error) {
notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, id, orgID)
func translateNotificationIDToUID(ctx context.Context, store AlertStore, id int64, orgID int64) (string, error) {
notificationUID, err := getAlertNotificationUIDByIDAndOrgID(ctx, store, id, orgID)
if err != nil {
return "", err
}
@@ -220,13 +219,13 @@ func translateNotificationIDToUID(ctx context.Context, id int64, orgID int64) (s
return notificationUID, nil
}
func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, notificationID int64, orgID int64) (string, error) {
func getAlertNotificationUIDByIDAndOrgID(ctx context.Context, store AlertStore, notificationID int64, orgID int64) (string, error) {
query := &models.GetAlertNotificationUidQuery{
OrgId: orgID,
Id: notificationID,
}
if err := bus.Dispatch(ctx, query); err != nil {
if err := store.GetAlertNotificationUidWithId(ctx, query); err != nil {
return "", err
}
+4 -4
View File
@@ -130,7 +130,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.Len(t, alertRule.Conditions, 1)
@@ -169,7 +169,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.NotContains(t, alertRule.Notifications, "999")
require.Contains(t, alertRule.Notifications, "notifier2")
@@ -200,7 +200,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
alertRule, err := NewRuleFromDBAlert(context.Background(), alert, false)
alertRule, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.Nil(t, err)
require.EqualValues(t, alertRule.Frequency, 60)
})
@@ -238,7 +238,7 @@ func TestAlertRuleModel(t *testing.T) {
Settings: alertJSON,
}
_, err := NewRuleFromDBAlert(context.Background(), alert, false)
_, err := NewRuleFromDBAlert(context.Background(), sqlStore, alert, false)
require.NotNil(t, err)
require.EqualValues(t, err.Error(), "alert validation error: Neither id nor uid is specified in 'notifications' block, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
})
-14
View File
@@ -28,20 +28,6 @@ func ProvideService(bus bus.Bus, store *sqlstore.SQLStore, encryptionService enc
NotificationService: notificationService,
}
s.Bus.AddHandler(s.GetAlertNotifications)
s.Bus.AddHandler(s.CreateAlertNotificationCommand)
s.Bus.AddHandler(s.UpdateAlertNotification)
s.Bus.AddHandler(s.DeleteAlertNotification)
s.Bus.AddHandler(s.GetAllAlertNotifications)
s.Bus.AddHandler(s.GetOrCreateAlertNotificationState)
s.Bus.AddHandler(s.SetAlertNotificationStateToCompleteCommand)
s.Bus.AddHandler(s.SetAlertNotificationStateToPendingCommand)
s.Bus.AddHandler(s.GetAlertNotificationsWithUid)
s.Bus.AddHandler(s.UpdateAlertNotificationWithUid)
s.Bus.AddHandler(s.DeleteAlertNotificationWithUid)
s.Bus.AddHandler(s.GetAlertNotificationsWithUidToSend)
s.Bus.AddHandler(s.HandleNotificationTestCommand)
return s
}
+1 -1
View File
@@ -25,7 +25,7 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID
if alert.PanelId != panelID {
continue
}
rule, err := NewRuleFromDBAlert(context.Background(), alert, true)
rule, err := NewRuleFromDBAlert(context.Background(), e.sqlStore, alert, true)
if err != nil {
return nil, err
}

Some files were not shown because too many files have changed in this diff Show More