From caa49f8d14aeaeea89a1257c827928a2a03da82a Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Wed, 25 May 2022 11:19:37 -0600 Subject: [PATCH 001/283] Explore: Add ability to include tags in trace to metrics queries (#49433) * Add tags input * Tracing: add ability to include tags in trace to metrics queries --- docs/sources/datasources/jaeger.md | 9 ++++- docs/sources/datasources/tempo.md | 9 ++++- docs/sources/datasources/zipkin.md | 3 +- .../components/TraceToLogs/KeyValueInput.tsx | 3 ++ .../TraceToMetrics/TraceToMetricsSettings.tsx | 23 +++++++++++-- .../explore/TraceView/createSpanLink.test.ts | 34 +++++++++++++++++++ .../explore/TraceView/createSpanLink.tsx | 31 ++++++++++++++--- 7 files changed, 103 insertions(+), 9 deletions(-) diff --git a/docs/sources/datasources/jaeger.md b/docs/sources/datasources/jaeger.md index 001574b4c45..fb1c841c8b4 100644 --- a/docs/sources/datasources/jaeger.md +++ b/docs/sources/datasources/jaeger.md @@ -47,11 +47,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t To configure trace to metrics, select the target Prometheus data source and create any desired linked queries. -- **Data source -** Target data source. +-- **Tags -** You can use tags in the linked queries. The key is the span attribute name. The optional value is the corresponding metric label name (for example, map `k8s.pod` to `pod`). You may interpolate these tags into your queries using the `$__tags` keyword. Each linked query consists of: -- **Link Label -** (Optional) Descriptive label for the linked query. --- **Query -** Query that runs when navigating from a trace to the metrics data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. Interpolate tags using the `$__tags` keyword. For example, when you configure the query `requests_total{$__tags}`with the tags `k8s.pod=pod` and `cluster`, it results in `requests_total{pod="nginx-554b9", cluster="us-east-1"}`. ### Node Graph @@ -169,6 +170,12 @@ datasources: spanEndTimeShift: '1h' filterByTraceID: false filterBySpanID: false + tracesToMetrics: + datasourceUid: 'prom' + tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] + queries: + - name: 'Sample query' + query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' secureJsonData: basicAuthPassword: my_password ``` diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md index 7993df800f8..7f9d0040d00 100644 --- a/docs/sources/datasources/tempo.md +++ b/docs/sources/datasources/tempo.md @@ -46,11 +46,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t To configure trace to metrics, select the target Prometheus data source and create any desired linked queries. -- **Data source -** Target data source. +-- **Tags -** You can use tags in the linked queries. The key is the span attribute name. The optional value is the corresponding metric label name (for example, map `k8s.pod` to `pod`). You may interpolate these tags into your queries using the `$__tags` keyword. Each linked query consists of: -- **Link Label -** (Optional) Descriptive label for the linked query. --- **Query -** Query that runs when navigating from a trace to the metrics data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. Interpolate tags using the `$__tags` keyword. For example, when you configure the query `requests_total{$__tags}`with the tags `k8s.pod=pod` and `cluster`, it results in `requests_total{pod="nginx-554b9", cluster="us-east-1"}`. ### Service Graph @@ -216,6 +217,12 @@ datasources: spanEndTimeShift: '1h' filterByTraceID: false filterBySpanID: false + tracesToMetrics: + datasourceUid: 'prom' + tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }] + queries: + - name: 'Sample query' + query: 'sum(rate(tempo_spanmetrics_latency_bucket{$__tags}[5m]))' serviceMap: datasourceUid: 'prometheus' search: diff --git a/docs/sources/datasources/zipkin.md b/docs/sources/datasources/zipkin.md index 88ba196ce12..a4284ad2177 100644 --- a/docs/sources/datasources/zipkin.md +++ b/docs/sources/datasources/zipkin.md @@ -47,11 +47,12 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t To configure trace to metrics, select the target Prometheus data source and create any desired linked queries. -- **Data source -** Target data source. +-- **Tags -** You can use tags in the linked queries. The key is the span attribute name. The optional value is the corresponding metric label name (for example, map `k8s.pod` to `pod`). You may interpolate these tags into your queries using the `$__tags` keyword. Each linked query consists of: -- **Link Label -** (Optional) Descriptive label for the linked query. --- **Query -** Query that runs when navigating from a trace to the metrics data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. Interpolate tags using the `$__tags` keyword. For example, when you configure the query `requests_total{$__tags}`with the tags `k8s.pod=pod` and `cluster`, it results in `requests_total{pod="nginx-554b9", cluster="us-east-1"}`. ### Node Graph diff --git a/public/app/core/components/TraceToLogs/KeyValueInput.tsx b/public/app/core/components/TraceToLogs/KeyValueInput.tsx index 45934073fdb..7d648c10293 100644 --- a/public/app/core/components/TraceToLogs/KeyValueInput.tsx +++ b/public/app/core/components/TraceToLogs/KeyValueInput.tsx @@ -65,6 +65,7 @@ const KeyValueInput = ({ onClick={() => onChange([...values.slice(0, idx), ...values.slice(idx + 1)])} className="gf-form-label query-part" aria-label="Remove tag" + type="button" > @@ -73,6 +74,7 @@ const KeyValueInput = ({ onClick={() => onChange([...values, { key: '', value: '' }])} className="gf-form-label query-part" aria-label="Add tag" + type="button" > @@ -84,6 +86,7 @@ const KeyValueInput = ({ onClick={() => onChange([...values, { key: '', value: '' }])} className="gf-form-label query-part" aria-label="Add tag" + type="button" > diff --git a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx index 4612892f0d8..3bc5c202f05 100644 --- a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx +++ b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx @@ -5,19 +5,23 @@ import { DataSourceJsonData, DataSourcePluginOptionsEditorProps, GrafanaTheme, + KeyValue, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; import { DataSourcePicker } from '@grafana/runtime'; import { Button, InlineField, InlineFieldRow, Input, useStyles } from '@grafana/ui'; +import KeyValueInput from '../TraceToLogs/KeyValueInput'; + export interface TraceToMetricsOptions { datasourceUid?: string; + tags?: Array>; queries: TraceToMetricQuery[]; } export interface TraceToMetricQuery { name?: string; - query: string; + query?: string; } export interface TraceToMetricsData extends DataSourceJsonData { @@ -71,6 +75,21 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) { ) : null} + + + + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', { + ...options.jsonData.tracesToMetrics, + tags: v, + }) + } + /> + + + {options.jsonData.tracesToMetrics?.queries?.map((query, i) => (
@@ -92,7 +111,7 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) { { }); }); + it('correctly interpolates span attributes', () => { + const splitOpenFn = jest.fn(); + const createLink = createSpanLinkFactory({ + splitOpenFn, + traceToMetricsOptions: { + datasourceUid: 'prom1', + queries: [{ name: 'Named Query', query: 'metric{$__tags}[5m]' }], + tags: [ + { key: 'job', value: '' }, + { key: 'k8s.pod', value: 'pod' }, + ], + } as TraceToMetricsOptions, + }); + expect(createLink).toBeDefined(); + + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'job', value: 'tns/app' }, + { key: 'k8s.pod', value: 'sample-pod' }, + ], + }, + }) + ); + expect(links).toBeDefined(); + expect(links!.metricLinks![0]!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1","queries":[{"expr":"metric{job=\\"tns/app\\", pod=\\"sample-pod\\"}[5m]","refId":"A"}],"panelsState":{}}' + )}` + ); + }); + describe('should return span links', () => { beforeAll(() => { setDataSourceSrv(new DatasourceSrv()); diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index f9b009dff03..57db5e73979 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -20,7 +20,7 @@ import { getTemplateSrv } from '@grafana/runtime'; import { Icon } from '@grafana/ui'; import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; -import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; +import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; @@ -150,10 +150,9 @@ function legacyCreateSpanLinkFactory( // Get metrics links if (metricsDataSourceSettings && traceToMetricsOptions?.queries) { - const defaultQuery = `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`; - links.metricLinks = []; for (const query of traceToMetricsOptions.queries) { + const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags, span); const dataLink: DataLink = { title: metricsDataSourceSettings.name, url: '', @@ -161,7 +160,7 @@ function legacyCreateSpanLinkFactory( datasourceUid: metricsDataSourceSettings.uid, datasourceName: metricsDataSourceSettings.name, query: { - expr: query.query || defaultQuery, + expr, refId: 'A', }, }, @@ -362,3 +361,27 @@ function getTimeRangeFromSpan( }, }; } + +// Interpolates span attributes into trace to metric query, or returns default query +function buildMetricsQuery(query: TraceToMetricQuery, tags: Array> = [], span: TraceSpan): string { + if (!query.query) { + return `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`; + } + + let expr = query.query; + if (tags.length && expr.indexOf('$__tags') !== -1) { + const spanTags = [...span.process.tags, ...span.tags]; + const labels = tags.reduce((acc, tag) => { + const tagValue = spanTags.find((t) => t.key === tag.key)?.value; + if (tagValue) { + acc.push(`${tag.value ? tag.value : tag.key}="${tagValue}"`); + } + return acc; + }, [] as string[]); + + const labelsQuery = labels?.join(', '); + expr = expr.replace('$__tags', labelsQuery); + } + + return expr; +} From 2449f62dbe0d7dbf2086da71eb04c307957958e4 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Wed, 25 May 2022 10:21:51 -0700 Subject: [PATCH 002/283] Canvas: Improve changing element options UX (#49555) --- public/app/features/canvas/runtime/frame.tsx | 2 +- public/app/features/canvas/runtime/scene.tsx | 4 ++-- .../plugins/panel/canvas/editor/PlacementEditor.tsx | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/canvas/runtime/frame.tsx b/public/app/features/canvas/runtime/frame.tsx index d09be5d7af5..cd1e1364d58 100644 --- a/public/app/features/canvas/runtime/frame.tsx +++ b/public/app/features/canvas/runtime/frame.tsx @@ -89,7 +89,7 @@ export class FrameState extends ElementState { reinitializeMoveable() { // Need to first clear current selection and then re-init moveable with slight delay this.scene.clearCurrentSelection(); - setTimeout(() => this.scene.initMoveable(true, this.scene.isEditingEnabled), 100); + setTimeout(() => this.scene.initMoveable(true, this.scene.isEditingEnabled)); } // ??? or should this be on the element directly? diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index e86919a43c6..32719381ca6 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -106,7 +106,7 @@ export class Scene { this.currentLayer = this.root; this.selection.next([]); } - }, 100); + }); return this.root; } @@ -226,7 +226,7 @@ export class Scene { if (this.div) { this.initMoveable(true, this.isEditingEnabled); } - }, 100); + }); } }; diff --git a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx index 1080cd38179..81cbc37e423 100644 --- a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx @@ -48,6 +48,12 @@ export const PlacementEditor: FC { + setTimeout(() => { + settings.scene.select({ targets: [element.div!] }); + }); + }; + const onHorizontalConstraintSelect = (h: SelectableValue) => { onHorizontalConstraintChange(h.value!); }; @@ -57,6 +63,7 @@ export const PlacementEditor: FC) => { @@ -68,16 +75,14 @@ export const PlacementEditor: FC { element.options.placement![placement] = value ?? element.options.placement![placement]; element.applyLayoutStylesToDiv(); settings.scene.clearCurrentSelection(true); - // TODO: This needs to have a better sync method with where div is - setTimeout(() => { - settings.scene.select({ targets: [element.div!] }); - }, 100); + reselectElementAfterChange(); }; const constraint = element.tempConstraint ?? layout ?? {}; From 5caf97be40341e506c742fef8ea69297e65330b5 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 25 May 2022 20:40:41 +0200 Subject: [PATCH 003/283] AccessControl: Replace IsEnterprise checks with license checks (#49572) --- pkg/api/api.go | 4 +- pkg/api/common_test.go | 7 ++- pkg/api/org_users.go | 9 ++-- pkg/models/org_user.go | 2 + .../ossaccesscontrol/permissions_services.go | 12 ++--- .../accesscontrol/resourcepermissions/api.go | 2 +- .../resourcepermissions/service.go | 15 ++++-- .../resourcepermissions/service_test.go | 9 +++- .../guardian/accesscontrol_guardian_test.go | 8 ++- pkg/services/licensing/licensingtest/fake.go | 52 +++++++++++++++++++ pkg/services/sqlstore/org_users.go | 3 +- 11 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 pkg/services/licensing/licensingtest/fake.go diff --git a/pkg/api/api.go b/pkg/api/api.go index d017b7d8465..5f691bbaf08 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -250,15 +250,15 @@ func (hs *HTTPServer) registerRoutes() { // current org without requirement of user to be org admin apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { lookupEvaluator := func() ac.Evaluator { - if hs.Cfg.IsEnterprise { + if hs.License.FeatureEnabled("accesscontrol.enforcement") { return ac.EvalPermission(ac.ActionOrgUsersRead) } // For oss we allow users with access to update permissions on either folders, teams or dashboards to perform the lookup return ac.EvalAny( ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionTeamsPermissionsWrite), - ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite), ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite), + ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite), ) } orgRoute.Get("/users/lookup", authorize(reqOrgAdminFolderAdminOrTeamAdmin, lookupEvaluator()), routing.Wrap(hs.GetOrgUsersForCurrentOrgLookup)) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 2df657da44a..0cdc01c4496 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -238,6 +238,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin hs := &HTTPServer{ Cfg: cfg, Live: newTestLive(t, store), + License: &licensing.OSSLicensingService{}, Features: featuremgmt.WithFeatures(), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routing.NewRouteRegister(), @@ -327,6 +328,7 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { return &HTTPServer{ Cfg: cfg, Features: features, + License: &licensing.OSSLicensingService{}, AccessControl: accesscontrolmock.New().WithDisabled(), } } @@ -390,7 +392,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo acmock = acmock.WithDisabled() } hs.AccessControl = acmock - teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, acmock, database.ProvideService(db)) + teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, acmock, database.ProvideService(db), hs.License) require.NoError(t, err) hs.teamPermissionsService = teamPermissionService } else { @@ -402,7 +404,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo require.NoError(t, err) err = ac.RegisterFixedRoles(context.Background()) require.NoError(t, err) - teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, database.ProvideService(db)) + teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, database.ProvideService(db), hs.License) require.NoError(t, err) hs.teamPermissionsService = teamPermissionService } @@ -463,6 +465,7 @@ func SetupAPITestServer(t *testing.T, opts ...APITestServerOption) *webtest.Serv hs := &HTTPServer{ RouteRegister: routing.NewRouteRegister(), Cfg: setting.NewCfg(), + License: &licensing.OSSLicensingService{}, AccessControl: accesscontrolmock.New().WithDisabled(), Features: featuremgmt.WithFeatures(), searchUsersService: &searchusers.OSSService{}, diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index b10aae80c45..7d07bc086a2 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -89,10 +89,11 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re // GET /api/org/users/lookup func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) response.Response { orgUsers, err := hs.getOrgUsersHelper(c, &models.GetOrgUsersQuery{ - OrgId: c.OrgId, - Query: c.Query("query"), - Limit: c.QueryInt("limit"), - User: c.SignedInUser, + OrgId: c.OrgId, + Query: c.Query("query"), + Limit: c.QueryInt("limit"), + User: c.SignedInUser, + DontEnforceAccessControl: !hs.License.FeatureEnabled("accesscontrol.enforcement"), }, c.SignedInUser) if err != nil { diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index e08a9352ee3..0b91f064956 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -119,6 +119,8 @@ type GetOrgUsersQuery struct { OrgId int64 Query string Limit int + // Flag used to allow oss edition to query users without access control + DontEnforceAccessControl bool User *SignedInUser Result []*OrgUserDTO diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 65f4621bb7f..e9b704cf548 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -35,7 +35,7 @@ var ( func ProvideTeamPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - ac accesscontrol.AccessControl, store resourcepermissions.Store, + ac accesscontrol.AccessControl, store resourcepermissions.Store, license models.Licensing, ) (*TeamPermissionsService, error) { options := resourcepermissions.Options{ Resource: "teams", @@ -91,7 +91,7 @@ func ProvideTeamPermissions( }, } - srv, err := resourcepermissions.New(options, cfg, router, ac, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, ac, store, sql) if err != nil { return nil, err } @@ -109,7 +109,7 @@ var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.Act func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store, - dashboardStore dashboards.Store, + license models.Licensing, dashboardStore dashboards.Store, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) { query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} @@ -164,7 +164,7 @@ func ProvideDashboardPermissions( RoleGroup: "Dashboards", } - srv, err := resourcepermissions.New(options, cfg, router, ac, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, ac, store, sql) if err != nil { return nil, err } @@ -182,7 +182,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol func ProvideFolderPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store, - dashboardStore dashboards.Store, + license models.Licensing, dashboardStore dashboards.Store, ) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ Resource: "folders", @@ -213,7 +213,7 @@ func ProvideFolderPermissions( WriterRoleName: "Folder permission writer", RoleGroup: "Folders", } - srv, err := resourcepermissions.New(options, cfg, router, accesscontrol, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, store, sql) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index 53fbbf55399..c89ff795af7 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -118,7 +118,7 @@ func (a *api) getPermissions(c *models.ReqContext) response.Response { return response.Error(http.StatusInternalServerError, "failed to get permissions", err) } - if a.service.options.Assignments.BuiltInRoles && !a.service.cfg.IsEnterprise { + if a.service.options.Assignments.BuiltInRoles && !a.service.license.FeatureEnabled("accesscontrol.enforcement") { permissions = append(permissions, accesscontrol.ResourcePermission{ Actions: a.service.actions, Scope: "*", diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 90291f16a26..5eb0b1ece9e 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -46,7 +46,10 @@ type Store interface { GetResourcePermissions(ctx context.Context, orgID int64, query types.GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error) } -func New(options Options, cfg *setting.Cfg, router routing.RouteRegister, ac accesscontrol.AccessControl, store Store, sqlStore *sqlstore.SQLStore) (*Service, error) { +func New( + options Options, cfg *setting.Cfg, router routing.RouteRegister, license models.Licensing, + ac accesscontrol.AccessControl, store Store, sqlStore *sqlstore.SQLStore, +) (*Service, error) { var permissions []string actionSet := make(map[string]struct{}) for permission, actions := range options.PermissionsToActions { @@ -71,6 +74,7 @@ func New(options Options, cfg *setting.Cfg, router routing.RouteRegister, ac acc cfg: cfg, store: store, options: options, + license: license, permissions: permissions, actions: actions, sqlStore: sqlStore, @@ -89,10 +93,11 @@ func New(options Options, cfg *setting.Cfg, router routing.RouteRegister, ac acc // Service is used to create access control sub system including api / and service for managed resource permission type Service struct { - cfg *setting.Cfg - ac accesscontrol.AccessControl - store Store - api *api + cfg *setting.Cfg + ac accesscontrol.AccessControl + store Store + api *api + license models.Licensing options Options permissions []string diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index 622ef8ad617..f5fc1cb0fcb 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/database" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -221,8 +222,12 @@ func setupTestEnvironment(t *testing.T, permissions []*accesscontrol.Permission, sql := sqlstore.InitTestDB(t) store := database.ProvideService(sql) cfg := setting.NewCfg() - cfg.IsEnterprise = true - service, err := New(ops, cfg, routing.NewRouteRegister(), accesscontrolmock.New().WithPermissions(permissions), store, sql) + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() + service, err := New( + ops, cfg, routing.NewRouteRegister(), license, + accesscontrolmock.New().WithPermissions(permissions), store, sql, + ) require.NoError(t, err) return service, sql diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 00ec21bd7e9..f1c540b76d6 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" dashdb "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -594,11 +595,14 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*acc }) require.NoError(t, err) ac := accesscontrolmock.New().WithPermissions(permissions) + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() + folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store), &dashboards.FakeDashboardStore{}) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store), license, &dashboards.FakeDashboardStore{}) require.NoError(t, err) dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store), &dashboards.FakeDashboardStore{}) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store), license, &dashboards.FakeDashboardStore{}) require.NoError(t, err) if dashboardSvc == nil { dashboardSvc = &dashboards.FakeDashboardService{} diff --git a/pkg/services/licensing/licensingtest/fake.go b/pkg/services/licensing/licensingtest/fake.go new file mode 100644 index 00000000000..f5d8454ece1 --- /dev/null +++ b/pkg/services/licensing/licensingtest/fake.go @@ -0,0 +1,52 @@ +package licensingtest + +import ( + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/models" +) + +var _ models.Licensing = new(FakeLicensing) + +func NewFakeLicensing() *FakeLicensing { + return &FakeLicensing{&mock.Mock{}} +} + +type FakeLicensing struct { + *mock.Mock +} + +func (f *FakeLicensing) Expiry() int64 { + mockedArgs := f.Called() + return mockedArgs.Get(0).(int64) +} + +func (f *FakeLicensing) Edition() string { + mockedArgs := f.Called() + return mockedArgs.Get(0).(string) +} + +func (f *FakeLicensing) ContentDeliveryPrefix() string { + mockedArgs := f.Called() + return mockedArgs.Get(0).(string) +} + +func (f *FakeLicensing) LicenseURL(showAdminLicensingPage bool) string { + mockedArgs := f.Called(showAdminLicensingPage) + return mockedArgs.Get(0).(string) +} + +func (f *FakeLicensing) StateInfo() string { + mockedArgs := f.Called() + return mockedArgs.Get(0).(string) +} + +func (f *FakeLicensing) EnabledFeatures() map[string]bool { + mockedArgs := f.Called() + return mockedArgs.Get(0).(map[string]bool) +} + +func (f *FakeLicensing) FeatureEnabled(feature string) bool { + mockedArgs := f.Called(feature) + return mockedArgs.Get(0).(bool) +} diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 65eb4a67c57..1afea95b156 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -112,7 +112,8 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu if query.User == nil { ss.log.Warn("Query user not set for filtering.") } - if ss.Cfg.IsEnterprise && !accesscontrol.IsDisabled(ss.Cfg) { + + if !query.DontEnforceAccessControl && !accesscontrol.IsDisabled(ss.Cfg) { acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead) if err != nil { return err From 7ed368ecc6b9fb00e62b2022d7c54160b9c25004 Mon Sep 17 00:00:00 2001 From: Maria Alexandra <239999+axelavargas@users.noreply.github.com> Date: Wed, 25 May 2022 20:57:18 +0200 Subject: [PATCH 004/283] SearchV2 - Fix starred dashboards for new organizations error (#49645) Co-authored-by: Ryan McKinley --- pkg/api/stars.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/api/stars.go b/pkg/api/stars.go index 15194d95938..73eeae19ab1 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -27,10 +27,11 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { OrgId: c.OrgId, } err := hs.dashboardService.GetDashboard(c.Req.Context(), query) - if err != nil { - return response.Error(500, "Failed to get dashboard", err) + + // Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org + if err == nil { + uids = append(uids, query.Result.Uid) } - uids = append(uids, query.Result.Uid) } return response.JSON(200, uids) } From 1d7d8bbf960a77c9784dccf664f844f8d9d7bf80 Mon Sep 17 00:00:00 2001 From: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> Date: Wed, 25 May 2022 15:07:50 -0400 Subject: [PATCH 005/283] Updated document with Michelle Tan's comments (#49648) --- docs/sources/whatsnew/whats-new-in-v9-0.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v9-0.md b/docs/sources/whatsnew/whats-new-in-v9-0.md index ca241500986..58411e37263 100644 --- a/docs/sources/whatsnew/whats-new-in-v9-0.md +++ b/docs/sources/whatsnew/whats-new-in-v9-0.md @@ -9,15 +9,15 @@ weight = -33 list = false +++ -# What’s new in Grafana v9.0 +# What’s new in Grafana v9.0 ((beta)) As tradition goes, GrafanaCon - our yearly community event for Grafana open source users, is also where we launch the latest software release of Grafana. Keeping up with tradition, we are excited to be announcing Grafana v9.0 - a release that elevates Grafana’s ease of use, discovery of data through new and improved visualizations and a default unified alerting experience. A big focus for Grafana is making observability and data visualization and analytics easier and more accessible for everyone. For popular data sources like Prometheus and Loki writing and understanding queries can be hard. This is why we are excited to announce that Grafana 9 comes with new visual query builders for both these data sources. These visual query builders will lower the barrier of entry and they help anyone to compose, understand and learn how the underlying query languages. -The release also includes a brand new powerful and fast heatmap visualization, a more accessible navigation menu, improvements to dashboard search, advanced security and authentication features, and more. +The release also includes a brand-new powerful and fast heatmap visualization, a more accessible navigation menu, improvements to dashboard search, advanced security and authentication features, and more. -We’ve summarized what’s new in the release here, but you might also be interested in the announcement blog post as well. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). +We’ve summarized what’s new in the beta release here. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). ## Prometheus query builder From 3ecee0663085cf4dc80e53f80a5ee8545cbf50e0 Mon Sep 17 00:00:00 2001 From: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com> Date: Wed, 25 May 2022 12:31:49 -0700 Subject: [PATCH 006/283] Docs: Identify which Grafana editions are relevant to each Enterprise doc (#49207) * Add section to Ent docs index re: Cloud features * Add and update notes identifying Enterprise and Cloud features * Address feedback --- docs/sources/enterprise/_index.md | 22 +++++++++------- docs/sources/enterprise/auditing.md | 6 ++--- .../enterprise/datasource_permissions.md | 2 +- docs/sources/enterprise/enhanced_ldap.md | 8 +++--- .../enterprise/enterprise-configuration.md | 10 +++---- docs/sources/enterprise/export-pdf.md | 8 +++--- docs/sources/enterprise/query-caching.md | 4 ++- docs/sources/enterprise/recorded-queries.md | 2 ++ docs/sources/enterprise/reporting.md | 26 ++++++++++--------- docs/sources/enterprise/request-security.md | 11 +++----- docs/sources/enterprise/saml/_index.md | 2 +- docs/sources/enterprise/saml/about-saml.md | 2 +- .../sources/enterprise/saml/configure-saml.md | 14 +++++----- docs/sources/enterprise/settings-updates.md | 6 ++--- docs/sources/enterprise/team-sync.md | 2 +- .../enterprise/usage-insights/_index.md | 2 ++ .../dashboard-datasource-insights.md | 4 +-- .../enterprise/usage-insights/export-logs.md | 2 +- .../usage-insights/improved-search.md | 2 +- .../usage-insights/presence-indicator.md | 2 +- docs/sources/enterprise/vault.md | 5 ++-- docs/sources/enterprise/white-labeling.md | 2 +- 22 files changed, 75 insertions(+), 69 deletions(-) diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index 5450e2f4fe8..a275a70effc 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -10,9 +10,13 @@ weight = 150 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-plugins">}}) and [additional features]({{< relref "#enterprise-features">}}). On top of that you get 24x7x365 support and training from the core Grafana team. +Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-plugins">}}) and [additional features]({{< relref "#enterprise-features">}}). You also get 24x7x365 support and training from the core Grafana team. -To learn more about Grafana Enterprise, refer to [our product page.](https://grafana.com/enterprise) +To learn more about Grafana Enterprise, refer to [our product page](https://grafana.com/enterprise). + +## Enterprise features in Grafana Cloud + +Many Grafana Enterprise features are also available in [Grafana Cloud]({{< relref "/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< relref "/grafana-cloud/reference/enterprise-features" >}}). ## Authentication @@ -34,23 +38,23 @@ Supported auth providers: ### Enhanced LDAP integration -With Grafana Enterprise [enhanced LDAP]({{< relref "enhanced_ldap.md" >}}), you can set up active LDAP synchronization. +With [enhanced LDAP integration]({{< relref "enhanced_ldap.md" >}}), you can set up active LDAP synchronization. ### SAML authentication -[SAML authentication]({{< relref "./saml" >}}) enables your Grafana Enterprise users to authenticate with SAML. +[SAML authentication]({{< relref "./saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML). ## Enterprise features -With Grafana Enterprise, you get access to the following features: +Grafana Enterprise adds the following features: -- [Role-based access control]({{< relref "./access-control/_index.md" >}}) to control access with role-based permissions. +- [Role-based access control]({{< relref "./access-control/" >}}) to control access with role-based permissions. - [Data source permissions]({{< relref "datasource_permissions.md" >}}) to restrict query access to specific teams and users. - [Data source query caching]({{< relref "query-caching.md" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting. - [Reporting]({{< relref "reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose. - [Export dashboard as PDF]({{< relref "export-pdf.md" >}}) - [White labeling]({{< relref "white-labeling.md" >}}) to customize Grafana from the brand and logo to the footer links. -- [Usage insights]({{< relref "usage-insights/_index.md" >}}) to understand how your Grafana instance is used. +- [Usage insights]({{< relref "./usage-insights/" >}}) to understand how your Grafana instance is used. - [Vault integration]({{< relref "vault.md" >}}) to manage your configuration or provisioning secrets with Vault. - [Auditing]({{< relref "auditing.md" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements. - [Request security]({{< relref "request-security.md" >}}) makes it possible to restrict outgoing requests from the Grafana server. @@ -58,7 +62,7 @@ With Grafana Enterprise, you get access to the following features: ## Enterprise data sources -With a Grafana Enterprise license, you get access to premium data sources, including: +With a Grafana Enterprise license, you also get access to premium data sources, including: - [AppDynamics](https://grafana.com/grafana/plugins/dlopes7-appdynamics-datasource) - [Azure Devops](https://grafana.com/grafana/plugins/grafana-azuredevops-datasource) @@ -80,4 +84,4 @@ With a Grafana Enterprise license, you get access to premium data sources, inclu ## Try Grafana Enterprise -To purchase or obtain a trial license contact the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). +To purchase or obtain a trial license, contact the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). diff --git a/docs/sources/enterprise/auditing.md b/docs/sources/enterprise/auditing.md index 644d974482e..f3560b6c5ca 100644 --- a/docs/sources/enterprise/auditing.md +++ b/docs/sources/enterprise/auditing.md @@ -8,10 +8,10 @@ weight = 1100 # Auditing -> **Note:** Only available in Grafana Enterprise v7.3+. - Auditing allows you to track important changes to your Grafana instance. By default, audit logs are logged to file but the auditing feature also supports sending logs directly to Loki. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.3 and later, and [Grafana Cloud Advanced]({{< relref "/grafana-cloud" >}}). + ## Audit logs Audit logs are JSON objects representing user actions like: @@ -331,7 +331,7 @@ max_file_size_mb = 256 Audit logs are sent to a [Loki](/oss/loki/) service, through HTTP or gRPC. -> The HTTP option for the Loki exporter is only available in Grafana Enterprise v7.4+. +> **Note:** The HTTP option for the Loki exporter is available only in Grafana Enterprise version 7.4 and later. ```ini [auditing.logs.loki] diff --git a/docs/sources/enterprise/datasource_permissions.md b/docs/sources/enterprise/datasource_permissions.md index 76fb1c0c9e6..2863a40c2bd 100644 --- a/docs/sources/enterprise/datasource_permissions.md +++ b/docs/sources/enterprise/datasource_permissions.md @@ -10,7 +10,7 @@ weight = 500 Data source permissions allow you to restrict access for users to query a data source. For each data source there is a permission page that allows you to enable permissions and restrict query permissions to specific **Users** and **Teams**. -> Only available in Grafana Enterprise. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). ## Enable data source permissions diff --git a/docs/sources/enterprise/enhanced_ldap.md b/docs/sources/enterprise/enhanced_ldap.md index ca2a0972226..fd49c83e719 100644 --- a/docs/sources/enterprise/enhanced_ldap.md +++ b/docs/sources/enterprise/enhanced_ldap.md @@ -1,6 +1,6 @@ +++ aliases = ["/docs/grafana/latest/enterprise/enhanced_ldap/"] -description = "Grafana Enhanced LDAP Integration Guide " +description = "Grafana Enhanced LDAP Integration Guide" keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"] title = "Enhanced LDAP Integration" weight = 600 @@ -10,9 +10,9 @@ weight = 600 The enhanced LDAP integration adds additional functionality on top of the [LDAP integration]({{< relref "../auth/ldap.md" >}}) available in the open source edition of Grafana. -> Enhanced LDAP integration is only available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< relref "/grafana-cloud" >}}). -> Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can control access with role-based permissions. +> To control user access with role-based permissions, refer to [role-based access control]({{< relref "./access-control" >}}). ## LDAP group synchronization for teams @@ -33,7 +33,7 @@ a user as member of a team, and it will not be removed when the user signs in. T In the open source version of Grafana, user data from LDAP is synchronized only during the login process when authenticating using LDAP. -With active LDAP synchronization, available in Grafana Enterprise v6.3+, you can configure Grafana to actively sync users with LDAP servers in the background. Only users that have logged into Grafana at least once are synchronized. +With active LDAP synchronization, available in Grafana Enterprise version 6.3 and later, you can configure Grafana to actively sync users with LDAP servers in the background. Only users that have logged into Grafana at least once are synchronized. Users with updated role and team membership will need to refresh the page to get access to the new features. diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index 2ccb1e2767b..e6b455574c8 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -19,7 +19,7 @@ Defaults to `/license.jwt`. ### license_text -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in Grafana Enterprise version 7.4 and later. When set to the text representation (i.e. content of the license file) of the license, Grafana will evaluate and apply the given license to @@ -27,7 +27,7 @@ the instance. ### auto_refresh_license -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in Grafana Enterprise version 7.4 and later. When enabled, Grafana will send the license and usage statistics to the license issuer. If the license has been updated on the issuer's @@ -37,7 +37,7 @@ automatically. Defaults to `true`. ### license_validation_type -> **Note:** Available in Grafana Enterprise v8.3+. +> **Note:** Available in Grafana Enterprise version 8.3 and later. When set to `aws`, Grafana will validate its license status with Amazon Web Services (AWS) instead of with Grafana Labs. Only use this setting if you purchased an Enterprise license from AWS Marketplace. Defaults to empty, which means that by default Grafana Enterprise will validate using a license issued by Grafana Labs. For details about licenses issued by AWS, refer to [Activate a Grafana Enterprise license purchased through AWS Marketplace]({{< relref "../enterprise/license/activate-aws-marketplace-license/" >}}). @@ -322,7 +322,7 @@ New duration for renewed tokens. Vault may be configured to ignore this value an ## [security.egress] -> **Note:** Available in Grafana Enterprise v7.4 and later versions. +> **Note:** Available in Grafana Enterprise version 7.4 and later. Security egress makes it possible to control outgoing traffic from the Grafana server. @@ -350,7 +350,7 @@ Encryption algorithm used to encrypt secrets stored in the database and cookies. ## [caching] -> **Note:** Available in Grafana Enterprise v7.5 and later versions. +> **Note:** Available in Grafana Enterprise version 7.5 and later. When query caching is enabled, Grafana can temporarily store the results of data source queries and serve cached responses to similar requests. diff --git a/docs/sources/enterprise/export-pdf.md b/docs/sources/enterprise/export-pdf.md index c7f7b97d28a..d8e2c73e25b 100644 --- a/docs/sources/enterprise/export-pdf.md +++ b/docs/sources/enterprise/export-pdf.md @@ -8,11 +8,11 @@ weight = 1400 # Export dashboard as PDF -You can generate PDFs from any of your dashboards and save it to file. +You can generate and save PDF files from any of your dashboards. -> Only available in Grafana Enterprise v6.7+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}). 1. In the upper right corner of the dashboard that you want to export as PDF, click the **Share dashboard** icon. 1. On the PDF tab, select the layout option for exported dashboard: **Portrait** or **Landscape**. -1. Click **Save as PDF** to render dashboard as a PDF document. - Grafana opens the PDF in a new window or browser tab. +1. Click **Save as PDF** to render the dashboard as a PDF file. + Grafana opens the PDF file in a new window or browser tab. diff --git a/docs/sources/enterprise/query-caching.md b/docs/sources/enterprise/query-caching.md index 1f62dad8eb1..344c24ddcb4 100644 --- a/docs/sources/enterprise/query-caching.md +++ b/docs/sources/enterprise/query-caching.md @@ -12,6 +12,8 @@ When query caching is enabled, Grafana temporarily stores the results of data so Query caching works for all backend data sources, and queries sent through the data source proxy. You can enable the cache globally and configure the cache duration (also called Time to Live, or TTL). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). + The following cache backends are available: in-memory, Redis, and Memcached. > **Note:** Storing cached queries in-memory can increase Grafana's memory footprint. In production environments, a Redis or Memcached backend is highly recommended. @@ -30,7 +32,7 @@ You can make a panel retrieve fresh data more frequently by increasing the **Max ## Data sources that work with query caching -Query caching works for all [Enterprise data sources](https://grafana.com/grafana/plugins/?type=datasource&enterprise=1), and it works for the following [built-in data sources]({{< relref "../datasources/_index.md" >}}): +Query caching works for all [Enterprise data sources](https://grafana.com/grafana/plugins/?type=datasource&enterprise=1) as well as the following [built-in data sources]({{< relref "../datasources/_index.md" >}}): - CloudWatch Metrics - Google Cloud Monitoring diff --git a/docs/sources/enterprise/recorded-queries.md b/docs/sources/enterprise/recorded-queries.md index 5a178de76d1..68d286a7de5 100644 --- a/docs/sources/enterprise/recorded-queries.md +++ b/docs/sources/enterprise/recorded-queries.md @@ -12,6 +12,8 @@ Recorded queries allow you to see trends over time by taking a snapshot of a dat For our plugins that do not return time series, it might be useful to plot historical data. For example, you might want to query ServiceNow to see a history of request response times but it can only return current point-in-time metrics. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}). + ## How recorded queries work > **Note:** An administrator must configure a Prometheus data source and associate it with a [Remote write target](#remote-write-target) before recorded queries can be used. diff --git a/docs/sources/enterprise/reporting.md b/docs/sources/enterprise/reporting.md index ed6fbe2b745..87b34202a29 100644 --- a/docs/sources/enterprise/reporting.md +++ b/docs/sources/enterprise/reporting.md @@ -10,7 +10,7 @@ weight = 800 Reporting allows you to automatically generate PDFs from any of your dashboards and have Grafana email them to interested parties on a schedule. This is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. -> If you have [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, for some actions you would need to have relevant permissions. +> If you enabled [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}), for some actions users would need to have relevant permissions. > Refer to specific guides to understand what permissions are required. {{< figure src="/static/img/docs/enterprise/reports_list_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} @@ -51,7 +51,7 @@ Only organization admins can create reports by default. You can customize who ca ### Choose template variables -> **Note:** Available in Grafana Enterprise version 7.5+ (behind `reportVariables` feature flag) and Grafana Enterprise version 8+ without a feature flag. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.5 and later behind the `reportVariables` feature flag, Grafana Enterprise version 8.0 and later without a feature flag, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). You can configure report-specific template variables for the dashboard on the report page. The variables that you select will override the variables from the dashboard, and they are used when rendering a PDF file of the report. For detailed information about using template variables, refer to the [Templates and variables]({{< relref "../variables/_index.md" >}}) section. @@ -59,7 +59,7 @@ You can configure report-specific template variables for the dashboard on the re ### Render a report with panels or rows set to repeat by a variable -> **Note:** Available in Grafana Enterprise v8+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). You can include dynamic dashboards with panels or rows, set to repeat by a variable, into reports. For detailed information about setting up repeating panels or rows in dashboards, refer to the [Repeat panels or rows]({{< relref "../panels/add-panels-dynamically/" >}}) section. @@ -71,7 +71,7 @@ You can include dynamic dashboards with panels or rows, set to repeat by a varia ### Report time range -> Setting custom report time range is available in Grafana Enterprise v7.2+. +> **Note:** You can set custom report time ranges in [Grafana Enterprise]({{< relref "../enterprise" >}}) 7.2+ and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). By default, reports use the saved time range of the dashboard. Changing the time range of the report can be done by: @@ -84,7 +84,7 @@ If the time zone is set differently between your Grafana server and its remote i ### Layout and orientation -> We're actively working on developing new report layout options. [Contact us](https://grafana.com/contact?about=grafana-enterprise&topic=design-process&value=reporting) if you would like to get involved in the design process. +> We're actively developing new report layout options. [Contact us](https://grafana.com/contact?about=grafana-enterprise&topic=design-process&value=reporting) to get involved in the design process. | Layout | Orientation | Support | Description | Preview | | ------ | ----------- | ------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -95,7 +95,7 @@ If the time zone is set differently between your Grafana server and its remote i ### CSV export -> **Note:** Only available in Grafana Enterprise v8.0+, with the [Grafana image renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer) v3.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) 8+ with the [Grafana image renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer) v3.0+, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). You can attach a CSV file to the report email for each table panel on the selected dashboard, along with the PDF report. By default, CSVs larger than 10Mb won't be sent to avoid email servers to reject the email. You can increase or decrease this limit in the [reporting configuration]({{< relref "#rendering-configuration" >}}). @@ -107,9 +107,10 @@ A background job runs every 10 minutes and removes temporary CSV files. You can ### Scheduling -> Note: Scheduler has been significantly changed in Grafana Enterprise v8.1. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). +> The scheduler was significantly changed in Grafana Enterprise version 8.1. -Scheduled reports can be sent once or repeatedly on an hourly, daily, weekly, or monthly basis, or at custom intervals. You can also disable scheduling by selecting **Never**: for example, if you want to send the report via the API. +Scheduled reports can be sent once, or repeated on an hourly, daily, weekly, or monthly basis, or sent at custom intervals. You can also disable scheduling by selecting **Never**, for example to send the report via the API. {{< figure src="/static/img/docs/enterprise/reports_scheduler_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} @@ -129,7 +130,7 @@ When you schedule a report with a monthly frequency, and set the start date betw ### Send test email -> Only available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). 1. In the report, click **Send test email**. 1. In the Email field, enter the email address or addresses that you want to test, separated by semicolon. @@ -142,7 +143,7 @@ The last saved version of the report will be sent to selected emails. You can us ## Pause report -> **Note:** Available in Grafana Enterprise v8+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). You can pause sending of reports from the report list view by clicking the pause icon. The report will not be sent according to its schedule until it is resumed by clicking the resume button on the report row. @@ -183,14 +184,15 @@ font_italic = DejaVuSansCondensed-Oblique.ttf ## Reports settings -> **Note:** Available in Grafana Enterprise v7.2+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.2 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). You can configure organization-wide report settings in the **Settings** tab on the **Reporting** page. Settings are applied to all the reports for current organization. You can customize the branding options. Report branding: -**Company logo URL** - Company logo displayed in the report PDF. Defaults to the Grafana logo. + +- **Company logo URL** - Company logo displayed in the report PDF. Defaults to the Grafana logo. Email branding: diff --git a/docs/sources/enterprise/request-security.md b/docs/sources/enterprise/request-security.md index ccf6a4d3acc..c4b0f670961 100644 --- a/docs/sources/enterprise/request-security.md +++ b/docs/sources/enterprise/request-security.md @@ -8,17 +8,12 @@ weight = 400 # Request security -> **Note:** Available in Grafana Enterprise v7.4 and later versions. - -Request security makes it possible to limit requests from the Grafana server, and it targets requests that are generated by users. - -For example: - -- Data source metric queries -- Alert notifications +Request security allows you to limit requests from the Grafana server by targeting requests generated by users, such as data source metric queries and alert notifications. This can be used to limit access to internal systems that the server Grafana runs on can access but that users of Grafana should not be able to access. This feature does not affect traffic from the Grafana users browser. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.4 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). + > **Note:** Although request security works with backend plugins, you can create a backend plugin that bypasses this security. ## IP and hostname blocking diff --git a/docs/sources/enterprise/saml/_index.md b/docs/sources/enterprise/saml/_index.md index a343bffb002..3a4cba3b96e 100644 --- a/docs/sources/enterprise/saml/_index.md +++ b/docs/sources/enterprise/saml/_index.md @@ -17,6 +17,6 @@ weight: 10 SAML authentication integration enables your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. -> Only available in Grafana Enterprise v6.3+. If you experience any issues with our implementation, contact our [Technical Support team](https://grafana.com/contact?plcmt=top-nav&cta=contactus) +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). {{< section >}} diff --git a/docs/sources/enterprise/saml/about-saml.md b/docs/sources/enterprise/saml/about-saml.md index c541856d1a0..47b625dfd64 100644 --- a/docs/sources/enterprise/saml/about-saml.md +++ b/docs/sources/enterprise/saml/about-saml.md @@ -20,7 +20,7 @@ SAML authentication integration allows your Grafana users to log in by using an The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. -> Only available in Grafana Enterprise v6.3+. If you encounter any problems with our implementation, please don't hesitate to contact us. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). ## Supported SAML diff --git a/docs/sources/enterprise/saml/configure-saml.md b/docs/sources/enterprise/saml/configure-saml.md index 848585e9663..907dacd05c0 100644 --- a/docs/sources/enterprise/saml/configure-saml.md +++ b/docs/sources/enterprise/saml/configure-saml.md @@ -46,7 +46,7 @@ The table below describes all SAML configuration options. Continue reading below ### Signature algorithm -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests. If the `signature_algorithm` option is configured, Grafana will put a digital signature into SAML requests. Supported signature types are `rsa-sha1`, `rsa-sha256`, `rsa-sha512`. This option should match your IdP configuration, otherwise, signature validation will fail. Grafana uses key and certificate configured with `private_key` and `certificate` options for signing SAML requests. @@ -83,7 +83,7 @@ The integration provides two key endpoints as part of Grafana: ### IdP-initiated Single Sign-On (SSO) -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. By default, Grafana allows only service provider (SP) initiated logins (when the user logs in with SAML via Grafana’s login page). If you want users to log in into Grafana directly from your identity provider (IdP), set the `allow_idp_initiated` configuration option to `true` and configure `relay_state` with the same value specified in the IdP configuration. @@ -91,7 +91,7 @@ IdP-initiated SSO has some security risks, so make sure you understand the risks ### Single logout -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. SAML's single logout feature allows users to log out from all applications associated with the current IdP session established via SAML SSO. If the `single_logout` option is set to `true` and a user logs out, Grafana requests IdP to end the user session which in turn triggers logout from all other applications the user is logged into using the same IdP session (applications should support single logout). Conversely, if another application connected to the same IdP logs out using single logout, Grafana receives a logout request from IdP and ends the user session. @@ -125,7 +125,7 @@ By default, new Grafana users using SAML authentication will have an account cre ### Configure team sync -> Team sync support for SAML only available in Grafana v7.0+ +> **Note:** Team sync support for SAML is available in Grafana version 7.0 and later. To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. @@ -133,7 +133,7 @@ To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref ".././enter ### Configure role sync -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). @@ -160,7 +160,7 @@ role_values_grafana_admin = superadmin ### Configure organization mapping -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. Organization mapping allows you to assign users to particular organization in Grafana depending on attribute value obtained from identity provider. @@ -186,7 +186,7 @@ You can use `*` as an Organization if you want all your users to be in some orga ### Configure allowed organizations -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. With the [`allowed_organizations`]({{< relref ".././enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. diff --git a/docs/sources/enterprise/settings-updates.md b/docs/sources/enterprise/settings-updates.md index d192920b1a8..e494d3c7a22 100644 --- a/docs/sources/enterprise/settings-updates.md +++ b/docs/sources/enterprise/settings-updates.md @@ -8,9 +8,9 @@ weight = 500 # Settings updates at runtime -> **Note:** Available in Grafana Enterprise v8.0+. +> **Note:** Available in Grafana Enterprise version 8.0 and later. -Settings updates at runtime allows you to update Grafana settings with no need to restart the Grafana server. +By updating settings at runtime, you can update Grafana settings without needing to restart the Grafana server. Updates that happen at runtime are stored in the database and override [settings from the other sources](https://grafana.com/docs/grafana/latest/administration/configuration/) @@ -88,5 +88,5 @@ HTTP API, then the other instances are synchronized through the database and the ## Control access with role-based access control -If you have [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, you can control who can read or update settings. +If you have [role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, you can control who can read or update settings. Refer to the [Admin API]({{< relref "../developers/http_api/admin.md#update-settings" >}}) for more information. diff --git a/docs/sources/enterprise/team-sync.md b/docs/sources/enterprise/team-sync.md index 708cce1470e..6e7340cfcb5 100644 --- a/docs/sources/enterprise/team-sync.md +++ b/docs/sources/enterprise/team-sync.md @@ -12,7 +12,7 @@ weight = 1000 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. -> Available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< relref "/grafana-cloud" >}}). Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. This mechanism allows Grafana to remove an existing synchronized user from a team when its group membership changes. This mechanism also enables you to manually add a user as member of a team, and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. diff --git a/docs/sources/enterprise/usage-insights/_index.md b/docs/sources/enterprise/usage-insights/_index.md index 9e2885baa24..6ab4ed84c04 100644 --- a/docs/sources/enterprise/usage-insights/_index.md +++ b/docs/sources/enterprise/usage-insights/_index.md @@ -10,6 +10,8 @@ weight = 200 Usage insights allow you to have a better understanding of how your Grafana instance is used. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). + The usage insights feature collects a number of aggregated data and stores them in the database: - Dashboard views (aggregated and per user) diff --git a/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md b/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md index ab18ad807dc..f6712208517 100644 --- a/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md +++ b/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md @@ -12,7 +12,7 @@ For every dashboard and data source, you can access usage information. ## Dashboard insights -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). To see dashboard usage information, go to the top bar and click **Dashboard insights**. @@ -27,7 +27,7 @@ Dashboard insights show the following information: ## Data source insights -> **Note:** Available in Grafana Enterprise v7.3+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.3 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). Data source insights give you information about how a data source has been used in the past 30 days, such as: diff --git a/docs/sources/enterprise/usage-insights/export-logs.md b/docs/sources/enterprise/usage-insights/export-logs.md index 53ad13afc47..3e82d0039a5 100644 --- a/docs/sources/enterprise/usage-insights/export-logs.md +++ b/docs/sources/enterprise/usage-insights/export-logs.md @@ -8,7 +8,7 @@ weight = 500 # Export logs of usage insights -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.4 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). By exporting usage logs to Loki, you can directly query them and create dashboards of the information that matters to you most, such as dashboard errors, most active organizations, or your top-10 most-used queries. diff --git a/docs/sources/enterprise/usage-insights/improved-search.md b/docs/sources/enterprise/usage-insights/improved-search.md index 399e9ec0cb6..1872bc78e03 100644 --- a/docs/sources/enterprise/usage-insights/improved-search.md +++ b/docs/sources/enterprise/usage-insights/improved-search.md @@ -8,7 +8,7 @@ weight = 400 # Sort dashboards by using insights data -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). In the search view, you can sort dashboards by using insights data. Doing so helps you find unused or broken dashboards or discover those that are most viewed. diff --git a/docs/sources/enterprise/usage-insights/presence-indicator.md b/docs/sources/enterprise/usage-insights/presence-indicator.md index 44e1603a65e..901670ea0c4 100644 --- a/docs/sources/enterprise/usage-insights/presence-indicator.md +++ b/docs/sources/enterprise/usage-insights/presence-indicator.md @@ -8,7 +8,7 @@ weight = 300 # Presence indicator -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). When you are signed in and looking at any given dashboard, you can know who is looking at the same dashboard as you are via a presence indicator, which displays avatars of users who have interacted with the dashboard recently. The default time frame is within the past 10 minutes. To see the user's name, hover over the user's avatar. The avatars come from [Gravatar](https://gravatar.com) based on the user's email. diff --git a/docs/sources/enterprise/vault.md b/docs/sources/enterprise/vault.md index 49e86a76e44..996c2c1bc92 100644 --- a/docs/sources/enterprise/vault.md +++ b/docs/sources/enterprise/vault.md @@ -8,10 +8,9 @@ weight = 1200 # Vault integration -> Only available in Grafana Enterprise v7.1+. +If you manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault), you can use them for [Configuration]({{< relref "../administration/configuration.md" >}}) and [Provisioning]({{< relref "../administration/provisioning.md" >}}). -If you manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault), you can use them for [Configuration]({{< relref "../administration/configuration.md" >}}) -and [Provisioning]({{< relref "../administration/provisioning.md" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< relref "/grafana-cloud" >}}). > **Note:** If you have Grafana [set up for high availability]({{< relref "../administration/set-up-for-high-availability.md" >}}), then we advise not to use dynamic secrets for provisioning files. > Each Grafana instance is responsible for renewing its own leases. Your data source leases might expire when one of your Grafana servers shuts down. diff --git a/docs/sources/enterprise/white-labeling.md b/docs/sources/enterprise/white-labeling.md index 1eac3352f51..aeae70448ea 100644 --- a/docs/sources/enterprise/white-labeling.md +++ b/docs/sources/enterprise/white-labeling.md @@ -10,7 +10,7 @@ weight = 1300 White labeling allows you to replace the Grafana brand and logo with your own corporate brand and logo. -> Only available in Grafana Enterprise v6.6+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< relref "/grafana-cloud" >}}). Grafana Enterprise has white labeling options in the `grafana.ini` file. As with all configuration options, you can also set them with environment variables. From 1f85101787a38b430991e112dad21d237a31cacb Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Wed, 25 May 2022 14:10:22 -0700 Subject: [PATCH 007/283] Util: Improve performance of strings.SplitString (#49115) Replaces the regexp with calls to strings.ReplaceAll and strings.Fields for simplicity and improved performance. Signed-off-by: Dave Henderson --- pkg/util/strings.go | 3 +-- pkg/util/strings_test.go | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 15c8801b541..e066ca5c2bf 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -3,7 +3,6 @@ package util import ( "fmt" "math" - "regexp" "strings" "time" "unicode" @@ -34,7 +33,7 @@ func SplitString(str string) []string { return []string{} } - return regexp.MustCompile("[, ]+").Split(str, -1) + return strings.Fields(strings.ReplaceAll(str, ",", " ")) } // GetAgeString returns a string representing certain time from years to minutes. diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go index 7e93cc5ff63..efb3dbbb773 100644 --- a/pkg/util/strings_test.go +++ b/pkg/util/strings_test.go @@ -58,6 +58,53 @@ func TestSplitString(t *testing.T) { } } +func BenchmarkSplitString(b *testing.B) { + b.Run("empty input", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("") + } + }) + b.Run("single string", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test") + } + }) + b.Run("space-separated", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test1 test2 test3") + } + }) + b.Run("comma-separated", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test1,test2,test3") + } + }) + b.Run("comma-separated with spaces", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test1 , test2 test3") + } + }) + b.Run("mixed commas and spaces", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test1 , test2 test3,test4") + } + }) + b.Run("very long mixed", func(b *testing.B) { + for i := 0; i < b.N; i++ { + SplitString("test1 , test2 test3,test4, test5 test6 test7,test8 test9 test10" + + " test11 test12 test13,test14 test15 test16,test17 test18 test19,test20 test21 test22" + + " test23,test24 test25 test26,test27 test28 test29,test30 test31 test32" + + " test33,test34 test35 test36,test37 test38 test39,test40 test41 test42" + + " test43,test44 test45 test46,test47 test48 test49,test50 test51 test52" + + " test53,test54 test55 test56,test57 test58 test59,test60 test61 test62" + + " test63,test64 test65 test66,test67 test68 test69,test70 test71 test72" + + " test73,test74 test75 test76,test77 test78 test79,test80 test81 test82" + + " test83,test84 test85 test86,test87 test88 test89,test90 test91 test92" + + " test93,test94 test95 test96,test97 test98 test99,test100 ") + } + }) +} + func TestDateAge(t *testing.T) { assert.Equal(t, "?", GetAgeString(time.Time{})) // base case From df90393057918328a11c16cf758e1b0848edfe27 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 25 May 2022 14:19:56 -0700 Subject: [PATCH 008/283] Timeseries: fix outside range stale state (#49633) Co-authored-by: Todd Treece --- .../panel/candlestick/CandlestickPanel.tsx | 2 +- .../state-timeline/StateTimelinePanel.tsx | 2 +- .../status-history/StatusHistoryPanel.tsx | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 2 +- .../timeseries/plugins/OutsideRangePlugin.tsx | 27 ++++++++++++------- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 7176739fa1f..c42b7a2fdf2 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -316,7 +316,7 @@ export const CandlestickPanel: React.FC = ({ /> )} - + ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 54d5ea2f0b0..3a0f899c06a 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -118,7 +118,7 @@ export const StateTimelinePanel: React.FC = ({ timeZone={timeZone} renderTooltip={renderCustomTooltip} /> - + ); }} diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 3cfea9dc0cb..8a1fcbc2a78 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -72,7 +72,7 @@ export const StatusHistoryPanel: React.FC = ({ <> - + ); }} diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 9e143d4f976..e727764fa16 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -138,7 +138,7 @@ export const TimeSeriesPanel: React.FC = ({ /> )} - + ); }} diff --git a/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx b/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx index 6cf3cd30532..721c9f45bb3 100644 --- a/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx @@ -1,34 +1,43 @@ -import React, { useLayoutEffect, useRef } from 'react'; -import uPlot from 'uplot'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import uPlot, { TypedArray, Scale } from 'uplot'; -import { TimeRange, AbsoluteTimeRange } from '@grafana/data'; +import { AbsoluteTimeRange } from '@grafana/data'; import { UPlotConfigBuilder, Button } from '@grafana/ui'; interface ThresholdControlsPluginProps { config: UPlotConfigBuilder; - range: TimeRange; onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void; } -export const OutsideRangePlugin: React.FC = ({ config, range, onChangeTimeRange }) => { +export const OutsideRangePlugin: React.FC = ({ config, onChangeTimeRange }) => { const plotInstance = useRef(); + const [timevalues, setTimeValues] = useState([]); + const [timeRange, setTimeRange] = useState(); useLayoutEffect(() => { config.addHook('init', (u) => { plotInstance.current = u; }); + + config.addHook('setScale', (u) => { + setTimeValues(u.data?.[0] ?? []); + setTimeRange(u.scales['x'] ?? undefined); + }); }, [config]); - const timevalues = plotInstance.current?.data?.[0]; - if (!timevalues || !plotInstance.current || timevalues.length < 2 || !onChangeTimeRange) { + if (timevalues.length < 2 || !onChangeTimeRange) { + return null; + } + + if (!timeRange || !timeRange.time || !timeRange.min || !timeRange.max!) { return null; } // Time values are always sorted for uPlot to work const first = timevalues[0]; const last = timevalues[timevalues.length - 1]; - const fromX = range.from.valueOf(); - const toX = range.to.valueOf(); + const fromX = timeRange.min; + const toX = timeRange.max; // (StartA <= EndB) and (EndA >= StartB) if (first <= toX && last >= fromX) { From b5d48d217aec05910620e37b0f11d249d039f079 Mon Sep 17 00:00:00 2001 From: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> Date: Wed, 25 May 2022 17:50:20 -0400 Subject: [PATCH 009/283] Docs: Created separate section for migration under alerting (#49616) * Lots of changes, including new topic, also fixed all alerting relrefs * Push stashed change. * Updated content to reflect that Grafana alerting is enabled during upgrade. * Fix typo * Update docs/sources/alerting/migrating-alerts/_index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Checkin changes. * Updates from Chris's review. Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/alerting/_index.md | 9 ++-- .../alerting/migrating-alerts/_index.md | 26 ++++++++++++ .../migrating-legacy-alerts.md | 17 ++------ .../alerting/migrating-alerts/opt-out.md | 40 ++++++++++++++++++ docs/sources/alerting/opt-in.md | 41 ------------------- .../rbac-fixed-basic-role-definitions.md | 2 +- docs/sources/whatsnew/whats-new-in-v9-0.md | 4 +- 7 files changed, 77 insertions(+), 62 deletions(-) create mode 100644 docs/sources/alerting/migrating-alerts/_index.md rename docs/sources/alerting/{ => migrating-alerts}/migrating-legacy-alerts.md (62%) create mode 100644 docs/sources/alerting/migrating-alerts/opt-out.md delete mode 100644 docs/sources/alerting/opt-in.md diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 72206e75b36..4284de8f5a0 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -1,5 +1,5 @@ +++ -aliases = ["/docs/grafana/latest/alerting/", "/docs/grafana/latest/alerting/unified-alerting/difference-old-new/"] +aliases = ["/docs/grafana/latest/alerting/", "/docs/grafana/latest/alerting/unified-alerting/alerting/"] title = "Alerting" weight = 114 +++ @@ -18,13 +18,12 @@ For new installations or existing installs without alerting configured, Grafana | ----------- | ------------- | ------------- | ------------- | | Grafana 9.0 | On by default | On by default | On by default | -- For existing OSS installations with legacy dashboard alerting, you can [opt-in]({{< relref "./opt-in.md" >}}) to Grafana alerting. -- For Grafana Cloud instances using legacy cloud alerting, contact customer support to migrate to Grafana alerting. +Existing installations that upgrade to v9.0 will have Grafana alerting enabled by default. For more information on migrating from legacy or the cloud alerting plugin, see [Migrating to Grafana alerting]({{< relref "./migrating-alerts/_index.md" >}}). Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. -- [Enable Grafana alerting in OSS]({{< relref "./opt-in.md" >}}) -- [Migrating legacy alerts]({{< relref "./migrating-legacy-alerts.md" >}}) +- [Migrating legacy alerts]({{< relref "./migrating-alerts/_index.md" >}}) +- [Disable Grafana alerting in OSS]({{< relref "./migrating-alerts/opt-out.md" >}}) - [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) - [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule.md" >}}) - [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}}) diff --git a/docs/sources/alerting/migrating-alerts/_index.md b/docs/sources/alerting/migrating-alerts/_index.md new file mode 100644 index 00000000000..fad45c086e7 --- /dev/null +++ b/docs/sources/alerting/migrating-alerts/_index.md @@ -0,0 +1,26 @@ ++++ +aliases = ["/docs/grafana/latest/alerting/migrating-alerts/"] +description = "Migrate Grafana alerts" +title = "Migrate to Grafana alerting" +weight = 113 ++++ + +# Migrate to Grafana alerting + +Grafana alerting is the default for new Cloud, Enterprise, and OSS installations. The new installations will only show the Grafana alerting icon in the left navigation panel. + +Existing installations that upgrade to v9.0 will have Grafana alerting enabled by default. + +| Grafana instance upgraded to v 90 | | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana alerting. Once migration is complete, you can access aman manage the older alerts from the new alerting Grafana alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana alerting icon as well as the old Cloud alerting plugin in the left navigation panel. | +| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana alerting. If you wish, you can [opt-out]({{< relref "./opt-out.md" >}}) of Grafana alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. | +| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana alerting icon. During upgrade, existing legacy alerts are migrated to Grafana alerting. If you wish, you can [opt-out]({{< relref "./opt-out.md" >}}) of Grafana alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. | + +During migration from legacy alerting to unified alerting, the legacy alerts are updated to the new alerts type, as a result, the user does not lose alerts or alerting data. However, if a user rolls back to legacy alerting after having migrated to unified alerting, they will only get the legacy alerts they had right before migration. + +## Roll back to legacy alerting + +Although we encourage you to use Grafana alerting, roll back to legacy alerting is supported in Grafana 9. Rolling back can result in data loss (you will loose all alerts that you created using Grafana alerting). This is applicable to the fresh installation as well as upgraded setups. + +> **Note:** Legacy alerting will be deprecated in a future release (v10). diff --git a/docs/sources/alerting/migrating-legacy-alerts.md b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md similarity index 62% rename from docs/sources/alerting/migrating-legacy-alerts.md rename to docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md index 51dfd5ffac7..ee404f55a01 100644 --- a/docs/sources/alerting/migrating-legacy-alerts.md +++ b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md @@ -7,7 +7,9 @@ weight = 114 # Migrating legacy dashboard alerts -When Grafana alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside new alerts. +When Grafana alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. This topic explains how legacy dashboard alerts are migrated and some limitations. + +> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana alerting for your Cloud stack. Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows: @@ -15,20 +17,9 @@ Read and write access to legacy dashboard alerts and Grafana alerts are governed - If there are no dashboard permissions and the dashboard is under a folder, then the rule is linked to this folder and inherits its permissions. - If there are no dashboard permissions and the dashboard is under the General folder, then the rule is linked to the `General Alerting` folder, and the rule inherits the default permissions. -> **Note:** Since there is no `Keep Last State` option for [`No Data`]({{< relref "./alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "./alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "./silences/_index.md" >}}) for each alert rule with a duration of 1 year. +> **Note:** Since there is no `Keep Last State` option for [`No Data`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) in Grafana alerting, this option becomes `NoData` during the legacy rules migration. Option "Keep Last State" for [`Error handling`]({{< relref "../alerting-rules/create-grafana-managed-rule/#no-data--error-handling" >}}) is migrated to a new option `Error`. To match the behavior of the `Keep Last State`, in both cases, during the migration Grafana automatically creates a [silence]({{< relref "../silences/_index.md" >}}) for each alert rule with a duration of 1 year. 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 Dashboard alert go to the `autogen-unlinked-channel-recv` route. Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. Silences (expiring after one year) are created for all paused dashboard alerts. - -## Disable Grafana alerts - -To disable Grafana alerts and enable legacy dashboard alerts: - -1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. -1. Set the `enabled` property to `false`. -1. For [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. -1. Restart Grafana for the configuration changes to take effect. - -> **Note:** Switching from one flavor of alerting to another can result in data loss. This is applicable to the fresh installation as well as upgraded setups. diff --git a/docs/sources/alerting/migrating-alerts/opt-out.md b/docs/sources/alerting/migrating-alerts/opt-out.md new file mode 100644 index 00000000000..fd42b5579b5 --- /dev/null +++ b/docs/sources/alerting/migrating-alerts/opt-out.md @@ -0,0 +1,40 @@ ++++ +aliases = ["/docs/grafana/latest/alerting/opt-in/", "/docs/grafana/latest/alerting/unified-alerting/opt-in/"] +description = "Disable Grafana alerts" +title = "Opt-out of Grafana alerting" +weight = 113 ++++ + +# Opt-out to Grafana alerting in OSS + +This topic discusses how to disable Grafana alerting and migrate to legacy dashboard alerting. It also provides guidance on how to enable Grafana alerting once you are ready to migrate to Grafana alerting. + +> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana alerting for your Grafana Cloud stack. + +## Before you begin + +We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5. + +## Opt-out of Grafana alerts + +To opt-out of Grafana alerts and roll back to legacy dashboard alerting: + +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section. +1. Set the `enabled` property to `false`. +1. For [legacy dashboard alerting]({{< relref "../../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. +1. Restart Grafana for the configuration changes to take effect. + +> **Note:** Rolling back from Grafana to legacy alerting can result in data loss. This is applicable to the fresh installation as well as upgraded setups. + +## Opt-in to Grafana alerting + +When you are ready to make the switch, the following procedure will help you migrate to Grafana alerting. + +To opt-in Grafana alerts: + +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section. +1. Set the `enabled` property to `true`. +1. Next, for [legacy dashboard alerting]({{< relref "../../administration/configuration.md#alerting" >}}), set the `enabled` flag to `false`. +1. Restart Grafana for the configuration changes to take effect. + +> **Note:** The `ngalert` toggle previously used to enable or disable Grafana alerting is no longer available. diff --git a/docs/sources/alerting/opt-in.md b/docs/sources/alerting/opt-in.md deleted file mode 100644 index ca31f27b927..00000000000 --- a/docs/sources/alerting/opt-in.md +++ /dev/null @@ -1,41 +0,0 @@ -+++ -aliases = ["/docs/grafana/latest/alerting/opt-in/", "/docs/grafana/latest/alerting/unified-alerting/opt-in/"] -description = "Enable Grafana alerts" -title = "Opt-in to Grafana alerting" -weight = 113 -+++ - -# Opt-in to Grafana alerting in OSS - -Grafana alerting is enabled by default for new Cloud and OSS installations. - -- For existing OSS installations that use legacy dashboard alerts, unified alerting is still an opt-in feature. -- For existing Grafana Cloud users, contact customer support to enable Grafana alerting for your Cloud stack. - -## Before you begin - -We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5. - -## Enable Grafana alerting - -To enable Grafana alerts: - -1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. -2. Set the `enabled` property to `true`. -3. Next, for [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `false`. -4. Restart Grafana for the configuration changes to take effect. - -> **Note:** The `ngalert` toggle previously used to enable or disable Grafana alerting is no longer available. - -Before v8.2, notification logs and silences were stored on a disk. If you did not use persistent disks, you would have lost any configured silences and logs on a restart, resulting in unwanted or duplicate notifications. We no longer require the use of a persistent disk. Instead, the notification logs and silences are stored regularly (every 15 minutes). If you used the file-based approach, Grafana reads the existing file and persists it eventually. - -## Disable Grafana alerts - -To disable Grafana alerts and roll back to legacy dashboard alerting: - -1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. -1. Set the `enabled` property to `false`. -1. For [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. -1. Restart Grafana for the configuration changes to take effect. - -> **Note:** Switching from one flavor of alerting to another can result in data loss. This is applicable to the fresh installation as well as upgraded setups. diff --git a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md index 71d56c6528b..70b9a852f15 100644 --- a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md +++ b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md @@ -80,7 +80,7 @@ The following tables list permissions associated with basic and fixed roles. ### Alerting roles -If alerting is [enabled]({{< relref "../../alerting/opt-in.md" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. +If alerting is [enabled]({{< relref "../../alerting/migrating-alerts/opt-in.md" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. Access to Grafana alert rules is an intersection of many permissions: diff --git a/docs/sources/whatsnew/whats-new-in-v9-0.md b/docs/sources/whatsnew/whats-new-in-v9-0.md index 58411e37263..abe9118b8ef 100644 --- a/docs/sources/whatsnew/whats-new-in-v9-0.md +++ b/docs/sources/whatsnew/whats-new-in-v9-0.md @@ -72,7 +72,7 @@ New new heatmap panel has a number enhancements compared to the old version. - For unbucketed data, it performs smarter auto bucket sizing - Supports filtering out bucket values close to but not exactly zero -The new heatmap by default assumes that the data is pre-bucked. So if your query returns time series each series is seen as separate bucket (y axis tick). The panel is so much faster than the old one so it can render many time series with thousands of data points each without issue. +The new heatmap by default assumes that the data is pre-bucketed. So if your query returns time series each series is seen as separate bucket (y axis tick). The panel is so much faster than the old one so it can render many time series with thousands of data points each without issue. {{< figure src="/static/img/docs/heatmap-panel/heatmap_with_time_series_light_theme.png" max-width="500px" caption="Heatmap panel with time series" >}} @@ -80,7 +80,7 @@ The new heatmap by default assumes that the data is pre-bucked. So if your query Unified alerting is now on by default if you upgrade from an earlier version of Grafana. If you have been using legacy alerting in an earlier version of Grafana and you upgrade to Grafana 9 your alert rules will be automatically migrated and the legacy alerting interface will be replaced by the unified alerting interface. -Unified alerting has been available since June, 2021, it now provides feature parity with legacy alerting and many additional benefits. To find out more on the process to revert back to legacy alerts if needed, click [here]({{< relref "../alerting/opt-in.md#disable-grafana-alertsd#" >}}). Note that if you do revert back (by setting the Grafana config flag GF_UNIFIED_ALERTING_ENABLED to false), that we expect to remove legacy alerting in the next major Grafana release, Grafana 10. +Unified alerting has been available since June, 2021, it now provides feature parity with legacy alerting and many additional benefits. To find out more on the process to revert back to legacy alerts if needed, click [here]({{< relref "../alerting/migrating-alerts/opt-out.md" >}}). Note that if you do revert back (by setting the Grafana config flag GF_UNIFIED_ALERTING_ENABLED to false), that we expect to remove legacy alerting in the next major Grafana release, Grafana 10. ### Alert state history for Grafana managed alerts From a968a43e0c6a90122e2982a9d1dd137c4215fd01 Mon Sep 17 00:00:00 2001 From: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> Date: Wed, 25 May 2022 18:53:05 -0400 Subject: [PATCH 010/283] Docs: Move alert rule section to alerting fundamentals section (#49657) * initial commit * Added links to alert rules, and fixes one broken alerting relref. --- docs/sources/alerting/_index.md | 1 + docs/sources/alerting/fundamentals/_index.md | 1 + .../alert-rules}/_index.md | 6 +++--- .../alert-rules}/alert-instances.md | 0 .../alert-rules}/alert-rule-types.md | 0 .../alert-rules}/organising-alerts.md | 0 docs/sources/alerting/migrating-alerts/opt-out.md | 2 +- 7 files changed, 6 insertions(+), 4 deletions(-) rename docs/sources/alerting/{about-alert-rules => fundamentals/alert-rules}/_index.md (84%) rename docs/sources/alerting/{about-alert-rules => fundamentals/alert-rules}/alert-instances.md (100%) rename docs/sources/alerting/{about-alert-rules => fundamentals/alert-rules}/alert-rule-types.md (100%) rename docs/sources/alerting/{about-alert-rules => fundamentals/alert-rules}/organising-alerts.md (100%) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 4284de8f5a0..080b0e9bf67 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -22,6 +22,7 @@ Existing installations that upgrade to v9.0 will have Grafana alerting enabled b Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. +- [About alert rules]({{< relref "./fundamentals/alert-rules/_index.md" >}}) - [Migrating legacy alerts]({{< relref "./migrating-alerts/_index.md" >}}) - [Disable Grafana alerting in OSS]({{< relref "./migrating-alerts/opt-out.md" >}}) - [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 8826764c5ca..a21778eb7a5 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -8,6 +8,7 @@ weight = 110 This section includes the following fundamental concepts of Grafana alerting: +- [Alert rules]({{< relref "./alert-rules/_index.md" >}}) - [Annotations and labels for alerting rules]({{< relref "./annotation-label/_index.md" >}}) - [Alertmanager]({{< relref "./alertmanager.md" >}}) - [State and health of alerting rules]({{< relref "./state-and-health.md" >}}) diff --git a/docs/sources/alerting/about-alert-rules/_index.md b/docs/sources/alerting/fundamentals/alert-rules/_index.md similarity index 84% rename from docs/sources/alerting/about-alert-rules/_index.md rename to docs/sources/alerting/fundamentals/alert-rules/_index.md index 33db80be0bc..8ca7ad6c983 100644 --- a/docs/sources/alerting/about-alert-rules/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rules/_index.md @@ -1,6 +1,6 @@ +++ -title = "About alert rules" -description = "Learn about Grafana alert rules" +title = "Alert rules" +description = "About Grafana alert rules" keywords = ["grafana", "alerting", "rules"] weight = 101 +++ @@ -16,4 +16,4 @@ An interval specifies how frequently an alerting rule is evaluated. Duration, wh - [Alert rule types]({{< relref "./alert-rule-types.md" >}}) - [Alert instances]({{< relref "./alert-instances.md" >}}) - [Organising alert rules]({{< relref "./organising-alerts.md" >}}) -- [Annotation and labels]({{< relref "../fundamentals/annotation-label/_index.md" >}}) +- [Annotation and labels]({{< relref "../annotation-label/_index.md" >}}) diff --git a/docs/sources/alerting/about-alert-rules/alert-instances.md b/docs/sources/alerting/fundamentals/alert-rules/alert-instances.md similarity index 100% rename from docs/sources/alerting/about-alert-rules/alert-instances.md rename to docs/sources/alerting/fundamentals/alert-rules/alert-instances.md diff --git a/docs/sources/alerting/about-alert-rules/alert-rule-types.md b/docs/sources/alerting/fundamentals/alert-rules/alert-rule-types.md similarity index 100% rename from docs/sources/alerting/about-alert-rules/alert-rule-types.md rename to docs/sources/alerting/fundamentals/alert-rules/alert-rule-types.md diff --git a/docs/sources/alerting/about-alert-rules/organising-alerts.md b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md similarity index 100% rename from docs/sources/alerting/about-alert-rules/organising-alerts.md rename to docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md diff --git a/docs/sources/alerting/migrating-alerts/opt-out.md b/docs/sources/alerting/migrating-alerts/opt-out.md index fd42b5579b5..b7756b6b1ff 100644 --- a/docs/sources/alerting/migrating-alerts/opt-out.md +++ b/docs/sources/alerting/migrating-alerts/opt-out.md @@ -30,7 +30,7 @@ To opt-out of Grafana alerts and roll back to legacy dashboard alerting: When you are ready to make the switch, the following procedure will help you migrate to Grafana alerting. -To opt-in Grafana alerts: +To opt-in to Grafana alerts: 1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section. 1. Set the `enabled` property to `true`. From 33d4850c90fea2711e6413d4a98e14420644da2d Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 25 May 2022 23:32:55 -0400 Subject: [PATCH 011/283] Prometheus: Fix sort issue in wide frames (#49660) --- pkg/tsdb/prometheus/buffered/framing_test.go | 2 +- .../buffered/prometeus_bench_test.go | 4 ++ pkg/tsdb/prometheus/querydata/framing_test.go | 2 +- .../querydata/prometeus_bench_test.go | 4 ++ .../testdata/range_simple.result.golden.json | 2 +- .../testdata/range_simple.result.golden.txt | 4 +- .../testdata/range_simple.result.json | 2 +- ...e_simple.result.streaming-wide.golden.json | 3 ++ ...ge_simple.result.streaming-wide.golden.txt | 7 +-- .../range_simple.result.streaming.golden.json | 2 +- .../range_simple.result.streaming.golden.txt | 4 +- pkg/util/converter/prom.go | 4 ++ .../testdata/loki-streams-a-frame.json | 20 ++++---- .../testdata/loki-streams-a-wide-frame.json | 34 +++++++------- .../testdata/loki-streams-b-frame.json | 28 +++++------ .../testdata/loki-streams-b-wide-frame.json | 46 +++++++++---------- 16 files changed, 92 insertions(+), 76 deletions(-) diff --git a/pkg/tsdb/prometheus/buffered/framing_test.go b/pkg/tsdb/prometheus/buffered/framing_test.go index 40a2ead8fb5..9d936bb0ffc 100644 --- a/pkg/tsdb/prometheus/buffered/framing_test.go +++ b/pkg/tsdb/prometheus/buffered/framing_test.go @@ -23,7 +23,7 @@ import ( apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) -var update = false +var update = true func TestMatrixResponses(t *testing.T) { tt := []struct { diff --git a/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go b/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go index 25cf7942b84..4a600e472ef 100644 --- a/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go @@ -54,6 +54,10 @@ func makeJsonTestValue(r *rand.Rand) string { func makeJsonTestSeries(start int64, step int64, timestampCount int, r *rand.Rand, seriesIndex int) string { var values []string for i := 0; i < timestampCount; i++ { + // create out of order timestamps to test sorting + if seriesIndex == 0 && i%2 == 0 { + continue + } value := fmt.Sprintf(`[%d,"%v"]`, start+(int64(i)*step), makeJsonTestValue(r)) values = append(values, value) } diff --git a/pkg/tsdb/prometheus/querydata/framing_test.go b/pkg/tsdb/prometheus/querydata/framing_test.go index 32ee249bc68..2418db326da 100644 --- a/pkg/tsdb/prometheus/querydata/framing_test.go +++ b/pkg/tsdb/prometheus/querydata/framing_test.go @@ -20,7 +20,7 @@ import ( "github.com/grafana/grafana/pkg/tsdb/prometheus/models" ) -var update = false +var update = true func TestMatrixResponses(t *testing.T) { tt := []struct { diff --git a/pkg/tsdb/prometheus/querydata/prometeus_bench_test.go b/pkg/tsdb/prometheus/querydata/prometeus_bench_test.go index b7ce55437bb..0f610b4a4d2 100644 --- a/pkg/tsdb/prometheus/querydata/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/querydata/prometeus_bench_test.go @@ -56,6 +56,10 @@ func makeJsonTestValue(r *rand.Rand) string { func makeJsonTestSeries(start int64, step int64, timestampCount int, r *rand.Rand, seriesIndex int) string { var values []string for i := 0; i < timestampCount; i++ { + // create out of order timestamps to test sorting + if seriesIndex == 0 && i%2 == 0 { + continue + } value := fmt.Sprintf(`[%d,"%v"]`, start+(int64(i)*step), makeJsonTestValue(r)) values = append(values, value) } diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json index 4a132324ed8..3df173ca027 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json @@ -98,7 +98,7 @@ "data": { "values": [ [ - 1641889530123, + 1641889529123, 1641889532123 ], [ diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt index aa173aa0149..9df8e3f31f3 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.txt @@ -35,11 +35,11 @@ Dimensions: 2 Fields by 2 Rows | Labels: | Labels: __name__=prometheus_http_requests_total, code=400, handler=/api/v1/query_range, job=prometheus | | Type: []time.Time | Type: []*float64 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+ -| 2022-01-11 08:25:30.123 +0000 UTC | 54 | +| 2022-01-11 08:25:29.123 +0000 UTC | 54 | | 2022-01-11 08:25:32.123 +0000 UTC | 76 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== FRAME=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 -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.json b/pkg/tsdb/prometheus/testdata/range_simple.result.json index cb77311b908..c98977b0bd5 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.json +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.json @@ -24,7 +24,7 @@ "job": "prometheus" }, "values": [ - [1641889530.123, "54"], + [1641889529.123, "54"], [1641889532.123, "76"] ] } diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.json b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.json index d438a646dc2..7f670e39e17 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.json +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.json @@ -53,11 +53,13 @@ "data": { "values": [ [ + 1641889529123, 1641889530123, 1641889531123, 1641889532123 ], [ + null, 21, 32, 43 @@ -65,6 +67,7 @@ [ 54, null, + null, 76 ] ] diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.txt index b0d7e4c2bb7..be8952b12e1 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming-wide.golden.txt @@ -8,17 +8,18 @@ Frame[0] { "executedQueryString": "Expr: \nStep: 1s" } Name: -Dimensions: 3 Fields by 3 Rows +Dimensions: 3 Fields by 4 Rows +-----------------------------------+--------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------+ | Name: Time | Name: prometheus_http_requests_total{code="200", handler="/api/v1/query_range", job="prometheus"} | Name: prometheus_http_requests_total{code="400", handler="/api/v1/query_range", job="prometheus"} | | Labels: | Labels: __name__=prometheus_http_requests_total, code=200, handler=/api/v1/query_range, job=prometheus | Labels: __name__=prometheus_http_requests_total, code=400, handler=/api/v1/query_range, job=prometheus | | Type: []time.Time | Type: []*float64 | Type: []*float64 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------+ -| 2022-01-11 08:25:30.123 +0000 UTC | 21 | 54 | +| 2022-01-11 08:25:29.123 +0000 UTC | null | 54 | +| 2022-01-11 08:25:30.123 +0000 UTC | 21 | null | | 2022-01-11 08:25:31.123 +0000 UTC | 32 | null | | 2022-01-11 08:25:32.123 +0000 UTC | 43 | 76 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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AAAAANT7//8IAAAAcAAAAGQAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLXdpZGUiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9LCJleGVjdXRlZFF1ZXJ5U3RyaW5nIjoiRXhwcjogXG5TdGVwOiAxcyJ9AAAAAAQAAABtZXRhAAAAAAMAAABcAwAAsAEAAAQAAABq/v//FAAAACABAAAgAQAAAAADASABAAACAAAAgAAAAAQAAACM/P//CAAAAGQAAABbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjQwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAEAAAAbmFtZQAAAAAE/f//CAAAAHgAAABtAAAAeyJfX25hbWVfXyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbCIsImNvZGUiOiI0MDAiLCJoYW5kbGVyIjoiL2FwaS92MS9xdWVyeV9yYW5nZSIsImpvYiI6InByb21ldGhldXMifQAAAAYAAABsYWJlbHMAAAAAAABW/f//AAACAFsAAABwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT0iNDAwIiwgaGFuZGxlcj0iL2FwaS92MS9xdWVyeV9yYW5nZSIsIGpvYj0icHJvbWV0aGV1cyJ9AAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAAAgAQAAIAEAAAAAAwEgAQAAAgAAAIAAAAAEAAAANP7//wgAAABkAAAAWwAAAHByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPSIyMDAiLCBoYW5kbGVyPSIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwgam9iPSJwcm9tZXRoZXVzIn0ABAAAAG5hbWUAAAAArP7//wgAAAB4AAAAbQAAAHsiX19uYW1lX18iOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWwiLCJjb2RlIjoiMjAwIiwiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAGAAAAbGFiZWxzAAAAAAAA/v7//wAAAgBbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjIwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAeAAAAIAAAAAAAAAKgAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAcAAAAEQAAAHsiaW50ZXJ2YWwiOjEwMDB9AAAABgAAAGNvbmZpZwAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAA4BQAAQVJST1cx +FRAME=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ZAAAALT7//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA1Pv//wgAAABwAAAAZAAAAHsidHlwZSI6InRpbWVzZXJpZXMtd2lkZSIsImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In0sImV4ZWN1dGVkUXVlcnlTdHJpbmciOiJFeHByOiBcblN0ZXA6IDFzIn0AAAAABAAAAG1ldGEAAAAAAwAAAFwDAACwAQAABAAAAGr+//8UAAAAIAEAACABAAAAAAMBIAEAAAIAAACAAAAABAAAAIz8//8IAAAAZAAAAFsAAABwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT0iNDAwIiwgaGFuZGxlcj0iL2FwaS92MS9xdWVyeV9yYW5nZSIsIGpvYj0icHJvbWV0aGV1cyJ9AAQAAABuYW1lAAAAAAT9//8IAAAAeAAAAG0AAAB7Il9fbmFtZV9fIjoicHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFsIiwiY29kZSI6IjQwMCIsImhhbmRsZXIiOiIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAABgAAAGxhYmVscwAAAAAAAFb9//8AAAIAWwAAAHByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPSI0MDAiLCBoYW5kbGVyPSIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwgam9iPSJwcm9tZXRoZXVzIn0AAAASABgAFAATABIADAAAAAgABAASAAAAFAAAACABAAAgAQAAAAADASABAAACAAAAgAAAAAQAAAA0/v//CAAAAGQAAABbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjIwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAEAAAAbmFtZQAAAACs/v//CAAAAHgAAABtAAAAeyJfX25hbWVfXyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbCIsImNvZGUiOiIyMDAiLCJoYW5kbGVyIjoiL2FwaS92MS9xdWVyeV9yYW5nZSIsImpvYiI6InByb21ldGhldXMifQAAAAYAAABsYWJlbHMAAAAAAAD+/v//AAACAFsAAABwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT0iMjAwIiwgaGFuZGxlcj0iL2FwaS92MS9xdWVyeV9yYW5nZSIsIGpvYj0icHJvbWV0aGV1cyJ9AAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAAB4AAAAgAAAAAAAAAqAAAAAAgAAADQAAAAEAAAA3P///wgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAACAAMAAgABAAIAAAACAAAABwAAAARAAAAeyJpbnRlcnZhbCI6MTAwMH0AAAAGAAAAY29uZmlnAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAADgFAABBUlJPVzE= diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.json b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.json index 0646fa5240d..7e425fd1ea6 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.json +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.json @@ -96,7 +96,7 @@ "data": { "values": [ [ - 1641889530123, + 1641889529123, 1641889532123 ], [ diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.txt b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.txt index 9856f08b4dc..028b1598ae0 100644 --- a/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.txt +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.streaming.golden.txt @@ -35,11 +35,11 @@ Dimensions: 2 Fields by 2 Rows | Labels: | Labels: __name__=prometheus_http_requests_total, code=400, handler=/api/v1/query_range, job=prometheus | | Type: []time.Time | Type: []float64 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+ -| 2022-01-11 08:25:30.123 +0000 UTC | 54 | +| 2022-01-11 08:25:29.123 +0000 UTC | 54 | | 2022-01-11 08:25:32.123 +0000 UTC | 76 | +-----------------------------------+--------------------------------------------------------------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== FRAME=QVJST1cxAAD/////qAMAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAADABAAADAAAApAAAACgAAAAEAAAA+Pz//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAY/f//CAAAAGQAAABbAAAAcHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9IjIwMCIsIGhhbmRsZXI9Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCBqb2I9InByb21ldGhldXMifQAEAAAAbmFtZQAAAACQ/f//CAAAAHAAAABkAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55IiwiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJtYXRyaXgifSwiZXhlY3V0ZWRRdWVyeVN0cmluZyI6IkV4cHI6IFxuU3RlcDogMXMifQAAAAAEAAAAbWV0YQAAAAACAAAAoAEAAAQAAAB6/v//FAAAAGgBAABoAQAAAAAAA2gBAAADAAAAvAAAACwAAAAEAAAASP7//wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAbP7//wgAAAB4AAAAbQAAAHsiX19uYW1lX18iOiJwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWwiLCJjb2RlIjoiMjAwIiwiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAGAAAAbGFiZWxzAAD4/v//CAAAAIQAAAB5AAAAeyJkaXNwbGF5TmFtZUZyb21EUyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbHtjb2RlPVwiMjAwXCIsIGhhbmRsZXI9XCIvYXBpL3YxL3F1ZXJ5X3JhbmdlXCIsIGpvYj1cInByb21ldGhldXNcIn0ifQAAAAYAAABjb25maWcAAAAAAABW////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAHgAAACAAAAAAAAACoAAAAACAAAANAAAAAQAAADc////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAIAAwACAAEAAgAAAAIAAAAHAAAABEAAAB7ImludGVydmFsIjoxMDAwfQAAAAYAAABjb25maWcAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAMAYahvUKckWwOIEV9QpyRbArJ+S1CnJFgAAAAAAADVAAAAAAAAAQEAAAAAAAIBFQBAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAC4AwAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAMAEAAAMAAACkAAAAKAAAAAQAAAD4/P//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAABj9//8IAAAAZAAAAFsAAABwcm9tZXRoZXVzX2h0dHBfcmVxdWVzdHNfdG90YWx7Y29kZT0iMjAwIiwgaGFuZGxlcj0iL2FwaS92MS9xdWVyeV9yYW5nZSIsIGpvYj0icHJvbWV0aGV1cyJ9AAQAAABuYW1lAAAAAJD9//8IAAAAcAAAAGQAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9LCJleGVjdXRlZFF1ZXJ5U3RyaW5nIjoiRXhwcjogXG5TdGVwOiAxcyJ9AAAAAAQAAABtZXRhAAAAAAIAAACgAQAABAAAAHr+//8UAAAAaAEAAGgBAAAAAAADaAEAAAMAAAC8AAAALAAAAAQAAABI/v//CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABs/v//CAAAAHgAAABtAAAAeyJfX25hbWVfXyI6InByb21ldGhldXNfaHR0cF9yZXF1ZXN0c190b3RhbCIsImNvZGUiOiIyMDAiLCJoYW5kbGVyIjoiL2FwaS92MS9xdWVyeV9yYW5nZSIsImpvYiI6InByb21ldGhldXMifQAAAAYAAABsYWJlbHMAAPj+//8IAAAAhAAAAHkAAAB7ImRpc3BsYXlOYW1lRnJvbURTIjoicHJvbWV0aGV1c19odHRwX3JlcXVlc3RzX3RvdGFse2NvZGU9XCIyMDBcIiwgaGFuZGxlcj1cIi9hcGkvdjEvcXVlcnlfcmFuZ2VcIiwgam9iPVwicHJvbWV0aGV1c1wifSJ9AAAABgAAAGNvbmZpZwAAAAAAAFb///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAAeAAAAIAAAAAAAAAKgAAAAAIAAAA0AAAABAAAANz///8IAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAgADAAIAAQACAAAAAgAAAAcAAAAEQAAAHsiaW50ZXJ2YWwiOjEwMDB9AAAABgAAAGNvbmZpZwAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAADQAwAAQVJST1cx -FRAME=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 +FRAME=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 diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 4f74f7ed957..34c72442f5a 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -3,11 +3,13 @@ package converter import ( "encoding/json" "fmt" + "sort" "strconv" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/experimental" jsoniter "github.com/json-iterator/go" ) @@ -437,6 +439,8 @@ func readMatrixOrVectorWide(iter *jsoniter.Iterator, resultType string) *backend } if len(rsp.Frames) == 0 { + sorter := experimental.NewFrameSorter(frame, frame.Fields[0]) + sort.Sort(sorter) rsp.Frames = append(rsp.Frames, frame) } diff --git a/pkg/util/converter/testdata/loki-streams-a-frame.json b/pkg/util/converter/testdata/loki-streams-a-frame.json index 06eb3fe7c06..60125db8f94 100644 --- a/pkg/util/converter/testdata/loki-streams-a-frame.json +++ b/pkg/util/converter/testdata/loki-streams-a-frame.json @@ -6,34 +6,34 @@ "custom": { "stats": { "summary": { - "totalLinesProcessed": 55, "execTime": 0.002216125, "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, - "totalBytesProcessed": 7772 + "totalBytesProcessed": 7772, + "totalLinesProcessed": 55 }, "store": { - "decompressedLines": 55, - "totalChunksRef": 2, "totalChunksDownloaded": 2, "chunksDownloadTime": 0.000390958, + "compressedBytes": 31432, + "totalDuplicates": 0, + "totalChunksRef": 2, "headChunkBytes": 0, "headChunkLines": 0, "decompressedBytes": 7772, - "compressedBytes": 31432, - "totalDuplicates": 0 + "decompressedLines": 55 }, "ingester": { "totalBatches": 0, "headChunkLines": 0, "decompressedBytes": 0, - "totalDuplicates": 0, - "totalReached": 0, - "totalChunksMatched": 0, "decompressedLines": 0, "compressedBytes": 0, + "totalReached": 0, "totalLinesSent": 0, - "headChunkBytes": 0 + "headChunkBytes": 0, + "totalDuplicates": 0, + "totalChunksMatched": 0 } } } diff --git a/pkg/util/converter/testdata/loki-streams-a-wide-frame.json b/pkg/util/converter/testdata/loki-streams-a-wide-frame.json index 18812654643..e5f77993812 100644 --- a/pkg/util/converter/testdata/loki-streams-a-wide-frame.json +++ b/pkg/util/converter/testdata/loki-streams-a-wide-frame.json @@ -5,18 +5,6 @@ "meta": { "custom": { "stats": { - "ingester": { - "compressedBytes": 0, - "totalDuplicates": 0, - "totalReached": 0, - "totalChunksMatched": 0, - "totalBatches": 0, - "totalLinesSent": 0, - "decompressedBytes": 0, - "decompressedLines": 0, - "headChunkBytes": 0, - "headChunkLines": 0 - }, "summary": { "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, @@ -26,14 +14,26 @@ }, "store": { "totalChunksRef": 2, - "totalChunksDownloaded": 2, "headChunkBytes": 0, - "decompressedBytes": 7772, - "chunksDownloadTime": 0.000390958, "headChunkLines": 0, + "totalDuplicates": 0, + "totalChunksDownloaded": 2, + "chunksDownloadTime": 0.000390958, + "decompressedBytes": 7772, "decompressedLines": 55, - "compressedBytes": 31432, - "totalDuplicates": 0 + "compressedBytes": 31432 + }, + "ingester": { + "totalLinesSent": 0, + "headChunkBytes": 0, + "headChunkLines": 0, + "compressedBytes": 0, + "totalDuplicates": 0, + "totalReached": 0, + "totalBatches": 0, + "decompressedBytes": 0, + "decompressedLines": 0, + "totalChunksMatched": 0 } } } diff --git a/pkg/util/converter/testdata/loki-streams-b-frame.json b/pkg/util/converter/testdata/loki-streams-b-frame.json index 671a28f708f..70e0aefa735 100644 --- a/pkg/util/converter/testdata/loki-streams-b-frame.json +++ b/pkg/util/converter/testdata/loki-streams-b-frame.json @@ -6,34 +6,34 @@ "custom": { "stats": { "summary": { + "totalLinesProcessed": 55, + "execTime": 0.002216125, "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, - "totalBytesProcessed": 7772, - "totalLinesProcessed": 55, - "execTime": 0.002216125 + "totalBytesProcessed": 7772 }, "store": { + "totalChunksDownloaded": 2, + "headChunkBytes": 0, + "decompressedLines": 55, + "totalDuplicates": 0, "totalChunksRef": 2, "chunksDownloadTime": 0.000390958, "headChunkLines": 0, "decompressedBytes": 7772, - "compressedBytes": 31432, - "totalChunksDownloaded": 2, - "headChunkBytes": 0, - "decompressedLines": 55, - "totalDuplicates": 0 + "compressedBytes": 31432 }, "ingester": { + "decompressedBytes": 0, "compressedBytes": 0, "totalDuplicates": 0, - "totalReached": 0, - "totalLinesSent": 0, - "headChunkBytes": 0, - "headChunkLines": 0, "totalChunksMatched": 0, "totalBatches": 0, - "decompressedBytes": 0, - "decompressedLines": 0 + "headChunkLines": 0, + "decompressedLines": 0, + "totalReached": 0, + "totalLinesSent": 0, + "headChunkBytes": 0 } } } diff --git a/pkg/util/converter/testdata/loki-streams-b-wide-frame.json b/pkg/util/converter/testdata/loki-streams-b-wide-frame.json index 160b2e3e68b..564bafb3ea0 100644 --- a/pkg/util/converter/testdata/loki-streams-b-wide-frame.json +++ b/pkg/util/converter/testdata/loki-streams-b-wide-frame.json @@ -5,35 +5,35 @@ "meta": { "custom": { "stats": { + "store": { + "totalChunksDownloaded": 2, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksRef": 2, + "chunksDownloadTime": 0.000390958, + "headChunkBytes": 0, + "headChunkLines": 0, + "decompressedBytes": 7772, + "compressedBytes": 31432 + }, + "ingester": { + "totalDuplicates": 0, + "totalReached": 0, + "totalLinesSent": 0, + "decompressedBytes": 0, + "compressedBytes": 0, + "decompressedLines": 0, + "totalChunksMatched": 0, + "totalBatches": 0, + "headChunkBytes": 0, + "headChunkLines": 0 + }, "summary": { "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, "totalBytesProcessed": 7772, "totalLinesProcessed": 55, "execTime": 0.002216125 - }, - "store": { - "headChunkLines": 0, - "decompressedBytes": 7772, - "totalChunksRef": 2, - "chunksDownloadTime": 0.000390958, - "decompressedLines": 55, - "compressedBytes": 31432, - "totalDuplicates": 0, - "totalChunksDownloaded": 2, - "headChunkBytes": 0 - }, - "ingester": { - "compressedBytes": 0, - "totalDuplicates": 0, - "totalReached": 0, - "totalBatches": 0, - "totalLinesSent": 0, - "headChunkBytes": 0, - "decompressedLines": 0, - "totalChunksMatched": 0, - "headChunkLines": 0, - "decompressedBytes": 0 } } } From 9e8efaa4599cad3ba272c5a5689b94bb7f6e8b8b Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Thu, 26 May 2022 13:29:56 +0800 Subject: [PATCH 012/283] Alerting: Add stored screenshot utilities to the channels package. (#49470) Adds three functions: `withStoredImages` iterates over a list of models.Alerts, extracting a stored image's data from storage, if available, and executing a user-provided function. `withStoredImage` does this for an image attached to a specific alert. `openImage` finds and opens an image file on disk. Moves `store.Image` to `models.Image` Simplifies `channels.ImageStore` interface and updates notifiers that use it to use the simpler methods. Updates all pkg/alert/notifier/channels to use withStoredImage routines. --- pkg/services/ngalert/image/mock.go | 53 -------- pkg/services/ngalert/image/service.go | 14 +-- pkg/services/ngalert/models/image.go | 23 ++++ pkg/services/ngalert/notifier/alertmanager.go | 2 +- .../ngalert/notifier/channels/discord.go | 117 +++++++++--------- .../ngalert/notifier/channels/email.go | 38 ++---- .../ngalert/notifier/channels/factory.go | 7 +- .../ngalert/notifier/channels/googlechat.go | 29 ++--- .../ngalert/notifier/channels/opsgenie.go | 27 ++-- .../ngalert/notifier/channels/pagerduty.go | 25 ++-- .../ngalert/notifier/channels/slack.go | 81 ++++++------ .../ngalert/notifier/channels/teams.go | 22 ++-- .../ngalert/notifier/channels/utils.go | 81 ++++++++++-- .../ngalert/notifier/channels/webhook.go | 29 ++--- pkg/services/ngalert/notifier/testing.go | 16 +-- pkg/services/ngalert/schedule/compat_test.go | 3 +- .../ngalert/state/manager_private_test.go | 10 +- pkg/services/ngalert/state/state.go | 3 +- pkg/services/ngalert/store/image.go | 82 ++---------- pkg/services/ngalert/store/image_test.go | 13 +- 20 files changed, 289 insertions(+), 386 deletions(-) delete mode 100644 pkg/services/ngalert/image/mock.go create mode 100644 pkg/services/ngalert/models/image.go diff --git a/pkg/services/ngalert/image/mock.go b/pkg/services/ngalert/image/mock.go deleted file mode 100644 index 1529f966ac4..00000000000 --- a/pkg/services/ngalert/image/mock.go +++ /dev/null @@ -1,53 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/grafana/grafana/pkg/services/ngalert/image (interfaces: ImageService) - -// Package image is a generated GoMock package. -package image - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - data "github.com/grafana/grafana-plugin-sdk-go/data" - models "github.com/grafana/grafana/pkg/services/ngalert/models" - store "github.com/grafana/grafana/pkg/services/ngalert/store" -) - -// MockImageService is a mock of ImageService interface. -type MockImageService struct { - ctrl *gomock.Controller - recorder *MockImageServiceMockRecorder -} - -// MockImageServiceMockRecorder is the mock recorder for MockImageService. -type MockImageServiceMockRecorder struct { - mock *MockImageService -} - -// NewMockImageService creates a new mock instance. -func NewMockImageService(ctrl *gomock.Controller) *MockImageService { - mock := &MockImageService{ctrl: ctrl} - mock.recorder = &MockImageServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockImageService) EXPECT() *MockImageServiceMockRecorder { - return m.recorder -} - -// NewImage mocks base method. -func (m *MockImageService) NewImage(arg0 context.Context, arg1 *models.AlertRule, arg2 data.Labels) (*store.Image, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewImage", arg0, arg1, arg2) - ret0, _ := ret[0].(*store.Image) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// NewImage indicates an expected call of NewImage. -func (mr *MockImageServiceMockRecorder) NewImage(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewImage", reflect.TypeOf((*MockImageService)(nil).NewImage), arg0, arg1, arg2) -} diff --git a/pkg/services/ngalert/image/service.go b/pkg/services/ngalert/image/service.go index 4af73af9a2f..f19501475fc 100644 --- a/pkg/services/ngalert/image/service.go +++ b/pkg/services/ngalert/image/service.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/services/dashboards" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/screenshot" @@ -20,7 +20,7 @@ import ( //go:generate mockgen -destination=mock.go -package=image github.com/grafana/grafana/pkg/services/ngalert/image ImageService type ImageService interface { // NewImage returns a new image for the alert instance. - NewImage(ctx context.Context, r *ngmodels.AlertRule) (*store.Image, error) + NewImage(ctx context.Context, r *models.AlertRule) (*models.Image, error) } var ( @@ -83,7 +83,7 @@ func NewScreenshotImageServiceFromCfg(cfg *setting.Cfg, metrics prometheus.Regis // NewImage returns a screenshot of the panel for the alert rule. It returns // ErrNoDashboard if the alert rule does not have a dashboard and ErrNoPanel // when the alert rule does not have a panel in a dashboard. -func (s *ScreenshotImageService) NewImage(ctx context.Context, r *ngmodels.AlertRule) (*store.Image, error) { +func (s *ScreenshotImageService) NewImage(ctx context.Context, r *models.AlertRule) (*models.Image, error) { if r.DashboardUID == nil { return nil, ErrNoDashboard } @@ -102,7 +102,7 @@ func (s *ScreenshotImageService) NewImage(ctx context.Context, r *ngmodels.Alert return nil, fmt.Errorf("failed to take screenshot: %w", err) } - v := store.Image{ + v := models.Image{ Path: screenshot.Path, URL: screenshot.URL, } @@ -115,12 +115,12 @@ func (s *ScreenshotImageService) NewImage(ctx context.Context, r *ngmodels.Alert type NotAvailableImageService struct{} -func (s *NotAvailableImageService) NewImage(ctx context.Context, r *ngmodels.AlertRule) (*store.Image, error) { +func (s *NotAvailableImageService) NewImage(ctx context.Context, r *models.AlertRule) (*models.Image, error) { return nil, screenshot.ErrScreenshotsUnavailable } type NoopImageService struct{} -func (s *NoopImageService) NewImage(ctx context.Context, r *ngmodels.AlertRule) (*store.Image, error) { - return &store.Image{}, nil +func (s *NoopImageService) NewImage(ctx context.Context, r *models.AlertRule) (*models.Image, error) { + return &models.Image{}, nil } diff --git a/pkg/services/ngalert/models/image.go b/pkg/services/ngalert/models/image.go new file mode 100644 index 00000000000..0610f6ca1c5 --- /dev/null +++ b/pkg/services/ngalert/models/image.go @@ -0,0 +1,23 @@ +package models + +import ( + "errors" + "time" +) + +// ErrImageNotFound is returned when the image does not exist. +var ErrImageNotFound = errors.New("image not found") + +type Image struct { + ID int64 `xorm:"pk autoincr 'id'"` + Token string `xorm:"token"` + Path string `xorm:"path"` + URL string `xorm:"url"` + CreatedAt time.Time `xorm:"created_at"` + ExpiresAt time.Time `xorm:"expires_at"` +} + +// A XORM interface that defines the used table for this struct. +func (i *Image) TableName() string { + return "alert_image" +} diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 7d1ff55c89b..7243a9ac606 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -88,7 +88,7 @@ type ClusterPeer interface { type AlertingStore interface { store.AlertingStore - channels.ImageStore + store.ImageStore } type Alertmanager struct { diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index c113dfd0a29..874b9354c40 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -196,65 +197,52 @@ func (d DiscordNotifier) SendResolved() bool { func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.Alert, embedQuota int) []discordAttachment { attachments := make([]discordAttachment, 0) - for i := range as { - if embedQuota == 0 { - break - } - imgToken := getTokenFromAnnotations(as[i].Annotations) - if len(imgToken) == 0 { - continue - } - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := d.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - d.log.Warn("failed to retrieve image url from store", "error", err) - } - } - - if len(imgURL) > 0 { - attachments = append(attachments, discordAttachment{ - url: imgURL, - state: as[i].Status(), - alertName: as[i].Name(), - }) - } else { - // Need to upload the file. Tell Discord that we're embedding an attachment. - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - fp, err := d.images.GetFilepath(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - d.log.Warn("failed to retrieve image filepath from store", "error", err) - } + _ = withStoredImages(ctx, d.log, d.images, + func(index int, image *ngmodels.Image) error { + if embedQuota < 1 { + // TODO: Could be a sentinel error to stop execution. + return nil } - base := filepath.Base(fp) - url := fmt.Sprintf("attachment://%s", base) - timeoutCtx, cancel = context.WithTimeout(ctx, ImageStoreTimeout) - reader, err := d.images.GetData(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. + if image == nil { + return nil + } + + if len(image.URL) > 0 { + attachments = append(attachments, discordAttachment{ + url: image.URL, + state: as[index].Status(), + alertName: as[index].Name(), + }) + embedQuota-- + return nil + } + + // If we have a local file, but no public URL, upload the image as an attachment. + if len(image.Path) > 0 { + base := filepath.Base(image.Path) + url := fmt.Sprintf("attachment://%s", base) + reader, err := openImage(image.Path) + if err != nil && !errors.Is(err, ngmodels.ErrImageNotFound) { d.log.Warn("failed to retrieve image data from store", "error", err) + return nil } - } - attachments = append(attachments, discordAttachment{ - url: url, - name: base, - reader: reader, - state: as[i].Status(), - alertName: as[i].Name(), - }) - } - embedQuota++ - } + attachments = append(attachments, discordAttachment{ + url: url, + name: base, + reader: reader, + state: as[index].Status(), + alertName: as[index].Name(), + }) + embedQuota-- + } + return nil + }, + as..., + ) + return attachments } @@ -282,21 +270,32 @@ func (d DiscordNotifier) buildRequest(ctx context.Context, url string, body []by if err != nil { return nil, err } + if _, err := payload.Write(body); err != nil { return nil, err } + for _, a := range attachments { - part, err := w.CreateFormFile("", a.name) - if err != nil { - return nil, err - } - if _, err := io.Copy(part, a.reader); err != nil { - return nil, err + if a.reader != nil { // We have an image to upload. + err = func() error { + defer func() { _ = a.reader.Close() }() + part, err := w.CreateFormFile("", a.name) + if err != nil { + return err + } + _, err = io.Copy(part, a.reader) + return err + }() + if err != nil { + return nil, err + } } } + if err := w.Close(); err != nil { return nil, fmt.Errorf("failed to close multipart writer: %w", err) } + cmd.ContentType = w.FormDataContentType() cmd.Body = b.String() return cmd, nil diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 3120c267516..bbbc6de13e0 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/util" ) @@ -126,38 +127,25 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, // TODO: modify the email sender code to support multiple file or image URL // fields. We cannot use images from every alert yet. - imgToken := getTokenFromAnnotations(as[0].Annotations) - if len(imgToken) != 0 { - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := en.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - en.log.Warn("failed to retrieve image url from store", "error", err) + _ = withStoredImage(ctx, en.log, en.images, + func(index int, image *ngmodels.Image) error { + if image == nil { + return nil } - } else if len(imgURL) > 0 { - cmd.Data["ImageLink"] = imgURL - } else { // Try to upload - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgPath, err := en.images.GetFilepath(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - en.log.Warn("failed to retrieve image url from store", "error", err) - } - } else if len(imgPath) != 0 { - file, err := os.Stat(imgPath) + + if len(image.URL) != 0 { + cmd.Data["ImageLink"] = image.URL + } else if len(image.Path) != 0 { + file, err := os.Stat(image.Path) if err == nil { - cmd.EmbeddedFiles = []string{imgPath} + cmd.EmbeddedFiles = []string{image.Path} cmd.Data["EmbeddedImage"] = file.Name() } else { en.log.Warn("failed to access email notification image attachment data", "error", err) } } - } - } + return nil + }, 0, as...) if tmplErr != nil { en.log.Warn("failed to template email message", "err", tmplErr.Error()) diff --git a/pkg/services/ngalert/notifier/channels/factory.go b/pkg/services/ngalert/notifier/channels/factory.go index 35334786d87..0a8d4c6a18b 100644 --- a/pkg/services/ngalert/notifier/channels/factory.go +++ b/pkg/services/ngalert/notifier/channels/factory.go @@ -3,9 +3,9 @@ package channels import ( "context" "errors" - "io" "strings" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/prometheus/alertmanager/template" ) @@ -19,11 +19,8 @@ type FactoryConfig struct { Template *template.Template } -// A specialization of store.ImageStore, to avoid an import loop. type ImageStore interface { - GetURL(ctx context.Context, token string) (string, error) - GetFilepath(ctx context.Context, token string) (string, error) - GetData(ctx context.Context, token string) (io.ReadCloser, error) + GetImage(ctx context.Context, token string) (*models.Image, error) } func NewFactoryConfig(config *NotificationChannelConfig, notificationService notifications.Service, diff --git a/pkg/services/ngalert/notifier/channels/googlechat.go b/pkg/services/ngalert/notifier/channels/googlechat.go index 2179221ffcc..5c7ed8ad68d 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat.go +++ b/pkg/services/ngalert/notifier/channels/googlechat.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -188,40 +189,32 @@ func (gcn *GoogleChatNotifier) buildScreenshotCard(ctx context.Context, alerts [ }, Sections: []section{}, } - for _, alert := range alerts { - imgToken := getTokenFromAnnotations(alert.Annotations) - if len(imgToken) == 0 { - continue - } - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := gcn.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - gcn.log.Warn("failed to retrieve image url from store", "error", err) + _ = withStoredImages(ctx, gcn.log, gcn.images, + func(index int, image *ngmodels.Image) error { + if image == nil || len(image.URL) == 0 { + return nil } - } - if len(imgURL) > 0 { section := section{ Widgets: []widget{ textParagraphWidget{ Text: text{ - Text: fmt.Sprintf("%s: %s", alert.Status(), alert.Name()), + Text: fmt.Sprintf("%s: %s", alerts[index].Status(), alerts[index].Name()), }, }, imageWidget{ Image: imageData{ - ImageURL: imgURL, + ImageURL: image.URL, }, }, }, } card.Sections = append(card.Sections, section) - } - } + + return nil + }, alerts...) + if len(card.Sections) == 0 { return nil } diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index 2afcedac7d3..0a06501eb9c 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" @@ -212,25 +213,15 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod } images := []string{} - for i := range as { - imgToken := getTokenFromAnnotations(as[i].Annotations) - if len(imgToken) == 0 { - continue - } - - dbContext, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := on.images.GetURL(dbContext, imgToken) - cancel() - - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - on.log.Warn("Error reading screenshot data from ImageStore: %v", err) + _ = withStoredImages(ctx, on.log, on.images, + func(index int, image *ngmodels.Image) error { + if image == nil || len(image.URL) == 0 { + return nil } - } else if len(imgURL) != 0 { - images = append(images, imgURL) - } - } + images = append(images, image.URL) + return nil + }, + as...) if len(images) != 0 { details.Set("image_urls", images) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index 944ce15bca1..65cc96ebc6c 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" @@ -186,23 +187,15 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m }, } - for i := range as { - imgToken := getTokenFromAnnotations(as[i].Annotations) - if len(imgToken) == 0 { - continue - } - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := pn.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - pn.log.Warn("failed to retrieve image url from store", "error", err) + _ = withStoredImages(ctx, pn.log, pn.images, + func(index int, image *ngmodels.Image) error { + if image != nil && len(image.URL) != 0 { + msg.Images = append(msg.Images, pagerDutyImage{Src: image.URL}) } - } else { - msg.Images = append(msg.Images, pagerDutyImage{Src: imgURL}) - } - } + + return nil + }, + as...) if len(msg.Payload.Summary) > 1024 { // This is the Pagerduty limit. diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 9a5e9e67e68..e1132d3c819 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/alertmanager/config" @@ -226,34 +227,38 @@ func (sn *SlackNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo return false, err } + var imgData io.ReadCloser + // Try to upload if we have an image path but no image URL. This uploads the file // immediately after the message. A bit of a hack, but it doesn't require the // user to have an image host set up. - // TODO: how many image files should we upload? In what order? Should we - // assume the alerts array is already sorted? // TODO: We need a refactoring so we don't do two database reads for the same data. - // TODO: Should we process all alerts' annotations? We can only have on image. - // TODO: Should we guard out-of-bounds errors here? Callers should prevent that from happening, imo - imgToken := getTokenFromAnnotations(alerts[0].Annotations) - dbContext, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgData, err := sn.images.GetData(dbContext, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - sn.log.Warn("Error reading screenshot data from ImageStore: %v", err) + if len(msg.Attachments[0].ImageURL) == 0 { + _ = withStoredImage(ctx, sn.log, sn.images, + func(index int, image *ngmodels.Image) error { + if image == nil || len(image.Path) == 0 { + return nil + } + + imgData, err = openImage(image.Path) + if err != nil { + imgData = nil + } + + return nil + }, + 0, alerts...) + + if imgData != nil { + defer func() { + _ = imgData.Close() + }() + + err = sn.slackFileUpload(ctx, imgData, sn.Recipient, sn.Token) + if err != nil { + sn.log.Warn("Error reading screenshot data from ImageStore: %v", err) + } } - return true, nil - } - - defer func() { - // Nothing for us to do. - _ = imgData.Close() - }() - - err = sn.slackFileUpload(ctx, imgData, sn.Recipient, sn.Token) - if err != nil { - sn.log.Warn("Error reading screenshot data from ImageStore: %v", err) } return true, nil @@ -319,26 +324,13 @@ var sendSlackRequest = func(request *http.Request, logger log.Logger) error { return nil } -func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Alert) (*slackMessage, error) { - alerts := types.Alerts(as...) +func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, alrts []*types.Alert) (*slackMessage, error) { + alerts := types.Alerts(alrts...) var tmplErr error - tmpl, _ := TmplText(ctx, sn.tmpl, as, sn.log, &tmplErr) + tmpl, _ := TmplText(ctx, sn.tmpl, alrts, sn.log, &tmplErr) ruleURL := joinUrlPath(sn.tmpl.ExternalURL.String(), "/alerting/list", sn.log) - // TODO: Should we process all alerts' annotations? We can only have on image. - // TODO: Should we guard out-of-bounds errors here? Callers should prevent that from happening, imo - imgToken := getTokenFromAnnotations(as[0].Annotations) - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := sn.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - sn.log.Warn("failed to retrieve image url from store", "error", err) - } - } - req := &slackMessage{ Channel: tmpl(sn.Recipient), Username: tmpl(sn.Username), @@ -353,7 +345,6 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Aler Fallback: tmpl(sn.Title), Footer: "Grafana v" + setting.BuildVersion, FooterIcon: FooterIconURL, - ImageURL: imgURL, Ts: time.Now().Unix(), TitleLink: ruleURL, Text: tmpl(sn.Text), @@ -361,6 +352,16 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, as []*types.Aler }, }, } + + _ = withStoredImage(ctx, sn.log, sn.images, + func(index int, image *ngmodels.Image) error { + if image != nil { + req.Attachments[0].ImageURL = image.URL + } + return nil + }, + 0, alrts...) + if tmplErr != nil { sn.log.Warn("failed to template Slack message", "err", tmplErr.Error()) } diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 0b9799f4d91..f08f68c15b4 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" ) @@ -93,21 +94,14 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, ruleURL := joinUrlPath(tn.tmpl.ExternalURL.String(), "/alerting/list", tn.log) images := []teamsImage{} - for i := range as { - imgToken := getTokenFromAnnotations(as[i].Annotations) - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err := tn.images.GetURL(timeoutCtx, imgToken) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - tn.log.Warn("failed to retrieve image url from store", "error", err) + _ = withStoredImages(ctx, tn.log, tn.images, + func(index int, image *ngmodels.Image) error { + if image != nil && len(image.URL) != 0 { + images = append(images, teamsImage{Image: image.URL}) } - } - if len(imgURL) > 0 { - images = append(images, teamsImage{Image: imgURL}) - } - } + return nil + }, + as...) // Note: these template calls must remain in this order title := tmpl(tn.Title) diff --git a/pkg/services/ngalert/notifier/channels/utils.go b/pkg/services/ngalert/notifier/channels/utils.go index 688d13f6a3c..01c9cae7382 100644 --- a/pkg/services/ngalert/notifier/channels/utils.go +++ b/pkg/services/ngalert/notifier/channels/utils.go @@ -10,10 +10,13 @@ import ( "net" "net/http" "net/url" + "os" "path" + "path/filepath" "time" "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/infra/log" @@ -38,6 +41,73 @@ var ( ErrImagesUnavailable = errors.New("alert screenshots are unavailable") ) +// For each alert, attempts to load the models.Image for an image token +// associated with the alert, then calls forEachFunc with the index of the +// alert and the retrieved image struct. If there is no image token, or the +// image does not exist, forEachFunc will be called with a nil value for the +// image. If forEachFunc returns an error, withStoredImages will return +// immediately. If there is a runtime error retrieving images from the image +// store, withStoredImages will attempt to continue executing, after logging +// a warning. +func withStoredImages(ctx context.Context, l log.Logger, imageStore ImageStore, forEachFunc func(index int, image *models.Image) error, alerts ...*types.Alert) error { + for i := range alerts { + err := withStoredImage(ctx, l, imageStore, forEachFunc, i, alerts...) + if err != nil { + return err + } + } + return nil +} + +func withStoredImage(ctx context.Context, l log.Logger, imageStore ImageStore, imageFunc func(index int, image *models.Image) error, index int, alerts ...*types.Alert) error { + imgToken := getTokenFromAnnotations(alerts[index].Annotations) + if len(imgToken) == 0 { + err := imageFunc(index, nil) + if err != nil { + return err + } + } + + timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) + img, err := imageStore.GetImage(timeoutCtx, imgToken) + cancel() + + if errors.Is(err, models.ErrImageNotFound) || errors.Is(err, ErrImagesUnavailable) { + err := imageFunc(index, nil) + if err != nil { + return err + } + } else if err != nil { + // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. + l.Warn("failed to retrieve image url from store", "error", err) + } + + err = imageFunc(index, img) + if err != nil { + return err + } + + return nil +} + +// The path argument here comes from reading internal image storage, not user +// input, so we ignore the security check here. +//nolint:gosec +func openImage(path string) (io.ReadCloser, error) { + fp := filepath.Clean(path) + _, err := os.Stat(fp) + if os.IsNotExist(err) || os.IsPermission(err) { + return nil, models.ErrImageNotFound + } + + f, err := os.Open(fp) + if err != nil { + return nil, err + } + + return f, nil +} + func getTokenFromAnnotations(annotations model.LabelSet) string { if value, ok := annotations[models.ScreenshotTokenAnnotation]; ok { return string(value) @@ -47,15 +117,8 @@ func getTokenFromAnnotations(annotations model.LabelSet) string { type UnavailableImageStore struct{} -func (n *UnavailableImageStore) GetURL(ctx context.Context, token string) (string, error) { - return "", ErrImagesUnavailable -} - -func (n *UnavailableImageStore) GetFilepath(ctx context.Context, token string) (string, error) { - return "", ErrImagesUnavailable -} - -func (n *UnavailableImageStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) { +// Get returns the image with the corresponding token, or ErrImageNotFound. +func (u *UnavailableImageStore) GetImage(ctx context.Context, token string) (*models.Image, error) { return nil, ErrImagesUnavailable } diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index 864623383a0..b88723277dd 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" @@ -113,33 +114,19 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool return false, err } - // Get screenshot reference tokens out of data before private annotations are cleared. - imgTokens := make([]string, 0, len(as)) - for i := range as { - imgTokens = append(imgTokens, getTokenFromAnnotations(as[i].Annotations)) - } - as, numTruncated := truncateAlerts(wn.MaxAlerts, as) var tmplErr error tmpl, data := TmplText(ctx, wn.tmpl, as, wn.log, &tmplErr) // Augment our Alert data with ImageURLs if available. - for i := range data.Alerts { - imgURL := "" - if len(imgTokens[i]) != 0 { - timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout) - imgURL, err = wn.images.GetURL(timeoutCtx, imgTokens[i]) - cancel() - if err != nil { - if !errors.Is(err, ErrImagesUnavailable) { - // Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist. - wn.log.Warn("failed to retrieve image url from store", "error", err) - } - } else if len(imgURL) != 0 { - data.Alerts[i].ImageURL = imgURL + _ = withStoredImages(ctx, wn.log, wn.images, + func(index int, image *ngmodels.Image) error { + if image != nil && len(image.URL) != 0 { + data.Alerts[index].ImageURL = image.URL } - } - } + return nil + }, + as...) msg := &webhookMessage{ Version: "1", diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index 1cfc41d1aee..a1fc49a03e5 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -5,7 +5,6 @@ import ( "crypto/md5" "errors" "fmt" - "io" "strings" "sync" "testing" @@ -19,18 +18,13 @@ type FakeConfigStore struct { configs map[int64]*models.AlertConfiguration } -func (f *FakeConfigStore) GetURL(ctx context.Context, token string) (string, error) { - return "", store.ErrImageNotFound +// Saves the image or returns an error. +func (f *FakeConfigStore) SaveImage(ctx context.Context, img *models.Image) error { + return models.ErrImageNotFound } -func (f *FakeConfigStore) GetFilepath(ctx context.Context, token string) (string, error) { - return "", store.ErrImageNotFound -} - -// Returns an io.ReadCloser that reads out the image data for the provided -// token, if available. May return ErrImageNotFound. -func (f *FakeConfigStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) { - return nil, store.ErrImageNotFound +func (f *FakeConfigStore) GetImage(ctx context.Context, token string) (*models.Image, error) { + return nil, models.ErrImageNotFound } func NewFakeConfigStore(t *testing.T, configs map[int64]*models.AlertConfiguration) FakeConfigStore { diff --git a/pkg/services/ngalert/schedule/compat_test.go b/pkg/services/ngalert/schedule/compat_test.go index 7af2ee8a171..b94d6e07e56 100644 --- a/pkg/services/ngalert/schedule/compat_test.go +++ b/pkg/services/ngalert/schedule/compat_test.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" - "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" ) @@ -122,7 +121,7 @@ func Test_stateToPostableAlert(t *testing.T) { t.Run("add __alertScreenshotToken__ if there is an image token", func(t *testing.T) { alertState := randomState(tc.state) alertState.Annotations = randomMapOfStrings() - alertState.Image = &store.Image{Token: "test_token"} + alertState.Image = &ngModels.Image{Token: "test_token"} result := stateToPostableAlert(alertState, appURL) diff --git a/pkg/services/ngalert/state/manager_private_test.go b/pkg/services/ngalert/state/manager_private_test.go index 00ad1160ac6..45933c2eef4 100644 --- a/pkg/services/ngalert/state/manager_private_test.go +++ b/pkg/services/ngalert/state/manager_private_test.go @@ -21,9 +21,9 @@ type CountingImageService struct { Called int } -func (c *CountingImageService) NewImage(_ context.Context, _ *ngmodels.AlertRule) (*store.Image, error) { +func (c *CountingImageService) NewImage(_ context.Context, _ *ngmodels.AlertRule) (*ngmodels.Image, error) { c.Called += 1 - return &store.Image{ + return &ngmodels.Image{ Token: fmt.Sprint(rand.Int()), }, nil } @@ -40,7 +40,7 @@ func Test_maybeNewImage(t *testing.T) { true, &State{ State: eval.Alerting, - Image: &store.Image{ + Image: &ngmodels.Image{ Token: "erase me", }, }, @@ -60,7 +60,7 @@ func Test_maybeNewImage(t *testing.T) { &State{ Resolved: true, State: eval.Normal, - Image: &store.Image{ + Image: &ngmodels.Image{ Token: "abcd", }, }, @@ -71,7 +71,7 @@ func Test_maybeNewImage(t *testing.T) { false, &State{ State: eval.Alerting, - Image: &store.Image{ + Image: &ngmodels.Image{ Token: "already set", }, }, diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 44a1bcb1cbb..7aa8c20a66a 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/store" ) type State struct { @@ -33,7 +32,7 @@ type State struct { Resolved bool Annotations map[string]string Labels data.Labels - Image *store.Image + Image *models.Image Error error } diff --git a/pkg/services/ngalert/store/image.go b/pkg/services/ngalert/store/image.go index 49a3781ee8c..4d0f94b0ee0 100644 --- a/pkg/services/ngalert/store/image.go +++ b/pkg/services/ngalert/store/image.go @@ -2,61 +2,32 @@ package store import ( "context" - "errors" "fmt" - "io" - "os" "time" "github.com/gofrs/uuid" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" ) -var ( - // ErrImageNotFound is returned when the image does not exist. - ErrImageNotFound = errors.New("image not found") -) - -type Image struct { - ID int64 `xorm:"pk autoincr 'id'"` - Token string `xorm:"token"` - Path string `xorm:"path"` - URL string `xorm:"url"` - CreatedAt time.Time `xorm:"created_at"` - ExpiresAt time.Time `xorm:"expires_at"` -} - -// A XORM interface that lets us clean up our SQL session definition. -func (i *Image) TableName() string { - return "alert_image" -} - type ImageStore interface { // Get returns the image with the token or ErrImageNotFound. - GetImage(ctx context.Context, token string) (*Image, error) + GetImage(ctx context.Context, token string) (*models.Image, error) // Saves the image or returns an error. - SaveImage(ctx context.Context, img *Image) error - - GetURL(ctx context.Context, token string) (string, error) - - GetFilepath(ctx context.Context, token string) (string, error) - - // Returns an io.ReadCloser that reads out the image data for the provided - // token, if available. May return ErrImageNotFound. - GetData(ctx context.Context, token string) (io.ReadCloser, error) + SaveImage(ctx context.Context, img *models.Image) error } -func (st DBstore) GetImage(ctx context.Context, token string) (*Image, error) { - var img Image +func (st DBstore) GetImage(ctx context.Context, token string) (*models.Image, error) { + var img models.Image if err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { exists, err := sess.Where("token = ?", token).Get(&img) if err != nil { return fmt.Errorf("failed to get image: %w", err) } if !exists { - return ErrImageNotFound + return models.ErrImageNotFound } return nil }); err != nil { @@ -65,7 +36,7 @@ func (st DBstore) GetImage(ctx context.Context, token string) (*Image, error) { return &img, nil } -func (st DBstore) SaveImage(ctx context.Context, img *Image) error { +func (st DBstore) SaveImage(ctx context.Context, img *models.Image) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { // TODO: Is this a good idea? Do we actually want to automatically expire // rows? See issue https://github.com/grafana/grafana/issues/49366 @@ -93,47 +64,10 @@ func (st DBstore) SaveImage(ctx context.Context, img *Image) error { }) } -func (st *DBstore) GetURL(ctx context.Context, token string) (string, error) { - img, err := st.GetImage(ctx, token) - if err != nil { - return "", err - } - return img.URL, nil -} - -func (st *DBstore) GetFilepath(ctx context.Context, token string) (string, error) { - img, err := st.GetImage(ctx, token) - if err != nil { - return "", err - } - return img.Path, nil -} - -func (st *DBstore) GetData(ctx context.Context, token string) (io.ReadCloser, error) { - // TODO: Should we support getting data from image.URL? One could configure - // the system to upload to S3 while still reading data for notifiers like - // Slack that take multipart uploads. - img, err := st.GetImage(ctx, token) - if err != nil { - return nil, err - } - - if len(img.Path) == 0 { - return nil, ErrImageNotFound - } - - f, err := os.Open(img.Path) - if err != nil { - return nil, err - } - - return f, nil -} - //nolint:unused func (st DBstore) DeleteExpiredImages(ctx context.Context) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - n, err := sess.Where("expires_at < ?", TimeNow()).Delete(&Image{}) + n, err := sess.Where("expires_at < ?", TimeNow()).Delete(&models.Image{}) if err != nil { return fmt.Errorf("failed to delete expired images: %w", err) } diff --git a/pkg/services/ngalert/store/image_test.go b/pkg/services/ngalert/store/image_test.go index 1669adc2c4e..bd607a9137f 100644 --- a/pkg/services/ngalert/store/image_test.go +++ b/pkg/services/ngalert/store/image_test.go @@ -12,12 +12,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests" ) -func createTestImg(fakeUrl string, fakePath string) *store.Image { - return &store.Image{ +func createTestImg(fakeUrl string, fakePath string) *models.Image { + return &models.Image{ ID: 0, Token: "", Path: fakeUrl + "local", @@ -25,12 +26,12 @@ func createTestImg(fakeUrl string, fakePath string) *store.Image { } } -func addID(img *store.Image, id int64) *store.Image { +func addID(img *models.Image, id int64) *models.Image { img.ID = id return img } -func addToken(img *store.Image) *store.Image { +func addToken(img *models.Image) *models.Image { token, err := uuid.NewV4() if err != nil { panic("wat") @@ -47,7 +48,7 @@ func TestIntegrationSaveAndGetImage(t *testing.T) { // Here are some images to save. imgs := []struct { name string - img *store.Image + img *models.Image errors bool }{ { @@ -99,7 +100,7 @@ func TestIntegrationDeleteExpiredImages(t *testing.T) { _, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds) // Save two images. - imgs := []*store.Image{ + imgs := []*models.Image{ createTestImg("", ""), createTestImg("", ""), } From 514d1bbbdd944b5f0e6a1bd5f8a7aad078b1edb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 26 May 2022 10:23:28 +0200 Subject: [PATCH 013/283] loki: added two new functions (#49617) --- public/app/plugins/datasource/loki/syntax.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/app/plugins/datasource/loki/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts index 94a5e54dab4..ad19acff4f7 100644 --- a/public/app/plugins/datasource/loki/syntax.ts +++ b/public/app/plugins/datasource/loki/syntax.ts @@ -122,6 +122,18 @@ export const RANGE_VEC_FUNCTIONS = [ detail: 'max_over_time(range-vector)', documentation: 'The maximum of all values in the specified interval. Only available in Loki 2.0+.', }, + { + insertText: 'first_over_time', + label: 'first_over_time', + detail: 'first_over_time(range-vector)', + documentation: 'The first of all values in the specified interval. Only available in Loki 2.3+.', + }, + { + insertText: 'last_over_time', + label: 'last_over_time', + detail: 'last_over_time(range-vector)', + documentation: 'The last of all values in the specified interval. Only available in Loki 2.3+.', + }, { insertText: 'sum_over_time', label: 'sum_over_time', From d8d7b3ec9d097a774621b9c28b36ca0c95e96510 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 26 May 2022 18:28:08 +1000 Subject: [PATCH 014/283] refactoring: saml (#48114) Co-authored-by: Vardan Torosyan --- docs/sources/enterprise/saml/about-saml.md | 34 ++----------------- .../enterprise/saml/set-up-saml-with-okta.md | 30 ++++++++++++++++ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/sources/enterprise/saml/about-saml.md b/docs/sources/enterprise/saml/about-saml.md index 47b625dfd64..75f02d25b32 100644 --- a/docs/sources/enterprise/saml/about-saml.md +++ b/docs/sources/enterprise/saml/about-saml.md @@ -44,38 +44,8 @@ In terms of initiation, Grafana supports: - SP-initiated requests - IdP-initiated requests -By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, see https://grafana.com/docs/grafana/latest/enterprise/saml/#idp-initiated-single-sign-on-sso. +By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, refer to [IdP-initiated]({{< relref "./configure-saml/#idp-initiated-single-sign-on-sso" >}}) to get more information. ### Edit SAML options in the Grafana config file -Once you have enabled saml, you can configure Grafana to use it for SAML authentication. Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}) to get more information about how to configure Grafana. - -**Edit SAML options in Grafana config file:** - -1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref ".././enterprise-configuration.md#enabled" >}}) to `true`. -1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). -1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. -1. Set the [`idp_metadata_url`]({{< relref ".././enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. -1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). - - [`assertion_attribute_login`]({{< relref ".././enterprise-configuration.md#assertion-attribute-login" >}}) - - [`assertion_attribute_email`]({{< relref ".././enterprise-configuration.md#assertion-attribute-email" >}}) - - [`assertion_attribute_name`]({{< relref ".././enterprise-configuration.md#assertion-attribute-name" >}}) - - [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) -1. Save the configuration file and and then restart the Grafana server. - -When you are finished, the Grafana configuration might look like this example: - -```bash -[server] -root_url = https://grafana.example.com - -[auth.saml] -enabled = true -private_key_path = "/path/to/private_key.pem" -certificate_path = "/path/to/certificate.cert" -idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" -assertion_attribute_name = DisplayName -assertion_attribute_login = Login -assertion_attribute_email = Email -assertion_attribute_groups = Group -``` +Once you have enabled saml, you can configure Grafana to use it for SAML authentication. Refer to [Configure SAML Authentication]({{< relref "./configure-saml.md#" >}}) to get more information about how to configure Grafana. diff --git a/docs/sources/enterprise/saml/set-up-saml-with-okta.md b/docs/sources/enterprise/saml/set-up-saml-with-okta.md index 11e2a727d11..9de28106e78 100644 --- a/docs/sources/enterprise/saml/set-up-saml-with-okta.md +++ b/docs/sources/enterprise/saml/set-up-saml-with-okta.md @@ -50,3 +50,33 @@ Grafana supports user authentication through Okta, which is useful when you want 1. Click **Next**. 1. On the final Feedback tab, fill out the form and then click **Finish**. + +**Edit SAML options for Okta in Grafana config file:** + +1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref ".././enterprise-configuration.md#enabled" >}}) to `true`. +1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). +1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. +1. Set the [`idp_metadata_url`]({{< relref ".././enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. +1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). + - [`assertion_attribute_login`]({{< relref ".././enterprise-configuration.md#assertion-attribute-login" >}}) + - [`assertion_attribute_email`]({{< relref ".././enterprise-configuration.md#assertion-attribute-email" >}}) + - [`assertion_attribute_name`]({{< relref ".././enterprise-configuration.md#assertion-attribute-name" >}}) + - [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) +1. Save the configuration file and and then restart the Grafana server. + +When you are finished, the Grafana configuration might look like this example: + +```bash +[server] +root_url = https://grafana.example.com + +[auth.saml] +enabled = true +private_key_path = "/path/to/private_key.pem" +certificate_path = "/path/to/certificate.cert" +idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" +assertion_attribute_name = DisplayName +assertion_attribute_login = Login +assertion_attribute_email = Email +assertion_attribute_groups = Group +``` From b54817033a4e0942f4c3fb82220da49612d8be1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 26 May 2022 11:26:20 +0200 Subject: [PATCH 015/283] loki: better unpack handling (#49074) --- public/app/plugins/datasource/loki/syntax.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/loki/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts index ad19acff4f7..e203e0b218f 100644 --- a/public/app/plugins/datasource/loki/syntax.ts +++ b/public/app/plugins/datasource/loki/syntax.ts @@ -72,6 +72,13 @@ export const PIPE_PARSERS: CompletionItem[] = [ insertText: 'pattern', documentation: 'Extracting labels from the log line using pattern parser. Only available in Loki 2.3+.', }, + { + label: 'unpack', + insertText: 'unpack', + detail: 'unpack identifier', + documentation: + 'Parses a JSON log line, unpacking all embedded labels in the pack stage. A special property "_entry" will also be used to replace the original log line. Only available in Loki 2.2+.', + }, ]; export const PIPE_OPERATORS: CompletionItem[] = [ @@ -82,13 +89,6 @@ export const PIPE_OPERATORS: CompletionItem[] = [ documentation: 'Take labels and use the values as sample data for metric aggregations. Only available in Loki 2.0+.', }, - { - label: 'unpack', - insertText: 'unpack', - detail: 'unpack identifier', - documentation: - 'Parses a JSON log line, unpacking all embedded labels in the pack stage. A special property "_entry" will also be used to replace the original log line. Only available in Loki 2.0+.', - }, { label: 'label_format', insertText: 'label_format', From 20a83ba14f6136bbb53d1cae2480eb95a1670022 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 26 May 2022 11:46:54 +0200 Subject: [PATCH 016/283] Narrow the alert condition picker (#49570) --- .../components/rule-editor/ConditionField.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx index a3eb1411c58..5ec13955b0e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx @@ -1,9 +1,10 @@ +import { css } from '@emotion/css'; import { last } from 'lodash'; import React, { FC, useEffect, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; -import { SelectableValue } from '@grafana/data'; -import { Alert, Card, Field, InputControl, RadioButtonList } from '@grafana/ui'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Alert, Card, Field, InputControl, RadioButtonList, useStyles2 } from '@grafana/ui'; import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; import { RuleFormValues } from '../../types/rule-form'; @@ -53,8 +54,10 @@ export const ConditionField: FC = () => { } }, [condition, expressions, options, setValue]); + const styles = useStyles2(getStyles); + return options.length ? ( - + Set alert condition Select one of your queries or expressions set above that contains your alert condition. @@ -75,8 +78,14 @@ export const ConditionField: FC = () => { ) : ( - + Create at least one query or expression to be alerted on ); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + max-width: ${theme.breakpoints.values.sm}px; + `, +}); From 78bef7a26a799209b5307d6bde8e25fcb4fbde7d Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 26 May 2022 11:49:18 +0200 Subject: [PATCH 017/283] Build: Enable long term caching for frontend assets (#47625) * build(webpack): move CopyUniconsPlugin into own file * chore(webpack): delete unused blobUrl and compile loaders * build(webpack): prefer contenthash over fullhash for longer caching * build(webpack): set optimization.moduleIds named only in dev * build(webpack): introduce HTMLWebpackCSSChunks so templates can access theme css by name * feat: inject css files with contenthash in html templates * revert(error-template): remove ContentDeliveryURL from CSS href * refactor(index-template): update grafanaBootData.themePaths * chore(webpack): add typescript annotations for CopyUniconsPlugin --- public/views/error-template.html | 6 +- public/views/index-template.html | 14 +-- scripts/webpack/loaders/blobUrl.js | 8 -- scripts/webpack/loaders/compile.js | 100 ------------------ scripts/webpack/plugins/CopyUniconsPlugin.js | 39 +++++++ .../webpack/plugins/HTMLWebpackCSSChunks.js | 42 ++++++++ scripts/webpack/webpack.common.js | 37 +------ scripts/webpack/webpack.dev.js | 7 +- scripts/webpack/webpack.hot.js | 5 +- scripts/webpack/webpack.prod.js | 4 +- 10 files changed, 108 insertions(+), 154 deletions(-) delete mode 100644 scripts/webpack/loaders/blobUrl.js delete mode 100644 scripts/webpack/loaders/compile.js create mode 100644 scripts/webpack/plugins/CopyUniconsPlugin.js create mode 100644 scripts/webpack/plugins/HTMLWebpackCSSChunks.js diff --git a/public/views/error-template.html b/public/views/error-template.html index 0f785120ce1..28e828a1554 100644 --- a/public/views/error-template.html +++ b/public/views/error-template.html @@ -10,7 +10,11 @@ - + [[ if eq .Theme "light" ]] + + [[ else ]] + + [[ end ]] diff --git a/public/views/index-template.html b/public/views/index-template.html index 2923b1918f9..955a1b4fc4e 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -20,10 +20,12 @@ - + + [[ if eq .Theme "light" ]] + + [[ else ]] + + [[ end ]]