From 4fc0294aab69a716ca3329fa2e293dce8ecc1155 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 25 Oct 2023 16:47:32 +0200 Subject: [PATCH] Add logging prometheus and ruler rules totals (#76812) --- .../features/alerting/unified/Analytics.ts | 75 +++++++++++++++++++ .../alerting/unified/RuleList.test.tsx | 14 +--- .../alert-groups/MatcherFilter.test.tsx | 8 +- .../components/alert-groups/MatcherFilter.tsx | 3 +- .../NewRuleFromPanelButton.test.tsx | 13 +--- .../NewRuleFromPanelButton.tsx | 3 +- .../alert-rule-form/AlertRuleForm.tsx | 4 +- .../unified/components/rules/NoRulesCTA.tsx | 3 +- .../rules/RuleListGroupView.test.tsx | 14 +--- .../components/rules/RuleListGroupView.tsx | 3 +- .../components/rules/RulesFilter.test.tsx | 14 +--- .../unified/components/rules/RulesFilter.tsx | 3 +- .../components/rules/RulesGroup.test.tsx | 15 ++-- .../unified/components/rules/RulesGroup.tsx | 3 +- .../alerting/unified/state/actions.ts | 19 +++-- 15 files changed, 119 insertions(+), 75 deletions(-) diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index d405c7b4f46..2cc69d578dd 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -4,6 +4,9 @@ import { getBackendSrv, logError } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime/src'; import { contextSrv } from 'app/core/core'; +import { RuleNamespace } from '../../../types/unified-alerting'; +import { RulerRulesConfigDTO } from '../../../types/unified-alerting-dto'; + export const USER_CREATION_MIN_DAYS = 7; export const LogMessages = { @@ -51,6 +54,78 @@ export function withPerformanceLogging Promise }; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function withPromRulesMetadataLogging Promise>( + func: TFunc, + message: string, + context: Record +) { + return async (...args: Parameters) => { + const startLoadingTs = performance.now(); + const response = await func(...args); + + const { namespacesCount, groupsCount, rulesCount } = getPromRulesMetadata(response); + + logInfo(message, { + loadTimeMs: (performance.now() - startLoadingTs).toFixed(0), + namespacesCount, + groupsCount, + rulesCount, + ...context, + }); + return response; + }; +} + +function getPromRulesMetadata(promRules: RuleNamespace[]) { + const namespacesCount = promRules.length; + const groupsCount = promRules.flatMap((ns) => ns.groups).length; + const rulesCount = promRules.flatMap((ns) => ns.groups).flatMap((g) => g.rules).length; + + const metadata = { + namespacesCount: namespacesCount.toFixed(0), + groupsCount: groupsCount.toFixed(0), + rulesCount: rulesCount.toFixed(0), + }; + + return metadata; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function withRulerRulesMetadataLogging Promise>( + func: TFunc, + message: string, + context: Record +) { + return async (...args: Parameters) => { + const startLoadingTs = performance.now(); + const response = await func(...args); + + const { namespacesCount, groupsCount, rulesCount } = getRulerRulesMetadata(response); + + logInfo(message, { + loadTimeMs: (performance.now() - startLoadingTs).toFixed(0), + namespacesCount, + groupsCount, + rulesCount, + ...context, + }); + return response; + }; +} + +function getRulerRulesMetadata(rulerRules: RulerRulesConfigDTO) { + const namespacesCount = Object.keys(rulerRules).length; + const groups = Object.values(rulerRules).flatMap((groups) => groups); + const rules = groups.flatMap((group) => group.rules); + + return { + namespacesCount: namespacesCount.toFixed(0), + groupsCount: groups.length.toFixed(0), + rulesCount: rules.length.toFixed(0), + }; +} + export async function isNewUser() { try { const { createdAt } = await getBackendSrv().get(`/api/user`); diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index ff1eddc5d38..2f072caf3d0 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -5,14 +5,14 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { byRole, byTestId, byText } from 'testing-library-selector'; -import { DataSourceSrv, locationService, logInfo, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; +import { DataSourceSrv, locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons'; import * as actions from 'app/features/alerting/unified/state/actions'; import { AccessControlAction } from 'app/types'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; -import { LogMessages } from './Analytics'; +import * as analytics from './Analytics'; import RuleList from './RuleList'; import { discoverFeatures } from './api/buildInfo'; import { fetchRules } from './api/prometheus'; @@ -46,14 +46,8 @@ jest.mock('app/core/core', () => ({ emit: () => {}, }, })); -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - return { - ...original, - logInfo: jest.fn(), - }; -}); +jest.spyOn(analytics, 'logInfo'); jest.spyOn(config, 'getAllDataSources'); jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]); @@ -830,7 +824,7 @@ describe('RuleList', () => { await userEvent.click(button); - expect(logInfo).toHaveBeenCalledWith(LogMessages.alertRuleFromScratch); + expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.alertRuleFromScratch); }); }); }); diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.test.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.test.tsx index 9043b8f8a7c..5138f5a8bfa 100644 --- a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.test.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.test.tsx @@ -3,13 +3,11 @@ import userEvent from '@testing-library/user-event'; import lodash from 'lodash'; // eslint-disable-line lodash/import-scope import React from 'react'; -import * as runtime from '@grafana/runtime'; - -import { LogMessages } from '../../Analytics'; +import * as analytics from '../../Analytics'; import { MatcherFilter } from './MatcherFilter'; -const logInfoSpy = jest.spyOn(runtime, 'logInfo'); +const logInfoSpy = jest.spyOn(analytics, 'logInfo'); describe('Analytics', () => { beforeEach(() => { @@ -25,7 +23,7 @@ describe('Analytics', () => { const searchInput = screen.getByTestId('search-query-input'); await userEvent.type(searchInput, 'job='); - expect(logInfoSpy).toHaveBeenCalledWith(LogMessages.filterByLabel); + expect(logInfoSpy).toHaveBeenCalledWith(analytics.LogMessages.filterByLabel); }); it('should call onChange handler', async () => { diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx index 551642f42da..7c6a51d20e5 100644 --- a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx @@ -4,10 +4,9 @@ import React, { FormEvent, useEffect, useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { logInfo } from '@grafana/runtime'; import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui'; -import { LogMessages } from '../../Analytics'; +import { logInfo, LogMessages } from '../../Analytics'; interface Props { className?: string; diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx index dbe2944c696..e1baefbd61d 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx @@ -2,11 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { logInfo } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; -import { LogMessages } from '../../Analytics'; +import * as analytics from '../../Analytics'; import { NewRuleFromPanelButton } from './NewRuleFromPanelButton'; @@ -24,13 +23,7 @@ jest.mock('react-router-dom', () => ({ }), })); -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - return { - ...original, - logInfo: jest.fn(), - }; -}); +jest.spyOn(analytics, 'logInfo'); jest.mock('react-use', () => ({ useAsync: () => ({ loading: false, value: {} }), @@ -52,6 +45,6 @@ describe('Analytics', () => { await userEvent.click(button); - expect(logInfo).toHaveBeenCalledWith(LogMessages.alertRuleFromPanel); + expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.alertRuleFromPanel); }); }); diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index f6000d528e0..cac61a795d4 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -3,12 +3,11 @@ import { useLocation } from 'react-router-dom'; import { useAsync } from 'react-use'; import { urlUtil } from '@grafana/data'; -import { logInfo } from '@grafana/runtime'; import { Alert, Button, LinkButton } from '@grafana/ui'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { useSelector } from 'app/types'; -import { LogMessages } from '../../Analytics'; +import { logInfo, LogMessages } from '../../Analytics'; import { panelToRuleFormValues } from '../../utils/rule-form'; interface Props { diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 057b1cd66d8..d4a7990ba92 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -5,7 +5,7 @@ import { Link, useParams } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { config, logInfo } from '@grafana/runtime'; +import { config } from '@grafana/runtime'; import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { useAppNotification } from 'app/core/copy/appNotification'; @@ -15,7 +15,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; -import { LogMessages, trackNewAlerRuleFormError } from '../../../Analytics'; +import { logInfo, LogMessages, trackNewAlerRuleFormError } from '../../../Analytics'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index 027a7a19029..ee64d3795d9 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -3,11 +3,10 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data/src/themes'; import { Stack } from '@grafana/experimental'; -import { logInfo } from '@grafana/runtime'; import { CallToActionCard, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { LogMessages } from '../../Analytics'; +import { logInfo, LogMessages } from '../../Analytics'; import { useRulesAccess } from '../../utils/accessControlHooks'; export const NoRulesSplash = () => { diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx index 7649000f67c..5e35dbdda97 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx @@ -4,25 +4,19 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import { byRole } from 'testing-library-selector'; -import { locationService, logInfo } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { LogMessages } from '../../Analytics'; +import * as analytics from '../../Analytics'; import { mockCombinedRule, mockDataSource } from '../../mocks'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { RuleListGroupView } from './RuleListGroupView'; -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - return { - ...original, - logInfo: jest.fn(), - }; -}); +jest.spyOn(analytics, 'logInfo'); const ui = { grafanaRulesHeading: byRole('heading', { name: 'Grafana' }), @@ -97,7 +91,7 @@ describe('RuleListGroupView', () => { renderRuleList(namespaces); - expect(logInfo).toHaveBeenCalledWith(LogMessages.loadedList); + expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.loadedList); }); }); }); diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx index 80edba11416..8ceffc20cba 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.tsx @@ -1,9 +1,8 @@ import React, { useEffect, useMemo } from 'react'; -import { logInfo } from '@grafana/runtime'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { LogMessages } from '../../Analytics'; +import { LogMessages, logInfo } from '../../Analytics'; import { AlertSourceAction } from '../../hooks/useAbilities'; import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; import { Authorize } from '../Authorize'; diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx index a8818b2158d..ea69d56047a 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx @@ -4,20 +4,14 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { byLabelText, byRole } from 'testing-library-selector'; -import { locationService, logInfo, setDataSourceSrv } from '@grafana/runtime'; +import { locationService, setDataSourceSrv } from '@grafana/runtime'; -import { LogMessages } from '../../Analytics'; +import * as analytics from '../../Analytics'; import { MockDataSourceSrv } from '../../mocks'; import RulesFilter from './RulesFilter'; -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - return { - ...original, - logInfo: jest.fn(), - }; -}); +jest.spyOn(analytics, 'logInfo'); jest.mock('./MultipleDataSourcePicker', () => { const original = jest.requireActual('./MultipleDataSourcePicker'); @@ -91,6 +85,6 @@ describe('Analytics', () => { await userEvent.click(button); - expect(logInfo).toHaveBeenCalledWith(LogMessages.clickingAlertStateFilters); + expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.clickingAlertStateFilters); }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index 9bdc54df798..25e95cbd72f 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -4,12 +4,11 @@ import { useForm } from 'react-hook-form'; import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { logInfo } from '@grafana/runtime'; import { Button, Field, Icon, Input, Label, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; -import { LogMessages } from '../../Analytics'; +import { logInfo, LogMessages } from '../../Analytics'; import { useRulesFilter } from '../../hooks/useFilteredRules'; import { RuleHealth } from '../../search/rulesSearchParser'; import { alertStateToReadable } from '../../utils/rules'; diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx index 7bdfa495c24..33f1411e3d1 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -5,13 +5,12 @@ import { Provider } from 'react-redux'; import { AutoSizerProps } from 'react-virtualized-auto-sizer'; import { byRole, byTestId, byText } from 'testing-library-selector'; -import { logInfo } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { LogMessages } from '../../Analytics'; +import * as analytics from '../../Analytics'; import { useHasRuler } from '../../hooks/useHasRuler'; import { mockExportApi, mockFolderApi, setupMswServer } from '../../mockApi'; import { grantUserPermissions, mockCombinedRule, mockDataSource, mockFolder, mockGrafanaRulerRule } from '../../mocks'; @@ -19,13 +18,9 @@ import { grantUserPermissions, mockCombinedRule, mockDataSource, mockFolder, moc import { RulesGroup } from './RulesGroup'; jest.mock('../../hooks/useHasRuler'); -jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - return { - ...original, - logInfo: jest.fn(), - }; -}); + +jest.spyOn(analytics, 'logInfo'); + jest.mock('react-virtualized-auto-sizer', () => { return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 }); }); @@ -235,7 +230,7 @@ describe('Rules group tests', () => { await userEvent.click(screen.getByText('Cancel')); expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); - expect(logInfo).toHaveBeenCalledWith(LogMessages.leavingRuleGroupEdit); + expect(analytics.logInfo).toHaveBeenCalledWith(analytics.LogMessages.leavingRuleGroupEdit); }); }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index df792d74b08..5c26d5a402e 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -4,12 +4,11 @@ import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { logInfo } from '@grafana/runtime'; import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { LogMessages } from '../../Analytics'; +import { logInfo, LogMessages } from '../../Analytics'; import { useFolder } from '../../hooks/useFolder'; import { useHasRuler } from '../../hooks/useHasRuler'; import { deleteRulesGroupAction } from '../../state/actions'; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 0907b3a9cb6..f6cb7a5e0a3 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -33,7 +33,13 @@ import { } from 'app/types/unified-alerting-dto'; import { backendSrv } from '../../../../core/services/backend_srv'; -import { logInfo, LogMessages, withPerformanceLogging } from '../Analytics'; +import { + logInfo, + LogMessages, + withPerformanceLogging, + withPromRulesMetadataLogging, + withRulerRulesMetadataLogging, +} from '../Analytics'; import { addAlertManagers, createOrUpdateSilence, @@ -115,10 +121,11 @@ export const fetchPromRulesAction = createAsyncThunk( ): Promise => { await thunkAPI.dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName })); - const fetchRulesWithLogging = withPerformanceLogging(fetchRules, `[${rulesSourceName}] Prometheus rules loaded`, { - dataSourceName: rulesSourceName, - thunk: 'unifiedalerting/fetchPromRules', - }); + const fetchRulesWithLogging = withPromRulesMetadataLogging( + fetchRules, + `[${rulesSourceName}] Prometheus rules loaded`, + { dataSourceName: rulesSourceName, thunk: 'unifiedalerting/fetchPromRules' } + ); return await withSerializedError( fetchRulesWithLogging(rulesSourceName, filter, limitAlerts, matcher, state, identifier) @@ -155,7 +162,7 @@ export const fetchRulerRulesAction = createAsyncThunk( await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName })); const rulerConfig = getDataSourceRulerConfig(getState, rulesSourceName); - const fetchRulerRulesWithLogging = withPerformanceLogging( + const fetchRulerRulesWithLogging = withRulerRulesMetadataLogging( fetchRulerRules, `[${rulesSourceName}] Ruler rules loaded`, {