[APM] EBT for unified search bar (#165819)

## Summary
closes: https://github.com/elastic/kibana/issues/146603

Introduce the telemetry service in APM which wraps the analytics module.

### New event-type telemetry 

Event type: `Search Query Submitted`
Payload: 
```json
{
  "kuery_fields": ["service.name", "service.name", "span.id"],
  "timerange": "2022-09-02T22:00:00.000Z - now+24h",
  "action": "refresh"
}
```

In addition to the custom properties, core sends context properties. See
all the fields [here](
https://docs.elastic.dev/telemetry/collection/event-based-telemetry-context)



e1e9a02c-b010-49f3-be8c-4ab2817d5845




### Notes
- Timerange: For the visualizations and the analysis the current format
might not be the best.
 
 TODO 

- [x] Update documentation
- [x] Add tests
- [x] Check visualizations

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Katerina 2023-09-08 14:32:09 +02:00 committed by GitHub
parent b7f57a09cb
commit 631ffbb9a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 271 additions and 19 deletions

View file

@ -1704,7 +1704,7 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
"total": {
"type": "long",
"_meta": {
"description": "Total number of shards for span and trasnaction indices"
"description": "Total number of shards for span and transaction indices"
}
}
}
@ -1718,7 +1718,7 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
"count": {
"type": "long",
"_meta": {
"description": "Total number of transaction and span documents overall"
"description": "Total number of metric documents overall"
}
}
}
@ -1728,7 +1728,7 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
"size_in_bytes": {
"type": "long",
"_meta": {
"description": "Size of the index in byte units overall."
"description": "Size of the metric indicess in byte units overall."
}
}
}

View file

