-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Investigation app] add entities route and investigation Contextual Insight #194432
Changes from 17 commits
27c0182
2d305e7
84a3817
dc8c47a
be4dfb7
16f926d
72914b4
a67953d
f041ec1
503b7f8
552d79c
1d65aec
2907990
3c542d4
53655e5
3fb9019
862e98f
ed126e4
2d119de
56b2fb1
1932efe
b70caeb
c24a620
d9d211b
2a27f75
4ab1a83
f95017d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
* Public License v 1"; you may not use this file except in compliance with, at | ||
* 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 { z } from '@kbn/zod'; | ||
|
||
const metricsSchema = z.object({ | ||
failedTransactionRate: z.number().optional(), | ||
latency: z.number().optional(), | ||
throughput: z.number().optional(), | ||
logErrorRate: z.number().optional(), | ||
logRate: z.number().optional(), | ||
}); | ||
|
||
const entitySchema = z.object({ | ||
id: z.string(), | ||
definitionId: z.string(), | ||
definitionVersion: z.string(), | ||
displayName: z.string(), | ||
firstSeenTimestamp: z.string(), | ||
lastSeenTimestamp: z.string(), | ||
identityFields: z.array(z.string()), | ||
schemaVersion: z.string(), | ||
type: z.string(), | ||
metrics: metricsSchema, | ||
}); | ||
|
||
const entitySourceSchema = z.object({ | ||
dataStream: z.string().optional(), | ||
}); | ||
|
||
const entityWithSourceSchema = z.intersection( | ||
entitySchema, | ||
z.object({ | ||
sources: z.array(entitySourceSchema), | ||
}) | ||
); | ||
|
||
type EntityWithSource = z.output<typeof entityWithSourceSchema>; | ||
type EntitySource = z.output<typeof entitySourceSchema>; | ||
|
||
export { entitySchema, entityWithSourceSchema }; | ||
export type { EntityWithSource, EntitySource }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
* Public License v 1"; you may not use this file except in compliance with, at | ||
* 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 { z } from '@kbn/zod'; | ||
import { entityWithSourceSchema } from './entity'; | ||
|
||
const getEntitiesParamsSchema = z | ||
.object({ | ||
query: z | ||
.object({ | ||
'service.name': z.string(), | ||
'service.environment': z.string(), | ||
'host.name': z.string(), | ||
'container.id': z.string(), | ||
}) | ||
.partial(), | ||
}) | ||
.partial(); | ||
|
||
const getEntitiesResponseSchema = z.object({ | ||
entities: z.array(entityWithSourceSchema), | ||
}); | ||
|
||
type GetEntitiesParams = z.infer<typeof getEntitiesParamsSchema.shape.query>; | ||
type GetEntitiesResponse = z.output<typeof getEntitiesResponseSchema>; | ||
|
||
export { getEntitiesParamsSchema, getEntitiesResponseSchema }; | ||
export type { GetEntitiesParams, GetEntitiesResponse }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* 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 { useQuery } from '@tanstack/react-query'; | ||
import { GetEntitiesResponse } from '@kbn/investigation-shared'; | ||
import { useKibana } from './use_kibana'; | ||
import { investigationKeys } from './query_key_factory'; | ||
|
||
export interface EntityParams { | ||
investigationId: string; | ||
serviceName?: string; | ||
serviceEnvironment?: string; | ||
hostName?: string; | ||
containerId?: string; | ||
} | ||
|
||
export function useFetchEntities({ | ||
investigationId, | ||
serviceName, | ||
serviceEnvironment, | ||
hostName, | ||
containerId, | ||
}: EntityParams) { | ||
const { | ||
core: { http }, | ||
} = useKibana(); | ||
|
||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ | ||
queryKey: investigationKeys.entities({ | ||
investigationId, | ||
serviceName, | ||
serviceEnvironment, | ||
hostName, | ||
containerId, | ||
}), | ||
queryFn: async ({ signal }) => { | ||
return await http.get<GetEntitiesResponse>('/api/observability/investigation/entities', { | ||
query: { | ||
'service.name': serviceName, | ||
'service.environment': serviceEnvironment, | ||
'host.name': hostName, | ||
'container.id': containerId, | ||
}, | ||
version: '2023-10-31', | ||
signal, | ||
}); | ||
}, | ||
refetchOnWindowFocus: false, | ||
onError: (error: Error) => { | ||
// ignore error | ||
}, | ||
enabled: Boolean(investigationId && (serviceName || hostName || containerId)), | ||
}); | ||
|
||
return { | ||
data, | ||
isInitialLoading, | ||
isLoading, | ||
isRefetching, | ||
isSuccess, | ||
isError, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* 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 dedent from 'dedent'; | ||
import { | ||
ALERT_RULE_PARAMETERS, | ||
ALERT_START, | ||
ALERT_RULE_CATEGORY, | ||
ALERT_REASON, | ||
} from '@kbn/rule-data-utils'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { EntityWithSource } from '@kbn/investigation-shared'; | ||
import React, { useCallback } from 'react'; | ||
import { useKibana } from '../../../../hooks/use_kibana'; | ||
import { useInvestigation } from '../../contexts/investigation_context'; | ||
import { useFetchEntities } from '../../../../hooks/use_fetch_entities'; | ||
|
||
export interface InvestigationContextualInsight { | ||
key: string; | ||
description: string; | ||
data: unknown; | ||
} | ||
|
||
export function AssistantHypothesis({ investigationId }: { investigationId: string }) { | ||
const { alert } = useInvestigation(); | ||
const { | ||
dependencies: { | ||
start: { | ||
observabilityAIAssistant: { | ||
ObservabilityAIAssistantContextualInsight, | ||
getContextualInsightMessages, | ||
}, | ||
}, | ||
}, | ||
} = useKibana(); | ||
const { data: entitiesData } = useFetchEntities({ | ||
investigationId, | ||
serviceName: alert?.['service.name'] ? `${alert?.['service.name']}` : undefined, | ||
serviceEnvironment: alert?.['service.environment'] | ||
? `${alert?.['service.environment']}` | ||
: undefined, | ||
hostName: alert?.['host.name'] ? `${alert?.['host.name']}` : undefined, | ||
containerId: alert?.['container.id'] ? `${alert?.['container.id']}` : undefined, | ||
}); | ||
|
||
const getAlertContextMessages = useCallback(async () => { | ||
if (!getContextualInsightMessages || !alert) { | ||
return []; | ||
} | ||
|
||
const entities = entitiesData?.entities ?? []; | ||
|
||
const entityContext = entities?.length | ||
? ` | ||
Alerts can optionally be associated with entities. Entities can be services, hosts, containers, or other resources. Entities can have metrics associated with them. | ||
|
||
The alert that triggered this investigation is associated with the following entities: ${entities | ||
.map((entity, index) => { | ||
return dedent(` | ||
## Entity ${index + 1}: | ||
${formatEntityMetrics(entity)}; | ||
`); | ||
}) | ||
.join('/n/n')}` | ||
: ''; | ||
|
||
return getContextualInsightMessages({ | ||
message: `I am investigating a failure in my system. I was made aware of the failure by an alert and I am trying to understand the root cause of the issue.`, | ||
instructions: dedent( | ||
`I'm an SRE. I am investigating a failure in my system. I was made aware of the failure via an alert. Your current task is to help me identify the root cause of the failure in my system. | ||
|
||
The rule that triggered the alert is a ${ | ||
alert[ALERT_RULE_CATEGORY] | ||
} rule. The alert started at ${alert[ALERT_START]}. The alert reason is ${ | ||
alert[ALERT_REASON] | ||
}. The rule parameters are ${JSON.stringify(ALERT_RULE_PARAMETERS)}. | ||
|
||
${entityContext} | ||
|
||
Based on the alert details, suggest a root cause and next steps to mitigate the issue. | ||
|
||
I do not have the alert details or entity details in front of me, so be sure to repeat the alert reason (${ | ||
alert[ALERT_REASON] | ||
}), when the alert was triggered (${ | ||
alert[ALERT_START] | ||
}), and the entity metrics in your response. | ||
|
||
When displaying the entity metrics, please convert the metrics to a human-readable format. For example, convert "logRate" to "Log Rate" and "errorRate" to "Error Rate". | ||
` | ||
), | ||
}); | ||
}, [alert, getContextualInsightMessages, entitiesData?.entities]); | ||
|
||
if (!ObservabilityAIAssistantContextualInsight) { | ||
return null; | ||
} | ||
|
||
return alert && entitiesData ? ( | ||
<ObservabilityAIAssistantContextualInsight | ||
title={i18n.translate( | ||
'xpack.investigateApp.assistantHypothesis.observabilityAIAssistantContextualInsight.helpMeInvestigateThisLabel', | ||
{ defaultMessage: 'Help me investigate this failure' } | ||
)} | ||
messages={getAlertContextMessages} | ||
/> | ||
) : null; | ||
} | ||
const formatEntityMetrics = (entity: EntityWithSource): string => { | ||
const entityMetrics = Object.entries(entity.metrics) | ||
.map(([key, value]) => `${key}: ${value}`) | ||
.join(', '); | ||
const entitySources = entity.sources.map((source) => source.dataStream).join(', '); | ||
return dedent(` | ||
Entity name: ${entity.displayName}; | ||
Entity type: ${entity.type}; | ||
Entity metrics: ${entityMetrics}; | ||
Entity data streams: ${entitySources} | ||
`); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -12,9 +12,10 @@ import { useInvestigation } from '../../contexts/investigation_context'; | |||||||||||||||||
import { AddInvestigationItem } from '../add_investigation_item/add_investigation_item'; | ||||||||||||||||||
import { InvestigationItemsList } from '../investigation_items_list/investigation_items_list'; | ||||||||||||||||||
import { InvestigationSearchBar } from '../investigation_search_bar/investigation_search_bar'; | ||||||||||||||||||
import { AssistantHypothesis } from '../assistant_hypothesis/assistant_hypothesis'; | ||||||||||||||||||
|
||||||||||||||||||
export function InvestigationItems() { | ||||||||||||||||||
const { globalParams, updateInvestigationParams } = useInvestigation(); | ||||||||||||||||||
const { globalParams, updateInvestigationParams, investigation } = useInvestigation(); | ||||||||||||||||||
|
||||||||||||||||||
return ( | ||||||||||||||||||
<EuiFlexGroup direction="column" gutterSize="m"> | ||||||||||||||||||
|
@@ -33,10 +34,14 @@ export function InvestigationItems() { | |||||||||||||||||
/> | ||||||||||||||||||
</EuiFlexItem> | ||||||||||||||||||
|
||||||||||||||||||
{investigation?.id && ( | ||||||||||||||||||
<EuiFlexItem grow={false}> | ||||||||||||||||||
<AssistantHypothesis investigationId={investigation.id} /> | ||||||||||||||||||
</EuiFlexItem> | ||||||||||||||||||
)} | ||||||||||||||||||
Comment on lines
+37
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🍰 nit: use the context hook
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually had this originally, but it made it so that the investigation was sometimes undefined, and I hated having to handle that all the time. Would you prefer that trade off? |
||||||||||||||||||
<EuiFlexItem grow={false}> | ||||||||||||||||||
<InvestigationItemsList /> | ||||||||||||||||||
</EuiFlexItem> | ||||||||||||||||||
|
||||||||||||||||||
<AddInvestigationItem /> | ||||||||||||||||||
</EuiFlexGroup> | ||||||||||||||||||
); | ||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's already in the requiredPlugins