[ML][AIOps] Telemetry: track analysis endpoint usage (#166988)

## Summary

This PR adds tracking for Log Rate Analysis and Log Pattern Analysis
endpoints for AIOps.
- tracks type of analysis and source (where the analysis is being run
from)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Melissa Alvarez 2023-09-29 09:12:09 -07:00 committed by GitHub
parent 7dd352a65e
commit 0bdbcc0ccc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 153 additions and 21 deletions

View file

@ -27,7 +27,7 @@ export interface VisualizeFieldContext {
export interface CategorizeFieldContext {
field: DataViewField;
dataView: DataView;
originatingApp?: string;
originatingApp: string;
}
export const ACTION_VISUALIZE_FIELD = 'ACTION_VISUALIZE_FIELD';

View file

@ -8,7 +8,7 @@
import startsWith from 'lodash/startsWith';
import type { Reducer, ReducerAction } from 'react';
import type { HttpSetup } from '@kbn/core/public';
import type { HttpSetup, HttpFetchOptions } from '@kbn/core/public';
type GeneratorError = string | null;
@ -42,7 +42,8 @@ export async function* fetchStream<B extends object, R extends Reducer<any, any>
apiVersion: string | undefined,
abortCtrl: React.MutableRefObject<AbortController>,
body?: B,
ndjson = true
ndjson = true,
headers?: HttpFetchOptions['headers']
): AsyncGenerator<[GeneratorError, ReducerAction<R> | Array<ReducerAction<R>> | undefined]> {
let stream: Readonly<Response> | undefined;
@ -52,6 +53,7 @@ export async function* fetchStream<B extends object, R extends Reducer<any, any>
version: apiVersion,
asResponse: true,
rawResponse: true,
headers,
...(body && Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}),
});

View file

@ -16,7 +16,7 @@ import {
} from 'react';
import useThrottle from 'react-use/lib/useThrottle';
import type { HttpSetup } from '@kbn/core/public';
import type { HttpSetup, HttpFetchOptions } from '@kbn/core/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { fetchStream } from './fetch_stream';
@ -64,7 +64,8 @@ export function useFetchStream<B extends object, R extends Reducer<any, any>>(
endpoint: string,
apiVersion?: string,
body?: B,
customReducer?: FetchStreamCustomReducer<R>
customReducer?: FetchStreamCustomReducer<R>,
headers?: HttpFetchOptions['headers']
) {
const [errors, setErrors] = useState<string[]>([]);
const [isCancelled, setIsCancelled] = useState(false);
@ -104,7 +105,8 @@ export function useFetchStream<B extends object, R extends Reducer<any, any>>(
apiVersion,
abortCtrl,
body,
customReducer !== undefined
customReducer !== undefined,
headers
)) {
if (fetchStreamError !== null) {
addError(fetchStreamError);

View file

@ -19,3 +19,8 @@ export const RANDOM_SAMPLER_SEED = 3867412;
export const CASES_ATTACHMENT_CHANGE_POINT_CHART = 'aiopsChangePointChart';
export const EMBEDDABLE_CHANGE_POINT_CHART_TYPE = 'aiopsChangePointChart' as const;
export const AIOPS_TELEMETRY_ID = {
AIOPS_DEFAULT_SOURCE: 'ml_aiops_labs',
AIOPS_ANALYSIS_RUN_ORIGIN: 'aiops-analysis-run-origin',
} as const;

View file

@ -20,7 +20,8 @@
"unifiedSearch"
],
"optionalPlugins": [
"cases"
"cases",
"usageCollection"
],
"requiredBundles": [
"fieldFormats",

View file

@ -27,7 +27,7 @@ export const categorizeFieldAction = (coreStart: CoreStart, plugins: AiopsPlugin
return field.esTypes?.includes('text') === true;
},
execute: async (context: CategorizeFieldContext) => {
const { field, dataView } = context;
showCategorizeFlyout(field, dataView, coreStart, plugins);
const { field, dataView, originatingApp } = context;
showCategorizeFlyout(field, dataView, coreStart, plugins, originatingApp);
},
});

View file

