Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
7736576
mock reset endpoint, UI first approach
miguelmartin-elastic Mar 9, 2026
daf42b8
pass monitors as prop
miguelmartin-elastic Mar 9, 2026
c6d6565
use icon with tooltip for missing policy warning
miguelmartin-elastic Mar 9, 2026
a3b0758
fetch configs from hook if none is provided
miguelmartin-elastic Mar 9, 2026
043faaf
_health api spec proposal
miguelmartin-elastic Mar 10, 2026
32ea22e
refactor monirots_integration_health
miguelmartin-elastic Mar 10, 2026
b01ce6a
add unit tests
miguelmartin-elastic Mar 10, 2026
889718b
move types to common/
miguelmartin-elastic Mar 10, 2026
de0a995
rename from status to health
miguelmartin-elastic Mar 10, 2026
86e9383
fix import
miguelmartin-elastic Mar 10, 2026
e4ff75e
i18n status display messages
miguelmartin-elastic Mar 10, 2026
8f77683
store configId for debugging purposes
miguelmartin-elastic Mar 11, 2026
d16abdd
remove unused import
miguelmartin-elastic Mar 11, 2026
37e3483
check if agentPolicy exists
miguelmartin-elastic Mar 11, 2026
d379d26
rename isUnhealthy to be more generic
miguelmartin-elastic Mar 11, 2026
e91cfb4
refactor unhealthy tooltip
miguelmartin-elastic Mar 11, 2026
e143d07
refactor ResetLocationMonitors
miguelmartin-elastic Mar 11, 2026
7908d0b
checkout bulk operation reset
miguelmartin-elastic Mar 11, 2026
c60be8b
parse error from reason
miguelmartin-elastic Mar 11, 2026
03df478
remove oas docs
miguelmartin-elastic Mar 11, 2026
3ee0c34
move health api to service
miguelmartin-elastic Mar 11, 2026
c5cc305
fix naming
miguelmartin-elastic Mar 11, 2026
00cc5a5
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 11, 2026
5bc168b
fix mock
miguelmartin-elastic Mar 12, 2026
1c82841
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 12, 2026
8fa37fe
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 12, 2026
d5d0b36
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 13, 2026
158ac3d
remove spaceId
miguelmartin-elastic Mar 13, 2026
2111b8f
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
19cf9c5
fix type
miguelmartin-elastic Mar 13, 2026
996629e
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 13, 2026
e9581aa
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
42a2722
Support legacy policy ID format in monitor health checks
miguelmartin-elastic Mar 13, 2026
4d7c9e9
Document intentional priority order of health status checks
miguelmartin-elastic Mar 13, 2026
947aa55
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 13, 2026
1ad223d
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 16, 2026
b0f8372
wire up reset api, add unit tests
miguelmartin-elastic Mar 16, 2026
b9a80f6
add toast feedback and reset confirmation modal
miguelmartin-elastic Mar 16, 2026
c68b4fd
listen to refresh
miguelmartin-elastic Mar 16, 2026
7a9783c
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 16, 2026
10cb00b
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 16, 2026
f9886a9
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 16, 2026
1c75100
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 18, 2026
9dadf1f
[Synthetics] Extend monitor integration health: detect missing and un…
miguelmartin-elastic Mar 19, 2026
1a672bf
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 20, 2026
1347bf1
fix integ test
miguelmartin-elastic Mar 20, 2026
14bb9da
* simplify reset callouts by removing internal state
miguelmartin-elastic Mar 20, 2026
b6a66fa
do not mark monitors with public locations only as unhealthy, cover w…
miguelmartin-elastic Mar 20, 2026
6b40f25
fix minor issues
miguelmartin-elastic Mar 20, 2026
0965ae8
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 20, 2026
29e9733
handle partial errors from reset api
miguelmartin-elastic Mar 20, 2026
7880bf3
Revert "fix integ test"
miguelmartin-elastic Mar 23, 2026
7a20f23
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 23, 2026
b3a0c62
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 23, 2026
17cd6a5
return 500 status code when reset call fails
miguelmartin-elastic Mar 23, 2026
5a8232e
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 24, 2026
e94d3ec
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 25, 2026
2a35b5c
add reset action to monitor list row actions menu
miguelmartin-elastic Mar 25, 2026
7016a7d
exclude non-fixable monitors from bulk reset and show skipped in modal
miguelmartin-elastic Mar 25, 2026
1e949ef
skip locations with missing agent policy during default reset
miguelmartin-elastic Mar 25, 2026
0cc68b0
rename getValidLocationIds and fix empty agentPolicyId edge case, add…
miguelmartin-elastic Mar 25, 2026
5dbf14d
convert private locations reset action to icon type for consistent ac…
miguelmartin-elastic Mar 25, 2026
ac733fe
move missing integration callout from edit page to monitor details su…
miguelmartin-elastic Mar 25, 2026
d1c76da
add configIds filter to monitor list and link unhealthy badge to filt…
miguelmartin-elastic Mar 25, 2026
764371a
align single and bulk health API error response field from 'error' to…
miguelmartin-elastic Mar 25, 2026
0a340ce
add missingAgentPolicyId
miguelmartin-elastic Mar 25, 2026
1dd1868
rename locations to privateLocations
miguelmartin-elastic Mar 25, 2026
2495f94
rename isUnhealthy to isHealthy in MonitorHealthStatus
miguelmartin-elastic Mar 25, 2026
5d6c994
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 25, 2026
6737a3c
include configIds in isMonitorsQueryFiltered to fix absoluteTotal count
miguelmartin-elastic Mar 26, 2026
1f943f3
fix Reset action enabled guards in monitor list actions column
miguelmartin-elastic Mar 26, 2026
5164a7f
add configIds to expected defaults in getSupportedUrlParams test
miguelmartin-elastic Mar 26, 2026
bb06301
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Mar 26, 2026
48e31d5
Merge branch 'main' of https://github.com/elastic/kibana into synthet…
miguelmartin-elastic Mar 26, 2026
cdefd2d
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 26, 2026
65ee6e6
Remove break_monitors.sh test script (A8)
miguelmartin-elastic Mar 27, 2026
ed4b8d8
Add writeAccess: false to GET monitor health route (A1)
miguelmartin-elastic Mar 27, 2026
bfdf133
Add permission guards to Reset action in monitor list (A2, A3)
miguelmartin-elastic Mar 27, 2026
d151aff
Rename isResetFixable → isFixableByReset (A5, A6)
miguelmartin-elastic Mar 27, 2026
2ed2415
Remove unused getUnhealthyLocationCount function (A7)
miguelmartin-elastic Mar 27, 2026
0335af5
Fix skippedMonitors in locations_table reset action (A4)
miguelmartin-elastic Mar 27, 2026
277887e
Fix multiline rendering in unhealthy tooltip (A9)
miguelmartin-elastic Mar 27, 2026
e25f9c3
Simplify getUnhealthyMonitorsForLocation: remove unnecessary seenConf…
miguelmartin-elastic Mar 27, 2026
549360c
Improve unhealthy tooltip visual hierarchy (A9 enhancement)
miguelmartin-elastic Mar 27, 2026
ab25068
Refactor unhealthy tooltip to use EUI components instead of inline st…
miguelmartin-elastic Mar 27, 2026
0aeab27
rename explicitConfigIds to configIds
miguelmartin-elastic Mar 27, 2026
9010313
Improve error statusCode capture to include all SavedObjects error types
miguelmartin-elastic Mar 27, 2026
2fcb0e4
Improve error statusCode capture to include all SavedObjects error types
miguelmartin-elastic Mar 27, 2026
77b2a7b
Add unit tests for statusCode capture from SavedObjects errors
miguelmartin-elastic Mar 27, 2026
c6f2f77
Simplify statusCode capture: use output.statusCode with 500 fallback
miguelmartin-elastic Mar 27, 2026
3aa11ed
Update error tests to use real SavedObjects errors instead of generic…
miguelmartin-elastic Mar 27, 2026
8fa943c
Make MonitorHealthError.statusCode required and propagate it in route…
miguelmartin-elastic Mar 27, 2026
9b9d7e7
Fix reset action in private locations table not opening modal
miguelmartin-elastic Mar 27, 2026
c982ced
merge main
miguelmartin-elastic Mar 27, 2026
ec150e0
Merge branch 'main' into synthetics/missing-integrations-ui
miguelmartin-elastic Mar 30, 2026
abc0346
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Mar 30, 2026
920f6f5
[Synthetics] Fix false "Failed to reset" toast when Fleet sync errors…
miguelmartin-elastic Apr 1, 2026
1606e53
[Synthetics] Improve unhealthy tooltip styling per design feedback
miguelmartin-elastic Apr 1, 2026
ebf59fe
[Synthetics] Remove AgentPolicyMismatch and PackageNotInstalled healt…
miguelmartin-elastic Apr 1, 2026
b6abfac
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 1, 2026
88ce1bf
restructure delete action in private locations table to be icon-based…
miguelmartin-elastic Apr 1, 2026
56c3c6d
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 1, 2026
a072b11
Merge remote-tracking branch 'upstream' into synthetics/missing-integ…
miguelmartin-elastic Apr 1, 2026
c3e738f
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 1, 2026
cf023a0
remove unnecessary JSON.stringify from useEffect deps
miguelmartin-elastic Apr 6, 2026
21e1b1e
run node scripts/i18n_check.js --fix
miguelmartin-elastic Apr 6, 2026
921d465
Merge branch 'synthetics/missing-integrations-ui' of https://github.c…
miguelmartin-elastic Apr 6, 2026
dc0e41a
Merge branch 'synthetics/missing-integrations-ui' into feat/synthetic…
miguelmartin-elastic Apr 6, 2026
ff7ee9c
Merge branch 'main' of https://github.com/elastic/kibana into feat/sy…
miguelmartin-elastic Apr 6, 2026
c6eb093
Merge branch 'main' of https://github.com/elastic/kibana into feat/sy…
miguelmartin-elastic Apr 7, 2026
57026ef
Merge remote-tracking branch 'upstream/main' into feat/synthetics-age…
miguelmartin-elastic Apr 8, 2026
e29572e
[Synthetics] Use status.active instead of status.all for missing_agen…
miguelmartin-elastic Apr 8, 2026
a263c62
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 8, 2026
151ffee
Merge branch 'main' into feat/synthetics-agent-health-status-258541
miguelmartin-elastic Apr 9, 2026
174e407
Merge branch 'main' into feat/synthetics-agent-health-status-258541
miguelmartin-elastic Apr 10, 2026
c135a42
Merge branch 'main' into feat/synthetics-agent-health-status-258541
miguelmartin-elastic Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export enum PrivateLocationHealthStatusValue {
MissingPackagePolicy = 'missing_package_policy',
MissingAgentPolicy = 'missing_agent_policy',
MissingLocation = 'missing_location',
MissingAgents = 'missing_agents',
UnhealthyAgent = 'unhealthy_agent',
}

