-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Synthetics] Detect and display missing/corrupted Synthetics integrations in monitor UIs #256738
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
Changes from 32 commits
7736576
daf42b8
c6d6565
a3b0758
043faaf
32ea22e
b01ce6a
889718b
de0a995
86e9383
e4ff75e
8f77683
d16abdd
37e3483
d379d26
e91cfb4
e143d07
7908d0b
c60be8b
03df478
3ee0c34
c5cc305
00cc5a5
5bc168b
1c82841
8fa37fe
d5d0b36
158ac3d
2111b8f
19cf9c5
996629e
e9581aa
42a2722
4d7c9e9
947aa55
1ad223d
b0f8372
b9a80f6
c68b4fd
7a9783c
10cb00b
f9886a9
1c75100
1a672bf
1347bf1
14bb9da
b6a66fa
6b40f25
0965ae8
29e9733
7880bf3
7a20f23
b3a0c62
17cd6a5
5a8232e
e94d3ec
2a35b5c
7016a7d
1e949ef
0cc68b0
5dbf14d
ac733fe
d1c76da
764371a
0a340ce
1dd1868
2495f94
5d6c994
6737a3c
1f943f3
5164a7f
bb06301
48e31d5
cdefd2d
65ee6e6
ed4b8d8
bfdf133
d151aff
2ed2415
0335af5
277887e
e25f9c3
549360c
ab25068
0aeab27
9010313
2fcb0e4
77b2a7b
c6f2f77
3aa11ed
8fa943c
9b9d7e7
c982ced
ec150e0
abc0346
920f6f5
1606e53
ebf59fe
b6abfac
88ce1bf
56c3c6d
a072b11
c3e738f
cf023a0
21e1b1e
921d465
5f33b51
9d0640b
9275d27
9b9a3a9
f0159d3
560c7b2
8f981fe
7df675a
c96927e
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,41 @@ | ||
| /* | ||
| * 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 enum LocationHealthStatusValue { | ||
| Healthy = 'healthy', | ||
| MissingPackagePolicy = 'missing_package_policy', | ||
| MissingAgentPolicy = 'missing_agent_policy', | ||
| AgentPolicyMismatch = 'agent_policy_mismatch', | ||
| MissingLocation = 'missing_location', | ||
| PackageNotInstalled = 'package_not_installed', | ||
| } | ||
|
|
||
| export interface LocationHealthStatus { | ||
| locationId: string; | ||
| locationLabel: string; | ||
| status: LocationHealthStatusValue; | ||
| policyId: string; | ||
| reason?: string; | ||
| } | ||
|
|
||
| export interface MonitorHealthStatus { | ||
| configId: string; | ||
| monitorName: string; | ||
| isUnhealthy: boolean; | ||
| locations: LocationHealthStatus[]; | ||
|
miguelmartin-elastic marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| export interface MonitorHealthError { | ||
| configId: string; | ||
| error: string; | ||
| statusCode?: number; | ||
|
Contributor
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. why is it optional? 🤔
Contributor
Author
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. Good catch! It no longer is, statusCode is now required. The server always populates it: output.statusCode from the Boom error, or 500 as a fallback for non-Boom errors |
||
| } | ||
|
|
||
| export interface MonitorsHealthResponse { | ||
| monitors: MonitorHealthStatus[]; | ||
| errors: MonitorHealthError[]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * 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 { i18n } from '@kbn/i18n'; | ||
| import { LocationHealthStatusValue } from '../../../../../../common/runtime_types'; | ||
|
|
||
| export const STATUS_LABELS: Record< | ||
| Exclude<LocationHealthStatusValue, LocationHealthStatusValue.Healthy>, | ||
| string | ||
| > = { | ||
| [LocationHealthStatusValue.MissingPackagePolicy]: i18n.translate( | ||
| 'xpack.synthetics.monitorHealth.status.missingPackagePolicy', | ||
| { | ||
| defaultMessage: 'The Fleet package policy for this monitor/location pair does not exist.', | ||
|
miguelmartin-elastic marked this conversation as resolved.
Outdated
|
||
| } | ||
| ), | ||
| [LocationHealthStatusValue.MissingAgentPolicy]: i18n.translate( | ||
| 'xpack.synthetics.monitorHealth.status.missingAgentPolicy', | ||
| { | ||
| defaultMessage: 'The agent policy referenced by this private location no longer exists.', | ||
| } | ||
| ), | ||
| [LocationHealthStatusValue.AgentPolicyMismatch]: i18n.translate( | ||
| 'xpack.synthetics.monitorHealth.status.agentPolicyMismatch', | ||
| { | ||
| defaultMessage: | ||
| 'The package policy exists but is attached to a different agent policy than expected.', | ||
| } | ||
| ), | ||
| [LocationHealthStatusValue.MissingLocation]: i18n.translate( | ||
| 'xpack.synthetics.monitorHealth.status.missingLocation', | ||
| { | ||
| defaultMessage: 'The monitor references a private location that no longer exists.', | ||
| } | ||
| ), | ||
| [LocationHealthStatusValue.PackageNotInstalled]: i18n.translate( | ||
| 'xpack.synthetics.monitorHealth.status.packageNotInstalled', | ||
| { | ||
| defaultMessage: 'The synthetics Fleet package is not installed.', | ||
| } | ||
| ), | ||
| }; | ||
|
|
||
| export const getStatusLabel = (status: LocationHealthStatusValue): string | undefined => { | ||
| if (status === LocationHealthStatusValue.Healthy) return undefined; | ||
| return STATUS_LABELS[status]; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| /* | ||
| * 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||
| import { useDispatch, useSelector } from 'react-redux'; | ||
| import { | ||
| fetchMonitorListAction, | ||
| getMonitorListPageStateWithDefaults, | ||
| selectMonitorListState, | ||
| } from '../../../state'; | ||
| import { ConfigKey, LocationHealthStatusValue } from '../../../../../../common/runtime_types'; | ||
| import { fetchMonitorHealthAction, selectMonitorHealth } from '../../../state/monitor_health'; | ||
|
|
||
| export interface MonitorIntegrationStatus { | ||
| configId: string; | ||
| locationId: string; | ||
| locationLabel: string; | ||
| policyId: string; | ||
|
miguelmartin-elastic marked this conversation as resolved.
Outdated
|
||
| status: LocationHealthStatusValue; | ||
| isUnhealthy: boolean; | ||
| } | ||
|
|
||
| interface UseMonitorIntegrationHealthOptions { | ||
| configIds?: string[]; | ||
| } | ||
|
|
||
| interface UseMonitorIntegrationHealthReturn { | ||
| statuses: Map<string, MonitorIntegrationStatus[]>; | ||
| loading: boolean; | ||
| isResetting: boolean; | ||
| resetMonitor: (configId: string) => Promise<void>; | ||
| resetMonitors: (configIds: string[]) => Promise<void>; | ||
| isUnhealthy: (configId: string) => boolean; | ||
| getUnhealthyLocationStatuses: (configId: string) => MonitorIntegrationStatus[]; | ||
| getUnhealthyLocationCount: () => number; | ||
| getUnhealthyMonitorCountForLocation: (locationId: string) => number; | ||
| getUnhealthyConfigIdsForLocation: (locationId: string) => string[]; | ||
| } | ||
|
|
||
| export const useMonitorIntegrationHealth = ( | ||
| options?: UseMonitorIntegrationHealthOptions | ||
| ): UseMonitorIntegrationHealthReturn => { | ||
| const { configIds: explicitConfigIds } = options ?? {}; | ||
|
benakansara marked this conversation as resolved.
Outdated
|
||
| const dispatch = useDispatch(); | ||
| const [isResetting, setIsResetting] = useState(false); | ||
|
|
||
| const { | ||
| data: { monitors: listMonitors }, | ||
| loaded: listLoaded, | ||
| loading: listLoading, | ||
| } = useSelector(selectMonitorListState); | ||
|
|
||
| const { data: healthData, loading: healthLoading } = useSelector(selectMonitorHealth); | ||
|
|
||
| useEffect(() => { | ||
| if (!explicitConfigIds && !listLoaded && !listLoading) { | ||
| dispatch(fetchMonitorListAction.get(getMonitorListPageStateWithDefaults())); | ||
| } | ||
| }, [dispatch, explicitConfigIds, listLoaded, listLoading]); | ||
|
|
||
| const monitorIdsToFetch = useMemo(() => { | ||
| if (explicitConfigIds) { | ||
| return explicitConfigIds; | ||
| } | ||
| if (!listLoaded) { | ||
| return []; | ||
| } | ||
| return listMonitors | ||
| .filter((m) => (m[ConfigKey.LOCATIONS] ?? []).some((loc) => !loc.isServiceManaged)) | ||
| .map((m) => m[ConfigKey.CONFIG_ID]); | ||
| }, [explicitConfigIds, listLoaded, listMonitors]); | ||
|
|
||
| const prevIdsRef = useRef<string>(''); | ||
|
|
||
| useEffect(() => { | ||
| if (monitorIdsToFetch.length === 0) return; | ||
|
|
||
| const key = monitorIdsToFetch.slice().sort().join(','); | ||
| if (key === prevIdsRef.current) return; | ||
| prevIdsRef.current = key; | ||
|
|
||
| dispatch(fetchMonitorHealthAction.get(monitorIdsToFetch)); | ||
| }, [dispatch, monitorIdsToFetch]); | ||
|
|
||
| const statuses = useMemo(() => { | ||
| const map = new Map<string, MonitorIntegrationStatus[]>(); | ||
| if (!healthData) return map; | ||
|
|
||
| for (const monitor of healthData.monitors) { | ||
| const locationStatuses: MonitorIntegrationStatus[] = monitor.locations.map((loc) => ({ | ||
| configId: monitor.configId, | ||
| locationId: loc.locationId, | ||
| locationLabel: loc.locationLabel, | ||
| policyId: loc.policyId, | ||
| status: loc.status, | ||
| isUnhealthy: loc.status !== LocationHealthStatusValue.Healthy, | ||
| })); | ||
| map.set(monitor.configId, locationStatuses); | ||
| } | ||
|
|
||
| return map; | ||
| }, [healthData]); | ||
|
|
||
| const isUnhealthy = useCallback( | ||
| (configId: string): boolean => { | ||
| const locationStatuses = statuses.get(configId); | ||
| return locationStatuses?.some((s) => s.isUnhealthy) ?? false; | ||
| }, | ||
| [statuses] | ||
| ); | ||
|
|
||
| const getUnhealthyLocationStatuses = useCallback( | ||
| (configId: string): MonitorIntegrationStatus[] => { | ||
| const locationStatuses = statuses.get(configId); | ||
| return locationStatuses?.filter((s) => s.isUnhealthy) ?? []; | ||
| }, | ||
| [statuses] | ||
| ); | ||
|
|
||
| const getUnhealthyLocationCount = useCallback((): number => { | ||
|
miguelmartin-elastic marked this conversation as resolved.
Outdated
|
||
| let count = 0; | ||
| for (const locationStatuses of statuses.values()) { | ||
| if (locationStatuses.some((s) => s.isUnhealthy)) count++; | ||
| } | ||
| return count; | ||
| }, [statuses]); | ||
|
|
||
| const getUnhealthyMonitorCountForLocation = useCallback( | ||
| (locationId: string): number => { | ||
| let count = 0; | ||
| for (const locationStatuses of statuses.values()) { | ||
| if (locationStatuses.some((s) => s.locationId === locationId && s.isUnhealthy)) count++; | ||
| } | ||
| return count; | ||
| }, | ||
| [statuses] | ||
| ); | ||
|
|
||
| const getUnhealthyConfigIdsForLocation = useCallback( | ||
| (locationId: string): string[] => { | ||
| const ids: string[] = []; | ||
| for (const entries of statuses.values()) { | ||
| if (entries.some((s) => s.locationId === locationId && s.isUnhealthy)) { | ||
| ids.push(entries[0].configId); | ||
| } | ||
| } | ||
| return ids; | ||
| }, | ||
| [statuses] | ||
| ); | ||
|
|
||
| // Reset functionality is a stub until the reset endpoint is implemented (#256394) | ||
| const resetMonitor = useCallback(async (_configId: string): Promise<void> => { | ||
| setIsResetting(true); | ||
| await new Promise((resolve) => setTimeout(resolve, 800)); | ||
| setIsResetting(false); | ||
| }, []); | ||
|
|
||
| const resetMonitors = useCallback(async (_ids: string[]): Promise<void> => { | ||
| setIsResetting(true); | ||
| await new Promise((resolve) => setTimeout(resolve, 800)); | ||
| setIsResetting(false); | ||
| }, []); | ||
|
|
||
| const loading = explicitConfigIds ? healthLoading : !listLoaded || healthLoading; | ||
|
|
||
| return { | ||
| statuses, | ||
| loading, | ||
| isResetting, | ||
| resetMonitor, | ||
| resetMonitors, | ||
| isUnhealthy, | ||
| getUnhealthyLocationStatuses, | ||
| getUnhealthyLocationCount, | ||
| getUnhealthyMonitorCountForLocation, | ||
| getUnhealthyConfigIdsForLocation, | ||
| }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.