@ -69,3 +69,27 @@ mappings snapshot used in the jest tests.
Behavioral telemetry is recorded with the ui_metrics and application_usage methods from the Usage Collection plugin.
Please fill this in with more details.
## Event based telemetry
Event-based telemetry (EBT) allows sending raw or minimally prepared data to the telemetry endpoints.
EBT is part of the core analytics service in Kibana and the `TelemetryService` provides an easy way to track custom events that are specific to `APM`.
#### Collect a new event type
1. You need to define the event type in the [telemetry_events.ts](https://github.com/elastic/kibana/blob/4283802c195231f710be0d9870615fbc31382a31/x-pack/plugins/apm/public/services/telemetry/telemetry_events.ts#L36)
2. Define the tracking method in the [telemetry_client.ts](https://github.com/elastic/kibana/blob/4283802c195231f710be0d9870615fbc31382a31/x-pack/plugins/apm/public/services/telemetry/telemetry_client.ts#L18)
3. Use the tracking method with the telemetry client (`telemetry.reportSearchQuerySumbitted({property: test})`)
In addition to the custom properties, analytics module automatically sends context properties. The list of the properties can be found [here](https://docs.elastic.dev/telemetry/collection/event-based-telemetry-context#browser-context)
#### How to check the events
In development, the events are sent to staging telemetry every hour and these events are stored in the `ebt-kibana-browser` dataview.
For instance, you can use a query like the following as an example to filter the apm event Search Query Submitted.
```
context.applicationId : "apm" and event_type : "Search Query Submitted"
```

View file

@ -15,7 +15,7 @@ import {
} from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { ConfigSchema } from '..';
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
import { ApmPluginSetupDeps, ApmPluginStartDeps, ApmServices } from '../plugin';
import { createCallApmApi } from '../services/rest/create_call_apm_api';
import { setHelpExtension } from '../set_help_extension';
import { setReadonlyBadge } from '../update_badge';
@ -24,7 +24,6 @@ import { ApmAppRoot } from '../components/routing/app_root';
/**
* This module is rendered asynchronously in the Kibana platform.
*/
export const renderApp = ({
coreStart,
pluginsSetup,
@ -32,6 +31,7 @@ export const renderApp = ({
config,
pluginsStart,
observabilityRuleTypeRegistry,
apmServices,
}: {
coreStart: CoreStart;
pluginsSetup: ApmPluginSetupDeps;
@ -39,6 +39,7 @@ export const renderApp = ({
config: ConfigSchema;
pluginsStart: ApmPluginStartDeps;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
apmServices: ApmServices;
}) => {
const { element, theme$ } = appMountParameters;
const apmPluginContextValue = {
@ -80,6 +81,7 @@ export const renderApp = ({
<ApmAppRoot
apmPluginContextValue={apmPluginContextValue}
pluginsStart={pluginsStart}
apmServices={apmServices}
/>
</KibanaThemeProvider>,
element

View file

@ -32,7 +32,7 @@ import { BreadcrumbsContextProvider } from '../../../context/breadcrumbs/context
import { LicenseProvider } from '../../../context/license/license_context';
import { TimeRangeIdContextProvider } from '../../../context/time_range_id/time_range_id_context';
import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context';
import { ApmPluginStartDeps } from '../../../plugin';
import { ApmPluginStartDeps, ApmServices } from '../../../plugin';
import { ApmErrorBoundary } from '../apm_error_boundary';
import { apmRouter } from '../apm_route_config';
import { TrackPageview } from '../track_pageview';
@ -49,9 +49,11 @@ const storage = new Storage(localStorage);
export function ApmAppRoot({
apmPluginContextValue,
pluginsStart,
apmServices,
}: {
apmPluginContextValue: ApmPluginContextValue;
pluginsStart: ApmPluginStartDeps;
apmServices: ApmServices;
}) {
const { appMountParameters, core } = apmPluginContextValue;
const { history } = appMountParameters;
@ -65,7 +67,9 @@ export function ApmAppRoot({
role="main"
>
<ApmPluginContext.Provider value={apmPluginContextValue}>
<KibanaContextProvider services={{ ...core, ...pluginsStart, storage }}>
<KibanaContextProvider
services={{ ...core, ...pluginsStart, storage, ...apmServices }}
>
<i18nCore.Context>
<ObservabilityAIAssistantProvider
value={apmPluginContextValue.observabilityAIAssistant}

View file

@ -22,7 +22,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWith
import { OnRefreshChangeProps } from '@elastic/eui/src/components/date_picker/types';
import { UIProcessorEvent } from '../../../../common/processor_event';
import { TimePickerTimeDefaults } from '../date_picker/typings';
import { ApmPluginStartDeps } from '../../../plugin';
import { ApmPluginStartDeps, ApmServices } from '../../../plugin';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { useApmDataView } from '../../../hooks/use_apm_data_view';
import { useProcessorEvent } from '../../../hooks/use_processor_event';
@ -36,6 +36,8 @@ import {
toBoolean,
toNumber,
} from '../../../context/url_params_context/helpers';
import { getKueryFields } from '../../../../common/utils/get_kuery_fields';
import { SearchQueryActions } from '../../../services/telemetry';
export const DEFAULT_REFRESH_INTERVAL = 60000;
@ -138,11 +140,12 @@ export function UnifiedSearchBar({
},
core,
} = useApmPluginContext();
const { services } = useKibana<ApmPluginStartDeps>();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const {
data: {
query: { queryString: queryStringService, timefilter: timeFilterService },
},
telemetry,
} = services;
const {
@ -241,6 +244,7 @@ export function UnifiedSearchBar({
payload: { dateRange: TimeRange; query?: Query },
isUpdate?: boolean
) => {
let action = SearchQueryActions.Submit;
if (dataView == null) {
return;
}
@ -256,6 +260,9 @@ export function UnifiedSearchBar({
if (!res) {
return;
}
const kueryFields = getKueryFields([
fromKueryExpression(query?.query as string),
]);
const existingQueryParams = toQuery(location.search);
const updatedQueryWithTime = {
@ -274,8 +281,14 @@ export function UnifiedSearchBar({
search: fromQuery(newSearchParams),
});
} else {
action = SearchQueryActions.Refresh;
onRefresh();
}
telemetry.reportSearchQuerySubmitted({
kueryFields,
action,
timerange: `${rangeFrom} - ${rangeTo}`,
});
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}

View file

@ -82,7 +82,7 @@ import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/
import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension';
import { featureCatalogueEntry } from './feature_catalogue_entry';
import { APMServiceDetailLocator } from './locator/service_detail_locator';
import { ITelemetryClient, TelemetryService } from './services/telemetry';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
export type ApmPluginStart = void;
@ -106,6 +106,10 @@ export interface ApmPluginSetupDeps {
profiling?: ProfilingPluginSetup;
}
export interface ApmServices {
telemetry: ITelemetryClient;
}
export interface ApmPluginStartDeps {
alerting?: AlertingPluginPublicStart;
charts?: ChartsPluginStart;
@ -181,16 +185,17 @@ const apmTutorialTitle = i18n.translate(
);
export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
private telemetry: TelemetryService;
constructor(
private readonly initializerContext: PluginInitializerContext<ConfigSchema>
) {
this.initializerContext = initializerContext;
this.telemetry = new TelemetryService();
}
public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) {
const config = this.initializerContext.config.get();
const pluginSetupDeps = plugins;
const { featureFlags } = config;
if (pluginSetupDeps.home) {
@ -273,6 +278,8 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
};
};
this.telemetry.setup({ analytics: core.analytics });
// Registers a status check callback for the tutorial to call and verify if the APM integration is installed on fleet.
pluginSetupDeps.home?.tutorials.registerCustomStatusCheck(
'apm_fleet_server_status_check',
@ -332,6 +339,9 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
const { observabilityRuleTypeRegistry } = plugins.observability;
// Register APM telemetry based events
const telemetry = this.telemetry.start();
core.application.register({
id: 'apm',
title: 'APM',
@ -388,7 +398,6 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
import('./application'),
core.getStartServices(),
]);
return renderApp({
coreStart,
pluginsSetup: pluginSetupDeps,
@ -396,6 +405,9 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
config,
pluginsStart: pluginsStart as ApmPluginStartDeps,
observabilityRuleTypeRegistry,
apmServices: {
telemetry,
},
});
},
});

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './telemetry_client';
export * from './telemetry_service';
export * from './types';

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
import {
TelemetryEventTypes,
ITelemetryClient,
SearchQuerySubmittedParams,
} from './types';
export class TelemetryClient implements ITelemetryClient {
constructor(private analytics: AnalyticsServiceSetup) {}
public reportSearchQuerySubmitted = ({
kueryFields,
timerange,
action,
}: SearchQuerySubmittedParams) => {
this.analytics.reportEvent(TelemetryEventTypes.SEARCH_QUERY_SUBMITTED, {
kueryFields,
timerange,
action,
});
};
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TelemetryEventTypes, TelemetryEvent } from './types';
const searchQuerySubmittedEventType: TelemetryEvent = {
eventType: TelemetryEventTypes.SEARCH_QUERY_SUBMITTED,
schema: {
kueryFields: {
type: 'array',
items: {
type: 'text',
_meta: {
description: 'The kuery fields used in the search',
},
},
},
timerange: {
type: 'text',
_meta: {
description: 'The timerange of the search',
},
},
action: {
type: 'keyword',
_meta: {
description: 'The action performed (e.g., submit, refresh)',
},
},
},
};
export const apmTelemetryEventBasedTypes = [searchQuerySubmittedEventType];

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { coreMock } from '@kbn/core/server/mocks';
import { apmTelemetryEventBasedTypes } from './telemetry_events';
import { TelemetryService } from './telemetry_service';
import { SearchQueryActions, TelemetryEventTypes } from './types';
describe('TelemetryService', () => {
const service = new TelemetryService();
const mockCoreStart = coreMock.createSetup();
service.setup({ analytics: mockCoreStart.analytics });
it('should register all events', () => {
expect(mockCoreStart.analytics.registerEventType).toHaveBeenCalledTimes(
apmTelemetryEventBasedTypes.length
);
});
it('should report search query event with the properties', async () => {
const telemetry = service.start();
telemetry.reportSearchQuerySubmitted({
kueryFields: ['service.name', 'span.id'],
action: SearchQueryActions.Submit,
timerange: 'now-15h-now',
});
expect(mockCoreStart.analytics.reportEvent).toHaveBeenCalledTimes(1);
expect(mockCoreStart.analytics.reportEvent).toHaveBeenCalledWith(
TelemetryEventTypes.SEARCH_QUERY_SUBMITTED,
{
kueryFields: ['service.name', 'span.id'],
action: SearchQueryActions.Submit,
timerange: 'now-15h-now',
}
);
});
});

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
import {
TelemetryServiceSetupParams,
ITelemetryClient,
TelemetryEventParams,
} from './types';
import { apmTelemetryEventBasedTypes } from './telemetry_events';
import { TelemetryClient } from './telemetry_client';
/**
* Service that interacts with the Core's analytics module
*/
export class TelemetryService {
constructor(private analytics: AnalyticsServiceSetup | null = null) {}
public setup({ analytics }: TelemetryServiceSetupParams) {
this.analytics = analytics;
apmTelemetryEventBasedTypes.forEach((eventConfig) =>
analytics.registerEventType<TelemetryEventParams>(eventConfig)
);
}
public start(): ITelemetryClient {
if (!this.analytics) {
throw new Error(
'The TelemetryService.setup() method has not been invoked, be sure to call it during the plugin setup.'
);
}
return new TelemetryClient(this.analytics);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RootSchema } from '@kbn/analytics-client';
import type { AnalyticsServiceSetup } from '@kbn/core/public';
export interface TelemetryServiceSetupParams {
analytics: AnalyticsServiceSetup;
}
export enum SearchQueryActions {
Submit = 'submit',
Refresh = 'refresh',
}
export interface SearchQuerySubmittedParams {
kueryFields: string[];
timerange: string;
action: SearchQueryActions;
}
export type TelemetryEventParams = SearchQuerySubmittedParams;
export interface ITelemetryClient {
reportSearchQuerySubmitted(params: SearchQuerySubmittedParams): void;
}
export enum TelemetryEventTypes {
SEARCH_QUERY_SUBMITTED = 'Search Query Submitted',
}
export interface TelemetryEvent {
eventType: TelemetryEventTypes.SEARCH_QUERY_SUBMITTED;
schema: RootSchema<SearchQuerySubmittedParams>;
}

View file

@ -931,7 +931,7 @@ export const apmSchema: MakeSchemaFrom<APMUsage, true> = {
type: 'long',
_meta: {
description:
'Total number of shards for span and trasnaction indices',
'Total number of shards for span and transaction indices',
},
},
},
@ -941,8 +941,7 @@ export const apmSchema: MakeSchemaFrom<APMUsage, true> = {
count: {
type: 'long',
_meta: {
description:
'Total number of transaction and span documents overall',
description: 'Total number of metric documents overall',
},
},
},
@ -950,7 +949,8 @@ export const apmSchema: MakeSchemaFrom<APMUsage, true> = {
size_in_bytes: {
type: 'long',
_meta: {
description: 'Size of the index in byte units overall.',
description:
'Size of the metric indicess in byte units overall.',
},
},
},

View file

@ -96,6 +96,8 @@
"@kbn/discover-plugin",
"@kbn/observability-ai-assistant-plugin",
"@kbn/apm-data-access-plugin",
"@kbn/core-analytics-server",
"@kbn/analytics-client",
"@kbn/monaco"
],
"exclude": ["target/**/*"]

View file

@ -4796,7 +4796,7 @@
"total": {
"type": "long",
"_meta": {
"description": "Total number of shards for span and trasnaction indices"
"description": "Total number of shards for span and transaction indices"
}
}
}
@ -4810,7 +4810,7 @@
"count": {
"type": "long",
"_meta": {
"description": "Total number of transaction and span documents overall"
"description": "Total number of metric documents overall"
}
}
}
@ -4820,7 +4820,7 @@
"size_in_bytes": {
"type": "long",
"_meta": {
"description": "Size of the index in byte units overall."
"description": "Size of the metric indicess in byte units overall."
}
}
}