From a8fa5f1796bff5894c484adaecd242721177724f Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Tue, 25 May 2021 12:34:52 +0100 Subject: [PATCH 01/41] Grafana-UI: Make card figure honor align prop (#34639) --- packages/grafana-ui/src/components/Card/Card.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index c991ab20ed7..0ba92fea8be 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -145,8 +145,6 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme2) => { media: css` margin-right: ${theme.spacing(2)}; width: 40px; - display: flex; - align-items: center; & > * { width: 100%; From 92ac2c40d3284c2a9a92163fb52f6a3e31aac76c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 25 May 2021 13:40:05 +0200 Subject: [PATCH 02/41] expose azure settings as env variables (#34484) --- pkg/plugins/backendplugin/manager/manager.go | 17 +++++++++++ .../backendplugin/manager/manager_test.go | 30 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/plugins/backendplugin/manager/manager.go b/pkg/plugins/backendplugin/manager/manager.go index 71de05f321f..2417e16b4d6 100644 --- a/pkg/plugins/backendplugin/manager/manager.go +++ b/pkg/plugins/backendplugin/manager/manager.go @@ -81,6 +81,8 @@ func (m *manager) Register(pluginID string, factory backendplugin.PluginFactoryF } hostEnv = append(hostEnv, m.getAWSEnvironmentVariables()...) + hostEnv = append(hostEnv, m.getAzureEnvironmentVariables()...) + pluginSettings := getPluginSettings(pluginID, m.Cfg) env := pluginSettings.ToEnv("GF_PLUGIN", hostEnv) @@ -164,6 +166,21 @@ func (m *manager) getAWSEnvironmentVariables() []string { return variables } +func (m *manager) getAzureEnvironmentVariables() []string { + variables := []string{} + if m.Cfg.Azure.Cloud != "" { + variables = append(variables, "AZURE_CLOUD="+m.Cfg.Azure.Cloud) + } + if m.Cfg.Azure.ManagedIdentityClientId != "" { + variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+m.Cfg.Azure.ManagedIdentityClientId) + } + if m.Cfg.Azure.ManagedIdentityEnabled { + variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true") + } + + return variables +} + //nolint: staticcheck // plugins.DataPlugin deprecated func (m *manager) GetDataPlugin(pluginID string) interface{} { p, _ := m.Get(pluginID) diff --git a/pkg/plugins/backendplugin/manager/manager_test.go b/pkg/plugins/backendplugin/manager/manager_test.go index 81ae11809c2..fba06c8e506 100644 --- a/pkg/plugins/backendplugin/manager/manager_test.go +++ b/pkg/plugins/backendplugin/manager/manager_test.go @@ -63,8 +63,16 @@ func TestManager(t *testing.T) { }) t.Run("Should provide expected host environment variables", func(t *testing.T) { - require.Len(t, ctx.env, 4) - require.EqualValues(t, []string{"GF_VERSION=7.0.0", "GF_EDITION=Open Source", fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName), fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName)}, ctx.env) + require.Len(t, ctx.env, 7) + require.EqualValues(t, []string{ + "GF_VERSION=7.0.0", + "GF_EDITION=Open Source", + fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName), + fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName), + "AZURE_CLOUD=AzureCloud", + "AZURE_MANAGED_IDENTITY_CLIENT_ID=client-id", + "AZURE_MANAGED_IDENTITY_ENABLED=true"}, + ctx.env) }) t.Run("When manager runs should start and stop plugin", func(t *testing.T) { @@ -282,8 +290,18 @@ func TestManager(t *testing.T) { require.NoError(t, err) t.Run("Should provide expected host environment variables", func(t *testing.T) { - require.Len(t, ctx.env, 6) - require.EqualValues(t, []string{"GF_VERSION=7.0.0", "GF_EDITION=Enterprise", "GF_ENTERPRISE_LICENSE_PATH=/license.txt", "GF_ENTERPRISE_LICENSE_TEXT=testtoken", fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName), fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName)}, ctx.env) + require.Len(t, ctx.env, 9) + require.EqualValues(t, []string{ + "GF_VERSION=7.0.0", + "GF_EDITION=Enterprise", + "GF_ENTERPRISE_LICENSE_PATH=/license.txt", + "GF_ENTERPRISE_LICENSE_TEXT=testtoken", + fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName), + fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName), + "AZURE_CLOUD=AzureCloud", + "AZURE_MANAGED_IDENTITY_CLIENT_ID=client-id", + "AZURE_MANAGED_IDENTITY_ENABLED=true"}, + ctx.env) }) }) }) @@ -304,6 +322,10 @@ func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *m cfg.AWSAllowedAuthProviders = []string{"keys", "credentials"} cfg.AWSAssumeRoleEnabled = true + cfg.Azure.ManagedIdentityEnabled = true + cfg.Azure.Cloud = "AzureCloud" + cfg.Azure.ManagedIdentityClientId = "client-id" + license := &testLicensingService{} validator := &testPluginRequestValidator{} ctx := &managerScenarioCtx{ From 881abb3af8dd0fe1295df26258812cca0b38347d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 25 May 2021 14:48:16 +0200 Subject: [PATCH 03/41] Admin: Fixes infinite loading edit profile page (#34627) * UserProfile: Fixes infinite loading spinner * Refactor: some clean up * Refactor: some more cleanup * Tests: Adds tests for UserProfileEditPage * Chore: updates after PR comments * Refactor: removes unnecessary unmount/mount --- .../SharedPreferences/SharedPreferences.tsx | 21 +- public/app/core/utils/UserProvider.tsx | 200 ------------- public/app/features/explore/Wrapper.test.tsx | 2 + .../features/profile/ChangePasswordForm.tsx | 7 +- .../features/profile/ChangePasswordPage.tsx | 72 ++--- .../features/profile/UserOrganizations.tsx | 13 +- .../app/features/profile/UserProfileEdit.tsx | 75 ----- .../features/profile/UserProfileEditForm.tsx | 22 +- .../profile/UserProfileEditPage.test.tsx | 279 ++++++++++++++++++ .../features/profile/UserProfileEditPage.tsx | 81 +++++ public/app/features/profile/UserSessions.tsx | 12 +- public/app/features/profile/UserTeams.tsx | 7 +- public/app/features/profile/api.ts | 57 ++++ public/app/features/profile/state/actions.ts | 89 ++++++ .../features/profile/state/reducers.test.ts | 187 ++++++++++++ public/app/features/profile/state/reducers.ts | 103 ++++++- .../app/features/profile/state/selectors.ts | 2 +- public/app/features/profile/types.ts | 11 + public/app/routes/routes.tsx | 2 +- public/app/types/store.ts | 3 +- public/app/types/user.ts | 6 - 21 files changed, 875 insertions(+), 376 deletions(-) delete mode 100644 public/app/core/utils/UserProvider.tsx delete mode 100644 public/app/features/profile/UserProfileEdit.tsx create mode 100644 public/app/features/profile/UserProfileEditPage.test.tsx create mode 100644 public/app/features/profile/UserProfileEditPage.tsx create mode 100644 public/app/features/profile/api.ts create mode 100644 public/app/features/profile/state/actions.ts create mode 100644 public/app/features/profile/state/reducers.test.ts create mode 100644 public/app/features/profile/types.ts diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 9b06fef9b9f..f717996f837 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -2,17 +2,17 @@ import React, { PureComponent } from 'react'; import { css } from '@emotion/css'; import { - Select, - Field, - Form, - Tooltip, - Icon, - stylesFactory, - Label, Button, - RadioButtonGroup, + Field, FieldSet, + Form, + Icon, + Label, + RadioButtonGroup, + Select, + stylesFactory, TimeZonePicker, + Tooltip, } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -142,6 +142,7 @@ export class SharedPreferences extends PureComponent { } + aria-label="User preferences home dashboard drop down" > } /> } /> - } /> + } + />
-
diff --git a/public/app/features/profile/UserProfileEditPage.test.tsx b/public/app/features/profile/UserProfileEditPage.test.tsx new file mode 100644 index 00000000000..085a2a67301 --- /dev/null +++ b/public/app/features/profile/UserProfileEditPage.test.tsx @@ -0,0 +1,279 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { within } from '@testing-library/dom'; +import { OrgRole } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; + +import { Props, UserProfileEditPage } from './UserProfileEditPage'; +import { initialUserState } from './state/reducers'; +import { getNavModel } from '../../core/selectors/navModel'; +import { backendSrv } from '../../core/services/backend_srv'; +import { TeamPermissionLevel } from '../../types'; + +const defaultProps: Props = { + ...initialUserState, + user: { + id: 1, + name: 'Test User', + email: 'test@test.com', + login: 'test', + isDisabled: false, + isGrafanaAdmin: false, + orgId: 0, + }, + teams: [ + { + id: 0, + name: 'Team One', + email: 'team.one@test.com', + avatarUrl: '/avatar/07d881f402480a2a511a9a15b5fa82c0', + memberCount: 2000, + permission: TeamPermissionLevel.Admin, + }, + ], + orgs: [ + { + name: 'Main', + orgId: 0, + role: OrgRole.Editor, + }, + { + name: 'Second', + orgId: 1, + role: OrgRole.Viewer, + }, + { + name: 'Third', + orgId: 2, + role: OrgRole.Admin, + }, + ], + sessions: [ + { + id: 0, + browser: 'Chrome', + browserVersion: '90', + clientIp: 'localhost', + createdAt: '2021-01-01 04:00:00', + device: 'Macbook Pro', + isActive: true, + os: 'Mac OS X', + osVersion: '11', + seenAt: new Date().toUTCString(), + }, + ], + navModel: getNavModel( + { + 'profile-settings': { + icon: 'sliders-v-alt', + id: 'profile-settings', + parentItem: { + id: 'profile', + text: 'Test User', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + }, + text: 'Preferences', + url: '/profile', + }, + }, + 'profile-settings' + ), + initUserProfilePage: jest.fn().mockResolvedValue(undefined), + revokeUserSession: jest.fn().mockResolvedValue(undefined), + changeUserOrg: jest.fn().mockResolvedValue(undefined), + updateUserProfile: jest.fn().mockResolvedValue(undefined), +}; + +function getSelectors() { + const dashboardSelect = () => screen.getByLabelText(/user preferences home dashboard drop down/i); + const timepickerSelect = () => screen.getByLabelText(selectors.components.TimeZonePicker.container); + const teamsTable = () => screen.getByRole('table', { name: /user teams table/i }); + const orgsTable = () => screen.getByRole('table', { name: /user organizations table/i }); + const sessionsTable = () => screen.getByRole('table', { name: /user sessions table/i }); + return { + name: () => screen.getByRole('textbox', { name: /^name$/i }), + email: () => screen.getByRole('textbox', { name: /email/i }), + username: () => screen.getByRole('textbox', { name: /username/i }), + saveProfile: () => screen.getByRole('button', { name: /edit user profile save button/i }), + dashboardSelect, + dashboardValue: () => within(dashboardSelect()).getByText(/default/i), + timepickerSelect, + timepickerValue: () => within(timepickerSelect()).getByText(/coordinated universal time/i), + savePreferences: () => screen.getByRole('button', { name: /user preferences save button/i }), + teamsTable, + teamsRow: () => within(teamsTable()).getByRole('row', { name: /team one team.one@test\.com 2000/i }), + orgsTable, + orgsEditorRow: () => within(orgsTable()).getByRole('row', { name: /main editor current/i }), + orgsViewerRow: () => within(orgsTable()).getByRole('row', { name: /second viewer select/i }), + orgsAdminRow: () => within(orgsTable()).getByRole('row', { name: /third admin select/i }), + sessionsTable, + sessionsRow: () => + within(sessionsTable()).getByRole('row', { + name: /now 2021-01-01 04:00:00 localhost chrome on mac os x 11/i, + }), + }; +} + +async function getTestContext(overrides: Partial = {}) { + jest.clearAllMocks(); + const putSpy = jest.spyOn(backendSrv, 'put'); + const getSpy = jest + .spyOn(backendSrv, 'get') + .mockResolvedValue({ timezone: 'UTC', homeDashboardId: 0, theme: 'dark' }); + const searchSpy = jest.spyOn(backendSrv, 'search').mockResolvedValue([]); + + const props = { ...defaultProps, ...overrides }; + const { rerender } = render(); + + await waitFor(() => expect(props.initUserProfilePage).toHaveBeenCalledTimes(1)); + + return { rerender, putSpy, getSpy, searchSpy, props }; +} + +describe('UserProfileEditPage', () => { + describe('when loading user', () => { + it('should show loading placeholder', async () => { + await getTestContext({ user: null }); + + expect(screen.getByText(/loading \.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('when user has loaded', () => { + it('should show edit profile form', async () => { + await getTestContext(); + + const { name, email, username, saveProfile } = getSelectors(); + expect(screen.getByText(/edit profile/i)).toBeInTheDocument(); + expect(name()).toBeInTheDocument(); + expect(name()).toHaveValue('Test User'); + expect(email()).toBeInTheDocument(); + expect(email()).toHaveValue('test@test.com'); + expect(username()).toBeInTheDocument(); + expect(username()).toHaveValue('test'); + expect(saveProfile()).toBeInTheDocument(); + }); + + it('should show shared preferences', async () => { + await getTestContext(); + + const { dashboardSelect, dashboardValue, timepickerSelect, timepickerValue, savePreferences } = getSelectors(); + expect(screen.getByRole('group', { name: /preferences/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /default/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /dark/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /light/i })).toBeInTheDocument(); + expect(dashboardSelect()).toBeInTheDocument(); + expect(dashboardValue()).toBeInTheDocument(); + expect(timepickerSelect()).toBeInTheDocument(); + expect(timepickerValue()).toBeInTheDocument(); + expect(savePreferences()).toBeInTheDocument(); + }); + + describe('and teams are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ teamsAreLoading: true }); + + expect(screen.getByText(/loading teams\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and teams are loaded', () => { + it('should show teams', async () => { + await getTestContext(); + + const { teamsTable, teamsRow } = getSelectors(); + expect(screen.getByRole('heading', { name: /teams/i })).toBeInTheDocument(); + expect(teamsTable()).toBeInTheDocument(); + expect(teamsRow()).toBeInTheDocument(); + }); + }); + + describe('and organizations are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ orgsAreLoading: true }); + + expect(screen.getByText(/loading organizations\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and organizations are loaded', () => { + it('should show organizations', async () => { + await getTestContext(); + + const { orgsTable, orgsEditorRow, orgsViewerRow, orgsAdminRow } = getSelectors(); + expect(screen.getByRole('heading', { name: /organizations/i })).toBeInTheDocument(); + expect(orgsTable()).toBeInTheDocument(); + expect(orgsEditorRow()).toBeInTheDocument(); + expect(orgsViewerRow()).toBeInTheDocument(); + expect(orgsAdminRow()).toBeInTheDocument(); + }); + }); + + describe('and sessions are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ sessionsAreLoading: true }); + + expect(screen.getByText(/loading sessions\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and sessions are loaded', () => { + it('should show sessions', async () => { + await getTestContext(); + + const { sessionsTable, sessionsRow } = getSelectors(); + expect(sessionsTable()).toBeInTheDocument(); + expect(sessionsRow()).toBeInTheDocument(); + }); + }); + + describe('and user is edited and saved', () => { + it('should call updateUserProfile', async () => { + const { props } = await getTestContext(); + + const { email, saveProfile } = getSelectors(); + userEvent.clear(email()); + await userEvent.type(email(), 'test@test.se'); + userEvent.click(saveProfile()); + + await waitFor(() => expect(props.updateUserProfile).toHaveBeenCalledTimes(1)); + expect(props.updateUserProfile).toHaveBeenCalledWith({ + email: 'test@test.se', + login: 'test', + name: 'Test User', + }); + }); + }); + + describe('and organization is changed', () => { + it('should call changeUserOrg', async () => { + const { props } = await getTestContext(); + const orgsAdminSelectButton = () => + within(getSelectors().orgsAdminRow()).getByRole('button', { name: /select/i }); + + userEvent.click(orgsAdminSelectButton()); + + await waitFor(() => expect(props.changeUserOrg).toHaveBeenCalledTimes(1)); + expect(props.changeUserOrg).toHaveBeenCalledWith({ + name: 'Third', + orgId: 2, + role: 'Admin', + }); + }); + }); + + describe('and session is revoked', () => { + it('should call revokeUserSession', async () => { + const { props } = await getTestContext(); + const sessionsRevokeButton = () => within(getSelectors().sessionsRow()).getByRole('button'); + + userEvent.click(sessionsRevokeButton()); + + await waitFor(() => expect(props.revokeUserSession).toHaveBeenCalledTimes(1)); + expect(props.revokeUserSession).toHaveBeenCalledWith(0); + }); + }); + }); +}); diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx new file mode 100644 index 00000000000..8ffdf34a7b7 --- /dev/null +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { useMount } from 'react-use'; +import { hot } from 'react-hot-loader'; +import { NavModel } from '@grafana/data'; +import { VerticalGroup } from '@grafana/ui'; + +import { getNavModel } from 'app/core/selectors/navModel'; +import { StoreState } from 'app/types'; +import Page from 'app/core/components/Page/Page'; +import { changeUserOrg, initUserProfilePage, revokeUserSession, updateUserProfile } from './state/actions'; +import UserProfileEditForm from './UserProfileEditForm'; +import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; +import { UserTeams } from './UserTeams'; +import UserOrganizations from './UserOrganizations'; +import UserSessions from './UserSessions'; + +export interface OwnProps { + navModel: NavModel; +} + +function mapStateToProps(state: StoreState) { + const userState = state.user; + const { user, teams, orgs, sessions, teamsAreLoading, orgsAreLoading, sessionsAreLoading, isUpdating } = userState; + return { + navModel: getNavModel(state.navIndex, 'profile-settings'), + orgsAreLoading, + sessionsAreLoading, + teamsAreLoading, + orgs, + sessions, + teams, + isUpdating, + user, + }; +} + +const mapDispatchToProps = { + initUserProfilePage, + revokeUserSession, + changeUserOrg, + updateUserProfile, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +export type Props = OwnProps & ConnectedProps; + +export function UserProfileEditPage({ + navModel, + orgsAreLoading, + sessionsAreLoading, + teamsAreLoading, + initUserProfilePage, + orgs, + sessions, + teams, + isUpdating, + user, + revokeUserSession, + changeUserOrg, + updateUserProfile, +}: Props) { + useMount(() => initUserProfilePage()); + + return ( + + + + + + + + + + + + ); +} + +export default hot(module)(connector(UserProfileEditPage)); diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index 545a0dd5b46..ac5f55a6135 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -1,20 +1,14 @@ import React, { PureComponent } from 'react'; -import { UserDTO, UserSession } from 'app/types'; -import { LoadingPlaceholder, Button, Icon } from '@grafana/ui'; +import { UserSession } from 'app/types'; +import { Button, Icon, LoadingPlaceholder } from '@grafana/ui'; export interface Props { - user: UserDTO; sessions: UserSession[]; isLoading: boolean; - loadSessions: () => void; revokeUserSession: (tokenId: number) => void; } export class UserSessions extends PureComponent { - componentDidMount() { - this.props.loadSessions(); - } - render() { const { isLoading, sessions, revokeUserSession } = this.props; @@ -28,7 +22,7 @@ export class UserSessions extends PureComponent { <>

Sessions

- +
diff --git a/public/app/features/profile/UserTeams.tsx b/public/app/features/profile/UserTeams.tsx index c9e2ffc4a88..61c701ae259 100644 --- a/public/app/features/profile/UserTeams.tsx +++ b/public/app/features/profile/UserTeams.tsx @@ -5,14 +5,9 @@ import { LoadingPlaceholder } from '@grafana/ui'; export interface Props { teams: Team[]; isLoading: boolean; - loadTeams: () => void; } export class UserTeams extends PureComponent { - componentDidMount() { - this.props.loadTeams(); - } - render() { const { isLoading, teams } = this.props; @@ -28,7 +23,7 @@ export class UserTeams extends PureComponent {

Teams

-
Last seen
+
diff --git a/public/app/features/profile/api.ts b/public/app/features/profile/api.ts new file mode 100644 index 00000000000..7adc848a5ec --- /dev/null +++ b/public/app/features/profile/api.ts @@ -0,0 +1,57 @@ +import { getBackendSrv } from '@grafana/runtime'; + +import { ChangePasswordFields, ProfileUpdateFields } from './types'; +import { Team, UserDTO, UserOrg, UserSession } from '../../types'; + +async function changePassword(payload: ChangePasswordFields): Promise { + try { + await getBackendSrv().put('/api/user/password', payload); + } catch (err) { + console.error(err); + } +} + +function loadUser(): Promise { + return getBackendSrv().get('/api/user'); +} + +function loadTeams(): Promise { + return getBackendSrv().get('/api/user/teams'); +} + +function loadOrgs(): Promise { + return getBackendSrv().get('/api/user/orgs'); +} + +function loadSessions(): Promise { + return getBackendSrv().get('/api/user/auth-tokens'); +} + +async function revokeUserSession(tokenId: number): Promise { + await getBackendSrv().post('/api/user/revoke-auth-token', { + authTokenId: tokenId, + }); +} + +async function setUserOrg(org: UserOrg): Promise { + await getBackendSrv().post('/api/user/using/' + org.orgId, {}); +} + +async function updateUserProfile(payload: ProfileUpdateFields): Promise { + try { + await getBackendSrv().put('/api/user', payload); + } catch (err) { + console.error(err); + } +} + +export const api = { + changePassword, + revokeUserSession, + loadUser, + loadSessions, + loadOrgs, + loadTeams, + setUserOrg, + updateUserProfile, +}; diff --git a/public/app/features/profile/state/actions.ts b/public/app/features/profile/state/actions.ts new file mode 100644 index 00000000000..2155b1088b2 --- /dev/null +++ b/public/app/features/profile/state/actions.ts @@ -0,0 +1,89 @@ +import { config } from '@grafana/runtime'; + +import { ChangePasswordFields, ProfileUpdateFields } from '../types'; +import { ThunkResult, UserOrg } from '../../../types'; +import { + initLoadOrgs, + initLoadSessions, + initLoadTeams, + orgsLoaded, + sessionsLoaded, + setUpdating, + teamsLoaded, + userLoaded, + userSessionRevoked, +} from './reducers'; +import { api } from '../api'; + +export function changePassword(payload: ChangePasswordFields): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.changePassword(payload); + dispatch(setUpdating({ updating: false })); + }; +} + +export function initUserProfilePage(): ThunkResult { + return async function (dispatch) { + await dispatch(loadUser()); + dispatch(loadTeams()); + dispatch(loadOrgs()); + dispatch(loadSessions()); + }; +} + +export function loadUser(): ThunkResult { + return async function (dispatch) { + const user = await api.loadUser(); + dispatch(userLoaded({ user })); + }; +} + +function loadTeams(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadTeams()); + const teams = await api.loadTeams(); + dispatch(teamsLoaded({ teams })); + }; +} + +function loadOrgs(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadOrgs()); + const orgs = await api.loadOrgs(); + dispatch(orgsLoaded({ orgs })); + }; +} + +function loadSessions(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadSessions()); + const sessions = await api.loadSessions(); + dispatch(sessionsLoaded({ sessions })); + }; +} + +export function revokeUserSession(tokenId: number): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.revokeUserSession(tokenId); + dispatch(userSessionRevoked({ tokenId })); + }; +} + +export function changeUserOrg(org: UserOrg): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.setUserOrg(org); + window.location.href = config.appSubUrl + '/profile'; + }; +} + +export function updateUserProfile(payload: ProfileUpdateFields): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.updateUserProfile(payload); + await dispatch(loadUser()); + dispatch(setUpdating({ updating: false })); + }; +} diff --git a/public/app/features/profile/state/reducers.test.ts b/public/app/features/profile/state/reducers.test.ts new file mode 100644 index 00000000000..2bccd7434a6 --- /dev/null +++ b/public/app/features/profile/state/reducers.test.ts @@ -0,0 +1,187 @@ +import { reducerTester } from '../../../../test/core/redux/reducerTester'; +import { OrgRole, TeamPermissionLevel } from '../../../types'; +import { + initialUserState, + orgsLoaded, + sessionsLoaded, + setUpdating, + teamsLoaded, + updateTimeZone, + userLoaded, + userReducer, + userSessionRevoked, + UserState, +} from './reducers'; + +describe('userReducer', () => { + describe('when updateTimeZone is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState }) + .whenActionIsDispatched(updateTimeZone({ timeZone: 'xyz' })) + .thenStateShouldEqual({ ...initialUserState, timeZone: 'xyz' }); + }); + }); + + describe('when setUpdating is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, isUpdating: false }) + .whenActionIsDispatched(setUpdating({ updating: true })) + .thenStateShouldEqual({ ...initialUserState, isUpdating: true }); + }); + }); + + describe('when userLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, user: null }) + .whenActionIsDispatched( + userLoaded({ + user: { + id: 2021, + email: 'test@test.com', + isDisabled: true, + login: 'test', + name: 'Test Account', + isGrafanaAdmin: false, + }, + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + user: { + id: 2021, + email: 'test@test.com', + isDisabled: true, + login: 'test', + name: 'Test Account', + isGrafanaAdmin: false, + }, + }); + }); + }); + + describe('when teamsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, teamsAreLoading: true }) + .whenActionIsDispatched( + teamsLoaded({ + teams: [ + { + id: 1, + email: 'team@team.com', + name: 'Team', + avatarUrl: '/avatar/12345', + memberCount: 4, + permission: TeamPermissionLevel.Admin, + }, + ], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + teamsAreLoading: false, + teams: [ + { + id: 1, + email: 'team@team.com', + name: 'Team', + avatarUrl: '/avatar/12345', + memberCount: 4, + permission: TeamPermissionLevel.Admin, + }, + ], + }); + }); + }); + + describe('when orgsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, orgsAreLoading: true }) + .whenActionIsDispatched( + orgsLoaded({ + orgs: [{ orgId: 1, name: 'Main', role: OrgRole.Viewer }], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + orgsAreLoading: false, + orgs: [{ orgId: 1, name: 'Main', role: OrgRole.Viewer }], + }); + }); + }); + + describe('when sessionsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, sessionsAreLoading: true }) + .whenActionIsDispatched( + sessionsLoaded({ + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: '2021-01-01 04:00:00', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '1996-01-01 04:00:00', + }, + ], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + sessionsAreLoading: false, + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: 'December 31, 2020', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '25 years ago', + }, + ], + }); + }); + }); + + describe('when userSessionRevoked is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { + ...initialUserState, + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: '2021-01-01', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '1996-01-01', + }, + ], + }) + .whenActionIsDispatched(userSessionRevoked({ tokenId: 1 })) + .thenStateShouldEqual({ + ...initialUserState, + sessions: [], + }); + }); + }); +}); diff --git a/public/app/features/profile/state/reducers.ts b/public/app/features/profile/state/reducers.ts index 60571e4f711..111fa1fc3ed 100644 --- a/public/app/features/profile/state/reducers.ts +++ b/public/app/features/profile/state/reducers.ts @@ -1,40 +1,115 @@ -import { isString, isEmpty, set } from 'lodash'; -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { UserState, ThunkResult } from 'app/types'; +import { isEmpty, isString, set } from 'lodash'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { dateTimeFormat, dateTimeFormatTimeAgo, TimeZone } from '@grafana/data'; + +import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types'; import config from 'app/core/config'; -import { TimeZone } from '@grafana/data'; import { contextSrv } from 'app/core/core'; -export const initialState: UserState = { +export interface UserState { + orgId: number; + timeZone: TimeZone; + user: UserDTO | null; + teams: Team[]; + orgs: UserOrg[]; + sessions: UserSession[]; + teamsAreLoading: boolean; + orgsAreLoading: boolean; + sessionsAreLoading: boolean; + isUpdating: boolean; +} + +export const initialUserState: UserState = { orgId: config.bootData.user.orgId, timeZone: config.bootData.user.timezone, + orgsAreLoading: false, + sessionsAreLoading: false, + teamsAreLoading: false, + isUpdating: false, + orgs: [], + sessions: [], + teams: [], + user: null, }; export const slice = createSlice({ name: 'user/profile', - initialState, + initialState: initialUserState, reducers: { - updateTimeZone: (state, action: PayloadAction): UserState => { - return { - ...state, - timeZone: action.payload, - }; + updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => { + state.timeZone = action.payload.timeZone; + }, + setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => { + state.isUpdating = action.payload.updating; + }, + userLoaded: (state, action: PayloadAction<{ user: UserDTO }>) => { + state.user = action.payload.user; + }, + initLoadTeams: (state, action: PayloadAction) => { + state.teamsAreLoading = true; + }, + teamsLoaded: (state, action: PayloadAction<{ teams: Team[] }>) => { + state.teams = action.payload.teams; + state.teamsAreLoading = false; + }, + initLoadOrgs: (state, action: PayloadAction) => { + state.orgsAreLoading = true; + }, + orgsLoaded: (state, action: PayloadAction<{ orgs: UserOrg[] }>) => { + state.orgs = action.payload.orgs; + state.orgsAreLoading = false; + }, + initLoadSessions: (state, action: PayloadAction) => { + state.sessionsAreLoading = true; + }, + sessionsLoaded: (state, action: PayloadAction<{ sessions: UserSession[] }>) => { + const sorted = action.payload.sessions.sort((a, b) => Number(b.isActive) - Number(a.isActive)); // Show active sessions first + state.sessions = sorted.map((session) => ({ + id: session.id, + isActive: session.isActive, + seenAt: dateTimeFormatTimeAgo(session.seenAt), + createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + })); + state.sessionsAreLoading = false; + }, + userSessionRevoked: (state, action: PayloadAction<{ tokenId: number }>) => { + state.sessions = state.sessions.filter((session: UserSession) => { + return session.id !== action.payload.tokenId; + }); + state.isUpdating = false; }, }, }); export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult => { return async (dispatch) => { - const { updateTimeZone } = slice.actions; - if (!isString(timeZone) || isEmpty(timeZone)) { timeZone = config?.bootData?.user?.timezone; } set(contextSrv, 'user.timezone', timeZone); - dispatch(updateTimeZone(timeZone)); + dispatch(updateTimeZone({ timeZone })); }; }; +export const { + setUpdating, + initLoadOrgs, + orgsLoaded, + initLoadTeams, + teamsLoaded, + userLoaded, + userSessionRevoked, + initLoadSessions, + sessionsLoaded, + updateTimeZone, +} = slice.actions; + export const userReducer = slice.reducer; export default { user: slice.reducer }; diff --git a/public/app/features/profile/state/selectors.ts b/public/app/features/profile/state/selectors.ts index d6d53dade9d..cd984b6a6c6 100644 --- a/public/app/features/profile/state/selectors.ts +++ b/public/app/features/profile/state/selectors.ts @@ -1,3 +1,3 @@ -import { UserState } from 'app/types'; +import { UserState } from './reducers'; export const getTimeZone = (state: UserState) => state.timeZone; diff --git a/public/app/features/profile/types.ts b/public/app/features/profile/types.ts new file mode 100644 index 00000000000..c5692e188b9 --- /dev/null +++ b/public/app/features/profile/types.ts @@ -0,0 +1,11 @@ +export interface ChangePasswordFields { + oldPassword: string; + newPassword: string; + confirmNew: string; +} + +export interface ProfileUpdateFields { + name: string; + email: string; + login: string; +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 9520220e7e1..253dfa7e49a 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -203,7 +203,7 @@ export function getAppRoutes(): RouteDescriptor[] { { path: '/profile', component: SafeDynamicImport( - () => import(/* webpackChunkName: "UserProfileEdit" */ 'app/features/profile/UserProfileEdit') + () => import(/* webpackChunkName: "UserProfileEditPage" */ 'app/features/profile/UserProfileEditPage') ), }, { diff --git a/public/app/types/store.ts b/public/app/types/store.ts index 55258e032d2..d793a622b46 100644 --- a/public/app/types/store.ts +++ b/public/app/types/store.ts @@ -8,7 +8,7 @@ import { FolderState } from './folders'; import { DashboardState } from './dashboard'; import { DataSourceSettingsState, DataSourcesState } from './datasources'; import { ExploreState } from './explore'; -import { UserAdminState, UserListAdminState, UsersState, UserState } from './user'; +import { UserAdminState, UserListAdminState, UsersState } from './user'; import { OrganizationState } from './organization'; import { AppNotificationsState } from './appNotifications'; import { PluginsState } from './plugins'; @@ -17,6 +17,7 @@ import { PanelEditorState } from '../features/dashboard/components/PanelEditor/s import { ApiKeysState } from './apiKeys'; import { TemplatingState } from '../features/variables/state/reducers'; import { ImportDashboardState } from '../features/manage-dashboards/state/reducers'; +import { UserState } from 'app/features/profile/state/reducers'; export interface StoreState { navIndex: NavIndex; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 076b5977182..0113e7dbb1a 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,4 +1,3 @@ -import { TimeZone } from '@grafana/data'; import { OrgRole } from '.'; export interface OrgUser { @@ -69,11 +68,6 @@ export interface UsersState { hasFetched: boolean; } -export interface UserState { - orgId: number; - timeZone: TimeZone; -} - export interface UserSession { id: number; createdAt: string; From b74a502dc466a464516aeb5c3e3f7da1ba895162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= Date: Tue, 25 May 2021 15:35:43 +0200 Subject: [PATCH 04/41] API: Use SettingsProvider on GET settings handler (#34632) --- pkg/api/admin.go | 17 ++--------------- pkg/api/api.go | 2 +- pkg/setting/provider.go | 21 ++++++++++++++++++++- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 090e4f07886..643db782be2 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -4,23 +4,10 @@ import ( "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" ) -func AdminGetSettings(c *models.ReqContext) response.Response { - settings := make(map[string]interface{}) - - for _, section := range setting.Raw.Sections() { - jsonSec := make(map[string]interface{}) - settings[section.Name()] = jsonSec - - for _, key := range section.Keys() { - keyName := key.Name() - jsonSec[keyName] = setting.RedactedValue(keyName, key.Value()) - } - } - - return response.JSON(200, settings) +func (hs *HTTPServer) AdminGetSettings(_ *models.ReqContext) response.Response { + return response.JSON(200, hs.SettingsProvider.Current()) } func AdminGetStats(c *models.ReqContext) response.Response { diff --git a/pkg/api/api.go b/pkg/api/api.go index 289f662b2f0..eed0833cbb7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -440,7 +440,7 @@ func (hs *HTTPServer) registerRoutes() { // admin api r.Group("/api/admin", func(adminRoute routing.RouteRegister) { - adminRoute.Get("/settings", reqGrafanaAdmin, routing.Wrap(AdminGetSettings)) + adminRoute.Get("/settings", reqGrafanaAdmin, routing.Wrap(hs.AdminGetSettings)) adminRoute.Get("/stats", reqGrafanaAdmin, routing.Wrap(AdminGetStats)) adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, bind(dtos.PauseAllAlertsCommand{}), routing.Wrap(PauseAllAlerts)) diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index cddc8177981..5e387669833 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -32,7 +32,13 @@ func (v ValidationError) Error() string { // Provider is a settings provider abstraction // with thread-safety and runtime updates. type Provider interface { - // Update + // Current returns a SettingsBag with a static copy of + // the current configured pairs of key/values for each + // configuration section. + Current() SettingsBag + // Update receives a SettingsBag with the pairs of key/values + // to be updated per section and a SettingsRemovals with the + // section keys to be removed. Update(updates SettingsBag, removals SettingsRemovals) error // KeyValue returns a key-value abstraction // for the given pair of section and key. @@ -94,6 +100,19 @@ func (o OSSImpl) Init() error { return nil } +func (o OSSImpl) Current() SettingsBag { + settingsCopy := make(SettingsBag) + + for _, section := range o.Cfg.Raw.Sections() { + settingsCopy[section.Name()] = make(map[string]string) + for _, key := range section.Keys() { + settingsCopy[section.Name()][key.Name()] = RedactedValue(key.Name(), key.Value()) + } + } + + return settingsCopy +} + func (OSSImpl) Update(SettingsBag, SettingsRemovals) error { return errors.New("oss settings provider do not have support for settings updates") } From 0f4806db8a6fba20974a7caf7755f83ac5a7944f Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Tue, 25 May 2021 15:36:01 +0200 Subject: [PATCH 05/41] Access control: Rename predefined roles to fixed roles (code) (#34469) * s/grafana:roles:/fixed:/ * Update free text references to predefined roles --- .../ossaccesscontrol/ossaccesscontrol.go | 4 +-- pkg/services/accesscontrol/roles.go | 30 +++++++++---------- pkg/services/accesscontrol/roles_test.go | 10 +++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index e94a3a137c2..b4f40a5513e 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -62,9 +62,9 @@ func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user builtinRoles := ac.GetUserBuiltInRoles(user) permissions := make([]*accesscontrol.Permission, 0) for _, builtin := range builtinRoles { - if roleNames, ok := accesscontrol.PredefinedRoleGrants[builtin]; ok { + if roleNames, ok := accesscontrol.FixedRoleGrants[builtin]; ok { for _, name := range roleNames { - r, exists := accesscontrol.PredefinedRoles[name] + r, exists := accesscontrol.FixedRoles[name] if !exists { continue } diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go index 3a3d503b4ed..44fd9a53e05 100644 --- a/pkg/services/accesscontrol/roles.go +++ b/pkg/services/accesscontrol/roles.go @@ -135,13 +135,13 @@ var provisioningAdminRole = RoleDTO{ }, } -// PredefinedRoles provides a map of permission sets/roles which can be +// FixedRoles provides a map of permission sets/roles which can be // assigned to a set of users. When adding a new resource protected by // Grafana access control the default permissions should be added to a -// new predefined role in this set so that users can access the new -// resource. PredefinedRoleGrants lists which organization roles are -// assigned which predefined roles in this list. -var PredefinedRoles = map[string]RoleDTO{ +// new fixed role in this set so that users can access the new +// resource. FixedRoleGrants lists which built-in roles are +// assigned which fixed roles in this list. +var FixedRoles = map[string]RoleDTO{ usersAdminRead: usersAdminReadRole, usersAdminEdit: usersAdminEditRole, @@ -155,21 +155,21 @@ var PredefinedRoles = map[string]RoleDTO{ } const ( - usersAdminEdit = "grafana:roles:users:admin:edit" - usersAdminRead = "grafana:roles:users:admin:read" + usersAdminEdit = "fixed:users:admin:edit" + usersAdminRead = "fixed:users:admin:read" - usersOrgEdit = "grafana:roles:users:org:edit" - usersOrgRead = "grafana:roles:users:org:read" + usersOrgEdit = "fixed:users:org:edit" + usersOrgRead = "fixed:users:org:read" - ldapAdminEdit = "grafana:roles:ldap:admin:edit" - ldapAdminRead = "grafana:roles:ldap:admin:read" + ldapAdminEdit = "fixed:ldap:admin:edit" + ldapAdminRead = "fixed:ldap:admin:read" - provisioningAdmin = "grafana:roles:provisioning:admin" + provisioningAdmin = "fixed:provisioning:admin" ) -// PredefinedRoleGrants specifies which organization roles are assigned -// to which set of PredefinedRoles by default. Alphabetically sorted. -var PredefinedRoleGrants = map[string][]string{ +// FixedRoleGrants specifies which built-in roles are assigned +// to which set of FixedRoles by default. Alphabetically sorted. +var FixedRoleGrants = map[string][]string{ RoleGrafanaAdmin: { ldapAdminEdit, ldapAdminRead, diff --git a/pkg/services/accesscontrol/roles_test.go b/pkg/services/accesscontrol/roles_test.go index f10a951847e..750abb43d63 100644 --- a/pkg/services/accesscontrol/roles_test.go +++ b/pkg/services/accesscontrol/roles_test.go @@ -9,10 +9,10 @@ import ( ) func TestPredefinedRoles(t *testing.T) { - for name, r := range PredefinedRoles { + for name, r := range FixedRoles { assert.Truef(t, - strings.HasPrefix(name, "grafana:roles:"), - "expected all predefined roles to be prefixed by 'grafana:roles:', found role '%s'", name, + strings.HasPrefix(name, "fixed:"), + "expected all fixed roles to be prefixed by 'fixed:', found role '%s'", name, ) assert.Equal(t, name, r.Name) assert.NotZero(t, r.Version) @@ -21,7 +21,7 @@ func TestPredefinedRoles(t *testing.T) { } func TestPredefinedRoleGrants(t *testing.T) { - for _, v := range PredefinedRoleGrants { + for _, v := range FixedRoleGrants { assert.True(t, sort.SliceIsSorted(v, func(i, j int) bool { return v[i] < v[j] @@ -29,7 +29,7 @@ func TestPredefinedRoleGrants(t *testing.T) { "require role grant lists to be sorted", ) for _, r := range v { - assert.Contains(t, PredefinedRoles, r) + assert.Contains(t, FixedRoles, r) } } } From adbe3834560611cd87817acf8802de4180364972 Mon Sep 17 00:00:00 2001 From: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Date: Tue, 25 May 2021 10:51:28 -0400 Subject: [PATCH 06/41] Update alert doc structure (#34659) * Restructured and fixed broken relrefs. * Added relrefs. * created shared alerts section. --- .../sources/administration/image_rendering.md | 2 +- .../set-up-for-high-availability.md | 2 +- docs/sources/alerting/_index.md | 15 +++--- docs/sources/alerting/difference-old-new.md | 6 +-- docs/sources/alerting/old-alerting/_index.md | 52 +++++++++++++++++++ .../add-notification-template.md | 0 .../{ => old-alerting}/create-alerts.md | 2 +- .../{ => old-alerting}/notifications.md | 10 ++-- .../{ => old-alerting}/pause-an-alert-rule.md | 0 .../{ => old-alerting}/troubleshoot-alerts.md | 0 .../{ => old-alerting}/view-alerts.md | 0 .../{8.0 => unified-alerting}/_index.md | 9 ++-- docs/sources/basics/timeseries-dimensions.md | 2 +- .../developers/plugins/backend/_index.md | 2 +- docs/sources/getting-started/_index.md | 2 +- .../alerting_notification_channels.md | 2 +- docs/sources/panels/panel-editor.md | 2 +- docs/sources/shared/alerts/first-step.md | 11 ++++ docs/sources/whatsnew/whats-new-in-v7-4.md | 2 +- 19 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 docs/sources/alerting/old-alerting/_index.md rename docs/sources/alerting/{ => old-alerting}/add-notification-template.md (100%) rename docs/sources/alerting/{ => old-alerting}/create-alerts.md (98%) rename docs/sources/alerting/{ => old-alerting}/notifications.md (94%) rename docs/sources/alerting/{ => old-alerting}/pause-an-alert-rule.md (100%) rename docs/sources/alerting/{ => old-alerting}/troubleshoot-alerts.md (100%) rename docs/sources/alerting/{ => old-alerting}/view-alerts.md (100%) rename docs/sources/alerting/{8.0 => unified-alerting}/_index.md (63%) create mode 100644 docs/sources/shared/alerts/first-step.md diff --git a/docs/sources/administration/image_rendering.md b/docs/sources/administration/image_rendering.md index f43f87df99d..cee132a6c7b 100755 --- a/docs/sources/administration/image_rendering.md +++ b/docs/sources/administration/image_rendering.md @@ -7,7 +7,7 @@ weight = 300 # Image rendering -Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/notifications.md" >}}). +Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}). >**Note:** Image rendering of dashboards is not supported at this time. diff --git a/docs/sources/administration/set-up-for-high-availability.md b/docs/sources/administration/set-up-for-high-availability.md index 7a4a2636aa6..2d40ef721d3 100644 --- a/docs/sources/administration/set-up-for-high-availability.md +++ b/docs/sources/administration/set-up-for-high-availability.md @@ -22,7 +22,7 @@ Grafana will now persist all long term data in the database. How to configure th ## Alerting -Currently alerting supports a limited form of high availability. [Alert notifications]({{< relref "../alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. +Currently alerting supports a limited form of high availability. [Alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. ## User sessions diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index bd4e93282cf..db2417aec8b 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -8,21 +8,24 @@ weight = 110 Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. +> **Note:** In Grafana 8.0, you can optionally choose the beta version of our new [unified altering]({{< relref "./unified-alerting/_index.md" >}}) system. Out of the box, Grafana still supports the [old alters]({{< relref "./old-alerting/_index.md" >}}) . + + Alerts have four main components: - Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying. -- Contact point - A channel for sending notifications when the conditions of an alert rule are met. +- Contact point - A channel for sending notifications when the conditions of an alert rule are met. (Available in unified alerting only.) - Notification policy - A set of matching and grouping criteria used to determine where, and how frequently, to send notifications. -- Silences - Date and matching criteria used to silence notifications. +- Silences - Date and matching criteria used to silence notifications. (Available in unified alerting only.) ## Alert tasks You can perform the following tasks for alerts: -- [Create an alert rule]({{< relref "create-alerts.md" >}}) -- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}}) -- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}}) -- [Add or edit an alert contact point]({{< relref "notifications.md" >}}) +- [Create an alert rule]({{< relref "./old-alerting/create-alerts.md" >}}) +- [View existing alert rules and their current state]({{< relref "./old-alerting/view-alerts.md" >}}) +- [Test alert rules and troubleshoot]({{< relref "./old-alerting/troubleshoot-alerts.md" >}}) +- [Add or edit an alert contact point]({{< relref "./old-alerting/notifications.md" >}}) ## Clustering diff --git a/docs/sources/alerting/difference-old-new.md b/docs/sources/alerting/difference-old-new.md index c3d15851e0f..3d34ca24a3d 100644 --- a/docs/sources/alerting/difference-old-new.md +++ b/docs/sources/alerting/difference-old-new.md @@ -1,11 +1,11 @@ +++ -title = "Difference between new and old alerts" +title = " Difference between unified and old Grafana alerts" description = "Pause an existing alert rule" keywords = ["grafana", "alerting", "guide", "rules", "view"] -weight = 400 +weight = 112 +++ -# Difference between new and old alerts +# Difference between unified and old Grafana alerts The `ngalert` feature toggle enables the beta version of our new alerting system. diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/alerting/old-alerting/_index.md new file mode 100644 index 00000000000..d7f127896ec --- /dev/null +++ b/docs/sources/alerting/old-alerting/_index.md @@ -0,0 +1,52 @@ ++++ +title = "Old Grafana Alerts" +aliases = ["/docs/grafana/latest/alerting/rules/", "/docs/grafana/latest/alerting/metrics/"] +weight = 114 ++++ + +# Old Grafana alerts + +Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. + +Alerts have four main components: + +- Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying. +- Contact point - A channel for sending notifications when the conditions of an alert rule are met. +- Notification policy - A set of matching and grouping criteria used to determine where, and how frequently, to send notifications. +- Silences - Date and matching criteria used to silence notifications. + +## Alert tasks + +You can perform the following tasks for alerts: + +- [Create an alert rule]({{< relref "create-alerts.md" >}}) +- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}}) +- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}}) +- [Add or edit an alert contact point]({{< relref "notifications.md" >}}) + +## Clustering + +Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future. + +## Alert evaluation + +Grafana managed alerts are evaluated by the Grafana backend. Rule evaluations are scheduled, according to the alert rule configuration, and queries are evaluated by an engine that is part of core Grafana. + +Alert rules can only query backend data sources with alerting enabled: +- builtin or developed and maintained by grafana: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`, +`Google Cloud Monitoring`, `Cloudwatch`, `Azure Monitor`, `MySQL`, `PostgreSQL`, `MSSQL`, `OpenTSDB`, `Oracle`, and `Azure Data Explorer` +- any community backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../../developers/plugins/metadata.md" >}})) + +## Metrics from the alert engine + +The alert engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../administration/view-server/internal-metrics.md" >}}). + +Metric Name | Type | Description +---------- | ----------- | ---------- +`alerting.alerts` | gauge | How many alerts by state +`alerting.request_duration_seconds` | histogram | Histogram of requests to the Alerting API +`alerting.active_configurations` | gauge | The number of active, non default alertmanager configurations for grafana managed alerts +`alerting.rule_evaluations_total` | counter | The total number of rule evaluations +`alerting.rule_evaluation_failures_total` | counter | The total number of rule evaluation failures +`alerting.rule_evaluation_duration_seconds` | summary | The duration for a rule to execute +`alerting.rule_group_rules` | gauge | The number of rules diff --git a/docs/sources/alerting/add-notification-template.md b/docs/sources/alerting/old-alerting/add-notification-template.md similarity index 100% rename from docs/sources/alerting/add-notification-template.md rename to docs/sources/alerting/old-alerting/add-notification-template.md diff --git a/docs/sources/alerting/create-alerts.md b/docs/sources/alerting/old-alerting/create-alerts.md similarity index 98% rename from docs/sources/alerting/create-alerts.md rename to docs/sources/alerting/old-alerting/create-alerts.md index d986b1390d3..0e68935c589 100644 --- a/docs/sources/alerting/create-alerts.md +++ b/docs/sources/alerting/old-alerting/create-alerts.md @@ -37,7 +37,7 @@ This section describes the fields you fill out to create an alert. - **Evaluate every -** Specify how often the scheduler should evaluate the alert rule. This is referred to as the _evaluation interval_. - **For -** Specify how long the query needs to violate the configured thresholds before the alert notification triggers. -You can set a minimum evaluation interval in the `alerting.min_interval_seconds` configuration field, to set a minimum time between evaluations. Refer to [Configuration]({{< relref "../administration/configuration.md" >}}#min-interval-seconds) for more information. +You can set a minimum evaluation interval in the `alerting.min_interval_seconds` configuration field, to set a minimum time between evaluations. Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}#min-interval-seconds) for more information. > **Caution:** Do not use `For` with the `If no data or all values are null` setting set to `No Data`. The triggering of `No Data` will trigger instantly and not take `For` into consideration. This may also result in that an OK notification not being sent if alert transitions from `No Data -> Pending -> OK`. diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/old-alerting/notifications.md similarity index 94% rename from docs/sources/alerting/notifications.md rename to docs/sources/alerting/old-alerting/notifications.md index 5550bbce827..e89bf56b8a8 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/old-alerting/notifications.md @@ -74,7 +74,7 @@ VictorOps | `victorops` | yes, external only | yes ### Email -To enable email notifications you have to set up [SMTP settings]({{< relref "../administration/configuration/#smtp" >}}) +To enable email notifications you have to set up [SMTP settings]({{< relref "../../administration/configuration/#smtp" >}}) in the Grafana config. Email notifications will upload an image of the alert graph to an external image destination if available or fallback to attaching the image to the email. Be aware that if you use the `local` image storage email servers and clients might not be @@ -252,19 +252,19 @@ Alertmanager handles alerts sent by client applications such as Prometheus serve ## Enable images in notifications {#external-image-store} Grafana can render the panel associated with the alert rule as a PNG image and include that in the notification. Read more about the requirements and how to configure -[image rendering]({{< relref "../administration/image_rendering/" >}}). +[image rendering]({{< relref "../../administration/image_rendering/" >}}). -You must configure an [external image storage provider]({{< relref "../administration/configuration/#external-image-storage" >}}) in order to receive images in alert notifications. If your notification channel requires that the image be publicly accessible (e.g. Slack, PagerDuty), configure a provider which uploads the image to a remote image store like Amazon S3, Webdav, Google Cloud Storage, or Azure Blob Storage. Otherwise, the local provider can be used to serve the image directly from Grafana. +You must configure an [external image storage provider]({{< relref "../../administration/configuration/#external-image-storage" >}}) in order to receive images in alert notifications. If your notification channel requires that the image be publicly accessible (e.g. Slack, PagerDuty), configure a provider which uploads the image to a remote image store like Amazon S3, Webdav, Google Cloud Storage, or Azure Blob Storage. Otherwise, the local provider can be used to serve the image directly from Grafana. Notification services which need public image access are marked as 'external only'. ## Configure the link back to Grafana from alert notifications All alert notifications contain a link back to the triggered alert in the Grafana instance. -This URL is based on the [domain]({{< relref "../administration/configuration/#domain" >}}) setting in Grafana. +This URL is based on the [domain]({{< relref "../../administration/configuration/#domain" >}}) setting in Grafana. ## Notification templating > **Note:** Alert notification templating is only available in Grafana v7.4 and above. -The alert notification template feature allows you to take the [label]({{< relref "../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template.md" >}}). +The alert notification template feature allows you to take the [label]({{< relref "../../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template.md" >}}). diff --git a/docs/sources/alerting/pause-an-alert-rule.md b/docs/sources/alerting/old-alerting/pause-an-alert-rule.md similarity index 100% rename from docs/sources/alerting/pause-an-alert-rule.md rename to docs/sources/alerting/old-alerting/pause-an-alert-rule.md diff --git a/docs/sources/alerting/troubleshoot-alerts.md b/docs/sources/alerting/old-alerting/troubleshoot-alerts.md similarity index 100% rename from docs/sources/alerting/troubleshoot-alerts.md rename to docs/sources/alerting/old-alerting/troubleshoot-alerts.md diff --git a/docs/sources/alerting/view-alerts.md b/docs/sources/alerting/old-alerting/view-alerts.md similarity index 100% rename from docs/sources/alerting/view-alerts.md rename to docs/sources/alerting/old-alerting/view-alerts.md diff --git a/docs/sources/alerting/8.0/_index.md b/docs/sources/alerting/unified-alerting/_index.md similarity index 63% rename from docs/sources/alerting/8.0/_index.md rename to docs/sources/alerting/unified-alerting/_index.md index d517f424354..8283dbcd507 100644 --- a/docs/sources/alerting/8.0/_index.md +++ b/docs/sources/alerting/unified-alerting/_index.md @@ -1,8 +1,11 @@ +++ -title = "Alerts" +title = "Unified Alerts" aliases = ["/docs/grafana/latest/alerting/rules/", "/docs/grafana/latest/alerting/metrics/"] -weight = 110 +weight = 113 +++ -# Alerts v2.0 overview +# Unified alerts + + + diff --git a/docs/sources/basics/timeseries-dimensions.md b/docs/sources/basics/timeseries-dimensions.md index e8394990a02..5e345e5954f 100644 --- a/docs/sources/basics/timeseries-dimensions.md +++ b/docs/sources/basics/timeseries-dimensions.md @@ -76,7 +76,7 @@ In this case the labels that represent the dimensions will have two keys based o > **Note:** More than one dimension is currently only supported in the Logs queries within the Azure Monitor service as of version 7.1. -> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. See the documentation on [creating alerts with multiple series]({{< relref "../alerting/create-alerts.md#multiple-series" >}}). +> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. See the documentation on [creating alerts with multiple series]({{< relref "../alerting/old-alerting/create-alerts.md#multiple-series" >}}). ### Multiple values diff --git a/docs/sources/developers/plugins/backend/_index.md b/docs/sources/developers/plugins/backend/_index.md index 087859fee5c..392f0047371 100644 --- a/docs/sources/developers/plugins/backend/_index.md +++ b/docs/sources/developers/plugins/backend/_index.md @@ -12,7 +12,7 @@ However, one limitation with these plugins are that they execute on the client-s We use the term _backend plugin_ to denote that a plugin has a backend component. Still, normally a backend plugin requires frontend components as well. This is for example true for backend data source plugins which normally need configuration and query editor components implemented for the frontend. -Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/notifications.md" >}}) and custom authentication to name a few. +Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/old-alerting/notifications.md" >}}) and custom authentication to name a few. ## Use cases for implementing a backend plugin diff --git a/docs/sources/getting-started/_index.md b/docs/sources/getting-started/_index.md index 795bda62455..85e4e6fb381 100644 --- a/docs/sources/getting-started/_index.md +++ b/docs/sources/getting-started/_index.md @@ -24,7 +24,7 @@ Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information. ## Alerts -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/notifications.md" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. +If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/old-alerting/notifications.md" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/_index.md" >}}) for your most important metrics. diff --git a/docs/sources/http_api/alerting_notification_channels.md b/docs/sources/http_api/alerting_notification_channels.md index c3225f124a0..77edc08808c 100644 --- a/docs/sources/http_api/alerting_notification_channels.md +++ b/docs/sources/http_api/alerting_notification_channels.md @@ -176,7 +176,7 @@ Content-Type: application/json ## Create notification channel -You can find the full list of [supported notifiers]({{< relref "../alerting/notifications/#list-of-supported-notifiers" >}}) on the alert notifiers page. +You can find the full list of [supported notifiers]({{< relref "../alerting/old-alerting/notifications/#list-of-supported-notifiers" >}}) on the alert notifiers page. `POST /api/alert-notifications` diff --git a/docs/sources/panels/panel-editor.md b/docs/sources/panels/panel-editor.md index a9c5164fe99..be050a75d80 100644 --- a/docs/sources/panels/panel-editor.md +++ b/docs/sources/panels/panel-editor.md @@ -59,7 +59,7 @@ The section contains tabs where you enter queries, transform your data, and crea - **Query tab -** Select your data source and enter queries here. For more information, refer to [Queries]({{< relref "queries.md" >}}). - **Transform tab -** Apply data transformations. For more information, refer to [Transformations]({{< relref "transformations/_index.md" >}}). -- **Alert tab -** Write alert rules. For more information, refer to [Create alerts]({{< relref "../alerting/create-alerts.md" >}}). +- **Alert tab -** Write alert rules. For more information, refer to [Create alerts]({{< relref "../alerting/old-alerting/create-alerts.md" >}}). ### Panel and field options (side pane) diff --git a/docs/sources/shared/alerts/first-step.md b/docs/sources/shared/alerts/first-step.md new file mode 100644 index 00000000000..5102f0eb0c6 --- /dev/null +++ b/docs/sources/shared/alerts/first-step.md @@ -0,0 +1,11 @@ +--- +title: TBD... +--- + +## TBD + +Use the instructions in [Getting started with Grafana]({{< relref "../../getting-started/getting-started.md" >}}) to: + +- Install Grafana. +- Log in to Grafana. +- Create your first dashboard. diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index b0dd1e4b3c7..ed76d7cc8bf 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -107,7 +107,7 @@ You can now provide detailed information to alert notification recipients by inj {{< figure src="/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}} -For more information, refer to the [alert notification docs]({{< relref "../alerting/notifications.md#notification-templating" >}}). +For more information, refer to the [alert notification docs]({{< relref "../alerting/old-alerting/notifications.md#notification-templating" >}}). ### Content security policy support From ab26c4dfa4b57e487837cee2a6e090b59f405118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Tue, 25 May 2021 17:22:04 +0200 Subject: [PATCH 07/41] InfluxDB: Flux: handle boolean data-columns (#34479) * InfluxDB: Flux: handle boolean data-columns * normalize data-type strings based on documentation * reference missing data type --- pkg/tsdb/influxdb/flux/builder.go | 32 +++++++++---------- pkg/tsdb/influxdb/flux/executor_test.go | 8 +++-- .../influxdb/flux/testdata/boolean_data.csv | 9 ++++++ .../flux/testdata/boolean_data.golden.txt | 32 +++++++++++++++++++ .../testdata/{boolean.csv => boolean_tag.csv} | 0 ...lean.golden.txt => boolean_tag.golden.csv} | 0 6 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 pkg/tsdb/influxdb/flux/testdata/boolean_data.csv create mode 100644 pkg/tsdb/influxdb/flux/testdata/boolean_data.golden.txt rename pkg/tsdb/influxdb/flux/testdata/{boolean.csv => boolean_tag.csv} (100%) rename pkg/tsdb/influxdb/flux/testdata/{boolean.golden.txt => boolean_tag.golden.csv} (100%) diff --git a/pkg/tsdb/influxdb/flux/builder.go b/pkg/tsdb/influxdb/flux/builder.go index c9cd1caa136..99ab251c2da 100644 --- a/pkg/tsdb/influxdb/flux/builder.go +++ b/pkg/tsdb/influxdb/flux/builder.go @@ -9,18 +9,18 @@ import ( "github.com/influxdata/influxdb-client-go/v2/api/query" ) -// Copied from: (Apache 2 license) -// https://github.com/influxdata/influxdb-client-go/blob/master/query.go#L30 +// based on https://docs.influxdata.com/influxdb/v2.0/reference/syntax/annotated-csv/#data-types const ( - stringDatatype = "string" - doubleDatatype = "double" - boolDatatype = "bool" - longDatatype = "long" - uLongDatatype = "unsignedLong" - durationDatatype = "duration" - base64BinaryDataType = "base64Binary" - timeDatatypeRFC = "dateTime:RFC3339" - timeDatatypeRFCNano = "dateTime:RFC3339Nano" + stringDatatype = "string" + doubleDatatype = "double" + booleanDatatype = "boolean" + longDatatype = "long" + unsignedLongDatatype = "unsignedLong" + durationDatatype = "duration" + base64BinaryDataType = "base64Binary" + datetimeRFC339DataType = "dateTime:RFC3339" + datetimeRFC339DataTypeNano = "dateTime:RFC3339Nano" + // based on the documentation there should also be "dateTime:number" but i have never seen it yet. ) type columnInfo struct { @@ -92,19 +92,19 @@ func getConverter(t string) (*data.FieldConverter, error) { switch t { case stringDatatype: return &converters.AnyToNullableString, nil - case timeDatatypeRFC: + case datetimeRFC339DataType: return &timeToOptionalTime, nil - case timeDatatypeRFCNano: + case datetimeRFC339DataTypeNano: return &timeToOptionalTime, nil case durationDatatype: return &converters.Int64ToNullableInt64, nil case doubleDatatype: return &converters.Float64ToNullableFloat64, nil - case boolDatatype: + case booleanDatatype: return &converters.BoolToNullableBool, nil case longDatatype: return &converters.Int64ToNullableInt64, nil - case uLongDatatype: + case unsignedLongDatatype: return &converters.Uint64ToNullableUInt64, nil case base64BinaryDataType: return &converters.AnyToNullableString, nil @@ -125,7 +125,7 @@ func getGroupColumnNames(cols []*query.FluxColumn) []string { } func isTimestampType(dataType string) bool { - return (dataType == timeDatatypeRFC) || (dataType == timeDatatypeRFCNano) + return (dataType == datetimeRFC339DataType) || (dataType == datetimeRFC339DataTypeNano) } func hasUsualStartStop(dataCols []*query.FluxColumn) bool { diff --git a/pkg/tsdb/influxdb/flux/executor_test.go b/pkg/tsdb/influxdb/flux/executor_test.go index 5b39cfabecf..7c3e8c9d863 100644 --- a/pkg/tsdb/influxdb/flux/executor_test.go +++ b/pkg/tsdb/influxdb/flux/executor_test.go @@ -204,8 +204,12 @@ func TestBuckets(t *testing.T) { verifyGoldenResponse(t, "buckets") } -func TestBooleanGrouping(t *testing.T) { - verifyGoldenResponse(t, "boolean") +func TestBooleanTagGrouping(t *testing.T) { + verifyGoldenResponse(t, "boolean_tag") +} + +func TestBooleanData(t *testing.T) { + verifyGoldenResponse(t, "boolean_data") } func TestGoldenFiles(t *testing.T) { diff --git a/pkg/tsdb/influxdb/flux/testdata/boolean_data.csv b/pkg/tsdb/influxdb/flux/testdata/boolean_data.csv new file mode 100644 index 00000000000..10a856d4a12 --- /dev/null +++ b/pkg/tsdb/influxdb/flux/testdata/boolean_data.csv @@ -0,0 +1,9 @@ +#group,false,false,true,true,false,false,true,true,true +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,boolean,string,string,string +#default,_result,,,,,,,, +,result,table,_start,_stop,_time,_value,_field,_measurement,direction +,,0,2021-05-20T08:36:26.7Z,2021-05-20T09:36:26.7Z,2021-05-20T09:35:39.6Z,true,exitcode,x,left +,,0,2021-05-20T08:36:26.7Z,2021-05-20T09:36:26.7Z,2021-05-20T09:35:43.0Z,false,exitcode,x,left +,,1,2021-05-20T08:36:26.7Z,2021-05-20T09:36:26.7Z,2021-05-20T09:35:47.3Z,false,exitcode,x,right +,,1,2021-05-20T08:36:26.7Z,2021-05-20T09:36:26.7Z,2021-05-20T09:35:49.6Z,true,exitcode,x,right + diff --git a/pkg/tsdb/influxdb/flux/testdata/boolean_data.golden.txt b/pkg/tsdb/influxdb/flux/testdata/boolean_data.golden.txt new file mode 100644 index 00000000000..cb8aaec5cfd --- /dev/null +++ b/pkg/tsdb/influxdb/flux/testdata/boolean_data.golden.txt @@ -0,0 +1,32 @@ +🌟 This was machine generated. Do not edit. 🌟 + +Frame[0] {} +Name: x +Dimensions: 2 Fields by 2 Rows ++---------------------------------+------------------------+ +| Name: _time | Name: exitcode | +| Labels: | Labels: direction=left | +| Type: []*time.Time | Type: []*bool | ++---------------------------------+------------------------+ +| 2021-05-20 09:35:39.6 +0000 UTC | true | +| 2021-05-20 09:35:43 +0000 UTC | false | ++---------------------------------+------------------------+ + + + +Frame[1] +Name: x +Dimensions: 2 Fields by 2 Rows ++---------------------------------+-------------------------+ +| Name: _time | Name: exitcode | +| Labels: | Labels: direction=right | +| Type: []*time.Time | Type: []*bool | ++---------------------------------+-------------------------+ +| 2021-05-20 09:35:47.3 +0000 UTC | false | +| 2021-05-20 09:35:49.6 +0000 UTC | true | ++---------------------------------+-------------------------+ + + +====== TEST DATA RESPONSE (arrow base64) ====== +FRAME=QVJST1cxAAD/////yAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAHQAAAADAAAATAAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADo/v//CAAAAAwAAAABAAAAeAAAAAQAAABuYW1lAAAAAAj///8IAAAADAAAAAIAAAB7fQAABAAAAG1ldGEAAAAAAgAAALQAAAAEAAAAZv///xQAAAB4AAAAfAAAAAAABgF4AAAAAgAAADAAAAAEAAAAWP///wgAAAAUAAAACAAAAGV4aXRjb2RlAAAAAAQAAABuYW1lAAAAAID///8IAAAAIAAAABQAAAB7ImRpcmVjdGlvbiI6ImxlZnQifQAAAAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAIAAAAZXhpdGNvZGUAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAWAAAAAIAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAC09z6yvIAWAJafCbO8gBYBAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAADYAQAAAAAAAMAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAHQAAAADAAAATAAAACgAAAAEAAAAyP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADo/v//CAAAAAwAAAABAAAAeAAAAAQAAABuYW1lAAAAAAj///8IAAAADAAAAAIAAAB7fQAABAAAAG1ldGEAAAAAAgAAALQAAAAEAAAAZv///xQAAAB4AAAAfAAAAAAABgF4AAAAAgAAADAAAAAEAAAAWP///wgAAAAUAAAACAAAAGV4aXRjb2RlAAAAAAQAAABuYW1lAAAAAID///8IAAAAIAAAABQAAAB7ImRpcmVjdGlvbiI6ImxlZnQifQAAAAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAIAAAAZXhpdGNvZGUAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAPgBAABBUlJPVzE= +FRAME=QVJST1cxAAD/////qAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADo/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAAj///8IAAAADAAAAAEAAAB4AAAABAAAAG5hbWUAAAAAAgAAALQAAAAEAAAAZv///xQAAAB4AAAAfAAAAAAABgF4AAAAAgAAADAAAAAEAAAAWP///wgAAAAUAAAACAAAAGV4aXRjb2RlAAAAAAQAAABuYW1lAAAAAID///8IAAAAIAAAABUAAAB7ImRpcmVjdGlvbiI6InJpZ2h0In0AAAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAIAAAAZXhpdGNvZGUAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABgAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAACAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAYewJtLyAFgCYA5O0vIAWAgAAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAAAwABAAAAuAEAAAAAAADAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADo/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAAj///8IAAAADAAAAAEAAAB4AAAABAAAAG5hbWUAAAAAAgAAALQAAAAEAAAAZv///xQAAAB4AAAAfAAAAAAABgF4AAAAAgAAADAAAAAEAAAAWP///wgAAAAUAAAACAAAAGV4aXRjb2RlAAAAAAQAAABuYW1lAAAAAID///8IAAAAIAAAABUAAAB7ImRpcmVjdGlvbiI6InJpZ2h0In0AAAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAIAAAAZXhpdGNvZGUAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAANABAABBUlJPVzE= diff --git a/pkg/tsdb/influxdb/flux/testdata/boolean.csv b/pkg/tsdb/influxdb/flux/testdata/boolean_tag.csv similarity index 100% rename from pkg/tsdb/influxdb/flux/testdata/boolean.csv rename to pkg/tsdb/influxdb/flux/testdata/boolean_tag.csv diff --git a/pkg/tsdb/influxdb/flux/testdata/boolean.golden.txt b/pkg/tsdb/influxdb/flux/testdata/boolean_tag.golden.csv similarity index 100% rename from pkg/tsdb/influxdb/flux/testdata/boolean.golden.txt rename to pkg/tsdb/influxdb/flux/testdata/boolean_tag.golden.csv From a5082ab1124ee16365482e1cee892017bc265be6 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Tue, 25 May 2021 18:35:54 +0300 Subject: [PATCH 08/41] Chore: additional check when decrypting values (#34637) * Chore: additional check when decrypting values * Apply suggestions from code review --- pkg/util/encryption.go | 4 ++++ pkg/util/encryption_test.go | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go index 6ecda766b47..99d56dc22a1 100644 --- a/pkg/util/encryption.go +++ b/pkg/util/encryption.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/sha256" "errors" + "fmt" "io" "golang.org/x/crypto/pbkdf2" @@ -15,6 +16,9 @@ const saltLength = 8 // Decrypt decrypts a payload with a given secret. func Decrypt(payload []byte, secret string) ([]byte, error) { + if len(payload) < saltLength { + return nil, fmt.Errorf("unable to compute salt") + } salt := payload[:saltLength] key, err := encryptionKeyToBytes(secret, string(salt)) if err != nil { diff --git a/pkg/util/encryption_test.go b/pkg/util/encryption_test.go index 2d8ee534c7a..d3d63aa4d16 100644 --- a/pkg/util/encryption_test.go +++ b/pkg/util/encryption_test.go @@ -27,4 +27,11 @@ func TestEncryption(t *testing.T) { assert.Equal(t, []byte("grafana"), decrypted) }) + + t.Run("decrypting empty payload should not fail", func(t *testing.T) { + _, err := Decrypt([]byte(""), "1234") + require.Error(t, err) + + assert.Equal(t, "unable to compute salt", err.Error()) + }) } From 0e0ed43153c9d2f70df24f00457a8a07862f5886 Mon Sep 17 00:00:00 2001 From: Owen Diehl Date: Tue, 25 May 2021 11:54:50 -0400 Subject: [PATCH 09/41] Alerting/testing promql extraction (#34665) * promql compat for marshaling * extracts upstream instant queries into data frame for alerting * eval string parity --- pkg/services/ngalert/api/api_testing.go | 2 +- pkg/services/ngalert/api/promql_compat.go | 145 ++++++++++++++++++ .../ngalert/api/promql_compat_test.go | 107 +++++++++++++ pkg/services/ngalert/api/util.go | 1 - 4 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 pkg/services/ngalert/api/promql_compat.go create mode 100644 pkg/services/ngalert/api/promql_compat_test.go diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go index 15f44609098..0567c2782cf 100644 --- a/pkg/services/ngalert/api/api_testing.go +++ b/pkg/services/ngalert/api/api_testing.go @@ -74,7 +74,7 @@ func (srv TestingApiSrv) RouteTestRuleConfig(c *models.ReqContext, body apimodel http.MethodGet, queryURL, nil, - jsonExtractor(nil), + instantQueryResultsExtractor, nil, ) } diff --git a/pkg/services/ngalert/api/promql_compat.go b/pkg/services/ngalert/api/promql_compat.go new file mode 100644 index 00000000000..e93ff9f890e --- /dev/null +++ b/pkg/services/ngalert/api/promql_compat.go @@ -0,0 +1,145 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + cortex_util "github.com/cortexproject/cortex/pkg/util" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/eval" + "github.com/prometheus/prometheus/pkg/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + + "github.com/grafana/grafana/pkg/util" +) + +type instantQueryResponse struct { + Status string `json:"status"` + Data queryData `json:"data,omitempty"` + ErrorType string `json:"errorType,omitempty"` + Error string `json:"error,omitempty"` +} + +type queryData struct { + ResultType parser.ValueType `json:"resultType"` + Result json.RawMessage `json:"result"` + vector vector `json:"-"` + scalar scalar `json:"-"` +} + +type scalar promql.Scalar + +func (s *scalar) UnmarshalJSON(b []byte) error { + var xs []interface{} + if err := json.Unmarshal(b, &xs); err != nil { + return err + } + // scalars are encoded like `[ts/1000, "value"]` + if len(xs) != 2 { + return fmt.Errorf("unexpected number of scalar encoded values: %d", len(xs)) + } + ts, ok := xs[0].(float64) + if !ok { + return fmt.Errorf("first value in scalar uncoercible to timestamp: %v", xs[0]) + } + s.T = int64(ts) * 1000 + v, ok := xs[1].(string) + if !ok { + return fmt.Errorf("second value in scalar not string encoded: %v", xs[1]) + } + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + s.V = f + return nil +} + +func (d *queryData) UnmarshalJSON(b []byte) error { + type plain queryData + if err := json.Unmarshal(b, (*plain)(d)); err != nil { + return err + } + + switch d.ResultType { + case parser.ValueTypeScalar: + return json.Unmarshal(d.Result, &d.scalar) + case parser.ValueTypeVector: + return json.Unmarshal(d.Result, &d.vector) + default: + return fmt.Errorf("unexpected response type: %s", d.ResultType) + } +} + +type sample struct { + Metric labels.Labels `json:"metric"` + Value scalar `json:"value"` +} +type vector []sample + +func instantQueryResults(resp instantQueryResponse) (eval.Results, error) { + if resp.Error != "" || resp.Status != "success" { + return nil, errors.New(resp.Error) + } + + switch resp.Data.ResultType { + case parser.ValueTypeScalar: + return eval.Results{{ + Instance: map[string]string{}, + State: eval.Alerting, + EvaluatedAt: cortex_util.TimeFromMillis(resp.Data.scalar.T), + EvaluationString: extractEvalStringFromProm(sample{ + Value: resp.Data.scalar, + }), + }}, nil + case parser.ValueTypeVector: + results := make(eval.Results, 0, len(resp.Data.vector)) + for _, s := range resp.Data.vector { + results = append(results, eval.Result{ + Instance: s.Metric.Map(), + State: eval.Alerting, + EvaluatedAt: cortex_util.TimeFromMillis(s.Value.T), + EvaluationString: extractEvalStringFromProm(s), + }) + } + return results, nil + default: + return nil, fmt.Errorf("unexpected response type: %s", resp.Data.ResultType) + } +} + +func instantQueryResultsExtractor(b []byte) (interface{}, error) { + var resp instantQueryResponse + err := json.Unmarshal(b, &resp) + if err != nil { + return nil, err + } + + res, err := instantQueryResults(resp) + if err != nil { + return nil, err + } + frame := res.AsDataFrame() + + return util.DynMap{ + "instances": []*data.Frame{&frame}, + }, nil +} + +// extractEvalStringFromProm is intended to mimic the functionality used in ngalert/eval +func extractEvalStringFromProm(s sample) string { + var sb strings.Builder + sb.WriteString("[ ") + var ls string + if len(s.Metric) > 0 { + ls = s.Metric.String() + } + sb.WriteString(fmt.Sprintf("labels={%s} ", ls)) + sb.WriteString(fmt.Sprintf("value=%v ", fmt.Sprintf("%v", s.Value.V))) + sb.WriteString("]") + return sb.String() +} diff --git a/pkg/services/ngalert/api/promql_compat_test.go b/pkg/services/ngalert/api/promql_compat_test.go new file mode 100644 index 00000000000..351d104b9e9 --- /dev/null +++ b/pkg/services/ngalert/api/promql_compat_test.go @@ -0,0 +1,107 @@ +package api + +import ( + "encoding/json" + "testing" + + "github.com/prometheus/prometheus/pkg/labels" + "github.com/prometheus/prometheus/promql/parser" + "github.com/stretchr/testify/require" +) + +func Test_instantQueryMarshaling(t *testing.T) { + for _, tc := range []struct { + desc string + in string + exp parser.ValueType + expScalar *scalar + expVector *vector + isError bool // successfully unpack an upstream error + }{ + { + desc: "scalar", + in: `{ + "status": "success", + "data": { + "resultType": "scalar", + "result": [ + 12, + "2" + ] + } +}`, + exp: parser.ValueTypeScalar, + expScalar: &scalar{ + T: 12000, + V: 2, + }, + }, + { + desc: "vector", + in: `{ + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": { + "__name__": "apiserver_request:burnrate1d" + }, + "value": [ + 12.04, + "10.5" + ] + } + ] + } +}`, + exp: parser.ValueTypeVector, + expVector: &vector{ + sample{ + Value: scalar{ + T: 12000, // loses some precision during marshaling + V: 10.5, + }, + Metric: []labels.Label{{ + Name: "__name__", + Value: "apiserver_request:burnrate1d", + }}, + }, + }, + }, + { + desc: "successfully parse error", + in: `{ + "status": "failure", + "errorType": "someErr", + "error": "error doing something" +}`, + isError: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + var out instantQueryResponse + err := json.Unmarshal([]byte(tc.in), &out) + require.NoError(t, err) + + if tc.isError { + require.Equal(t, out.Status, "failure") + require.Greater(t, len(out.ErrorType), 0) + require.Greater(t, len(out.Error), 0) + return + } + + require.Equal(t, tc.exp, out.Data.ResultType) + b, err := json.MarshalIndent(out, "", " ") + require.Nil(t, err) + require.Equal(t, tc.in, string(b)) + + if tc.expScalar != nil { + require.Equal(t, *tc.expScalar, out.Data.scalar) + } + if tc.expVector != nil { + require.Equal(t, *tc.expVector, out.Data.vector) + } + }) + } +} diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 7cfc104a418..2862af18bd6 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -229,7 +229,6 @@ func conditionEval(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand, } frame := evalResults.AsDataFrame() - return response.JSONStreaming(http.StatusOK, util.DynMap{ "instances": []*data.Frame{&frame}, }) From 93c5c8345c4d301ee6c6a816751f29504fdc6fe0 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 25 May 2021 18:56:46 +0200 Subject: [PATCH 10/41] Prometheus: Update default HTTP method to POST for existing data sources (#34599) * Change default method to POST * Update comment * Add tests * Update Whats new * Update docs/sources/whatsnew/whats-new-in-v8-0.md Co-authored-by: Julien Pivotto * Update docs/sources/whatsnew/whats-new-in-v8-0.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/whatsnew/whats-new-in-v8-0.md Co-authored-by: Julien Pivotto Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- docs/sources/administration/provisioning.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 16 ++- .../configuration/PromSettings.test.tsx | 14 +-- .../prometheus/configuration/PromSettings.tsx | 9 +- .../datasource/prometheus/datasource.test.ts | 104 +++++++++++++----- .../datasource/prometheus/datasource.ts | 2 +- 6 files changed, 92 insertions(+), 55 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index ff8d8db8c74..279c92f7cab 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -152,7 +152,7 @@ Since not all datasources have the same configuration settings we only have the | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source. | | httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET | | maxSeries | number | Influxdb | Max number of series/tables that Grafana processes | -| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET | +| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to POST | | customQueryParameters | string | Prometheus | Query parameters to add, as a URL-encoded string. | | esVersion | string | Elasticsearch | Elasticsearch version (E.g. `7.0.0`, `7.6.1`) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index c4c611bbdd0..0d5ab479a84 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -24,7 +24,7 @@ Library panels allow users to build panels that can be used in multiple dashboar ### Real-time streaming -Data sources can now send real-time updates to dashboards over a websocket connection. This can be used with the [MQTT data source](https://github.com/grafana/mqtt-datasource). +Data sources can now send real-time updates to dashboards over a websocket connection. This can be used with the [MQTT data source](https://github.com/grafana/mqtt-datasource). In addition to data source integration, events can be sent to dashboards by posting metrics to the new live endpoint: `/api/live/push endpoint`. @@ -48,7 +48,7 @@ This release introduces a new histogram panel visualization. ### Time series visualization updates -The Time series is out of beta! We are removing the `Beta` tag and graduating the Time series visualization to a stable state. +The Time series is out of beta! We are removing the `Beta` tag and graduating the Time series visualization to a stable state. - **Time series** is now the default visualization option, replacing the **Graph (old)**. - The Time series panel now supports stacking. For more information, refer to [Graph stacked time series]({{< relref "../panels/visualizations/time-series/graph-time-series-stacking.md" >}}). @@ -60,7 +60,7 @@ The Time series is out of beta! We are removing the `Beta` tag and graduating t ### Pie chart visualization updates -The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state. +The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state. ### Panel editor updates @@ -115,7 +115,7 @@ Grafana 8.0 includes many performance enhancements. #### Initial startup and load performance -We reduced the Grafana initial download size massively, approximately 40%. This means that on slower or mobile connections, the initial login page or home dashboard will load much faster. +We reduced the Grafana initial download size massively, approximately 40%. This means that on slower or mobile connections, the initial login page or home dashboard will load much faster. All panels that have migrated from Flot to uPlot will also render two to three times faster because the library is much more efficient. Right now, this includes the Time series, Stat, Timeline, Histogram, and Barchart panel visualizations. @@ -206,9 +206,9 @@ You can now configure generic OAuth with empty scopes. This allows OAuth Identit ##### Added OAuth support for strict parsing of role_attribute_path -You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if th `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. +You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if the `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. -#### Singlestat panel deprecated +#### Singlestat panel deprecated Support for Singlestat panel has been discontinued. When you upgrade to version 8.0, all existing Singlestat panels automatically becomes Stat panels. Stat panel is available as plugin. @@ -243,3 +243,7 @@ Documentation was updated to reflect these changes. ### Elasticsearch: Use application/x-ndjson content type for multi-search requests For multi-search requests, we now use the correct application/x-ndjson content type instead of the incorrect application/json. Although this should be transparent to most of the users, if you are running Elasticsearch behind a proxy, then be sure that your proxy correctly handles requests with this content type. + +### Prometheus: Update default HTTP method to POST for existing data sources + +The default HTTP method for Prometheus data source is now POST, previously it was GET. The POST APIs are there since January 2018 (Prometheus 2.1.0) and they have fewer limitations than the GET APIs. Users with Prometheus instance with version < 2.1.0 that use the default HTTP method should update their HTTP method to GET. diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx index ae426979d3d..fd4525b09de 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx @@ -88,7 +88,7 @@ describe('PromSettings', () => { describe('PromSettings component', () => { const defaultProps = createDefaultConfigOptions(); - it('should show POST httpMethod if no httpMethod and no url', () => { + it('should show POST httpMethod if no httpMethod', () => { const options = defaultProps; options.url = ''; options.jsonData.httpMethod = ''; @@ -100,18 +100,6 @@ describe('PromSettings', () => { ); expect(screen.getByText('POST')).toBeInTheDocument(); }); - it('should show GET httpMethod if no httpMethod and url', () => { - const options = defaultProps; - options.url = 'test_url'; - options.jsonData.httpMethod = ''; - - render( -
- {}} options={options} /> -
- ); - expect(screen.getByText('GET')).toBeInTheDocument(); - }); it('should show POST httpMethod if POST httpMethod is configured', () => { const options = defaultProps; options.url = 'test_url'; diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx index 6a0386d6364..fab61014329 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx @@ -20,15 +20,10 @@ type Props = Pick, 'options' | ' export const PromSettings = (props: Props) => { const { options, onOptionsChange } = props; - /** - * We want to change the default httpMethod to 'POST' for all of the new Prometheus data sources instances (no url) added in 7.5+. - * We are explicitly adding httpMethod, as previously it could be undefined and defaulted to 'GET'. - * Undefined httpMethod is still going to be considered 'GET' for backward compatibility reasons, but if users open data - * source settings it is going to be set to 'GET' explicitly and it will be selected in httpMethod dropdown as 'GET'. - * */ + // We are explicitly adding httpMethod so it is correctly displayed in dropdown. This way, it is more predictable for users. if (!options.jsonData.httpMethod) { - options.url ? (options.jsonData.httpMethod = 'GET') : (options.jsonData.httpMethod = 'POST'); + options.jsonData.httpMethod = 'POST'; } return ( diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index d4878c5ce87..3a90c7dc976 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -127,12 +127,6 @@ describe('PrometheusDatasource', () => { }); describe('customQueryParams', () => { - const promDs = new PrometheusDatasource( - { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123' } as any }, - templateSrvStub as any, - timeSrvStub as any - ); - const target = { expr: 'test{job="testjob"}', format: 'time_series', refId: '' }; function makeQuery(target: PromQuery) { return { @@ -141,31 +135,87 @@ describe('PrometheusDatasource', () => { interval: '60s', } as any; } - it('added to metadata request', () => { - promDs.metadataRequest('/foo'); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); - }); - it('adds params to timeseries query', () => { - promDs.query(makeQuery(target)); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe( - 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123' + describe('with GET http method', () => { + const promDs = new PrometheusDatasource( + { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'GET' } as any }, + templateSrvStub as any, + timeSrvStub as any ); - }); - it('adds params to exemplars query', () => { - promDs.query(makeQuery({ ...target, exemplar: true })); - // We do also range query for single exemplars target - expect(fetchMock.mock.calls.length).toBe(2); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); - expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123'); + + it('added to metadata request', () => { + promDs.metadataRequest('/foo'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + }); + + it('adds params to timeseries query', () => { + promDs.query(makeQuery(target)); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe( + 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123' + ); + }); + it('adds params to exemplars query', () => { + promDs.query(makeQuery({ ...target, exemplar: true })); + // We do also range query for single exemplars target + expect(fetchMock.mock.calls.length).toBe(2); + expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123'); + }); + + it('adds params to instant query', () => { + promDs.query(makeQuery({ ...target, instant: true })); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + }); }); - it('adds params to instant query', () => { - promDs.query(makeQuery({ ...target, instant: true })); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + describe('with POST http method', () => { + const promDs = new PrometheusDatasource( + { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'POST' } as any }, + templateSrvStub as any, + timeSrvStub as any + ); + + it('added to metadata request with non-POST endpoint', () => { + promDs.metadataRequest('/foo'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + }); + + it('added to metadata request with POST endpoint', () => { + promDs.metadataRequest('/api/v1/labels'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/labels'); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + }); + + it('adds params to timeseries query', () => { + promDs.query(makeQuery(target)); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/query_range'); + expect(fetchMock.mock.calls[0][0].data).toEqual({ + customQuery: '123', + query: 'test{job="testjob"}', + step: 60, + end: 180, + start: 60, + }); + }); + it('adds params to exemplars query', () => { + promDs.query(makeQuery({ ...target, exemplar: true })); + // We do also range query for single exemplars target + expect(fetchMock.mock.calls.length).toBe(2); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + expect(fetchMock.mock.calls[1][0].data.customQuery).toBe('123'); + }); + + it('adds params to instant query', () => { + promDs.query(makeQuery({ ...target, instant: true })); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + }); }); }); diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index bfcbcec3fb5..91ccc0ff170 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -78,7 +78,7 @@ export class PrometheusDatasource extends DataSourceApi this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; this.queryTimeout = instanceSettings.jsonData.queryTimeout; - this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'POST'; this.directUrl = instanceSettings.jsonData.directUrl; this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; this.ruleMappings = {}; From 063e1b5ff5b2f51998562c8f7e5807b8acb984a9 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Tue, 25 May 2021 20:29:02 +0300 Subject: [PATCH 11/41] Live: use latest changes in Go SDK to deal with streaming frames (#34668) --- go.mod | 2 +- go.sum | 4 +- .../backendplugin/coreplugin/core_plugin.go | 2 +- .../backendplugin/grpcplugin/client_v2.go | 8 ++-- .../backendplugin/grpcplugin/grpc_plugin.go | 2 +- .../backendplugin/manager/manager_test.go | 2 +- pkg/services/live/features/plugin.go | 4 +- pkg/services/live/live.go | 4 +- pkg/services/live/managedstream/runner.go | 6 ++- pkg/services/live/plugin_helpers.go | 12 ++--- pkg/services/live/runstream/manager.go | 33 ++++++------- pkg/services/live/runstream/manager_test.go | 46 ++++++++----------- pkg/services/live/runstream/mock.go | 32 ++++++------- pkg/tsdb/testdatasource/scenarios_test.go | 3 +- pkg/tsdb/testdatasource/stream_handler.go | 25 +++------- 15 files changed, 84 insertions(+), 101 deletions(-) diff --git a/go.mod b/go.mod index 5ade367b175..5d74db17fbf 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/gosimple/slug v1.9.0 github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-live-sdk v0.0.6 - github.com/grafana/grafana-plugin-sdk-go v0.100.0 + github.com/grafana/grafana-plugin-sdk-go v0.101.0 github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v0.16.0 diff --git a/go.sum b/go.sum index 823e47bd3a1..da25fa8f181 100644 --- a/go.sum +++ b/go.sum @@ -922,8 +922,8 @@ github.com/grafana/grafana-live-sdk v0.0.6 h1:P1QFn0ZradOJp3zVpfG0STZMP+pgZrW0e0 github.com/grafana/grafana-live-sdk v0.0.6/go.mod h1:f15hHmWyLdFjmuWLsjeKeZnq/HnNQ3QkoPcaEww45AY= github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60= github.com/grafana/grafana-plugin-sdk-go v0.91.0/go.mod h1:Ot3k7nY7P6DXmUsDgKvNB7oG1v7PRyTdmnYVoS554bU= -github.com/grafana/grafana-plugin-sdk-go v0.100.0 h1:BryvIFdx/HrsKMt2hkxN7cJ0WrCgKpgjdJW8y8TSol0= -github.com/grafana/grafana-plugin-sdk-go v0.100.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= +github.com/grafana/grafana-plugin-sdk-go v0.101.0 h1:QyXMkgwZXUX9EQjLv5S5uDcvYjwsntqFV/dCC49Fn+w= +github.com/grafana/grafana-plugin-sdk-go v0.101.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= diff --git a/pkg/plugins/backendplugin/coreplugin/core_plugin.go b/pkg/plugins/backendplugin/coreplugin/core_plugin.go index 01bd1ea5688..4ead1024b57 100644 --- a/pkg/plugins/backendplugin/coreplugin/core_plugin.go +++ b/pkg/plugins/backendplugin/coreplugin/core_plugin.go @@ -110,7 +110,7 @@ func (cp *corePlugin) PublishStream(ctx context.Context, req *backend.PublishStr return nil, backendplugin.ErrMethodNotImplemented } -func (cp *corePlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { +func (cp *corePlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if cp.StreamHandler != nil { return cp.StreamHandler.RunStream(ctx, req, sender) } diff --git a/pkg/plugins/backendplugin/grpcplugin/client_v2.go b/pkg/plugins/backendplugin/grpcplugin/client_v2.go index ba70c84c0d1..2b4b4abf405 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_v2.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_v2.go @@ -194,7 +194,7 @@ func (c *clientV2) PublishStream(ctx context.Context, req *backend.PublishStream return backend.FromProto().PublishStreamResponse(protoResp), nil } -func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { +func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if c.StreamClient == nil { return backendplugin.ErrMethodNotImplemented } @@ -209,7 +209,7 @@ func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, } for { - protoResp, err := protoStream.Recv() + p, err := protoStream.Recv() if err != nil { if status.Code(err) == codes.Unimplemented { return backendplugin.ErrMethodNotImplemented @@ -219,7 +219,9 @@ func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, } return fmt.Errorf("error running stream: %w", err) } - if err := sender.Send(backend.FromProto().StreamPacket(protoResp)); err != nil { + // From GRPC connection we receive already prepared JSON. + err = sender.SendJSON(p.Data) + if err != nil { return err } } diff --git a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go index 2643692f412..59e638c3287 100644 --- a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go +++ b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go @@ -161,7 +161,7 @@ func (p *grpcPlugin) PublishStream(ctx context.Context, request *backend.Publish return pluginClient.PublishStream(ctx, request) } -func (p *grpcPlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { +func (p *grpcPlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { pluginClient, ok := p.getPluginClient() if !ok { return backendplugin.ErrPluginUnavailable diff --git a/pkg/plugins/backendplugin/manager/manager_test.go b/pkg/plugins/backendplugin/manager/manager_test.go index fba06c8e506..183d09cfa19 100644 --- a/pkg/plugins/backendplugin/manager/manager_test.go +++ b/pkg/plugins/backendplugin/manager/manager_test.go @@ -457,7 +457,7 @@ func (tp *testPlugin) PublishStream(ctx context.Context, request *backend.Publis return nil, backendplugin.ErrMethodNotImplemented } -func (tp *testPlugin) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender backend.StreamPacketSender) error { +func (tp *testPlugin) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error { return backendplugin.ErrMethodNotImplemented } diff --git a/pkg/services/live/features/plugin.go b/pkg/services/live/features/plugin.go index 5dc260904b5..68745326318 100644 --- a/pkg/services/live/features/plugin.go +++ b/pkg/services/live/features/plugin.go @@ -95,7 +95,9 @@ func (r *PluginPathRunner) OnSubscribe(ctx context.Context, user *models.SignedI reply := models.SubscribeReply{ Presence: true, - Data: resp.Data, + } + if resp.InitialData != nil { + reply.Data = resp.InitialData.Data() } return reply, backend.SubscribeStreamStatusOK, nil } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index c75d66bef21..2f5521869be 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -155,9 +155,9 @@ func (g *GrafanaLive) Init() error { g.node = node g.contextGetter = newPluginContextGetter(g.PluginContextProvider) - packetSender := newPluginPacketSender(node) + channelSender := newPluginChannelSender(node) presenceGetter := newPluginPresenceGetter(node) - g.runStreamManager = runstream.NewManager(packetSender, presenceGetter, g.contextGetter) + g.runStreamManager = runstream.NewManager(channelSender, presenceGetter, g.contextGetter) // Initialize the main features dash := &features.DashboardHandler{ diff --git a/pkg/services/live/managedstream/runner.go b/pkg/services/live/managedstream/runner.go index a587bff7dbe..db055df9c7f 100644 --- a/pkg/services/live/managedstream/runner.go +++ b/pkg/services/live/managedstream/runner.go @@ -106,11 +106,12 @@ func (s *ManagedStream) ListChannels(orgID int64, prefix string) []util.DynMap { // unstableSchema flag can be set to disable schema caching for a path. func (s *ManagedStream) Push(orgID int64, path string, frame *data.Frame, unstableSchema bool) error { // Keep schema + data for last packet. - frameJSON, err := data.FrameToJSON(frame, true, true) + frameJSONWrapper, err := data.FrameToJSON(frame) if err != nil { logger.Error("Error marshaling frame with Schema", "error", err) return err } + frameJSON := frameJSONWrapper.Bytes(data.IncludeAll) if !unstableSchema { // If schema is stable we can safely cache it, and only send values if @@ -128,11 +129,12 @@ func (s *ManagedStream) Push(orgID int64, path string, frame *data.Frame, unstab // frame to keep Schema JSON and Values JSON in frame object // to avoid encoding twice. if exists { - frameJSON, err = data.FrameToJSON(frame, false, true) + frameJSONWrapper, err = data.FrameToJSON(frame) if err != nil { logger.Error("Error marshaling Frame to JSON", "error", err) return err } + frameJSON = frameJSONWrapper.Bytes(data.IncludeDataOnly) } } else { // For unstable schema we always need to send everything to a connection. diff --git a/pkg/services/live/plugin_helpers.go b/pkg/services/live/plugin_helpers.go index 223d8877f6d..d90b79dcaf6 100644 --- a/pkg/services/live/plugin_helpers.go +++ b/pkg/services/live/plugin_helpers.go @@ -10,18 +10,18 @@ import ( "github.com/grafana/grafana/pkg/plugins/plugincontext" ) -type pluginPacketSender struct { +type pluginChannelSender struct { node *centrifuge.Node } -func newPluginPacketSender(node *centrifuge.Node) *pluginPacketSender { - return &pluginPacketSender{node: node} +func newPluginChannelSender(node *centrifuge.Node) *pluginChannelSender { + return &pluginChannelSender{node: node} } -func (p *pluginPacketSender) Send(channel string, packet *backend.StreamPacket) error { - _, err := p.node.Publish(channel, packet.Data) +func (p *pluginChannelSender) Send(channel string, data []byte) error { + _, err := p.node.Publish(channel, data) if err != nil { - return fmt.Errorf("error publishing %s: %w", string(packet.Data), err) + return fmt.Errorf("error publishing %s: %w", string(data), err) } return nil } diff --git a/pkg/services/live/runstream/manager.go b/pkg/services/live/runstream/manager.go index e0959521f46..74179d8c6bf 100644 --- a/pkg/services/live/runstream/manager.go +++ b/pkg/services/live/runstream/manager.go @@ -18,10 +18,10 @@ var ( logger = log.New("live.runstream") ) -//go:generate mockgen -destination=mock.go -package=runstream github.com/grafana/grafana/pkg/services/live/runstream StreamPacketSender,PresenceGetter,StreamRunner,PluginContextGetter +//go:generate mockgen -destination=mock.go -package=runstream github.com/grafana/grafana/pkg/services/live/runstream ChannelSender,PresenceGetter,StreamRunner,PluginContextGetter -type StreamPacketSender interface { - Send(channel string, packet *backend.StreamPacket) error +type ChannelSender interface { + Send(channel string, data []byte) error } type PluginContextGetter interface { @@ -33,23 +33,16 @@ type PresenceGetter interface { } type StreamRunner interface { - RunStream(ctx context.Context, request *backend.RunStreamRequest, sender backend.StreamPacketSender) error + RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error } -type streamSender struct { - channel string - packetSender StreamPacketSender +type packetSender struct { + channelSender ChannelSender + channel string } -func newStreamSender(channel string, packetSender StreamPacketSender) *streamSender { - return &streamSender{ - channel: channel, - packetSender: packetSender, - } -} - -func (p *streamSender) Send(packet *backend.StreamPacket) error { - return p.packetSender.Send(p.channel, packet) +func (p *packetSender) Send(packet *backend.StreamPacket) error { + return p.channelSender.Send(p.channel, packet.Data) } // Manager manages streams from Grafana to plugins (i.e. RunStream method). @@ -60,7 +53,7 @@ type Manager struct { datasourceStreams map[string]map[string]struct{} presenceGetter PresenceGetter pluginContextGetter PluginContextGetter - packetSender StreamPacketSender + channelSender ChannelSender registerCh chan submitRequest closedCh chan struct{} checkInterval time.Duration @@ -86,11 +79,11 @@ const ( ) // NewManager creates new Manager. -func NewManager(packetSender StreamPacketSender, presenceGetter PresenceGetter, pluginContextGetter PluginContextGetter, opts ...ManagerOption) *Manager { +func NewManager(channelSender ChannelSender, presenceGetter PresenceGetter, pluginContextGetter PluginContextGetter, opts ...ManagerOption) *Manager { sm := &Manager{ streams: make(map[string]streamContext), datasourceStreams: map[string]map[string]struct{}{}, - packetSender: packetSender, + channelSender: channelSender, presenceGetter: presenceGetter, pluginContextGetter: pluginContextGetter, registerCh: make(chan submitRequest), @@ -308,7 +301,7 @@ func (s *Manager) runStream(ctx context.Context, cancelFn func(), sr streamReque PluginContext: pluginCtx, Path: sr.Path, }, - newStreamSender(sr.Channel, s.packetSender), + backend.NewStreamSender(&packetSender{channelSender: s.channelSender, channel: sr.Channel}), ) if err != nil { if errors.Is(ctx.Err(), context.Canceled) { diff --git a/pkg/services/live/runstream/manager_test.go b/pkg/services/live/runstream/manager_test.go index f8ac522ca7e..fd1881f45ac 100644 --- a/pkg/services/live/runstream/manager_test.go +++ b/pkg/services/live/runstream/manager_test.go @@ -27,11 +27,11 @@ func TestStreamManager_Run(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockChannelSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) - manager := NewManager(mockPacketSender, mockPresenceGetter, mockContextGetter) + manager := NewManager(mockChannelSender, mockPresenceGetter, mockContextGetter) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -48,7 +48,7 @@ func TestStreamManager_SubmitStream_Send(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -84,12 +84,10 @@ func TestStreamManager_SubmitStream_Send(t *testing.T) { mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { require.Equal(t, "test", req.Path) close(startedCh) - err := sender.Send(&backend.StreamPacket{ - Data: []byte("test"), - }) + err := sender.SendJSON([]byte("{}")) require.NoError(t, err) <-ctx.Done() close(doneCh) @@ -115,7 +113,7 @@ func TestStreamManager_SubmitStream_DifferentOrgID(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -142,12 +140,10 @@ func TestStreamManager_SubmitStream_DifferentOrgID(t *testing.T) { mockStreamRunner1 := NewMockStreamRunner(mockCtrl) mockStreamRunner1.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { require.Equal(t, "test", req.Path) close(startedCh1) - err := sender.Send(&backend.StreamPacket{ - Data: []byte("test"), - }) + err := sender.SendJSON([]byte("{}")) require.NoError(t, err) <-ctx.Done() close(doneCh1) @@ -157,12 +153,10 @@ func TestStreamManager_SubmitStream_DifferentOrgID(t *testing.T) { mockStreamRunner2 := NewMockStreamRunner(mockCtrl) mockStreamRunner2.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { require.Equal(t, "test", req.Path) close(startedCh2) - err := sender.Send(&backend.StreamPacket{ - Data: []byte("test"), - }) + err := sender.SendJSON([]byte("{}")) require.NoError(t, err) <-ctx.Done() close(doneCh2) @@ -190,7 +184,7 @@ func TestStreamManager_SubmitStream_CloseNoSubscribers(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -218,7 +212,7 @@ func TestStreamManager_SubmitStream_CloseNoSubscribers(t *testing.T) { mockPresenceGetter.EXPECT().GetNumSubscribers("1/test").Return(0, nil).Times(3) mockStreamRunner := NewMockStreamRunner(mockCtrl) - mockStreamRunner.EXPECT().RunStream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + mockStreamRunner.EXPECT().RunStream(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { close(startedCh) <-ctx.Done() close(doneCh) @@ -237,7 +231,7 @@ func TestStreamManager_SubmitStream_ErrorRestartsRunStream(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -271,7 +265,7 @@ func TestStreamManager_SubmitStream_ErrorRestartsRunStream(t *testing.T) { mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if currentErrors >= numErrors { return nil } @@ -290,7 +284,7 @@ func TestStreamManager_SubmitStream_NilErrorStopsRunStream(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -309,7 +303,7 @@ func TestStreamManager_SubmitStream_NilErrorStopsRunStream(t *testing.T) { mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { return nil }).Times(1) @@ -323,7 +317,7 @@ func TestStreamManager_HandleDatasourceUpdate(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -359,7 +353,7 @@ func TestStreamManager_HandleDatasourceUpdate(t *testing.T) { mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if isFirstCall { // first RunStream will wait till context done. isFirstCall = false @@ -389,7 +383,7 @@ func TestStreamManager_HandleDatasourceDelete(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() - mockPacketSender := NewMockStreamPacketSender(mockCtrl) + mockPacketSender := NewMockChannelSender(mockCtrl) mockPresenceGetter := NewMockPresenceGetter(mockCtrl) mockContextGetter := NewMockPluginContextGetter(mockCtrl) @@ -422,7 +416,7 @@ func TestStreamManager_HandleDatasourceDelete(t *testing.T) { mockStreamRunner := NewMockStreamRunner(mockCtrl) mockStreamRunner.EXPECT().RunStream( gomock.Any(), gomock.Any(), gomock.Any(), - ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error { + ).DoAndReturn(func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { close(doneCh) <-ctx.Done() return ctx.Err() diff --git a/pkg/services/live/runstream/mock.go b/pkg/services/live/runstream/mock.go index e3528153bc6..e8cbfa40eb5 100644 --- a/pkg/services/live/runstream/mock.go +++ b/pkg/services/live/runstream/mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/grafana/grafana/pkg/services/live/runstream (interfaces: StreamPacketSender,PresenceGetter,StreamRunner,PluginContextGetter) +// Source: github.com/grafana/grafana/pkg/services/live/runstream (interfaces: ChannelSender,PresenceGetter,StreamRunner,PluginContextGetter) // Package runstream is a generated GoMock package. package runstream @@ -13,31 +13,31 @@ import ( models "github.com/grafana/grafana/pkg/models" ) -// MockStreamPacketSender is a mock of StreamPacketSender interface. -type MockStreamPacketSender struct { +// MockChannelSender is a mock of ChannelSender interface. +type MockChannelSender struct { ctrl *gomock.Controller - recorder *MockStreamPacketSenderMockRecorder + recorder *MockChannelSenderMockRecorder } -// MockStreamPacketSenderMockRecorder is the mock recorder for MockStreamPacketSender. -type MockStreamPacketSenderMockRecorder struct { - mock *MockStreamPacketSender +// MockChannelSenderMockRecorder is the mock recorder for MockChannelSender. +type MockChannelSenderMockRecorder struct { + mock *MockChannelSender } -// NewMockStreamPacketSender creates a new mock instance. -func NewMockStreamPacketSender(ctrl *gomock.Controller) *MockStreamPacketSender { - mock := &MockStreamPacketSender{ctrl: ctrl} - mock.recorder = &MockStreamPacketSenderMockRecorder{mock} +// NewMockChannelSender creates a new mock instance. +func NewMockChannelSender(ctrl *gomock.Controller) *MockChannelSender { + mock := &MockChannelSender{ctrl: ctrl} + mock.recorder = &MockChannelSenderMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStreamPacketSender) EXPECT() *MockStreamPacketSenderMockRecorder { +func (m *MockChannelSender) EXPECT() *MockChannelSenderMockRecorder { return m.recorder } // Send mocks base method. -func (m *MockStreamPacketSender) Send(arg0 string, arg1 *backend.StreamPacket) error { +func (m *MockChannelSender) Send(arg0 string, arg1 []byte) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Send", arg0, arg1) ret0, _ := ret[0].(error) @@ -45,9 +45,9 @@ func (m *MockStreamPacketSender) Send(arg0 string, arg1 *backend.StreamPacket) e } // Send indicates an expected call of Send. -func (mr *MockStreamPacketSenderMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockChannelSenderMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockStreamPacketSender)(nil).Send), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockChannelSender)(nil).Send), arg0, arg1) } // MockPresenceGetter is a mock of PresenceGetter interface. @@ -112,7 +112,7 @@ func (m *MockStreamRunner) EXPECT() *MockStreamRunnerMockRecorder { } // RunStream mocks base method. -func (m *MockStreamRunner) RunStream(arg0 context.Context, arg1 *backend.RunStreamRequest, arg2 backend.StreamPacketSender) error { +func (m *MockStreamRunner) RunStream(arg0 context.Context, arg1 *backend.RunStreamRequest, arg2 *backend.StreamSender) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RunStream", arg0, arg1, arg2) ret0, _ := ret[0].(error) diff --git a/pkg/tsdb/testdatasource/scenarios_test.go b/pkg/tsdb/testdatasource/scenarios_test.go index 5e32273fbd0..d2d64ce1515 100644 --- a/pkg/tsdb/testdatasource/scenarios_test.go +++ b/pkg/tsdb/testdatasource/scenarios_test.go @@ -278,8 +278,9 @@ func TestReadCSV(t *testing.T) { require.NoError(t, err) frame := data.NewFrame("", fBool, fBool2, fNum, fStr) - out, err := data.FrameToJSON(frame, true, true) + frameToJSON, err := data.FrameToJSON(frame) require.NoError(t, err) + out := frameToJSON.Bytes(data.IncludeAll) // require.Equal(t, "", string(out)) diff --git a/pkg/tsdb/testdatasource/stream_handler.go b/pkg/tsdb/testdatasource/stream_handler.go index ac42c1b604d..73a7c5c6091 100644 --- a/pkg/tsdb/testdatasource/stream_handler.go +++ b/pkg/tsdb/testdatasource/stream_handler.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/infra/log" ) @@ -32,14 +31,14 @@ func newTestStreamHandler(logger log.Logger) *testStreamHandler { } func (p *testStreamHandler) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - schema, err := data.FrameToJSON(p.frame, true, false) + p.logger.Debug("Allowing access to stream", "path", req.Path, "user", req.PluginContext.User) + initialData, err := backend.NewInitialFrame(p.frame, data.IncludeSchemaOnly) if err != nil { return nil, err } - p.logger.Debug("Allowing access to stream", "path", req.Path, "user", req.PluginContext.User) return &backend.SubscribeStreamResponse{ - Status: backend.SubscribeStreamStatusOK, - Data: schema, + Status: backend.SubscribeStreamStatusOK, + InitialData: initialData, }, nil } @@ -50,7 +49,7 @@ func (p *testStreamHandler) PublishStream(_ context.Context, req *backend.Publis }, nil } -func (p *testStreamHandler) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender backend.StreamPacketSender) error { +func (p *testStreamHandler) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error { p.logger.Debug("New stream call", "path", request.Path) var conf testStreamConfig switch request.Path { @@ -78,7 +77,7 @@ type testStreamConfig struct { Drop float64 } -func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf testStreamConfig, sender backend.StreamPacketSender) error { +func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf testStreamConfig, sender *backend.StreamSender) error { spread := 50.0 walker := rand.Float64() * 100 @@ -101,17 +100,7 @@ func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf p.frame.Fields[1].Set(0, walker) // Value p.frame.Fields[2].Set(0, walker-((rand.Float64()*spread)+0.01)) // Min p.frame.Fields[3].Set(0, walker+((rand.Float64()*spread)+0.01)) // Max - - bytes, err := data.FrameToJSON(p.frame, false, true) - if err != nil { - logger.Warn("unable to marshal line", "error", err) - continue - } - - packet := &backend.StreamPacket{ - Data: bytes, - } - if err := sender.Send(packet); err != nil { + if err := sender.SendFrame(p.frame, data.IncludeDataOnly); err != nil { return err } } From f9b8f3777a5a105ffa3658f6e83d5858335c3ebc Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 25 May 2021 23:11:13 -0700 Subject: [PATCH 12/41] CloudWatch: Add Lambda@Edge Amazon CloudFront metrics (#34561) * Add LambdaExecutionError to AWS/CloudFront * Add LambdaValidationError to AWS/CloudFront * Add LambdaLimitExceededErrors to AWS/CloudFront --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 6d96aea3d0c..58bda19f5a8 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -54,7 +54,7 @@ var metricsMap = map[string][]string{ "AWS/Billing": {"EstimatedCharges"}, "AWS/Chatbot": {"EventsThrottled", "EventsProcessed", "MessageDeliverySuccess", "MessageDeliveryFailure", "UnsupportedEvents"}, "AWS/ClientVPN": {"ActiveConnectionsCount", "AuthenticationFailures", "CrlDaysToExpiry", "EgressBytes", "EgressPackets", "IngressBytes", "IngressPackets", "SelfServicePortalClientConfigurationDownloads"}, - "AWS/CloudFront": {"4xxErrorRate", "5xxErrorRate", "BytesDownloaded", "BytesUploaded", "Requests", "TotalErrorRate", "CacheHitRate", "OriginLatency", "401ErrorRate", "403ErrorRate", "404ErrorRate", "502ErrorRate", "503ErrorRate", "504ErrorRate"}, + "AWS/CloudFront": {"4xxErrorRate", "5xxErrorRate", "BytesDownloaded", "BytesUploaded", "Requests", "TotalErrorRate", "CacheHitRate", "OriginLatency", "401ErrorRate", "403ErrorRate", "404ErrorRate", "502ErrorRate", "503ErrorRate", "504ErrorRate", "LambdaExecutionError", "LambdaValidationError", "LambdaLimitExceededErrors"}, "AWS/CloudHSM": {"HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSessionCount", "HsmSslCtxsOccupied", "HsmTemperature", "HsmUnhealthy", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"}, "AWS/CloudSearch": {"IndexUtilization", "Partitions", "SearchableDocuments", "SuccessfulRequests"}, "AWS/CodeBuild": {"BuildDuration", "Builds", "DownloadSourceDuration", "Duration", "FailedBuilds", "FinalizingDuration", "InstallDuration", "PostBuildDuration", "PreBuildDuration", "ProvisioningDuration", "QueuedDuration", "SubmittedDuration", "SucceededBuilds", "UploadArtifactsDuration"}, From 80c05becc549d84c81f9184b82b0c7680b2bb9fc Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 25 May 2021 23:11:26 -0700 Subject: [PATCH 13/41] Add missing AWS/AppSync metrics (#34691) --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 58bda19f5a8..58ca6300590 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -47,7 +47,7 @@ var metricsMap = map[string][]string{ "AWS/AmazonMQ": {"AckRate", "BurstBalance", "ChannelCount", "ConfirmRate", "ConnectionCount", "ConsumerCount", "CpuCreditBalance", "CpuUtilization", "CurrentConnectionsCount", "DequeueCount", "DispatchCount", "EnqueueCount", "EnqueueTime", "EstablishedConnectionsCount", "ExchangeCount", "ExpiredCount", "HeapUsage", "InactiveDurableTopicSubscribersCount", "InFlightCount", "JobSchedulerStorePercentUsage", "JournalFilesForFastRecovery", "JournalFilesForFullRecovery", "MemoryUsage", "MessageCount", "MessageReadyCount", "MessageUnacknowledgedCount", "NetworkIn", "NetworkOut", "OpenTransactionCount", "ProducerCount", "PublishRate", "QueueCount", "QueueSize", "RabbitMQDiskFree", "RabbitMQDiskFreeLimit", "RabbitMQFdUsed", "RabbitMQMemLimit", "RabbitMQMemUsed", "ReceiveCount", "StorePercentUsage", "SystemCpuUtilization", "TempPercentUsage", "TotalConsumerCount", "TotalDequeueCount", "TotalEnqueueCount", "TotalMessageCount", "TotalProducerCount", "VolumeReadOps", "VolumeWriteOps"}, "AWS/ApiGateway": {"4xx", "4XXError", "5xx", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "DataProcessed", "IntegrationLatency", "Latency"}, "AWS/AppStream": {"ActualCapacity", "AvailableCapacity", "CapacityUtilization", "DesiredCapacity", "InUseCapacity", "InsufficientCapacityError", "PendingCapacity", "RunningCapacity"}, - "AWS/AppSync": {"4XXError", "5XXError", "Latency"}, + "AWS/AppSync": {"4XXError", "5XXError", "Latency", "ActiveConnections", "ActiveSubscriptions", "ConnectClientError", "ConnectionDuration", "ConnectServerError", "ConnectSuccess", "DisconnectClientError", "DisconnectServerError", "DisconnectSuccess", "PublishDataMessageClientError", "PublishDataMessageServerError", "PublishDataMessageSize", "PublishDataMessageSuccess", "SubscribeClientError", "SubscribeServerError", "SubscribeSuccess", "UnsubscribeClientError", "UnsubscribeServerError", "UnsubscribeSuccess"}, "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "ConsumedLCUs", "ELBAuthError", "ELBAuthFailure", "ELBAuthLatency", "ELBAuthRefreshTokenSuccess", "ELBAuthSuccess", "ELBAuthUserClaimsSizeExceeded", "HTTPCode_ELB_3XX_Count", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "HTTP_Fixed_Response_Count", "HTTP_Redirect_Count", "HTTP_Redirect_Url_Limit_Exceeded_Count", "HealthyHostCount", "IPv6ProcessedBytes", "IPv6RequestCount", "LambdaInternalError", "LambdaTargetProcessedBytes", "LambdaUserError", "NewConnectionCount", "NonStickyRequestCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "RuleEvaluations", "StandardProcessedBytes", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, "AWS/Athena": {"EngineExecutionTime", "QueryPlanningTime", "QueryQueueTime", "ProcessedBytes", "ServiceProcessingTime", "TotalExecutionTime"}, "AWS/AutoScaling": {"GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, From ec33bae8d4172b6ac3cc5e5a1c79db70e7486153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 May 2021 08:59:49 +0200 Subject: [PATCH 14/41] PanelEdit: Add data source label to data source picker dropdown (#34605) * PanelEdit: Add data source label to data source picker dropdown * Updated based on ux feedback --- .../src/components/DataSourcePicker.tsx | 16 ++++++++++++++-- .../app/features/query/components/QueryGroup.tsx | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 1146c35fe02..b0af3414b0d 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -2,10 +2,11 @@ import React, { PureComponent } from 'react'; // Components -import { HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui'; +import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui'; import { DataSourceInstanceSettings, isUnsignedPluginSignature, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv } from '../services/dataSourceSrv'; +import { css, cx } from '@emotion/css'; /** * Component props description for the {@link DataSourcePicker} @@ -138,11 +139,12 @@ export class DataSourcePicker extends PureComponent