Elasticsearch: Add tracking for plugin adoption stats (#59954)

* Elasticsearch: Add tracking

* Update public/app/plugins/datasource/elasticsearch/tracking.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Update public/app/plugins/datasource/elasticsearch/tracking.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Update public/app/plugins/datasource/elasticsearch/tracking.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Refactor getLineLimit

* Update public/app/plugins/datasource/elasticsearch/tracking.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Update public/app/plugins/datasource/elasticsearch/tracking.ts

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>

* Update not tracking for volume queries

Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
This commit is contained in:
Ivana Huckova
2022-12-08 11:28:40 +01:00
committed by GitHub
parent c5ee4e4ae1
commit 4b56493789
5 changed files with 347 additions and 6 deletions

View File

@@ -18,7 +18,7 @@ import {
TimeRange,
toUtc,
} from '@grafana/data';
import { BackendSrvRequest, FetchResponse } from '@grafana/runtime';
import { BackendSrvRequest, FetchResponse, reportInteraction } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
@@ -35,6 +35,7 @@ const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local';
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getBackendSrv: () => backendSrv,
reportInteraction: jest.fn(),
getDataSourceSrv: () => {
return {
getInstanceSettings: () => {
@@ -275,6 +276,23 @@ describe('ElasticDatasource', () => {
const { body } = await runScenario();
expect(body.query.bool.filter[1].query_string.query).toBe('escape\\:test');
});
it('should report query interaction', async () => {
await runScenario();
expect(reportInteraction).toHaveBeenCalledWith(
'grafana_elasticsearch_query_executed',
expect.objectContaining({
alias: '$varAlias',
app: 'test',
has_data: true,
has_error: false,
line_limit: undefined,
query_type: 'metric',
simultaneously_sent_query_count: 1,
with_lucene_query: true,
})
);
});
});
describe('When issuing logs query with interval pattern', () => {
@@ -344,6 +362,23 @@ describe('ElasticDatasource', () => {
expect(links[0].url).toBe('http://localhost:3000/${__value.raw}');
expect(links[0].title).toBe('Custom Label');
});
it('should report query interaction', async () => {
await setupDataSource();
expect(reportInteraction).toHaveBeenCalledWith(
'grafana_elasticsearch_query_executed',
expect.objectContaining({
alias: '$varAlias',
app: undefined,
has_data: true,
has_error: false,
line_limit: undefined,
query_type: 'logs',
simultaneously_sent_query_count: 1,
with_lucene_query: true,
})
);
});
});
describe('When issuing document query', () => {
@@ -380,6 +415,22 @@ describe('ElasticDatasource', () => {
const { body } = await runScenario();
expect(body.size).toBe(500);
});
it('should report query interaction', async () => {
await runScenario();
expect(reportInteraction).toHaveBeenCalledWith(
'grafana_elasticsearch_query_executed',
expect.objectContaining({
alias: undefined,
app: 'test',
has_data: false,
has_error: false,
line_limit: undefined,
query_type: 'raw_document',
simultaneously_sent_query_count: 1,
with_lucene_query: true,
})
);
});
});
describe('When getting an error on response', () => {

View File

@@ -1,6 +1,6 @@
import { cloneDeep, find, first as _first, isNumber, isObject, isString, map as _map } from 'lodash';
import { generate, lastValueFrom, Observable, of, throwError } from 'rxjs';
import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty } from 'rxjs/operators';
import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators';
import {
DataFrame,
@@ -50,9 +50,11 @@ import {
} from './components/QueryEditor/MetricAggregationsEditor/aggregations';
import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils';
import { defaultBucketAgg, hasMetricOfType } from './queryDef';
import { trackQuery } from './tracking';
import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery, TermsQuery } from './types';
import { coerceESVersion, getScriptValue, isSupportedVersion } from './utils';
export const REF_ID_STARTER_LOG_VOLUME = 'log-volume-';
// Those are metadata fields as defined in https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_identity_metadata_fields.
// custom fields can start with underscores, therefore is not safe to exclude anything that starts with one.
const ELASTIC_META_FIELDS = [
@@ -616,7 +618,7 @@ export class ElasticDatasource
});
const logsVolumeQuery: ElasticsearchQuery = {
refId: target.refId,
refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`,
query: target.query,
metrics: [{ type: 'count', id: '1' }],
timeField,
@@ -636,7 +638,7 @@ export class ElasticDatasource
const shouldRunTroughBackend =
request.app === CoreApp.Explore && config.featureToggles.elasticsearchBackendMigration;
if (shouldRunTroughBackend) {
return super.query(request);
return super.query(request).pipe(tap((response) => trackQuery(response, request.targets, request.app)));
}
let payload = '';
const targets = this.interpolateVariablesInQueries(cloneDeep(request.targets), request.scopedVars);
@@ -718,7 +720,8 @@ export class ElasticDatasource
}
return er.getTimeSeries();
})
}),
tap((response) => trackQuery(response, request.targets, request.app))
);
}

View File

@@ -0,0 +1,152 @@
import { DashboardLoadedEvent } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import './module';
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
getAppEvents: () => ({
subscribe: jest.fn((_, handler) => {
// Trigger test event
handler(
new DashboardLoadedEvent({
dashboardId: 'dashboard123',
orgId: 1,
userId: 2,
grafanaVersion: 'v9.0.0',
queries: {
elasticsearch: [
{
alias: '',
bucketAggs: [],
datasource: {
type: 'elasticsearch',
uid: 'PE50363A9B6833EE7',
},
metrics: [
{
id: '1',
settings: {
limit: '501',
},
type: 'logs',
},
],
query: 'abc:def',
refId: 'A',
timeField: '@timestamp',
},
{
alias: '',
bucketAggs: [],
datasource: {
type: 'elasticsearch',
uid: 'es1',
},
metrics: [
{
id: '1',
settings: {
size: '600',
},
type: 'raw_data',
},
],
query: '',
},
{
alias: 'alias',
bucketAggs: [
{
field: '@timestamp',
id: '2',
settings: {
interval: 'auto',
},
type: 'date_histogram',
},
],
datasource: {
type: 'elasticsearch',
uid: 'es1',
},
metrics: [
{
id: '3',
type: 'count',
},
],
query: 'abc:def',
},
{
alias: '',
bucketAggs: [],
datasource: {
type: 'elasticsearch',
uid: 'PE50363A9B6833EE7',
},
metrics: [
{
id: '1',
settings: {
size: '600',
},
type: 'raw_document',
},
],
query: '',
refId: 'A',
timeField: '@timestamp',
},
{
alias: '',
bucketAggs: [
{
field: '@timestamp',
id: '2',
settings: {
interval: 'auto',
},
type: 'date_histogram',
},
],
datasource: {
type: 'elasticsearch',
uid: 'es1',
},
metrics: [
{
field: 'counter',
id: '1',
type: 'avg',
},
],
query: '$test:abc',
},
],
},
})
);
}),
}),
};
});
describe('queriesOnInitDashboard', () => {
it('should report a grafana_elasticsearch_dashboard_loaded interaction ', () => {
expect(reportInteraction).toHaveBeenCalledWith('grafana_elasticsearch_dashboard_loaded', {
grafana_version: 'v9.0.0',
dashboard_id: 'dashboard123',
org_id: 1,
queries_count: 5,
queries_with_changed_line_limit_count: 1,
queries_with_lucene_query_count: 3,
queries_with_template_variables_count: 1,
raw_data_queries_count: 1,
raw_document_queries_count: 1,
logs_queries_count: 1,
metric_queries_count: 2,
});
});
});

View File

@@ -1,7 +1,13 @@
import { DataSourcePlugin } from '@grafana/data';
import { DashboardLoadedEvent, DataSourcePlugin } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime';
import { QueryEditor } from './components/QueryEditor';
import { ConfigEditor } from './configuration/ConfigEditor';
import { ElasticDatasource } from './datasource';
import { onDashboardLoadedHandler } from './tracking';
import { ElasticsearchQuery } from './types';
export const plugin = new DataSourcePlugin(ElasticDatasource).setQueryEditor(QueryEditor).setConfigEditor(ConfigEditor);
// Subscribe to on dashboard loaded event so that we can track plugin adoption
getAppEvents().subscribe<DashboardLoadedEvent<ElasticsearchQuery>>(DashboardLoadedEvent, onDashboardLoadedHandler);

View File

@@ -0,0 +1,129 @@
import { DashboardLoadedEvent, DataQueryResponse } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { variableRegex } from 'app/features/variables/utils';
import { REF_ID_STARTER_LOG_VOLUME } from './datasource';
import pluginJson from './plugin.json';
import { ElasticsearchQuery } from './types';
type ElasticSearchOnDashboardLoadedTrackingEvent = {
grafana_version?: string;
dashboard_id?: string;
org_id?: number;
/* The number of Elasticsearch queries present in the dashboard*/
queries_count: number;
/* The number of Elasticsearch logs queries present in the dashboard*/
logs_queries_count: number;
/* The number of Elasticsearch metric queries present in the dashboard*/
metric_queries_count: number;
/* The number of Elasticsearch raw data queries present in the dashboard*/
raw_data_queries_count: number;
/* The number of Elasticsearch raw documents queries present in the dashboard*/
raw_document_queries_count: number;
/* The number of Elasticsearch queries with used template variables present in the dashboard*/
queries_with_template_variables_count: number;
/* The number of Elasticsearch queries with changed line limit present in the dashboard*/
queries_with_changed_line_limit_count: number;
/* The number of Elasticsearch queries with lucene query present in the dashboard*/
queries_with_lucene_query_count: number;
};
export const onDashboardLoadedHandler = ({
payload: { dashboardId, orgId, grafanaVersion, queries },
}: DashboardLoadedEvent<ElasticsearchQuery>) => {
try {
// We only want to track visible ElasticSearch queries
const elasticsearchQueries = queries[pluginJson.id].filter((query) => !query.hide);
if (!elasticsearchQueries?.length) {
return;
}
const queriesWithTemplateVariables = elasticsearchQueries.filter(isQueryWithTemplateVariables);
const queriesWithLuceneQuery = elasticsearchQueries.filter((query) => !!query.query);
const logsQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'logs');
const metricQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'metric');
const rawDataQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'raw_data');
const rawDocumentQueries = elasticsearchQueries.filter((query) => getQueryType(query) === 'raw_document');
const queriesWithChangedLineLimit = elasticsearchQueries.filter(isQueryWithChangedLineLimit);
const event: ElasticSearchOnDashboardLoadedTrackingEvent = {
grafana_version: grafanaVersion,
dashboard_id: dashboardId,
org_id: orgId,
queries_count: elasticsearchQueries.length,
logs_queries_count: logsQueries.length,
metric_queries_count: metricQueries.length,
raw_data_queries_count: rawDataQueries.length,
raw_document_queries_count: rawDocumentQueries.length,
queries_with_template_variables_count: queriesWithTemplateVariables.length,
queries_with_changed_line_limit_count: queriesWithChangedLineLimit.length,
queries_with_lucene_query_count: queriesWithLuceneQuery.length,
};
reportInteraction('grafana_elasticsearch_dashboard_loaded', event);
} catch (error) {
console.error('error in elasticsearch tracking handler', error);
}
};
const getQueryType = (query: ElasticsearchQuery): string | undefined => {
if (!query.metrics || !query.metrics.length) {
return undefined;
}
const nonMetricQueryTypes = ['logs', 'raw_data', 'raw_document'];
if (nonMetricQueryTypes.includes(query.metrics[0].type)) {
return query.metrics[0].type;
}
return 'metric';
};
const getLineLimit = (query: ElasticsearchQuery): number | undefined => {
if (query.metrics?.[0]?.type !== 'logs') {
return undefined;
}
const lineLimit = query.metrics?.[0].settings?.limit;
return lineLimit ? parseInt(lineLimit, 10) : undefined;
};
const isQueryWithChangedLineLimit = (query: ElasticsearchQuery): boolean => {
const lineLimit = getLineLimit(query);
return lineLimit !== undefined && lineLimit !== 500;
};
const isQueryWithTemplateVariables = (query: ElasticsearchQuery): boolean => {
return variableRegex.test(query.query ?? '');
};
const shouldNotReportBasedOnRefId = (refId: string): boolean => {
if (refId.startsWith(REF_ID_STARTER_LOG_VOLUME)) {
return true;
}
return false;
};
export function trackQuery(response: DataQueryResponse, queries: ElasticsearchQuery[], app: string): void {
for (const query of queries) {
if (shouldNotReportBasedOnRefId(query.refId)) {
return;
}
reportInteraction('grafana_elasticsearch_query_executed', {
app,
with_lucene_query: query.query ? true : false,
query_type: getQueryType(query),
line_limit: getLineLimit(query),
alias: query.alias,
has_error: response.error !== undefined,
has_data: response.data.some((frame) => frame.length > 0),
simultaneously_sent_query_count: queries.length,
});
}
}