export interface PrivateLocationHealthStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ export const STATUS_LABELS: Record<
defaultMessage: 'The monitor references a private location that no longer exists.',
}
),
[PrivateLocationHealthStatusValue.MissingAgents]: i18n.translate(
'xpack.synthetics.monitorHealth.status.missingAgents',
{
defaultMessage:
'No Fleet agents are enrolled in the agent policy for this private location. Enroll an agent in Fleet to resolve this.',
}
),
[PrivateLocationHealthStatusValue.UnhealthyAgent]: i18n.translate(
'xpack.synthetics.monitorHealth.status.unhealthyAgent',
{
defaultMessage:
'All Fleet agents for this private location are unhealthy or offline. Check the agent status in Fleet.',
}
),
};

export const getStatusLabel = (status: PrivateLocationHealthStatusValue): string | undefined => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const buildApi = (overrides: {
fleetGetByIDs?: jest.Mock;
fleetAgentPolicyGetByIds?: jest.Mock;
fleetGetInstallation?: jest.Mock;
fleetGetAgentStatusForAgentPolicy?: jest.Mock;
}): MonitorIntegrationHealthApi => {
const fleetGetByIDs = overrides.fleetGetByIDs ?? jest.fn().mockResolvedValue([]);

Expand All @@ -85,6 +86,11 @@ const buildApi = (overrides: {
const fleetGetInstallation =
overrides.fleetGetInstallation ?? jest.fn().mockResolvedValue({ install_status: 'installed' });

// Default: all agents healthy (active > 0, online > 0)
const fleetGetAgentStatusForAgentPolicy =
overrides.fleetGetAgentStatusForAgentPolicy ??
jest.fn().mockResolvedValue({ all: 1, active: 1, online: 1 });

const server = {
coreStart: {
savedObjects: {
Expand All @@ -97,6 +103,11 @@ const buildApi = (overrides: {
packageService: {
asInternalUser: { getInstallation: fleetGetInstallation },
},
agentService: {
asInternalUser: {
getAgentStatusForAgentPolicy: fleetGetAgentStatusForAgentPolicy,
},
},
},
} as unknown as SyntheticsServerSetup;

Expand Down Expand Up @@ -602,6 +613,97 @@ describe('MonitorIntegrationHealthApi', () => {
});
});

describe('missing agents', () => {
it('returns MissingAgents when no agents are enrolled in the agent policy', async () => {
const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1');
const so = createMonitorSO('mon-1', {
locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }],
});

mockedGetPrivateLocations.mockResolvedValue([privateLoc]);

const expectedPolicyId = 'mon-1-priv-loc-1';
const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']);
const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]);
const fleetGetAgentStatusForAgentPolicy = jest
.fn()
.mockResolvedValue({ all: 0, active: 0, online: 0 });

const api = buildApi({
monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) },
fleetGetByIDs,
fleetGetAgentStatusForAgentPolicy,
});

const result = await api.getHealth(['mon-1']);

const locStatus = result.monitors[0].privateLocations[0];
expect(locStatus.status).toBe(PrivateLocationHealthStatusValue.MissingAgents);
expect(locStatus.reason).toBeDefined();
expect(result.monitors[0].isHealthy).toBe(false);
});
});

