Merge branch 'main' into ifrost/rich-history-migration/frontend-storage-refactor

# Conflicts:
#	public/app/features/explore/Wrapper.tsx
This commit is contained in:
Piotr Jamróz
2022-02-04 12:01:23 +01:00
243 changed files with 2759 additions and 1522 deletions
+5 -5
View File
@@ -1094,7 +1094,7 @@ type: docker
---
depends_on: []
kind: pipeline
name: oss-build-publish-e2e-release
name: oss-build-e2e-publish-release
node:
type: no-parallel
platform:
@@ -1599,7 +1599,7 @@ volumes:
medium: memory
---
depends_on:
- oss-build-publish-e2e-release
- oss-build-e2e-publish-release
- oss-test-release
- oss-integration-tests-release
kind: pipeline
@@ -2858,7 +2858,7 @@ volumes:
---
depends_on: []
kind: pipeline
name: oss-build-publish-e2e-release-branch
name: oss-build-e2e-publish-release-branch
node:
type: no-parallel
platform:
@@ -3305,7 +3305,7 @@ volumes:
medium: memory
---
depends_on:
- oss-build-publish-e2e-release-branch
- oss-build-e2e-publish-release-branch
- oss-test-release-branch
- oss-integration-tests-release-branch
kind: pipeline
@@ -4191,6 +4191,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 4d1a5696bf1e510fb51a021c07e240c50cb913724ce08ed52cce037ff02dd8de
hmac: f26fc6de1d7ec3cf5608b70c851c8cf2b998e07abad8c52714e72ad072492387
...
+1
View File
@@ -8,6 +8,7 @@
"mode": "auto",
"program": "${workspaceFolder}/pkg/cmd/grafana-server/",
"env": {},
"cwd": "${workspaceFolder}",
"args": ["--homepath", "${workspaceFolder}", "--packaging", "dev"]
},
{
+20
View File
@@ -1,3 +1,23 @@
<!-- 8.4.0-beta1 START -->
# 8.4.0-beta1 (2022-02-02)
### Features and enhancements
- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath)
- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman)
- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ)
- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer)
- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark)
- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne)
- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi)
- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa)
- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv)
- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas)
- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90)
- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney)
<!-- 8.4.0-beta1 END -->
<!-- 8.3.4 START -->
# 8.3.4 (2022-01-17)
+5 -4
View File
@@ -15,10 +15,11 @@ Grafana 8.0 introduced new and improved alerting that centralizes alerting infor
Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature.
| Release | Cloud | Enterprise | OSS |
| ----------- | ------------- | ---------- | -------------------------------- |
| Grafana 8.2 | On by default | Opt-in | Opt-in |
| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* |
| Release | Cloud | Enterprise | OSS |
| ------------------------ | ------------- | ------------- | -------------------------------- |
| Grafana 8.2 | On by default | Opt-in | Opt-in |
| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* |
| Grafana 9.0 (unreleased) | On by default | On by default | On by default |
> **Note:** New installs include existing installs which do not have any alerts configured.
+1 -1
View File
@@ -7,7 +7,7 @@ weight = 114
Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "../unified-alerting/opt-in.md" >}}) feature.
> **Note**: Legacy dashboard alerts is deprecated and will be removed in a future release. We encourage you to migrate to [Grafana alerting]({{< relref "../unified-alerting/_index.md" >}}) 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/_index.md" >}}) for all existing installations.
Legacy dashboard alerts have two main components:
@@ -501,9 +501,3 @@ The org id of the datasource where the query data will be written.
If all `default_remote_write_*` properties are set, this information will be populated at startup. If a remote write target has
already been configured, nothing will happen.
## [feature_highlights]
### enabled
Whether the feature highlights feature is enabled
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
- [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}})
- [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}})
- [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}})
@@ -0,0 +1,23 @@
+++
title = "Release notes for Grafana 8.4.0-beta1"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.4.0-beta1
### Features and enhancements
- **Alerting:** Support WeCom as a contact point type. [#40975](https://github.com/grafana/grafana/pull/40975), [@smallpath](https://github.com/smallpath)
- **Alerting:** UI for mute timings. [#41578](https://github.com/grafana/grafana/pull/41578), [@nathanrodman](https://github.com/nathanrodman)
- **Alerting:** add settings for peer reconnection in HA mode. [#42300](https://github.com/grafana/grafana/pull/42300), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ)
- **Auth:** implement auto_sign_up for auth.jwt. [#37040](https://github.com/grafana/grafana/pull/37040), [@Roguelazer](https://github.com/Roguelazer)
- **Dashboard:** Add Show unknown variables toggle to dashboard settings. [#41854](https://github.com/grafana/grafana/pull/41854), [@hugohaggmark](https://github.com/hugohaggmark)
- **Instrumentation:** Logger migration from log15 to gokit/log. [#41636](https://github.com/grafana/grafana/pull/41636), [@ying-jeanne](https://github.com/ying-jeanne)
- **MSSQL:** Change regex to validate Provider connection string. [#40248](https://github.com/grafana/grafana/pull/40248), [@ianselmi](https://github.com/ianselmi)
- **MSSQL:** Configuration of certificate verification for TLS connection. [#31865](https://github.com/grafana/grafana/pull/31865), [@mortenaa](https://github.com/mortenaa)
- **Middleware:** Don't require HTTPS for HSTS headers to be emitted. [#35147](https://github.com/grafana/grafana/pull/35147), [@alexmv](https://github.com/alexmv)
- **Navigation:** Implement Keyboard Navigation. [#41618](https://github.com/grafana/grafana/pull/41618), [@axelavargas](https://github.com/axelavargas)
- **News:** Reload feed when changing the time range or refreshing. [#42217](https://github.com/grafana/grafana/pull/42217), [@ashharrison90](https://github.com/ashharrison90)
- **UI/Plot:** Implement keyboard controls for plot cursor. [#42244](https://github.com/grafana/grafana/pull/42244), [@kaydelaney](https://github.com/kaydelaney)
@@ -3,23 +3,23 @@ import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
describe('Variables - Constant', () => {
it('can add a new text box variable', () => {
it('can add a new constant variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Constant" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Constant{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type('pesto').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Constant{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.components.BackButton.backArrow().click({ force: true });
e2e.components.RefreshPicker.runButtonV2().click();
// Assert it was rendered
@@ -3,9 +3,9 @@ import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
function fillInCustomVariable(name: string, label: string, value: string) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type(name).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput().type(value).blur();
}
@@ -42,7 +42,7 @@ describe('Variables - Custom', () => {
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Custom{enter}');
// Set it's name, label, and content
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'One : 1,Two : 2, Three : 3');
@@ -50,7 +50,7 @@ describe('Variables - Custom', () => {
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.components.BackButton.backArrow().click({ force: true });
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click();
@@ -0,0 +1,37 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
describe('Variables - Datasource', () => {
it('can add a new datasource variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Datasource" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Data source{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
// If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus"
// Or, just update is to match some gdev datasources to test with :)
e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().type('Prometheus{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.eq(0)
.should('have.text', 'gdev-prometheus');
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.eq(1)
.should('have.text', 'gdev-slow-prometheus');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().click({ force: true });
e2e.components.RefreshPicker.runButtonV2().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('gdev-slow-prometheus').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus');
});
});
@@ -0,0 +1,41 @@
import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
function assertPreviewValues(expectedValues: string[]) {
for (const expected of expectedValues) {
const index = expectedValues.indexOf(expected);
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(index).should('have.text', expected);
}
}
describe('Variables - Interval', () => {
it('can add a new interval variable', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Interval" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Interval{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput()
.clear()
.type('10s,10m,60m,90m,1h30m')
.blur();
assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']);
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().click({ force: true });
e2e.components.RefreshPicker.runButtonV2().click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m');
});
});
+13 -13
View File
@@ -9,18 +9,18 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('name');
expect(input.val()).equals('query0');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'Query');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('optional display name');
@@ -33,7 +33,7 @@ describe('Variables - Query - Add variable', () => {
expect(input.attr('placeholder')).equals('descriptive text');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', '');
@@ -45,19 +45,19 @@ describe('Variables - Query - Add variable', () => {
e2e.components.Select.singleValue().should('have.text', 'gdev-testdata');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect()
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'On dashboard load');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput()
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.within((input) => {
const placeholder = '/.*-(?<text>.*)-(?<value>.*)-.*/';
expect(input.attr('placeholder')).equals(placeholder);
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelect()
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'Disabled');
@@ -66,7 +66,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch().should('not.be.checked');
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().should('not.exist');
});
it('adding a single value query variable', () => {
@@ -75,7 +75,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.clear()
.type('a label');
@@ -89,7 +89,7 @@ describe('Variables - Query - Add variable', () => {
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput()
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.type('/.*C.*/')
.blur();
@@ -125,7 +125,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.clear()
.type('a label');
@@ -139,7 +139,7 @@ describe('Variables - Query - Add variable', () => {
.type('*')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput()
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.type('/.*C.*/')
.blur();
@@ -152,7 +152,7 @@ describe('Variables - Query - Add variable', () => {
.click({ force: true })
.should('be.checked');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().within((input) => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().within((input) => {
expect(input.attr('placeholder')).equals('blank = auto');
expect(input.val()).equals('');
});
@@ -7,19 +7,19 @@ describe('Variables - Text box', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
// Create a new "Custom" variable
// Create a new "text box" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().type('Text box{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput().type('cat-dog').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Text box{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog');
// Navigate back to the homepage and change the selected variable value
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
e2e.components.BackButton.backArrow().should('be.visible').click({ force: true });
e2e.components.BackButton.backArrow().click({ force: true });
e2e().get('#VariableUnderTest').clear().type('dog-cat').blur();
// Assert it was rendered
@@ -201,7 +201,7 @@ function validateVariable(value: string) {
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput()
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2()
.should('be.visible')
.should('have.value', value);
}
@@ -234,7 +234,7 @@ function changeQueryInput() {
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text').should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput()
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2()
.should('be.visible')
.clear()
.type('changed value')
+60 -56
View File
@@ -1,11 +1,14 @@
<!-- The template body needs to be wrapped in div to prevent Premailer from adding </tr></td> tags which breaks the HTML structure -->
<div>
[[Subject .Subject "[[.Title]]"]]
[[ define "alert" ]]
<tr>
<td colspan="2" class="value">
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
</td>
</tr>
<tr>
<td colspan="2" class="value">
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
</td>
</tr>
[[ if gt (len .Annotations.SortedPairs) 0 ]]
<tr>
<td colspan="2" class="annotations">
@@ -187,62 +190,63 @@
<tr>
<td class="twelve">
<table>
[[ if gt (len .Alerts.Firing) 0 ]]
<tr>
<td colspan="2" class="section-heading">
Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
[[ range .GroupLabels.SortedPairs ]]
[[ .Name ]]=[[ .Value ]]
[[ end ]][[ end ]]
</td>
</tr>
[[ range .Alerts.Firing ]]
[[ if gt (len .Alerts.Firing) 0 ]]
<tr>
<td colspan="2" class="section-heading">
Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
[[ range .GroupLabels.SortedPairs ]]
[[ .Name ]]=[[ .Value ]]
[[ end ]][[ end ]]
</td>
</tr>
[[ range .Alerts.Firing ]]
<tr>
<td
class="status-tag status-firing"
width="68"
>
Firing
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
[[ if gt (len .Alerts.Resolved) 0 ]]
<tr>
<td colspan="2" class="section-heading">
Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
[[ range .GroupLabels.SortedPairs ]]
[[ .Name ]]=[[ .Value ]]
[[ end ]][[ end ]]
</td>
</tr>
[[ range .Alerts.Resolved ]]
<tr>
<td
class="status-tag status-resolved"
width="68"
>
Resolved
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
<tr>
<td
class="status-tag status-firing"
width="68"
>
Firing
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
<td colspan="2">
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
[[ if gt (len .Alerts.Resolved) 0 ]]
<tr>
<td colspan="2" class="section-heading">
Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
[[ range .GroupLabels.SortedPairs ]]
[[ .Name ]]=[[ .Value ]]
[[ end ]][[ end ]]
</td>
</tr>
[[ range .Alerts.Resolved ]]
<tr>
<td
class="status-tag status-resolved"
width="68"
>
Resolved
</td>
<td class="alert-label">
[[ .Labels.alertname ]]
</td>
</tr>
[[ template "alert" . ]]
[[ end ]]
[[ end ]]
<tr>
<td colspan="2">
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
</td>
</tr>
</table>
</table>
</td>
</tr>
</table>
[[ end ]]
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
{
"stable": "8.3.4",
"testing": "8.3.4"
"testing": "8.4.0-beta1"
}
+1 -1
View File
@@ -2,5 +2,5 @@
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"version": "8.4.0-pre"
"version": "8.5.0-pre"
}
+2 -2
View File
@@ -3,7 +3,7 @@
"license": "AGPL-3.0-only",
"private": true,
"name": "grafana",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"repository": "github:grafana/grafana",
"scripts": {
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
@@ -35,7 +35,7 @@
"packages:typecheck": "lerna run typecheck",
"packages:clean": "lerna run clean",
"precommit": "yarn run lint-staged",
"prettier:check": "prettier --list-different \"**/*.{scss,md,mdx}\"",
"prettier:check": "prettier --check --list-different=false --loglevel=warn \"**/*.{scss,md,mdx}\"",
"prettier:write": "prettier --list-different \"**/*.{scss,md,mdx}\" --write",
"start": "yarn themes:generate && yarn dev --watch",
"start:noTsCheck": "yarn start --env noTsCheck=1",
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/data",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana Data Library",
"keywords": [
"typescript"
@@ -22,7 +22,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "5.0.2",
"@grafana/schema": "8.4.0-pre",
"@grafana/schema": "8.5.0-pre",
"@types/d3-interpolate": "^1.4.0",
"d3-interpolate": "1.4.0",
"date-fns": "2.28.0",
@@ -36,4 +36,5 @@ export interface FeatureToggles {
showFeatureFlagsInUI?: boolean;
disable_http_request_histogram?: boolean;
validatedQueries?: boolean;
featureHighlights?: boolean;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e-selectors",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana End-to-End Test Selectors Library",
"keywords": [
"cli",
@@ -110,20 +110,28 @@ export const Pages = {
*/
modeLabelEdit: 'Variable editor Header mode Edit',
generalNameInput: 'Variable editor Form Name field',
generalNameInputV2: 'data-testid Variable editor Form Name field',
generalTypeSelect: 'Variable editor Form Type select',
generalTypeSelectV2: 'data-testid Variable editor Form Type select',
generalLabelInput: 'Variable editor Form Label field',
generalLabelInputV2: 'data-testid Variable editor Form Label field',
generalHideSelect: 'Variable editor Form Hide select',
generalHideSelectV2: 'data-testid Variable editor Form Hide select',
selectionOptionsMultiSwitch: 'Variable editor Form Multi switch',
selectionOptionsIncludeAllSwitch: 'Variable editor Form IncludeAll switch',
selectionOptionsCustomAllInput: 'Variable editor Form IncludeAll field',
selectionOptionsCustomAllInputV2: 'data-testid Variable editor Form IncludeAll field',
previewOfValuesOption: 'Variable editor Preview of Values option',
submitButton: 'Variable editor Submit button',
},
QueryVariable: {
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
queryOptionsSortSelectV2: 'data-testid Variable editor Form Query Sort select',
queryOptionsQueryInput: 'Variable editor Form Default Variable Query Editor textarea',
valueGroupsTagsEnabledSwitch: 'Variable editor Form Query UseTags switch',
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
@@ -131,13 +139,21 @@ export const Pages = {
},
ConstantVariable: {
constantOptionsQueryInput: 'Variable editor Form Constant Query field',
constantOptionsQueryInputV2: 'data-testid Variable editor Form Constant Query field',
},
DatasourceVariable: {
datasourceSelect: 'data-testid datasource variable datasource type',
},
TextBoxVariable: {
textBoxOptionsQueryInput: 'Variable editor Form TextBox Query field',
textBoxOptionsQueryInputV2: 'data-testid Variable editor Form TextBox Query field',
},
CustomVariable: {
customValueInput: 'data-testid custom-variable-input',
},
IntervalVariable: {
intervalsValueInput: 'data-testid interval variable intervals input',
},
},
},
},
+2 -2
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/e2e",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana End-to-End Test Library",
"keywords": [
"cli",
@@ -48,7 +48,7 @@
"@babel/core": "7.16.7",
"@babel/preset-env": "7.16.7",
"@cypress/webpack-preprocessor": "5.11.0",
"@grafana/e2e-selectors": "8.4.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/tsconfig": "^1.0.0-rc1",
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "8.2.3",
@@ -162,7 +162,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
// This field is key to many reactive changes
if (type !== VARIABLE_TYPE_QUERY) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect()
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
.should('be.visible')
.within(() => {
e2e.components.Select.singleValue().should('have.text', 'Query').click();
@@ -171,10 +171,10 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
}
if (label) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label);
}
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().clear().type(name);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name);
if (
dataSource &&
@@ -188,7 +188,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
}
if (constantValue && type === VARIABLE_TYPE_CONSTANT) {
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(constantValue);
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type(constantValue);
}
if (type === VARIABLE_TYPE_QUERY) {
@@ -197,7 +197,7 @@ const addVariable = (config: PartialAddVariableConfig, isFirst: boolean): AddVar
}
if (regex) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput().type(regex);
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2().type(regex);
}
}
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/runtime",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana Runtime Library",
"keywords": [
"grafana",
@@ -22,9 +22,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "8.4.0-pre",
"@grafana/e2e-selectors": "8.4.0-pre",
"@grafana/ui": "8.4.0-pre",
"@grafana/data": "8.5.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/ui": "8.5.0-pre",
"@sentry/browser": "6.17.2",
"history": "4.10.1",
"lodash": "4.17.21",
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/schema",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana Schema Library",
"keywords": [
"typescript"
+3 -3
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/toolkit",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana Toolkit",
"keywords": [
"grafana",
@@ -28,10 +28,10 @@
"dependencies": {
"@babel/core": "7.13.14",
"@babel/preset-env": "7.13.12",
"@grafana/data": "8.4.0-pre",
"@grafana/data": "8.5.0-pre",
"@grafana/eslint-config": "2.5.2",
"@grafana/tsconfig": "^1.0.0-rc1",
"@grafana/ui": "8.4.0-pre",
"@grafana/ui": "8.5.0-pre",
"@jest/core": "26.6.3",
"@rushstack/eslint-patch": "1.0.6",
"@types/command-exists": "^1.2.0",
+4 -4
View File
@@ -2,7 +2,7 @@
"author": "Grafana Labs",
"license": "Apache-2.0",
"name": "@grafana/ui",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"description": "Grafana Components Library",
"keywords": [
"grafana",
@@ -33,9 +33,9 @@
"@emotion/css": "11.7.1",
"@emotion/react": "11.7.1",
"@grafana/aws-sdk": "0.0.31",
"@grafana/data": "8.4.0-pre",
"@grafana/e2e-selectors": "8.4.0-pre",
"@grafana/schema": "8.4.0-pre",
"@grafana/data": "8.5.0-pre",
"@grafana/e2e-selectors": "8.5.0-pre",
"@grafana/schema": "8.5.0-pre",
"@grafana/slate-react": "0.22.10-grafana",
"@monaco-editor/react": "4.3.1",
"@popperjs/core": "2.11.2",
@@ -18,6 +18,7 @@ import {
import { Field } from '../../Forms/Field';
import { getInputStyles, Input } from '../../Input/Input';
import { Icon } from '../../Icon/Icon';
import { Tooltip } from '../../Tooltip/Tooltip';
/**
* @internal
@@ -115,14 +116,13 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
</CustomScrollbar>
<div className={styles.rightSide}>
<div className={styles.title}>
<TimePickerTitle>Specify time range</TimePickerTitle>
<div className={styles.description}>
Specify a relative time range, for more information see{' '}
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/">
docs <Icon name="external-link-alt" />
</a>
.
</div>
<TimePickerTitle>
<Tooltip content={<TooltipContent />} placement="bottom" theme="info">
<div>
Specify time range <Icon name="info-circle" />
</div>
</Tooltip>
</TimePickerTitle>
</div>
<Field label="From" invalid={!from.validation.isValid} error={from.validation.errorMessage}>
<Input
@@ -152,6 +152,38 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): Re
);
}
const TooltipContent = () => {
const styles = useStyles2(toolTipStyles);
return (
<>
<div className={styles.supported}>
Supported formats: <code className={styles.tooltip}>now-[digit]s/m/h/d/w</code>
</div>
<div>Example: to select a time range from 10 minutes ago to now</div>
<code className={styles.tooltip}>From: now-10m To: now</code>
<div className={styles.link}>
For more information see{' '}
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/">
docs <Icon name="external-link-alt" />
</a>
.
</div>
</>
);
};
const toolTipStyles = (theme: GrafanaTheme2) => ({
supported: css`
margin-bottom: ${theme.spacing(1)};
`,
tooltip: css`
margin: 0;
`,
link: css`
margin-top: ${theme.spacing(1)};
`,
});
const getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme2) => {
const inputStyles = getInputStyles({ theme, invalid: false });
const bodyMinimumHeight = 250;
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@jaegertracing/jaeger-ui-components",
"version": "8.4.0-pre",
"version": "8.5.0-pre",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@emotion/css": "11.7.1",
"@grafana/data": "8.4.0-pre",
"@grafana/ui": "8.4.0-pre",
"@grafana/data": "8.5.0-pre",
"@grafana/ui": "8.5.0-pre",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
+10 -10
View File
@@ -13,7 +13,6 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
"github.com/grafana/grafana/pkg/services/featuremgmt"
sa "github.com/grafana/grafana/pkg/services/serviceaccounts/manager"
)
var plog = log.New("api")
@@ -58,8 +57,9 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index)
r.Get("/org/users/new", reqOrgAdmin, hs.Index)
r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index)
r.Get("/org/teams", reqCanAccessTeams, hs.Index)
r.Get("/org/teams/*", reqCanAccessTeams, hs.Index)
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index)
r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index)
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
@@ -127,8 +127,8 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/user/password/send-reset-email", reqNotSignedIn, hs.Index)
r.Get("/user/password/reset", hs.Index)
r.Post("/api/user/password/send-reset-email", routing.Wrap(SendResetPasswordEmail))
r.Post("/api/user/password/reset", routing.Wrap(ResetPassword))
r.Post("/api/user/password/send-reset-email", routing.Wrap(hs.SendResetPasswordEmail))
r.Post("/api/user/password/reset", routing.Wrap(hs.ResetPassword))
// dashboard snapshots
r.Get("/dashboard/snapshot/*", reqNoAuth, hs.Index)
@@ -256,10 +256,10 @@ func (hs *HTTPServer) registerRoutes() {
// auth api keys
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
keysRoute.Get("/", authorize(reqOrgAdmin, sa.ActionApikeyListEv), routing.Wrap(GetAPIKeys))
keysRoute.Post("/", authorize(reqOrgAdmin, sa.ActionApikeyAddEv), quota("api_key"), routing.Wrap(hs.AddAPIKey))
keysRoute.Post("/additional", authorize(reqOrgAdmin, sa.ActionApikeyAddAdditionalEv), quota("api_key"), routing.Wrap(hs.AdditionalAPIKey))
keysRoute.Delete("/:id", authorize(reqOrgAdmin, sa.ActionApikeyRemoveEv), routing.Wrap(DeleteAPIKey))
keysRoute.Get("/", routing.Wrap(GetAPIKeys))
keysRoute.Post("/", quota("api_key"), routing.Wrap(hs.AddAPIKey))
keysRoute.Post("/additional", quota("api_key"), routing.Wrap(hs.AdditionalAPIKey))
keysRoute.Delete("/:id", routing.Wrap(DeleteAPIKey))
}, reqOrgAdmin)
// Preferences
@@ -373,7 +373,7 @@ func (hs *HTTPServer) registerRoutes() {
// Search
apiRoute.Get("/search/sorting", routing.Wrap(hs.ListSortOptions))
apiRoute.Get("/search/", routing.Wrap(Search))
apiRoute.Get("/search/", routing.Wrap(hs.Search))
// metrics
apiRoute.Post("/tsdb/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), routing.Wrap(hs.QueryMetrics))
+4 -3
View File
@@ -311,9 +311,10 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
cfg.IsFeatureToggleEnabled = features.IsEnabled
return &HTTPServer{
Cfg: cfg,
Features: features,
Bus: bus.GetBus(),
Cfg: cfg,
Features: features,
Bus: bus.GetBus(),
AccessControl: accesscontrolmock.New().WithDisabled(),
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro
}))
origHandler := frontendLogger.GetLogger()
frontendLogger.AddLogger(newfrontendLogger, "info", map[string]level.Option{})
frontendLogger.SetLogger(level.NewFilter(newfrontendLogger, level.AllowInfo()))
sourceMapReads := []SourceMapReadRecord{}
t.Cleanup(func() {
-3
View File
@@ -273,9 +273,6 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"enabled": hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true),
},
"unifiedAlertingEnabled": hs.Cfg.UnifiedAlerting.Enabled,
"featureHighlights": map[string]bool{
"enabled": hs.SettingsProvider.Section("feature_highlights").KeyValue("enabled").MustBool(false),
},
}
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
+6 -3
View File
@@ -44,8 +44,8 @@ import (
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory"
@@ -127,8 +127,9 @@ type HTTPServer struct {
teamGuardian teamguardian.TeamGuardian
queryDataService *query.Service
serviceAccountsService serviceaccounts.Service
authInfoService authinfoservice.Service
authInfoService login.AuthInfoService
TeamPermissionsService *resourcepermissions.Service
NotificationService *notifications.NotificationService
}
type ServerOptions struct {
@@ -155,7 +156,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
dataSourcesService *datasources.Service, secretsService secrets.Service, queryDataService *query.Service,
ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
authInfoService authinfoservice.Service, resourcePermissionServices *resourceservices.ResourceServices) (*HTTPServer, error) {
authInfoService login.AuthInfoService, resourcePermissionServices *resourceservices.ResourceServices,
notificationService *notifications.NotificationService) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -215,6 +217,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
serviceAccountsService: serviceaccountsService,
authInfoService: authInfoService,
TeamPermissionsService: resourcePermissionServices.GetTeamService(),
NotificationService: notificationService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+4 -5
View File
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/navlinks"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -148,7 +147,7 @@ func enableServiceAccount(hs *HTTPServer, c *models.ReqContext) bool {
hs.Features.IsEnabled(featuremgmt.FlagServiceAccounts)
}
func enableTeams(hs *HTTPServer, c *models.ReqContext) bool {
func (hs *HTTPServer) ReqCanAdminTeams(c *models.ReqContext) bool {
return c.OrgRole == models.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == models.ROLE_EDITOR)
}
@@ -263,7 +262,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
if enableTeams(hs, c) {
if hasAccess(hs.ReqCanAdminTeams, teamsAccessEvaluator) {
configNodes = append(configNodes, &dtos.NavLink{
Text: "Teams",
Id: "teams",
@@ -540,7 +539,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) {
hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
if err := bus.Dispatch(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil {
return nil, err
}
hasEditPerm := hasEditPermissionInFoldersQuery.Result
@@ -553,7 +552,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
settings["dateFormats"] = hs.Cfg.DateFormats
prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
if err := bus.Dispatch(c.Req.Context(), &prefsQuery); err != nil {
if err := hs.SQLStore.GetPreferencesWithDefaults(c.Req.Context(), &prefsQuery); err != nil {
return nil, err
}
prefs := prefsQuery.Result
+13 -7
View File
@@ -1,19 +1,19 @@
package api
import (
"context"
"errors"
"net/http"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func SendResetPasswordEmail(c *models.ReqContext) response.Response {
func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Response {
form := dtos.SendResetPasswordEmailForm{}
if err := web.Bind(c.Req, &form); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -27,27 +27,33 @@ func SendResetPasswordEmail(c *models.ReqContext) response.Response {
userQuery := models.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
if err := bus.Dispatch(c.Req.Context(), &userQuery); err != nil {
if err := hs.SQLStore.GetUserByLogin(c.Req.Context(), &userQuery); err != nil {
c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
return response.Error(200, "Email sent", err)
}
emailCmd := models.SendResetPasswordEmailCommand{User: userQuery.Result}
if err := bus.Dispatch(c.Req.Context(), &emailCmd); err != nil {
if err := hs.NotificationService.SendResetPasswordEmail(c.Req.Context(), &emailCmd); err != nil {
return response.Error(500, "Failed to send email", err)
}
return response.Success("Email sent")
}
func ResetPassword(c *models.ReqContext) response.Response {
func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response {
form := dtos.ResetUserPasswordForm{}
if err := web.Bind(c.Req, &form); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
query := models.ValidateResetPasswordCodeQuery{Code: form.Code}
if err := bus.Dispatch(c.Req.Context(), &query); err != nil {
getUserByLogin := func(ctx context.Context, login string) (*models.User, error) {
userQuery := models.GetUserByLoginQuery{LoginOrEmail: login}
err := hs.SQLStore.GetUserByLogin(ctx, &userQuery)
return userQuery.Result, err
}
if err := hs.NotificationService.ValidateResetPasswordCode(c.Req.Context(), &query, getUserByLogin); err != nil {
if errors.Is(err, models.ErrInvalidEmailCode) {
return response.Error(400, "Invalid or expired reset password code", nil)
}
@@ -66,7 +72,7 @@ func ResetPassword(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to encode password", err)
}
if err := bus.Dispatch(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.ChangeUserPassword(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to change user password", err)
}
+19
View File
@@ -298,3 +298,22 @@ var orgsCreateAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(ActionOrgsRead),
accesscontrol.EvalPermission(ActionOrgsCreate),
)
// teamsAccessEvaluator is used to protect the "Configuration > Teams" page access
var teamsAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsCreate),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
),
)
// teamsEditAccessEvaluator is used to protect the "Configuration > Teams > edit" page access
var teamsEditAccessEvaluator = accesscontrol.EvalAll(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsRead),
accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite),
accesscontrol.EvalPermission(accesscontrol.ActionTeamsPermissionsWrite),
),
)
+2 -3
View File
@@ -7,13 +7,12 @@ import (
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
func Search(c *models.ReqContext) response.Response {
func (hs *HTTPServer) Search(c *models.ReqContext) response.Response {
query := c.Query("query")
tags := c.QueryStrings("tag")
starred := c.Query("starred")
@@ -62,7 +61,7 @@ func Search(c *models.ReqContext) response.Response {
Sort: sort,
}
err := bus.Dispatch(c.Req.Context(), &searchQuery)
err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery)
if err != nil {
return response.Error(500, "Search failed", err)
}
+45
View File
@@ -2,12 +2,14 @@ package api
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -103,6 +105,20 @@ func (hs *HTTPServer) DeleteTeamByID(c *models.ReqContext) response.Response {
return response.Success("Team deleted")
}
func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamIDs map[string]bool) (map[string]accesscontrol.Metadata, error) {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
return nil, err
}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs), nil
}
// GET /api/teams/search
func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
perPage := c.QueryInt("perpage")
@@ -134,8 +150,17 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to search Teams", err)
}
teamIDs := map[string]bool{}
for _, team := range query.Result.Teams {
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
teamIDs[strconv.FormatInt(team.Id, 10)] = true
}
metadata, err := hs.getTeamsAccessControlMetadata(c, teamIDs)
if err == nil && len(metadata) != 0 {
for _, team := range query.Result.Teams {
team.AccessControl = metadata[strconv.FormatInt(team.Id, 10)]
}
}
query.Result.Page = page
@@ -144,6 +169,23 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
return response.JSON(200, query.Result)
}
func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID int64) (accesscontrol.Metadata, error) {
if hs.AccessControl.IsDisabled() || !c.QueryBool("accesscontrol") {
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err)
return nil, err
}
key := fmt.Sprintf("%d", teamID)
teamIDs := map[string]bool{key: true}
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil
}
// GET /api/teams/:teamId
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
@@ -165,6 +207,9 @@ func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
return response.Error(500, "Failed to get Team", err)
}
metadata, _ := hs.getTeamAccessControlMetadata(c, query.Result.Id)
query.Result.AccessControl = metadata
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
return response.JSON(200, &query.Result)
}
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
@@ -152,7 +153,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
}
teamIDString := strconv.FormatInt(teamId, 10)
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, userId, teamIDString, ""); err != nil {
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
if errors.Is(err, models.ErrTeamNotFound) {
return response.Error(404, "Team not found", nil)
}
@@ -171,7 +172,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
// Stubbable by tests.
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error {
teamIDString := strconv.FormatInt(teamID, 10)
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, userID, teamIDString, permission); err != nil {
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil {
return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err)
}
return nil
+3 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
@@ -46,7 +47,8 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
loggedInUserScenario(t, "When calling GET on", "api/users/1", "api/users/:id", func(sc *scenarioContext) {
fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC)
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
srv := authinfoservice.ProvideAuthInfoService(bus.New(), sqlStore, &authinfoservice.OSSUserProtectionImpl{}, secretsService)
authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, bus.New(), secretsService)
srv := authinfoservice.ProvideAuthInfoService(&authinfoservice.OSSUserProtectionImpl{}, authInfoStore)
hs.authInfoService = srv
createUserCmd := models.CreateUserCommand{
+25
View File
@@ -0,0 +1,25 @@
package log
import gokitlog "github.com/go-kit/log"
type compositeLogger struct {
loggers []gokitlog.Logger
}
func newCompositeLogger(loggers ...gokitlog.Logger) *compositeLogger {
if len(loggers) == 0 {
loggers = []gokitlog.Logger{}
}
return &compositeLogger{loggers: loggers}
}
func (l *compositeLogger) Log(keyvals ...interface{}) error {
for _, logger := range l.loggers {
if err := logger.Log(keyvals...); err != nil {
return err
}
}
return nil
}
+1 -1
View File
@@ -12,7 +12,7 @@ const (
type Logger interface {
// New returns a new Logger that has this logger's context plus the given context
New(ctx ...interface{}) MultiLoggers
New(ctx ...interface{}) *ConcreteLogger
Log(keyvals ...interface{}) error
+224 -115
View File
@@ -7,9 +7,12 @@ package log
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
gokitlog "github.com/go-kit/log"
@@ -25,8 +28,7 @@ import (
var loggersToClose []DisposableHandler
var loggersToReload []ReloadableHandler
var filters map[string]level.Option
var Root MultiLoggers
var root *logManager
const (
// top 7 calls in the stack are within logger
@@ -37,11 +39,212 @@ const (
func init() {
loggersToClose = make([]DisposableHandler, 0)
loggersToReload = make([]ReloadableHandler, 0)
filters = map[string]level.Option{}
// Use console by default
format := getLogFormat("console")
Root.AddLogger(format(os.Stderr), "info", filters)
logger := level.NewFilter(format(os.Stderr), level.AllowInfo())
root = newManager(logger)
}
// logManager manage loggers
type logManager struct {
*ConcreteLogger
loggersByName map[string]*ConcreteLogger
logFilters []LogWithFilters
mutex sync.RWMutex
}
func newManager(logger gokitlog.Logger) *logManager {
return &logManager{
ConcreteLogger: newConcreteLogger(logger),
loggersByName: map[string]*ConcreteLogger{},
}
}
func (lm *logManager) initialize(loggers []LogWithFilters) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
defaultLoggers := make([]gokitlog.Logger, len(loggers))
for index, logger := range loggers {
defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel)
}
lm.ConcreteLogger.SetLogger(&compositeLogger{loggers: defaultLoggers})
lm.logFilters = loggers
loggersByName := []string{}
for k := range lm.loggersByName {
loggersByName = append(loggersByName, k)
}
sort.Strings(loggersByName)
for _, name := range loggersByName {
ctxLoggers := make([]gokitlog.Logger, len(loggers))
for index, logger := range loggers {
if filterLevel, exists := logger.filters[name]; !exists {
ctxLoggers[index] = level.NewFilter(logger.val, logger.maxLevel)
} else {
ctxLoggers[index] = level.NewFilter(logger.val, filterLevel)
}
}
lm.loggersByName[name].SetLogger(&compositeLogger{loggers: ctxLoggers})
}
}
func (lm *logManager) SetLogger(logger gokitlog.Logger) {
lm.ConcreteLogger.SetLogger(logger)
}
func (lm *logManager) GetLogger() gokitlog.Logger {
return lm.ConcreteLogger.GetLogger()
}
func (lm *logManager) Log(args ...interface{}) error {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
if err := lm.ConcreteLogger.Log(args...); err != nil {
log.Println("Logging error", "error", err)
}
return nil
}
func (lm *logManager) New(ctx ...interface{}) *ConcreteLogger {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if len(ctx) == 0 {
return lm.ConcreteLogger
}
loggerName, ok := ctx[0].(string)
if !ok {
return lm.ConcreteLogger
}
if logger, exists := lm.loggersByName[loggerName]; exists {
return logger
}
ctx = append([]interface{}{"logger"}, ctx...)
if len(lm.logFilters) == 0 {
ctxLogger := newConcreteLogger(lm.logger, ctx...)
lm.loggersByName[loggerName] = ctxLogger
return ctxLogger
}
compositeLogger := newCompositeLogger()
for _, logWithFilter := range lm.logFilters {
filterLevel, ok := logWithFilter.filters[loggerName]
if ok {
logWithFilter.val = level.NewFilter(logWithFilter.val, filterLevel)
} else {
logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel)
}
compositeLogger.loggers = append(compositeLogger.loggers, logWithFilter.val)
}
ctxLogger := newConcreteLogger(compositeLogger, ctx...)
lm.loggersByName[loggerName] = ctxLogger
return ctxLogger
}
type ConcreteLogger struct {
ctx []interface{}
logger gokitlog.Logger
mutex sync.RWMutex
}
func newConcreteLogger(logger gokitlog.Logger, ctx ...interface{}) *ConcreteLogger {
if len(ctx) == 0 {
ctx = []interface{}{}
} else {
logger = gokitlog.With(logger, ctx...)
}
return &ConcreteLogger{
ctx: ctx,
logger: logger,
}
}
func (cl *ConcreteLogger) SetLogger(logger gokitlog.Logger) {
cl.mutex.Lock()
cl.logger = gokitlog.With(logger, cl.ctx...)
cl.mutex.Unlock()
}
func (cl *ConcreteLogger) GetLogger() gokitlog.Logger {
cl.mutex.Lock()
defer cl.mutex.Unlock()
return cl.logger
}
func (cl *ConcreteLogger) Warn(msg string, args ...interface{}) {
_ = cl.log(msg, level.WarnValue(), args...)
}
func (cl *ConcreteLogger) Debug(msg string, args ...interface{}) {
// args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...)
_ = cl.log(msg, level.DebugValue(), args...)
}
func (cl *ConcreteLogger) Error(msg string, args ...interface{}) {
_ = cl.log(msg, level.ErrorValue(), args...)
}
func (cl *ConcreteLogger) Info(msg string, args ...interface{}) {
_ = cl.log(msg, level.InfoValue(), args...)
}
func (cl *ConcreteLogger) log(msg string, logLevel level.Value, args ...interface{}) error {
cl.mutex.RLock()
logger := gokitlog.With(cl.logger, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700"))
cl.mutex.RUnlock()
args = append([]interface{}{level.Key(), logLevel, "msg", msg}, args...)
return logger.Log(args...)
}
func (cl *ConcreteLogger) Log(keyvals ...interface{}) error {
cl.mutex.RLock()
defer cl.mutex.RUnlock()
return cl.logger.Log(keyvals...)
}
func (cl *ConcreteLogger) New(ctx ...interface{}) *ConcreteLogger {
if len(ctx) == 0 {
root.New()
}
keyvals := []interface{}{}
if len(cl.ctx)%2 == 1 {
cl.ctx = append(cl.ctx, nil)
}
for i := 0; i < len(cl.ctx); i += 2 {
k, v := cl.ctx[i], cl.ctx[i+1]
if k == "logger" {
continue
}
keyvals = append(keyvals, k, v)
}
keyvals = append(keyvals, ctx...)
return root.New(keyvals...)
}
func New(ctx ...interface{}) *ConcreteLogger {
return root.New(ctx...)
}
type LogWithFilters struct {
@@ -50,111 +253,23 @@ type LogWithFilters struct {
maxLevel level.Option
}
type MultiLoggers struct {
loggers []LogWithFilters
}
func (ml *MultiLoggers) AddLogger(val gokitlog.Logger, levelName string, filters map[string]level.Option) {
logger := LogWithFilters{val: val, filters: filters, maxLevel: getLogLevelFromString(levelName)}
ml.loggers = append(ml.loggers, logger)
}
func (ml *MultiLoggers) SetLogger(des MultiLoggers) {
ml.loggers = des.loggers
}
func (ml *MultiLoggers) GetLogger() MultiLoggers {
return *ml
}
func (ml MultiLoggers) Warn(msg string, args ...interface{}) {
args = append([]interface{}{level.Key(), level.WarnValue(), "msg", msg}, args...)
err := ml.Log(args...)
if err != nil {
_ = level.Error(Root).Log("Logging error", "error", err)
}
}
func (ml MultiLoggers) Debug(msg string, args ...interface{}) {
args = append([]interface{}{level.Key(), level.DebugValue(), "msg", msg}, args...)
err := ml.Log(args...)
if err != nil {
_ = level.Error(Root).Log("Logging error", "error", err)
}
}
func (ml MultiLoggers) Error(msg string, args ...interface{}) {
args = append([]interface{}{level.Key(), level.ErrorValue(), "msg", msg}, args...)
err := ml.Log(args...)
if err != nil {
_ = level.Error(Root).Log("Logging error", "error", err)
}
}
func (ml MultiLoggers) Info(msg string, args ...interface{}) {
args = append([]interface{}{level.Key(), level.InfoValue(), "msg", msg}, args...)
err := ml.Log(args...)
if err != nil {
_ = level.Error(Root).Log("Logging error", "error", err)
}
}
func (ml MultiLoggers) Log(keyvals ...interface{}) error {
for _, multilogger := range ml.loggers {
multilogger.val = gokitlog.With(multilogger.val, "t", gokitlog.TimestampFormat(time.Now, "2006-01-02T15:04:05.99-0700"))
if err := multilogger.val.Log(keyvals...); err != nil {
return err
}
}
return nil
}
// New creates a new logger from the existing one with additional context
func (ml MultiLoggers) New(ctx ...interface{}) MultiLoggers {
return with(ml, gokitlog.With, ctx)
}
// New creates MultiLoggers with the provided context and caller that is added as a suffix.
// The first element of the context must be the logger name
func New(ctx ...interface{}) MultiLoggers {
func with(ctxLogger *ConcreteLogger, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) *ConcreteLogger {
if len(ctx) == 0 {
return Root
return ctxLogger
}
var newloger MultiLoggers
ctx = append([]interface{}{"logger"}, ctx...)
for _, logWithFilter := range Root.loggers {
logWithFilter.val = gokitlog.With(logWithFilter.val, ctx...)
v, ok := logWithFilter.filters[ctx[0].(string)]
if ok {
logWithFilter.val = level.NewFilter(logWithFilter.val, v)
} else {
logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel)
}
newloger.loggers = append(newloger.loggers, logWithFilter)
}
return newloger
}
func with(loggers MultiLoggers, withFunc func(gokitlog.Logger, ...interface{}) gokitlog.Logger, ctx []interface{}) MultiLoggers {
if len(ctx) == 0 {
return loggers
}
var newloger MultiLoggers
for _, l := range loggers.loggers {
l.val = withFunc(l.val, ctx...)
newloger.loggers = append(newloger.loggers, l)
}
return newloger
ctxLogger.logger = withFunc(ctxLogger.logger, ctx...)
return ctxLogger
}
// WithPrefix adds context that will be added to the log message
func WithPrefix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers {
return with(loggers, gokitlog.WithPrefix, ctx)
func WithPrefix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger {
return with(ctxLogger, gokitlog.WithPrefix, ctx)
}
// WithSuffix adds context that will be appended at the end of the log message
func WithSuffix(loggers MultiLoggers, ctx ...interface{}) MultiLoggers {
return with(loggers, gokitlog.WithSuffix, ctx)
func WithSuffix(ctxLogger *ConcreteLogger, ctx ...interface{}) *ConcreteLogger {
return with(ctxLogger, gokitlog.WithSuffix, ctx)
}
var logLevels = map[string]level.Option{
@@ -177,7 +292,7 @@ func getLogLevelFromString(levelName string) level.Option {
loglevel, ok := logLevels[levelName]
if !ok {
_ = level.Error(Root).Log("Unknown log level", "level", levelName)
_ = level.Error(root).Log("Unknown log level", "level", levelName)
return level.AllowError()
}
@@ -282,7 +397,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
mode = strings.TrimSpace(mode)
sec, err := cfg.GetSection("log." + mode)
if err != nil {
_ = level.Error(Root).Log("Unknown log mode", "mode", mode)
_ = level.Error(root).Log("Unknown log mode", "mode", mode)
return errutil.Wrapf(err, "failed to get config section log.%s", mode)
}
@@ -301,8 +416,8 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log"))
dpath := filepath.Dir(fileName)
if err := os.MkdirAll(dpath, os.ModePerm); err != nil {
_ = level.Error(Root).Log("Failed to create directory", "dpath", dpath, "err", err)
return errutil.Wrapf(err, "failed to create log directory %q", dpath)
_ = level.Error(root).Log("Failed to create directory", "dpath", dpath, "err", err)
continue
}
fileHandler := NewFileWriter()
fileHandler.Filename = fileName
@@ -313,8 +428,8 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
fileHandler.Daily = sec.Key("daily_rotate").MustBool(true)
fileHandler.Maxdays = sec.Key("max_days").MustInt64(7)
if err := fileHandler.Init(); err != nil {
_ = level.Error(Root).Log("Failed to initialize file handler", "dpath", dpath, "err", err)
return errutil.Wrapf(err, "failed to initialize file handler")
_ = level.Error(root).Log("Failed to initialize file handler", "dpath", dpath, "err", err)
continue
}
loggersToClose = append(loggersToClose, fileHandler)
@@ -336,20 +451,14 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
}
}
// copy joined default + mode filters into filters
for key, value := range modeFilters {
if _, exist := filters[key]; !exist {
filters[key] = value
}
}
handler.filters = modeFilters
handler.maxLevel = leveloption
// handler = LogFilterHandler(leveloption, modeFilters, handler)
configLoggers = append(configLoggers, handler)
}
if len(configLoggers) > 0 {
Root.loggers = configLoggers
root.initialize(configLoggers)
}
return nil
}
+1 -1
View File
@@ -62,7 +62,7 @@ func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler {
handler.Tag = sec.Key("tag").MustString("")
if err := handler.Init(); err != nil {
_ = level.Error(Root).Log("Failed to init syslog log handler", "error", err)
_ = level.Error(root).Log("Failed to init syslog log handler", "error", err)
os.Exit(1)
}
handler.logger = gokitsyslog.NewSyslogLogger(handler.syslog, format, gokitsyslog.PrioritySelectorOption(selector))
+1 -1
View File
@@ -25,7 +25,7 @@ type UsageStats struct {
kvStore *kvstore.NamespacedKVStore
RouteRegister routing.RouteRegister
log log.MultiLoggers
log log.Logger
oauthProviders map[string]bool
externalMetrics []usagestats.MetricsFunc
+1 -1
View File
@@ -17,7 +17,7 @@ import (
func newLogger(name string, lev string) log.Logger {
logger := log.New(name)
logger.AddLogger(logger, lev, map[string]level.Option{})
logger.SetLogger(level.NewFilter(logger.GetLogger(), level.AllowInfo()))
return logger
}
+1 -1
View File
@@ -107,7 +107,7 @@ func Recovery(cfg *setting.Cfg) web.Handler {
defer func() {
if r := recover(); r != nil {
var panicLogger log.Logger
panicLogger = log.Root
panicLogger = log.New("recovery")
// try to get request logger
ctx := contexthandler.FromContext(c.Req.Context())
if ctx != nil {
+8 -7
View File
@@ -77,13 +77,14 @@ type SearchTeamsQuery struct {
}
type TeamDTO struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
MemberCount int64 `json:"memberCount"`
Permission PermissionType `json:"permission"`
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
MemberCount int64 `json:"memberCount"`
Permission PermissionType `json:"permission"`
AccessControl map[string]bool `json:"accessControl"`
}
type SearchTeamQueryResult struct {
@@ -212,11 +212,11 @@ func (*testLicensingService) FeatureEnabled(feature string) bool {
}
type fakeLogger struct {
log.MultiLoggers
*log.ConcreteLogger
}
func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers {
return log.MultiLoggers{}
func (f fakeLogger) New(_ ...interface{}) *log.ConcreteLogger {
return &log.ConcreteLogger{}
}
func (f fakeLogger) Warn(_ string, _ ...interface{}) {
+2 -2
View File
@@ -1128,8 +1128,8 @@ type fakeLogger struct {
log.Logger
}
func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers {
return log.MultiLoggers{}
func (fl fakeLogger) New(_ ...interface{}) *log.ConcreteLogger {
return &log.ConcreteLogger{}
}
func (fl fakeLogger) Info(_ string, _ ...interface{}) {
+5 -1
View File
@@ -45,6 +45,7 @@ import (
"github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/ngalert"
ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics"
@@ -144,6 +145,8 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(login.Service), new(*loginservice.Implementation)),
authinfoservice.ProvideAuthInfoService,
wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)),
authinfodatabase.ProvideAuthInfoStore,
wire.Bind(new(login.Store), new(*authinfodatabase.AuthInfoStore)),
datasourceproxy.ProvideService,
search.ProvideService,
live.ProvideService,
@@ -190,7 +193,6 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)),
teamguardianManager.ProvideService,
wire.Bind(new(teamguardian.TeamGuardian), new(*teamguardianManager.Service)),
wire.Bind(new(authinfoservice.Service), new(*authinfoservice.Implementation)),
featuremgmt.ProvideManagerService,
featuremgmt.ProvideToggles,
resourceservices.ProvideResourceServices,
@@ -202,6 +204,7 @@ var wireBasicSet = wire.NewSet(
var wireSet = wire.NewSet(
wireBasicSet,
sqlstore.ProvideService,
wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)),
ngmetrics.ProvideService,
wire.Bind(new(notifications.Service), new(*notifications.NotificationService)),
wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)),
@@ -214,6 +217,7 @@ var wireTestSet = wire.NewSet(
ProvideTestEnv,
sqlstore.ProvideServiceForTests,
ngmetrics.ProvideServiceForTest,
wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)),
notifications.MockNotificationService,
wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)),
+6 -1
View File
@@ -37,13 +37,18 @@ type ResourcePermissionsService interface {
// GetPermissions returns all permissions for given resourceID
GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error)
// SetUserPermission sets permission on resource for a user
SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*ResourcePermission, error)
SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error)
// SetTeamPermission sets permission on resource for a team
SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*ResourcePermission, error)
// SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer)
SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*ResourcePermission, error)
}
type User struct {
ID int64
IsExternal bool
}
// Metadata contains user accesses for a given resource
// Ex: map[string]bool{"create":true, "delete": true}
type Metadata map[string]bool
@@ -5,7 +5,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
@@ -81,7 +80,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
user, team := createUserAndTeam(t, sql, tt.orgID)
for _, id := range tt.userPermissions {
_, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{
_, err := store.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.Id}, accesscontrol.SetResourcePermissionCommand{
Actions: []string{"dashboards:write"},
Resource: "dashboards",
ResourceID: id,
@@ -34,20 +34,20 @@ func (p *flatResourcePermission) Managed() bool {
}
func (s *AccessControlStore) SetUserResourcePermission(
ctx context.Context, orgID, userID int64,
ctx context.Context, orgID int64, user accesscontrol.User,
cmd accesscontrol.SetResourcePermissionCommand,
hook types.UserResourceHookFunc,
) (*accesscontrol.ResourcePermission, error) {
if userID == 0 {
if user.ID == 0 {
return nil, models.ErrUserNotFound
}
var err error
var permission *accesscontrol.ResourcePermission
err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd)
permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd)
if err == nil && hook != nil {
return hook(sess, orgID, userID, cmd.ResourceID, cmd.Permission)
return hook(sess, orgID, user, cmd.ResourceID, cmd.Permission)
}
return err
@@ -93,7 +93,7 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce
_, err := ac.SetUserResourcePermission(
context.Background(),
accesscontrol.GlobalOrgID,
userIds[i],
accesscontrol.User{ID: userIds[i]},
accesscontrol.SetResourcePermissionCommand{
Actions: []string{dsAction},
Resource: dsResource,
@@ -70,11 +70,11 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
store, _ := setupTestEnv(t)
for _, s := range test.seeds {
_, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s, nil)
_, err := store.SetUserResourcePermission(context.Background(), test.orgID, accesscontrol.User{ID: test.userID}, s, nil)
require.NoError(t, err)
}
added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{
added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, accesscontrol.SetResourcePermissionCommand{
Actions: test.actions,
Resource: test.resource,
ResourceID: test.resourceID,
@@ -352,7 +352,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst
})
require.NoError(t, err)
_, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{
_, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, accesscontrol.SetResourcePermissionCommand{
Actions: actions,
Resource: resource,
ResourceID: resourceID,
@@ -131,7 +131,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
_, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, cmd.Permission)
_, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: userID}, resourceID, cmd.Permission)
if err != nil {
return response.Error(http.StatusBadRequest, "failed to set user permission", err)
}
@@ -160,7 +160,7 @@ func TestApi_getPermissions(t *testing.T) {
// seed user 1 with "View" permission on dashboard 1
u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1})
require.NoError(t, err)
_, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, "View")
_, err = service.SetUserPermission(context.Background(), u.OrgId, accesscontrol.User{ID: u.Id}, tt.resourceID, "View")
require.NoError(t, err)
// seed built in role Admin with "Edit" permission on dashboard 1
@@ -3,6 +3,7 @@ package resourcepermissions
import (
"context"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
@@ -28,7 +29,7 @@ type Options struct {
// RoleGroup is the group name for the generated fixed roles
RoleGroup string
// OnSetUser if configured will be called each time a permission is set for a user
OnSetUser func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error
OnSetUser func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error
// OnSetTeam if configured will be called each time a permission is set for a team
OnSetTeam func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error
// OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role
@@ -16,7 +16,8 @@ import (
type Store interface {
// SetUserResourcePermission sets permission for managed user role on a resource
SetUserResourcePermission(
ctx context.Context, orgID, userID int64,
ctx context.Context, orgID int64,
user accesscontrol.User,
cmd accesscontrol.SetResourcePermissionCommand,
hook types.UserResourceHookFunc,
) (*accesscontrol.ResourcePermission, error)
@@ -100,7 +101,7 @@ func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID st
})
}
func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
if !s.options.Assignments.Users {
return nil, ErrInvalidAssignment
}
@@ -114,11 +115,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, re
return nil, err
}
if err := s.validateUser(ctx, orgID, userID); err != nil {
if err := s.validateUser(ctx, orgID, user.ID); err != nil {
return nil, err
}
return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{
return s.store.SetUserResourcePermission(ctx, orgID, user, accesscontrol.SetResourcePermissionCommand{
Actions: actions,
Permission: permission,
ResourceID: resourceID,
@@ -0,0 +1,33 @@
package resourcepermissions
import (
"context"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
type MockService struct {
mock.Mock
}
func (m *MockService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, resourceID)
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, user, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
func (m *MockService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission)
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
}
@@ -46,13 +46,13 @@ func TestService_SetUserPermission(t *testing.T) {
var hookCalled bool
if tt.callHook {
service.options.OnSetUser = func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error {
service.options.OnSetUser = func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error {
hookCalled = true
return nil
}
}
_, err = service.SetUserPermission(context.Background(), user.OrgId, user.Id, "1", "")
_, err = service.SetUserPermission(context.Background(), user.OrgId, accesscontrol.User{ID: user.Id}, "1", "")
require.NoError(t, err)
assert.Equal(t, tt.callHook, hookCalled)
})
@@ -1,7 +1,15 @@
package types
import "github.com/grafana/grafana/pkg/services/sqlstore"
import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type UserResourceHookFunc func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error
type UserResourceHookFunc func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error
type TeamResourceHookFunc func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error
type BuiltinResourceHookFunc func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error
type User struct {
ID int64
IsExternal bool
}
@@ -77,20 +77,20 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore
ReaderRoleName: "Team permission reader",
WriterRoleName: "Team permission writer",
RoleGroup: "Teams",
OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error {
OnSetUser: func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error {
teamId, err := strconv.ParseInt(resourceID, 10, 64)
if err != nil {
return err
}
switch permission {
case "Member":
return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, 0)
return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0)
case "Admin":
return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN)
return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN)
case "":
return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{
OrgId: orgID,
UserId: userID,
UserId: user.ID,
TeamId: teamId,
})
default:
+2 -2
View File
@@ -28,7 +28,7 @@ type UsageStatsQuerier interface {
// configured in Grafana.
func (e *AlertEngine) QueryUsageStats(ctx context.Context) (*UsageStats, error) {
cmd := &models.GetAllAlertsQuery{}
err := e.Bus.Dispatch(ctx, cmd)
err := e.sqlStore.GetAllAlertQueryHandler(ctx, cmd)
if err != nil {
return nil, err
}
@@ -63,7 +63,7 @@ func (e *AlertEngine) mapRulesToUsageStats(ctx context.Context, rules []*models.
result := map[string]int{}
for k, v := range typeCount {
query := &models.GetDataSourceQuery{Id: k}
err := e.Bus.Dispatch(ctx, query)
err := e.sqlStore.GetDataSource(ctx, query)
if err != nil {
return map[string]int{}, nil
}
+6 -6
View File
@@ -7,18 +7,18 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
func TestAlertingUsageStats(t *testing.T) {
store := &AlertStoreMock{}
ae := &AlertEngine{
Bus: bus.New(),
sqlStore: store,
}
ae.Bus.AddHandler(func(ctx context.Context, query *models.GetAllAlertsQuery) error {
store.getAllAlerts = func(ctx context.Context, query *models.GetAllAlertsQuery) error {
var createFake = func(file string) *simplejson.Json {
// Ignore gosec warning G304 since it's a test
// nolint:gosec
@@ -37,9 +37,9 @@ func TestAlertingUsageStats(t *testing.T) {
{Id: 3, Settings: createFake("testdata/settings/empty.json")},
}
return nil
})
}
ae.Bus.AddHandler(func(ctx context.Context, query *models.GetDataSourceQuery) error {
store.getDataSource = func(ctx context.Context, query *models.GetDataSourceQuery) error {
ds := map[int64]*models.DataSource{
1: {Type: "influxdb"},
2: {Type: "graphite"},
@@ -54,7 +54,7 @@ func TestAlertingUsageStats(t *testing.T) {
query.Result = r
return nil
})
}
result, err := ae.QueryUsageStats(context.Background())
require.NoError(t, err, "getAlertingUsage should not return error")
+1 -2
View File
@@ -13,7 +13,6 @@ import (
gocontext "context"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
@@ -140,7 +139,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange l
OrgId: context.Rule.OrgID,
}
if err := bus.Dispatch(context.Ctx, getDsInfo); err != nil {
if err := context.Store.GetDataSource(context.Ctx, getDsInfo); err != nil {
return nil, fmt.Errorf("could not find datasource: %w", err)
}
@@ -4,10 +4,10 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
@@ -137,16 +137,15 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo *
//nolint: staticcheck // legacydata.DataResponse deprecated
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) {
t.Run("desc", func(t *testing.T) {
bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error {
query.Result = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData}
return nil
})
store := mockstore.NewSQLStoreMock()
store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData}
ctx := &queryIntervalTestContext{}
ctx.result = &alerting.EvalContext{
Ctx: context.Background(),
Rule: &alerting.Rule{},
RequestValidator: &validations.OSSPluginRequestValidator{},
Store: store,
}
jsonModel, err := simplejson.NewJson([]byte(`{
@@ -6,13 +6,13 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
@@ -35,10 +35,8 @@ func newTimeSeriesPointsFromArgs(values ...float64) legacydata.DataTimeSeriesPoi
func TestQueryCondition(t *testing.T) {
setup := func() *queryConditionTestContext {
ctx := &queryConditionTestContext{}
bus.AddHandler("test", func(ctx context.Context, query *models.GetDataSourceQuery) error {
query.Result = &models.DataSource{Id: 1, Type: "graphite"}
return nil
})
store := mockstore.NewSQLStoreMock()
store.ExpectedDatasource = &models.DataSource{Id: 1, Type: "graphite"}
ctx.reducer = `{"type":"avg"}`
ctx.evaluator = `{"type":"gt","params":[100]}`
@@ -46,6 +44,7 @@ func TestQueryCondition(t *testing.T) {
Ctx: context.Background(),
Rule: &alerting.Rule{},
RequestValidator: &validations.OSSPluginRequestValidator{},
Store: store,
}
return ctx
}
+20 -4
View File
@@ -16,11 +16,25 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
// AlertStore is a subset of SQLStore API to satisfy the needs of the alerting service.
// A subset is needed to make it easier to mock during the tests.
type AlertStore interface {
GetAllAlertQueryHandler(context.Context, *models.GetAllAlertsQuery) error
GetDataSource(context.Context, *models.GetDataSourceQuery) error
GetDashboardUIDById(context.Context, *models.GetDashboardRefByIdQuery) error
SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error
GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error
GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error
SetAlertState(context.Context, *models.SetAlertStateCommand) error
}
// AlertEngine is the background process that
// schedules alert evaluations and makes sure notifications
// are sent.
@@ -40,6 +54,7 @@ type AlertEngine struct {
resultHandler resultHandler
usageStatsService usagestats.Service
tracer tracing.Tracer
sqlStore AlertStore
}
// IsDisabled returns true if the alerting service is disabled for this instance.
@@ -50,7 +65,7 @@ func (e *AlertEngine) IsDisabled() bool {
// ProvideAlertEngine returns a new AlertEngine.
func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidator models.PluginRequestValidator,
dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal,
cfg *setting.Cfg, tracer tracing.Tracer) *AlertEngine {
notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg) *AlertEngine {
e := &AlertEngine{
Cfg: cfg,
RenderService: renderer,
@@ -59,14 +74,15 @@ func ProvideAlertEngine(renderer rendering.Service, bus bus.Bus, requestValidato
DataService: dataService,
usageStatsService: usageStatsService,
tracer: tracer,
sqlStore: sqlStore,
}
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New(), 1)
e.execQueue = make(chan *Job, 1000)
e.scheduler = newScheduler()
e.evalHandler = NewEvalHandler(e.DataService)
e.ruleReader = newRuleReader()
e.ruleReader = newRuleReader(sqlStore)
e.log = log.New("alerting.engine")
e.resultHandler = newResultHandler(e.RenderService, encryptionService.GetDecryptedValue)
e.resultHandler = newResultHandler(e.RenderService, sqlStore, notificationService, encryptionService.GetDecryptedValue)
e.registerUsageMetrics()
@@ -179,7 +195,7 @@ func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan
alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout)
cancelChan <- cancelFn
alertCtx, span := e.tracer.Start(alertCtx, "alert execution")
evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator)
evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator, e.sqlStore)
evalContext.Ctx = alertCtx
go func() {
@@ -24,7 +24,7 @@ func TestEngineTimeouts(t *testing.T) {
usMock := &usagestats.UsageStatsMock{T: t}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer)
engine := ProvideAlertEngine(nil, nil, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, nil, setting.NewCfg())
setting.AlertingNotificationTimeout = 30 * time.Second
setting.AlertingMaxAttempts = 3
engine.resultHandler = &FakeResultHandler{}
+59 -5
View File
@@ -43,12 +43,66 @@ func (handler *FakeResultHandler) handle(evalContext *EvalContext) error {
return nil
}
// A mock implementation of the AlertStore interface, allowing to override certain methods individually
type AlertStoreMock struct {
getAllAlerts func(context.Context, *models.GetAllAlertsQuery) error
getDataSource func(context.Context, *models.GetDataSourceQuery) error
getAlertNotificationsWithUidToSend func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error
getOrCreateNotificationState func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error
}
func (a *AlertStoreMock) GetDataSource(c context.Context, cmd *models.GetDataSourceQuery) error {
if a.getDataSource != nil {
return a.getDataSource(c, cmd)
}
return nil
}
func (a *AlertStoreMock) GetAllAlertQueryHandler(c context.Context, cmd *models.GetAllAlertsQuery) error {
if a.getAllAlerts != nil {
return a.getAllAlerts(c, cmd)
}
return nil
}
func (a *AlertStoreMock) GetAlertNotificationsWithUidToSend(c context.Context, cmd *models.GetAlertNotificationsWithUidToSendQuery) error {
if a.getAlertNotificationsWithUidToSend != nil {
return a.getAlertNotificationsWithUidToSend(c, cmd)
}
return nil
}
func (a *AlertStoreMock) GetOrCreateAlertNotificationState(c context.Context, cmd *models.GetOrCreateNotificationStateQuery) error {
if a.getOrCreateNotificationState != nil {
return a.getOrCreateNotificationState(c, cmd)
}
return nil
}
func (a *AlertStoreMock) GetDashboardUIDById(_ context.Context, _ *models.GetDashboardRefByIdQuery) error {
return nil
}
func (a *AlertStoreMock) SetAlertNotificationStateToCompleteCommand(_ context.Context, _ *models.SetAlertNotificationStateToCompleteCommand) error {
return nil
}
func (a *AlertStoreMock) SetAlertNotificationStateToPendingCommand(_ context.Context, _ *models.SetAlertNotificationStateToPendingCommand) error {
return nil
}
func (a *AlertStoreMock) SetAlertState(_ context.Context, _ *models.SetAlertStateCommand) error {
return nil
}
func TestEngineProcessJob(t *testing.T) {
bus := bus.New()
usMock := &usagestats.UsageStatsMock{T: t}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), setting.NewCfg(), tracer)
store := &AlertStoreMock{}
engine := ProvideAlertEngine(nil, bus, nil, nil, usMock, ossencryption.ProvideService(), nil, tracer, store, setting.NewCfg())
setting.AlertingEvaluationTimeout = 30 * time.Second
setting.AlertingNotificationTimeout = 30 * time.Second
setting.AlertingMaxAttempts = 3
@@ -56,19 +110,19 @@ func TestEngineProcessJob(t *testing.T) {
job := &Job{running: true, Rule: &Rule{}}
t.Run("Should register usage metrics func", func(t *testing.T) {
bus.AddHandler(func(ctx context.Context, q *models.GetAllAlertsQuery) error {
store.getAllAlerts = func(ctx context.Context, q *models.GetAllAlertsQuery) error {
settings, err := simplejson.NewJson([]byte(`{"conditions": [{"query": { "datasourceId": 1}}]}`))
if err != nil {
return err
}
q.Result = []*models.Alert{{Settings: settings}}
return nil
})
}
bus.AddHandler(func(ctx context.Context, q *models.GetDataSourceQuery) error {
store.getDataSource = func(ctx context.Context, q *models.GetDataSourceQuery) error {
q.Result = &models.DataSource{Id: 1, Type: models.DS_PROMETHEUS}
return nil
})
}
report, err := usMock.GetUsageReport(context.Background())
require.Nil(t, err)
+5 -3
View File
@@ -6,7 +6,6 @@ import (
"regexp"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@@ -36,10 +35,12 @@ type EvalContext struct {
RequestValidator models.PluginRequestValidator
Ctx context.Context
Store AlertStore
}
// NewEvalContext is the EvalContext constructor.
func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator) *EvalContext {
func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator, sqlStore AlertStore) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
StartTime: time.Now(),
@@ -49,6 +50,7 @@ func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator model
Log: log.New("alerting.evalContext"),
PrevAlertState: rule.State,
RequestValidator: requestValidator,
Store: sqlStore,
}
}
@@ -108,7 +110,7 @@ func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) {
}
uidQuery := &models.GetDashboardRefByIdQuery{Id: c.Rule.DashboardID}
if err := bus.Dispatch(c.Ctx, uidQuery); err != nil {
if err := c.Store.GetDashboardUIDById(c.Ctx, uidQuery); err != nil {
return nil, err
}
+2 -2
View File
@@ -15,7 +15,7 @@ import (
)
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{})
ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil)
t.Run("ok -> alerting", func(t *testing.T) {
ctx.PrevAlertState = models.AlertStateOK
@@ -200,7 +200,7 @@ func TestGetStateFromEvalContext(t *testing.T) {
}
for _, tc := range tcs {
evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{})
evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil)
tc.applyFn(evalContext)
newState := evalContext.GetNewState()
+14 -14
View File
@@ -29,7 +29,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
Conditions: []Condition{&conditionStub{
firing: true,
}},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -39,7 +39,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
t.Run("Show return triggered with single passing condition2", func(t *testing.T) {
context := NewEvalContext(context.Background(), &Rule{
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -52,7 +52,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
&conditionStub{firing: false, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -65,7 +65,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -78,7 +78,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -92,7 +92,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -106,7 +106,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -120,7 +120,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -134,7 +134,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -148,7 +148,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -163,7 +163,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.False(t, context.NoDataFound)
@@ -174,7 +174,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -187,7 +187,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "and", noData: true},
&conditionStub{operator: "and", noData: false},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.True(t, context.NoDataFound)
@@ -199,7 +199,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "or", noData: true},
&conditionStub{operator: "or", noData: false},
},
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
handler.Eval(context)
require.True(t, context.NoDataFound)
+20 -16
View File
@@ -6,11 +6,11 @@ import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting"
)
@@ -83,18 +83,22 @@ type ShowWhen struct {
Is string `json:"is"`
}
func newNotificationService(renderService rendering.Service, decryptFn GetDecryptedValueFn) *notificationService {
func newNotificationService(renderService rendering.Service, sqlStore AlertStore, notificationSvc *notifications.NotificationService, decryptFn GetDecryptedValueFn) *notificationService {
return &notificationService{
log: log.New("alerting.notifier"),
renderService: renderService,
decryptFn: decryptFn,
log: log.New("alerting.notifier"),
renderService: renderService,
sqlStore: sqlStore,
notificationService: notificationSvc,
decryptFn: decryptFn,
}
}
type notificationService struct {
log log.Logger
renderService rendering.Service
decryptFn GetDecryptedValueFn
log log.Logger
renderService rendering.Service
sqlStore AlertStore
notificationService *notifications.NotificationService
decryptFn GetDecryptedValueFn
}
func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error {
@@ -152,7 +156,7 @@ func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, no
Version: notifierState.state.Version,
}
return bus.Dispatch(evalContext.Ctx, cmd)
return n.sqlStore.SetAlertNotificationStateToCompleteCommand(evalContext.Ctx, cmd)
}
func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *notifierState) error {
@@ -163,7 +167,7 @@ func (n *notificationService) sendNotification(evalContext *EvalContext, notifie
AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges,
}
err := bus.Dispatch(evalContext.Ctx, setPendingCmd)
err := n.sqlStore.SetAlertNotificationStateToPendingCommand(evalContext.Ctx, setPendingCmd)
if err != nil {
if errors.Is(err, models.ErrAlertNotificationStateVersionConflict) {
return nil
@@ -251,13 +255,13 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) {
query := &models.GetAlertNotificationsWithUidToSendQuery{OrgId: orgID, Uids: notificationUids}
if err := bus.Dispatch(evalContext.Ctx, query); err != nil {
if err := n.sqlStore.GetAlertNotificationsWithUidToSend(evalContext.Ctx, query); err != nil {
return nil, err
}
var result notifierStateSlice
for _, notification := range query.Result {
not, err := InitNotifier(notification, n.decryptFn)
not, err := InitNotifier(notification, n.decryptFn, n.notificationService)
if err != nil {
n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err)
continue
@@ -269,7 +273,7 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [
OrgId: evalContext.Rule.OrgID,
}
err = bus.Dispatch(evalContext.Ctx, query)
err = n.sqlStore.GetOrCreateAlertNotificationState(evalContext.Ctx, query)
if err != nil {
n.log.Error("Could not get notification state.", "notifier", notification.Id, "error", err)
continue
@@ -287,13 +291,13 @@ func (n *notificationService) getNeededNotifiers(orgID int64, notificationUids [
}
// InitNotifier instantiate a new notifier based on the model.
func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Notifier, error) {
func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn, notificationService *notifications.NotificationService) (Notifier, error) {
notifierPlugin, found := notifierFactories[model.Type]
if !found {
return nil, fmt.Errorf("unsupported notification type %q", model.Type)
}
return notifierPlugin.Factory(model, fn)
return notifierPlugin.Factory(model, fn, notificationService)
}
// GetDecryptedValueFn is a function that returns the decrypted value of
@@ -301,7 +305,7 @@ func InitNotifier(model *models.AlertNotification, fn GetDecryptedValueFn) (Noti
type GetDecryptedValueFn func(ctx context.Context, sjd map[string][]byte, key string, fallback string, secret string) string
// NotifierFactory is a signature for creating notifiers.
type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn) (Notifier, error)
type NotifierFactory func(*models.AlertNotification, GetDecryptedValueFn, notifications.Service) (Notifier, error)
var notifierFactories = make(map[string]*NotifierPlugin)
+13 -9
View File
@@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -21,17 +22,18 @@ import (
func TestNotificationService(t *testing.T) {
testRule := &Rule{Name: "Test", Message: "Something is bad"}
evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{})
store := &AlertStoreMock{}
evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store)
testRuleTemplated := &Rule{Name: "Test latency ${quantile}", Message: "Something is bad on instance ${instance}"}
evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{})
evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store)
evalCtxWithMatch.EvalMatches = []*EvalMatch{{
Tags: map[string]string{
"instance": "localhost:3000",
"quantile": "0.99",
},
}}
evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{})
evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store)
notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification",
evalCtx, true, func(sc *scenarioContext) {
@@ -177,7 +179,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
evalCtx.dashboardRef = &models.DashboardRef{Uid: "db-uid"}
bus.AddHandler("test", func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error {
store := evalCtx.Store.(*AlertStoreMock)
store.getAlertNotificationsWithUidToSend = func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error {
query.Result = []*models.AlertNotification{
{
Id: 1,
@@ -188,9 +192,9 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
},
}
return nil
})
}
bus.AddHandler("test", func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error {
store.getOrCreateNotificationState = func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error {
query.Result = &models.AlertNotificationState{
AlertId: evalCtx.Rule.ID,
AlertRuleStateUpdatedVersion: 1,
@@ -199,7 +203,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
State: models.AlertNotificationStateUnknown,
}
return nil
})
}
bus.AddHandler("test", func(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error {
return nil
@@ -263,7 +267,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
},
}
scenarioCtx.notificationService = newNotificationService(renderService, nil)
scenarioCtx.notificationService = newNotificationService(renderService, store, nil, nil)
fn(scenarioCtx)
})
}
@@ -279,7 +283,7 @@ type testNotifier struct {
Frequency time.Duration
}
func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn) (Notifier, error) {
func newTestNotifier(model *models.AlertNotification, _ GetDecryptedValueFn, ns notifications.Service) (Notifier, error) {
uploadImage := true
value, exist := model.Settings.CheckGet("uploadImage")
if exist {
@@ -7,11 +7,11 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -50,7 +50,7 @@ func init() {
}
// NewAlertmanagerNotifier returns a new Alertmanager notifier
func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
urlString := model.Settings.Get("url").MustString()
if urlString == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -67,7 +67,7 @@ func NewAlertmanagerNotifier(model *models.AlertNotification, fn alerting.GetDec
basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString(), setting.SecretKey)
return &AlertmanagerNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
URL: url,
BasicAuthUser: basicAuthUser,
BasicAuthPassword: basicAuthPassword,
@@ -183,7 +183,7 @@ func (am *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error
Body: string(body),
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := am.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
am.log.Error("Failed to send alertmanager", "error", err, "alertmanager", am.Name, "url", url)
errCnt++
}
@@ -68,7 +68,7 @@ func TestWhenAlertManagerShouldNotify(t *testing.T) {
am := &AlertmanagerNotifier{log: log.New("test.logger")}
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
State: tc.prevState,
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
evalContext.Rule.State = tc.newState
@@ -92,7 +92,7 @@ func TestAlertmanagerNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -106,7 +106,7 @@ func TestAlertmanagerNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
require.NoError(t, err)
@@ -125,7 +125,7 @@ func TestAlertmanagerNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
require.NoError(t, err)
+5 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
)
const (
@@ -24,11 +25,13 @@ type NotifierBase struct {
DisableResolveMessage bool
Frequency time.Duration
NotificationService notifications.Service
log log.Logger
}
// NewNotifierBase returns a new `NotifierBase`.
func NewNotifierBase(model *models.AlertNotification) NotifierBase {
func NewNotifierBase(model *models.AlertNotification, notificationService notifications.Service) NotifierBase {
uploadImage := true
if value, exists := model.Settings.CheckGet("uploadImage"); exists {
uploadImage = value.MustBool()
@@ -43,6 +46,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
SendReminder: model.SendReminder,
DisableResolveMessage: model.DisableResolveMessage,
Frequency: model.Frequency,
NotificationService: notificationService,
log: log.New("alerting.notifier." + model.Name),
}
}
+5 -5
View File
@@ -170,7 +170,7 @@ func TestShouldSendAlertNotification(t *testing.T) {
for _, tc := range tcs {
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
State: tc.prevState,
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
if tc.state == nil {
tc.state = &models.AlertNotificationState{}
@@ -197,24 +197,24 @@ func TestBaseNotifier(t *testing.T) {
t.Run("can parse false value", func(t *testing.T) {
bJSON.Set("uploadImage", false)
base := NewNotifierBase(model)
base := NewNotifierBase(model, nil)
require.False(t, base.UploadImage)
})
t.Run("can parse true value", func(t *testing.T) {
bJSON.Set("uploadImage", true)
base := NewNotifierBase(model)
base := NewNotifierBase(model, nil)
require.True(t, base.UploadImage)
})
t.Run("default value should be true for backwards compatibility", func(t *testing.T) {
base := NewNotifierBase(model)
base := NewNotifierBase(model, nil)
require.True(t, base.UploadImage)
})
t.Run("default value should be false for backwards compatibility", func(t *testing.T) {
base := NewNotifierBase(model)
base := NewNotifierBase(model, nil)
require.False(t, base.DisableResolveMessage)
})
}
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"net/url"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
)
const defaultDingdingMsgType = "link"
@@ -47,7 +47,7 @@ func init() {
})
}
func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
@@ -56,7 +56,7 @@ func newDingDingNotifier(model *models.AlertNotification, _ alerting.GetDecrypte
msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType)
return &DingDingNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
MsgType: msgType,
URL: url,
log: log.New("alerting.notifier.dingding"),
@@ -91,7 +91,7 @@ func (dd *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
Body: string(body),
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := dd.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
dd.log.Error("Failed to send DingDing", "error", err, "dingding", dd.Name)
return err
}
@@ -24,7 +24,7 @@ func TestDingDingNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
t.Run("settings should trigger incident", func(t *testing.T) {
@@ -37,7 +37,7 @@ func TestDingDingNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
notifier := not.(*DingDingNotifier)
require.Nil(t, err)
@@ -50,7 +50,7 @@ func TestDingDingNotifier(t *testing.T) {
&alerting.Rule{
State: models.AlertStateAlerting,
Message: `{host="localhost"}`,
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
_, err = notifier.genBody(evalContext, "")
require.Nil(t, err)
})
+4 -4
View File
@@ -9,11 +9,11 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -57,7 +57,7 @@ func init() {
})
}
func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
avatar := model.Settings.Get("avatar_url").MustString()
content := model.Settings.Get("content").MustString()
url := model.Settings.Get("url").MustString()
@@ -67,7 +67,7 @@ func newDiscordNotifier(model *models.AlertNotification, _ alerting.GetDecrypted
useDiscordUsername := model.Settings.Get("use_discord_username").MustBool(false)
return &DiscordNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
Content: content,
AvatarURL: avatar,
WebhookURL: url,
@@ -177,7 +177,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
}
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := dn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
dn.log.Error("Failed to send notification to Discord", "error", err)
return err
}
@@ -22,7 +22,7 @@ func TestDiscordNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -41,7 +41,7 @@ func TestDiscordNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
discordNotifier := not.(*DiscordNotifier)
require.Nil(t, err)
+4 -4
View File
@@ -3,12 +3,12 @@ package notifiers
import (
"os"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -48,7 +48,7 @@ type EmailNotifier struct {
// NewEmailNotifier is the constructor function
// for the EmailNotifier.
func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
addressesString := model.Settings.Get("addresses").MustString()
singleEmail := model.Settings.Get("singleEmail").MustBool(false)
@@ -60,7 +60,7 @@ func NewEmailNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa
addresses := util.SplitEmails(addressesString)
return &EmailNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
Addresses: addresses,
SingleEmail: singleEmail,
log: log.New("alerting.notifier.email"),
@@ -117,7 +117,7 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
}
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := en.NotificationService.SendEmailCommandHandlerSync(evalContext.Ctx, cmd); err != nil {
en.log.Error("Failed to send alert notification email", "error", err)
return err
}
@@ -22,7 +22,7 @@ func TestEmailNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -39,7 +39,7 @@ func TestEmailNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
emailNotifier := not.(*EmailNotifier)
require.Nil(t, err)
@@ -63,7 +63,7 @@ func TestEmailNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
emailNotifier := not.(*EmailNotifier)
require.Nil(t, err)
@@ -5,10 +5,10 @@ import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -32,14 +32,14 @@ func init() {
})
}
func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func newGoogleChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
return &GoogleChatNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
URL: url,
log: log.New("alerting.notifier.googlechat"),
}, nil
@@ -220,7 +220,7 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error {
Body: string(body),
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := gcn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
gcn.log.Error("Failed to send Google Hangouts Chat alert", "error", err, "webhook", gcn.Name)
return err
}
@@ -22,7 +22,7 @@ func TestGoogleChatNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -39,7 +39,7 @@ func TestGoogleChatNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
webhookNotifier := not.(*GoogleChatNotifier)
require.Nil(t, err)
+4 -4
View File
@@ -7,10 +7,10 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
)
func init() {
@@ -53,7 +53,7 @@ const (
// NewHipChatNotifier is the constructor functions
// for the HipChatNotifier
func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
@@ -66,7 +66,7 @@ func NewHipChatNotifier(model *models.AlertNotification, _ alerting.GetDecrypted
roomID := model.Settings.Get("roomid").MustString()
return &HipChatNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
URL: url,
APIKey: apikey,
RoomID: roomID,
@@ -177,7 +177,7 @@ func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
hc.log.Info("Request payload", "json", string(data))
cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := hc.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name)
return err
}
@@ -23,7 +23,7 @@ func TestHipChatNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -39,7 +39,7 @@ func TestHipChatNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
hipchatNotifier := not.(*HipChatNotifier)
require.Nil(t, err)
@@ -65,7 +65,7 @@ func TestHipChatNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
hipchatNotifier := not.(*HipChatNotifier)
require.Nil(t, err)
+4 -4
View File
@@ -5,11 +5,11 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
)
func init() {
@@ -41,7 +41,7 @@ func init() {
}
// NewKafkaNotifier is the constructor function for the Kafka notifier.
func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
endpoint := model.Settings.Get("kafkaRestProxy").MustString()
if endpoint == "" {
return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"}
@@ -52,7 +52,7 @@ func NewKafkaNotifier(model *models.AlertNotification, _ alerting.GetDecryptedVa
}
return &KafkaNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
Endpoint: endpoint,
Topic: topic,
log: log.New("alerting.notifier.kafka"),
@@ -124,7 +124,7 @@ func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
},
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := kn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
kn.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body))
return err
}
@@ -22,7 +22,7 @@ func TestKafkaNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -40,7 +40,7 @@ func TestKafkaNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
kafkaNotifier := not.(*KafkaNotifier)
require.Nil(t, err)
+4 -4
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"net/url"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -37,14 +37,14 @@ const (
)
// NewLINENotifier is the constructor for the LINE notifier
func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewLINENotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
token := fn(context.Background(), model.SecureSettings, "token", model.Settings.Get("token").MustString(), setting.SecretKey)
if token == "" {
return nil, alerting.ValidationError{Reason: "Could not find token in settings"}
}
return &LineNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
Token: token,
log: log.New("alerting.notifier.line"),
}, nil
@@ -92,7 +92,7 @@ func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
Body: form.Encode(),
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := ln.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
ln.log.Error("Failed to send notification to LINE", "error", err, "body", body)
return err
}
+2 -2
View File
@@ -21,7 +21,7 @@ func TestLineNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
t.Run("settings should trigger incident", func(t *testing.T) {
@@ -36,7 +36,7 @@ func TestLineNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
lineNotifier := not.(*LineNotifier)
require.Nil(t, err)
+5 -5
View File
@@ -5,11 +5,11 @@ import (
"fmt"
"strconv"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
)
@@ -84,7 +84,7 @@ const (
)
// NewOpsGenieNotifier is the constructor for OpsGenie.
func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn) (alerting.Notifier, error) {
func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
autoClose := model.Settings.Get("autoClose").MustBool(true)
overridePriority := model.Settings.Get("overridePriority").MustBool(true)
apiKey := fn(context.Background(), model.SecureSettings, "apiKey", model.Settings.Get("apiKey").MustString(), setting.SecretKey)
@@ -104,7 +104,7 @@ func NewOpsGenieNotifier(model *models.AlertNotification, fn alerting.GetDecrypt
}
return &OpsGenieNotifier{
NotifierBase: NewNotifierBase(model),
NotifierBase: NewNotifierBase(model, ns),
APIKey: apiKey,
APIUrl: apiURL,
AutoClose: autoClose,
@@ -205,7 +205,7 @@ func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error
},
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
}
@@ -229,7 +229,7 @@ func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error
},
}
if err := bus.Dispatch(evalContext.Ctx, cmd); err != nil {
if err := on.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil {
on.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
return err
}
@@ -6,11 +6,11 @@ import (
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/stretchr/testify/require"
@@ -28,7 +28,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
})
@@ -45,7 +45,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
opsgenieNotifier := not.(*OpsGenieNotifier)
require.Nil(t, err)
@@ -69,7 +69,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, nil)
require.Error(t, err)
require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(alerting.ValidationError{}))
require.True(t, strings.HasSuffix(err.Error(), "Invalid value for sendTagsAs: \"not_a_valid_value\""))
@@ -92,7 +92,8 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
notificationService := notifications.MockNotificationService()
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
@@ -102,22 +103,20 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, &validations.OSSPluginRequestValidator{})
}, &validations.OSSPluginRequestValidator{}, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"})
@@ -142,7 +141,8 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
notificationService := notifications.MockNotificationService()
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
@@ -152,22 +152,20 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
}, nil, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{})
@@ -192,7 +190,8 @@ func TestOpsGenieNotifier(t *testing.T) {
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
notificationService := notifications.MockNotificationService()
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue, notificationService) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
@@ -202,22 +201,20 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
}, nil, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandler("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
bodyJSON, err := simplejson.NewJson([]byte(notificationService.Webhook.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"})

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