Merge remote-tracking branch 'origin/main' into guicaulada/unified-secrets
This commit is contained in:
+13
-9
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
@@ -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 '&'. |
|
||||
| `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 '&'. |
|
||||
| **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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "8.4.5",
|
||||
"testing": "8.4.5"
|
||||
"testing": "8.5.0-beta1"
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "8.5.0-pre"
|
||||
"version": "9.0.0-pre"
|
||||
}
|
||||
|
||||
+6
-6
@@ -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": {
|
||||
|
||||
@@ -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/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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())),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+55
-25
@@ -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"),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user