describe('unhealthy agent', () => {
it('returns UnhealthyAgent when agents are enrolled but none are online', async () => {
const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1');
const so = createMonitorSO('mon-1', {
locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }],
});

mockedGetPrivateLocations.mockResolvedValue([privateLoc]);

const expectedPolicyId = 'mon-1-priv-loc-1';
const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']);
const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]);
const fleetGetAgentStatusForAgentPolicy = jest
.fn()
.mockResolvedValue({ all: 2, active: 2, online: 0 });

const api = buildApi({
monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) },
fleetGetByIDs,
fleetGetAgentStatusForAgentPolicy,
});

const result = await api.getHealth(['mon-1']);

const locStatus = result.monitors[0].privateLocations[0];
expect(locStatus.status).toBe(PrivateLocationHealthStatusValue.UnhealthyAgent);
expect(locStatus.reason).toBeDefined();
expect(result.monitors[0].isHealthy).toBe(false);
});

it('returns Healthy when at least one agent is online', async () => {
const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1');
const so = createMonitorSO('mon-1', {
locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }],
});

mockedGetPrivateLocations.mockResolvedValue([privateLoc]);

const expectedPolicyId = 'mon-1-priv-loc-1';
const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']);
const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]);
const fleetGetAgentStatusForAgentPolicy = jest
.fn()
.mockResolvedValue({ all: 3, active: 3, online: 1 });

