mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Incident Management] add recommended dashboards api (#213287)
## Summary Adds a basic api for finding suggested dashboards by alert. This internal API is currently not in use anywhere. This api finds suggested dashboards in two ways: 1. Finding dashboards with lens visualizations that query against the same data view 2. Finding dashboards with lens visualizations that utilize fields in the rule configuration, or alert data. These are two naive approaches to finding suggested dashboards. These heuristics will be improved over time and incorporate more sophisticated approaches that have been explored by numerous engineers across Observability. ## Testing A basic api integration test is included that covers matching dashboards by index and field, across spaces. As we develop this feature further over time more fine grain tests will be added ### Manual Testing 1. Run ``` node x-pack/scripts/data_forge.js --events-per-cycle 200 --lookback now-1h --ephemeral-project-ids 10 --dataset fake_stack --install-kibana-assets --kibana-url http://localhost:5601 --event-template bad ``` 2. Save the file as a .ndjson file and import it via saved objects import https://p.elstc.co/paste/3BWKIHLU#f0WfGYx7G9DhWy88yDXhaEXTX16Fn+ovDcomNIx6E3a 3. Navigate to the alert details page, you should start to see a lot of alerts 4. Click on one, and copy the alert id 5. Navigate to console and paste `GET kbn:/internal/observability/alerts/suggested_dashboards?alertId=[YOUR_ALERT_ID]` to see the recommended dashboards --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl <nickpeihl@gmail.com>
This commit is contained in:
parent
da7e44988d
commit
71b7458767
38 changed files with 1955 additions and 8 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -933,6 +933,7 @@ x-pack/solutions/observability/packages/get-padded-alert-time-range-util @elasti
|
|||
x-pack/solutions/observability/packages/kbn-alerts-grouping @elastic/response-ops
|
||||
x-pack/solutions/observability/packages/kbn-custom-integrations @elastic/obs-ux-logs-team
|
||||
x-pack/solutions/observability/packages/kbn-investigation-shared @elastic/obs-ux-management-team
|
||||
x-pack/solutions/observability/packages/kbn-observability-schema @elastic/obs-ux-management-team
|
||||
x-pack/solutions/observability/packages/kbn-scout-oblt @elastic/appex-qa
|
||||
x-pack/solutions/observability/packages/observability-ai/observability-ai-common @elastic/obs-ai-assistant
|
||||
x-pack/solutions/observability/packages/observability-ai/observability-ai-server @elastic/obs-ai-assistant
|
||||
|
@ -1390,6 +1391,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
|
|||
/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/ @elastic/obs-ux-management-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitors @elastic/obs-ux-management-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_private_location @elastic/obs-ux-management-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/ @elastic/obs-ux-management-team
|
||||
/x-pack/test/functional/page_objects/alert_controls.ts @elastic/obs-ux-management-team
|
||||
|
||||
# Elastic Stack Monitoring
|
||||
|
|
|
@ -715,6 +715,7 @@
|
|||
"@kbn/observability-logs-explorer-plugin": "link:x-pack/solutions/observability/plugins/observability_logs_explorer",
|
||||
"@kbn/observability-onboarding-plugin": "link:x-pack/solutions/observability/plugins/observability_onboarding",
|
||||
"@kbn/observability-plugin": "link:x-pack/solutions/observability/plugins/observability",
|
||||
"@kbn/observability-schema": "link:x-pack/solutions/observability/packages/kbn-observability-schema",
|
||||
"@kbn/observability-shared-plugin": "link:x-pack/solutions/observability/plugins/observability_shared",
|
||||
"@kbn/observability-utils-browser": "link:x-pack/solutions/observability/packages/utils-browser",
|
||||
"@kbn/observability-utils-common": "link:x-pack/solutions/observability/packages/utils-common",
|
||||
|
|
|
@ -134,7 +134,7 @@ const namespaces = {
|
|||
ALERT_RULE_NAMESPACE,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
export const fields = {
|
||||
ALERT_ACTION_GROUP,
|
||||
ALERT_CASE_IDS,
|
||||
ALERT_DURATION,
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { customThresholdParamsSchema } from './latest';
|
||||
export { customThresholdParamsSchema, type CustomThresholdParams } from './latest';
|
||||
export { customThresholdParamsSchema as customThresholdParamsSchemaV1 } from './v1';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { COMPARATORS } from '@kbn/alerting-comparators';
|
||||
import { dataViewSpecSchema } from '../common';
|
||||
|
@ -85,3 +85,5 @@ export const customThresholdParamsSchema = schema.object(
|
|||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export type CustomThresholdParams = TypeOf<typeof customThresholdParamsSchema>;
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { Version } from '@kbn/object-versioning';
|
|||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import type { FavoritesSetup } from '@kbn/content-management-favorites-server';
|
||||
import type { CoreApi, StorageContextGetTransformFn } from './core';
|
||||
export type { IContentClient } from './content_client/types';
|
||||
|
||||
export interface ContentManagementServerSetupDependencies {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
|
|
|
@ -26,7 +26,7 @@ export async function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export type { DashboardPluginSetup, DashboardPluginStart } from './types';
|
||||
export type { DashboardAttributes } from './content_management';
|
||||
export type { DashboardAttributes, DashboardPanel } from './content_management';
|
||||
export type { DashboardSavedObjectAttributes } from './dashboard_saved_object';
|
||||
|
||||
export { PUBLIC_API_PATH } from './api/constants';
|
||||
|
|
|
@ -1358,6 +1358,8 @@
|
|||
"@kbn/observability-onboarding-plugin/*": ["x-pack/solutions/observability/plugins/observability_onboarding/*"],
|
||||
"@kbn/observability-plugin": ["x-pack/solutions/observability/plugins/observability"],
|
||||
"@kbn/observability-plugin/*": ["x-pack/solutions/observability/plugins/observability/*"],
|
||||
"@kbn/observability-schema": ["x-pack/solutions/observability/packages/kbn-observability-schema"],
|
||||
"@kbn/observability-schema/*": ["x-pack/solutions/observability/packages/kbn-observability-schema/*"],
|
||||
"@kbn/observability-shared-plugin": ["x-pack/solutions/observability/plugins/observability_shared"],
|
||||
"@kbn/observability-shared-plugin/*": ["x-pack/solutions/observability/plugins/observability_shared/*"],
|
||||
"@kbn/observability-synthetics-test-data": ["x-pack/solutions/observability/packages/synthetics-test-data"],
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-schema
|
||||
|
||||
Provides schema for generic Observability apis
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 './related_dashboards/latest';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/solutions/observability/packages/kbn-observability-schema'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/observability-schema",
|
||||
"owner": "@elastic/obs-ux-management-team",
|
||||
"group": "observability",
|
||||
"visibility": "private",
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@kbn/observability-schema",
|
||||
"descriptio": "Utils to generate observability synthetics test data",
|
||||
"author": "UX Management",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { type RelevantPanel, relevantPanelSchema } from './schema/relevant_panel/v1';
|
||||
export { type RelatedDashboard, relatedDashboardSchema } from './schema/related_dashboard/v1';
|
||||
export {
|
||||
type GetRelatedDashboardsResponse,
|
||||
getRelatedDashboardsResponseSchema,
|
||||
getRelatedDashboardsParamsSchema,
|
||||
} from './rest_specs/get_related_dashboards/v1';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 {
|
||||
type GetRelatedDashboardsResponse,
|
||||
getRelatedDashboardsResponseSchema,
|
||||
getRelatedDashboardsParamsSchema,
|
||||
} from './v1';
|
|
@ -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 { z } from '@kbn/zod';
|
||||
import { relatedDashboardSchema } from '../../schema/related_dashboard/v1';
|
||||
|
||||
export const getRelatedDashboardsParamsSchema = z.object({
|
||||
query: z.object({
|
||||
alertId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const getRelatedDashboardsResponseSchema = z.object({
|
||||
suggestedDashboards: z.array(relatedDashboardSchema),
|
||||
linkedDashboards: z.array(relatedDashboardSchema),
|
||||
});
|
||||
|
||||
export type GetRelatedDashboardsResponse = z.output<typeof getRelatedDashboardsResponseSchema>;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { type RelatedDashboard, relatedDashboardSchema } from './v1';
|
|
@ -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 { z } from '@kbn/zod';
|
||||
import { relevantPanelSchema } from '../relevant_panel/latest';
|
||||
|
||||
export const relatedDashboardSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
matchedBy: z.object({
|
||||
fields: z.array(z.string()).optional(),
|
||||
index: z.array(z.string()).optional(),
|
||||
}),
|
||||
relevantPanelCount: z.number(),
|
||||
relevantPanels: z.array(relevantPanelSchema),
|
||||
});
|
||||
|
||||
export type RelatedDashboard = z.output<typeof relatedDashboardSchema>;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { type RelevantPanel, relevantPanelSchema } from './v1';
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { z } from '@kbn/zod';
|
||||
|
||||
export const relevantPanelSchema = z.object({
|
||||
panel: z.object({
|
||||
panelIndex: z.string(),
|
||||
type: z.string(),
|
||||
panelConfig: z.record(z.string(), z.any()),
|
||||
title: z.string().optional(),
|
||||
}),
|
||||
matchedBy: z.object({
|
||||
fields: z.array(z.string()).optional(),
|
||||
index: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type RelevantPanel = z.output<typeof relevantPanelSchema>;
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/zod"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 class AlertNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AlertNotFoundError';
|
||||
}
|
||||
}
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server';
|
||||
import type { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server';
|
||||
import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
|
||||
import type { DashboardPluginStart } from '@kbn/dashboard-plugin/server';
|
||||
import {
|
||||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
|
@ -23,7 +25,10 @@ import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discov
|
|||
import { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
RuleRegistryPluginSetupContract,
|
||||
RuleRegistryPluginStartContract,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/server';
|
||||
import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
|
@ -64,12 +69,15 @@ interface PluginSetup {
|
|||
spaces?: SpacesPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
cloud?: CloudSetup;
|
||||
contentManagement: ContentManagementServerSetup;
|
||||
}
|
||||
|
||||
interface PluginStart {
|
||||
alerting: AlertingServerStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
dataViews: DataViewsServerPluginStart;
|
||||
ruleRegistry: RuleRegistryPluginStartContract;
|
||||
dashboard: DashboardPluginStart;
|
||||
}
|
||||
|
||||
const alertingFeatures = OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES.map(
|
||||
|
@ -192,6 +200,8 @@ export class ObservabilityPlugin
|
|||
...plugins,
|
||||
core,
|
||||
},
|
||||
dashboard: pluginStart.dashboard,
|
||||
ruleRegistry: pluginStart.ruleRegistry,
|
||||
dataViews: pluginStart.dataViews,
|
||||
spaces: pluginStart.spaces,
|
||||
ruleDataService,
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 {
|
||||
getRelatedDashboardsParamsSchema,
|
||||
GetRelatedDashboardsResponse,
|
||||
} from '@kbn/observability-schema';
|
||||
import { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import type { SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import type { DashboardAttributes } from '@kbn/dashboard-plugin/server';
|
||||
import { createObservabilityServerRoute } from '../create_observability_server_route';
|
||||
import { RelatedDashboardsClient } from '../../services/related_dashboards_client';
|
||||
import { InvestigateAlertsClient } from '../../services/investigate_alerts_client';
|
||||
import { AlertNotFoundError } from '../../common/errors/alert_not_found_error';
|
||||
|
||||
const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({
|
||||
endpoint: 'GET /internal/observability/alerts/related_dashboards',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason:
|
||||
'This route is opted out from authorization because it is a wrapper around Saved Object client',
|
||||
},
|
||||
},
|
||||
options: { access: 'internal' },
|
||||
params: getRelatedDashboardsParamsSchema,
|
||||
handler: async (services): Promise<GetRelatedDashboardsResponse | IKibanaResponse> => {
|
||||
const { dependencies, params, request, response, context, logger } = services;
|
||||
const { alertId } = params.query;
|
||||
const { ruleRegistry, dashboard } = dependencies;
|
||||
const { contentClient } = dashboard;
|
||||
const dashboardClient = contentClient!.getForRequest<
|
||||
SavedObjectsFindResult<DashboardAttributes>
|
||||
>({
|
||||
requestHandlerContext: context,
|
||||
request,
|
||||
version: 3,
|
||||
});
|
||||
|
||||
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
|
||||
const investigateAlertsClient = new InvestigateAlertsClient(alertsClient);
|
||||
|
||||
const dashboardParser = new RelatedDashboardsClient(
|
||||
logger,
|
||||
dashboardClient,
|
||||
investigateAlertsClient,
|
||||
alertId
|
||||
);
|
||||
try {
|
||||
const { suggestedDashboards } = await dashboardParser.fetchSuggestedDashboards();
|
||||
return {
|
||||
suggestedDashboards,
|
||||
linkedDashboards: [],
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof AlertNotFoundError) {
|
||||
return response.badRequest({ body: { message: e.message } });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const alertsSuggestedDashboardRepository = alertsDynamicDashboardSuggestions;
|
|
@ -9,11 +9,13 @@ import { EndpointOf } from '@kbn/server-route-repository';
|
|||
import { ObservabilityConfig } from '..';
|
||||
import { aiAssistantRouteRepository } from './assistant/route';
|
||||
import { rulesRouteRepository } from './rules/route';
|
||||
import { alertsSuggestedDashboardRepository } from './alerts/route';
|
||||
|
||||
export function getObservabilityServerRouteRepository(config: ObservabilityConfig) {
|
||||
const repository = {
|
||||
...aiAssistantRouteRepository,
|
||||
...rulesRouteRepository,
|
||||
...alertsSuggestedDashboardRepository,
|
||||
};
|
||||
return repository;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { CoreSetup, KibanaRequest, Logger } from '@kbn/core/server';
|
||||
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server';
|
||||
import type { DashboardPluginStart } from '@kbn/dashboard-plugin/server';
|
||||
import {
|
||||
RuleDataPluginService,
|
||||
RuleRegistryPluginStartContract,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
import { registerRoutes as registerServerRoutes } from '@kbn/server-route-repository';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||
import { AlertDetailsContextualInsightsService } from '../services';
|
||||
|
@ -28,6 +32,8 @@ export interface RegisterRoutesDependencies {
|
|||
dataViews: DataViewsServerPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
ruleDataService: RuleDataPluginService;
|
||||
ruleRegistry: RuleRegistryPluginStartContract;
|
||||
dashboard: DashboardPluginStart;
|
||||
assistant: {
|
||||
alertDetailsContextualInsightsService: AlertDetailsContextualInsightsService;
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import type { EndpointOf, ReturnOf, ServerRouteRepository } from '@kbn/server-route-repository';
|
||||
import { KibanaRequest, Logger } from '@kbn/core/server';
|
||||
import { KibanaRequest, KibanaResponseFactory, Logger } from '@kbn/core/server';
|
||||
|
||||
import {
|
||||
ObservabilityServerRouteRepository,
|
||||
|
@ -21,6 +21,7 @@ export interface ObservabilityRouteHandlerResources {
|
|||
dependencies: RegisterRoutesDependencies;
|
||||
logger: Logger;
|
||||
request: KibanaRequest;
|
||||
response: KibanaResponseFactory;
|
||||
}
|
||||
|
||||
export interface ObservabilityRouteCreateOptions {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { omit } from 'lodash';
|
||||
import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold';
|
||||
import {
|
||||
ALERT_RULE_PARAMETERS,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
fields as TECHNICAL_ALERT_FIELDS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
export class AlertData {
|
||||
constructor(private alert: any) {}
|
||||
|
||||
getRuleParameters() {
|
||||
return this.alert[ALERT_RULE_PARAMETERS];
|
||||
}
|
||||
|
||||
getRelevantRuleFields(): Set<string> {
|
||||
const ruleParameters = this.getRuleParameters();
|
||||
const relevantFields = new Set<string>();
|
||||
if (!ruleParameters) {
|
||||
throw new Error('No rule parameters found');
|
||||
}
|
||||
switch (this.getRuleTypeId()) {
|
||||
case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID:
|
||||
const customThresholdParams = ruleParameters as CustomThresholdParams;
|
||||
const metrics = customThresholdParams.criteria[0].metrics;
|
||||
metrics.forEach((metric) => {
|
||||
relevantFields.add(metric.field);
|
||||
});
|
||||
return relevantFields;
|
||||
default:
|
||||
return relevantFields;
|
||||
}
|
||||
}
|
||||
|
||||
getRelevantAADFields(): string[] {
|
||||
const ignoredFields = ['_index'];
|
||||
const allKibanaFields = Object.keys(this.alert).filter((field) => field.startsWith('kibana.'));
|
||||
const nonTechnicalFields = omit(this.alert, [
|
||||
...Object.values(TECHNICAL_ALERT_FIELDS),
|
||||
...allKibanaFields,
|
||||
...ignoredFields,
|
||||
]);
|
||||
return Object.keys(nonTechnicalFields);
|
||||
}
|
||||
|
||||
getAlertTags(): string[] {
|
||||
return this.alert.tags || [];
|
||||
}
|
||||
|
||||
getRuleQueryIndex() {
|
||||
const ruleParameters = this.getRuleParameters();
|
||||
const ruleTypeId = this.getRuleTypeId();
|
||||
if (!ruleParameters) {
|
||||
throw new Error('No rule parameters found');
|
||||
}
|
||||
switch (ruleTypeId) {
|
||||
case OBSERVABILITY_THRESHOLD_RULE_TYPE_ID:
|
||||
const customThresholdParams = ruleParameters as CustomThresholdParams;
|
||||
return customThresholdParams.searchConfiguration.index;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getRuleTypeId() {
|
||||
return this.alert[ALERT_RULE_TYPE_ID];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { OBSERVABILITY_RULE_TYPE_IDS } from '@kbn/rule-data-utils';
|
||||
import { AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
import { AlertNotFoundError } from '../common/errors/alert_not_found_error';
|
||||
import { AlertData } from './alert_data';
|
||||
|
||||
export class InvestigateAlertsClient {
|
||||
constructor(private alertsClient: AlertsClient) {}
|
||||
|
||||
async getAlertById(alertId: string): Promise<AlertData> {
|
||||
const indices = (await this.getAlertsIndices()) || [];
|
||||
if (!indices.length) {
|
||||
throw new Error('No alert indices exist');
|
||||
}
|
||||
try {
|
||||
const alert = await this.alertsClient.get({
|
||||
id: alertId,
|
||||
index: indices.join(','),
|
||||
});
|
||||
return new AlertData(alert);
|
||||
} catch (e) {
|
||||
if (e.output.payload.statusCode === 404) {
|
||||
throw new AlertNotFoundError(`Alert with id ${alertId} not found`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async getAlertsIndices() {
|
||||
return await this.alertsClient.getAuthorizedAlertsIndices(OBSERVABILITY_RULE_TYPE_IDS);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import type { SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import { IContentClient } from '@kbn/content-management-plugin/server/types';
|
||||
import type {
|
||||
FieldBasedIndexPatternColumn,
|
||||
GenericIndexPatternColumn,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
|
||||
import type { RelevantPanel, RelatedDashboard } from '@kbn/observability-schema';
|
||||
import type { DashboardAttributes, DashboardPanel } from '@kbn/dashboard-plugin/server';
|
||||
import type { InvestigateAlertsClient } from './investigate_alerts_client';
|
||||
import type { AlertData } from './alert_data';
|
||||
|
||||
type Dashboard = SavedObjectsFindResult<DashboardAttributes>;
|
||||
export class RelatedDashboardsClient {
|
||||
private dashboardsById = new Map<string, Dashboard>();
|
||||
private alert: AlertData | null = null;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
private dashboardClient: IContentClient<Dashboard>,
|
||||
private alertsClient: InvestigateAlertsClient,
|
||||
private alertId: string
|
||||
) {}
|
||||
|
||||
async fetchSuggestedDashboards(): Promise<{ suggestedDashboards: RelatedDashboard[] }> {
|
||||
const allRelatedDashboards = new Set<RelatedDashboard>();
|
||||
const relevantDashboardsById = new Map<string, RelatedDashboard>();
|
||||
const [alert] = await Promise.all([
|
||||
this.alertsClient.getAlertById(this.alertId),
|
||||
this.fetchAllDashboards(),
|
||||
]);
|
||||
this.alert = alert;
|
||||
if (!this.alert) {
|
||||
return { suggestedDashboards: [] };
|
||||
}
|
||||
const index = await this.getRuleQueryIndex();
|
||||
const relevantRuleFields = this.alert.getRelevantRuleFields();
|
||||
const relevantAlertFields = this.alert.getRelevantAADFields();
|
||||
const allRelevantFields = new Set([...relevantRuleFields, ...relevantAlertFields]);
|
||||
|
||||
if (index) {
|
||||
const { dashboards } = this.getDashboardsByIndex(index);
|
||||
dashboards.forEach((dashboard) => allRelatedDashboards.add(dashboard));
|
||||
}
|
||||
if (allRelevantFields.size > 0) {
|
||||
const { dashboards } = this.getDashboardsByField(Array.from(allRelevantFields));
|
||||
dashboards.forEach((dashboard) => allRelatedDashboards.add(dashboard));
|
||||
}
|
||||
allRelatedDashboards.forEach((dashboard) => {
|
||||
const dedupedPanels = this.dedupePanels([
|
||||
...(relevantDashboardsById.get(dashboard.id)?.relevantPanels || []),
|
||||
...dashboard.relevantPanels,
|
||||
]);
|
||||
const relevantPanelCount = dedupedPanels.length;
|
||||
relevantDashboardsById.set(dashboard.id, {
|
||||
...dashboard,
|
||||
matchedBy: {
|
||||
...relevantDashboardsById.get(dashboard.id)?.matchedBy,
|
||||
...dashboard.matchedBy,
|
||||
},
|
||||
relevantPanelCount,
|
||||
relevantPanels: dedupedPanels,
|
||||
});
|
||||
});
|
||||
return { suggestedDashboards: Array.from(relevantDashboardsById.values()) };
|
||||
}
|
||||
|
||||
async fetchDashboards(page: number) {
|
||||
const perPage = 1000;
|
||||
const dashboards = await this.dashboardClient.search(
|
||||
{ limit: perPage, cursor: `${page}` },
|
||||
{ spaces: ['*'] }
|
||||
);
|
||||
const {
|
||||
result: { hits },
|
||||
} = dashboards;
|
||||
hits.forEach((dashboard: Dashboard) => {
|
||||
this.dashboardsById.set(dashboard.id, dashboard);
|
||||
});
|
||||
const fetchedUntil = (page - 1) * perPage + dashboards.result.hits.length;
|
||||
|
||||
if (dashboards.result.pagination.total <= fetchedUntil) {
|
||||
return;
|
||||
}
|
||||
await this.fetchDashboards(page + 1);
|
||||
}
|
||||
|
||||
async fetchAllDashboards() {
|
||||
await this.fetchDashboards(1);
|
||||
}
|
||||
|
||||
getDashboardsByIndex(index: string): {
|
||||
dashboards: RelatedDashboard[];
|
||||
} {
|
||||
const relevantDashboards: RelatedDashboard[] = [];
|
||||
this.dashboardsById.forEach((d) => {
|
||||
const panels = d.attributes.panels;
|
||||
const matchingPanels = this.getPanelsByIndex(index, panels);
|
||||
if (matchingPanels.length > 0) {
|
||||
this.logger.info(
|
||||
() => `Found ${matchingPanels.length} panel(s) in dashboard ${d.id} using index ${index}`
|
||||
);
|
||||
relevantDashboards.push({
|
||||
id: d.id,
|
||||
title: d.attributes.title,
|
||||
matchedBy: { index: [index] },
|
||||
relevantPanelCount: matchingPanels.length,
|
||||
relevantPanels: matchingPanels.map((p) => ({
|
||||
panel: {
|
||||
panelIndex: p.panelIndex || uuidv4(),
|
||||
type: p.type,
|
||||
panelConfig: p.panelConfig,
|
||||
title: p.title,
|
||||
},
|
||||
matchedBy: { index: [index] },
|
||||
})),
|
||||
});
|
||||
}
|
||||
});
|
||||
return { dashboards: relevantDashboards };
|
||||
}
|
||||
|
||||
dedupePanels(panels: RelevantPanel[]): RelevantPanel[] {
|
||||
const uniquePanels = new Map<string, RelevantPanel>();
|
||||
panels.forEach((p) => {
|
||||
uniquePanels.set(p.panel.panelIndex, {
|
||||
...p,
|
||||
matchedBy: { ...uniquePanels.get(p.panel.panelIndex)?.matchedBy, ...p.matchedBy },
|
||||
});
|
||||
});
|
||||
return Array.from(uniquePanels.values());
|
||||
}
|
||||
|
||||
getDashboardsByField(fields: string[]): {
|
||||
dashboards: RelatedDashboard[];
|
||||
} {
|
||||
const relevantDashboards: RelatedDashboard[] = [];
|
||||
this.dashboardsById.forEach((d) => {
|
||||
const panels = d.attributes.panels;
|
||||
const matchingPanels = this.getPanelsByField(fields, panels);
|
||||
const allMatchingFields = new Set(
|
||||
matchingPanels.map((p) => Array.from(p.matchingFields)).flat()
|
||||
);
|
||||
if (matchingPanels.length > 0) {
|
||||
this.logger.info(
|
||||
() =>
|
||||
`Found ${matchingPanels.length} panel(s) in dashboard ${
|
||||
d.id
|
||||
} using field(s) ${Array.from(allMatchingFields).toString()}`
|
||||
);
|
||||
relevantDashboards.push({
|
||||
id: d.id,
|
||||
title: d.attributes.title,
|
||||
matchedBy: { fields: Array.from(allMatchingFields) },
|
||||
relevantPanelCount: matchingPanels.length,
|
||||
relevantPanels: matchingPanels.map((p) => ({
|
||||
panel: {
|
||||
panelIndex: p.panel.panelIndex || uuidv4(),
|
||||
type: p.panel.type,
|
||||
panelConfig: p.panel.panelConfig,
|
||||
title: p.panel.title,
|
||||
},
|
||||
matchedBy: { fields: Array.from(p.matchingFields) },
|
||||
})),
|
||||
});
|
||||
}
|
||||
});
|
||||
return { dashboards: relevantDashboards };
|
||||
}
|
||||
|
||||
getPanelsByIndex(index: string, panels: DashboardPanel[]): DashboardPanel[] {
|
||||
const panelsByIndex = panels.filter((p) => {
|
||||
const panelIndices = this.getPanelIndices(p);
|
||||
return panelIndices.has(index);
|
||||
});
|
||||
return panelsByIndex;
|
||||
}
|
||||
|
||||
getPanelsByField(
|
||||
fields: string[],
|
||||
panels: DashboardPanel[]
|
||||
): Array<{ matchingFields: Set<string>; panel: DashboardPanel }> {
|
||||
const panelsByField = panels.reduce((acc, p) => {
|
||||
const panelFields = this.getPanelFields(p);
|
||||
const matchingFields = fields.filter((f) => panelFields.has(f));
|
||||
if (matchingFields.length) {
|
||||
acc.push({ matchingFields: new Set(matchingFields), panel: p });
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<{ matchingFields: Set<string>; panel: DashboardPanel }>);
|
||||
return panelsByField;
|
||||
}
|
||||
|
||||
getPanelIndices(panel: DashboardPanel): Set<string> {
|
||||
const indices = new Set<string>();
|
||||
switch (panel.type) {
|
||||
case 'lens':
|
||||
const lensAttr = panel.panelConfig.attributes as unknown as LensAttributes;
|
||||
if (!lensAttr) {
|
||||
return indices;
|
||||
}
|
||||
const lensIndices = this.getLensVizIndices(lensAttr);
|
||||
return lensIndices;
|
||||
default:
|
||||
return indices;
|
||||
}
|
||||
}
|
||||
|
||||
getPanelFields(panel: DashboardPanel): Set<string> {
|
||||
const fields = new Set<string>();
|
||||
switch (panel.type) {
|
||||
case 'lens':
|
||||
const lensAttr = panel.panelConfig.attributes as unknown as LensAttributes;
|
||||
const lensFields = this.getLensVizFields(lensAttr);
|
||||
return lensFields;
|
||||
default:
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
async getRuleQueryIndex(): Promise<string> {
|
||||
if (!this.alert) {
|
||||
throw new Error('Alert not found. Could not get the rule query index.');
|
||||
}
|
||||
const index = this.alert.getRuleQueryIndex();
|
||||
return typeof index === 'string' ? index : index.id || '';
|
||||
}
|
||||
|
||||
getLensVizIndices(lensAttr: LensAttributes): Set<string> {
|
||||
const indices = new Set(
|
||||
lensAttr.references
|
||||
.filter((r) => r.name.match(`indexpattern`))
|
||||
.map((reference) => reference.id)
|
||||
);
|
||||
if (indices.size === 0) {
|
||||
throw new Error('No index patterns found in lens visualization');
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
getLensVizFields(lensAttr: LensAttributes): Set<string> {
|
||||
const fields = new Set<string>();
|
||||
const dataSourceLayers = lensAttr.state.datasourceStates.formBased?.layers || {};
|
||||
Object.values(dataSourceLayers).forEach((ds) => {
|
||||
const columns = ds.columns;
|
||||
Object.values(columns).forEach((col) => {
|
||||
const hasSourceField = (
|
||||
c: FieldBasedIndexPatternColumn | GenericIndexPatternColumn
|
||||
): c is FieldBasedIndexPatternColumn =>
|
||||
(c as FieldBasedIndexPatternColumn).sourceField !== undefined;
|
||||
if (hasSourceField(col)) {
|
||||
fields.add(col.sourceField);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@
|
|||
"@kbn/inspector-plugin",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/observability-schema",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/react-kibana-context-render",
|
||||
|
@ -117,6 +118,7 @@
|
|||
"@kbn/data-service",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/response-ops-rule-params",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/fields-metadata-plugin",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/core-http-browser"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('Incident Management', () => {
|
||||
loadTestFile(require.resolve('./suggested_dashboards'));
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -24,5 +24,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
loadTestFile(require.resolve('../../apis/observability/ai_assistant'));
|
||||
loadTestFile(require.resolve('../../apis/observability/streams'));
|
||||
loadTestFile(require.resolve('../../apis/observability/onboarding'));
|
||||
loadTestFile(require.resolve('../../apis/observability/incident_management'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,5 +18,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
loadTestFile(require.resolve('../../apis/observability/ai_assistant'));
|
||||
loadTestFile(require.resolve('../../apis/observability/streams'));
|
||||
loadTestFile(require.resolve('../../apis/observability/onboarding'));
|
||||
loadTestFile(require.resolve('../../apis/observability/incident_management'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6394,6 +6394,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-schema@link:x-pack/solutions/observability/packages/kbn-observability-schema":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-shared-plugin@link:x-pack/solutions/observability/plugins/observability_shared":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue