Merge remote-tracking branch 'origin' into es-convert-to-dataframes

This commit is contained in:
dsotirakis
2021-06-07 11:41:39 +03:00
35 changed files with 771 additions and 164 deletions
+3 -3
View File
@@ -11,9 +11,9 @@
# In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones.
# Documentation owner: Diana Payton
/docs/ @grafana/docs-squad
/contribute/ @marcusolsson @grafana/docs-squad
# Documentation owner: Jita Chatterjee
/docs/ @grafana/docs-squad @pkolyvas
/contribute/ @marcusolsson @grafana/docs-squad @pkolyvas
/docs/sources/developers/plugins/ @marcusolsson @grafana/docs-squad
/docs/sources/enterprise/ @osg-grafana @grafana/docs-squad
@@ -20,6 +20,7 @@ Alerts have four main components:
You can perform the following tasks for alerts:
- [Create a Grafana managed alert rule]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}})
- [Create a Cortex or Loki managed alert rule]({{< relref "alerting-rules/create-cortex-loki-managed-rule.md" >}})
- [View existing alert rules and their current state]({{< relref "alerting-rules/rule-list.md" >}})
@@ -6,9 +6,12 @@ description = "How to enable Grafana 8 Alerts"
# Enable Grafana 8 Alerts
Setting the `ngalert` feature toggle enables the new Grafana 8 Alerting system.
>**Note:** It is recommended to backup Grafana's database before enabling this feature.
>**Note:** It is recommended to backup Grafana's database before enabling this feature. If you are using PostgreSQL as the backend data source, then the minimum required version is 9.5.
At startup, when the feature toggle is enabled, Grafana dashboard alerting is disabled and existing dashboard alerts are migrated into a format that is compatible with the Grafana 8 Alerting system. You are able to view these migrated rules, alongside any new alerts you create after the migration, from the Alerting page of your grafana installation.
Also notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any dahsboard alert go to the `autogen-unlinked-channel-recv` route.
Since `Hipchat` and `Sensu` are discontinued, they are not migrated to the new alerting. If you have dashboard alerts associated with those types of channels and you want to migrate to the new alerting, make sure you assign another supported notification channel, so that you continue to receive notification for those alerts.
Finally, silences (expiring after one year) are created for all paused dashboard alerts.
During beta, the migration of existing dashboard rules may change.
@@ -5,11 +5,11 @@ keywords = ["grafana", "fine-grained access-control", "roles", "permissions", "e
weight = 115
+++
# Permissions
# Permissions
Each permission is defined by an action and a scope. When evaluating a fine-grained access decision, consider what specific action a user should be allowed to perform, and on what resources (its scope).
A permission is an action and a scope. When creating a fine-grained access control, consider what specific action a user should be allowed to perform, and on what resources (its scope).
To grant permissions to a user, create built-in role assignments. A built-in role assignment is a *modification* to one of the existing built-in roles in Grafana (Viewer, Editor, Admin) For more information, refer to [Built-in role assignments]({{< relref "./roles.md#built-in-role-assignments" >}}).
To grant permissions to a user, you create a built-in role assignment to map a role to a built-in role. A built-in role assignment *modifies* to one of the existing built-in roles in Grafana (Viewer, Editor, Admin). For more information, refer to [Built-in role assignments]({{< relref "./roles.md#built-in-role-assignments" >}}).
To learn more about which permissions are used for which resources, refer to [Resources with fine-grained permissions]({{< relref "./_index.md#resources-with-fine-grained-permissions" >}}).
@@ -17,59 +17,61 @@ action
: The specific action on a resource defines what a user is allowed to perform if they have permission with the relevant action assigned to it.
scope
: The scope describes where an action can be performed, such as reading a specific user profile. In such case, a permission is associated with the scope `users:<userId>` to the relevant role. Also, you can combine multiple scopes by using the `/` delimiter.
: The scope describes where an action can be performed, such as reading a specific user profile. In such case, a permission is associated with the scope `users:<userId>` to the relevant role.
## Action definitions
Note that below list is not exhaustive yet and more permissions will be available with further releases of fine-grained access control.
The following list contains fine-grained access control actions.
Action | Applicable scopes | Description
Actions | Applicable scopes | Descriptions
--- | --- | ---
roles:list | roles:* | Allows to list available roles without permissions.
roles:read | roles:* | Allows to read a specific role with it's permissions.
roles:write | permissions:delegate | Allows to create or update a custom role.
roles:delete | permissions:delegate | Allows to delete a custom role.
roles.builtin:list | roles:* | Allows to list built-in role assignments.
roles.builtin:add | permissions:delegate | Allows to create a built-in role assignment.
roles.builtin:remove | permissions:delegate | Allows to delete a built-in role assignment.
reports.admin:write | reports:* | Allows to create or update reports.
reports:delete | reports:* | Allows to delete reports.
reports:read | reports:* | Allows to list all available reports and to get a specific report.
reports:send | reports:* | Allows to send report email.
reports.settings:write | n/a | Allows to update report settings.
reports.settings:read | n/a | Allows to read report settings.
provisioning:reload | service:access-control | Allows to reload provisioning files after an update.
users:read | global:users:* | Allows to read, search user profiles.
users:write | global:users:* | Allows to update user profiles.
users.teams:read | global:users:* | Allows to read user teams.
users.authtoken:list | global:users:* | Allows to list auth tokens assigned to users.
users.authtoken:update | global:users:* | Allows to update auth tokens assigned to users.
users.password:update | global:users:* | Allows to update users password.
users:delete | global:users:* | Allows to delete users.
users:create | n/a | Allows to create users.
users:enable | global:users:* | Allows to enable users.
users:disable | global:users:* | Allows to disable users.
users.permissions:update | global:users:* | Allows to update users org level permissions.
users:logout | global:users:* | Allows to enforce logout for users.
users.quotas:list | global:users:* | Allows to list user quotas.
users.quotas:update | global:users:* | Allows to update user quotas.
org.users.read | users:* | Allows to get user profiles within the organization.
org.users.add | users:* | Allows to add users to the organization.
org.users.remove | users:* | Allows to remove users from the organization.
org.users.role:update | users:* | Allows to update users organization role for the assigned organization.
ldap.user:read | n/a | Allows to read LDAP users.
ldap.user:sync | n/a | Allows to sync LDAP users.
ldap.status:read | n/a | Allows to check LDAP status.
roles:list | roles:* | List available roles without permissions.
roles:read | roles:* | Read a specific role with it's permissions.
roles:write | permissions:delegate | Create or update a custom role.
roles:delete | permissions:delegate | Delete a custom role.
roles.builtin:list | roles:* | List built-in role assignments.
roles.builtin:add | permissions:delegate | Create a built-in role assignment.
roles.builtin:remove | permissions:delegate | Delete a built-in role assignment.
reports.admin:create | reports:* | Create reports.
reports.admin:write | reports:* | Update reports.
reports:delete | reports:* | Delete reports.
reports:read | reports:* | List all available reports or get a specific report.
reports:send | reports:* | Send a report email.
reports.settings:write | n/a | Update report settings.
reports.settings:read | n/a | Read report settings.
provisioning:reload | service:access-control | Reload provisioning files.
users:read | global:users:* | Read or search user profiles.
users:write | global:users:* | Update a user’s profile.
users.teams:read | global:users:* | Read a user’s teams.
users.authtoken:list | global:users:* | List authentication tokens that are assigned to a user.
users.authtoken:update | global:users:* | Update authentication tokens that are assigned to a user.
users.password:update | global:users:* | Update a user’s password.
users:delete | global:users:* | Delete a user.
users:create | n/a | Create a user.
users:enable | global:users:* | Enable a user.
users:disable | global:users:* | Disable a user.
users.permissions:update | global:users:* | Update a user’s organization-level permissions.
users:logout | global:users:* | Log out a user.
users.quotas:list | global:users:* | List a user’s quotas.
users.quotas:update | global:users:* | Update a user’s quotas.
org.users.read | users:* | Get user profiles within an organization.
org.users.add | users:* | Add a user to an organization.
org.users.remove | users:* | Remove a user from an organization.
org.users.role:update | users:* | Update the organization role (`Viewer`, `Editor`, `Admin`) for an organization.
ldap.user:read | n/a | Get a user via LDAP.
ldap.user:sync | n/a | Sync a user via LDAP.
ldap.status:read | n/a | Verify the LDAP servers’ availability.
status:accesscontrol | service:access-control | Get access-control enabled status.
## Scope definitions
Note that below list is not exhaustive yet and more scopes will be available with further releases of fine-grained access control.
The following list contains fine-grained access control scopes.
Scope | Description
--- | ---
roles:* | Indicates against what roles an action can be performed. For example, `roles:*` assumes any roles, and `roles:randomuid` assumes only a role with UID `randomuid`.
permissions:delegate | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment.
reports:* | Indicates against what reports an action can be performed.
service:access-control | Only relevant for provisioning and indicates that the action can be performed only for access control provisioning files.
global:users:* | Indicates that action can be performed against users globally.
users:* | Indicates that an action can be performed against users in organization level.
Scopes | Descriptions
--- | ---
roles:* | Restrict an action to a set of roles. For example, `roles:*` matches any role, `roles:randomuid` matches only the role with UID `randomuid` and `roles:custom:reports:{editor,viewer}` matches both `custom:reports:editor` and `custom:reports:viewer` roles.
permissions:delegate | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment.
reports:* | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:1` matches the report with id `1`.
service:accesscontrol | Restrict an action to target only the fine-grained access control service. For example, you can use this in conjunction with the `provisioning:reload` or the `status:accesscontrol` actions.
global:users:* | Restrict an action to a set of global users.
users:* | Restrict an action to a set of users from an organization.
@@ -349,12 +349,25 @@ This value is `true` by default.
_Time to live_ (TTL) is the time that a query result is stored in the caching system before it is deleted or refreshed. This setting defines the time to live for query caching, when TTL is not configured in data source settings. The default value is `5m` (5 minutes).
### max_value_mb
This value limits the size of a single cache value. If a cache value (or query result) exceeds this size, then it is not cached. To disable this limit, set this value to `0`.
## [caching.memory]
### gc_interval
When storing cache data in-memory, this setting defines how often a background process cleans up stale data from the in-memory cache. More frequent "garbage collection" can keep memory usage from climbing but will increase CPU usage.
### max_size_mb
The maximum size of the in-memory cache in megabytes. Once this size is reached, new cache items are rejected. For more flexible control over cache eviction policies and size, use the Redis or Memcached backend.
To disable the maximum, set this value to `0`.
> **Note:** Disabling the maximum is not recommended in production environments.
## [caching.redis]
### url
View File
+83
View File
@@ -0,0 +1,83 @@
+++
title = "Library panels"
weight = 400
+++
# Library panels
Library panels allow users to create reusable panels where any changes made to one instance of the library panel is reflected on every dashboard
affecting all other instances where the panel is used. These panels can be saved in folders alongside Dashboards and streamline reuse of panels
across multiple dashboards.
## Create a library panel
Before you can use library panels, you have to create them.
> **Note:** When you create library panels, the panel on the source dashboard is converted to a library panel as well. You will need to save the original dashboard once a panel is converted.
To create a library panel:
1. Create a Grafana panel as you normally would, following instructions in [Add a panel]({{< relref "./add-a-panel.md" >}}). You an also use an existing panel.
1. Click the title of the panel and then click **Edit**. The panel opens in edit mode.
1. In the panel display options (side pane), click the down arrow option to bring changes to the visualization.
{{< figure src="/static/img/docs/library-panels/create-lib-panel-from-edit-8-0.png" class="docs-image--no-shadow" max-width= "800px" caption="Screenshot of the edit panel" >}}
1. Click the **Library panels** option, and then click **Create new library panel** to open the create dialog.
{{< figure src="/static/img/docs/library-panels/create-lib-panel-8-0.png" class="docs-image--no-shadow" max-width= "500px" caption="Screenshot of the create library panel dialog" >}}
1. In **Library panel name**, enter the name.
1. In **Save in folder**, select the folder to save the library panel. By default, the General folder is selected.
1. Click **Create library panel** to save your changes.
1. Save the dashboard.
Optionally, you can click title of the panel and then click **More > Create Library panel**. Next, follow the instructions Step 5 in the procedure above.
{{< figure src="/static/img/docs/library-panels/create-from-more-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
Also, library panels can also be created using the “Share” option for any panel.
Once created, you can modify the library panel using any dashboard on which it appears. Once the library panel changes are saved, all instances of the library panel will reflect these modifications.
## Add a library panel
To add a library panel to a dashboard:
1. Hover over the **+** option on the left menu, then select **Create** from the drop down options. The Add panel dialog opens.
{{< figure src="/static/img/docs/library-panels/add-library-panel-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
1. Click the **Add a panel from the panel library** option. You will see a list of your library panels.
1. Filter the list or search to find the panel you want to add.
1. Click your desired panel and add it to the dashboard.
## Manage library panels
To view and manage existing library panels:
1. Hover over the **Dashboard** option on the left menu, then click **Library panels**. You can see a list of previously defined library panels.
{{< figure src="/static/img/docs/library-panels/library-panel-list-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
1. Search for a specific library panel if you know its name. You can also filter the panels by folder or type.
## Unlink a library panel
In case you have a library panel on your dashboard that want to modify it without affecting all other instances of the library panel, you can unlink the library panel.
To unlink a library panel from a dashboard:
1. Hover over **Dashboard** on the left menu, and then click **Library panels**.
1. Select a library panel that is being used in different dashboards. You will see a list of all the dashboards where the library panel is used.
{{< figure src="/static/img/docs/library-panels/unlink-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
1. Select the panel you want to unlink and update.
1. Click the title of the panel and then click **Edit**. The panel opens in edit mode.
1. Click the **Unlink** option on the top right corner of the UI.
## Delete a library panel
Before you delete a library panel, verify that it is no longer in use on any dashboard.
To delete a library panel:
1. Hover over **Dashboard** on the left menu, and select Library panels from the drop down options.
1. Select a library panel that is being used in different dashboards. You will see a list of all the dashboards.
1. Select the panel you want to delete.
1. Click the delete icon next to the library panel name.
@@ -0,0 +1,66 @@
+++
title = "Histogram"
description = "Histogram visualization"
keywords = ["grafana", "docs", "bar chart", "panel", "barchart"]
weight = 605
+++
# Histogram
The histogram visualization calculates the distribution of values and presents them as a bar chart. The Y-axis and the height of each bar represent the count of values that fall into each bracket while the X-axis represents the value range.
{{< figure src="/static/img/docs/histogram-panel/histogram-example-v8-0.png" max-width="625px" caption="Bar chart example" >}}
## Supported data formats
Histogram visualization supports time series and any table results with one or more numerical fields.
## Display options
Use the following options to refine your visualization.
### Bucket size
The size of the buckets. Leave this empty for automatic bucket sizing (~10% of the full range).
### Bucket offset
If the first bucket should not start at zero. A non-zero offset has the effect of shifting the aggregation window. For example, 5-sized buckets that are 0-5, 5-10, 10-15 with a default 0 offset would become 2-7, 7-12, 12-17 with an offset of 2; offsets of 0, 5, or 10, in this case, would effectively do nothing. Typically, this option would be used with an explicitly defined bucket size rather than automatic. For this setting to affect, the offset amount should be greater than 0 and less than the bucket size; values outside this range will have the same effect as values within this range.
### Combine series
This will merge all series and fields into a combined histogram.
### Line width
Controls line width of the bars.
### Fill opacity
Controls the fill opacity bars.
### Gradient mode
Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard [color scheme]({{< relref "../standard-options.md#color-scheme" >}}) field option.
Gradient display is influenced by the **Fill opacity** setting.
#### None
No gradient fill. This is the default setting.
#### Opacity
Transparency of the gradient is calculated based on the values on the Y-axis. The opacity of the fill is increasing with the values on the Y-axis.
#### Hue
Gradient color is generated based on the hue of the line color.
{{< docs/shared "visualizations/tooltip-mode.md" >}}
{{< docs/shared "visualizations/legend-mode.md" >}}
### Legend calculations
Choose a [standard calculations]({{< relref "../calculations-list.md">}}) to show in the legend. You can select more than one.
@@ -1,6 +1,6 @@
+++
title = "State timeline"
description = "Stat timeline visualization"
description = "State timeline visualization"
keywords = ["grafana", "docs", "state timeline", "panel"]
weight = 900
+++
@@ -21,7 +21,7 @@ Controls whether Grafana merges identical values if they are next to each other.
### Show values
Controls whether values are rendered inside the state regions. Auto will render values if there is room.
Controls whether values are rendered inside the state regions. Auto will render values if there is sufficient space.
### Align values
@@ -47,7 +47,7 @@ To assign colors to boolean or string values, use the [Value mappings](< {{ refr
## Time series data with thresholds
The panel can be with time series data as well. In this case, the thresholds are used to turn the time series into discrete colored state regions.
The panel can be used with time series data as well. In this case, the thresholds are used to turn the time series into discrete colored state regions.
{{< figure src="/static/img/docs/v8/state_timeline_time_series.png" max-width="1025px" caption="state timeline with time series" >}}
@@ -0,0 +1,55 @@
+++
title = "Status history"
description = "Status history visualization"
keywords = ["grafana", "docs", "status history", "panel"]
weight = 900
+++
# Status history
The Status history visualization shows periodic states over time. Each field or series is rendered as a horizontal row. Boxes are rendered and centered around each value.
{{< figure src="/static/img/docs/status-history-panel/status-history-example-v8-0.png" max-width="1025px" caption="Status history example" >}}
## Supported data
Status history visualization works with string, boolean and numerical fields or time series. A time field is required. You can use value mappings to color strings or assign text values to numerical ranges.
## Display options
Use these options to refine the visualization.
### Show values
Controls whether values are rendered inside the value boxes. Auto will render values if there is sufficient space.
### Column width
Controls the width of boxes. 1 = maximum space and 0 = minimum space.
### Line width
Controls line width of state regions.
### Fill opacity
Controls the opacity of state regions.
## Value mappings
To assign colors to boolean or string values, use the [Value mappings](< {{ refref "../value-mappings.md"}} >).
{{< figure src="/static/img/docs/v8/value_mappings_side_editor.png" max-width="300px" caption="Value mappings side editor" >}}
## Time series data with thresholds
The panel can be used with time series data as well. In this case, the thresholds are used to color the boxes. You can also
use gradient color schemes to color values.
{{< figure src="/static/img/docs/v8/state_timeline_time_series.png" max-width="1025px" caption="state timeline with time series" >}}
## Legend options
When the legend option is enabled it can show either the value mappings or the threshold brackets. To show the value mappings in the legend, it's important that the `Color scheme` option under [Standard options]({{< relref "../standard-options.md" >}}) is set to `Single color` or `Classic palette`. To see the threshold brackets in the legend set the `Color scheme` to `From thresholds`.
{{< docs/shared "visualizations/legend-mode.md" >}}
@@ -1,14 +1,13 @@
+++
title = "Filter table columns"
keywords = ["grafana", "table options", "documentation", "format tables", "table filter", "filter columns"]
aliases = ["/docs/grafana/latest/reference/table/", "/docs/grafana/latest/features/panels/table_panel/", "/docs/grafana/next/panels/visualizations/table/table-field-options/"]
weight = 600
+++
# Filter table columns
> **Note:** This feature is available in Grafana 7.2+.
If you turn on the **Column filter** in [Table options]({{< relref "table-field-options.md" >}}), then you can filter table options.
If you turn on the **Column filter**, then you can filter table options.
## Turn on column filtering
@@ -146,21 +146,25 @@ Dot spacing set to 0, 30:
![Line style dots 0, 30 example](/static/img/docs/time-series-panel/line-graph-line-style-dots-0-30-7-4.png)
### Null values
### Connect null values
Choose how null values (gaps in the data) are displayed on the graph.
Choose how null values (gaps in the data) are displayed on the graph. Null values can be connected to form a continuous line or, optionally, set a threshold above which gaps in the data should no longer be connected.
#### Gaps
![Image name](/static/img/docs/time-series-panel/connect-null-values-8-0.png)
If there is a gap in the series, the line in the graph will be broken and show the gap.
#### Never
![Null values gaps example](/static/img/docs/time-series-panel/line-graph-null-gaps-7-4.png)
Time series data points with gaps in the the data are never connected.
#### Connected
#### Always
If there is a gap in the series, the line will skip the gap and connect to the next non-null value.
Time series data points with gaps in the the data are always connected.
![Null values connected example](/static/img/docs/time-series-panel/line-graph-null-connected-7-4.png)
#### Threshold
A threshold can be set above which gaps in the data should no longer be connected. This can be useful when the connected gaps in the data are of a known size and/or within a known range and gaps outside this range should no longer be connected.
![Image name](/static/img/docs/time-series-panel/connect-null-values-8-0.png)
### Show points
+17 -5
View File
@@ -1,20 +1,21 @@
+++
title = "Share a panel"
keywords = ["grafana", "dashboard", "documentation", "sharing"]
keywords = ["grafana", "dashboard", "documentation", "sharing", "library panel"]
aliases = ["/docs/grafana/latest/dashboards/share-dashboard/","/docs/grafana/latest/reference/share_panel/"]
weight = 6
+++
# Share a panel
You can share a panel as a direct link, as a snapshot or as an embedded link.
You can share a panel as a direct link, as a snapshot or as an embedded link. You can also create library panels using the “Share” option for any panel.
To share a panel:
1. Click a panel title to open the panel menu.
1. Click **Share**. The share dialog opens and shows the Link tab.
![Panel share direct link](/static/img/docs/sharing/share-panel-direct-link-7-3.png)
![Panel share direct link](/static/img/docs/sharing/share-panel-direct-link-8-0.png)
## Use direct link
The Link tab has the current time range, template variables and theme selected by default. You can optionally enable a shortened URL to share.
@@ -46,7 +47,7 @@ A panel snapshot shares an interactive panel publicly. Grafana strips sensitive
You can publish snapshots to your local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The latter is a free service provided by [Raintank](http://raintank.io), that allows you to publish dashboard snapshots to an external Grafana instance. You can optionally set an expiration time if you want the snapshot to be removed after a certain time period.
![Panel share snapshot](/static/img/docs/sharing/share-panel-snapshot-7-3.png)
![Panel share snapshot](/static/img/docs/sharing/share-panel-snapshot-8-0.png)
To publish a snapshot:
@@ -60,7 +61,7 @@ If you created a snapshot by mistake, click **delete snapshot** to remove the sn
You can embed a panel using an iframe on another web site. Unless anonymous access permission is enabled, the viewer must be signed into Grafana to view the graph.
![Panel share embed](/static/img/docs/sharing/share-panel-embedded-link-7-3.png)
![Panel share embed](/static/img/docs/sharing/share-panel-embedded-link-8-0.png)
Here is an example of the HTML code:
@@ -71,3 +72,14 @@ Here is an example of the HTML code:
The result is an interactive Grafana graph embedded in an iframe:
<iframe src="https://snapshot.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4" width="650" height="300" frameborder="0"></iframe>
## Library panel
To create a library panel from the Share Panel dialog:
1. Click **Library panel**.
{{< figure src="/static/img/docs/library-panels/create-lib-panel-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the create library panel dialog" >}}
1. In **Library panel name**, enter the name.
1. In **Save in folder**, select the folder to save the library panel. By default, the General folder is selected.
1. Click **Create library panel** to save your changes.
1. Save the dashboard.
+26 -4
View File
@@ -30,7 +30,7 @@ To learn more about the differences between new alerts and the legacy alerts, re
### Library panels
Library panels allow users to build panels that can be used in multiple dashboards. Any updates made to that shared panel will then automatically be applied to all the dashboards that have that panel.
Library panels allow users to build panels that can be used in multiple dashboards. Any updates made to that shared panel will then automatically be applied to all the dashboards that have that panel. For instructions on how to create, add, unlink and manage library panels, refer to [Library panels]({{< relref "../panels/panel-library.md" >}}).
### Real-time streaming
@@ -73,7 +73,8 @@ For more information, refer to [State timeline visualization]({{< relref "../pan
### Status history visualization (beta)
A sister panel to the state timeline is the new Status history panel visualization. It can display periodic state in a grid. Works with both numerical, string or boolean state.
A sister panel to the state timeline is the new Status history panel visualization. It can display periodic states in a grid view. It supports both numerical, string, or boolean states. You can assign colors using value mappings, thresholds, or gradient color. For more information, refer to [Status history]({{< relref "../panels/visualizations/status-history.md" >}}).
schemes.
![Status grid visualization](/static/img/docs/status-grid/status-grid-8-0.png)
@@ -81,7 +82,9 @@ A sister panel to the state timeline is the new Status history panel visualizati
This hidden feature of the old Graph panel is now a standalone visualization. It combines a histogram transformation and bar chart visualization into a single, integrated, easy-to-use panel. There is also a new standalone histogram transformation that can be paired with any visualization.
![Histogram visualization](/static/img/docs/histogram/histogram-8-0.png)
{{< figure src="/static/img/docs/histogram/histogram-8-0.png" max-width="1025px" caption="Histogram example" >}}
For more information, refer to [Histogram]({{< relref "../panels/visualizations/histogram.md" >}})
### Time series visualization updates
@@ -90,14 +93,23 @@ The Time series is out of beta! We are removing the `Beta` tag and graduating th
- **Time series** is now the default visualization option, replacing the **Graph (old)**.
- The Time series panel now supports stacking. For more information, refer to [Graph stacked time series]({{< relref "../panels/visualizations/time-series/graph-time-series-stacking.md" >}}).
- You can now add alerts in the Time series panel, just like the old Graph panel.
- Updated [connect null values]({{< relref "../panels/visualizations/time-series/graph-time-series-as-lines.md#connect-null-values" >}}) options.
- We added support for a shared crosshair and a tooltip that’s now smarter when it comes to data display in the tooltip.
- Various performance improvements.
[Time series panel]({{< relref "../panels/visualizations/time-series/_index.md" >}}) topics have been updated as a result of these changes.
### Node graph visualization updates
You can now expand the node graph for the displayed trace when using the Trace to logs feature.. Depending on the data source, this can show spans of the trace as nodes in the graph, or as some additional context like service map based on the current trace.
We also added a grid view and the ability to explore hidden nodes.
[Tracing in Explore]({{< relref "../explore/trace-integration.md" >}}) and [Node graph]({{< relref "../panels/visualizations/node-graph.md" >}}) were updated as a result of these changes.
### Pie chart visualization updates
The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state.
The Pie chart is out of beta! We are removing the `beta` tag and graduating the Pie chart visualization to a stable state.
### Panel editor updates
@@ -163,6 +175,16 @@ These improvements affect any subsequent data updates or interactions, including
The following data source updates are included with this Grafana release.
#### Azure Monitor data source
Azure Resource Graph is now supported in the Azure Monitor data source. This is a service in Azure that is designed to extend Azure Resource Management by providing efficient and performance resource exploration.
Also, in addition to querying Log Analytics Workspaces, you can now query the logs for any individual [supported resource](https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-supported), or for all resources in a subscription or resource group.
> **Note:** In Grafana 7.5 we started the deprecation for separate Application Insights queries, in favor of querying Application Insights resources through Metrics and Logs. In Grafana 8.0 new Application Insights and Insights Analytics queries cannot be made, and existing queries have been made read-only.
For a comprehensive list of all enhancements, refer to the [Azure Monitor data source documentation]({{< relref "../datasources/azuremonitor.md" >}}).
#### Elasticsearch data source
[Elasticsearch data source]({{< relref "../datasources/elasticsearch.md" >}}) and [Provisioning]({{< relref "../administration/provisioning.md" >}}) were updated as a result of these changes.
@@ -12,7 +12,7 @@ describe('histogram frames frames', () => {
const series1 = toDataFrame({
fields: [
{ name: 'A', type: FieldType.number, values: [1, 2, 3, 4, 5] },
{ name: 'B', type: FieldType.number, values: [3, 4, 5, 6, 7] },
{ name: 'B', type: FieldType.number, values: [3, 4, 5, 6, 7], config: { unit: 'mph' } },
{ name: 'C', type: FieldType.number, values: [5, 6, 7, 8, 9] },
],
});
@@ -26,10 +26,14 @@ describe('histogram frames frames', () => {
out.fields.map((f) => ({
name: f.name,
values: f.values.toArray(),
config: f.config,
}))
).toMatchInlineSnapshot(`
Array [
Object {
"config": Object {
"unit": "mph",
},
"name": "BucketMin",
"values": Array [
1,
@@ -44,6 +48,9 @@ describe('histogram frames frames', () => {
],
},
Object {
"config": Object {
"unit": "mph",
},
"name": "BucketMax",
"values": Array [
2,
@@ -58,6 +65,9 @@ describe('histogram frames frames', () => {
],
},
Object {
"config": Object {
"unit": undefined,
},
"name": "A",
"values": Array [
1,
@@ -72,6 +82,9 @@ describe('histogram frames frames', () => {
],
},
Object {
"config": Object {
"unit": undefined,
},
"name": "B",
"values": Array [
0,
@@ -86,6 +99,9 @@ describe('histogram frames frames', () => {
],
},
Object {
"config": Object {
"unit": undefined,
},
"name": "C",
"values": Array [
0,
@@ -100,6 +116,9 @@ describe('histogram frames frames', () => {
],
},
Object {
"config": Object {
"unit": undefined,
},
"name": "C",
"values": Array [
0,
@@ -2,23 +2,37 @@ import { DataTransformerInfo } from '../../types';
import { map } from 'rxjs/operators';
import { DataTransformerID } from './ids';
import { DataFrame, Field, FieldType } from '../../types/dataFrame';
import { DataFrame, Field, FieldConfig, FieldType } from '../../types/dataFrame';
import { ArrayVector } from '../../vector/ArrayVector';
import { AlignedData, join } from './joinDataFrames';
import { getDisplayProcessor } from '../../field';
import { createTheme, GrafanaTheme2 } from '../../themes';
/* eslint-disable */
// prettier-ignore
/**
* @internal
*/
/* eslint-disable */
// prettier-ignore
export const histogramBucketSizes = [
.001, .002, .0025, .005,
.01, .02, .025, .05,
.1, .2, .25, .5,
1, 2, 4, 5,
10, 20, 25, 50,
100, 200, 250, 500,
1000, 2000, 2500, 5000,
1e-9, 2e-9, 2.5e-9, 4e-9, 5e-9,
1e-8, 2e-8, 2.5e-8, 4e-8, 5e-8,
1e-7, 2e-7, 2.5e-7, 4e-7, 5e-7,
1e-6, 2e-6, 2.5e-6, 4e-6, 5e-6,
1e-5, 2e-5, 2.5e-5, 4e-5, 5e-5,
1e-4, 2e-4, 2.5e-4, 4e-4, 5e-4,
1e-3, 2e-3, 2.5e-3, 4e-3, 5e-3,
1e-2, 2e-2, 2.5e-2, 4e-2, 5e-2,
1e-1, 2e-1, 2.5e-1, 4e-1, 5e-1,
1, 2, 4, 5,
1e+1, 2e+1, 2.5e+1, 4e+1, 5e+1,
1e+2, 2e+2, 2.5e+2, 4e+2, 5e+2,
1e+3, 2e+3, 2.5e+3, 4e+3, 5e+3,
1e+4, 2e+4, 2.5e+4, 4e+4, 5e+4,
1e+5, 2e+5, 2.5e+5, 4e+5, 5e+5,
1e+6, 2e+6, 2.5e+6, 4e+6, 5e+6,
1e+7, 2e+7, 2.5e+7, 4e+7, 5e+7,
1e+8, 2e+8, 2.5e+8, 4e+8, 5e+8,
1e+9, 2e+9, 2.5e+9, 4e+9, 5e+9,
];
/* eslint-enable */
@@ -133,6 +147,8 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
return undefined;
}
const APPROX_BUCKETS = 20;
/**
* @alpha
*/
@@ -142,27 +158,47 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
// if bucket size is auto, try to calc from all numeric fields
if (!bucketSize) {
let min = Infinity,
max = -Infinity;
let allValues: number[] = [];
// TODO: include field configs!
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.number) {
for (const value of field.values.toArray()) {
min = Math.min(min, value);
max = Math.max(max, value);
}
allValues = allValues.concat(field.values.toArray());
}
}
}
let range = Math.abs(max - min);
allValues.sort((a, b) => a - b);
let smallestDelta = Infinity;
// TODO: case of 1 value needs work
if (allValues.length === 1) {
smallestDelta = 1;
} else {
for (let i = 1; i < allValues.length; i++) {
let delta = allValues[i] - allValues[i - 1];
if (delta !== 0) {
smallestDelta = Math.min(smallestDelta, delta);
}
}
}
let min = allValues[0];
let max = allValues[allValues.length - 1];
let range = max - min;
const targetSize = range / APPROX_BUCKETS;
// choose bucket
for (const size of histogramBucketSizes) {
if (range / 10 < size) {
bucketSize = size;
for (let i = 0; i < histogramBucketSizes.length; i++) {
let _bucketSize = histogramBucketSizes[i];
if (targetSize < _bucketSize && _bucketSize >= smallestDelta) {
bucketSize = _bucketSize;
break;
}
}
@@ -172,13 +208,23 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
let histograms: AlignedData[] = [];
let counts: Field[] = [];
let config: FieldConfig | undefined = undefined;
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.number) {
let fieldHist = histogram(field.values.toArray(), getBucket, histFilter, histSort) as AlignedData;
histograms.push(fieldHist);
counts.push({ ...field });
counts.push({
...field,
config: {
...field.config,
unit: undefined,
},
});
if (!config && field.config.unit) {
config = field.config;
}
}
}
}
@@ -202,18 +248,17 @@ export function buildHistogram(frames: DataFrame[], options?: HistogramTransform
}
}
const bucketMin = {
...counts[0],
const bucketMin: Field = {
name: histogramFrameBucketMinFieldName,
values: new ArrayVector(joinedHists[0]),
type: FieldType.number,
state: undefined,
config: config ?? {},
};
const bucketMax = {
...bucketMin,
name: histogramFrameBucketMaxFieldName,
values: new ArrayVector(joinedHists[0].map((v) => v + bucketSize!)),
state: undefined,
};
if (options?.combine) {
@@ -301,7 +346,15 @@ function histogram(
/**
* @internal
*/
export function histogramFieldsToFrame(info: HistogramFields): DataFrame {
export function histogramFieldsToFrame(info: HistogramFields, theme?: GrafanaTheme2): DataFrame {
if (!info.bucketMin.display) {
const display = getDisplayProcessor({
field: info.bucketMin,
theme: theme ?? createTheme(),
});
info.bucketMin.display = display;
info.bucketMax.display = display;
}
return {
fields: [info.bucketMin, info.bucketMax, ...info.counts],
length: info.bucketMin.values.length,
+1 -1
View File
@@ -72,7 +72,7 @@
"react-transition-group": "4.4.1",
"slate": "0.47.8",
"tinycolor2": "1.4.1",
"uplot": "1.6.11"
"uplot": "1.6.12"
},
"devDependencies": {
"@rollup/plugin-commonjs": "16.0.0",
@@ -14,8 +14,9 @@ export default {
},
},
argTypes: {
icon: { control: { type: 'select', options: iconOptions } },
icon: { options: iconOptions, control: { type: 'select' } },
color: { control: 'select' },
text: { control: 'text' },
},
};
@@ -85,7 +85,7 @@ export default {
disable: true,
},
controls: {
exclude: ['className', 'series', 'timeRange', 'ariaLabel'],
exclude: ['className', 'ariaLabel'],
},
},
args: {
@@ -107,7 +107,7 @@ export default {
tooltipMode: { control: { type: 'radio', options: ['multi', 'single'] } },
timeZone: { control: { type: 'radio', options: ['browser', 'utc'] } },
width: { control: { type: 'range', min: 200, max: 800 } },
height: { control: { type: 'range', min: 200, max: 800 } },
height: { control: { type: 'range', min: 200, max: 1700, step: 300 } },
lineWidth: { control: { type: 'range', min: 1, max: 10 } },
},
};
@@ -15,7 +15,7 @@ export default {
disable: true,
},
controls: {
exclude: ['className', 'series', 'timeRange', 'ariaLabel', 'legendDisplayMode'],
exclude: ['className', 'ariaLabel', 'legendDisplayMode'],
},
},
argTypes: {
@@ -24,7 +24,7 @@ export default {
rightAxisSeries: { name: 'Right y-axis series, i.e. A,C' },
timeZone: { control: { type: 'radio', options: ['browser', 'utc'] } },
width: { control: { type: 'range', min: 200, max: 800 } },
height: { control: { type: 'range', min: 200, max: 800 } },
height: { control: { type: 'range', min: 1700, step: 300 } },
lineWidth: { control: { type: 'range', min: 1, max: 10 } },
},
};
@@ -1,7 +1,7 @@
import React, { CSSProperties, ReactNode } from 'react';
import { css } from '@emotion/css';
import { useTheme, useStyles } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { useStyles2, useTheme2 } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data';
/**
* @internal
@@ -31,8 +31,9 @@ export const PanelChrome: React.FC<PanelChromeProps> = ({
padding = 'md',
leftItems = [],
}) => {
const theme = useTheme();
const styles = useStyles(getStyles);
const theme = useTheme2();
const styles = useStyles2(getStyles);
const headerHeight = getHeaderHeight(theme, title, leftItems);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height);
@@ -62,15 +63,21 @@ const itemsRenderer = (items: ReactNode[], renderer: (items: ReactNode[]) => Rea
return toRender.length > 0 ? renderer(toRender) : null;
};
const getHeaderHeight = (theme: GrafanaTheme, title: string, items: ReactNode[]) => {
const getHeaderHeight = (theme: GrafanaTheme2, title: string, items: ReactNode[]) => {
if (title.length > 0 || items.length > 0) {
return theme.panelHeaderHeight;
return theme.spacing.gridSize * theme.components.panel.headerHeight;
}
return 0;
};
const getContentStyle = (padding: string, theme: GrafanaTheme, width: number, headerHeight: number, height: number) => {
const chromePadding = padding === 'md' ? theme.panelPadding : 0;
const getContentStyle = (
padding: string,
theme: GrafanaTheme2,
width: number,
headerHeight: number,
height: number
) => {
const chromePadding = padding === 'md' ? theme.components.panel.padding : 0;
const panelBorder = 1 * 2;
const innerWidth = width - chromePadding * 2 - panelBorder;
const innerHeight = height - headerHeight - chromePadding * 2 - panelBorder;
@@ -82,12 +89,14 @@ const getContentStyle = (padding: string, theme: GrafanaTheme, width: number, he
return { contentStyle, innerWidth, innerHeight };
};
const getStyles = (theme: GrafanaTheme) => {
const getStyles = (theme: GrafanaTheme2) => {
const { padding, background, borderColor } = theme.components.panel;
return {
container: css`
label: panel-container;
background-color: ${theme.colors.panelBg};
border: 1px solid ${theme.colors.panelBorder};
background-color: ${background};
border: 1px solid ${borderColor};
position: relative;
border-radius: 3px;
height: 100%;
@@ -110,12 +119,13 @@ const getStyles = (theme: GrafanaTheme) => {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding-left: ${theme.panelPadding}px;
padding-left: ${theme.spacing(padding)};
flex-grow: 1;
font-weight: ${theme.typography.fontWeightMedium};
`,
leftItems: css`
display: flex;
padding-right: ${theme.panelPadding}px;
padding-right: ${theme.spacing(padding)};
`,
};
};
@@ -16,7 +16,7 @@ export default {
page: mdx,
},
knobs: {
disabled: true,
disable: true,
},
controls: {
exclude: ['onChange', 'options'],
@@ -24,9 +24,9 @@ export default {
},
argTypes: {
variant: {
options: ['primary', 'secondary', 'destructive', 'link'],
control: {
type: 'select',
options: ['primary', 'secondary', 'destructive', 'link'],
},
},
icon: {
@@ -35,6 +35,12 @@ export default {
options: getAvailableIcons(),
},
},
size: {
options: ['sm', 'md', 'lg'],
control: {
type: 'select',
},
},
},
};
const options = generateOptions();
+4
View File
@@ -118,6 +118,10 @@ func (p *AlertingProxy) withReq(
errMessage = message.(string)
}
}
} else if strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html") {
// if Content-Type is text/html
// do not return the body
errMessage = "redacted html"
}
return ErrResp(status, errors.New(errMessage), "")
}
@@ -17,6 +17,8 @@ var (
ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace")
// ErrAlertRuleFailedValidation
ErrAlertRuleFailedValidation = errors.New("invalid alert rule")
// ErrAlertRuleUniqueConstraintViolation
ErrAlertRuleUniqueConstraintViolation = errors.New("a conflicting alert rule is found: rule title under the same organisation and folder should be unique")
)
type NoDataState string
+3
View File
@@ -522,6 +522,9 @@ func (st DBstore) UpdateRuleGroup(cmd UpdateRuleGroupCmd) error {
}
if err := st.UpsertAlertRules(upsertRules); err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return ngmodels.ErrAlertRuleUniqueConstraintViolation
}
return err
}
@@ -7,7 +7,6 @@ import (
)
// AddMigration defines database migrations.
// If Alerting NG is not enabled does nothing.
func AddTablesMigrations(mg *migrator.Migrator) {
AddAlertDefinitionMigrations(mg, 60)
AddAlertDefinitionVersionMigrations(mg)
@@ -194,6 +193,14 @@ func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64)
// add labels column
mg.AddMigration("add column labels to alert_rule", migrator.NewAddColumnMigration(alertRule, &migrator.Column{Name: "labels", Type: migrator.DB_Text, Nullable: true}))
mg.AddMigration("remove unique index from alert_rule on org_id, title columns", migrator.NewDropIndexMigration(alertRule, &migrator.Index{
Cols: []string{"org_id", "title"}, Type: migrator.UniqueIndex,
}))
mg.AddMigration("add index in alert_rule on org_id, namespase_uid and title columns", migrator.NewAddIndexMigration(alertRule, &migrator.Index{
Cols: []string{"org_id", "namespace_uid", "title"}, Type: migrator.UniqueIndex,
}))
}
func AddAlertRuleVersionMigrations(mg *migrator.Migrator) {
+120
View File
@@ -298,3 +298,123 @@ func createRule(t *testing.T, grafanaListedAddr string, folder string, user, pas
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
}
func TestAlertRuleConflictingTitle(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
EnableQuota: true,
DisableAnonymous: true,
ViewersCanEdit: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create the namespace we'll save our alerts to.
_, err := createFolder(t, store, 0, "folder1")
require.NoError(t, err)
_, err = createFolder(t, store, 0, "folder2")
require.NoError(t, err)
// Create user
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
Condition: "A",
Data: []ngmodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: ngmodels.RelativeTimeRange{
From: ngmodels.Duration(time.Duration(5) * time.Hour),
To: ngmodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: "-100",
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ruler/grafana/api/v1/rules/folder1", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
t.Run("trying to create alert with same title under same folder should fail", func(t *testing.T) {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ruler/grafana/api/v1/rules/folder1", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
require.JSONEq(t, `{"message":"failed to update rule group: a conflicting alert rule is found: rule title under the same organisation and folder should be unique"}`, string(b))
})
t.Run("trying to create alert with same title under another folder should succeed", func(t *testing.T) {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ruler/grafana/api/v1/rules/folder2", grafanaListedAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
})
}
+35 -14
View File
@@ -100,6 +100,9 @@ func (e *timeSeriesQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilde
// iterate backwards to create aggregations bottom-down
for _, bucketAgg := range q.BucketAggs {
bucketAgg.Settings = simplejson.NewFromAny(
bucketAgg.generateSettingsForDSL(),
)
switch bucketAgg.Type {
case dateHistType:
aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to)
@@ -182,26 +185,34 @@ func (e *timeSeriesQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilde
return nil
}
// Casts values to int when required by Elastic's query DSL
func (metricAggregation MetricAgg) generateSettingsForDSL(version *semver.Version) map[string]interface{} {
setFloatPath := func(path ...string) {
if stringValue, err := metricAggregation.Settings.GetPath(path...).String(); err == nil {
if value, err := strconv.ParseFloat(stringValue, 64); err == nil {
metricAggregation.Settings.SetPath(path, value)
}
func setFloatPath(settings *simplejson.Json, path ...string) {
if stringValue, err := settings.GetPath(path...).String(); err == nil {
if value, err := strconv.ParseFloat(stringValue, 64); err == nil {
settings.SetPath(path, value)
}
}
}
func setIntPath(settings *simplejson.Json, path ...string) {
if stringValue, err := settings.GetPath(path...).String(); err == nil {
if value, err := strconv.ParseInt(stringValue, 10, 64); err == nil {
settings.SetPath(path, value)
}
}
}
// Casts values to float when required by Elastic's query DSL
func (metricAggregation MetricAgg) generateSettingsForDSL(version *semver.Version) map[string]interface{} {
switch metricAggregation.Type {
case "moving_avg":
setFloatPath("window")
setFloatPath("predict")
setFloatPath("settings", "alpha")
setFloatPath("settings", "beta")
setFloatPath("settings", "gamma")
setFloatPath("settings", "period")
setFloatPath(metricAggregation.Settings, "window")
setFloatPath(metricAggregation.Settings, "predict")
setFloatPath(metricAggregation.Settings, "settings", "alpha")
setFloatPath(metricAggregation.Settings, "settings", "beta")
setFloatPath(metricAggregation.Settings, "settings", "gamma")
setFloatPath(metricAggregation.Settings, "settings", "period")
case "serial_diff":
setFloatPath("lag")
setFloatPath(metricAggregation.Settings, "lag")
}
if isMetricAggregationWithInlineScriptSupport(metricAggregation.Type) {
@@ -225,6 +236,16 @@ func (metricAggregation MetricAgg) generateSettingsForDSL(version *semver.Versio
return metricAggregation.Settings.MustMap()
}
func (bucketAgg BucketAgg) generateSettingsForDSL() map[string]interface{} {
// TODO: This might also need to be applied to other bucket aggregations and other fields.
switch bucketAgg.Type {
case "date_histogram":
setIntPath(bucketAgg.Settings, "min_doc_count")
}
return bucketAgg.Settings.MustMap()
}
func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo string) es.AggBuilder {
aggBuilder.DateHistogram(bucketAgg.ID, bucketAgg.Field, func(a *es.DateHistogramAgg, b es.AggBuilder) {
a.Interval = bucketAgg.Settings.Get("interval").MustString("auto")
@@ -934,6 +934,78 @@ func TestSettingsCasting(t *testing.T) {
assert.Equal(t, 1., serialDiffSettings["lag"])
})
t.Run("Date Histogram Settings", func(t *testing.T) {
t.Run("Correctly transforms date_histogram settings", func(t *testing.T) {
c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{
"timeField": "@timestamp",
"bucketAggs": [
{
"type": "date_histogram",
"field": "@timestamp",
"id": "2",
"settings": {
"min_doc_count": "1"
}
}
],
"metrics": [
{ "id": "1", "type": "average", "field": "@value" },
{
"id": "3",
"type": "serial_diff",
"field": "1",
"pipelineAgg": "1",
"settings": {
"lag": "1"
}
}
]
}`, from, to, 15*time.Second)
assert.Nil(t, err)
sr := c.multisearchRequests[0].Requests[0]
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.Equal(t, 1, dateHistogramAgg.MinDocCount)
})
t.Run("Correctly uses already int min_doc_count", func(t *testing.T) {
c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{
"timeField": "@timestamp",
"bucketAggs": [
{
"type": "date_histogram",
"field": "@timestamp",
"id": "2",
"settings": {
"min_doc_count": 10
}
}
],
"metrics": [
{ "id": "1", "type": "average", "field": "@value" },
{
"id": "3",
"type": "serial_diff",
"field": "1",
"pipelineAgg": "1",
"settings": {
"lag": "1"
}
}
]
}`, from, to, 15*time.Second)
assert.Nil(t, err)
sr := c.multisearchRequests[0].Requests[0]
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.Equal(t, 10, dateHistogramAgg.MinDocCount)
})
})
t.Run("Inline Script", func(t *testing.T) {
t.Run("Correctly handles scripts for ES < 5.6", func(t *testing.T) {
c := newFakeClient("5.0.0")
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { gt, satisfies } from 'semver';
import { config } from '@grafana/runtime';
@@ -25,6 +25,7 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => {
const [shouldUpdate, setShouldUpdate] = useState(
remotePlugin?.version && localPlugin?.info.version && gt(remotePlugin?.version!, localPlugin?.info.version!)
);
const [hasInstalledPanel, setHasInstalledPanel] = useState(false);
const isExternallyManaged = config.pluginAdminExternalManageEnabled;
const externalManageLink = getExternalManageLink(remotePlugin);
@@ -37,6 +38,7 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => {
appEvents.emit(AppEvents.alertSuccess, [`Installed ${remotePlugin?.name}`]);
setLoading(false);
setIsInstalled(true);
setHasInstalledPanel(remotePlugin.typeCode === 'panel');
} catch (error) {
setLoading(false);
}
@@ -115,6 +117,11 @@ export const InstallControls = ({ localPlugin, remotePlugin }: Props) => {
<Button variant="destructive" disabled={loading || !hasPermission} onClick={onUninstall}>
{loading && !shouldUpdate ? 'Uninstalling' : 'Uninstall'}
</Button>
{hasInstalledPanel && (
<div className={cx(styles.message, styles.messageMargin)}>
Please refresh your browser window before using this plugin.
</div>
)}
{!hasPermission && <div className={styles.message}>You need admin privileges to manage this plugin.</div>}
</>
)}
@@ -158,6 +165,9 @@ export const getStyles = (theme: GrafanaTheme2) => {
message: css`
color: ${theme.colors.text.secondary};
`,
messageMargin: css`
margin-left: ${theme.spacing()};
`,
readme: css`
margin: ${theme.spacing(3)} 0;
@@ -198,7 +198,9 @@ export function restoreCustomOverrideRules(current: FieldConfigSource, old: Fiel
if (isCustomFieldProp(prop)) {
const currentOverride = result.overrides.find((o) => isEqual(o.matcher, override.matcher));
if (currentOverride) {
currentOverride.properties.push(prop);
if (currentOverride !== override) {
currentOverride.properties.push(prop);
}
} else {
result.overrides.push(override);
}
@@ -13,6 +13,8 @@ const MAX_LABEL_COUNT = 10000;
const MAX_VALUE_COUNT = 50000;
const EMPTY_SELECTOR = '{}';
const METRIC_LABEL = '__name__';
const LIST_ITEM_SIZE = 25;
export const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels';
export interface BrowserProps {
@@ -105,7 +107,7 @@ export function facetLabels(
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css`
background-color: ${theme.colors.bg2};
padding: ${theme.spacing.md};
padding: ${theme.spacing.sm};
width: 100%;
`,
list: css`
@@ -114,6 +116,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
flex-wrap: wrap;
max-height: 200px;
overflow: auto;
align-content: flex-start;
`,
section: css`
& + & {
@@ -177,6 +180,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
* to create a single, generic component.
*/
export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserProps, BrowserState> {
valueListsRef = React.createRef<HTMLDivElement>();
state: BrowserState = {
labels: [] as SelectableLabel[],
labelSearchTerm: '',
@@ -439,7 +443,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
const { languageProvider } = this.props;
this.setState({ validationStatus: `Validating selector ${selector}`, error: '' });
const streams = await languageProvider.fetchSeries(selector);
this.setState({ validationStatus: `Selector is valid (${streams.length} streams found)` });
this.setState({ validationStatus: `Selector is valid (${streams.length} series found)` });
}
render() {
@@ -479,12 +483,14 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
}
const selector = buildSelector(this.state.labels);
const empty = selector === EMPTY_SELECTOR;
const metricCount = metrics?.values?.length || 0;
return (
<div className={styles.wrapper}>
<HorizontalGroup align="flex-start" spacing="lg">
<div>
<div className={styles.section}>
<Label description="Which metric do you want to use?">1. Select metric to search in</Label>
<Label description="Once a metric is selected only possible labels are shown.">1. Select a metric</Label>
<div>
<Input
onChange={this.onChangeMetricSearch}
@@ -494,9 +500,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
</div>
<div role="list" className={styles.valueListWrapper}>
<FixedSizeList
height={550}
itemCount={metrics?.values?.length || 0}
itemSize={25}
height={Math.min(450, metricCount * LIST_ITEM_SIZE)}
itemCount={metricCount}
itemSize={LIST_ITEM_SIZE}
itemKey={(i) => (metrics!.values as FacettableValue[])[i].name}
width={300}
className={styles.valueList}
@@ -526,7 +532,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
<div>
<div className={styles.section}>
<Label description="Which labels would you like to consider for your search?">
<Label description="Once label values are selected, only possible label combinations are shown.">
2. Select labels to search in
</Label>
<div>
@@ -536,7 +542,8 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
value={labelSearchTerm}
/>
</div>
<div className={styles.list}>
{/* Using fixed height here to prevent jumpy layout */}
<div className={styles.list} style={{ height: 120 }}>
{nonMetricLabels.map((label) => (
<PromLabel
key={label.name}
@@ -552,8 +559,8 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
</div>
</div>
<div className={styles.section}>
<Label description="Choose the label values that you would like to use for the query. Use the search field to find values across selected labels.">
3. Find values for the selected labels
<Label description="Use the search field to find values across selected labels.">
3. Select (multiple) values for your labels
</Label>
<div>
<Input
@@ -562,7 +569,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
value={valueSearchTerm}
/>
</div>
<div className={styles.valueListArea}>
<div className={styles.valueListArea} ref={this.valueListsRef}>
{selectedLabels.map((label) => (
<div
role="list"
@@ -582,7 +589,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
/>
</div>
<FixedSizeList
height={200}
height={Math.min(200, LIST_ITEM_SIZE * (label.values?.length || 0))}
itemCount={label.values?.length || 0}
itemSize={25}
itemKey={(i) => (label.values as FacettableValue[])[i].name}
@@ -622,7 +629,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
{validationStatus && <div className={styles.validationStatus}>{validationStatus}</div>}
<HorizontalGroup>
<Button aria-label="Use selector for query button" disabled={empty} onClick={this.onClickRunQuery}>
Run query
Use query
</Button>
<Button
aria-label="Use selector as metrics button"
@@ -630,7 +637,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
disabled={empty}
onClick={this.onClickRunRateQuery}
>
Run rate query
Use as rate query
</Button>
<Button
aria-label="Validate submit button"
@@ -38,6 +38,7 @@ function incrRoundUp(num: number, incr: number) {
export interface HistogramProps extends Themeable2 {
options: PanelOptions; // used for diff
alignedFrame: DataFrame; // This could take HistogramFields
bucketSize: number;
width: number;
height: number;
structureRev?: number; // a number that will change when the frames[] structure changes
@@ -45,13 +46,18 @@ export interface HistogramProps extends Themeable2 {
children?: (builder: UPlotConfigBuilder, frame: DataFrame) => React.ReactNode;
}
export function getBucketSize(frame: DataFrame) {
// assumes BucketMin is fields[0] and BucktMax is fields[1]
return frame.fields[1].values.get(0) - frame.fields[0].values.get(0);
}
const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
// todo: scan all values in BucketMin and BucketMax fields to assert if uniform bucketSize
let builder = new UPlotConfigBuilder();
// assumes BucketMin is fields[0] and BucktMax is fields[1]
let bucketSize = frame.fields[1].values.get(0) - frame.fields[0].values.get(0);
let bucketSize = getBucketSize(frame);
// splits shifter, to ensure splits always start at first bucket
let xSplits: uPlot.Axis.Splits = (u, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace) => {
@@ -251,13 +257,14 @@ export class Histogram extends React.Component<HistogramProps, State> {
}
componentDidUpdate(prevProps: HistogramProps) {
const { structureRev, alignedFrame } = this.props;
const { structureRev, alignedFrame, bucketSize } = this.props;
if (alignedFrame !== prevProps.alignedFrame) {
let newState = this.prepState(this.props, false);
if (newState) {
const shouldReconfig =
bucketSize !== prevProps.bucketSize ||
this.props.options !== prevProps.options ||
this.state.config === undefined ||
structureRev !== prevProps.structureRev ||
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { PanelProps, buildHistogram, getHistogramFields } from '@grafana/data';
import { Histogram } from './Histogram';
import { Histogram, getBucketSize } from './Histogram';
import { PanelOptions } from './models.gen';
import { useTheme2 } from '@grafana/ui';
@@ -27,8 +27,8 @@ export const HistogramPanel: React.FC<Props> = ({ data, options, width, height }
return undefined;
}
return histogramFieldsToFrame(hist);
}, [data.series, options]);
return histogramFieldsToFrame(hist, theme);
}, [data.series, options, theme]);
if (!histogram || !histogram.fields.length) {
return (
@@ -38,6 +38,8 @@ export const HistogramPanel: React.FC<Props> = ({ data, options, width, height }
);
}
const bucketSize = getBucketSize(histogram);
return (
<Histogram
options={options}
@@ -47,6 +49,7 @@ export const HistogramPanel: React.FC<Props> = ({ data, options, width, height }
width={width}
height={height}
alignedFrame={histogram}
bucketSize={bucketSize}
>
{(config, alignedFrame) => {
return null; // <TooltipPlugin data={alignedFrame} config={config} mode={options.tooltip.mode} timeZone={timeZone} />;
+4 -4
View File
@@ -22043,10 +22043,10 @@ update-notifier@^2.5.0:
semver-diff "^2.0.0"
xdg-basedir "^3.0.0"
uplot@1.6.11:
version "1.6.11"
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.11.tgz#2ade33071e4eb30c6504ad7c68eb4c05d9dd47eb"
integrity sha512-DTA/wLLFSccSghZKM02hLgHoe8zDoCGFEkbbszXwkNRFgkkRKYc0gPNaJG76JFrwDlZNc7+u9noUHxt9fLu+lg==
uplot@1.6.12:
version "1.6.12"
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.12.tgz#a2741f224c3d3b4f4f25a53797c1687ddfef2989"
integrity sha512-afBjwy/9SM0E7w29Gen+wQ+UAe6ipiA2zE/s1dMCidMXQYvYkCpEgxLRkVA179/8eRJz+og2r7ZFS1HIzT75fQ==
upper-case@^1.1.1:
version "1.1.3"