@ -15,6 +15,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import { DataSourceContext } from '../../hooks/use_data_source';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
@ -65,7 +66,7 @@ export const LogCategorizationAppState: FC<LogCategorizationAppStateProps> = ({
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<LogCategorizationPage />
<LogCategorizationPage embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE} />
</DatePickerContextProvider>
</StorageContextProvider>
</DataSourceContext.Provider>

View file

@ -22,6 +22,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { buildEmptyFilter, Filter } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import type { FieldValidationResults } from '@kbn/ml-category-validator';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import type { Category, SparkLinesPerCategory } from '../../../common/api/log_categorization/types';
@ -49,6 +50,8 @@ export interface LogCategorizationPageProps {
savedSearch: SavedSearch | null;
selectedField: DataViewField;
onClose: () => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
const BAR_TARGET = 20;
@ -58,6 +61,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
savedSearch,
selectedField,
onClose,
embeddingOrigin,
}) => {
const {
notifications: { toasts },
@ -151,7 +155,8 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
timeField,
earliest,
latest,
searchQuery
searchQuery,
{ [AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin }
),
runCategorizeRequest(
index,
@ -193,6 +198,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
runCategorizeRequest,
intervalMs,
toasts,
embeddingOrigin,
]);
const onAddFilter = useCallback(

View file

@ -26,6 +26,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
import type { FieldValidationResults } from '@kbn/ml-category-validator';
import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import type { Category, SparkLinesPerCategory } from '../../../common/api/log_categorization/types';
@ -53,7 +54,12 @@ import { FieldValidationCallout } from './category_validation_callout';
const BAR_TARGET = 20;
const DEFAULT_SELECTED_FIELD = 'message';
export const LogCategorizationPage: FC = () => {
interface LogCategorizationPageProps {
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddingOrigin }) => {
const {
notifications: { toasts },
} = useAiopsAppContext();
@ -208,7 +214,10 @@ export const LogCategorizationPage: FC = () => {
try {
const [validationResult, categorizationResult] = await Promise.all([
runValidateFieldRequest(index, selectedField, timeField, earliest, latest, searchQuery),
runValidateFieldRequest(index, selectedField, timeField, earliest, latest, searchQuery, {
[AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin,
}),
runCategorizeRequest(
index,
selectedField,
@ -245,6 +254,7 @@ export const LogCategorizationPage: FC = () => {
runCategorizeRequest,
intervalMs,
toasts,
embeddingOrigin,
]);
useEffect(

View file

@ -29,7 +29,8 @@ export async function showCategorizeFlyout(
field: DataViewField,
dataView: DataView,
coreStart: CoreStart,
plugins: AiopsPluginStartDeps
plugins: AiopsPluginStartDeps,
originatingApp: string
): Promise<void> {
const { http, theme, overlays, application, notifications, uiSettings, i18n } = coreStart;
@ -70,6 +71,7 @@ export async function showCategorizeFlyout(
savedSearch={null}
selectedField={field}
onClose={onFlyoutClose}
embeddingOrigin={originatingApp}
/>
</StorageContextProvider>
</DatePickerContextProvider>

View file

@ -11,6 +11,8 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type
import type { FieldValidationResults } from '@kbn/ml-category-validator';
import type { HttpFetchOptions } from '@kbn/core/public';
import { AIOPS_API_ENDPOINT } from '../../../common/api';
import { createCategorizeQuery } from '../../../common/api/log_categorization/create_categorize_query';
@ -27,7 +29,8 @@ export function useValidateFieldRequest() {
timeField: string,
start: number | undefined,
end: number | undefined,
queryIn: QueryDslQueryContainer
queryIn: QueryDslQueryContainer,
headers?: HttpFetchOptions['headers']
) => {
const query = createCategorizeQuery(queryIn, timeField, start, end);
const resp = await http.post<FieldValidationResults>(
@ -48,6 +51,7 @@ export function useValidateFieldRequest() {
indicesOptions: undefined,
includeExamples: false,
}),
headers,
version: '1',
}
);

View file

@ -64,6 +64,8 @@ export interface LogRateAnalysisContentProps {
barHighlightColorOverride?: string;
/** Optional callback that exposes data of the completed analysis */
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
@ -76,6 +78,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
embeddingOrigin,
}) => {
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
const [initialAnalysisStart, setInitialAnalysisStart] = useState<
@ -172,6 +175,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
onAnalysisCompleted={onAnalysisCompleted}
embeddingOrigin={embeddingOrigin}
/>
)}
{windowParameters === undefined && (

View file

@ -59,6 +59,8 @@ export interface LogRateAnalysisContentWrapperProps {
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Optional flag to indicate whether kibana is running in serverless */
showFrozenDataTierChoice?: boolean;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProps> = ({
@ -73,6 +75,7 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
barHighlightColorOverride,
onAnalysisCompleted,
showFrozenDataTierChoice = true,
embeddingOrigin,
}) => {
if (!dataView) return null;
@ -105,6 +108,7 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
onAnalysisCompleted={onAnalysisCompleted}
embeddingOrigin={embeddingOrigin}
/>
</DatePickerContextProvider>
</StorageContextProvider>

View file

@ -22,6 +22,7 @@ import {
getDefaultAiOpsListState,
type AiOpsPageUrlState,
} from '../../application/utils/url_state';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import { SearchPanel } from '../search_panel';
import { useLogRateAnalysisResultsTableRowContext } from '../log_rate_analysis_results_table/log_rate_analysis_results_table_row_provider';
@ -151,6 +152,7 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
setGlobalState={setGlobalState}
esSearchQuery={searchQuery}
stickyHistogram={stickyHistogram}
embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE}
/>
</EuiFlexGroup>
</EuiPageSection>

View file

@ -36,6 +36,7 @@ import type { SignificantTerm, SignificantTermGroup } from '@kbn/ml-agg-utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { initialState, streamReducer } from '../../../common/api/stream_reducer';
import type { AiopsApiLogRateAnalysis } from '../../../common/api';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import {
getGroupTableItems,
LogRateAnalysisResultsTable,
@ -113,6 +114,8 @@ interface LogRateAnalysisResultsProps {
barHighlightColorOverride?: string;
/** Optional callback that exposes data of the completed analysis */
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
@ -129,6 +132,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
embeddingOrigin,
}) => {
const { http } = useAiopsAppContext();
@ -198,7 +202,8 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
overrides,
sampleProbability,
},
{ reducer: streamReducer, initialState }
{ reducer: streamReducer, initialState },
{ [AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin }
);
const { significantTerms } = data;

View file

@ -0,0 +1,32 @@
/*
* 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 { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
import { trackAIOpsRouteUsage } from './track_route_usage';
describe('trackAIOpsRouteUsage', () => {
it('should call `usageCounter.incrementCounter`', () => {
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
trackAIOpsRouteUsage('test_type', 'test_source', mockUsageCounter);
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'test_type',
counterType: 'run_via_test_source',
incrementBy: 1,
});
});
it('should do nothing if no usage counter is provided', () => {
let err;
try {
trackAIOpsRouteUsage('test', undefined);
} catch (e) {
err = e;
}
expect(err).toBeUndefined();
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 { UsageCounter } from '@kbn/usage-collection-plugin/server';
export function trackAIOpsRouteUsage(
analysisType: string,
source?: string | string[],
usageCounter?: UsageCounter
) {
if (usageCounter && typeof source === 'string') {
usageCounter.incrementCounter({
counterName: analysisType,
counterType: `run_via_${source}`,
incrementBy: 1,
});
}
}

View file

@ -9,8 +9,10 @@ import { Subscription } from 'rxjs';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants';
import { PLUGIN_ID } from '../common';
import { isActiveLicense } from './lib/license';
import {
AiopsLicense,
@ -28,6 +30,7 @@ export class AiopsPlugin
{
private readonly logger: Logger;
private licenseSubscription: Subscription | null = null;
private usageCounter?: UsageCounter;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
@ -38,6 +41,7 @@ export class AiopsPlugin
plugins: AiopsPluginSetupDeps
) {
this.logger.debug('aiops: Setup');
this.usageCounter = plugins.usageCollection?.createUsageCounter(PLUGIN_ID);
// Subscribe to license changes and store the current license in `currentLicense`.
// This way we can pass on license changes to the route factory having always
@ -51,8 +55,8 @@ export class AiopsPlugin
// Register server side APIs
core.getStartServices().then(([coreStart, depsStart]) => {
defineLogRateAnalysisRoute(router, aiopsLicense, this.logger, coreStart);
defineLogCategorizationRoutes(router, aiopsLicense);
defineLogRateAnalysisRoute(router, aiopsLicense, this.logger, coreStart, this.usageCounter);
defineLogCategorizationRoutes(router, aiopsLicense, this.usageCounter);
});
if (plugins.cases) {

View file

@ -8,14 +8,18 @@
import type { IRouter } from '@kbn/core/server';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import { categorizationExamplesProvider } from '@kbn/ml-category-validator';
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { categorizationFieldValidationSchema } from '../../common/api/log_categorization/schema';
import { AIOPS_API_ENDPOINT } from '../../common/api';
import type { AiopsLicense } from '../types';
import { wrapError } from './error_wrapper';
import { trackAIOpsRouteUsage } from '../lib/track_route_usage';
import { AIOPS_TELEMETRY_ID } from '../../common/constants';
export const defineLogCategorizationRoutes = (
router: IRouter<DataRequestHandlerContext>,
license: AiopsLicense
license: AiopsLicense,
usageCounter?: UsageCounter
) => {
router.versioned
.post({
@ -32,6 +36,13 @@ export const defineLogCategorizationRoutes = (
},
},
async (context, request, response) => {
const { headers } = request;
trackAIOpsRouteUsage(
`POST ${AIOPS_API_ENDPOINT.CATEGORIZATION_FIELD_VALIDATION}`,
headers[AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN],
usageCounter
);
if (!license.isActivePlatinumLicense) {
return response.forbidden();
}

View file

@ -23,8 +23,9 @@ import type {
} from '@kbn/ml-agg-utils';
import { fetchHistogramsForFields } from '@kbn/ml-agg-utils';
import { createExecutionContext } from '@kbn/ml-route-utils';
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { RANDOM_SAMPLER_SEED } from '../../common/constants';
import { RANDOM_SAMPLER_SEED, AIOPS_TELEMETRY_ID } from '../../common/constants';
import {
addSignificantTermsAction,
addSignificantTermsGroupAction,
@ -52,6 +53,7 @@ import { fetchFrequentItemSets } from './queries/fetch_frequent_item_sets';
import { getHistogramQuery } from './queries/get_histogram_query';
import { getGroupFilter } from './queries/get_group_filter';
import { getSignificantTermGroups } from './queries/get_significant_term_groups';
import { trackAIOpsRouteUsage } from '../lib/track_route_usage';
// 10s ping frequency to keep the stream alive.
const PING_FREQUENCY = 10000;
@ -67,7 +69,8 @@ export const defineLogRateAnalysisRoute = (
router: IRouter<DataRequestHandlerContext>,
license: AiopsLicense,
logger: Logger,
coreStart: CoreStart
coreStart: CoreStart,
usageCounter?: UsageCounter
) => {
router.versioned
.post({
@ -85,6 +88,14 @@ export const defineLogRateAnalysisRoute = (
},
},
async (context, request, response) => {
const { headers } = request;
trackAIOpsRouteUsage(
`POST ${AIOPS_API_ENDPOINT.LOG_RATE_ANALYSIS}`,
headers[AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN],
usageCounter
);
if (!license.isActivePlatinumLicense) {
return response.forbidden();
}

View file

@ -8,11 +8,13 @@
import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type { CasesSetup } from '@kbn/cases-plugin/server';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
export interface AiopsPluginSetupDeps {
data: PluginSetup;
licensing: LicensingPluginStart;
cases?: CasesSetup;
usageCollection?: UsageCollectionSetup;
}
export interface AiopsPluginStartDeps {

View file

@ -63,6 +63,7 @@
"@kbn/core-lifecycle-browser",
"@kbn/cases-plugin",
"@kbn/react-kibana-mount",
"@kbn/usage-collection-plugin",
],
"exclude": [
"target/**/*",

View file

@ -268,6 +268,7 @@ export const LogRateAnalysis: FC<AlertDetailsLogRateAnalysisSectionProps> = ({ r
</EuiFlexItem>
<EuiFlexItem>
<LogRateAnalysisContent
embeddingOrigin="observability_log_threshold_alert_details"
dataView={dataView}
timeRange={timeRange}
esSearchQuery={esSearchQuery}