diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index fb7c51376278..e530ca152add 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -163,6 +163,7 @@ export interface AgentDiagnostics { filePath: string; status: 'READY' | 'AWAITING_UPLOAD' | 'DELETED' | 'IN_PROGRESS' | 'FAILED'; actionId: string; + error?: string; } // Generated from FleetServer schema.json diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx index b0a588a1d12c..2c38fdfba536 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx @@ -37,6 +37,10 @@ const FlexStartEuiFlexItem = styled(EuiFlexItem)` align-self: flex-start; `; +const MarginedIcon = styled(EuiIcon)` + margin-right: 7px; +`; + export interface AgentDiagnosticsProps { agent: Agent; } @@ -48,6 +52,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent const [isLoading, setIsLoading] = useState(true); const [diagnosticsEntries, setDiagnosticEntries] = useState([]); const [prevDiagnosticsEntries, setPrevDiagnosticEntries] = useState([]); + const [loadInterval, setLoadInterval] = useState(10000); const loadData = useCallback(async () => { try { @@ -59,8 +64,20 @@ export const AgentDiagnosticsTab: React.FunctionComponent if (!uploadsResponse.data) { throw new Error('No data'); } - setDiagnosticEntries(uploadsResponse.data.items); + const entries = uploadsResponse.data.items; + setDiagnosticEntries(entries); setIsLoading(false); + + // query faster if an action is in progress, for quicker feedback + if ( + entries.some( + (entry) => entry.status === 'IN_PROGRESS' || entry.status === 'AWAITING_UPLOAD' + ) + ) { + setLoadInterval(3000); + } else { + setLoadInterval(10000); + } } catch (err) { notifications.toasts.addError(err, { title: i18n.translate( @@ -71,13 +88,13 @@ export const AgentDiagnosticsTab: React.FunctionComponent ), }); } - }, [agent.id, notifications.toasts]); + }, [agent.id, notifications.toasts, setLoadInterval]); useEffect(() => { loadData(); const interval: ReturnType | null = setInterval(async () => { loadData(); - }, 10000); + }, loadInterval); const cleanup = () => { if (interval) { @@ -86,7 +103,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent }; return cleanup; - }, [loadData]); + }, [loadData, loadInterval]); useEffect(() => { setPrevDiagnosticEntries(diagnosticsEntries); @@ -112,6 +129,9 @@ export const AgentDiagnosticsTab: React.FunctionComponent } }, [prevDiagnosticsEntries, diagnosticsEntries, notifications.toasts]); + const errorIcon = ; + const getErrorMessage = (error?: string) => (error ? `Error: ${error}` : ''); + const columns: Array> = [ { field: 'id', @@ -123,21 +143,32 @@ export const AgentDiagnosticsTab: React.FunctionComponent   {currentItem?.name} ) : currentItem?.status === 'IN_PROGRESS' || currentItem?.status === 'AWAITING_UPLOAD' ? ( - +   - + ) : ( - - - - + + {currentItem?.status ? ( + +

Diagnostics status: {currentItem?.status}

+

{getErrorMessage(currentItem?.error)}

