mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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', () => {
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
152
public/app/plugins/datasource/elasticsearch/module.test.ts
Normal file
152
public/app/plugins/datasource/elasticsearch/module.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
129
public/app/plugins/datasource/elasticsearch/tracking.ts
Normal file
129
public/app/plugins/datasource/elasticsearch/tracking.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user