diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/cleanup_unknown_and_excluded.mocks.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/cleanup_unknown_and_excluded.mocks.ts index 1b0e0a49e5062f..54fb9902cc53e5 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/cleanup_unknown_and_excluded.mocks.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/cleanup_unknown_and_excluded.mocks.ts @@ -97,6 +97,11 @@ export const initialExcludeOnUpgradeQueryMock = { type: 'ml-telemetry', }, }, + { + term: { + type: 'osquery-manager-usage-metric', + }, + }, { term: { type: 'osquery-usage-metric', diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_page.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_page.tsx index 98ab86190dd160..15f618946cb8d5 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_page.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_page.tsx @@ -121,6 +121,7 @@ export const ChangePointDetectionPage: FC = () => { onClick={() => setFlyoutVisible(!isFlyoutVisible)} size={'s'} disabled={!hasSelectedChangePoints} + data-test-subj={'aiopsChangePointDetectionViewSelected'} > { onClose={setFlyoutVisible.bind(null, false)} aria-labelledby={'change_point_charts'} size={'l'} + data-test-subj={'aiopsChangePointDetectionSelectedCharts'} > diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_points_table.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_points_table.tsx index dd054b88076ece..905a76fe90e94c 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_points_table.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_points_table.tsx @@ -94,7 +94,9 @@ export const ChangePointsTable: FC = ({ const columns: Array> = [ { + id: 'timestamp', field: 'timestamp', + 'data-test-subj': 'aiopsChangePointTimestamp', name: i18n.translate('xpack.aiops.changePointDetection.timeColumn', { defaultMessage: 'Time', }), @@ -104,6 +106,8 @@ export const ChangePointsTable: FC = ({ render: (timestamp: ChangePointAnnotation['timestamp']) => dateFormatter.convert(timestamp), }, { + id: 'preview', + 'data-test-subj': 'aiopsChangePointPreview', name: i18n.translate('xpack.aiops.changePointDetection.previewColumn', { defaultMessage: 'Preview', }), @@ -118,6 +122,8 @@ export const ChangePointsTable: FC = ({ }, }, { + id: 'type', + 'data-test-subj': 'aiopsChangePointType', field: 'type', name: i18n.translate('xpack.aiops.changePointDetection.typeColumn', { defaultMessage: 'Type', @@ -127,6 +133,8 @@ export const ChangePointsTable: FC = ({ render: (type: ChangePointAnnotation['type']) => {type}, }, { + id: 'pValue', + 'data-test-subj': 'aiopsChangePointPValue', field: 'p_value', name: ( = ({ ...(fieldConfig.splitField ? [ { + id: 'groupName', + 'data-test-subj': 'aiopsChangePointGroupName', field: 'group.name', name: i18n.translate('xpack.aiops.changePointDetection.fieldNameColumn', { defaultMessage: 'Field name', @@ -160,6 +170,8 @@ export const ChangePointsTable: FC = ({ truncateText: false, }, { + id: 'groupValue', + 'data-test-subj': 'aiopsChangePointGroupValue', field: 'group.value', name: i18n.translate('xpack.aiops.changePointDetection.fieldValueColumn', { defaultMessage: 'Field value', @@ -218,7 +230,7 @@ export const ChangePointsTable: FC = ({ ); }, isPrimary: true, - 'data-test-subj': 'aiopsChangePointFilterForValue', + 'data-test-subj': 'aiopsChangePointFilterOutValue', }, ] as Array>, }, @@ -247,11 +259,15 @@ export const ChangePointsTable: FC = ({ itemId={'id'} selection={selectionValue} loading={isLoading} + data-test-subj={`aiopsChangePointResultsTable ${isLoading ? 'loading' : 'loaded'}`} items={annotations} columns={columns} pagination={{ pageSizeOptions: [5, 10, 15] }} sorting={defaultSorting} hasActions={hasActions} + rowProps={(item) => ({ + 'data-test-subj': `aiopsChangePointResultsTableRow row-${item.id}`, + })} message={ isLoading ? ( = ({ fieldConfig, annotat }); return ( -
+
{ return ( onChange(value, index)} onRemove={onRemove.bind(null, index)} @@ -105,7 +106,11 @@ export const FieldsConfig: FC = () => { ); })} - = MAX_CHANGE_POINT_CONFIGS}> + = MAX_CHANGE_POINT_CONFIGS} + data-test-subj={'aiopsChangePointAddConfig'} + > void; onRemove: () => void; onSelectionChange: (update: SelectedChangePoint[]) => void; + 'data-test-subj': string; } /** @@ -137,6 +143,7 @@ const FieldPanel: FC = ({ onRemove, removeDisabled, onSelectionChange, + 'data-test-subj': dataTestSubj, }) => { const { combinedQuery, requestParams } = useChangePointDetectionContext(); @@ -151,7 +158,7 @@ const FieldPanel: FC = ({ } = useChangePointResults(fieldConfig, requestParams, combinedQuery, splitFieldCardinality); return ( - + diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts index 981dbef21de7c6..9c1804674919a1 100644 --- a/x-pack/plugins/osquery/common/types.ts +++ b/x-pack/plugins/osquery/common/types.ts @@ -9,10 +9,6 @@ export const savedQuerySavedObjectType = 'osquery-saved-query'; export const packSavedObjectType = 'osquery-pack'; export const packAssetSavedObjectType = 'osquery-pack-asset'; export const usageMetricSavedObjectType = 'osquery-manager-usage-metric'; -export type SavedObjectType = - | 'osquery-saved-query' - | 'osquery-pack' - | 'osquery-manager-usage-metric'; /** * This makes any optional property the same as Required would but also has the diff --git a/x-pack/plugins/osquery/kibana.jsonc b/x-pack/plugins/osquery/kibana.jsonc index d01388a1619cd9..cd8e929da43d9f 100644 --- a/x-pack/plugins/osquery/kibana.jsonc +++ b/x-pack/plugins/osquery/kibana.jsonc @@ -26,7 +26,6 @@ "optionalPlugins": [ "fleet", "home", - "usageCollection", "lens", "telemetry", "cases" diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 49152eeba5cc68..084cb2b1ba9f5f 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -27,7 +27,6 @@ import type { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; import { initSavedObjects } from './saved_objects'; -import { initUsageCollectors } from './usage'; import type { OsqueryAppContext } from './lib/osquery_app_context_services'; import { OsqueryAppContextService } from './lib/osquery_app_context_services'; import type { ConfigType } from '../common/config'; @@ -78,11 +77,6 @@ export class OsqueryPlugin implements Plugin { - describe('Metric initalizer', () => { - const get = savedObjectsClient.get as jest.Mock; - const create = savedObjectsClient.create as jest.Mock; - afterEach(() => { - get.mockClear(); - create.mockClear(); - }); - it('should create metrics that do not exist', async () => { - get.mockRejectedValueOnce('stub value'); - create.mockReturnValueOnce('stub value'); - const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query'); - checkGetCalls(get.mock.calls); - checkCreateCalls(create.mock.calls); - expect(result).toBe('stub value'); - }); - - it('should handle previously created objects properly', async () => { - get.mockReturnValueOnce('stub value'); - create.mockRejectedValueOnce('stub value'); - const result = await getOrCreateMetricObject(savedObjectsClient, 'live_query'); - checkGetCalls(get.mock.calls); - checkCreateCalls(create.mock.calls, []); - expect(result).toBe('stub value'); - }); - }); - - describe('Incrementation', () => { - let counterMap: { [key: string]: CounterValue }; - const get = savedObjectsClient.get as jest.Mock; - const update = savedObjectsClient.update as jest.Mock; - update.mockImplementation( - async (objectType: string, route: RouteString, newVal: CounterValue) => { - counterMap[`${objectType}-${route}`] = newVal; - } - ); - get.mockImplementation(async (objectType: string, route: RouteString) => ({ - attributes: counterMap[`${objectType}-${route}`], - })); - beforeEach(() => { - counterMap = routeStrings.reduce((acc, route) => { - acc[`${usageMetricSavedObjectType}-${route}`] = { - count: 0, - errors: 0, - }; - - return acc; - }, {} as { [key: string]: CounterValue }); - get.mockClear(); - update.mockClear(); - }); - it('should increment the route counter', async () => { - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 0, - errors: 0, - }); - await incrementCount(savedObjectsClient, 'live_query'); - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 1, - errors: 0, - }); - }); - - it('should allow incrementing the error counter', async () => { - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 0, - errors: 0, - }); - await incrementCount(savedObjectsClient, 'live_query', 'errors'); - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 0, - errors: 1, - }); - }); - - it('should allow adjustment of the increment', async () => { - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 0, - errors: 0, - }); - await incrementCount(savedObjectsClient, 'live_query', 'count', 2); - expect(await getRouteMetric(savedObjectsClient, 'live_query')).toEqual({ - count: 2, - errors: 0, - }); - }); - }); -}); diff --git a/x-pack/plugins/osquery/server/routes/usage/recorder.ts b/x-pack/plugins/osquery/server/routes/usage/recorder.ts deleted file mode 100644 index 308166aefb08fb..00000000000000 --- a/x-pack/plugins/osquery/server/routes/usage/recorder.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { SavedObjectsClientContract } from '@kbn/core/server'; -import { usageMetricSavedObjectType } from '../../../common/types'; -import type { LiveQuerySessionUsage } from '../../usage/types'; - -export interface RouteUsageMetric { - queries: number; - errors: number; -} - -export type RouteString = 'live_query'; - -export const routeStrings: RouteString[] = ['live_query']; - -export async function getOrCreateMetricObject( - soClient: SavedObjectsClientContract, - route: string -) { - try { - return await soClient.get(usageMetricSavedObjectType, route); - } catch (e) { - return await soClient.create( - usageMetricSavedObjectType, - { - errors: 0, - count: 0, - }, - { - id: route, - } - ); - } -} - -export async function getCount(soClient: SavedObjectsClientContract, route: RouteString) { - return await getOrCreateMetricObject(soClient, route); -} - -export interface CounterValue { - count: number; - errors: number; -} - -export async function incrementCount( - soClient: SavedObjectsClientContract, - route: RouteString, - key: keyof CounterValue = 'count', - increment = 1 -) { - const metric = await getOrCreateMetricObject(soClient, route); - metric.attributes[key] += increment; - await soClient.update(usageMetricSavedObjectType, route, metric.attributes); -} - -export async function getRouteMetric(soClient: SavedObjectsClientContract, route: RouteString) { - return (await getCount(soClient, route)).attributes; -} diff --git a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts deleted file mode 100644 index 6b76d2875a958b..00000000000000 --- a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { SavedObjectsType } from '@kbn/core/server'; - -import { usageMetricSavedObjectType } from '../../../common/types'; - -export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = { - properties: { - count: { - type: 'long', - }, - errors: { - type: 'long', - }, - }, -}; - -export const usageMetricType: SavedObjectsType = { - name: usageMetricSavedObjectType, - hidden: false, - namespaceType: 'agnostic', - mappings: usageMetricSavedObjectMappings, -}; diff --git a/x-pack/plugins/osquery/server/types.ts b/x-pack/plugins/osquery/server/types.ts index 7a6353cccee407..b4893193e71120 100644 --- a/x-pack/plugins/osquery/server/types.ts +++ b/x-pack/plugins/osquery/server/types.ts @@ -13,7 +13,6 @@ import type { } from '@kbn/data-plugin/server'; import type { FleetStartContract } from '@kbn/fleet-plugin/server'; -import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { PluginSetupContract } from '@kbn/features-plugin/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { @@ -34,7 +33,6 @@ export interface OsqueryPluginSetup { export interface OsqueryPluginStart {} export interface SetupPlugins { - usageCollection?: UsageCollectionSetup; actions: ActionsPlugin['setup']; cases: CasesSetup; data: DataPluginSetup; diff --git a/x-pack/plugins/osquery/server/usage/collector.ts b/x-pack/plugins/osquery/server/usage/collector.ts deleted file mode 100644 index f9a68908a1c672..00000000000000 --- a/x-pack/plugins/osquery/server/usage/collector.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { CoreSetup } from '@kbn/core/server'; -import { SavedObjectsClient } from '@kbn/core/server'; -import type { CollectorFetchContext } from '@kbn/usage-collection-plugin/server'; -import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers'; -import type { CollectorDependencies, UsageData } from './types'; -import { usageSchema } from './types'; - -export type RegisterCollector = (deps: CollectorDependencies) => void; -export const getInternalSavedObjectsClient = async ( - getStartServices: CoreSetup['getStartServices'] -) => { - const [coreStart] = await getStartServices(); - - return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); -}; - -export const registerCollector: RegisterCollector = ({ core, osqueryContext, usageCollection }) => { - if (!usageCollection) { - return; - } - - const collector = usageCollection.makeUsageCollector({ - type: 'osquery', - schema: usageSchema, - isReady: () => true, - fetch: async ({ esClient }: CollectorFetchContext): Promise => { - const savedObjectsClient = await getInternalSavedObjectsClient(core.getStartServices); - - return { - beat_metrics: { - usage: await getBeatUsage(esClient), - }, - live_query_usage: await getLiveQueryUsage(savedObjectsClient, esClient), - ...(await getPolicyLevelUsage( - esClient, - savedObjectsClient, - osqueryContext.service.getPackagePolicyService() - )), - }; - }, - }); - - usageCollection.registerCollector(collector); -}; diff --git a/x-pack/plugins/osquery/server/usage/constants.ts b/x-pack/plugins/osquery/server/usage/constants.ts deleted file mode 100644 index f463380ee0b331..00000000000000 --- a/x-pack/plugins/osquery/server/usage/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * 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 const METRICS_INDICES = 'logs-elastic_agent.osquerybeat*'; diff --git a/x-pack/plugins/osquery/server/usage/fetchers.test.ts b/x-pack/plugins/osquery/server/usage/fetchers.test.ts deleted file mode 100644 index 13da639e2c72d3..00000000000000 --- a/x-pack/plugins/osquery/server/usage/fetchers.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { extractBeatUsageMetrics } from './fetchers'; - -describe('extractBeatUsageMetrics', () => { - it('should not blow when no values are supplied for the aggregations', () => { - expect(extractBeatUsageMetrics({})).toEqual({ - memory: { - rss: {}, - }, - cpu: {}, - }); - }); - - it('should not blow when some values are missing from the aggregations', () => { - expect( - extractBeatUsageMetrics({ - aggregations: { - lastDay: { - max_rss: { - value: 1, - }, - }, - }, - }) - ).toEqual({ - memory: { - rss: { - max: 1, - }, - }, - cpu: {}, - }); - }); - - it('should pick out all the max/avg/latest for memory/cpu', () => { - expect( - extractBeatUsageMetrics({ - aggregations: { - lastDay: { - max_rss: { - value: 1, - }, - avg_rss: { - value: 1, - }, - max_cpu: { - value: 2, - }, - avg_cpu: { - value: 2, - }, - latest: { - hits: { - total: 1, - hits: [ - { - _index: '', - _id: '', - _source: { - monitoring: { - metrics: { - beat: { - cpu: { - total: { - time: { - ms: 2, - }, - }, - }, - memstats: { - rss: 1, - }, - }, - }, - }, - }, - }, - ], - }, - }, - }, - }, - }) - ).toEqual({ - memory: { - rss: { - max: 1, - avg: 1, - latest: 1, - }, - }, - cpu: { - max: 2, - avg: 2, - latest: 2, - }, - }); - }); -}); diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts deleted file mode 100644 index 98a9f53c2c8c99..00000000000000 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 { - AggregationsSingleBucketAggregateBase, - AggregationsTopHitsAggregate, - AggregationsRateAggregate, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; -import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; -import type { ListResult, PackagePolicy } from '@kbn/fleet-plugin/common'; -import { - AGENTS_INDEX, - AGENT_ACTIONS_INDEX, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '@kbn/fleet-plugin/common'; -import { getRouteMetric } from '../routes/usage'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; -import { METRICS_INDICES } from './constants'; -import type { AgentInfo, BeatMetricsUsage, LiveQueryUsage } from './types'; - -interface PolicyLevelUsage { - scheduled_queries?: ScheduledQueryUsageMetrics; - agent_info?: AgentInfo; -} - -export async function getPolicyLevelUsage( - esClient: ElasticsearchClient, - soClient: SavedObjectsClientContract, - packagePolicyService?: PackagePolicyClient -): Promise { - if (!packagePolicyService) { - return {}; - } - - const packagePolicies = await packagePolicyService.list(soClient, { - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`, - perPage: 10_000, - }); - - const result: PolicyLevelUsage = { - scheduled_queries: getScheduledQueryUsage(packagePolicies), - // TODO: figure out how to support dynamic keys in metrics - // packageVersions: getPackageVersions(packagePolicies), - }; - const agentResponse = await esClient.search< - unknown, - { - policied: AggregationsSingleBucketAggregateBase; - } - >({ - body: { - size: 0, - query: { - match: { - active: true, - }, - }, - aggs: { - policied: { - filter: { - terms: { - policy_id: packagePolicies.items.map((p) => p.policy_id), - }, - }, - }, - }, - }, - index: AGENTS_INDEX, - ignore_unavailable: true, - }); - const policied = agentResponse.aggregations?.policied; - if (policied && typeof policied.doc_count === 'number') { - result.agent_info = { - enrolled: policied.doc_count, - }; - } - - return result; -} - -export function getPackageVersions(packagePolicies: ListResult) { - return packagePolicies.items.reduce((acc, item) => { - if (item.package) { - acc[item.package.version] = (acc[item.package.version] ?? 0) + 1; - } - - return acc; - }, {} as { [version: string]: number }); -} - -interface ScheduledQueryUsageMetrics { - queryGroups: { - total: number; - empty: number; - }; -} - -export function getScheduledQueryUsage(packagePolicies: ListResult) { - return packagePolicies.items.reduce( - (acc, item) => { - ++acc.queryGroups.total; - const policyAgents = item.inputs.reduce((sum, input) => sum + input.streams.length, 0); - if (policyAgents === 0) { - ++acc.queryGroups.empty; - } - - return acc; - }, - { - queryGroups: { - total: 0, - empty: 0, - }, - } as ScheduledQueryUsageMetrics - ); -} - -export async function getLiveQueryUsage( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient -) { - const metricResponse = await esClient.search< - unknown, - { - queries: AggregationsSingleBucketAggregateBase; - } - >({ - body: { - size: 0, - aggs: { - queries: { - filter: { - term: { - input_type: 'osquery', - }, - }, - }, - }, - }, - index: AGENT_ACTIONS_INDEX, - ignore_unavailable: true, - }); - const result: LiveQueryUsage = { - session: await getRouteMetric(soClient, 'live_query'), - }; - const esQueries = metricResponse.aggregations?.queries; - if (esQueries && typeof esQueries.doc_count === 'number') { - // getting error stats out of ES is difficult due to a lack of error info on .fleet-actions - // and a lack of indexable osquery specific info on .fleet-actions-results - result.cumulative = { - queries: esQueries.doc_count, - }; - } - - return result; -} - -interface BeatUsageAggs { - lastDay: { - max_rss?: AggregationsRateAggregate; - max_cpu?: AggregationsRateAggregate; - latest?: AggregationsTopHitsAggregate; - - // not used in code, declared to satisfy type - avg_rss?: AggregationsRateAggregate; - avg_cpu?: AggregationsRateAggregate; - }; -} - -export function extractBeatUsageMetrics( - metricResponse: Pick, 'aggregations'> -) { - const lastDayAggs = metricResponse.aggregations?.lastDay; - const result: BeatMetricsUsage = { - memory: { - rss: {}, - }, - cpu: {}, - }; - - if (lastDayAggs) { - if (lastDayAggs.max_rss !== undefined) { - result.memory.rss.max = lastDayAggs.max_rss.value; - } - - if (lastDayAggs.avg_rss !== undefined) { - // @ts-expect-error condition check another property, not idea why. consider fixing - result.memory.rss.avg = lastDayAggs.max_rss.value; - } - - if (lastDayAggs.max_cpu !== undefined) { - result.cpu.max = lastDayAggs.max_cpu.value; - } - - if (lastDayAggs.avg_cpu !== undefined) { - // @ts-expect-error condition check another property, not idea why. consider fixing - result.cpu.avg = lastDayAggs.max_cpu.value; - } - - if (lastDayAggs.latest !== undefined) { - const latest = lastDayAggs.latest.hits.hits[0]?._source?.monitoring.metrics.beat; - if (latest) { - result.cpu.latest = latest.cpu.total.time.ms; - result.memory.rss.latest = latest.memstats.rss; - } - } - } - - return result; -} - -export async function getBeatUsage(esClient: ElasticsearchClient) { - const metricResponse = await esClient.search({ - body: { - size: 0, - aggs: { - lastDay: { - filter: { - range: { - '@timestamp': { - gte: 'now-24h', - lte: 'now', - }, - }, - }, - aggs: { - latest: { - top_hits: { - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - size: 1, - }, - }, - max_rss: { - max: { - field: 'monitoring.metrics.beat.memstats.rss', - }, - }, - avg_rss: { - avg: { - field: 'monitoring.metrics.beat.memstats.rss', - }, - }, - max_cpu: { - max: { - field: 'monitoring.metrics.beat.cpu.total.time.ms', - }, - }, - avg_cpu: { - avg: { - field: 'monitoring.metrics.beat.cpu.total.time.ms', - }, - }, - }, - }, - }, - }, - index: METRICS_INDICES, - ignore_unavailable: true, - }); - - return extractBeatUsageMetrics(metricResponse); -} diff --git a/x-pack/plugins/osquery/server/usage/index.ts b/x-pack/plugins/osquery/server/usage/index.ts deleted file mode 100644 index fad6f9616bf41b..00000000000000 --- a/x-pack/plugins/osquery/server/usage/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 { CollectorDependencies } from './types'; -import { registerCollector } from './collector'; - -export type InitUsageCollectors = (deps: CollectorDependencies) => void; - -export const initUsageCollectors: InitUsageCollectors = (dependencies) => { - registerCollector(dependencies); -}; diff --git a/x-pack/plugins/osquery/server/usage/types.ts b/x-pack/plugins/osquery/server/usage/types.ts deleted file mode 100644 index 95533255040b68..00000000000000 --- a/x-pack/plugins/osquery/server/usage/types.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 { CoreSetup } from '@kbn/core/server'; -import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; -import type { OsqueryAppContext } from '../lib/osquery_app_context_services'; -import type { SetupPlugins } from '../types'; - -export type CollectorDependencies = { - osqueryContext: OsqueryAppContext; - core: CoreSetup; -} & Pick; - -export interface LiveQuerySessionUsage { - count: number; - errors: number; -} -export interface LiveQueryCumulativeUsage { - queries: number; -} - -export interface LiveQueryUsage { - session: LiveQuerySessionUsage; - cumulative?: LiveQueryCumulativeUsage; -} - -export interface ScheduledQueryUsage { - queryGroups: { - total: number; - empty: number; - }; -} -export interface AgentInfo { - enrolled: number; -} - -export interface MetricEntry { - max?: number; - latest?: number; - avg?: number; -} - -export interface BeatMetricsUsage { - cpu: MetricEntry; - memory: { - rss: MetricEntry; - }; -} - -export interface BeatMetrics { - usage: BeatMetricsUsage; -} - -export interface UsageData { - live_query_usage?: LiveQueryUsage; - scheduled_queries?: ScheduledQueryUsage; - agent_info?: AgentInfo; - beat_metrics?: BeatMetrics; -} - -export const usageSchema: MakeSchemaFrom = { - live_query_usage: { - session: { - count: { - type: 'long', - _meta: { - description: 'Number of osquery action requests', - }, - }, - errors: { - type: 'long', - _meta: { - description: 'Number of osquery action requests that resulted in errors', - }, - }, - }, - cumulative: { - queries: { - type: 'long', - _meta: { - description: 'Number of osquery actions stored in Elasticsearch', - }, - }, - }, - }, - scheduled_queries: { - queryGroups: { - total: { - type: 'long', - _meta: { - description: 'Number of osquery policies/query groups', - }, - }, - empty: { - type: 'long', - _meta: { - description: 'Number of empty osquery policies/query groups', - }, - }, - }, - }, - agent_info: { - enrolled: { - type: 'long', - _meta: { - description: 'Number of agents enrolled in a policy with an osquery integration', - }, - }, - }, - beat_metrics: { - usage: { - cpu: { - latest: { - type: 'long', - _meta: { - description: 'Latest cpu usage sample in ms', - }, - }, - max: { - type: 'long', - _meta: { - description: 'Max cpu usage sample over 24 hours in ms', - }, - }, - avg: { - type: 'long', - _meta: { - description: 'Mean cpu usage over 24 hours in ms', - }, - }, - }, - memory: { - rss: { - latest: { - type: 'long', - _meta: { - description: 'Latest resident set size sample', - }, - }, - max: { - type: 'long', - _meta: { - description: 'Max resident set size sample over 24 hours', - }, - }, - avg: { - type: 'long', - _meta: { - description: 'Mean resident set size sample over 24 hours', - }, - }, - }, - }, - }, - }, -}; diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index c71fbc2871f94d..534a424a02d068 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -57,7 +57,6 @@ "@kbn/dev-cli-runner", "@kbn/telemetry-plugin", "@kbn/actions-plugin", - "@kbn/usage-collection-plugin", "@kbn/features-plugin", "@kbn/task-manager-plugin", "@kbn/data-views-plugin", diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index a336d97ee896fc..55c0a867b547d3 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -6940,126 +6940,6 @@ } } }, - "osquery": { - "properties": { - "live_query_usage": { - "properties": { - "session": { - "properties": { - "count": { - "type": "long", - "_meta": { - "description": "Number of osquery action requests" - } - }, - "errors": { - "type": "long", - "_meta": { - "description": "Number of osquery action requests that resulted in errors" - } - } - } - }, - "cumulative": { - "properties": { - "queries": { - "type": "long", - "_meta": { - "description": "Number of osquery actions stored in Elasticsearch" - } - } - } - } - } - }, - "scheduled_queries": { - "properties": { - "queryGroups": { - "properties": { - "total": { - "type": "long", - "_meta": { - "description": "Number of osquery policies/query groups" - } - }, - "empty": { - "type": "long", - "_meta": { - "description": "Number of empty osquery policies/query groups" - } - } - } - } - } - }, - "agent_info": { - "properties": { - "enrolled": { - "type": "long", - "_meta": { - "description": "Number of agents enrolled in a policy with an osquery integration" - } - } - } - }, - "beat_metrics": { - "properties": { - "usage": { - "properties": { - "cpu": { - "properties": { - "latest": { - "type": "long", - "_meta": { - "description": "Latest cpu usage sample in ms" - } - }, - "max": { - "type": "long", - "_meta": { - "description": "Max cpu usage sample over 24 hours in ms" - } - }, - "avg": { - "type": "long", - "_meta": { - "description": "Mean cpu usage over 24 hours in ms" - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "latest": { - "type": "long", - "_meta": { - "description": "Latest resident set size sample" - } - }, - "max": { - "type": "long", - "_meta": { - "description": "Max resident set size sample over 24 hours" - } - }, - "avg": { - "type": "long", - "_meta": { - "description": "Mean resident set size sample over 24 hours" - } - } - } - } - } - } - } - } - } - } - } - }, "reporting": { "properties": { "csv_searchsource": { diff --git a/x-pack/test/functional/apps/aiops/change_point_detection.ts b/x-pack/test/functional/apps/aiops/change_point_detection.ts new file mode 100644 index 00000000000000..d93409c3109014 --- /dev/null +++ b/x-pack/test/functional/apps/aiops/change_point_detection.ts @@ -0,0 +1,95 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const elasticChart = getService('elasticChart'); + const esArchiver = getService('esArchiver'); + const aiops = getService('aiops'); + + // aiops lives in the ML UI so we need some related services. + const ml = getService('ml'); + + describe('change point detection', async function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.testResources.deleteIndexPatternByTitle('ft_ecommerce'); + }); + + it(`loads the change point detection page`, async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await elasticChart.setNewChartUiDebugFlag(true); + await aiops.changePointDetectionPage.navigateToIndexPatternSelection(); + await ml.jobSourceSelection.selectSourceForChangePointDetection('ft_ecommerce'); + await aiops.changePointDetectionPage.assertChangePointDetectionPageExists(); + }); + + it('detects a change point when no split field is selected', async () => { + await aiops.changePointDetectionPage.clickUseFullDataButton(); + await aiops.changePointDetectionPage.selectMetricField(0, 'products.discount_amount'); + const result = await aiops.changePointDetectionPage.getTable(0).parseTable(); + expect(result.length).to.eql(1); + expect(parseInt(result[0].pValue, 10)).to.eql(0); + expect(result[0].type).to.eql('distribution_change'); + + await elasticChart.waitForRenderComplete('aiopChangePointPreviewChart > xyVisChart'); + const chartState = await elasticChart.getChartDebugData( + 'aiopChangePointPreviewChart > xyVisChart', + 0, + 5000 + ); + if (!chartState) { + throw new Error('Preview chart debug state is not available'); + } + expect(chartState.annotations![0].data.details).to.eql('distribution_change'); + expect(chartState.annotations![0].domainType).to.eql('xDomain'); + expect(chartState.lines![0].points.length).to.be.above(30); + }); + + it('shows multiple results when split field is selected', async () => { + await aiops.changePointDetectionPage.clickUseFullDataButton(); + await aiops.changePointDetectionPage.selectMetricField(0, 'products.discount_amount'); + await aiops.changePointDetectionPage.selectSplitField(0, 'geoip.city_name'); + const result = await aiops.changePointDetectionPage.getTable(0).parseTable(); + expect(result.length).to.eql(5); + // assert asc sorting by p_value is applied + expect(parseFloat(result[0].pValue)).to.be.lessThan(parseFloat(result[4].pValue)); + }); + + it('allows change point selection for detailed view', async () => { + await aiops.changePointDetectionPage.getTable(0).selectAllRows(); + await aiops.changePointDetectionPage.viewSelected(); + await aiops.changePointDetectionPage.assertDetailedView(5); + await aiops.changePointDetectionPage.closeFlyout(); + // deselect + await aiops.changePointDetectionPage.getTable(0).selectAllRows(); + }); + + it('supports a quick filter actions', async () => { + await aiops.changePointDetectionPage + .getTable(0) + .invokeAction(0, 'aiopsChangePointFilterForValue'); + const resultFor = await aiops.changePointDetectionPage.getTable(0).parseTable(); + expect(resultFor.length).to.eql(1); + }); + + it('supports multiple configurations for change point detection', async () => { + await aiops.changePointDetectionPage.assertPanelExist(0); + await aiops.changePointDetectionPage.addChangePointConfig(); + await aiops.changePointDetectionPage.assertPanelExist(1); + }); + }); +} diff --git a/x-pack/test/functional/apps/aiops/index.ts b/x-pack/test/functional/apps/aiops/index.ts index 6bcda131220043..2fb0f84c990c3b 100644 --- a/x-pack/test/functional/apps/aiops/index.ts +++ b/x-pack/test/functional/apps/aiops/index.ts @@ -30,5 +30,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./explain_log_rate_spikes')); + loadTestFile(require.resolve('./change_point_detection')); }); } diff --git a/x-pack/test/functional/services/aiops/change_point_detection_page.ts b/x-pack/test/functional/services/aiops/change_point_detection_page.ts new file mode 100644 index 00000000000000..2eb539ef4fc784 --- /dev/null +++ b/x-pack/test/functional/services/aiops/change_point_detection_page.ts @@ -0,0 +1,145 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlTableService } from '../ml/common_table_service'; + +export function ChangePointDetectionPageProvider( + { getService, getPageObject }: FtrProviderContext, + tableService: MlTableService +) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + const browser = getService('browser'); + const elasticChart = getService('elasticChart'); + + return { + async navigateToIndexPatternSelection() { + await testSubjects.click('mlMainTab changePointDetection'); + await testSubjects.existOrFail('mlPageSourceSelection'); + }, + + async assertChangePointDetectionPageExists() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsChangePointDetectionPage'); + }); + }, + + async assertQueryInput(expectedQueryString: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + const actualQueryString = await aiopsQueryInput.getVisibleText(); + expect(actualQueryString).to.eql( + expectedQueryString, + `Expected query bar text to be '${expectedQueryString}' (got '${actualQueryString}')` + ); + }, + + async assertPanelLoaded() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.waitForHidden('aiopsChangePointResultsTable loading'); + }); + }, + + async assertMetricFieldSelection(panelIndex: number = 0, expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + `aiopsChangePointPanel_${panelIndex} > aiopsChangePointMetricField > comboBoxInput` + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected a metric field to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectMetricField(panelIndex: number = 0, value: string) { + await comboBox.set( + `aiopsChangePointPanel_${panelIndex} > aiopsChangePointMetricField > comboBoxInput`, + value + ); + await this.assertMetricFieldSelection(panelIndex, [value]); + }, + + async assertSplitFieldSelection(panelIndex: number = 0, expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + `aiopsChangePointPanel_${panelIndex} > aiopsChangePointSplitField > comboBoxInput` + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected a split field to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectSplitField(panelIndex: number = 0, value: string) { + await comboBox.set( + `aiopsChangePointPanel_${panelIndex} > aiopsChangePointSplitField > comboBoxInput`, + value + ); + await this.assertSplitFieldSelection(panelIndex, [value]); + }, + + async clickUseFullDataButton() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabledWithoutRetry('mlDatePickerButtonUseFullData'); + await testSubjects.clickWhenNotDisabledWithoutRetry('superDatePickerApplyTimeButton'); + await testSubjects.existOrFail('aiopsChangePointResultsTable loaded'); + }); + }, + + async viewSelected() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabledWithoutRetry( + 'aiopsChangePointDetectionViewSelected' + ); + await testSubjects.existOrFail('aiopsChangePointDetectionSelectedCharts'); + }); + }, + + async assertDetailedView(expectedChartCount: number) { + const testSubj = 'aiopsChangePointDetectionSelectedCharts > xyVisChart'; + await elasticChart.waitForRenderComplete(testSubj); + const changePointCharts = await testSubjects.findAll(testSubj); + expect(changePointCharts.length).to.eql( + expectedChartCount, + `Expected ${expectedChartCount} charts in the flyout (got '${changePointCharts.length}')` + ); + }, + + async closeFlyout() { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.missingOrFail('aiopsChangePointDetectionSelectedCharts'); + }, + + async addChangePointConfig() { + await testSubjects.click('aiopsChangePointAddConfig'); + }, + + async assertPanelExist(index: number) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail(`aiopsChangePointPanel_${index}`); + }); + }, + + getTable(index: number) { + return tableService.getServiceInstance( + 'ChangePointResultsTable', + `aiopsChangePointResultsTable`, + 'aiopsChangePointResultsTableRow', + [ + { id: 'timestamp', testSubj: 'aiopsChangePointTimestamp' }, + { id: 'preview', testSubj: 'aiopsChangePointPreview' }, + { id: 'type', testSubj: 'aiopsChangePointType' }, + { id: 'pValue', testSubj: 'aiopsChangePointPValue' }, + { id: 'groupName', testSubj: 'aiopsChangePointGroupName' }, + { id: 'groupValue', testSubj: 'aiopsChangePointGroupValue' }, + ], + '', + `aiopsChangePointPanel_${index}` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/aiops/index.ts b/x-pack/test/functional/services/aiops/index.ts index 8c208f182f3bd1..1edd17a7ba7ddc 100644 --- a/x-pack/test/functional/services/aiops/index.ts +++ b/x-pack/test/functional/services/aiops/index.ts @@ -12,6 +12,8 @@ import { ExplainLogRateSpikesAnalysisTableProvider } from './explain_log_rate_sp import { ExplainLogRateSpikesAnalysisGroupsTableProvider } from './explain_log_rate_spikes_analysis_groups_table'; import { ExplainLogRateSpikesDataGeneratorProvider } from './explain_log_rate_spikes_data_generator'; import { LogPatternAnalysisPageProvider } from './log_pattern_analysis_page'; +import { ChangePointDetectionPageProvider } from './change_point_detection_page'; +import { MlTableServiceProvider } from '../ml/common_table_service'; export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesPage = ExplainLogRateSpikesPageProvider(context); @@ -21,7 +23,12 @@ export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesDataGenerator = ExplainLogRateSpikesDataGeneratorProvider(context); const logPatternAnalysisPageProvider = LogPatternAnalysisPageProvider(context); + const tableService = MlTableServiceProvider(context); + + const changePointDetectionPage = ChangePointDetectionPageProvider(context, tableService); + return { + changePointDetectionPage, explainLogRateSpikesPage, explainLogRateSpikesAnalysisTable, explainLogRateSpikesAnalysisGroupsTable, diff --git a/x-pack/test/functional/services/ml/common_table_service.ts b/x-pack/test/functional/services/ml/common_table_service.ts index 063c6095c85b5c..653df177090599 100644 --- a/x-pack/test/functional/services/ml/common_table_service.ts +++ b/x-pack/test/functional/services/ml/common_table_service.ts @@ -21,7 +21,8 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide public readonly tableTestSubj: string, public readonly tableRowSubj: string, public readonly columns: Array<{ id: string; testSubj: string }>, - public readonly searchInputSubj: string + public readonly searchInputSubj: string, + public readonly parentSubj?: string ) {} public async assertTableLoaded() { @@ -33,7 +34,9 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide } public async parseTable() { - const table = await testSubjects.find(`~${this.tableTestSubj}`); + const table = await testSubjects.find( + `${this.parentSubj ? `${this.parentSubj} > ` : ''}~${this.tableTestSubj}` + ); const $ = await table.parseDomContent(); const rows = []; @@ -138,6 +141,28 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide await this.assertTableSorting(columnName, columnIndex, direction); }); } + + public async invokeAction(rowIndex: number, actionSubject: string) { + const rows = await testSubjects.findAll( + `${this.parentSubj ? `${this.parentSubj} > ` : ''}~${this.tableTestSubj} > ~${ + this.tableRowSubj + }` + ); + + const requestedRow = rows[rowIndex]; + const actionButton = await requestedRow.findByTestSubject(actionSubject); + + await retry.tryForTime(5000, async () => { + await actionButton.click(); + await this.waitForTableToLoad(); + }); + } + + public async selectAllRows() { + await testSubjects.click( + `${this.parentSubj ? `${this.parentSubj} > ` : ''} > checkboxSelectAll` + ); + } }; return { @@ -146,10 +171,11 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide tableTestSubj: string, tableRowSubj: string, columns: Array<{ id: string; testSubj: string }>, - searchInputSubj: string + searchInputSubj: string, + parentSubj?: string ) { Object.defineProperty(TableService, 'name', { value: name }); - return new TableService(tableTestSubj, tableRowSubj, columns, searchInputSubj); + return new TableService(tableTestSubj, tableRowSubj, columns, searchInputSubj, parentSubj); }, }; } diff --git a/x-pack/test/functional/services/ml/job_source_selection.ts b/x-pack/test/functional/services/ml/job_source_selection.ts index 95a2bf0dbab797..e671b294c580a8 100644 --- a/x-pack/test/functional/services/ml/job_source_selection.ts +++ b/x-pack/test/functional/services/ml/job_source_selection.ts @@ -46,5 +46,9 @@ export function MachineLearningJobSourceSelectionProvider({ getService }: FtrPro async selectSourceForExplainLogRateSpikes(sourceName: string) { await this.selectSource(sourceName, 'aiopsExplainLogRateSpikesPage'); }, + + async selectSourceForChangePointDetection(sourceName: string) { + await this.selectSource(sourceName, 'aiopsChangePointDetectionPage'); + }, }; }