From fd664e4beb3f024a2cf9965c484746c85c24cadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Wed, 8 Jun 2022 05:04:51 +0200 Subject: [PATCH 01/70] Alerting: replace a duplicated configuration key (#50350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR renames the configuration key enabled to capture. This is needed as we already have a configuration key with the name enabled. Fixes #50328 Co-authored-by: Jean-Philippe Quéméner --- conf/defaults.ini | 2 +- pkg/services/ngalert/image/service.go | 2 +- pkg/setting/setting_unified_alerting.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 370bf1ff46f..250a8396e1e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -844,7 +844,7 @@ min_interval = 10s [unified_alerting.screenshots] # Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please # see [rendering] for further configuration options. -enabled = +capture = false # The maximum number of screenshots that can be taken at the same time. This option is different from # concurrent_render_request_limit as max_concurrent_screenshots sets the number of concurrent screenshots diff --git a/pkg/services/ngalert/image/service.go b/pkg/services/ngalert/image/service.go index f19501475fc..fc873441a3b 100644 --- a/pkg/services/ngalert/image/service.go +++ b/pkg/services/ngalert/image/service.go @@ -55,7 +55,7 @@ func NewScreenshotImageService(screenshots screenshot.ScreenshotService, store s // from the configuration. func NewScreenshotImageServiceFromCfg(cfg *setting.Cfg, metrics prometheus.Registerer, db *store.DBstore, ds dashboards.DashboardService, rs rendering.Service) (ImageService, error) { - if !cfg.UnifiedAlerting.Screenshots.Enabled { + if !cfg.UnifiedAlerting.Screenshots.Capture { return &ScreenshotImageService{ screenshots: &screenshot.ScreenshotUnavailableService{}, }, nil diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 96bdf6bdc0c..e0bcc3da0e3 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -48,7 +48,7 @@ const ( schedulereDefaultExecuteAlerts = true schedulerDefaultMaxAttempts = 3 schedulerDefaultLegacyMinInterval = 1 - screenshotsDefaultEnabled = false + screenshotsDefaultCapture = false screenshotsDefaultMaxConcurrent = 5 screenshotsDefaultUploadImageStorage = false // SchedulerBaseInterval base interval of the scheduler. Controls how often the scheduler fetches database for new changes as well as schedules evaluation of a rule @@ -84,7 +84,7 @@ type UnifiedAlertingSettings struct { } type UnifiedAlertingScreenshotSettings struct { - Enabled bool + Capture bool MaxConcurrentScreenshots int64 UploadExternalImageStorage bool } @@ -260,7 +260,7 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { screenshots := iniFile.Section("unified_alerting.screenshots") uaCfgScreenshots := uaCfg.Screenshots - uaCfgScreenshots.Enabled = screenshots.Key("enabled").MustBool(screenshotsDefaultEnabled) + uaCfgScreenshots.Capture = screenshots.Key("capture").MustBool(screenshotsDefaultCapture) uaCfgScreenshots.MaxConcurrentScreenshots = screenshots.Key("max_concurrent_screenshots").MustInt64(screenshotsDefaultMaxConcurrent) uaCfgScreenshots.UploadExternalImageStorage = screenshots.Key("upload_external_image_storage").MustBool(screenshotsDefaultUploadImageStorage) uaCfg.Screenshots = uaCfgScreenshots From 50145e16177f05d0133d38a103be79de045dd8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 8 Jun 2022 08:14:34 +0200 Subject: [PATCH 02/70] Loki: remove frontend mode code (#50185) * feature-flag removed * loki: remove frontend-mode --- .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles_gen.go | 4 - .../loki/backendResultTransformer.test.ts | 6 +- .../datasource/loki/datasource.test.ts | 685 ++++++------------ .../app/plugins/datasource/loki/datasource.ts | 272 +------ .../loki/result_transformer.test.ts | 241 +----- .../datasource/loki/result_transformer.ts | 448 +----------- 8 files changed, 260 insertions(+), 1404 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7096557bf67..757bf20da8d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -28,7 +28,6 @@ export interface FeatureToggles { queryOverLive?: boolean; panelTitleSearch?: boolean; tempoServiceGraph?: boolean; - lokiBackendMode?: boolean; prometheus_azure_auth?: boolean; prometheusAzureOverrideAudience?: boolean; influxdbBackendMigration?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d1391222781..ad2fada0e58 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -72,13 +72,6 @@ var ( State: FeatureStateBeta, FrontendOnly: true, }, - { - Name: "lokiBackendMode", - Description: "Loki datasource works as backend datasource", - State: FeatureStateAlpha, - FrontendOnly: true, - Expression: "true", // Enabled by default - }, { Name: "prometheus_azure_auth", Description: "Experimental. Azure authentication for Prometheus datasource", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 61a6545d69b..d27b4f2256f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -55,10 +55,6 @@ const ( // show service FlagTempoServiceGraph = "tempoServiceGraph" - // FlagLokiBackendMode - // Loki datasource works as backend datasource - FlagLokiBackendMode = "lokiBackendMode" - // FlagPrometheusAzureAuth // Experimental. Azure authentication for Prometheus datasource FlagPrometheusAzureAuth = "prometheus_azure_auth" diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 050db46247d..c159f07374e 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -28,20 +28,20 @@ const inputFrame: DataFrame = { }, fields: [ { - name: 'time', + name: 'Time', type: FieldType.time, config: {}, values: new ArrayVector([1645030244810, 1645030247027]), }, { - name: 'value', + name: 'Line', type: FieldType.string, config: {}, values: new ArrayVector(['line1', 'line2']), }, { name: 'labels', - type: FieldType.string, + type: FieldType.other, config: {}, values: new ArrayVector([ { level: 'info', code: '41🌙' }, diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index ac3223f6e9c..4375dbb87f0 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1,37 +1,30 @@ -import { lastValueFrom, of, throwError } from 'rxjs'; +import { of } from 'rxjs'; import { take } from 'rxjs/operators'; -import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { getQueryOptions } from 'test/helpers/getQueryOptions'; import { AbstractLabelOperator, AnnotationQueryRequest, - CoreApp, + ArrayVector, DataFrame, + dataFrameToJSON, + DataQueryResponse, dateTime, - FieldCache, FieldType, LogRowModel, MutableDataFrame, toUtc, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse } from '@grafana/runtime'; -import { backendSrv } from 'app/core/services/backend_srv'; +import { BackendSrvRequest, FetchResponse, setBackendSrv, getBackendSrv, BackendSrv } from '@grafana/runtime'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { initialCustomVariableModelState } from '../../../features/variables/custom/reducer'; import { CustomVariableModel } from '../../../features/variables/types'; -import { isMetricsQuery, LokiDatasource, RangeQueryOptions } from './datasource'; +import { isMetricsQuery, LokiDatasource } from './datasource'; import { makeMockLokiDatasource } from './mocks'; -import { LokiQuery, LokiResponse, LokiResultType } from './types'; - -jest.mock('@grafana/runtime', () => ({ - // @ts-ignore - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); +import { LokiQuery } from './types'; const rawRange = { from: toUtc('2018-04-25 10:00'), @@ -51,18 +44,60 @@ const templateSrvStub = { replace: jest.fn((a: string, ...rest: any) => a), }; -const testLogsResponse: FetchResponse = { - data: { - data: { - resultType: LokiResultType.Stream, - result: [ +const testFrame: DataFrame = { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2]), + }, + { + name: 'Line', + type: FieldType.string, + config: {}, + values: new ArrayVector(['line1', 'line2']), + }, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: new ArrayVector([ + { + label: 'value', + label2: 'value ', + }, { - stream: {}, - values: [['1573646419522934000', 'hello']], + label: '', + label2: 'value2', + label3: ' ', }, - ], + ]), + }, + { + name: 'tsNs', + type: FieldType.string, + config: {}, + values: new ArrayVector(['1000000', '2000000']), + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: new ArrayVector(['id1', 'id2']), + }, + ], + length: 2, +}; + +const testLogsResponse: FetchResponse = { + data: { + results: { + A: { + frames: [dataFrameToJSON(testFrame)], + }, }, - status: 'success', }, ok: true, headers: {} as unknown as Headers, @@ -74,29 +109,6 @@ const testLogsResponse: FetchResponse = { config: {} as unknown as BackendSrvRequest, }; -const testMetricsResponse: FetchResponse = { - data: { - data: { - resultType: LokiResultType.Matrix, - result: [ - { - metric: {}, - values: [[1605715380, '1.1']], - }, - ], - }, - status: 'success', - }, - ok: true, - headers: {} as unknown as Headers, - redirected: false, - status: 200, - statusText: 'OK', - type: 'basic', - url: '', - config: {} as unknown as BackendSrvRequest, -}; - interface AdHocFilter { condition: string; key: string; @@ -105,113 +117,22 @@ interface AdHocFilter { } describe('LokiDatasource', () => { - const fetchMock = jest.spyOn(backendSrv, 'fetch'); + let origBackendSrv: BackendSrv; beforeEach(() => { - jest.clearAllMocks(); - fetchMock.mockImplementation(() => of(createFetchResponse({}))); + origBackendSrv = getBackendSrv(); }); - describe('when creating range query', () => { - let ds: LokiDatasource; - let adjustIntervalSpy: jest.SpyInstance; - - beforeEach(() => { - ds = createLokiDSForTests(); - adjustIntervalSpy = jest.spyOn(ds, 'adjustInterval'); - }); - - it('should use default intervalMs if one is not provided', () => { - const target = { expr: '{job="grafana"}', refId: 'B' }; - const raw = { from: 'now', to: 'now-1h' }; - const range = { from: dateTime(), to: dateTime(), raw: raw }; - const options = { - range, - }; - - const req = ds.createRangeQuery(target, options as any, 1000); - expect(req.start).toBeDefined(); - expect(req.end).toBeDefined(); - expect(adjustIntervalSpy).toHaveBeenCalledWith(1000, 1, expect.anything()); - }); - - it('should use provided intervalMs', () => { - const target = { expr: '{job="grafana"}', refId: 'B' }; - const raw = { from: 'now', to: 'now-1h' }; - const range = { from: dateTime(), to: dateTime(), raw: raw }; - const options = { - range, - intervalMs: 2000, - }; - - const req = ds.createRangeQuery(target, options as any, 1000); - expect(req.start).toBeDefined(); - expect(req.end).toBeDefined(); - expect(adjustIntervalSpy).toHaveBeenCalledWith(2000, 1, expect.anything()); - }); - - it('should set the minimal step to 1ms', () => { - const target = { expr: '{job="grafana"}', refId: 'B' }; - const raw = { from: 'now', to: 'now-1h' }; - const range = { from: dateTime('2020-10-14T00:00:00'), to: dateTime('2020-10-14T00:00:01'), raw: raw }; - const options = { - range, - intervalMs: 0.0005, - }; - - const req = ds.createRangeQuery(target, options as any, 1000); - expect(req.start).toBeDefined(); - expect(req.end).toBeDefined(); - expect(adjustIntervalSpy).toHaveBeenCalledWith(0.0005, expect.anything(), 1000); - // Step is in seconds (1 ms === 0.001 s) - expect(req.step).toEqual(0.001); - }); - - describe('log volume hint', () => { - let options: RangeQueryOptions; - - beforeEach(() => { - const raw = { from: 'now', to: 'now-1h' }; - const range = { from: dateTime(), to: dateTime(), raw: raw }; - options = { - range, - } as unknown as RangeQueryOptions; - }); - - it('should add volume hint param for log volume queries', () => { - const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: true }; - ds.runRangeQuery(target, options); - expect(backendSrv.fetch).toBeCalledWith( - expect.objectContaining({ - headers: { - 'X-Query-Tags': 'Source=logvolhist', - }, - }) - ); - }); - - it('should not add volume hint param for regular queries', () => { - const target = { expr: '{job="grafana"}', refId: 'B', volumeQuery: false }; - ds.runRangeQuery(target, options); - expect(backendSrv.fetch).not.toBeCalledWith( - expect.objectContaining({ - headers: { - 'X-Query-Tags': 'Source=logvolhist', - }, - }) - ); - }); - }); + afterEach(() => { + setBackendSrv(origBackendSrv); }); describe('when doing logs queries with limits', () => { - const runLimitTest = async ({ - maxDataPoints = 123, - queryMaxLines, - dsMaxLines = 456, - expectedLimit, - expr = '{label="val"}', - }: any) => { + const runTest = async ( + queryMaxLines: number | undefined, + dsMaxLines: number | undefined, + expectedMaxLines: number + ) => { let settings: any = { url: 'myloggingurl', jsonData: { @@ -226,264 +147,85 @@ describe('LokiDatasource', () => { const ds = new LokiDatasource(settings, templateSrvMock, timeSrvStub as any); - const options = getQueryOptions({ targets: [{ expr, refId: 'B', maxLines: queryMaxLines }] }); - options.maxDataPoints = maxDataPoints; + // we need to check the final query before it is sent out, + // and applyTemplateVariables is a convenient place to do that. + const spy = jest.spyOn(ds, 'applyTemplateVariables'); - fetchMock.mockImplementation(() => of(testLogsResponse)); + const options = getQueryOptions({ + targets: [{ expr: '{a="b"}', refId: 'B', maxLines: queryMaxLines }], + }); + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); await expect(ds.query(options).pipe(take(1))).toEmitValuesWith(() => { expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toContain(`limit=${expectedLimit}`); + expect(spy.mock.calls[0][0].maxLines).toBe(expectedMaxLines); }); }; - it('should use datasource max lines when no limit given and it is log query', async () => { - await runLimitTest({ expectedLimit: 456 }); + it('should use datasource max lines when no query max lines', async () => { + await runTest(undefined, 40, 40); }); - it('should use custom max lines from query if set and it is logs query', async () => { - await runLimitTest({ queryMaxLines: 20, expectedLimit: 20 }); + it('should use query max lines, if exists', async () => { + await runTest(80, undefined, 80); }); - it('should use custom max lines from query if set and it is logs query even if it is higher than data source limit', async () => { - await runLimitTest({ queryMaxLines: 500, expectedLimit: 500 }); - }); - - it('should use maxDataPoints if it is metrics query', async () => { - await runLimitTest({ expr: 'rate({label="val"}[10m])', expectedLimit: 123 }); - }); - - it('should use maxDataPoints if it is metrics query and using search', async () => { - await runLimitTest({ expr: 'rate({label="val"}[10m])', expectedLimit: 123 }); + it('should use query max lines, if both exist, even if it is higher than ds max lines', async () => { + await runTest(80, 40, 80); }); }); - describe('when querying', () => { - function setup(expr: string, app: CoreApp, instant?: boolean, range?: boolean) { - const ds = createLokiDSForTests(); - const options = getQueryOptions({ - targets: [{ expr, refId: 'B', instant, range }], - app, - }); - ds.runInstantQuery = jest.fn(() => of({ data: [] })); - ds.runRangeQuery = jest.fn(() => of({ data: [] })); - return { ds, options }; - } + describe('When using adhoc filters', () => { + const DEFAULT_EXPR = 'rate({bar="baz", job="foo"} |= "bar" [5m])'; + const query: LokiQuery = { expr: DEFAULT_EXPR, refId: 'A' }; + const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters(); + const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any); - const metricsQuery = 'rate({job="grafana"}[10m])'; - const logsQuery = '{job="grafana"} |= "foo"'; - - it('should run logs instant if only instant is selected', async () => { - const { ds, options } = setup(logsQuery, CoreApp.Explore, true, false); - await lastValueFrom(ds.query(options)); - expect(ds.runInstantQuery).toBeCalled(); - expect(ds.runRangeQuery).not.toBeCalled(); + afterAll(() => { + templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock); }); - it('should run metrics instant if only instant is selected', async () => { - const { ds, options } = setup(metricsQuery, CoreApp.Explore, true, false); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).toBeCalled(); - expect(ds.runRangeQuery).not.toBeCalled(); + it('should not modify expression with no filters', async () => { + expect(ds.applyTemplateVariables(query, {}).expr).toBe(DEFAULT_EXPR); }); - it('should run only logs range query if only range is selected', async () => { - const { ds, options } = setup(logsQuery, CoreApp.Explore, false, true); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should run only metrics range query if only range is selected', async () => { - const { ds, options } = setup(metricsQuery, CoreApp.Explore, false, true); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should run only logs range query if no query type is selected in Explore', async () => { - const { ds, options } = setup(logsQuery, CoreApp.Explore); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should run only metrics range query if no query type is selected in Explore', async () => { - const { ds, options } = setup(metricsQuery, CoreApp.Explore); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should run only logs range query in Dashboard', async () => { - const { ds, options } = setup(logsQuery, CoreApp.Dashboard); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should run only metrics range query in Dashboard', async () => { - const { ds, options } = setup(metricsQuery, CoreApp.Dashboard); - lastValueFrom(await ds.query(options)); - expect(ds.runInstantQuery).not.toBeCalled(); - expect(ds.runRangeQuery).toBeCalled(); - }); - - it('should return dataframe data for metrics range queries', async () => { - const ds = createLokiDSForTests(); - const options = getQueryOptions({ - targets: [{ expr: metricsQuery, refId: 'B', range: true }], - app: CoreApp.Explore, - }); - - fetchMock.mockImplementation(() => of(testMetricsResponse)); - - await expect(ds.query(options)).toEmitValuesWith((received) => { - const result = received[0]; - const frame = result.data[0] as DataFrame; - - expect(frame.meta?.preferredVisualisationType).toBe('graph'); - expect(frame.refId).toBe('B'); - frame.fields.forEach((field) => { - const value = field.values.get(0); - - if (field.type === FieldType.time) { - expect(value).toBe(1605715380000); - } else { - expect(value).toBe(1.1); - } - }); - }); - }); - - it('should return series data for logs range query', async () => { - const ds = createLokiDSForTests(); - const options = getQueryOptions({ - targets: [{ expr: logsQuery, refId: 'B' }], - }); - - fetchMock.mockImplementation(() => of(testLogsResponse)); - - await expect(ds.query(options)).toEmitValuesWith((received) => { - const result = received[0]; - const dataFrame = result.data[0] as DataFrame; - const fieldCache = new FieldCache(dataFrame); - - expect(fieldCache.getFieldByName('Line')?.values.get(0)).toBe('hello'); - expect(dataFrame.meta?.limit).toBe(20); - expect(dataFrame.meta?.searchWords).toEqual(['foo']); - }); - }); - - it('should return custom error message when Loki returns escaping error', async () => { - const ds = createLokiDSForTests(); - const options = getQueryOptions({ - targets: [{ expr: '{job="gra\\fana"}', refId: 'B' }], - }); - - fetchMock.mockImplementation(() => - throwError({ - data: { - message: 'parse error at line 1, col 6: invalid char escape', - }, - status: 400, - statusText: 'Bad Request', - }) - ); - - await expect(ds.query(options)).toEmitValuesWith((received) => { - const err: any = received[0]; - expect(err.data.message).toBe( - 'Error: parse error at line 1, col 6: invalid char escape. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/.' - ); - }); - }); - - describe('When using adhoc filters', () => { - const DEFAULT_EXPR = 'rate({bar="baz", job="foo"} |= "bar" [5m])'; - const options = { - targets: [{ expr: DEFAULT_EXPR }], - }; - const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters(); - const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any); - ds.runRangeQuery = jest.fn(() => of({ data: [] })); - - afterAll(() => { - templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock); - }); - - it('should not modify expression with no filters', async () => { - await lastValueFrom(ds.query(options as any)); - expect(ds.runRangeQuery).toBeCalledWith({ expr: DEFAULT_EXPR }, expect.anything()); - }); - - it('should add filters to expression', async () => { - templateSrvStub.getAdhocFilters.mockReturnValue([ - { - key: 'k1', - operator: '=', - value: 'v1', - }, - { - key: 'k2', - operator: '!=', - value: 'v2', - }, - ]); - - await lastValueFrom(ds.query(options as any)); - expect(ds.runRangeQuery).toBeCalledWith( - { expr: 'rate({bar="baz",job="foo",k1="v1",k2!="v2"} |= "bar" [5m])' }, - expect.anything() - ); - }); - - it('should add escaping if needed to regex filter expressions', async () => { - templateSrvStub.getAdhocFilters.mockReturnValue([ - { - key: 'k1', - operator: '=~', - value: 'v.*', - }, - { - key: 'k2', - operator: '=~', - value: `v'.*`, - }, - ]); - await lastValueFrom(ds.query(options as any)); - expect(ds.runRangeQuery).toBeCalledWith( - { expr: 'rate({bar="baz",job="foo",k1=~"v\\\\.\\\\*",k2=~"v\'\\\\.\\\\*"} |= "bar" [5m])' }, - expect.anything() - ); - }); - }); - - describe('__range, __range_s and __range_ms variables', () => { - const options = { - targets: [{ expr: 'rate(process_cpu_seconds_total[$__range])', refId: 'A', stepInterval: '2s' }], - range: { - from: rawRange.from, - to: rawRange.to, - raw: rawRange, + it('should add filters to expression', async () => { + templateSrvStub.getAdhocFilters.mockReturnValue([ + { + key: 'k1', + operator: '=', + value: 'v1', }, - }; + { + key: 'k2', + operator: '!=', + value: 'v2', + }, + ]); - const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any); + expect(ds.applyTemplateVariables(query, {}).expr).toBe( + 'rate({bar="baz",job="foo",k1="v1",k2!="v2"} |= "bar" [5m])' + ); + }); - beforeEach(() => { - templateSrvStub.replace.mockClear(); - }); - - it('should be correctly interpolated', () => { - ds.query(options as any); - const range = templateSrvStub.replace.mock.calls[0][1].__range; - const rangeMs = templateSrvStub.replace.mock.calls[0][1].__range_ms; - const rangeS = templateSrvStub.replace.mock.calls[0][1].__range_s; - expect(range).toEqual({ text: '3600s', value: '3600s' }); - expect(rangeMs).toEqual({ text: 3600000, value: 3600000 }); - expect(rangeS).toEqual({ text: 3600, value: 3600 }); - }); + it('should add escaping if needed to regex filter expressions', async () => { + templateSrvStub.getAdhocFilters.mockReturnValue([ + { + key: 'k1', + operator: '=~', + value: 'v.*', + }, + { + key: 'k2', + operator: '=~', + value: `v'.*`, + }, + ]); + expect(ds.applyTemplateVariables(query, {}).expr).toBe( + 'rate({bar="baz",job="foo",k1=~"v\\\\.\\\\*",k2=~"v\'\\\\.\\\\*"} |= "bar" [5m])' + ); }); }); @@ -589,45 +331,65 @@ describe('LokiDatasource', () => { }); describe('when calling annotationQuery', () => { - const getTestContext = (response: any, options: any = []) => { + const getTestContext = (frame: DataFrame, options: any = []) => { const query = makeAnnotationQueryRequest(options); - fetchMock.mockImplementation(() => of(response)); const ds = createLokiDSForTests(); - const promise = ds.annotationQuery(query); - - return { promise }; + const response: DataQueryResponse = { + data: [frame], + }; + ds.query = () => of(response); + return ds.annotationQuery(query); }; it('should transform the loki data to annotation response', async () => { - const response: FetchResponse = { - data: { - data: { - resultType: LokiResultType.Stream, - result: [ - { - stream: { - label: 'value', - label2: 'value ', - }, - values: [['1549016857498000000', 'hello']], - }, - { - stream: { - label: '', // empty value gets filtered - label2: 'value2', - label3: ' ', // whitespace value gets trimmed then filtered - }, - values: [['1549024057498000000', 'hello 2']], - }, - ], + const testFrame: DataFrame = { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2]), }, - status: 'success', - }, - } as unknown as FetchResponse; - const { promise } = getTestContext(response, { stepInterval: '15s' }); - - const res = await promise; + { + name: 'Line', + type: FieldType.string, + config: {}, + values: new ArrayVector(['hello', 'hello 2']), + }, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: new ArrayVector([ + { + label: 'value', + label2: 'value ', + }, + { + label: '', + label2: 'value2', + label3: ' ', + }, + ]), + }, + { + name: 'tsNs', + type: FieldType.string, + config: {}, + values: new ArrayVector(['1000000', '2000000']), + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: new ArrayVector(['id1', 'id2']), + }, + ], + length: 2, + }; + const res = await getTestContext(testFrame, { stepInterval: '15s' }); expect(res.length).toBe(2); expect(res[0].text).toBe('hello'); @@ -636,30 +398,53 @@ describe('LokiDatasource', () => { expect(res[1].text).toBe('hello 2'); expect(res[1].tags).toEqual(['value2']); }); + describe('Formatting', () => { - const response: FetchResponse = { - data: { - data: { - resultType: LokiResultType.Stream, - result: [ - { - stream: { - label: 'value', - label2: 'value2', - label3: 'value3', - }, - values: [['1549016857498000000', 'hello']], - }, - ], + const testFrame: DataFrame = { + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1]), }, - status: 'success', - }, - } as unknown as FetchResponse; + { + name: 'Line', + type: FieldType.string, + config: {}, + values: new ArrayVector(['hello']), + }, + { + name: 'labels', + type: FieldType.other, + config: {}, + values: new ArrayVector([ + { + label: 'value', + label2: 'value2', + label3: 'value3', + }, + ]), + }, + { + name: 'tsNs', + type: FieldType.string, + config: {}, + values: new ArrayVector(['1000000']), + }, + { + name: 'id', + type: FieldType.string, + config: {}, + values: new ArrayVector(['id1']), + }, + ], + length: 1, + }; describe('When tagKeys is set', () => { it('should only include selected labels', async () => { - const { promise } = getTestContext(response, { tagKeys: 'label2,label3', stepInterval: '15s' }); - - const res = await promise; + const res = await getTestContext(testFrame, { tagKeys: 'label2,label3', stepInterval: '15s' }); expect(res.length).toBe(1); expect(res[0].text).toBe('hello'); @@ -668,9 +453,7 @@ describe('LokiDatasource', () => { }); describe('When textFormat is set', () => { it('should fromat the text accordingly', async () => { - const { promise } = getTestContext(response, { textFormat: 'hello {{label2}}', stepInterval: '15s' }); - - const res = await promise; + const res = await getTestContext(testFrame, { textFormat: 'hello {{label2}}', stepInterval: '15s' }); expect(res.length).toBe(1); expect(res[0].text).toBe('hello value2'); @@ -678,9 +461,7 @@ describe('LokiDatasource', () => { }); describe('When titleFormat is set', () => { it('should fromat the title accordingly', async () => { - const { promise } = getTestContext(response, { titleFormat: 'Title {{label2}}', stepInterval: '15s' }); - - const res = await promise; + const res = await getTestContext(testFrame, { titleFormat: 'Title {{label2}}', stepInterval: '15s' }); expect(res.length).toBe(1); expect(res[0].title).toBe('Title value2'); @@ -920,26 +701,6 @@ describe('LokiDatasource', () => { }); }); - describe('adjustInterval', () => { - const dynamicInterval = 15; - const range = 1642; - const resolution = 1; - const ds = createLokiDSForTests(); - it('should return the interval as a factor of dynamicInterval and resolution', () => { - let interval = ds.adjustInterval(dynamicInterval, resolution, range); - expect(interval).toBe(resolution * dynamicInterval); - }); - it('should not return a value less than the safe interval', () => { - let safeInterval = range / 11000; - if (safeInterval > 1) { - safeInterval = Math.ceil(safeInterval); - } - const unsafeInterval = safeInterval - 0.01; - let interval = ds.adjustInterval(unsafeInterval, resolution, range); - expect(interval).toBeGreaterThanOrEqual(safeInterval); - }); - }); - describe('prepareLogRowContextQueryTarget', () => { const ds = createLokiDSForTests(); it('creates query with only labels from /labels API', () => { @@ -1002,7 +763,7 @@ describe('LokiDatasource', () => { describe('importing queries', () => { it('keeps all labels when no labels are loaded', async () => { const ds = createLokiDSForTests(); - fetchMock.mockImplementation(() => of(createFetchResponse({ data: [] }))); + ds.getResource = () => Promise.resolve({ data: [] }); const queries = await ds.importFromAbstractQueries([ { refId: 'A', @@ -1017,7 +778,7 @@ describe('LokiDatasource', () => { it('filters out non existing labels', async () => { const ds = createLokiDSForTests(); - fetchMock.mockImplementation(() => of(createFetchResponse({ data: ['foo'] }))); + ds.getResource = () => Promise.resolve({ data: ['foo'] }); const queries = await ds.importFromAbstractQueries([ { refId: 'A', diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index c3e9290a989..cd9a1a5d23d 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -1,5 +1,5 @@ // Libraries -import { cloneDeep, isEmpty, map as lodashMap } from 'lodash'; +import { cloneDeep, map as lodashMap } from 'lodash'; import Prism from 'prismjs'; import { lastValueFrom, merge, Observable, of, throwError } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; @@ -29,13 +29,12 @@ import { LoadingState, LogLevel, LogRowModel, - QueryResultMeta, ScopedVars, TimeRange, rangeUtil, toUtc, } from '@grafana/data'; -import { BackendSrvRequest, FetchError, getBackendSrv, config, DataSourceWithBackend } from '@grafana/runtime'; +import { FetchError, config, DataSourceWithBackend } from '@grafana/runtime'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { queryLogsVolume } from 'app/core/logs_model'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; @@ -48,38 +47,20 @@ import { renderLegendFormat } from '../prometheus/legend'; import { addLabelToQuery } from './add_label_to_query'; import { transformBackendResult } from './backendResultTransformer'; import { LokiAnnotationsQueryEditor } from './components/AnnotationsQueryEditor'; -import { DEFAULT_RESOLUTION } from './components/LokiOptionFields'; import LanguageProvider from './language_provider'; import { escapeLabelValueInSelector } from './language_utils'; import { LiveStreams, LokiLiveTarget } from './live_streams'; import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils'; -import { lokiResultsToTableModel, lokiStreamsToDataFrames, processRangeQueryResponse } from './result_transformer'; import { sortDataFrameByTime } from './sortDataFrame'; import { doLokiChannelStream } from './streaming'; import syntax from './syntax'; -import { - LokiOptions, - LokiQuery, - LokiQueryDirection, - LokiQueryType, - LokiRangeQueryRequest, - LokiResultType, - LokiStreamResponse, -} from './types'; +import { LokiOptions, LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; export type RangeQueryOptions = DataQueryRequest | AnnotationQueryRequest; export const DEFAULT_MAX_LINES = 1000; export const LOKI_ENDPOINT = '/loki/api/v1'; const NS_IN_MS = 1000000; -const RANGE_QUERY_ENDPOINT = `${LOKI_ENDPOINT}/query_range`; -const INSTANT_QUERY_ENDPOINT = `${LOKI_ENDPOINT}/query`; - -const DEFAULT_QUERY_PARAMS: Partial = { - limit: DEFAULT_MAX_LINES, - query: '', -}; - function makeRequest(query: LokiQuery, range: TimeRange, app: CoreApp, requestId: string): DataQueryRequest { const intervalInfo = rangeUtil.calculateInterval(range, 1); return { @@ -106,7 +87,6 @@ export class LokiDatasource private streams = new LiveStreams(); languageProvider: LanguageProvider; maxLines: number; - useBackendMode: boolean; constructor( private instanceSettings: DataSourceInstanceSettings, @@ -118,30 +98,11 @@ export class LokiDatasource this.languageProvider = new LanguageProvider(this); const settingsData = instanceSettings.jsonData || {}; this.maxLines = parseInt(settingsData.maxLines ?? '0', 10) || DEFAULT_MAX_LINES; - this.useBackendMode = config.featureToggles.lokiBackendMode ?? false; this.annotations = { QueryEditor: LokiAnnotationsQueryEditor, }; } - _request(apiUrl: string, data?: any, options?: Partial): Observable> { - const baseUrl = this.instanceSettings.url; - const params = data ? serializeParams(data) : ''; - const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`; - if (this.instanceSettings.withCredentials || this.instanceSettings.basicAuth) { - options = { ...options, withCredentials: true }; - if (this.instanceSettings.basicAuth) { - options.headers = { ...options.headers, Authorization: this.instanceSettings.basicAuth }; - } - } - const req = { - ...options, - url, - }; - - return getBackendSrv().fetch>(req); - } - getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { const isLogsVolumeAvailable = request.targets.some((target) => target.expr && !isMetricsQuery(target.expr)); if (!isLogsVolumeAvailable) { @@ -168,79 +129,37 @@ export class LokiDatasource } query(request: DataQueryRequest): Observable { - const subQueries: Array> = []; - const scopedVars = { - ...request.scopedVars, - ...this.getRangeScopedVars(request.range), + const queries = request.targets + .map(getNormalizedLokiQuery) // "fix" the `.queryType` prop + .map((q) => ({ ...q, maxLines: q.maxLines || this.maxLines })); // set maxLines if not set + + const fixedRequest = { + ...request, + targets: queries, }; - if (this.useBackendMode) { - const queries = request.targets - .map(getNormalizedLokiQuery) // "fix" the `.queryType` prop - .map((q) => ({ ...q, maxLines: q.maxLines || this.maxLines })); // set maxLines if not set - - const fixedRequest = { - ...request, - targets: queries, + const streamQueries = fixedRequest.targets.filter((q) => q.queryType === LokiQueryType.Stream); + if (config.featureToggles.lokiLive && streamQueries.length > 0 && fixedRequest.rangeRaw?.to === 'now') { + // this is still an in-development feature, + // we do not support mixing stream-queries with normal-queries for now. + const streamRequest = { + ...fixedRequest, + targets: streamQueries, }; - - const streamQueries = fixedRequest.targets.filter((q) => q.queryType === LokiQueryType.Stream); - if (config.featureToggles.lokiLive && streamQueries.length > 0 && fixedRequest.rangeRaw?.to === 'now') { - // this is still an in-development feature, - // we do not support mixing stream-queries with normal-queries for now. - const streamRequest = { - ...fixedRequest, - targets: streamQueries, - }; - return merge(...streamQueries.map((q) => doLokiChannelStream(q, this, streamRequest))); - } - - if (fixedRequest.liveStreaming) { - return this.runLiveQueryThroughBackend(fixedRequest); - } else { - return super - .query(fixedRequest) - .pipe( - map((response) => - transformBackendResult(response, fixedRequest.targets, this.instanceSettings.jsonData.derivedFields ?? []) - ) - ); - } + return merge(...streamQueries.map((q) => doLokiChannelStream(q, this, streamRequest))); } - const filteredTargets = request.targets - .filter((target) => target.expr && !target.hide) - .map((target) => { - const expr = this.addAdHocFilters(target.expr); - return { - ...target, - expr: this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr), - }; - }); - - for (const target of filteredTargets) { - if (target.instant || target.queryType === LokiQueryType.Instant) { - subQueries.push(this.runInstantQuery(target, request, filteredTargets.length)); - } else if ( - config.featureToggles.lokiLive && - target.queryType === LokiQueryType.Stream && - request.rangeRaw?.to === 'now' - ) { - subQueries.push(doLokiChannelStream(target, this, request)); - } else { - subQueries.push(this.runRangeQuery(target, request)); - } + if (fixedRequest.liveStreaming) { + return this.runLiveQueryThroughBackend(fixedRequest); + } else { + return super + .query(fixedRequest) + .pipe( + map((response) => + transformBackendResult(response, fixedRequest.targets, this.instanceSettings.jsonData.derivedFields ?? []) + ) + ); } - - // No valid targets, return the empty result to save a round trip. - if (isEmpty(subQueries)) { - return of({ - data: [], - state: LoadingState.Done, - }); - } - - return merge(...subQueries); } runLiveQueryThroughBackend(request: DataQueryRequest): Observable { @@ -264,119 +183,6 @@ export class LokiDatasource return merge(...subQueries); } - runInstantQuery = ( - target: LokiQuery, - options: DataQueryRequest, - responseListLength = 1 - ): Observable => { - const timeNs = this.getTime(options.range.to, true); - const queryLimit = isMetricsQuery(target.expr) ? options.maxDataPoints : target.maxLines; - const query = { - query: target.expr, - time: `${timeNs + (1e9 - (timeNs % 1e9))}`, - limit: Math.min(queryLimit || Infinity, this.maxLines), - direction: target.direction === LokiQueryDirection.Forward ? 'FORWARD' : 'BACKWARD', - }; - - /** Used only for results of metrics instant queries */ - const meta: QueryResultMeta = { - preferredVisualisationType: 'table', - }; - - return this._request(INSTANT_QUERY_ENDPOINT, query).pipe( - map((response) => { - if (response.data.data.resultType === LokiResultType.Stream) { - return { - data: response.data - ? lokiStreamsToDataFrames( - response.data as LokiStreamResponse, - target, - query.limit, - this.instanceSettings.jsonData - ) - : [], - key: `${target.refId}_instant`, - }; - } - - return { - data: [lokiResultsToTableModel(response.data.data.result, responseListLength, target.refId, meta)], - key: `${target.refId}_instant`, - }; - }), - catchError((err) => throwError(() => this.processError(err, target))) - ); - }; - - createRangeQuery(target: LokiQuery, options: RangeQueryOptions, limit: number): LokiRangeQueryRequest { - const query = target.expr; - let range: { start?: number; end?: number; step?: number } = {}; - if (options.range) { - const startNs = this.getTime(options.range.from, false); - const endNs = this.getTime(options.range.to, true); - const rangeMs = Math.ceil((endNs - startNs) / 1e6); - - const resolution = target.resolution || (DEFAULT_RESOLUTION.value as number); - - const adjustedInterval = - this.adjustInterval((options as DataQueryRequest).intervalMs || 1000, resolution, rangeMs) / 1000; - // We want to ceil to 3 decimal places - const step = Math.ceil(adjustedInterval * 1000) / 1000; - - range = { - start: startNs, - end: endNs, - step, - }; - } - - return { - ...DEFAULT_QUERY_PARAMS, - ...range, - query, - limit, - direction: target.direction === LokiQueryDirection.Forward ? 'FORWARD' : 'BACKWARD', - }; - } - - /** - * Attempts to send a query to /loki/api/v1/query_range - */ - runRangeQuery = (target: LokiQuery, options: RangeQueryOptions): Observable => { - // For metric query we use maxDataPoints from the request options which should be something like width of the - // visualisation in pixels. In case of logs request we either use lines limit defined in the query target or - // global limit defined for the data source which ever is lower. - let maxDataPoints = isMetricsQuery(target.expr) - ? // We fallback to maxLines here because maxDataPoints is defined as possibly undefined. Not sure that can - // actually happen both Dashboards and Explore should send some value here. If not maxLines does not make that - // much sense but nor any other arbitrary value. - (options as DataQueryRequest).maxDataPoints || this.maxLines - : // If user wants maxLines 0 we still fallback to data source limit. I think that makes sense as why would anyone - // want to do a query and not see any results? - target.maxLines || this.maxLines; - - if ((options as DataQueryRequest).liveStreaming) { - return this.runLiveQuery(target, maxDataPoints); - } - const query = this.createRangeQuery(target, options, maxDataPoints); - - const headers = target.volumeQuery ? { 'X-Query-Tags': 'Source=logvolhist' } : undefined; - - return this._request(RANGE_QUERY_ENDPOINT, query, { headers }).pipe( - catchError((err) => throwError(() => this.processError(err, target))), - switchMap((response) => - processRangeQueryResponse( - response.data, - target, - query, - maxDataPoints, - this.instanceSettings.jsonData, - (options as DataQueryRequest).scopedVars - ) - ) - ); - }; - createLiveTarget(target: LokiQuery, maxDataPoints: number): LokiLiveTarget { const query = target.expr; const baseUrl = this.instanceSettings.url; @@ -470,14 +276,8 @@ export class LokiDatasource throw new Error(`invalid metadata request url: ${url}`); } - if (this.useBackendMode) { - const res = await this.getResource(url, params); - return res.data || []; - } else { - const lokiURL = `${LOKI_ENDPOINT}/${url}`; - const res = await lastValueFrom(this._request(lokiURL, params, { hideFromInspector: true })); - return res.data.data || []; - } + const res = await this.getResource(url, params); + return res.data || []; } async metricFindQuery(query: string) { @@ -829,18 +629,6 @@ export class LokiDatasource return error; } - adjustInterval(dynamicInterval: number, resolution: number, range: number) { - // Loki will drop queries that might return more than 11000 data points. - // Calibrate interval if it is too small. - let safeInterval = range / 11000; - if (safeInterval > 1) { - safeInterval = Math.ceil(safeInterval); - } - - let adjustedInterval = Math.max(resolution * dynamicInterval, safeInterval); - return adjustedInterval; - } - addAdHocFilters(queryExpr: string) { const adhocFilters = this.templateSrv.getAdhocFilters(this.name); let expr = queryExpr; diff --git a/public/app/plugins/datasource/loki/result_transformer.test.ts b/public/app/plugins/datasource/loki/result_transformer.test.ts index 17c8ddcca94..63c9e090bf4 100644 --- a/public/app/plugins/datasource/loki/result_transformer.test.ts +++ b/public/app/plugins/datasource/loki/result_transformer.test.ts @@ -1,146 +1,9 @@ -import { CircularDataFrame, FieldCache, FieldType, MutableDataFrame } from '@grafana/data'; -import { setTemplateSrv } from '@grafana/runtime'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { CircularDataFrame, FieldType } from '@grafana/data'; import * as ResultTransformer from './result_transformer'; -import { - LokiStreamResult, - LokiTailResponse, - LokiStreamResponse, - LokiResultType, - TransformerOptions, - LokiMatrixResult, -} from './types'; - -const streamResult: LokiStreamResult[] = [ - { - stream: { - foo: 'bar', - }, - values: [['1579857562021616000', "foo: 'bar'"]], - }, - { - stream: { - bar: 'foo', - }, - values: [['1579857562031616000', "bar: 'foo'"]], - }, -]; - -const lokiResponse: LokiStreamResponse = { - status: 'success', - data: { - result: streamResult, - resultType: LokiResultType.Stream, - stats: { - summary: { - bytesTotal: 900, - }, - }, - }, -}; - -jest.mock('@grafana/runtime', () => ({ - // @ts-ignore - ...jest.requireActual('@grafana/runtime'), - getDataSourceSrv: () => { - return { - getInstanceSettings: () => { - return { name: 'Loki1' }; - }, - }; - }, -})); +import { LokiTailResponse } from './types'; describe('loki result transformer', () => { - beforeAll(() => { - setTemplateSrv(new TemplateSrv()); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('lokiStreamsToRawDataFrame', () => { - it('converts streams to series', () => { - const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult); - - expect(data.fields[0].values.get(0)).toStrictEqual({ foo: 'bar' }); - expect(data.fields[1].values.get(0)).toEqual('2020-01-24T09:19:22.021Z'); - expect(data.fields[2].values.get(0)).toEqual(streamResult[0].values[0][1]); - expect(data.fields[3].values.get(0)).toEqual(streamResult[0].values[0][0]); - expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc'); - expect(data.fields[0].values.get(1)).toStrictEqual({ bar: 'foo' }); - expect(data.fields[1].values.get(1)).toEqual('2020-01-24T09:19:22.031Z'); - expect(data.fields[2].values.get(1)).toEqual(streamResult[1].values[0][1]); - expect(data.fields[3].values.get(1)).toEqual(streamResult[1].values[0][0]); - expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1'); - }); - - it('should always generate unique ids for logs', () => { - const streamResultWithDuplicateLogs: LokiStreamResult[] = [ - { - stream: { - foo: 'bar', - }, - - values: [ - ['1579857562021616000', 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"'], - ['1579857562021616000', 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"'], - ['1579857562021616000', 't=2020-02-12T15:04:51+0000 lvl=info msg="Non-duplicated"'], - ['1579857562021616000', 't=2020-02-12T15:04:51+0000 lvl=info msg="Duplicated"'], - ], - }, - { - stream: { - bar: 'foo', - }, - values: [['1579857562021617000', 't=2020-02-12T15:04:51+0000 lvl=info msg="Non-dupliicated"']], - }, - ]; - - const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResultWithDuplicateLogs); - - expect(data.fields[4].values.get(0)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa'); - expect(data.fields[4].values.get(1)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_1'); - expect(data.fields[4].values.get(2)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); - expect(data.fields[4].values.get(3)).toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_2'); - expect(data.fields[4].values.get(4)).not.toEqual('b48fe7dc-36aa-5d37-bfba-087ef810d8fa_3'); - }); - - it('should append refId to the unique ids if refId is provided', () => { - const data = ResultTransformer.lokiStreamsToRawDataFrame(streamResult, 'B'); - expect(data.fields[4].values.get(0)).toEqual('4b79cb43-81ce-52f7-b1e9-a207fff144dc_B'); - expect(data.fields[4].values.get(1)).toEqual('73d144f6-57f2-5a45-a49c-eb998e2006b1_B'); - }); - }); - - describe('lokiStreamsToDataFrames', () => { - it('should enhance data frames', () => { - jest.spyOn(ResultTransformer, 'enhanceDataFrame'); - const dataFrames = ResultTransformer.lokiStreamsToDataFrames(lokiResponse, { refId: 'B', expr: '' }, 500, { - derivedFields: [ - { - matcherRegex: 'trace=(w+)', - name: 'test', - url: 'example.com', - }, - ], - }); - - expect(ResultTransformer.enhanceDataFrame).toBeCalled(); - dataFrames.forEach((frame) => { - expect( - frame.fields.filter((field) => field.name === 'test' && field.type === 'string').length - ).toBeGreaterThanOrEqual(1); - }); - }); - }); - describe('appendResponseToBufferedData', () => { it('should return a dataframe with ts in iso format', () => { const tailResponse: LokiTailResponse = { @@ -214,104 +77,4 @@ describe('loki result transformer', () => { expect(data.get(5).id).toEqual('3ca99d6b-3ab5-5970-93c0-eb3c9449088e_1_C'); }); }); - - describe('createMetricLabel', () => { - it('should create correct label based on passed variables', () => { - const label = ResultTransformer.createMetricLabel({}, { - scopedVars: { testLabel: { selected: true, text: 'label1', value: 'label1' } }, - legendFormat: '{{$testLabel}}', - } as unknown as TransformerOptions); - expect(label).toBe('label1'); - }); - }); - - describe('lokiResultsToTableModel', () => { - it('should correctly set the type of the label column to be a string', () => { - const lokiResultWithIntLabel = [ - { metric: { test: 1 }, value: [1610367143, 10] }, - { metric: { test: 2 }, value: [1610367144, 20] }, - ] as unknown as LokiMatrixResult[]; - - const table = ResultTransformer.lokiResultsToTableModel(lokiResultWithIntLabel, 1, 'A', {}); - expect(table.columns[0].type).toBe('time'); - expect(table.columns[1].type).toBe('string'); - expect(table.columns[2].type).toBe('number'); - }); - }); -}); - -describe('enhanceDataFrame', () => { - it('adds links to fields', () => { - const df = new MutableDataFrame({ fields: [{ name: 'Line', values: ['nothing', 'trace1=1234', 'trace2=foo'] }] }); - ResultTransformer.enhanceDataFrame(df, { - derivedFields: [ - { - matcherRegex: 'trace1=(\\w+)', - name: 'trace1', - url: 'http://localhost/${__value.raw}', - }, - { - matcherRegex: 'trace2=(\\w+)', - name: 'trace2', - url: 'test', - datasourceUid: 'uid', - }, - { - matcherRegex: 'trace2=(\\w+)', - name: 'trace2', - url: 'test', - datasourceUid: 'uid2', - urlDisplayLabel: 'Custom Label', - }, - ], - }); - expect(df.fields.length).toBe(3); - const fc = new FieldCache(df); - expect(fc.getFieldByName('trace1')!.values.toArray()).toEqual([null, '1234', null]); - expect(fc.getFieldByName('trace1')!.config.links![0]).toEqual({ - url: 'http://localhost/${__value.raw}', - title: '', - }); - - expect(fc.getFieldByName('trace2')!.values.toArray()).toEqual([null, null, 'foo']); - expect(fc.getFieldByName('trace2')!.config.links!.length).toBe(2); - expect(fc.getFieldByName('trace2')!.config.links![0]).toEqual({ - title: '', - internal: { datasourceName: 'Loki1', datasourceUid: 'uid', query: { query: 'test' } }, - url: '', - }); - expect(fc.getFieldByName('trace2')!.config.links![1]).toEqual({ - title: 'Custom Label', - internal: { datasourceName: 'Loki1', datasourceUid: 'uid2', query: { query: 'test' } }, - url: '', - }); - }); - - describe('lokiPointsToTimeseriesPoints()', () => { - /** - * NOTE on time parameters: - * - Input time series data has timestamps in sec (like Prometheus) - * - Output time series has timestamps in ms (as expected for the chart lib) - */ - const data: Array<[number, string]> = [ - [1, '1'], - [2, '0'], - [4, '1'], - [7, 'NaN'], - [8, '+Inf'], - [9, '-Inf'], - ]; - - it('returns data as is if step, start, and end align', () => { - const result = ResultTransformer.lokiPointsToTimeseriesPoints(data); - expect(result).toEqual([ - [1, 1000], - [0, 2000], - [1, 4000], - [NaN, 7000], - [Infinity, 8000], - [-Infinity, 9000], - ]); - }); - }); }); diff --git a/public/app/plugins/datasource/loki/result_transformer.ts b/public/app/plugins/datasource/loki/result_transformer.ts index ca976959809..b34f4a9ca88 100644 --- a/public/app/plugins/datasource/loki/result_transformer.ts +++ b/public/app/plugins/datasource/loki/result_transformer.ts @@ -1,108 +1,11 @@ -import { capitalize, groupBy, isEmpty } from 'lodash'; -import { of } from 'rxjs'; import { v5 as uuidv5 } from 'uuid'; -import { - FieldType, - TimeSeries, - Labels, - DataFrame, - ArrayVector, - MutableDataFrame, - findUniqueLabels, - DataFrameView, - DataLink, - Field, - QueryResultMetaStat, - QueryResultMeta, - TimeSeriesValue, - ScopedVars, - toDataFrame, -} from '@grafana/data'; -import { getTemplateSrv, getDataSourceSrv } from '@grafana/runtime'; -import TableModel from 'app/core/table_model'; +import { FieldType, Labels, MutableDataFrame, findUniqueLabels } from '@grafana/data'; -import { renderLegendFormat } from '../prometheus/legend'; - -import { formatQuery, getHighlighterExpressionsFromQuery } from './query_utils'; -import { dataFrameHasLokiError } from './responseUtils'; -import { - LokiRangeQueryRequest, - LokiResponse, - LokiMatrixResult, - LokiVectorResult, - TransformerOptions, - LokiResultType, - LokiStreamResult, - LokiTailResponse, - LokiQuery, - LokiOptions, - DerivedFieldConfig, - LokiStreamResponse, - LokiStats, -} from './types'; +import { LokiStreamResult, LokiTailResponse } from './types'; const UUID_NAMESPACE = '6ec946da-0f49-47a8-983a-1d76d17e7c92'; -/** - * Transforms LokiStreamResult structure into a dataFrame. Used when doing standard queries - */ -export function lokiStreamsToRawDataFrame(streams: LokiStreamResult[], refId?: string): DataFrame { - const labels = new ArrayVector<{}>([]); - const times = new ArrayVector([]); - const timesNs = new ArrayVector([]); - const lines = new ArrayVector([]); - const uids = new ArrayVector([]); - - // We need to store and track all used uids to ensure that uids are unique - const usedUids: { string?: number } = {}; - - for (const stream of streams) { - const streamLabels: Labels = stream.stream; - const labelsString = Object.entries(streamLabels) - .map(([key, val]) => `${key}="${val}"`) - .sort() - .join(''); - - for (const [ts, line] of stream.values) { - labels.add(streamLabels); - // num ns epoch in string, we convert it to iso string here so it matches old format - times.add(new Date(parseInt(ts.slice(0, -6), 10)).toISOString()); - timesNs.add(ts); - lines.add(line); - uids.add(createUid(ts, labelsString, line, usedUids, refId)); - } - } - - return constructDataFrame(times, timesNs, lines, uids, labels, refId); -} - -/** - * Constructs dataFrame with supplied fields and other data. - */ -function constructDataFrame( - times: ArrayVector, - timesNs: ArrayVector, - lines: ArrayVector, - uids: ArrayVector, - labels: ArrayVector<{}>, - refId?: string -) { - const dataFrame = { - refId, - fields: [ - { name: 'labels', type: FieldType.other, config: {}, values: labels }, - { name: 'Time', type: FieldType.time, config: {}, values: times }, // Time - { name: 'Line', type: FieldType.string, config: {}, values: lines }, // Line - needs to be the first field with string type - { name: 'tsNs', type: FieldType.time, config: {}, values: timesNs }, // Time - { name: 'id', type: FieldType.string, config: {}, values: uids }, - ], - length: times.length, - }; - - return dataFrame; -} - /** * Transform LokiResponse data and appends it to MutableDataFrame. Used for streaming where the dataFrame can be * a CircularDataFrame creating a fixed size rolling buffer. @@ -179,350 +82,3 @@ function createUid(ts: string, labelsString: string, line: string, usedUids: any } return id; } - -function lokiMatrixToTimeSeries(matrixResult: LokiMatrixResult, options: TransformerOptions): TimeSeries { - const name = createMetricLabel(matrixResult.metric, options); - return { - target: name, - title: name, - datapoints: lokiPointsToTimeseriesPoints(matrixResult.values), - tags: matrixResult.metric, - meta: options.meta, - refId: options.refId, - }; -} - -function parsePrometheusFormatSampleValue(value: string): number { - switch (value) { - case '+Inf': - return Number.POSITIVE_INFINITY; - case '-Inf': - return Number.NEGATIVE_INFINITY; - default: - return parseFloat(value); - } -} - -export function lokiPointsToTimeseriesPoints(data: Array<[number, string]>): TimeSeriesValue[][] { - const datapoints: TimeSeriesValue[][] = []; - - for (const [time, value] of data) { - let datapointValue: TimeSeriesValue = parsePrometheusFormatSampleValue(value); - - const timestamp = time * 1000; - - datapoints.push([datapointValue, timestamp]); - } - - return datapoints; -} - -export function lokiResultsToTableModel( - lokiResults: Array, - resultCount: number, - refId: string, - meta: QueryResultMeta -): TableModel { - if (!lokiResults || lokiResults.length === 0) { - return new TableModel(); - } - - // Collect all labels across all metrics - const metricLabels: Set = new Set( - lokiResults.reduce((acc, cur) => acc.concat(Object.keys(cur.metric)), [] as string[]) - ); - - // Sort metric labels, create columns for them and record their index - const sortedLabels = [...metricLabels.values()].sort(); - const table = new TableModel(); - table.refId = refId; - table.meta = meta; - table.columns = [ - { text: 'Time', type: FieldType.time }, - ...sortedLabels.map((label) => ({ text: label, filterable: true, type: FieldType.string })), - { text: `Value #${refId}`, type: FieldType.number }, - ]; - - // Populate rows, set value to empty string when label not present. - lokiResults.forEach((series) => { - const newSeries: LokiMatrixResult = { - metric: series.metric, - values: (series as LokiVectorResult).value - ? [(series as LokiVectorResult).value] - : (series as LokiMatrixResult).values, - }; - - if (!newSeries.values) { - return; - } - - if (!newSeries.metric) { - table.rows.concat(newSeries.values.map(([a, b]) => [a * 1000, parseFloat(b)])); - } else { - table.rows.push( - ...newSeries.values.map(([a, b]) => [ - a * 1000, - ...sortedLabels.map((label) => newSeries.metric[label] || ''), - parseFloat(b), - ]) - ); - } - }); - - return table; -} - -export function createMetricLabel(labelData: { [key: string]: string }, options?: TransformerOptions) { - let label = - options === undefined || isEmpty(options.legendFormat) - ? getOriginalMetricName(labelData) - : renderLegendFormat(getTemplateSrv().replace(options.legendFormat ?? '', options.scopedVars), labelData); - - if (!label && options) { - label = options.query; - } - return label; -} - -function getOriginalMetricName(labelData: { [key: string]: string }) { - const labelPart = Object.entries(labelData) - .map((label) => `${label[0]}="${label[1]}"`) - .join(','); - return `{${labelPart}}`; -} - -export function decamelize(s: string): string { - return s.replace(/[A-Z]/g, (m) => ` ${m.toLowerCase()}`); -} - -// Turn loki stats { metric: value } into meta stat { title: metric, value: value } -function lokiStatsToMetaStat(stats: LokiStats | undefined): QueryResultMetaStat[] { - const result: QueryResultMetaStat[] = []; - - if (!stats) { - return result; - } - - for (const section in stats) { - const values = stats[section]; - for (const label in values) { - const value = values[label]; - let unit; - if (/time/i.test(label) && value) { - unit = 's'; - } else if (/bytes.*persecond/i.test(label)) { - unit = 'Bps'; - } else if (/bytes/i.test(label)) { - unit = 'decbytes'; - } - const title = `${capitalize(section)}: ${decamelize(label)}`; - result.push({ displayName: title, value, unit }); - } - } - - return result; -} - -export function lokiStreamsToDataFrames( - response: LokiStreamResponse, - target: LokiQuery, - limit: number, - config: LokiOptions -): DataFrame[] { - const data = limit > 0 ? response.data.result : []; - const stats: QueryResultMetaStat[] = lokiStatsToMetaStat(response.data.stats); - // Use custom mechanism to identify which stat we want to promote to label - const custom = { - lokiQueryStatKey: 'Summary: total bytes processed', - // TODO: when we get a real frame-type in @grafana/data - // move this to frame.meta.type - frameType: 'LabeledTimeValues', - }; - - const meta: QueryResultMeta = { - searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.expr)), - limit, - stats, - custom, - preferredVisualisationType: 'logs', - }; - - const dataFrame = lokiStreamsToRawDataFrame(data, target.refId); - enhanceDataFrame(dataFrame, config); - - if (meta.custom && dataFrameHasLokiError(dataFrame)) { - meta.custom.error = 'Error when parsing some of the logs'; - } - - if (stats.length && !data.length) { - return [ - { - fields: [], - length: 0, - refId: target.refId, - meta, - }, - ]; - } - - return [ - { - ...dataFrame, - refId: target.refId, - meta, - }, - ]; -} - -/** - * Adds new fields and DataLinks to DataFrame based on DataSource instance config. - */ -export const enhanceDataFrame = (dataFrame: DataFrame, config: LokiOptions | null): void => { - if (!config) { - return; - } - - const derivedFields = config.derivedFields ?? []; - if (!derivedFields.length) { - return; - } - const derivedFieldsGrouped = groupBy(derivedFields, 'name'); - - const newFields = Object.values(derivedFieldsGrouped).map(fieldFromDerivedFieldConfig); - - const view = new DataFrameView(dataFrame); - view.forEach((row: { Line: string }) => { - for (const field of newFields) { - const logMatch = row.Line.match(derivedFieldsGrouped[field.name][0].matcherRegex); - field.values.add(logMatch && logMatch[1]); - } - }); - - dataFrame.fields = [...dataFrame.fields, ...newFields]; -}; - -/** - * Transform derivedField config into dataframe field with config that contains link. - */ -function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): Field { - const dataSourceSrv = getDataSourceSrv(); - - const dataLinks = derivedFieldConfigs.reduce((acc, derivedFieldConfig) => { - // Having field.datasourceUid means it is an internal link. - if (derivedFieldConfig.datasourceUid) { - const dsSettings = dataSourceSrv.getInstanceSettings(derivedFieldConfig.datasourceUid); - - acc.push({ - // Will be filled out later - title: derivedFieldConfig.urlDisplayLabel || '', - url: '', - // This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object - internal: { - query: { query: derivedFieldConfig.url }, - datasourceUid: derivedFieldConfig.datasourceUid, - datasourceName: dsSettings?.name ?? 'Data source not found', - }, - }); - } else if (derivedFieldConfig.url) { - acc.push({ - // We do not know what title to give here so we count on presentation layer to create a title from metadata. - title: derivedFieldConfig.urlDisplayLabel || '', - // This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object - url: derivedFieldConfig.url, - }); - } - return acc; - }, [] as DataLink[]); - - return { - name: derivedFieldConfigs[0].name, - type: FieldType.string, - config: { - links: dataLinks, - }, - // We are adding values later on - values: new ArrayVector([]), - }; -} - -function rangeQueryResponseToTimeSeries( - response: LokiResponse, - query: LokiRangeQueryRequest, - target: LokiQuery, - scopedVars: ScopedVars -): TimeSeries[] { - /** Show results of Loki metric queries only in graph */ - const meta: QueryResultMeta = { - preferredVisualisationType: 'graph', - }; - const transformerOptions: TransformerOptions = { - legendFormat: target.legendFormat ?? '', - query: query.query, - refId: target.refId, - meta, - scopedVars, - }; - - switch (response.data.resultType) { - case LokiResultType.Vector: - return response.data.result.map((vecResult) => - lokiMatrixToTimeSeries({ metric: vecResult.metric, values: [vecResult.value] }, transformerOptions) - ); - case LokiResultType.Matrix: - return response.data.result.map((matrixResult) => lokiMatrixToTimeSeries(matrixResult, transformerOptions)); - default: - return []; - } -} - -export function rangeQueryResponseToDataFrames( - response: LokiResponse, - query: LokiRangeQueryRequest, - target: LokiQuery, - scopedVars: ScopedVars -): DataFrame[] { - const series = rangeQueryResponseToTimeSeries(response, query, target, scopedVars); - const frames = series.map((s) => toDataFrame(s)); - - const { step } = query; - - if (step != null) { - const intervalMs = step * 1000; - - frames.forEach((frame) => { - frame.fields.forEach((field) => { - if (field.type === FieldType.time) { - field.config.interval = intervalMs; - } - }); - }); - } - - return frames; -} - -export function processRangeQueryResponse( - response: LokiResponse, - target: LokiQuery, - query: LokiRangeQueryRequest, - limit: number, - config: LokiOptions, - scopedVars: ScopedVars -) { - switch (response.data.resultType) { - case LokiResultType.Stream: - return of({ - data: lokiStreamsToDataFrames(response as LokiStreamResponse, target, limit, config), - key: `${target.refId}_log`, - }); - - case LokiResultType.Vector: - case LokiResultType.Matrix: - return of({ - data: rangeQueryResponseToDataFrames(response, query, target, scopedVars), - key: target.refId, - }); - default: - throw new Error(`Unknown result type "${(response.data as any).resultType}".`); - } -} From 8bd56b82263e2a55446517c3cf31075ff7b80b54 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Wed, 8 Jun 2022 01:03:58 -0700 Subject: [PATCH 03/70] Chore: convert Cloud Monitoring Aggregation enzyme tests to RTL (#50326) --- .betterer.results | 3 --- .../components/Aggregation.test.tsx | 27 +++++++------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/.betterer.results b/.betterer.results index 4146be12bc7..d9734967ab5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -158,9 +158,6 @@ exports[`no enzyme tests`] = { "public/app/features/users/UsersListPage.test.tsx:2518052139": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx:3822406835": [ - [1, 19, 13, "RegExp match", "2409514259"] - ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:227258837": [ [0, 19, 13, "RegExp match", "2409514259"] ], diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx index 02fd2285a08..10a8ca40bca 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Aggregation.test.tsx @@ -1,10 +1,8 @@ import { render, screen } from '@testing-library/react'; -import { shallow } from 'enzyme'; import React from 'react'; +import { openMenu } from 'react-select-event'; import { TemplateSrvStub } from 'test/specs/helpers'; -import { Select } from '@grafana/ui'; - import { ValueTypes, MetricKind } from '../types'; import { Aggregation, Props } from './Aggregation'; @@ -39,14 +37,11 @@ describe('Aggregation', () => { }; it('should not have the reduce values', () => { - const wrapper = shallow(); - const { options } = wrapper.find(Select).props() as any; - const [, aggGroup] = options; - - expect(aggGroup.options.length).toEqual(11); - expect(aggGroup.options.map((o: any) => o.value)).toEqual( - expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE']) - ); + render(); + const label = screen.getByLabelText('Group by function'); + openMenu(label); + expect(screen.queryByText('count true')).not.toBeInTheDocument(); + expect(screen.queryByText('count false')).not.toBeInTheDocument(); }); }); @@ -60,12 +55,10 @@ describe('Aggregation', () => { }; it('should have the reduce values', () => { - const wrapper = shallow(); - const { options } = wrapper.find(Select).props() as any; - const [, aggGroup] = options; - - expect(aggGroup.options.length).toEqual(11); - expect(aggGroup.options.map((o: any) => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE'])); + render(); + const label = screen.getByLabelText('Group by function'); + openMenu(label); + expect(screen.getByText('none')).toBeInTheDocument(); }); }); }); From fd408652dc653a6019fda08b5f55de7c4ef60522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 8 Jun 2022 10:34:44 +0200 Subject: [PATCH 04/70] Dashboard: Fixes random scrolling on time range change (#50379) * Dashboard: Fixes random scrolling on time range change * fix ts issue * removed logging --- .../CustomScrollbar/CustomScrollbar.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index f860dccebd0..9fb2443c081 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; -import { isNil } from 'lodash'; import React, { FC, RefCallback, useCallback, useEffect, useRef } from 'react'; import Scrollbars, { positionValues } from 'react-custom-scrollbars-2'; @@ -46,28 +45,24 @@ export const CustomScrollbar: FC = ({ children, }) => { const ref = useRef(null); - useEffect(() => { - if (ref.current) { - scrollRefCallback?.(ref.current.view); - } - }, [ref, scrollRefCallback]); const styles = useStyles2(getStyles); - const updateScroll = () => { - if (ref.current && !isNil(scrollTop)) { - ref.current.scrollTop(scrollTop); + useEffect(() => { + if (ref.current && scrollRefCallback) { + scrollRefCallback(ref.current.view); } - }; + }, [ref, scrollRefCallback]); useEffect(() => { - updateScroll(); - }); + if (ref.current && scrollTop != null) { + ref.current.scrollTop(scrollTop); + } + }, [scrollTop]); /** * Special logic for doing a update a few milliseconds after mount to check for * updated height due to dynamic content */ - useEffect(() => { if (!updateAfterMountMs) { return; From 288eede638aa4994399e23e0dad9e29ce1ffaf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 8 Jun 2022 10:34:58 +0200 Subject: [PATCH 05/70] Angular: Adds back two angular directives that are still used by remaining angular bits and plugins (#50380) --- public/app/angular/bsTooltip.ts | 59 +++++ public/app/angular/bsTypeahead.ts | 63 +++++ public/app/angular/index.ts | 2 + .../datasource/mssql/partials/config.html | 224 +++++++++--------- 4 files changed, 237 insertions(+), 111 deletions(-) create mode 100644 public/app/angular/bsTooltip.ts create mode 100644 public/app/angular/bsTypeahead.ts diff --git a/public/app/angular/bsTooltip.ts b/public/app/angular/bsTooltip.ts new file mode 100644 index 00000000000..38bb09b769c --- /dev/null +++ b/public/app/angular/bsTooltip.ts @@ -0,0 +1,59 @@ +import angular from 'angular'; +import $ from 'jquery'; + +import coreModule from './core_module'; + +coreModule.directive('bsTooltip', [ + '$parse', + '$compile', + function ($parse: any, $compile: any) { + return { + restrict: 'A', + scope: true, + link: function postLink(scope: any, element: any, attrs: any) { + var getter = $parse(attrs.bsTooltip), + value = getter(scope); + scope.$watch(attrs.bsTooltip, function (newValue: any, oldValue: any) { + if (newValue !== oldValue) { + value = newValue; + } + }); + // Grafana change, always hide other tooltips + if (true) { + element.on('show', function (ev: any) { + $('.tooltip.in').each(function () { + var $this = $(this), + tooltip = $this.data('tooltip'); + if (tooltip && !tooltip.$element.is(element)) { + $this.tooltip('hide'); + } + }); + }); + } + element.tooltip({ + title: function () { + return angular.isFunction(value) ? value.apply(null, arguments) : value; + }, + html: true, + container: 'body', // Grafana change + }); + var tooltip = element.data('tooltip'); + tooltip.show = function () { + var r = $.fn.tooltip.Constructor.prototype.show.apply(this, arguments); + this.tip().data('tooltip', this); + return r; + }; + scope._tooltip = function (event: any) { + element.tooltip(event); + }; + scope.hide = function () { + element.tooltip('hide'); + }; + scope.show = function () { + element.tooltip('show'); + }; + scope.dismiss = scope.hide; + }, + }; + }, +]); diff --git a/public/app/angular/bsTypeahead.ts b/public/app/angular/bsTypeahead.ts new file mode 100644 index 00000000000..515f82994ba --- /dev/null +++ b/public/app/angular/bsTypeahead.ts @@ -0,0 +1,63 @@ +import angular from 'angular'; +import $ from 'jquery'; +import { isFunction } from 'lodash'; + +import coreModule from './core_module'; + +coreModule.directive('bsTypeahead', [ + '$parse', + function ($parse: any) { + return { + restrict: 'A', + require: '?ngModel', + link: function postLink(scope: any, element: any, attrs: any, controller: any) { + var getter = $parse(attrs.bsTypeahead), + value = getter(scope); + scope.$watch(attrs.bsTypeahead, function (newValue: any, oldValue: any) { + if (newValue !== oldValue) { + value = newValue; + } + }); + element.attr('data-provide', 'typeahead'); + element.typeahead({ + source: function () { + return angular.isFunction(value) ? value.apply(null, arguments) : value; + }, + minLength: attrs.minLength || 1, + items: attrs.item, + updater: function (value: any) { + if (controller) { + scope.$apply(function () { + controller.$setViewValue(value); + }); + } + scope.$emit('typeahead-updated', value); + return value; + }, + }); + var typeahead = element.dat('typeahead'); + typeahead.lookup = function (ev: any) { + var items; + this.query = this.$element.val() || ''; + if (this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this; + } + items = isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source; + return items ? this.process(items) : this; + }; + if (!!attrs.matchAll) { + typeahead.matcher = function () { + return true; + }; + } + if (attrs.minLength === '0') { + setTimeout(function () { + element.on('focus', function () { + element.val().length === 0 && setTimeout(element.typeahead.bind(element, 'lookup'), 200); + }); + }); + } + }, + }; + }, +]); diff --git a/public/app/angular/index.ts b/public/app/angular/index.ts index ada6fe8f6a5..3fbf6ae93f2 100644 --- a/public/app/angular/index.ts +++ b/public/app/angular/index.ts @@ -14,6 +14,8 @@ import './dropdown_typeahead'; import './autofill_event_fix'; import './metric_segment'; import './misc'; +import './bsTooltip'; +import './bsTypeahead'; import './ng_model_on_blur'; import './tags'; import './rebuild_on_change'; diff --git a/public/app/plugins/datasource/mssql/partials/config.html b/public/app/plugins/datasource/mssql/partials/config.html index 6d3cb01e3d5..680ba4dd75b 100644 --- a/public/app/plugins/datasource/mssql/partials/config.html +++ b/public/app/plugins/datasource/mssql/partials/config.html @@ -1,46 +1,45 @@ -

MS SQL connection

-
- Host - -
+
+ Host + +
-
- Database - -
+
+ Database + +
-
- -
- - -
    -
  • SQL Server Authentication This is the default mechanism to connect to MS SQL Server. Enter the SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
  • -
  • Windows Authentication Windows Integrated Security - single sign on for users who are already logged onto Windows and have enabled this option for MS SQL Server.
  • -
-
-
-
-
-
- User - -
-
- -
-
+
+ +
+ + +
    +
  • SQL Server Authentication This is the default mechanism to connect to MS SQL Server. Enter the SQL + Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
  • +
  • Windows Authentication Windows Integrated Security - single sign on for users who are already + logged onto Windows and have enabled this option for MS SQL Server.
  • +
+
+
+
+
+
+ User + +
+
+ +
+
@@ -48,33 +47,37 @@
-
- -
- - - Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. -
    -
  • disable - Data sent between client and server is not encrypted.
  • -
  • false - Data sent between client and server is not encrypted beyond the login packet. (default)
  • -
  • true - Data sent between client and server is encrypted.
  • -
- If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect. -
-
-
+
+ +
+ + + Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. +
    +
  • disable - Data sent between client and server is not encrypted.
  • +
  • false - Data sent between client and server is not encrypted beyond the login packet. (default) +
  • +
  • true - Data sent between client and server is encrypted.
  • +
+ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption + to be able to connect. +
+
+
+ tooltip="Skip verifying Server Certificate for TLS/SSL. If this is enabled, any certificate presented by the server and any host name in that certificate will be accepted. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing." + checked="ctrl.current.jsonData.tlsSkipVerify" switch-class="max-width-8" on-change="ctrl.onEncryptChange()"> +
TLS/SSL Root Certificate + placeholder="TLS/SSL root certificate file"> Path to file containing the public key certificate of the CA that signed the SQL Server certificate. Needed when the server certificate is self signed. @@ -84,7 +87,7 @@
Hostname in server certificate + placeholder="Common Name (CN) in server certificate"> Specifies the Common Name (CN) in the server certificate. Default is the server host. @@ -95,65 +98,64 @@

Connection limits

-
- Max open - - - The maximum number of open connections to the database. If Max idle connections is greater than 0 and the - Max open connections is less than Max idle connections, then Max idle connections will be - reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open - connections. - -
-
- Max idle - - - The maximum number of connections in the idle connection pool. If Max open connections is greater than 0 but - less than the Max idle connections, then the Max idle connections will be reduced to match the - Max open connections limit. If set to 0, no idle connections are retained. - -
-
- Max lifetime - - - The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. - -
+
+ Max open + + + The maximum number of open connections to the database. If Max idle connections is greater than 0 and the + Max open connections is less than Max idle connections, then Max idle connections will be + reduced to match the Max open connections limit. If set to 0, there is no limit on the number of open + connections. + +
+
+ Max idle + + + The maximum number of connections in the idle connection pool. If Max open connections is greater than 0 + but + less than the Max idle connections, then the Max idle connections will be reduced to match the + Max open connections limit. If set to 0, no idle connections are retained. + +
+
+ Max lifetime + + + The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever. + +

MS SQL details

-
-
- Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
-
+
+
+ Min time interval + + + A lower limit for the auto group by time interval. Recommended to be set to write frequency, + for example 1m if your data is written every minute. + +
+
-
-
User Permission
-

- The database user should only be granted SELECT permissions on the specified database and tables you want to query. - Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements - like USE otherdb; and DROP TABLE user; would be executed. To protect against this we - highly recommmend you create a specific MS SQL user with restricted permissions. -

-
+
+
User Permission
+

+ The database user should only be granted SELECT permissions on the specified database and tables you want to + query. + Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, statements + like USE otherdb; and DROP TABLE user; would be executed. To protect against this we + highly recommmend you create a specific MS SQL user with restricted permissions. +

+
- From 689f349cda7962e8100a2979b3823c600a27cd76 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 8 Jun 2022 10:42:43 +0200 Subject: [PATCH 06/70] Docs: add note on alerting limitation for being an alert receiver (#50331) Co-authored-by: gotjosh Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- docs/sources/alerting/alerting-limitations.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/alerting-limitations.md b/docs/sources/alerting/alerting-limitations.md index 1efc1fffeea..58b9a955f95 100644 --- a/docs/sources/alerting/alerting-limitations.md +++ b/docs/sources/alerting/alerting-limitations.md @@ -7,5 +7,22 @@ weight: 552 # Limitations -- Grafana alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch alerting rules from all other supported data sources at this time. -- We aim to support the latest two minor versions of both Prometheus and Alertmanager. We cannot guarantee that older versions will work. As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`. +## Limited rule sources support + +Grafana Alerting can retrieve alerting and recording rules **stored** in most available Prometheus, Loki, Mimir, and Alertmanager compatible data sources. + +It does not support reading or writing alerting rules from any other data sources but the ones previously mentioned at this time. + +## Prometheus version support + +We support the latest two minor versions of both Prometheus and Alertmanager. We cannot guarantee that older versions will work. + +As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`. + +## Grafana is not an alert receiver + +Grafana is not an alert receiver; is it an alert generator. This means that Grafana cannot receive alerts from anything other than its internal alert generator. + +Receiving alerts from Prometheus (or anything else) is not supported at the time. + +For more information, refer to [this GitHub discussion](https://github.com/grafana/grafana/discussions/45773). From 322a705fc33befd99301840c89742a5c5abf5aaa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:32:18 +0100 Subject: [PATCH 07/70] Update dependency @cypress/webpack-preprocessor to v5.12.0 (#50320) Co-authored-by: Renovate Bot --- packages/grafana-e2e/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index e4cae600a77..b40a9faa5f0 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -47,7 +47,7 @@ "dependencies": { "@babel/core": "7.18.2", "@babel/preset-env": "7.18.2", - "@cypress/webpack-preprocessor": "5.11.1", + "@cypress/webpack-preprocessor": "5.12.0", "@grafana/e2e-selectors": "9.0.0-pre", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 117bf2671a4..41a0f816943 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3745,9 +3745,9 @@ __metadata: languageName: node linkType: hard -"@cypress/webpack-preprocessor@npm:5.11.1": - version: 5.11.1 - resolution: "@cypress/webpack-preprocessor@npm:5.11.1" +"@cypress/webpack-preprocessor@npm:5.12.0": + version: 5.12.0 + resolution: "@cypress/webpack-preprocessor@npm:5.12.0" dependencies: bluebird: 3.7.1 debug: ^4.3.2 @@ -3757,7 +3757,7 @@ __metadata: "@babel/preset-env": ^7.0.0 babel-loader: ^8.0.2 webpack: ^4 || ^5 - checksum: 48f0c083b491db88920f3bb05089d2de5f079f3eb33b579d77ce60a804ae9e02eb9170d677f4632eabb1abb48dad50978ca918ea537d68b27733d96b9765b9a6 + checksum: 3fa209655412369eb5bf0e8e0eaf25c89f0dfc6937d9aa4c51aa351449cf3d56b1834908dc81e507eb3d6508fee8534769b576f46f26034220a9f3f82ed7b3f8 languageName: node linkType: hard @@ -4391,7 +4391,7 @@ __metadata: dependencies: "@babel/core": 7.18.2 "@babel/preset-env": 7.18.2 - "@cypress/webpack-preprocessor": 5.11.1 + "@cypress/webpack-preprocessor": 5.12.0 "@grafana/e2e-selectors": 9.0.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 From 6e3d58d7796bf4282e69bbdeb7656434bd7e4380 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Wed, 8 Jun 2022 10:39:21 +0100 Subject: [PATCH 08/70] Improve docs for values in custom annotations and labels (#50346) --- .../annotation-label/variables-label-annotation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md b/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md index ad8895e4de7..8bfa09c6362 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md +++ b/docs/sources/alerting/fundamentals/annotation-label/variables-label-annotation.md @@ -15,8 +15,8 @@ weight: 117 The following template variables are available when expanding annotations and labels. -| Name | Description | -| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "../../alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). | -| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. If the rule uses classic conditions, then a combination of the `refID` and position of the condition is used. For example, `{{ $values.A0.Value }}` or `{{ $values.A1.Value }}` | -| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. | +| Name | Description | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "../../alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). | +| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. If the rule uses classic conditions, then a combination of the `refID` and position of the condition is used. For example, `{{ $values.A0.Value }}` or `{{ $values.A1.Value }}`. If a value can return no data then it is recommended to use either `{{ $values.A }}` where the missing value will show `` or an if statement to check if `A` exists when using `{{ $values.A.Labels }}` and `{{ $values.A.Value }}`. For example, `{{ if $values.A }}{{ $values.A.Value }}{{ end }}`. | +| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. | From a1cf709ad2dd517b56d62cba23f48c13297093db Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 8 Jun 2022 10:39:36 +0100 Subject: [PATCH 09/70] Playlists: Save button now correctly creates a new playlist (#50381) * Add type="submit" to PlaylistForm submit button * fix linting errors --- public/app/features/playlist/PlaylistForm.test.tsx | 14 +++++++------- public/app/features/playlist/PlaylistForm.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index d257436645d..eb699483a47 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -1,5 +1,5 @@ import { within } from '@testing-library/dom'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -133,8 +133,8 @@ describe('PlaylistForm', () => { it('then the correct item should be submitted', async () => { const { onSubmitMock } = getTestContext(playlist); - fireEvent.submit(screen.getByRole('button', { name: /save/i })); - await waitFor(() => expect(onSubmitMock).toHaveBeenCalledTimes(1)); + await userEvent.click(screen.getByRole('button', { name: /save/i })); + expect(onSubmitMock).toHaveBeenCalledTimes(1); expect(onSubmitMock).toHaveBeenCalledWith(playlist); }); @@ -142,8 +142,8 @@ describe('PlaylistForm', () => { it('then an alert should appear and nothing should be submitted', async () => { const { onSubmitMock } = getTestContext({ ...playlist, name: undefined }); - fireEvent.submit(screen.getByRole('button', { name: /save/i })); - expect(await screen.findAllByRole('alert')).toHaveLength(1); + await userEvent.click(screen.getByRole('button', { name: /save/i })); + expect(screen.getAllByRole('alert')).toHaveLength(1); expect(onSubmitMock).not.toHaveBeenCalled(); }); }); @@ -153,8 +153,8 @@ describe('PlaylistForm', () => { const { onSubmitMock } = getTestContext(playlist); await userEvent.clear(screen.getByRole('textbox', { name: /playlist interval/i })); - fireEvent.submit(screen.getByRole('button', { name: /save/i })); - expect(await screen.findAllByRole('alert')).toHaveLength(1); + await userEvent.click(screen.getByRole('button', { name: /save/i })); + expect(screen.getAllByRole('alert')).toHaveLength(1); expect(onSubmitMock).not.toHaveBeenCalled(); }); }); diff --git a/public/app/features/playlist/PlaylistForm.tsx b/public/app/features/playlist/PlaylistForm.tsx index b648b151330..5b9293dc6e6 100644 --- a/public/app/features/playlist/PlaylistForm.tsx +++ b/public/app/features/playlist/PlaylistForm.tsx @@ -70,7 +70,7 @@ export const PlaylistForm: FC = ({ onSubmit, playlist }) => {
- From aec020b67e5656be054ce16a99462e9937800e60 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:44:28 +0100 Subject: [PATCH 10/70] Update dependency @microsoft/api-extractor to v7.25.0 (#50334) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a86a1c101b0..792a190ebf7 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@lingui/cli": "3.13.3", "@lingui/macro": "3.13.3", - "@microsoft/api-extractor": "7.24.1", + "@microsoft/api-extractor": "7.25.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.6", "@react-types/button": "3.4.5", "@react-types/menu": "3.5.3", diff --git a/yarn.lock b/yarn.lock index 41a0f816943..434f9311c50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6596,11 +6596,11 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor@npm:7.24.1": - version: 7.24.1 - resolution: "@microsoft/api-extractor@npm:7.24.1" +"@microsoft/api-extractor@npm:7.25.0": + version: 7.25.0 + resolution: "@microsoft/api-extractor@npm:7.25.0" dependencies: - "@microsoft/api-extractor-model": 7.17.3 + "@microsoft/api-extractor-model": 7.18.0 "@microsoft/tsdoc": 0.14.1 "@microsoft/tsdoc-config": ~0.16.1 "@rushstack/node-core-library": 3.45.5 @@ -6614,7 +6614,7 @@ __metadata: typescript: ~4.6.3 bin: api-extractor: bin/api-extractor - checksum: b615783f6248c0d95931c1a0272860358a9618b9a1bff7959e7c5a242799eec7efc2b87db460bf2a3ae967da793dd457ac90570b60810fe9f094ca84b0deabe3 + checksum: 3b57304c6dfec8cb42095c1bd7813ecdaf19042a3f435a7931e55449a38a6aa619b238bafe53ee686b745393d3a41f3dc55aef10c91695fcbe1a109c437fb144 languageName: node linkType: hard @@ -20605,7 +20605,7 @@ __metadata: "@lingui/core": 3.13.3 "@lingui/macro": 3.13.3 "@lingui/react": 3.13.3 - "@microsoft/api-extractor": 7.24.1 + "@microsoft/api-extractor": 7.25.0 "@opentelemetry/api": 1.1.0 "@opentelemetry/exporter-collector": 0.25.0 "@opentelemetry/semantic-conventions": 1.2.0 From a938ae1d9e46df7af9dd27c5add383c5569d6145 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 8 Jun 2022 12:17:03 +0200 Subject: [PATCH 11/70] Chore: Remove integration tag (#50395) --- .../database/database_dashboard_public_test.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/services/dashboards/database/database_dashboard_public_test.go b/pkg/services/dashboards/database/database_dashboard_public_test.go index 56f63256774..4e073cc958b 100644 --- a/pkg/services/dashboards/database/database_dashboard_public_test.go +++ b/pkg/services/dashboards/database/database_dashboard_public_test.go @@ -1,6 +1,3 @@ -//go:build integration -// +build integration - package database import ( @@ -15,7 +12,7 @@ import ( ) // GetPublicDashboard -func TestGetPublicDashboard(t *testing.T) { +func TestIntegrationGetPublicDashboard(t *testing.T) { var sqlStore *sqlstore.SQLStore var dashboardStore *DashboardStore var savedDashboard *models.Dashboard @@ -78,11 +75,10 @@ func TestGetPublicDashboard(t *testing.T) { _, _, err = dashboardStore.GetPublicDashboard("abc1234") require.Error(t, models.ErrDashboardNotFound, err) }) - } // GetPublicDashboardConfig -func TestGetPublicDashboardConfig(t *testing.T) { +func TestIntegrationGetPublicDashboardConfig(t *testing.T) { var sqlStore *sqlstore.SQLStore var dashboardStore *DashboardStore var savedDashboard *models.Dashboard @@ -131,7 +127,7 @@ func TestGetPublicDashboardConfig(t *testing.T) { } // SavePublicDashboardConfig -func TestSavePublicDashboardConfig(t *testing.T) { +func TestIntegrationSavePublicDashboardConfig(t *testing.T) { var sqlStore *sqlstore.SQLStore var dashboardStore *DashboardStore var savedDashboard *models.Dashboard @@ -171,6 +167,7 @@ func TestSavePublicDashboardConfig(t *testing.T) { // verify we didn't update all dashboards pdc2, err := dashboardStore.GetPublicDashboardConfig(savedDashboard2.OrgId, savedDashboard2.Uid) + require.NoError(t, err) assert.False(t, pdc2.IsPublic) }) From e9f8d582c8084ade23fa6c4791e986dc9992f713 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 8 Jun 2022 12:22:55 +0200 Subject: [PATCH 12/70] Chore: Remove dashboard version from models (#50287) * Remove dashbpard version from models * Fix lint * Fix api & sqlstore tests * Remove integration tags * Fix lint again * Add integration test to correct namespace * Lont fix 2 * Change Id to ID in dashVersionMeta --- pkg/api/dashboard.go | 12 +-- pkg/api/dashboard_test.go | 22 +---- .../docs/definitions/dashboard_versions.go | 6 +- pkg/models/dashboard_version.go | 89 ------------------- pkg/services/dashboards/database/database.go | 5 +- .../dashverimpl/store_test.go | 10 +-- pkg/services/dashboardversion/model.go | 18 +++- .../sqlstore/dashboard_thumbs_test.go | 5 +- .../sqlstore/migrations/ualert/permissions.go | 5 +- pkg/services/sqlstore/mockstore/mockstore.go | 2 - pkg/services/sqlstore/org_test.go | 5 +- 11 files changed, 44 insertions(+), 135 deletions(-) delete mode 100644 pkg/models/dashboard_version.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index c9e8a7a50f2..8dc4f324728 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -634,9 +634,9 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons creator = hs.getUserLogin(c.Req.Context(), res.CreatedBy) } - dashVersionMeta := &models.DashboardVersionMeta{ - Id: res.ID, - DashboardId: res.DashboardID, + dashVersionMeta := &dashver.DashboardVersionMeta{ + ID: res.ID, + DashboardID: res.DashboardID, DashboardUID: dashUID, Data: res.Data, ParentVersion: res.ParentVersion, @@ -691,7 +691,7 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp baseVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &baseVersionQuery) if err != nil { - if errors.Is(err, models.ErrDashboardVersionNotFound) { + if errors.Is(err, dashver.ErrDashboardVersionNotFound) { return response.Error(404, "Dashboard version not found", err) } return response.Error(500, "Unable to compute diff", err) @@ -705,7 +705,7 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp newVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &newVersionQuery) if err != nil { - if errors.Is(err, models.ErrDashboardVersionNotFound) { + if errors.Is(err, dashver.ErrDashboardVersionNotFound) { return response.Error(404, "Dashboard version not found", err) } return response.Error(500, "Unable to compute diff", err) @@ -717,7 +717,7 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Resp result, err := dashdiffs.CalculateDiff(c.Req.Context(), &options, baseData, newData) if err != nil { - if errors.Is(err, models.ErrDashboardVersionNotFound) { + if errors.Is(err, dashver.ErrDashboardVersionNotFound) { return response.Error(404, "Dashboard version not found", err) } return response.Error(500, "Unable to compute diff", err) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e8561c60cd1..06c79d645ab 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -722,20 +722,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given two dashboards being compared", func(t *testing.T) { - dashboardvs := []*models.DashboardVersion{ - { - DashboardId: 1, - Version: 1, - Data: simplejson.NewFromAny(map[string]interface{}{ - "title": "Dash1", - })}, - { - DashboardId: 2, - Version: 2, - Data: simplejson.NewFromAny(map[string]interface{}{ - "title": "Dash2", - })}, - } fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersion{ { @@ -753,7 +739,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { }), }, } - sqlmock := mockstore.SQLStoreMock{ExpectedDashboardVersions: dashboardvs} + sqlmock := mockstore.SQLStoreMock{} setUp := func() { dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Return(nil) @@ -861,12 +847,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { Version: 1, } mockSQLStore := mockstore.NewSQLStoreMock() - mockSQLStore.ExpectedDashboardVersions = []*models.DashboardVersion{ - { - DashboardId: 2, - Version: 1, - Data: fakeDash.Data, - }} restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { callRestoreDashboardVersion(sc) diff --git a/pkg/api/docs/definitions/dashboard_versions.go b/pkg/api/docs/definitions/dashboard_versions.go index f486e9a0b31..84a938e7386 100644 --- a/pkg/api/docs/definitions/dashboard_versions.go +++ b/pkg/api/docs/definitions/dashboard_versions.go @@ -2,7 +2,7 @@ package definitions import ( "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" ) // swagger:route GET /dashboards/id/{DashboardID}/versions dashboard_versions getDashboardVersions @@ -122,11 +122,11 @@ type GetDashboardVersionsParams struct { // swagger:response dashboardVersionsResponse type DashboardVersionsResponse struct { // in: body - Body []*models.DashboardVersionDTO `json:"body"` + Body []*dashver.DashboardVersionDTO `json:"body"` } // swagger:response dashboardVersionResponse type DashboardVersionResponse struct { // in: body - Body *models.DashboardVersionMeta `json:"body"` + Body *dashver.DashboardVersionMeta `json:"body"` } diff --git a/pkg/models/dashboard_version.go b/pkg/models/dashboard_version.go deleted file mode 100644 index ba99d246db8..00000000000 --- a/pkg/models/dashboard_version.go +++ /dev/null @@ -1,89 +0,0 @@ -package models - -import ( - "errors" - "time" - - "github.com/grafana/grafana/pkg/components/simplejson" -) - -var ( - ErrDashboardVersionNotFound = errors.New("dashboard version not found") - ErrNoVersionsForDashboardId = errors.New("no dashboard versions found for the given DashboardId") -) - -// A DashboardVersion represents the comparable data in a dashboard, allowing -// diffs of the dashboard to be performed. -type DashboardVersion struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - ParentVersion int `json:"parentVersion"` - RestoredFrom int `json:"restoredFrom"` - Version int `json:"version"` - - Created time.Time `json:"created"` - CreatedBy int64 `json:"createdBy"` - - Message string `json:"message"` - Data *simplejson.Json `json:"data"` -} - -// DashboardVersionMeta extends the dashboard version model with the names -// associated with the UserIds, overriding the field with the same name from -// the DashboardVersion model. -type DashboardVersionMeta struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - DashboardUID string `json:"uid"` - ParentVersion int `json:"parentVersion"` - RestoredFrom int `json:"restoredFrom"` - Version int `json:"version"` - Created time.Time `json:"created"` - Message string `json:"message"` - Data *simplejson.Json `json:"data"` - CreatedBy string `json:"createdBy"` -} - -// DashboardVersionDTO represents a dashboard version, without the dashboard -// map. -type DashboardVersionDTO struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - DashboardUID string `json:"dashboardUid"` - ParentVersion int `json:"parentVersion"` - RestoredFrom int `json:"restoredFrom"` - Version int `json:"version"` - Created time.Time `json:"created"` - CreatedBy string `json:"createdBy"` - Message string `json:"message"` -} - -// -// Queries -// - -type GetDashboardVersionQuery struct { - DashboardId int64 - OrgId int64 - Version int - - Result *DashboardVersion -} - -type GetDashboardVersionsQuery struct { - DashboardId int64 - DashboardUID string - OrgId int64 - Limit int - Start int - - Result []*DashboardVersionDTO -} - -// -// Commands -// - -type DeleteExpiredVersionsCommand struct { - DeletedRows int64 -} diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 3ce8e800597..847654c416c 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" @@ -472,8 +473,8 @@ func saveDashboard(sess *sqlstore.DBSession, cmd *models.SaveDashboardCommand) e return models.ErrDashboardNotFound } - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go index 1c5154938ff..845856b8e0b 100644 --- a/pkg/services/dashboardversion/dashverimpl/store_test.go +++ b/pkg/services/dashboardversion/dashverimpl/store_test.go @@ -54,7 +54,7 @@ func TestIntegrationGetDashboardVersion(t *testing.T) { _, err := dashVerStore.Get(context.Background(), &query) require.Error(t, err) - assert.Equal(t, models.ErrDashboardVersionNotFound, err) + assert.Equal(t, dashver.ErrDashboardVersionNotFound, err) }) } @@ -165,8 +165,8 @@ func insertTestDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, title string dash.Data.Set("uid", dash.Uid) err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: dash.Version, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, @@ -227,8 +227,8 @@ func updateTestDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, dashboard *m require.Nil(t, err) err = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, diff --git a/pkg/services/dashboardversion/model.go b/pkg/services/dashboardversion/model.go index cfc45807625..6de84dd4d53 100644 --- a/pkg/services/dashboardversion/model.go +++ b/pkg/services/dashboardversion/model.go @@ -14,7 +14,7 @@ var ( type DashboardVersion struct { ID int64 `json:"id"` - DashboardID int64 `json:"dashboardId"` + DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"` ParentVersion int `json:"parentVersion"` RestoredFrom int `json:"restoredFrom"` Version int `json:"version"` @@ -55,3 +55,19 @@ type DashboardVersionDTO struct { CreatedBy string `json:"createdBy"` Message string `json:"message"` } + +// DashboardVersionMeta extends the dashboard version model with the names +// associated with the UserIds, overriding the field with the same name from +// the DashboardVersion model. +type DashboardVersionMeta struct { + ID int64 `json:"id"` + DashboardID int64 `json:"dashboardId"` + DashboardUID string `json:"uid"` + ParentVersion int `json:"parentVersion"` + RestoredFrom int `json:"restoredFrom"` + Version int `json:"version"` + Created time.Time `json:"created"` + Message string `json:"message"` + Data *simplejson.Json `json:"data"` + CreatedBy string `json:"createdBy"` +} diff --git a/pkg/services/sqlstore/dashboard_thumbs_test.go b/pkg/services/sqlstore/dashboard_thumbs_test.go index c3dde09ca8a..af7f364d2c4 100644 --- a/pkg/services/sqlstore/dashboard_thumbs_test.go +++ b/pkg/services/sqlstore/dashboard_thumbs_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/require" ) @@ -295,8 +296,8 @@ func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Das require.Nil(t, err) err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, diff --git a/pkg/services/sqlstore/migrations/ualert/permissions.go b/pkg/services/sqlstore/migrations/ualert/permissions.go index 0765dcb0c68..0e2bd3b7e1d 100644 --- a/pkg/services/sqlstore/migrations/ualert/permissions.go +++ b/pkg/services/sqlstore/migrations/ualert/permissions.go @@ -7,6 +7,7 @@ import ( "xorm.io/xorm" "github.com/grafana/grafana/pkg/components/simplejson" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" @@ -118,8 +119,8 @@ func (m *folderHelper) createFolder(orgID int64, title string) (*dashboard, erro return nil, err } - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 8fa7e32784c..c49b48b2535 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -21,8 +21,6 @@ type SQLStoreMock struct { ExpectedAlert *models.Alert ExpectedPluginSetting *models.PluginSetting ExpectedDashboards []*models.Dashboard - ExpectedDashboardVersion *models.DashboardVersion - ExpectedDashboardVersions []*models.DashboardVersion ExpectedDashboardAclInfoList []*models.DashboardAclInfoDTO ExpectedUserOrgList []*models.UserOrgDTO ExpectedOrgListResponse OrgListResponse diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 9f866db87ec..6665850e0d3 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/util" ) @@ -441,8 +442,8 @@ func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId i dash.Data.Set("uid", dash.Uid) err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error { - dashVersion := &models.DashboardVersion{ - DashboardId: dash.Id, + dashVersion := &dashver.DashboardVersion{ + DashboardID: dash.Id, ParentVersion: dash.Version, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, From 35ba4a6ac14df2db262a759ec8576fa3092dd694 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 8 Jun 2022 13:53:24 +0300 Subject: [PATCH 13/70] Reporting: Update documentation and screenshots with v9 changes (#50072) * Reporting: Update docs * Update screenshots * Update docs/sources/enterprise/reporting.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/enterprise/reporting.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/enterprise/reporting.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * reporting docs copy edits reformat reporting docs, slightly correct multiple dashboard rendering doc * Format * Format[2] Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: Mitch Seaman --- docs/sources/enterprise/reporting.md | 56 +++++++++++++++++++--------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/sources/enterprise/reporting.md b/docs/sources/enterprise/reporting.md index fa93d3af79f..0ce64f5fb03 100644 --- a/docs/sources/enterprise/reporting.md +++ b/docs/sources/enterprise/reporting.md @@ -34,24 +34,34 @@ When [RBAC]({{< relref "access-control/" >}}) is enabled, you need to have the r Only organization admins can create reports by default. You can customize who can create reports with [Role-based access control]({{< relref "access-control/" >}}). -1. Click on the reports icon in the side menu. The Reports tab allows you to view, create, and update your reports. -1. Enter report information. All fields are required unless otherwise indicated. +1. Click on the Reports icon in the side navigation menu. + The Reports tab allows you to view, create, and update your reports. The report form has a multi-step layout. The steps do not need to be completed in succession and can be skipped over by clicking a step name. +2. Select report dashboard. + - **Source dashboard -** Select the dashboard to generate the report from. + - **Time range -** (optional) Use custom time range for the report. For more information, refer to [Report time range]({{< relref "#report-time-range" >}}). + - **Add another dashboard -** Add more than one dashboard to the report. +3. Format report. + - **Choose format options for the report -** Select at least one option. Attach report as PDF, embed dashboard as an image, or attach CSV file of table panel data. + - If you selected the PDF format option: + - Select an orientation for the report: **Portrait** or **Landscape**. + - Select a layout for the generated report: **Simple** or **Grid**. The simple layout renders each panel as full-width across the PDF. The grid layout renders the PDF with the same panel arrangement and width as the source dashboard. + - Click **Preview PDF** to view a rendered PDF with the options you selected. +4. Schedule report. + - Enter scheduling information. Options vary depending on the frequency selected. +5. Enter report information. All fields are required unless otherwise indicated. - **Report name -** Name of the report as you want it to appear in the Reports list. It's also used as the email subject. - **Recipients -** Enter the emails of the people or teams that you want to receive the report, separated by commas or semicolons. - **Reply to -** (optional) The address that will appear in the **Reply to** field of the email. - **Message -** (optional) Message body in the email with the report. - **Include a dashboard link -** Include a link to the dashboard from within the report email. - - **Source dashboard -** Select the dashboard to generate the report from. - - **Time range -** (optional) Use custom time range for the report. For more information check [Report time range]({{< relref "#report-time-range" >}}). -1. Select an orientation for the report: **Portrait** or **Landscape**. -1. Select a layout for the generated report: **Simple** or **Grid**. The simple layout renders each panel as full-width across the PDF. The grid layout renders the PDF with the same panel arrangement and width as the source dashboard. -1. **Add a CSV file of table panel data**: check this box to attach a CSV file to the report email for each table panel on the dashboard. -1. **Preview PDF** View a rendered PDF with the options you have selected. -1. Enter scheduling information. Options vary depending on the frequency you select. -1. **Save** the report. -1. **Send test email** to verify that the whole configuration is working as expected. You can choose to send this email to the recipients configured for the report, or to a different set of email addresses only used for testing. + - **Send test email** to verify that the whole configuration is working as expected. You can choose to send this email to the recipients configured for the report, or to a different set of email addresses only used for testing. +6. Preview and save the report. -{{< figure src="/static/img/docs/enterprise/reports_create_new_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} +{{< figure src="/static/img/docs/enterprise/reports/select-dashboard.png" max-width="500px" class="docs-image--no-shadow" >}} +{{< figure src="/static/img/docs/enterprise/reports/format-report.png" max-width="500px" class="docs-image--no-shadow" >}} +{{< figure src="/static/img/docs/enterprise/reports/schedule.png" max-width="500px" class="docs-image--no-shadow" >}} +{{< figure src="/static/img/docs/enterprise/reports/share.png" max-width="500px" class="docs-image--no-shadow" >}} +{{< figure src="/static/img/docs/enterprise/reports/confirm.png" max-width="500px" class="docs-image--no-shadow" >}} ### Choose template variables @@ -132,7 +142,7 @@ For reports that have an hourly or daily frequency, you can choose to send them When you schedule a report with a monthly frequency, and set the start date between the 29th and the 31st of the month, the report is only sent during the months that have those dates. If you want the report to be sent every month, select the **Send on the last day of the month** option instead. This way, the report is sent on the last day of every month regardless of how many days there are in any given month. -### Send test email +### Send a test email > **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). @@ -145,19 +155,31 @@ The last saved version of the report will be sent to selected emails. You can us {{< figure src="/static/img/docs/enterprise/reports_send_test_mail.png" max-width="500px" class="docs-image--no-shadow" >}} -## Pause report +### Pause a report > **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/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. -## Send report via the API +### Add multiple dashboards to a report + +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 9.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). + +You can add more than one dashboard to a report. Additional dashboards will be rendered as new pages in the same PDF file, or additional images if you chose to embed images in your report email. Note: you cannot add the same dashboard to a report twice. + +### Embed a dashboard as an image into a report + +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 9.0 and later, and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). + +You can send a report email with an image of the dashboard embedded in the email itself, instead of attached as a PDF. In this case, the email recipients can see the dashboard at a glance instead of having to open the PDF. + +## Send a report via the API You can send reports programmatically with the [send report]({{< relref "../developers/http_api/reporting/#send-report" >}}) endpoint in the [HTTP APIs]({{< relref "../developers/http_api/" >}}). ## Rendering configuration -When generating reports, each panel renders separately before being collected in a PDF. The per panel rendering timeout and number of concurrently rendered panels can be configured. +When generating reports, each panel renders separately before being collected in a PDF. The per-panel rendering timeout and number of concurrently rendered panels can be configured. To make a panel more legible, you can set a scale factor for the rendered images. However, a higher scale factor increases the file size of the generated PDF. @@ -188,7 +210,7 @@ font_bold = DejaVuSansCondensed-Bold.ttf font_italic = DejaVuSansCondensed-Oblique.ttf ``` -## Reports settings +## Report settings > **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.2 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). From 63ed5367c3ce72ce75f3aebf70924b016158fbe1 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 8 Jun 2022 14:00:52 +0300 Subject: [PATCH 14/70] Remove test_steps (#50396) --- scripts/drone/pipelines/main.star | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/scripts/drone/pipelines/main.star b/scripts/drone/pipelines/main.star index 3d1ca91229b..6f82f18a47f 100644 --- a/scripts/drone/pipelines/main.star +++ b/scripts/drone/pipelines/main.star @@ -116,16 +116,6 @@ def get_steps(edition): wire_install_step(), yarn_install_step(), ] - test_steps = [ - lint_drone_step(), - codespell_step(), - shellcheck_step(), - lint_backend_step(edition=edition), - lint_frontend_step(), - test_backend_step(edition=edition), - test_backend_integration_step(edition=edition), - test_frontend_step(), - ] build_steps = [ trigger_test_release(), enterprise_downstream_step(edition=edition, ver_mode=ver_mode), @@ -169,7 +159,7 @@ def get_steps(edition): windows_steps = get_windows_steps(edition=edition, ver_mode=ver_mode) store_steps = [store_packages_step(edition=edition, ver_mode=ver_mode),] - return init_steps, test_steps, build_steps, integration_test_steps, windows_steps, store_steps + return init_steps, build_steps, integration_test_steps, windows_steps, store_steps def trigger_test_release(): return { @@ -221,7 +211,7 @@ def main_pipelines(edition): ], }, } - init_steps, test_steps, build_steps, integration_test_steps, windows_steps, store_steps = get_steps(edition=edition) + init_steps, build_steps, integration_test_steps, windows_steps, store_steps = get_steps(edition=edition) pipelines = [docs_pipelines(edition, ver_mode, trigger), main_test_frontend(), main_test_backend(), pipeline( name='main-build-e2e-publish', edition=edition, trigger=trigger, services=[], From 49dc9da9c1e8dd6ea9ae53e1810932e5c6a5f89f Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Wed, 8 Jun 2022 08:25:53 -0300 Subject: [PATCH 15/70] Prometheus: Templating queries should pass on custom headers (#50344) * pass on all headers except for accept headers * touch up and testing * add custom header values to resource queries * remove my picture. oops * handle gzip responses as well * fix linting issues * add my space * no lint * removed cookies from being proxied * clean up and handle errors from io.reader.Close() calls --- pkg/tsdb/prometheus/resource/resource.go | 126 +++++++++++++++++++---- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/pkg/tsdb/prometheus/resource/resource.go b/pkg/tsdb/prometheus/resource/resource.go index 5ff90575264..44babce41c5 100644 --- a/pkg/tsdb/prometheus/resource/resource.go +++ b/pkg/tsdb/prometheus/resource/resource.go @@ -1,11 +1,15 @@ package resource import ( + "compress/gzip" "context" "encoding/json" "fmt" + "io" "io/ioutil" + "net/http" "net/url" + "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -16,8 +20,54 @@ import ( ) type Resource struct { - provider *client.Provider - log log.Logger + provider *client.Provider + log log.Logger + customHeaders map[string]string +} + +// Hop-by-hop headers. These are removed when sent to the backend. +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = []string{ + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +// The following headers will be removed from the request +var stopHeaders = []string{ + "cookie", + "Cookie", +} + +func delHopHeaders(header http.Header) { + for _, h := range hopHeaders { + header.Del(h) + } +} + +func delStopHeaders(header http.Header) { + for _, h := range stopHeaders { + header.Del(h) + } +} + +func addHeaders(header http.Header, toAdd map[string]string) { + for k, v := range toAdd { + header.Add(k, v) + } +} + +func normalizeReqHeaders(headers map[string][]string) map[string]string { + h := make(map[string]string, len(headers)) + for k, v := range headers { + h[k] = strings.Join(v, ",") + } + return h } func New( @@ -34,14 +84,46 @@ func New( p := client.NewProvider(settings, jsonData, httpClientProvider, cfg, features, plog) + customHeaders := make(map[string]string) + var jsonDataMap map[string]interface{} + + err := json.Unmarshal(settings.JSONData, &jsonDataMap) + if err != nil { + return nil, err + } + + index := 1 + for { + headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index) + headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index) + + key := jsonDataMap[headerNameSuffix] + if key == nil { + // No (more) header values are available + break + } + + if val, ok := settings.DecryptedSecureJSONData[headerValueSuffix]; ok { + switch k := key.(type) { + case string: + customHeaders[k] = val + } + } + index++ + } + return &Resource{ - log: plog, - provider: p, + log: plog, + provider: p, + customHeaders: customHeaders, }, nil } func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest) (int, []byte, error) { - client, err := r.provider.GetClient(reqHeaders(req.Headers)) + delHopHeaders(req.Headers) + delStopHeaders(req.Headers) + addHeaders(req.Headers, r.customHeaders) + client, err := r.provider.GetClient(normalizeReqHeaders(req.Headers)) if err != nil { return 500, nil, err } @@ -65,25 +147,29 @@ func (r *Resource) fetch(ctx context.Context, client *client.Client, req *backen return statusCode, nil, err } - defer resp.Body.Close() //nolint (we don't care about the error being returned by resp.Body.Close()) + defer func() { + err = resp.Body.Close() + }() - data, err := ioutil.ReadAll(resp.Body) + // Check that the server actually sent compressed data + var reader io.ReadCloser + switch resp.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(resp.Body) + defer func() { + err = reader.Close() + }() + if err != nil { + return 500, nil, err + } + default: + reader = resp.Body + } + + data, err := ioutil.ReadAll(reader) if err != nil { return 500, nil, err } return resp.StatusCode, data, err } - -func reqHeaders(headers map[string][]string) map[string]string { - // Keep only the authorization header, incase downstream the authorization header is required. - // Strip all the others out as appropriate headers will be applied to speak with prometheus. - h := make(map[string]string) - accessValues := headers["Authorization"] - - if len(accessValues) > 0 { - h["Authorization"] = accessValues[0] - } - - return h -} From e0e76f0252a1b841a8f4e01a4843936882f9b6a0 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:35:37 +0200 Subject: [PATCH 16/70] Fixes term Grafana Alerting (#50404) --- CHANGELOG.md | 2 +- docs/sources/alerting/_index.md | 10 +++++----- docs/sources/alerting/about-alerting.md | 10 +++++----- docs/sources/alerting/alerting-rules/_index.md | 2 +- .../create-grafana-managed-rule.md | 2 +- .../create-mimir-loki-managed-rule.md | 2 +- docs/sources/alerting/contact-points/_index.md | 2 +- docs/sources/alerting/difference-old-new.md | 10 +++++----- docs/sources/alerting/fundamentals/_index.md | 2 +- .../alerting/fundamentals/alertmanager.md | 2 +- .../fundamentals/annotation-label/_index.md | 2 +- .../annotation-label/how-to-use-labels.md | 4 ++-- .../alerting/fundamentals/state-and-health.md | 2 +- .../sources/alerting/high-availability/_index.md | 4 ++-- docs/sources/alerting/migrating-alerts/_index.md | 16 ++++++++-------- .../migrating-alerts/migrating-legacy-alerts.md | 8 ++++---- .../sources/alerting/migrating-alerts/opt-out.md | 16 ++++++++-------- docs/sources/alerting/notifications/_index.md | 2 +- docs/sources/alerting/performance.md | 4 ++-- docs/sources/dashboards/time-range-controls.md | 4 ++-- docs/sources/datasources/alertmanager.md | 4 ++-- docs/sources/developers/http_api/alerting.md | 2 +- docs/sources/developers/http_api/folder.md | 2 +- .../sources/developers/plugins/backend/_index.md | 4 ++-- docs/sources/introduction/_index.md | 2 +- docs/sources/old-alerting/_index.md | 4 ++-- docs/sources/old-alerting/create-alerts.md | 2 +- .../about-expressions.md | 2 +- .../sources/release-notes/release-notes-8-2-2.md | 2 +- .../setup-grafana/configure-grafana/_index.md | 4 ++-- .../set-up-for-high-availability.md | 4 ++-- docs/sources/whatsnew/whats-new-in-v5-3.md | 2 +- docs/sources/whatsnew/whats-new-in-v7-4.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 2 +- pkg/tsdb/legacydata/dataframes.go | 2 +- .../alerting/components/DeprecationNotice.tsx | 6 +++--- 36 files changed, 76 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d33bd3995..77ce5191d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1087,7 +1087,7 @@ If an alert should evaluate to `OK` when one or all conditions return `No Data` - **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404) - **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas) - **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova) -- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana Alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) - **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova) - **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo) - **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 51a173c4b41..9803ae16ed7 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -6,7 +6,7 @@ title: Alerting weight: 114 --- -# Grafana alerting +# Grafana Alerting Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to: @@ -14,19 +14,19 @@ Grafana alerts allow you to learn about problems in your systems moments after t - Create and manage Grafana Mimir and Loki managed alerts - View alerting information from Prometheus and Alertmanager compatible data sources -For new installations or existing installs without alerting configured, Grafana alerting is enabled by default. +For new installations or existing installs without alerting configured, Grafana Alerting is enabled by default. | Release | Cloud | Enterprise | OSS | | ----------- | ------------- | ------------- | ------------- | | Grafana 9.0 | On by default | On by default | On by default | -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/" >}}). +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/" >}}). -Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "fundamentals/" >}}) of Grafana alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. +Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "fundamentals/" >}}) of Grafana Alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. - [About alert rules]({{< relref "fundamentals/alert-rules/" >}}) - [Migrating legacy alerts]({{< relref "migrating-alerts/" >}}) -- [Disable Grafana alerting in OSS]({{< relref "migrating-alerts/opt-out/" >}}) +- [Disable Grafana Alerting in OSS]({{< relref "migrating-alerts/opt-out/" >}}) - [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule/" >}}) - [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule/" >}}) - [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list/" >}}) diff --git a/docs/sources/alerting/about-alerting.md b/docs/sources/alerting/about-alerting.md index c4d35a80402..68006f516a4 100644 --- a/docs/sources/alerting/about-alerting.md +++ b/docs/sources/alerting/about-alerting.md @@ -1,18 +1,18 @@ --- aliases: - /docs/grafana/latest/alerting/about-alerting/ -description: A quick overview of Grafana alerting +description: A quick overview of Grafana Alerting keywords: - grafana - alerting - overview - concepts - basics -title: About Grafana alerting +title: About Grafana Alerting weight: 100 --- -# About Grafana alerting +# About Grafana Alerting Grafana Alerting consists of several individual concepts that are at the core of a flexible and powerful alerting engine. @@ -24,9 +24,9 @@ We will also touch on various other concepts such as [silences]({{< relref "sile ## Overview -{{< figure src="/static/img/docs/alerting/unified/about-alerting-flow-diagram.jpg" caption="Grafana alerting overview" >}} +{{< figure src="/static/img/docs/alerting/unified/about-alerting-flow-diagram.jpg" caption="Grafana Alerting overview" >}} -As shown in the diagram above, Grafana alerting uses [labels]({{< relref "fundamentals/annotation-label/how-to-use-labels/" >}}) to match an alert rule and its instances to a specific notification policy. This concept of labels and label matching is important and is also used in [silences]({{< relref "silences/" >}}). +As shown in the diagram above, Grafana Alerting uses [labels]({{< relref "fundamentals/annotation-label/how-to-use-labels/" >}}) to match an alert rule and its instances to a specific notification policy. This concept of labels and label matching is important and is also used in [silences]({{< relref "silences/" >}}). Each notification policy specifies a set of [label matchers]({{< relref "fundamentals/annotation-label/labels-and-label-matchers/" >}}) to indicate what alerts they are responsible for. diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index e121804910a..adb867cee92 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -7,7 +7,7 @@ title: Create and manage rules weight: 130 --- -# Create and manage Grafana alerting rules +# Create and manage Grafana Alerting rules An alerting rule is a set of evaluation criteria that determines whether an alert will fire. The rule consists of one or more queries and expressions, a condition, the frequency of evaluation, and optionally, the duration over which the condition is met. diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 3d2b68d2b0f..028b39870b8 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -15,7 +15,7 @@ weight: 400 # Create a Grafana managed alerting rule -Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana alerting, see [About Grafana alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana alerting. +Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. ## Add Grafana managed rule diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index f2ad158fa5c..1706d6dcef7 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -16,7 +16,7 @@ weight: 400 # Create a Grafana Mimir or Loki managed alerting rule -Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana alerting, see [About Grafana alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana alerting. +Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. ## Before you begin diff --git a/docs/sources/alerting/contact-points/_index.md b/docs/sources/alerting/contact-points/_index.md index 6d1c31d63e8..877636f8324 100644 --- a/docs/sources/alerting/contact-points/_index.md +++ b/docs/sources/alerting/contact-points/_index.md @@ -20,7 +20,7 @@ Use contact points to define how your contacts are notified when an alert fires. You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager/" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager/" >}}). -Before you begin, see [About Grafana alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana alerting. +Before you begin, see [About Grafana Alerting]({{< relref "../about-alerting/" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. - [Create contact point]({{< relref "create-contact-point/" >}}) - [Edit contact point]({{< relref "edit-contact-point/" >}}) diff --git a/docs/sources/alerting/difference-old-new.md b/docs/sources/alerting/difference-old-new.md index bb32cd3228f..e6274a97e3b 100644 --- a/docs/sources/alerting/difference-old-new.md +++ b/docs/sources/alerting/difference-old-new.md @@ -8,13 +8,13 @@ keywords: - grafana - alerting - guide -title: What's new in Grafana alerting +title: What's new in Grafana Alerting weight: 108 --- -# What's new in Grafana alerting +# What's new in Grafana Alerting -Grafana alerting has several enhancements over legacy dashboard alerting. +Grafana Alerting has several enhancements over legacy dashboard alerting. ## Multi-dimensional alerting @@ -28,7 +28,7 @@ Since unified alerts are no longer directly tied to panel queries, they do not i ## Create Loki and Grafana Mimir alerting rules -In Grafana alerting, you can manage Loki and Grafana Mimir alerting rules using the same UI and API as your Grafana managed alerts. +In Grafana Alerting, you can manage Loki and Grafana Mimir alerting rules using the same UI and API as your Grafana managed alerts. ## View and search for alerts from Prometheus compatible data sources @@ -36,7 +36,7 @@ Alerts for Prometheus compatible data sources are now listed under the Grafana a ## Special alerts for alert state NoData and Error -Grafana alerting introduced a new concept of the alert states. When evaluation of an alerting rule produces state NoData or Error, Grafana alerting will generate special alerts that will have the following labels: +Grafana Alerting introduced a new concept of the alert states. When evaluation of an alerting rule produces state NoData or Error, Grafana Alerting will generate special alerts that will have the following labels: - `alertname` with value DatasourceNoData or DatasourceError depending on the state. - `rulename` name of the alert rule the special alert belongs to. diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 5315b7899f7..fccce0b1a0e 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -9,7 +9,7 @@ weight: 110 # Alerting fundamentals -This section includes the following fundamental concepts of Grafana alerting: +This section includes the following fundamental concepts of Grafana Alerting: - [Annotations and labels for alerting rules]({{< relref "annotation-label/" >}}) - [Alertmanager]({{< relref "alertmanager/" >}}) diff --git a/docs/sources/alerting/fundamentals/alertmanager.md b/docs/sources/alerting/fundamentals/alertmanager.md index 7e72361e4eb..181e5abdfe5 100644 --- a/docs/sources/alerting/fundamentals/alertmanager.md +++ b/docs/sources/alerting/fundamentals/alertmanager.md @@ -15,7 +15,7 @@ Grafana includes built-in support for Prometheus Alertmanager. By default, notif > **Note:** Before v8.2, the configuration of the embedded Alertmanager was shared across organizations. If you are on an older Grafana version, we recommend that you use Grafana alerts only if you have one organization. Otherwise, your contact points are visible to all organizations. -Grafana alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../datasources/alertmanager/" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Grafana Mimir or Loki data sources. +Grafana Alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../datasources/alertmanager/" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Grafana Mimir or Loki data sources. {{< figure max-width="40%" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" max-width="250px" caption="Select Alertmanager" >}} diff --git a/docs/sources/alerting/fundamentals/annotation-label/_index.md b/docs/sources/alerting/fundamentals/annotation-label/_index.md index 9627f373571..954b6ce8345 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/_index.md +++ b/docs/sources/alerting/fundamentals/annotation-label/_index.md @@ -28,6 +28,6 @@ Labels are key-value pairs that contain information about, and are used to uniqu Before you begin using annotations and labels, familiarize yourself with: -- [Labels in Grafana alerting]({{< relref "how-to-use-labels/" >}}) +- [Labels in Grafana Alerting]({{< relref "how-to-use-labels/" >}}) - [How label matching works]({{< relref "how-to-use-labels/" >}}) - [Template variables for alerting rule labels and annotations]({{< relref "variables-label-annotation/" >}}) diff --git a/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md b/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md index f5932693bbb..f2030094b15 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md +++ b/docs/sources/alerting/fundamentals/annotation-label/how-to-use-labels.md @@ -7,11 +7,11 @@ keywords: - alerting - guide - fundamentals -title: Labels in Grafana alerting +title: Labels in Grafana Alerting weight: 117 --- -# Labels in Grafana alerting +# Labels in Grafana Alerting This topic explains why labels are a fundamental component of alerting. diff --git a/docs/sources/alerting/fundamentals/state-and-health.md b/docs/sources/alerting/fundamentals/state-and-health.md index dc2ff746fd4..7376c3b13c5 100644 --- a/docs/sources/alerting/fundamentals/state-and-health.md +++ b/docs/sources/alerting/fundamentals/state-and-health.md @@ -53,7 +53,7 @@ An alert rule can have one the following health statuses: ## Special alerts for `NoData` and `Error` -When evaluation of an alerting rule produces state `NoData` or `Error`, Grafana alerting will generate alert instances that have the following additional labels: +When evaluation of an alerting rule produces state `NoData` or `Error`, Grafana Alerting will generate alert instances that have the following additional labels: | Label | Description | | ------------------ | ---------------------------------------------------------------------- | diff --git a/docs/sources/alerting/high-availability/_index.md b/docs/sources/alerting/high-availability/_index.md index ce631cc2b7b..4d0eba3ded0 100644 --- a/docs/sources/alerting/high-availability/_index.md +++ b/docs/sources/alerting/high-availability/_index.md @@ -15,9 +15,9 @@ weight: 450 # About alerting high availability -The Grafana alerting system has two main components: a `Scheduler` and an internal `Alertmanager`. The `Scheduler` evaluates your [alert rules]({{< relref "../fundamentals/evaluate-grafana-alerts/" >}}), while the internal Alertmanager manages **routing** and **grouping**. +The Grafana Alerting system has two main components: a `Scheduler` and an internal `Alertmanager`. The `Scheduler` evaluates your [alert rules]({{< relref "../fundamentals/evaluate-grafana-alerts/" >}}), while the internal Alertmanager manages **routing** and **grouping**. -When running Grafana alerting in high availability, the operational mode of the scheduler remains unaffected, and each Grafana instance evaluates all alerts. The operational change happens in the Alertmanager when it deduplicates alert notifications across Grafana instances. +When running Grafana Alerting in high availability, the operational mode of the scheduler remains unaffected, and each Grafana instance evaluates all alerts. The operational change happens in the Alertmanager when it deduplicates alert notifications across Grafana instances. {{< figure src="/static/img/docs/alerting/unified/high-availability-ua.png" class="docs-image--no-shadow" max-width= "750px" caption="High availability" >}} diff --git a/docs/sources/alerting/migrating-alerts/_index.md b/docs/sources/alerting/migrating-alerts/_index.md index 3824cd50f1d..c2b1a057a0f 100644 --- a/docs/sources/alerting/migrating-alerts/_index.md +++ b/docs/sources/alerting/migrating-alerts/_index.md @@ -2,26 +2,26 @@ aliases: - /docs/grafana/latest/alerting/migrating-alerts/ description: Migrate Grafana alerts -title: Migrate to Grafana alerting +title: Migrate to Grafana Alerting weight: 113 --- -# Migrate to Grafana alerting +# 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. +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. +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/" >}}) 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/" >}}) 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. | +| 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/" >}}) 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/" >}}) 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. +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-alerts/migrating-legacy-alerts.md b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md index c9fd8e5409a..e517e3e90fa 100644 --- a/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md +++ b/docs/sources/alerting/migrating-alerts/migrating-legacy-alerts.md @@ -10,9 +10,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 any new alerts. This topic explains how legacy dashboard alerts are migrated and some limitations. +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. +> **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: @@ -20,9 +20,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/" >}}) 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/" >}}) 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. +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. diff --git a/docs/sources/alerting/migrating-alerts/opt-out.md b/docs/sources/alerting/migrating-alerts/opt-out.md index b270a532461..72edb664d98 100644 --- a/docs/sources/alerting/migrating-alerts/opt-out.md +++ b/docs/sources/alerting/migrating-alerts/opt-out.md @@ -4,15 +4,15 @@ 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 +title: Opt-out of Grafana Alerting weight: 113 --- -# Opt-out to Grafana alerting in OSS +# 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. +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. +> **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 @@ -22,16 +22,16 @@ We recommend that you backup Grafana's database. If you are using PostgreSQL as 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 "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section. +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana Alerting]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section. 1. Set the `enabled` property to `false`. 1. For [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#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 +## Opt-in to Grafana Alerting -When you are ready to make the switch, the following procedure will help you migrate to Grafana alerting. +When you are ready to make the switch, the following procedure will help you migrate to Grafana Alerting. To opt-in to Grafana alerts: @@ -40,4 +40,4 @@ To opt-in to Grafana alerts: 1. Next, for [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#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. +> **Note:** The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available. diff --git a/docs/sources/alerting/notifications/_index.md b/docs/sources/alerting/notifications/_index.md index 1a168328410..76412b8857d 100644 --- a/docs/sources/alerting/notifications/_index.md +++ b/docs/sources/alerting/notifications/_index.md @@ -23,7 +23,7 @@ You can configure Grafana managed notification policies as well as notification {{< figure max-width="40%" src="/static/img/docs/alerting/unified/notification-policies-grouping.png" max-width="650px" caption="Notification policies grouping" >}} -Grouping is a new and key concept of Grafana alerting that categorizes alert notifications of similar nature into a single funnel. This allows you to properly route alert notifications during larger outages when many parts of a system fail at once causing a high number of alerts to fire simultaneously. +Grouping is a new and key concept of Grafana Alerting that categorizes alert notifications of similar nature into a single funnel. This allows you to properly route alert notifications during larger outages when many parts of a system fail at once causing a high number of alerts to fire simultaneously. For example, suppose you have 100 services connected to a database in different environments. These services are differentiated by the label `env=environmentname`. An alert rule is in place to monitor whether your services can reach the database named `alertname=DatabaseUnreachable`. diff --git a/docs/sources/alerting/performance.md b/docs/sources/alerting/performance.md index a24fa0bb661..c4a2802e90f 100644 --- a/docs/sources/alerting/performance.md +++ b/docs/sources/alerting/performance.md @@ -7,7 +7,7 @@ weight = 100 # Alerting performance considerations -Grafana alerting supports multi-dimensional alerting, where one alert rule can generate many alerts. For example, you can configure an alert rule to fire an alert every time the CPU of individual VMs max out. This topic discusses performance considerations resulting from multi-dimensional alerting. +Grafana Alerting supports multi-dimensional alerting, where one alert rule can generate many alerts. For example, you can configure an alert rule to fire an alert every time the CPU of individual VMs max out. This topic discusses performance considerations resulting from multi-dimensional alerting. Evaluating alerting rules consumes RAM and CPU to compute the output of an alerting query, and network resources to send alert notifications and write the results to the Grafana SQL database. The configuration of individual alert rules affects the resource consumption and, therefore, the maximum number of rules a given configuration can support. @@ -19,6 +19,6 @@ The following section provides a list of alerting performance considerations. Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. -Grafana alerting exposes a metric, `grafana_alerting_rule_evaluations_total` that counts the number of alert rule evaluations. To get a feel for the influence of rule evaluations on your Grafana instance, you can observe the rate of evaluations and compare it with resource consumption. In a Prometheus-compatible database, you can use the query `rate(grafana_alerting_rule_evaluations_total[5m])` to compute the rate over 5 minute windows of time. It's important to remember that this isn't the full picture of rule evaluation. For example, the load will be unevenly distributed if you have some rules that evaluate every 10 seconds, and others every 30 minutes. +Grafana Alerting exposes a metric, `grafana_alerting_rule_evaluations_total` that counts the number of alert rule evaluations. To get a feel for the influence of rule evaluations on your Grafana instance, you can observe the rate of evaluations and compare it with resource consumption. In a Prometheus-compatible database, you can use the query `rate(grafana_alerting_rule_evaluations_total[5m])` to compute the rate over 5 minute windows of time. It's important to remember that this isn't the full picture of rule evaluation. For example, the load will be unevenly distributed if you have some rules that evaluate every 10 seconds, and others every 30 minutes. These factors all affect the load on the Grafana instance, but you should also be aware of the performance impact that evaluating these rules has on your data sources. Alerting queries are often the vast majority of queries handled by monitoring databases, so the same load factors that affect the Grafana instance affect them as well. diff --git a/docs/sources/dashboards/time-range-controls.md b/docs/sources/dashboards/time-range-controls.md index 22f9fd3c307..1c1a9fe16fb 100644 --- a/docs/sources/dashboards/time-range-controls.md +++ b/docs/sources/dashboards/time-range-controls.md @@ -40,9 +40,9 @@ Here are some examples: | This Year | `now/Y` | `now/Y` | | Previous fiscal year | `now-1y/fy` | `now-1y/fy` | -### Note about Grafana alerting +### Note about Grafana Alerting -For Grafana alerting, we do not support the following syntaxes at this time. +For Grafana Alerting, we do not support the following syntaxes at this time. - now+n for future timestamps. - now-1n/n for "start of n until end of n" since this is an absolute timestamp. diff --git a/docs/sources/datasources/alertmanager.md b/docs/sources/datasources/alertmanager.md index 27522a8f0f9..e02d54b40d1 100644 --- a/docs/sources/datasources/alertmanager.md +++ b/docs/sources/datasources/alertmanager.md @@ -13,11 +13,11 @@ weight: 150 # Alertmanager data source -Grafana includes built-in support for Prometheus Alertmanager. It is presently in alpha and not accessible unless [alpha plugins are enabled in Grafana settings](https://grafana.com/docs/grafana/latest/administration/configuration/#enable_alpha-1). Once you add it as a data source, you can use the [Grafana alerting UI](https://grafana.com/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop-down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources. +Grafana includes built-in support for Prometheus Alertmanager. It is presently in alpha and not accessible unless [alpha plugins are enabled in Grafana settings](https://grafana.com/docs/grafana/latest/administration/configuration/#enable_alpha-1). Once you add it as a data source, you can use the [Grafana Alerting UI](https://grafana.com/docs/grafana/latest/alerting/) to manage silences, contact points as well as notification policies. A drop-down option in these pages allows you to switch between Grafana and any configured Alertmanager data sources. ## Alertmanager implementations -[Prometheus](https://prometheus.io/) and [Grafana Mimir](https://grafana.com/docs/mimir/latest/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana alerting UI, as it does not support updating configuration via HTTP API. +[Prometheus](https://prometheus.io/) and [Grafana Mimir](https://grafana.com/docs/mimir/latest/) (default) implementations of Alertmanager are supported. You can specify implementation in the data source settings page. In case of Prometheus contact points and notification policies are read-only in the Grafana Alerting UI, as it does not support updating configuration via HTTP API. ## Provision the Alertmanager data source diff --git a/docs/sources/developers/http_api/alerting.md b/docs/sources/developers/http_api/alerting.md index 1b729fa240e..465e6deb6b9 100644 --- a/docs/sources/developers/http_api/alerting.md +++ b/docs/sources/developers/http_api/alerting.md @@ -17,7 +17,7 @@ title: 'Alerting HTTP API ' > **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/latest/alerting/old-alerting/) only. -You can find Grafana alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts. +You can find Grafana Alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana Alerting alerts documentation]({{< relref "../../alerting/" >}}) for details on how to create and manage new alerts. You can use the Alerting API to get information about legacy dashboard alerts and their states but this API cannot be used to modify the alert. To create new alerts or modify them you need to update the dashboard JSON that contains the alerts. diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md index d99887f8dc2..bba3eb08c39 100644 --- a/docs/sources/developers/http_api/folder.md +++ b/docs/sources/developers/http_api/folder.md @@ -280,7 +280,7 @@ Content-Length: 97 Deletes an existing folder identified by UID along with all dashboards (and their alerts) stored in the folder. This operation cannot be reverted. -If [Grafana alerting]({{< relref "../../alerting/" >}}) is enabled, you can set an optional query parameter `forceDeleteRules=false` so that requests will fail with 400 (Bad Request) error if the folder contains any Grafana alerts. However, if this parameter is set to `true` then it will delete any Grafana alerts under this folder. +If [Grafana Alerting]({{< relref "../../alerting/" >}}) is enabled, you can set an optional query parameter `forceDeleteRules=false` so that requests will fail with 400 (Bad Request) error if the folder contains any Grafana alerts. However, if this parameter is set to `true` then it will delete any Grafana alerts under this folder. **Required permissions** diff --git a/docs/sources/developers/plugins/backend/_index.md b/docs/sources/developers/plugins/backend/_index.md index 8e34a3e8143..bbd197cb21c 100644 --- a/docs/sources/developers/plugins/backend/_index.md +++ b/docs/sources/developers/plugins/backend/_index.md @@ -20,13 +20,13 @@ However, one limitation with these plugins are that they execute on the client-s We use the term _backend plugin_ to denote that a plugin has a backend component. Still, normally a backend plugin requires frontend components as well. This is for example true for backend data source plugins which normally need configuration and query editor components implemented for the frontend. -Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana alerting]({{< relref "../../../alerting/notifications/" >}}) and custom authentication to name a few. +Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/notifications/" >}}) and custom authentication to name a few. ## Use cases for implementing a backend plugin The following examples gives you an idea of why you'd consider implementing a backend plugin: -- Enable [Grafana alerting]({{< relref "../../../alerting/" >}}) for data sources. +- Enable [Grafana Alerting]({{< relref "../../../alerting/" >}}) for data sources. - Connect to non-HTTP services that normally can't be connected to from a web browser, e.g. SQL database servers. - Keep state between users, e.g. query caching for data sources. - Use custom authentication methods and/or authorization checks that aren't supported in Grafana. diff --git a/docs/sources/introduction/_index.md b/docs/sources/introduction/_index.md index 5a130f7bfa3..eb883794a1a 100644 --- a/docs/sources/introduction/_index.md +++ b/docs/sources/introduction/_index.md @@ -21,7 +21,7 @@ Explore your data through ad-hoc queries and dynamic drilldown. Split view and c ## Alerts -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. +If you're using Grafana Alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. diff --git a/docs/sources/old-alerting/_index.md b/docs/sources/old-alerting/_index.md index 41379e1e94c..a4183e9c884 100644 --- a/docs/sources/old-alerting/_index.md +++ b/docs/sources/old-alerting/_index.md @@ -8,9 +8,9 @@ weight: 114 # Legacy Grafana alerts -Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in/" >}}) feature. +Grafana Alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in/" >}}) feature. -> **Note**: Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/" >}}) for all existing installations. +> **Note**: Legacy dashboard alerts are deprecated and will be removed in Grafana 9. We encourage you to migrate to [Grafana Alerting]({{< relref "../unified-alerting/" >}}) for all existing installations. Legacy dashboard alerts have two main components: diff --git a/docs/sources/old-alerting/create-alerts.md b/docs/sources/old-alerting/create-alerts.md index df9e5430796..2ad8eb027a7 100644 --- a/docs/sources/old-alerting/create-alerts.md +++ b/docs/sources/old-alerting/create-alerts.md @@ -15,7 +15,7 @@ weight: 200 # Create alerts -Grafana alerting allows you to attach rules to your dashboard panels. When you save the dashboard, Grafana extracts the alert rules into a separate alert rule storage and schedules them for evaluation. +Grafana Alerting allows you to attach rules to your dashboard panels. When you save the dashboard, Grafana extracts the alert rules into a separate alert rule storage and schedules them for evaluation. ![Alerting overview](/static/img/docs/alerting/drag_handles_gif.gif) diff --git a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md index 434c5dee4af..4e760a47c3f 100644 --- a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md +++ b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md @@ -14,7 +14,7 @@ Server-side expressions allow you to manipulate data returned from queries with ## Using expressions -Expressions are primarily used by [Grafana alerting]({{< relref "../../../alerting/" >}}). The processing is done server-side, so expressions can operate without a browser session. However, expressions can also be used with backend data sources and visualization. +Expressions are primarily used by [Grafana Alerting]({{< relref "../../../alerting/" >}}). The processing is done server-side, so expressions can operate without a browser session. However, expressions can also be used with backend data sources and visualization. > **Note:** Expressions do not work with legacy dashboard alerts. diff --git a/docs/sources/release-notes/release-notes-8-2-2.md b/docs/sources/release-notes/release-notes-8-2-2.md index 526f5761245..baf4a83c210 100644 --- a/docs/sources/release-notes/release-notes-8-2-2.md +++ b/docs/sources/release-notes/release-notes-8-2-2.md @@ -29,7 +29,7 @@ title: Release notes for Grafana 8.2.2 - **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404) - **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas) - **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova) -- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) +- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana Alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart) - **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova) - **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo) - **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 3586fa9237d..ce8223697f6 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1177,7 +1177,7 @@ Sets a global limit on number of alert rules that can be created. Default is -1 ## [unified_alerting] -For more information about the Grafana alerts, refer to [About Grafana alerting]({{< relref "../../alerting/" >}}). +For more information about the Grafana alerts, refer to [About Grafana Alerting]({{< relref "../../alerting/" >}}). ### enabled @@ -1265,7 +1265,7 @@ For more information about the legacy dashboard alerting feature in Grafana, ref ### enabled -Set to `false` to [enable Grafana alerting]({{}}) and to disable legacy alerting engine. to disable Grafana alerting, set to `true`. +Set to `false` to [enable Grafana Alerting]({{}}) and to disable legacy alerting engine. to disable Grafana Alerting, set to `true`. ### execute_alerts diff --git a/docs/sources/setup-grafana/set-up-for-high-availability.md b/docs/sources/setup-grafana/set-up-for-high-availability.md index 8fac082d8cf..a5c94b9012d 100644 --- a/docs/sources/setup-grafana/set-up-for-high-availability.md +++ b/docs/sources/setup-grafana/set-up-for-high-availability.md @@ -29,13 +29,13 @@ Grafana will now persist all long term data in the database. How to configure th ## Alerting high availability -Grafana alerting provides a new [highly-available model]({{< relref "../alerting/high-availability/" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time. +Grafana Alerting provides a new [highly-available model]({{< relref "../alerting/high-availability/" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time. For instructions on setting up alerting high availability, see [enable alerting high availability](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/). **Legacy dashboard alerts** -Legacy Grafana alerting supports a limited form of high availability. In this model, [alert notifications](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. +Legacy Grafana Alerting supports a limited form of high availability. In this model, [alert notifications](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. ## Grafana Live diff --git a/docs/sources/whatsnew/whats-new-in-v5-3.md b/docs/sources/whatsnew/whats-new-in-v5-3.md index a50367ae7c2..260f06a1e0b 100644 --- a/docs/sources/whatsnew/whats-new-in-v5-3.md +++ b/docs/sources/whatsnew/whats-new-in-v5-3.md @@ -63,7 +63,7 @@ certain view mode enabled. Additionally, this also enables [playlists](/dashboar ## Notification Reminders -Do you use Grafana alerting and have some notifications that are more important than others? Then it's possible to set reminders so that you continue to be alerted until the problem is fixed. This is done on the notification channel itself and will affect all alerts that use that channel. +Do you use Grafana Alerting and have some notifications that are more important than others? Then it's possible to set reminders so that you continue to be alerted until the problem is fixed. This is done on the notification channel itself and will affect all alerts that use that channel. For additional examples of why reminders might be useful for you, see [multiple series](/alerting/alerts-overview/#multiple-series). For more information about how to enable and configure reminders, refer to [alerting reminders](/alerting/notifications/#send-reminders). diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index 90a05b5ccef..bfd8b0e087a 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -104,7 +104,7 @@ _Server-side expressions_ is an experimental feature that allows you to manipula The main use case is for [multi-dimensional]({{< relref "../basics/timeseries-dimensions/" >}}) data sources used with the upcoming next generation alerting, but expressions can be used with backend data sources and visualization as well. -> **Note:** Queries built with this feature might break with minor version upgrades until Grafana 8 is released. This feature does not work with the current Grafana alerting. +> **Note:** Queries built with this feature might break with minor version upgrades until Grafana 8 is released. This feature does not work with the current Grafana Alerting. For more information, refer to [About expressions]({{< relref "../panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions/" >}}). [About queries]({{< relref "../panels/query-a-data-source/about-queries/" >}}) was also updated as a result of this feature. diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index aba40d713cc..1ab026e6fb7 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -25,7 +25,7 @@ These features are included in the Grafana open source edition. ### Grafana v8.0 alerts -The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Mimir alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Grafana alerting]({{< relref "../alerting/" >}}). +The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Mimir alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Grafana Alerting]({{< relref "../alerting/" >}}). As part of the new alert changes, we have introduced a new data source, Alertmanager, which includes built-in support for Prometheus Alertmanager. It is presently in alpha and it not accessible unless alpha plugins are enabled in Grafana settings. For more information, refer to [Alertmanager data source]({{< relref "../datasources/alertmanager/" >}}). diff --git a/pkg/tsdb/legacydata/dataframes.go b/pkg/tsdb/legacydata/dataframes.go index 95cc39442b1..f3b60c4657f 100644 --- a/pkg/tsdb/legacydata/dataframes.go +++ b/pkg/tsdb/legacydata/dataframes.go @@ -28,7 +28,7 @@ type dataFrames struct { // NewDecodedDataFrames instantiates DataFrames from decoded frames. // // This should be the primary function for creating DataFrames if you're implementing a plugin. -// In a Grafana alerting scenario it needs to operate on decoded frames, which is why this function is +// In a Grafana Alerting scenario it needs to operate on decoded frames, which is why this function is // preferrable. When encoded data frames are needed, e.g. returned from Grafana HTTP API, it will // happen automatically when MarshalJSON() is called. func NewDecodedDataFrames(decodedFrames data.Frames) DataFrames { diff --git a/public/app/features/alerting/components/DeprecationNotice.tsx b/public/app/features/alerting/components/DeprecationNotice.tsx index ce95598a8cf..c89586f3ea4 100644 --- a/public/app/features/alerting/components/DeprecationNotice.tsx +++ b/public/app/features/alerting/components/DeprecationNotice.tsx @@ -10,16 +10,16 @@ const DeprecationNotice: FC<{}> = () => ( You are using Grafana legacy alerting, it has been deprecated and will be removed in the next major version of Grafana.
- We encourage you to upgrade to the new Grafana alerting experience. + We encourage you to upgrade to the new Grafana Alerting experience.

See{' '} - What’s New with Grafana alerting + What’s New with Grafana Alerting {' '} to learn more about what‘s new or learn{' '} - how to enable the new Grafana alerting feature + how to enable the new Grafana Alerting feature .

From 008ad463fd5c20cfaaa3f49e96850997780af9a9 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:49:49 +0200 Subject: [PATCH 17/70] Docs: Fixes Grafana Alerting in whats new (#50409) --- docs/sources/whatsnew/whats-new-in-v9-0.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/whatsnew/whats-new-in-v9-0.md b/docs/sources/whatsnew/whats-new-in-v9-0.md index 320d7e4cc80..169f90db421 100644 --- a/docs/sources/whatsnew/whats-new-in-v9-0.md +++ b/docs/sources/whatsnew/whats-new-in-v9-0.md @@ -17,7 +17,7 @@ weight: -33 # 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. +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 Grafana 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. @@ -82,11 +82,11 @@ The new heatmap by default assumes that the data is pre-bucketed. So if your que {{< figure src="/static/img/docs/heatmap-panel/heatmap_with_time_series_light_theme.png" max-width="500px" caption="Heatmap panel with time series" >}} -## Unified alerting is the default in Grafana 9 +## Grafana Alerting is the default in Grafana 9 -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. +Grafana 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 Grafana 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/migrating-alerts/opt-out/" >}}). 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. +Grafana Alerting, called unified alerting in Grafana 8, has been available since June, 2021 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/" >}}). 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 @@ -94,7 +94,7 @@ Alert state history for Grafana managed alerts indicates the cause of the state. ### Images in notifications for Grafana managed alerts -Unified alerting now provides the ability to capture an image for Grafana managed alerts and deliver it via a notification. This is configurable via the contact points tab of Unified alerting . This functionality was available in legacy dashboard alerting, but was missing in unified alerting. It requires the alert to be related to a specific dashboard panel using the “Dashboard UID” and “Panel ID” annotations. +Grafana Alerting now provides the ability to capture an image for Grafana managed alerts and deliver it via a notification. This is configurable via the contact points tab of Grafana Alerting . This functionality was available in legacy dashboard alerting, but was missing in Grafana Alerting. It requires the alert to be related to a specific dashboard panel using the “Dashboard UID” and “Panel ID” annotations. ## Envelope encryption is generally available and enabled by default From 58a783c0e4f0d6922d5aab3754887a5084bf0d12 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Wed, 8 Jun 2022 15:50:59 +0400 Subject: [PATCH 18/70] Previews: improve docs phrasing (#50405) --- docs/sources/dashboards/previews.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/dashboards/previews.md b/docs/sources/dashboards/previews.md index 8cf8bcb5b3b..877d4f189e9 100644 --- a/docs/sources/dashboards/previews.md +++ b/docs/sources/dashboards/previews.md @@ -19,7 +19,7 @@ Dashboard previews provide an overview of all available dashboards. They help yo > **Note:** Dashboard previews are available in Grafana 9.0+ as an opt-in beta feature. Data source permissions are not yet taken into the account when displaying the dashboard previews - refer to the [permissions]({{< relref "#preview-visibility">}}) to learn more before enabling the feature. -You can view dashboard previews after the administrator enables the feature following the procedure described below, and after you select the new grid layout. The dashboard previews feature is an opt-in feature that is disabled by default. +The dashboard previews feature is an opt-in feature that is disabled by default. You can view dashboard previews after an administrator enables the feature, and after you select the new grid layout. The feature-enablement procedure is outlined in the following section. - [Enable dashboard previews](#enable-dashboard-previews) - [About the dashboard previews crawler](#about-the-dashboard-previews-crawler) From 74608d6480b1b1b40a3ad52e4930edb0172073b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 8 Jun 2022 13:55:08 +0200 Subject: [PATCH 19/70] Security: Fixes javascript security warnings in old vendored libs (#50382) --- public/vendor/bootstrap/bootstrap.js | 705 +-- public/vendor/flot/jquery.flot.js | 6288 +++++++++++++------------- 2 files changed, 3500 insertions(+), 3493 deletions(-) diff --git a/public/vendor/bootstrap/bootstrap.js b/public/vendor/bootstrap/bootstrap.js index 870ead5783d..8730550092a 100644 --- a/public/vendor/bootstrap/bootstrap.js +++ b/public/vendor/bootstrap/bootstrap.js @@ -18,7 +18,7 @@ * ========================================================== */ -!function ($) { +!function($) { "use strict"; // jshint ;_; @@ -26,22 +26,22 @@ /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) * ======================================================= */ - $(function () { + $(function() { - $.support.transition = (function () { + $.support.transition = (function() { - var transitionEnd = (function () { + var transitionEnd = (function() { var el = document.createElement('bootstrap') , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } + 'WebkitTransition': 'webkitTransitionEnd' + , 'MozTransition': 'transitionend' + , 'OTransition': 'oTransitionEnd otransitionend' + , 'transition': 'transitionend' + } , name - for (name in transEndEventNames){ + for (name in transEndEventNames) { if (el.style[name] !== undefined) { return transEndEventNames[name] } @@ -77,46 +77,46 @@ * ========================================================== */ -!function ($) { +!function($) { "use strict"; // jshint ;_; -/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#dropdowns - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ + /* ============================================================ + * bootstrap-dropdown.js v2.3.2 + * http://getbootstrap.com/2.3.2/javascript.html#dropdowns + * ============================================================ + * Copyright 2013 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ - /* DROPDOWN CLASS DEFINITION - * ========================= */ + /* DROPDOWN CLASS DEFINITION + * ========================= */ var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } + , Dropdown = function(element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function() { + $el.parent().removeClass('open') + }) + } Dropdown.prototype = { constructor: Dropdown - , toggle: function (e) { + , toggle: function(e) { var $this = $(this) , $parent , isActive @@ -142,7 +142,7 @@ return false } - , keydown: function (e) { + , keydown: function(e) { var $this , $items , $active @@ -187,7 +187,7 @@ function clearMenus() { $('.dropdown-backdrop').remove() - $(toggle).each(function () { + $(toggle).each(function() { getParent($(this)).removeClass('open') }) } @@ -202,7 +202,7 @@ selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } - $parent = selector && $(selector) + $parent = selector && $.find(selector) if (!$parent || !$parent.length) $parent = $this.parent() @@ -215,8 +215,8 @@ var old = $.fn.dropdown - $.fn.dropdown = function (option) { - return this.each(function () { + $.fn.dropdown = function(option) { + return this.each(function() { var $this = $(this) , data = $this.data('dropdown') if (!data) $this.data('dropdown', (data = new Dropdown(this))) @@ -227,10 +227,10 @@ $.fn.dropdown.Constructor = Dropdown - /* DROPDOWN NO CONFLICT - * ==================== */ + /* DROPDOWN NO CONFLICT + * ==================== */ - $.fn.dropdown.noConflict = function () { + $.fn.dropdown.noConflict = function() { $.fn.dropdown = old return this } @@ -241,9 +241,9 @@ $(document) .on('click.dropdown.data-api', clearMenus) - .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + .on('click.dropdown.data-api', '.dropdown form', function(e) { e.stopPropagation() }) + .on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api', toggle + ', [role=menu]', Dropdown.prototype.keydown) }(window.jQuery); /* ========================================================= @@ -266,15 +266,15 @@ * ========================================================= */ -!function ($) { +!function($) { "use strict"; // jshint ;_; - /* MODAL CLASS DEFINITION - * ====================== */ + /* MODAL CLASS DEFINITION + * ====================== */ - var Modal = function (element, options) { + var Modal = function(element, options) { this.options = options this.$element = $(element) .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) @@ -283,170 +283,170 @@ Modal.prototype = { - constructor: Modal + constructor: Modal - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() - } + , toggle: function() { + return this[!this.isShown ? 'show' : 'hide']() + } - , show: function () { - var that = this - , e = $.Event('show') + , show: function() { + var that = this + , e = $.Event('show') - this.$element.trigger(e) + this.$element.trigger(e) - if (this.isShown || e.isDefaultPrevented()) return + if (this.isShown || e.isDefaultPrevented()) return - this.isShown = true + this.isShown = true - this.escape() + this.escape() - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') + this.backdrop(function() { + var transition = $.support.transition && that.$element.hasClass('fade') - if (!that.$element.parent().length) { - that.$element.appendTo(document.body) //don't move modals dom position - } + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } - that.$element.show() + that.$element.show() - if (transition) { - that.$element[0].offsetWidth // force reflow - } + if (transition) { + that.$element[0].offsetWidth // force reflow + } - that.$element - .addClass('in') - .attr('aria-hidden', false) + that.$element + .addClass('in') + .attr('aria-hidden', false) - that.enforceFocus() + that.enforceFocus() - transition ? - that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : - that.$element.focus().trigger('shown') + transition ? + that.$element.one($.support.transition.end, function() { that.$element.focus().trigger('shown') }) : + that.$element.focus().trigger('shown') + }) + } + + , hide: function(e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function() { + var that = this + $(document).on('focusin.modal', function(e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function() { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function(e) { + e.which == 27 && that.hide() }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') } + } - , hide: function (e) { - e && e.preventDefault() + , hideWithTransition: function() { + var that = this + , timeout = setTimeout(function() { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) - var that = this + this.$element.one($.support.transition.end, function() { + clearTimeout(timeout) + that.hideModal() + }) + } - e = $.Event('hide') + , hideModal: function() { + var that = this + this.$element.hide() + this.backdrop(function() { + that.removeBackdrop() + that.$element.trigger('hidden') + }) + } - this.$element.trigger(e) + , removeBackdrop: function() { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } - if (!this.isShown || e.isDefaultPrevented()) return + , backdrop: function(callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' - this.isShown = false + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate - this.escape() + this.$backdrop = $('