Skip to content

Commit

Permalink
[Fleet] Display errors in Agent activity with link to Logs (elastic#1…
Browse files Browse the repository at this point in the history
…52583)

## Summary

Improvement of Agent activity to show action errors with a link to
`Review error logs`

Part of elastic#141206

Extended `action_status` API to return latest errors, these are the most
recent docs from `.fleet-action-results` that require errors.
We could do something more clever like aggregate the most frequent
errors and take the top hits from each bucket if that's a desirable
feature to group the same errors together.

To verify:
- Enroll agents (with horde/normally)
- Trigger some actions with failures (e.g. upgrade agents that are not
upgradeable, change artifact repo to an invalid url)
- Go to Agent Activity and click on `Show errors` under the failed
actions.
- The last 3 errors will be shown, with buttons to `Review error log`.
These are distinct errors per agent id.
- Click on `Review error log`, verify that the `Logs UI` shows the
expected filters (see
[here](elastic#152583 (comment)))

```
GET kbn:/api/fleet/agents/action_status

    {
      "actionId": "3de4a573-011b-4c8c-9ccb-c6516bcc27d2",
      "nbAgentsActionCreated": 1,
      "nbAgentsAck": 0,
      "version": "8.6.1",
      "startTime": "2023-02-28T16:34:10.553Z",
      "type": "UPGRADE",
      "nbAgentsActioned": 102,
      "status": "FAILED",
      "expiration": "2023-02-28T16:54:10.553Z",
      "creationTime": "2023-02-28T16:34:50.352Z",
      "nbAgentsFailed": 102,
      "hasRolloutPeriod": true,
      "completionTime": "2023-02-28T16:39:28.000Z",
      "latestErrors": [
        {
          "agentId": "906560bc-2af4-4916-8261-3769e8c38931",
          "error": """failed verification of agent binary: 2 errors occurred:
	* fetching asc file from '/Library/Elastic/Agent/data/elastic-agent-496e7e/downloads/elastic-agent-8.6.1-darwin-x86_64.tar.gz.asc': open /Library/Elastic/Agent/data/elastic-agent-496e7e/downloads/elastic-agent-8.6.1-darwin-x86_64.tar.gz.asc: no such file or directory
	* invalid signature for /Library/Elastic/Agent/data/elastic-agent-496e7e/downloads/elastic-agent-8.6.1-darwin-x86_64.tar.gz: openpgp: invalid signature: hash tag doesn't match

""",
          "timestamp": "2023-02-28T16:39:28Z",
          "hostname": "Julias-MacBook-Pro.local"
        },
        {
          "agentId": "080bf24f-f3ac-4256-b525-41d5bec1514e",
          "error": "Agent 080bf24f-f3ac-4256-b525-41d5bec1514e is not upgradeable",
          "timestamp": "2023-02-28T16:34:50.715Z",
          "hostname": "Julias-MacBook-Pro.local"
        },
        {
          "agentId": "6c6cbc39-5214-4001-928d-374bfed8ef1d",
          "error": "Agent 6c6cbc39-5214-4001-928d-374bfed8ef1d is not upgradeable",
          "timestamp": "2023-02-28T16:34:50.715Z",
          "hostname": "Julias-MacBook-Pro.local"
        }
      ]
    },
```

Added an accordion on the UI to show error messages with a link to Logs.
In the design there was only one `Review error logs` button per action,
I thought it is better to drill down to a specific agent id, we could do
either/both.
See reasoning here
elastic#141206 (comment)

Latest styling, included host name on UI after feedback from Nima:
<img width="577" alt="image"
src="https://user-images.githubusercontent.com/90178898/223428882-bfecf2fe-0b71-4c7e-8359-8110c74eb6a0.png">

<img width="1769" alt="image"
src="https://user-images.githubusercontent.com/90178898/222465434-99170fbe-441b-48f0-b585-dbf18e0e8e9b.png">




### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and bmorelli25 committed Mar 10, 2023
1 parent 9b27eed commit 2f1eb2c
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 6 deletions.
29 changes: 28 additions & 1 deletion x-pack/plugins/fleet/common/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,14 @@
},
{
"$ref": "#/components/parameters/page_index"
},
{
"schema": {
"type": "integer",
"default": 5
},
"in": "query",
"name": "errorSize"
}
],
"responses": {
Expand All @@ -1730,7 +1738,8 @@
"EXPIRED",
"CANCELLED",
"FAILED",
"IN_PROGRESS"
"IN_PROGRESS",
"ROLLOUT_PASSED"
]
},
"nbAgentsActioned": {
Expand Down Expand Up @@ -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": [
Expand Down
20 changes: 20 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1086,6 +1091,7 @@ paths:
- CANCELLED
- FAILED
- IN_PROGRESS
- ROLLOUT_PASSED
nbAgentsActioned:
type: number
nbAgentsActionCreated:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +33,7 @@ get:
- CANCELLED
- FAILED
- IN_PROGRESS
- ROLLOUT_PASSED
nbAgentsActioned:
type: number
nbAgentsActionCreated:
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/fleet/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -155,6 +162,7 @@ export interface ActionStatus {
newPolicyId?: string;
creationTime: string;
hasRolloutPeriod?: boolean;
latestErrors?: ActionErrorResult[];
}

export interface AgentDiagnostics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -502,6 +503,11 @@ const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ actio
{displayByStatus[action.status].description}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{action.status === 'FAILED' && action.latestErrors && action.latestErrors.length > 0 ? (
<ViewErrors action={action} />
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <div>{props.children}</div>;
},
}));

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(
<I18nProvider>
<ViewErrors action={action} />
</I18nProvider>
);
};

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)`
);
});
});
Original file line number Diff line number Diff line change
@@ -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<ActionErrorResult>['columns'] = [
{
field: 'hostname',
name: i18n.translate('xpack.fleet.agentList.viewErrors.hostnameColumnTitle', {
defaultMessage: 'Host Name',
}),
render: (hostname: string) => (
<EuiText size="s" data-test-subj="hostText">
{hostname}
</EuiText>
),
},
{
field: 'error',
name: i18n.translate('xpack.fleet.agentList.viewErrors.errorColumnTitle', {
defaultMessage: 'Error Message',
}),
render: (error: string) => (
<EuiToolTip content={error}>
<TruncatedEuiText size="s" color="red" data-test-subj="errorText">
{error}
</TruncatedEuiText>
</EuiToolTip>
),
},
{
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 (
<RedirectAppLinks coreStart={coreStart}>
<EuiButton
href={getErrorLogsUrl(agentId, errorItem!.timestamp)}
color="danger"
data-test-subj="viewLogsBtn"
>
<FormattedMessage
id="xpack.fleet.agentActivityFlyout.reviewErrorLogs"
defaultMessage="Review error logs"
/>
</EuiButton>
</RedirectAppLinks>
);
},
},
];

return (
<>
<EuiAccordion id={action.actionId + '_errors'} buttonContent="Show errors">
<EuiBasicTable items={action.latestErrors ?? []} columns={columns} tableLayout="auto" />
</EuiAccordion>
</>
);
};
Loading

0 comments on commit 2f1eb2c

Please sign in to comment.