diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json
index 4ff0b522325ca6..6bc611d0238878 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.json
+++ b/x-pack/plugins/fleet/common/openapi/bundled.json
@@ -1705,6 +1705,14 @@
},
{
"$ref": "#/components/parameters/page_index"
+ },
+ {
+ "schema": {
+ "type": "integer",
+ "default": 5
+ },
+ "in": "query",
+ "name": "errorSize"
}
],
"responses": {
@@ -1730,7 +1738,8 @@
"EXPIRED",
"CANCELLED",
"FAILED",
- "IN_PROGRESS"
+ "IN_PROGRESS",
+ "ROLLOUT_PASSED"
]
},
"nbAgentsActioned": {
@@ -1768,6 +1777,24 @@
},
"creationTime": {
"type": "string"
+ },
+ "latestErrors": {
+ "type": "array",
+ "description": "latest errors that happened when the agents executed the action",
+ "items": {
+ "type": "object",
+ "properties": {
+ "agentId": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string"
+ }
+ }
+ }
}
},
"required": [
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml
index 9d6ae17a04a412..5c14990778c32f 100644
--- a/x-pack/plugins/fleet/common/openapi/bundled.yaml
+++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml
@@ -1063,6 +1063,11 @@ paths:
parameters:
- $ref: '#/components/parameters/page_size'
- $ref: '#/components/parameters/page_index'
+ - schema:
+ type: integer
+ default: 5
+ in: query
+ name: errorSize
responses:
'200':
description: OK
@@ -1086,6 +1091,7 @@ paths:
- CANCELLED
- FAILED
- IN_PROGRESS
+ - ROLLOUT_PASSED
nbAgentsActioned:
type: number
nbAgentsActionCreated:
@@ -1110,6 +1116,20 @@ paths:
type: string
creationTime:
type: string
+ latestErrors:
+ type: array
+ description: >-
+ latest errors that happened when the agents executed
+ the action
+ items:
+ type: object
+ properties:
+ agentId:
+ type: string
+ error:
+ type: string
+ timestamp:
+ type: string
required:
- actionId
- complete
diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml
index 1c2d013457d6f3..6daea7e6ebb2bb 100644
--- a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml
+++ b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml
@@ -5,6 +5,11 @@ get:
parameters:
- $ref: ../components/parameters/page_size.yaml
- $ref: ../components/parameters/page_index.yaml
+ - schema:
+ type: integer
+ default: 5
+ in: query
+ name: errorSize
responses:
'200':
description: OK
@@ -28,6 +33,7 @@ get:
- CANCELLED
- FAILED
- IN_PROGRESS
+ - ROLLOUT_PASSED
nbAgentsActioned:
type: number
nbAgentsActionCreated:
@@ -52,6 +58,18 @@ get:
type: string
creationTime:
type: string
+ latestErrors:
+ type: array
+ description: latest errors that happened when the agents executed the action
+ items:
+ type: object
+ properties:
+ agentId:
+ type: string
+ error:
+ type: string
+ timestamp:
+ type: string
required:
- actionId
- complete
diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts
index 537d0f250e8282..0d5e15088219e4 100644
--- a/x-pack/plugins/fleet/common/types/models/agent.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent.ts
@@ -135,6 +135,13 @@ export interface CurrentUpgrade {
startTime?: string;
}
+export interface ActionErrorResult {
+ agentId: string;
+ error: string;
+ timestamp: string;
+ hostname?: string;
+}
+
export interface ActionStatus {
actionId: string;
// how many agents are successfully included in action documents
@@ -155,6 +162,7 @@ export interface ActionStatus {
newPolicyId?: string;
creationTime: string;
hasRolloutPeriod?: boolean;
+ latestErrors?: ActionErrorResult[];
}
export interface AgentDiagnostics {
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout.tsx
index b57e1aea8b39ea..de6da14fd5014d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout.tsx
@@ -35,6 +35,7 @@ import { SO_SEARCH_LIMIT } from '../../../../constants';
import { Loading } from '../../components';
import { getTodayActions, getOtherDaysActions } from './agent_activity_helper';
+import { ViewErrors } from './view_errors';
const FullHeightFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflowContent {
@@ -502,6 +503,11 @@ const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ actio
{displayByStatus[action.status].description}
+
+ {action.status === 'FAILED' && action.latestErrors && action.latestErrors.length > 0 ? (
+
+ ) : null}
+
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx
new file mode 100644
index 00000000000000..f907edd52e2b43
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 React from 'react';
+import { render } from '@testing-library/react';
+
+import { I18nProvider } from '@kbn/i18n-react';
+
+import type { ActionStatus } from '../../../../../../../common/types';
+
+import { ViewErrors } from './view_errors';
+
+jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
+ RedirectAppLinks: (props: any) => {
+ return
{props.children}
;
+ },
+}));
+
+jest.mock('../../../../hooks', () => {
+ return {
+ useStartServices: jest.fn().mockReturnValue({
+ http: {
+ basePath: {
+ prepend: jest.fn().mockImplementation((str) => 'http://localhost' + str),
+ },
+ },
+ }),
+ };
+});
+
+describe('ViewErrors', () => {
+ const renderComponent = (action: ActionStatus) => {
+ return render(
+
+
+
+ );
+ };
+
+ it('should render error message with btn to logs', () => {
+ const result = renderComponent({
+ actionId: 'action1',
+ latestErrors: [
+ {
+ agentId: 'agent1',
+ error: 'Agent agent1 is not upgradeable',
+ timestamp: '2023-03-06T14:51:24.709Z',
+ },
+ ],
+ } as any);
+
+ const errorText = result.getByTestId('errorText');
+ expect(errorText.textContent).toEqual('Agent agent1 is not upgradeable');
+
+ const viewErrorBtn = result.getByTestId('viewLogsBtn');
+ expect(viewErrorBtn.getAttribute('href')).toEqual(
+ `http://localhost/app/logs/stream?logPosition=(position%3A(time%3A1678114284709)%2CstreamLive%3A!f)&logFilter=(expression%3A'elastic_agent.id%3Aagent1%20and%20(data_stream.dataset%3Aelastic_agent)%20and%20(log.level%3Aerror)'%2Ckind%3Akuery)`
+ );
+ });
+});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx
new file mode 100644
index 00000000000000..d49be8d4cacda9
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx
@@ -0,0 +1,115 @@
+/*
+ * 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 { stringify } from 'querystring';
+
+import styled from 'styled-components';
+import React from 'react';
+import { encode } from '@kbn/rison';
+import type { EuiBasicTableProps } from '@elastic/eui';
+import { EuiButton, EuiAccordion, EuiToolTip, EuiText, EuiBasicTable } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
+
+import { i18n } from '@kbn/i18n';
+
+import type { ActionErrorResult } from '../../../../../../../common/types';
+
+import { buildQuery } from '../../agent_details_page/components/agent_logs/build_query';
+
+import type { ActionStatus } from '../../../../types';
+import { useStartServices } from '../../../../hooks';
+
+const TruncatedEuiText = styled(EuiText)`
+ overflow: hidden;
+ max-height: 3rem;
+ text-overflow: ellipsis;
+`;
+
+export const ViewErrors: React.FunctionComponent<{ action: ActionStatus }> = ({ action }) => {
+ const coreStart = useStartServices();
+
+ const logStreamQuery = (agentId: string) =>
+ buildQuery({
+ agentId,
+ datasets: ['elastic_agent'],
+ logLevels: ['error'],
+ userQuery: '',
+ });
+
+ const getErrorLogsUrl = (agentId: string, timestamp: string) => {
+ const queryParams = stringify({
+ logPosition: encode({
+ position: { time: Date.parse(timestamp) },
+ streamLive: false,
+ }),
+ logFilter: encode({
+ expression: logStreamQuery(agentId),
+ kind: 'kuery',
+ }),
+ });
+ return coreStart.http.basePath.prepend(`/app/logs/stream?${queryParams}`);
+ };
+
+ const columns: EuiBasicTableProps['columns'] = [
+ {
+ field: 'hostname',
+ name: i18n.translate('xpack.fleet.agentList.viewErrors.hostnameColumnTitle', {
+ defaultMessage: 'Host Name',
+ }),
+ render: (hostname: string) => (
+
+ {hostname}
+
+ ),
+ },
+ {
+ field: 'error',
+ name: i18n.translate('xpack.fleet.agentList.viewErrors.errorColumnTitle', {
+ defaultMessage: 'Error Message',
+ }),
+ render: (error: string) => (
+
+
+ {error}
+
+
+ ),
+ },
+ {
+ field: 'agentId',
+ name: i18n.translate('xpack.fleet.agentList.viewErrors.actionColumnTitle', {
+ defaultMessage: 'Action',
+ }),
+ render: (agentId: string) => {
+ const errorItem = (action.latestErrors ?? []).find((item) => item.agentId === agentId);
+ return (
+
+
+
+
+
+ );
+ },
+ },
+ ];
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts
index a1a86d5963f60e..0c31baab77d4ed 100644
--- a/x-pack/plugins/fleet/server/services/agents/action_status.ts
+++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts
@@ -9,8 +9,13 @@ import type { ElasticsearchClient } from '@kbn/core/server';
import { SO_SEARCH_LIMIT } from '../../constants';
-import type { FleetServerAgentAction, ActionStatus, ListWithKuery } from '../../types';
-import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common';
+import type {
+ FleetServerAgentAction,
+ ActionStatus,
+ ActionErrorResult,
+ ListWithKuery,
+} from '../../types';
+import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX, AGENTS_INDEX } from '../../../common';
import { appContextService } from '..';
const PRECISION_THRESHOLD = 40000;
@@ -20,7 +25,7 @@ const PRECISION_THRESHOLD = 40000;
*/
export async function getActionStatuses(
esClient: ElasticsearchClient,
- options: ListWithKuery
+ options: ListWithKuery & { errorSize: number }
): Promise {
const actions = await _getActions(esClient, options);
const cancelledActions = await getCancelledActions(esClient);
@@ -84,9 +89,10 @@ export async function getActionStatuses(
const cancelledAction = cancelledActions.find((a) => a.actionId === action.actionId);
let errorCount = 0;
+ let latestErrors: ActionErrorResult[] = [];
try {
// query to find errors in action results, cannot do aggregation on text type
- const res = await esClient.search({
+ const errorResults = await esClient.search({
index: AGENT_ACTIONS_RESULTS_INDEX,
track_total_hits: true,
rest_total_hits_as_int: true,
@@ -104,8 +110,41 @@ export async function getActionStatuses(
},
},
size: 0,
+ aggs: {
+ top_error_hits: {
+ top_hits: {
+ sort: [
+ {
+ '@timestamp': {
+ order: 'desc',
+ },
+ },
+ ],
+ _source: {
+ includes: ['@timestamp', 'agent_id', 'error'],
+ },
+ size: options.errorSize,
+ },
+ },
+ },
});
- errorCount = (res.hits.total as number) ?? 0;
+ errorCount = (errorResults.hits.total as number) ?? 0;
+ latestErrors = ((errorResults.aggregations?.top_error_hits as any)?.hits.hits ?? []).map(
+ (hit: any) => ({
+ agentId: hit._source.agent_id,
+ error: hit._source.error,
+ timestamp: hit._source['@timestamp'],
+ })
+ );
+ if (latestErrors.length > 0) {
+ const hostNames = await getHostNames(
+ esClient,
+ latestErrors.map((errorItem: ActionErrorResult) => errorItem.agentId)
+ );
+ latestErrors.forEach((errorItem: ActionErrorResult) => {
+ errorItem.hostname = hostNames[errorItem.agentId] ?? errorItem.agentId;
+ });
+ }
} catch (err) {
if (err.statusCode === 404) {
// .fleet-actions-results does not yet exist
@@ -129,12 +168,36 @@ export async function getActionStatuses(
nbAgentsActioned,
cancellationTime: cancelledAction?.timestamp,
completionTime,
+ latestErrors,
});
}
return results;
}
+async function getHostNames(esClient: ElasticsearchClient, agentIds: string[]) {
+ const agentsRes = await esClient.search({
+ index: AGENTS_INDEX,
+ query: {
+ bool: {
+ filter: {
+ terms: {
+ 'agent.id': agentIds,
+ },
+ },
+ },
+ },
+ size: agentIds.length,
+ _source: ['local_metadata.host.name'],
+ });
+ const hostNames = agentsRes.hits.hits.reduce((acc: { [key: string]: string }, curr) => {
+ acc[curr._id] = (curr._source as any).local_metadata.host.name;
+ return acc;
+ }, {});
+
+ return hostNames;
+}
+
export async function getCancelledActions(
esClient: ElasticsearchClient
): Promise> {
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index ef8b5c639c8ace..82447d7c2ed175 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -13,6 +13,7 @@ export type {
AgentType,
AgentAction,
ActionStatus,
+ ActionErrorResult,
CurrentUpgrade,
PackagePolicy,
PackagePolicyInput,
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts
index 598ea0fa67fe11..f549774884ab6f 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts
@@ -216,5 +216,6 @@ export const GetActionStatusRequestSchema = {
page: schema.number({ defaultValue: 0 }),
perPage: schema.number({ defaultValue: 20 }),
kuery: schema.maybe(schema.string()),
+ errorSize: schema.number({ defaultValue: 5 }),
}),
};
diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json
index 3cd28d6cb1f937..84f88ffea366f8 100644
--- a/x-pack/plugins/fleet/tsconfig.json
+++ b/x-pack/plugins/fleet/tsconfig.json
@@ -94,5 +94,6 @@
"@kbn/utils",
"@kbn/core-http-request-handler-context-server",
"@kbn/shared-ux-router",
+ "@kbn/shared-ux-link-redirect-app",
]
}
diff --git a/x-pack/test/fleet_api_integration/apis/agents/action_status.ts b/x-pack/test/fleet_api_integration/apis/agents/action_status.ts
index 84fb3244aa0443..dac5871e702db7 100644
--- a/x-pack/test/fleet_api_integration/apis/agents/action_status.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents/action_status.ts
@@ -196,6 +196,7 @@ export default function (providerContext: FtrProviderContext) {
creationTime: '2022-09-15T10:00:00.000Z',
nbAgentsFailed: 0,
hasRolloutPeriod: false,
+ latestErrors: [],
},
{
actionId: 'action3',
@@ -209,6 +210,7 @@ export default function (providerContext: FtrProviderContext) {
nbAgentsFailed: 0,
completionTime: '2022-09-15T12:00:00.000Z',
hasRolloutPeriod: false,
+ latestErrors: [],
},
{
actionId: 'action4',
@@ -221,6 +223,7 @@ export default function (providerContext: FtrProviderContext) {
creationTime: '2022-09-15T10:00:00.000Z',
nbAgentsFailed: 0,
hasRolloutPeriod: false,
+ latestErrors: [],
},
{
actionId: 'action5',
@@ -235,6 +238,7 @@ export default function (providerContext: FtrProviderContext) {
nbAgentsFailed: 0,
cancellationTime: '2022-09-15T11:00:00.000Z',
hasRolloutPeriod: false,
+ latestErrors: [],
},
{
actionId: 'action7',
@@ -249,6 +253,14 @@ export default function (providerContext: FtrProviderContext) {
nbAgentsFailed: 1,
completionTime: '2022-09-15T11:00:00.000Z',
hasRolloutPeriod: false,
+ latestErrors: [
+ {
+ agentId: 'agent1',
+ error: 'agent already assigned',
+ timestamp: '2022-09-15T11:00:00.000Z',
+ hostname: 'agent1',
+ },
+ ],
},
]);
});