+ + } + > + {errorIcon} +
+ ) : ( + errorIcon + )}   {currentItem?.name} -
+ ); }, }, @@ -149,7 +180,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent const currentItem = diagnosticsEntries.find((item) => item.id === id); return ( - {formatDate(currentItem?.createTime, 'll')} + {formatDate(currentItem?.createTime, 'lll')} ); }, @@ -171,6 +202,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent } ); notifications.toasts.addSuccess(successMessage); + loadData(); } catch (error) { setIsSubmitting(false); notifications.toasts.addError(error, { diff --git a/x-pack/plugins/fleet/server/services/agents/uploads.ts b/x-pack/plugins/fleet/server/services/agents/uploads.ts index 7402eedc840e..3683b9b0d90a 100644 --- a/x-pack/plugins/fleet/server/services/agents/uploads.ts +++ b/x-pack/plugins/fleet/server/services/agents/uploads.ts @@ -31,21 +31,24 @@ export async function getAgentUploads( esClient: ElasticsearchClient, agentId: string ): Promise { - const getFile = async (fileId: string) => { - if (!fileId) return; + const getFile = async (actionId: string) => { try { const fileResponse = await esClient.search({ index: FILE_STORAGE_METADATA_AGENT_INDEX, query: { bool: { filter: { - term: { upload_id: fileId }, + bool: { + must: [{ term: { agent_id: agentId } }, { term: { action_id: actionId } }], + }, }, }, }, }); - if (fileResponse.hits.total === 0) { - appContextService.getLogger().debug(`No matches for upload_id ${fileId}`); + if (fileResponse.hits.hits.length === 0) { + appContextService + .getLogger() + .debug(`No matches for action_id ${actionId} and agent_id ${agentId}`); return; } return { @@ -64,10 +67,14 @@ export async function getAgentUploads( const actions = await _getRequestDiagnosticsActions(esClient, agentId); - const results = []; + const results: AgentDiagnostics[] = []; for (const action of actions) { - const file = action.fileId ? await getFile(action.fileId) : undefined; - const fileName = file?.name ?? `${moment(action.timestamp!).format('YYYY-MM-DD HH:mm:ss')}.zip`; + const file = await getFile(action.actionId); + const fileName = + file?.name ?? + `elastic-agent-diagnostics-${moment + .utc(action.timestamp!) + .format('YYYY-MM-DDTHH-mm-ss')}Z-00.zip`; const filePath = file ? agentRouteService.getAgentFileDownloadLink(file.id, file.name) : ''; const result = { actionId: action.actionId, @@ -76,6 +83,7 @@ export async function getAgentUploads( name: fileName, createTime: action.timestamp!, filePath, + error: action.error, }; results.push(result); } @@ -91,6 +99,7 @@ async function _getRequestDiagnosticsActions( index: AGENT_ACTIONS_INDEX, ignore_unavailable: true, size: SO_SEARCH_LIMIT, + sort: { '@timestamp': 'desc' }, query: { bool: { must: [ @@ -150,7 +159,7 @@ async function _getRequestDiagnosticsActions( const actionResult = actionResults.find((result) => result.actionId === action.actionId); return { actionId: action.actionId, - timestamp: actionResult?.timestamp ?? action.timestamp, + timestamp: action.timestamp, fileId: actionResult?.fileId, error: actionResult?.error, }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/uploads.ts b/x-pack/test/fleet_api_integration/apis/agents/uploads.ts index c0f32104d24f..6f548d3d955d 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/uploads.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/uploads.ts @@ -68,6 +68,8 @@ export default function (providerContext: FtrProviderContext) { doc_as_upsert: true, doc: { upload_id: 'file1', + action_id: 'action1', + agent_id: 'agent1', file: { ChunkSize: 4194304, extension: 'zip', @@ -96,7 +98,7 @@ export default function (providerContext: FtrProviderContext) { expect(body.items[0]).to.eql({ actionId: 'action1', - createTime: '2022-10-07T12:00:00.000Z', + createTime: '2022-10-07T11:00:00.000Z', filePath: '/api/fleet/agents/files/file1/elastic-agent-diagnostics-2022-10-07T12-00-00Z-00.zip', id: 'file1', @@ -130,5 +132,49 @@ export default function (providerContext: FtrProviderContext) { 'attachment; filename="elastic-agent-diagnostics-2022-10-07T12-00-00Z-00.zip"' ); }); + + it('should return failed status with error message', async () => { + await esClient.create({ + index: AGENT_ACTIONS_INDEX, + id: new Date().toISOString(), + refresh: true, + body: { + type: 'REQUEST_DIAGNOSTICS', + action_id: 'action2', + agents: ['agent2'], + '@timestamp': '2022-10-07T11:00:00.000Z', + }, + }); + await esClient.create( + { + index: AGENT_ACTIONS_RESULTS_INDEX, + id: new Date().toISOString(), + refresh: true, + body: { + action_id: 'action2', + agent_id: 'agent2', + '@timestamp': '2022-10-07T12:00:00.000Z', + data: {}, + error: 'rate limit exceeded', + }, + }, + ES_INDEX_OPTIONS + ); + + const { body } = await supertest + .get(`/api/fleet/agents/agent2/uploads`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body.items[0]).to.eql({ + actionId: 'action2', + createTime: '2022-10-07T11:00:00.000Z', + filePath: '', + id: 'action2', + name: 'elastic-agent-diagnostics-2022-10-07T11-00-00Z-00.zip', + status: 'FAILED', + error: 'rate limit exceeded', + }); + }); }); }