const api = buildApi({
monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) },
fleetGetByIDs,
fleetGetAgentStatusForAgentPolicy,
});

const result = await api.getHealth(['mon-1']);

expect(result.monitors[0].privateLocations[0].status).toBe(
PrivateLocationHealthStatusValue.Healthy
);
expect(result.monitors[0].isHealthy).toBe(true);
});
});

describe('healthy status has no reason field', () => {
it('omits reason for healthy locations', async () => {
const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const STATUS_REASONS: Record<
'The agent policy referenced by this private location no longer exists.',
[PrivateLocationHealthStatusValue.MissingLocation]:
'The monitor references a private location that no longer exists.',
[PrivateLocationHealthStatusValue.MissingAgents]:
'No Fleet agents are enrolled in the agent policy for this private location.',
[PrivateLocationHealthStatusValue.UnhealthyAgent]:
'All Fleet agents enrolled in the agent policy for this private location are unhealthy or offline.',
};

interface FoundMonitor {
Expand Down Expand Up @@ -68,12 +72,14 @@ export class MonitorIntegrationHealthApi {
const referencedAgentPolicyIds = [
...new Set(allPrivateLocations.map((loc) => loc.agentPolicyId)),
];
const [existingPackagePoliciesMap, existingAgentPoliciesMap] = await Promise.all([
this.getExistingPackagePoliciesMap(
this.getExpectedPackagePolicyIds(foundMonitors, privateLocationAPI, allSpaces)
),
this.getExistingAgentPoliciesMap(referencedAgentPolicyIds),
]);
const [existingPackagePoliciesMap, existingAgentPoliciesMap, agentStatusMap] =
await Promise.all([
this.getExistingPackagePoliciesMap(
this.getExpectedPackagePolicyIds(foundMonitors, privateLocationAPI, allSpaces)
),
this.getExistingAgentPoliciesMap(referencedAgentPolicyIds),
this.getAgentStatusMap(referencedAgentPolicyIds),
]);

const existingPoliciesArray = [...existingPackagePoliciesMap.values()];

Expand All @@ -85,7 +91,7 @@ export class MonitorIntegrationHealthApi {
// Only the first matching status is returned per location — downstream issues
// are moot when a more fundamental problem exists.
//
// Priority: missing_location > missing_agent_policy > missing_package_policy > healthy
// Priority: missing_location > missing_agent_policy > missing_package_policy > missing_agents > unhealthy_agent > healthy
const locationStatuses: PrivateLocationHealthStatus[] = privateLocations.map((loc) => {
const existingPrivateLocation = allPrivateLocationsMap.get(loc.id);
const newFormatPolicyId = privateLocationAPI.getPolicyId(
Expand Down Expand Up @@ -133,6 +139,28 @@ export class MonitorIntegrationHealthApi {
const resolvedPolicyId = hasNewFormatPolicyId ? newFormatPolicyId : legacyPolicyIds[0];
const expectedAgentPolicyId = existingPrivateLocation.agentPolicyId;

const agentStatus = agentStatusMap.get(expectedAgentPolicyId);
if (agentStatus !== undefined) {
if (agentStatus.total === 0) {
return MonitorIntegrationHealthApi.buildLocationStatus(
loc.id,
existingPrivateLocation.label,
PrivateLocationHealthStatusValue.MissingAgents,
resolvedPolicyId,
expectedAgentPolicyId
);
}
if (agentStatus.online === 0) {
return MonitorIntegrationHealthApi.buildLocationStatus(
loc.id,
existingPrivateLocation.label,
PrivateLocationHealthStatusValue.UnhealthyAgent,
resolvedPolicyId,
expectedAgentPolicyId
);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
}
}

return MonitorIntegrationHealthApi.buildLocationStatus(
loc.id,
existingPrivateLocation.label,
Expand Down Expand Up @@ -239,6 +267,26 @@ export class MonitorIntegrationHealthApi {
return new Map((existingAgentPolicies ?? []).map((policy) => [policy.id, policy]));
}

private async getAgentStatusMap(
agentPolicyIds: string[]
): Promise<Map<string, { total: number; online: number }>> {
if (agentPolicyIds.length === 0) {
return new Map();
}

const entries = await Promise.all(
agentPolicyIds.map(async (policyId) => {
const status =
await this.server.fleet.agentService.asInternalUser.getAgentStatusForAgentPolicy(
policyId
);
return [policyId, { total: status.active, online: status.online }] as const;
})
);

return new Map(entries);
}

private static buildLocationStatus(
locationId: string,
locationLabel: string,
Expand Down
Loading