Usage insights query caching (#47893)

* Updates queryResponse tests to include new cachedResponse bool

* Adds cachedResponse bool to QueryResponse

* Adds tests to assert the correct query counts (totalQueries and cachedQueries) are dispatched for data-requests

* Adds totalQueries and cachedQueries counts to the data-request events

* Adds new metrics and their descriptions to docs

* uses more descriptive variable name

* changes naming

* removes hyphen in docs

* extracts calculations to own lines prior to assignment
This commit is contained in:
owensmallwood 2022-04-21 09:16:13 -06:00 committed by GitHub
parent f3743bb652
commit 9a4bd1f2d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 11 deletions

View File

@ -45,6 +45,8 @@ Logs of usage insights contain the following fields, where the fields followed b
| `tokenId`\* | number | ID of the users authentication token. | | `tokenId`\* | number | ID of the users authentication token. |
| `username`\* | string | Name of the Grafana user that made the request. | | `username`\* | string | Name of the Grafana user that made the request. |
| `userId`\* | number | ID of the Grafana user that made the request. | | `userId`\* | number | ID of the Grafana user that made the request. |
| `totalQueries`\* | number | Number of queries executed for the data request. |
| `cachedQueries`\* | number | Number of fetched queries that came from the cache. |
## Configuration ## Configuration

View File

@ -47,6 +47,9 @@ export interface QueryResultMeta {
/** The path for live stream updates for this frame */ /** The path for live stream updates for this frame */
channel?: string; channel?: string;
/** Did the query response come from the cache */
isCachedResponse?: boolean;
/** /**
* Optionally identify which topic the frame should be assigned to. * Optionally identify which topic the frame should be assigned to.
* A value specified in the response will override what the request asked for. * A value specified in the response will override what the request asked for.

View File

@ -55,6 +55,8 @@ export interface DashboardViewEventPayload extends DashboardInfo {
*/ */
export interface DataRequestEventPayload extends DataRequestInfo { export interface DataRequestEventPayload extends DataRequestInfo {
eventName: MetaAnalyticsEventName.DataRequest; eventName: MetaAnalyticsEventName.DataRequest;
totalQueries?: number;
cachedQueries?: number;
} }
/** /**

View File

@ -298,10 +298,12 @@ describe('Query Response parser', () => {
}; };
}); });
test('adds notice for responses with X-Cache: HIT header', () => { test('adds notice and cached boolean for responses with X-Cache: HIT header', () => {
const queries: DataQuery[] = [{ refId: 'A' }]; const queries: DataQuery[] = [{ refId: 'A' }];
resp.headers.set('X-Cache', 'HIT'); resp.headers.set('X-Cache', 'HIT');
expect(toDataQueryResponse(resp, queries).data[0].meta.notices).toStrictEqual([cachedResponseNotice]); const meta = toDataQueryResponse(resp, queries).data[0].meta;
expect(meta.notices).toStrictEqual([cachedResponseNotice]);
expect(meta.isCachedResponse).toBeTruthy();
}); });
test('does not remove existing notices', () => { test('does not remove existing notices', () => {
@ -314,10 +316,11 @@ describe('Query Response parser', () => {
]); ]);
}); });
test('does not add notice for responses with X-Cache: MISS header', () => { test('does not add notice or cached response boolean for responses with X-Cache: MISS header', () => {
const queries: DataQuery[] = [{ refId: 'A' }]; const queries: DataQuery[] = [{ refId: 'A' }];
resp.headers.set('X-Cache', 'MISS'); resp.headers.set('X-Cache', 'MISS');
expect(toDataQueryResponse(resp, queries).data[0].meta?.notices).toBeUndefined(); expect(toDataQueryResponse(resp, queries).data[0].meta?.notices).toBeUndefined();
expect(toDataQueryResponse(resp, queries).data[0].meta?.isCachedResponse).toBeUndefined();
}); });
test('does not add notice for responses without X-Cache header', () => { test('does not add notice for responses without X-Cache header', () => {

View File

@ -152,6 +152,7 @@ function addCacheNotice(frame: DataFrameJSON): DataFrameJSON {
meta: { meta: {
...frame.schema?.meta, ...frame.schema?.meta,
notices: [...(frame.schema?.meta?.notices ?? []), cachedResponseNotice], notices: [...(frame.schema?.meta?.notices ?? []), cachedResponseNotice],
isCachedResponse: true,
}, },
}, },
}; };

View File

@ -1,7 +1,7 @@
import { MetaAnalyticsEventName, reportMetaAnalytics } from '@grafana/runtime'; import { MetaAnalyticsEventName, reportMetaAnalytics } from '@grafana/runtime';
import { CoreApp, DataQueryRequest, DataSourceApi, dateTime, LoadingState, PanelData } from '@grafana/data'; import { CoreApp, DataFrame, DataQueryRequest, DataSourceApi, dateTime, LoadingState, PanelData } from '@grafana/data';
import { emitDataRequestEvent } from './queryAnalytics'; import { emitDataRequestEvent } from './queryAnalytics';
import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { DashboardModel } from '../../dashboard/state';
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -40,7 +40,39 @@ jest.mock('@grafana/data', () => ({
}, },
})); }));
function getTestData(requestApp: string): PanelData { const partiallyCachedSeries = [
{
refId: 'A',
meta: {
isCachedResponse: true,
},
fields: [],
length: 0,
},
{
refId: 'B',
fields: [],
length: 0,
},
];
const multipleDataframesWithSameRefId = [
{
refId: 'A',
meta: {
isCachedResponse: true,
},
fields: [],
length: 0,
},
{
refId: 'A',
fields: [],
length: 0,
},
];
function getTestData(requestApp: string, series: DataFrame[] = []): PanelData {
const now = dateTime(); const now = dateTime();
return { return {
request: { request: {
@ -50,7 +82,7 @@ function getTestData(requestApp: string): PanelData {
startTime: now.unix(), startTime: now.unix(),
endTime: now.add(1, 's').unix(), endTime: now.add(1, 's').unix(),
} as DataQueryRequest, } as DataQueryRequest,
series: [], series,
state: LoadingState.Done, state: LoadingState.Done,
timeRange: { timeRange: {
from: dateTime(), from: dateTime(),
@ -61,10 +93,9 @@ function getTestData(requestApp: string): PanelData {
} }
describe('emitDataRequestEvent - from a dashboard panel', () => { describe('emitDataRequestEvent - from a dashboard panel', () => {
const data = getTestData(CoreApp.Dashboard);
const fn = emitDataRequestEvent(datasource);
it('Should report meta analytics', () => { it('Should report meta analytics', () => {
fn(data); const data = getTestData(CoreApp.Dashboard);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1); expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith( expect(reportMetaAnalytics).toBeCalledWith(
@ -79,19 +110,71 @@ describe('emitDataRequestEvent - from a dashboard panel', () => {
folderName: 'Test Folder', folderName: 'Test Folder',
dataSize: 0, dataSize: 0,
duration: 1, duration: 1,
totalQueries: 0,
cachedQueries: 0,
})
);
});
it('Should report meta analytics with counts for cached and total queries', () => {
const data = getTestData(CoreApp.Dashboard, partiallyCachedSeries);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceId: datasource.id,
panelId: 2,
dashboardId: 1,
dashboardName: 'Test Dashboard',
dashboardUid: 'test',
folderName: 'Test Folder',
dataSize: 2,
duration: 1,
totalQueries: 2,
cachedQueries: 1,
})
);
});
it('Should report meta analytics with counts for cached and total queries when same refId spread across multiple DataFrames', () => {
const data = getTestData(CoreApp.Dashboard, multipleDataframesWithSameRefId);
emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
expect(reportMetaAnalytics).toBeCalledWith(
expect.objectContaining({
eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name,
datasourceId: datasource.id,
panelId: 2,
dashboardId: 1,
dashboardName: 'Test Dashboard',
dashboardUid: 'test',
folderName: 'Test Folder',
dataSize: 2,
duration: 1,
totalQueries: 1,
cachedQueries: 1,
}) })
); );
}); });
it('Should not report meta analytics twice if the request receives multiple responses', () => { it('Should not report meta analytics twice if the request receives multiple responses', () => {
const data = getTestData(CoreApp.Dashboard);
const fn = emitDataRequestEvent(datasource);
fn(data); fn(data);
expect(reportMetaAnalytics).not.toBeCalled(); fn(data);
expect(reportMetaAnalytics).toBeCalledTimes(1);
}); });
it('Should not report meta analytics in edit mode', () => { it('Should not report meta analytics in edit mode', () => {
mockGetUrlSearchParams.mockImplementationOnce(() => { mockGetUrlSearchParams.mockImplementationOnce(() => {
return { editPanel: 2 }; return { editPanel: 2 };
}); });
const data = getTestData(CoreApp.Dashboard);
emitDataRequestEvent(datasource)(data); emitDataRequestEvent(datasource)(data);
expect(reportMetaAnalytics).not.toBeCalled(); expect(reportMetaAnalytics).not.toBeCalled();
}); });

View File

@ -19,6 +19,16 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
return; return;
} }
const queryCacheStatus: { [key: string]: boolean } = {};
for (let i = 0; i < data.series.length; i++) {
const refId = data.series[i].refId;
if (refId && !queryCacheStatus[refId]) {
queryCacheStatus[refId] = data.series[i].meta?.isCachedResponse ?? false;
}
}
const totalQueries = Object.keys(queryCacheStatus).length;
const cachedQueries = Object.values(queryCacheStatus).filter((val) => val === true).length;
const eventData: DataRequestEventPayload = { const eventData: DataRequestEventPayload = {
eventName: MetaAnalyticsEventName.DataRequest, eventName: MetaAnalyticsEventName.DataRequest,
datasourceName: datasource.name, datasourceName: datasource.name,
@ -28,6 +38,8 @@ export function emitDataRequestEvent(datasource: DataSourceApi) {
dashboardId: data.request.dashboardId, dashboardId: data.request.dashboardId,
dataSize: 0, dataSize: 0,
duration: data.request.endTime! - data.request.startTime, duration: data.request.endTime! - data.request.startTime,
totalQueries,
cachedQueries,
}; };
// enrich with dashboard info // enrich with dashboard info