diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index f35b6b3f7de6a..4af3f3beb32be 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -140,6 +140,8 @@ export const agentRouteService = { getBulkUpgradePath: () => AGENT_API_ROUTES.BULK_UPGRADE_PATTERN, getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, + getCreateActionPath: (agentId: string) => + AGENT_API_ROUTES.ACTIONS_PATTERN.replace('{agentId}', agentId), }; export const outputRoutesService = { diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts index 564e7b225cf45..7bbf621c57894 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts @@ -26,6 +26,8 @@ import { PostBulkAgentUpgradeRequest, PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse, + PostNewAgentActionRequest, + PostNewAgentActionResponse, } from '../../types'; type RequestOptions = Pick<Partial<UseRequestConfig>, 'pollIntervalMs'>; @@ -144,6 +146,19 @@ export function sendPostAgentUpgrade( }); } +export function sendPostAgentAction( + agentId: string, + body: PostNewAgentActionRequest['body'], + options?: RequestOptions +) { + return sendRequest<PostNewAgentActionResponse>({ + path: agentRouteService.getCreateActionPath(agentId), + method: 'post', + body, + ...options, + }); +} + export function sendPostBulkAgentUpgrade( body: PostBulkAgentUpgradeRequest['body'], options?: RequestOptions diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details.tsx index 5ce757734e637..1b6ad35cc6424 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details.tsx @@ -109,6 +109,17 @@ export const AgentDetailsContent: React.FunctionComponent<{ : 'stable' : '-', }, + { + title: i18n.translate('xpack.fleet.agentDetails.logLevel', { + defaultMessage: 'Log level', + }), + description: + typeof agent.local_metadata.elastic === 'object' && + typeof agent.local_metadata.elastic.agent === 'object' && + typeof agent.local_metadata.elastic.agent.log_level === 'string' + ? agent.local_metadata.elastic.agent.log_level + : '-', + }, { title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { defaultMessage: 'Platform', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx index b56e27356ef34..41069e7107862 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx @@ -24,3 +24,12 @@ export const DEFAULT_DATE_RANGE = { start: 'now-1d', end: 'now', }; + +export const AGENT_LOG_LEVELS = { + ERROR: 'error', + WARNING: 'warning', + INFO: 'info', + DEBUG: 'debug', +}; + +export const DEFAULT_LOG_LEVEL = AGENT_LOG_LEVELS.INFO; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index b034168dc8a15..a45831b2bbd2a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -6,8 +6,10 @@ import React, { memo, useState, useEffect } from 'react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { AGENT_LOG_LEVELS, AGENT_LOG_INDEX_PATTERN, LOG_LEVEL_FIELD } from './constants'; import { useStartServices } from '../../../../../hooks'; -import { AGENT_LOG_INDEX_PATTERN, LOG_LEVEL_FIELD } from './constants'; + +const LEVEL_VALUES = Object.values(AGENT_LOG_LEVELS); export const LogLevelFilter: React.FunctionComponent<{ selectedLevels: string[]; @@ -16,13 +18,13 @@ export const LogLevelFilter: React.FunctionComponent<{ const { data } = useStartServices(); const [isOpen, setIsOpen] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false); - const [levelValues, setLevelValues] = useState<string[]>([]); + const [levelValues, setLevelValues] = useState<string[]>(LEVEL_VALUES); useEffect(() => { const fetchValues = async () => { setIsLoading(true); try { - const values = await data.autocomplete.getValueSuggestions({ + const values: string[] = await data.autocomplete.getValueSuggestions({ indexPattern: { title: AGENT_LOG_INDEX_PATTERN, fields: [LOG_LEVEL_FIELD], @@ -30,7 +32,7 @@ export const LogLevelFilter: React.FunctionComponent<{ field: LOG_LEVEL_FIELD, query: '', }); - setLevelValues(values.sort()); + setLevelValues([...new Set([...LEVEL_VALUES, ...values.sort()])]); } catch (e) { setLevelValues([]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index e033781a850a0..bed857c073099 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -17,6 +17,8 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import semverGte from 'semver/functions/gte'; +import semverCoerce from 'semver/functions/coerce'; import { RedirectAppLinks } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { TimeRange, esKuery } from '../../../../../../../../../../../src/plugins/data/public'; import { LogStream } from '../../../../../../../../../infra/public'; @@ -27,6 +29,7 @@ import { DatasetFilter } from './filter_dataset'; import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; import { buildQuery } from './build_query'; +import { SelectLogLevel } from './select_log_level'; const WrapperFlexGroup = styled(EuiFlexGroup)` height: 100%; @@ -137,6 +140,18 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen [logStreamQuery, dateRange.endExpression, dateRange.startExpression, http.basePath] ); + const agentVersion = agent.local_metadata?.elastic?.agent?.version; + const isLogLevelSelectionAvailable = useMemo(() => { + if (!agentVersion) { + return false; + } + const agentVersionWithPrerelease = semverCoerce(agentVersion)?.version; + if (!agentVersionWithPrerelease) { + return false; + } + return semverGte(agentVersionWithPrerelease, '7.11.0'); + }, [agentVersion]); + return ( <WrapperFlexGroup direction="column" gutterSize="m"> <EuiFlexItem grow={false}> @@ -213,6 +228,11 @@ export const AgentLogs: React.FunctionComponent<{ agent: Agent }> = memo(({ agen /> </EuiPanel> </EuiFlexItem> + {isLogLevelSelectionAvailable && ( + <EuiFlexItem grow={false}> + <SelectLogLevel agent={agent} /> + </EuiFlexItem> + )} </WrapperFlexGroup> ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/select_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/select_log_level.tsx new file mode 100644 index 0000000000000..7879c969d644a --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/select_log_level.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSelect, EuiFormLabel, EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { Agent } from '../../../../../types'; +import { sendPostAgentAction, useStartServices } from '../../../../../hooks'; +import { AGENT_LOG_LEVELS, DEFAULT_LOG_LEVEL } from './constants'; + +const LEVEL_VALUES = Object.values(AGENT_LOG_LEVELS); + +export const SelectLogLevel: React.FC<{ agent: Agent }> = memo(({ agent }) => { + const { notifications } = useStartServices(); + const [isLoading, setIsLoading] = useState(false); + const [agentLogLevel, setAgentLogLevel] = useState( + agent.local_metadata?.elastic?.agent?.log_level ?? DEFAULT_LOG_LEVEL + ); + const [selectedLogLevel, setSelectedLogLevel] = useState(agentLogLevel); + + const onClickApply = useCallback(() => { + setIsLoading(true); + async function send() { + try { + const res = await sendPostAgentAction(agent.id, { + action: { + type: 'SETTINGS', + data: { + log_level: selectedLogLevel, + }, + }, + }); + if (res.error) { + throw res.error; + } + setAgentLogLevel(selectedLogLevel); + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.agentLogs.selectLogLevel.successText', { + defaultMessage: `Changed agent logging level to '{logLevel}'.`, + values: { + logLevel: selectedLogLevel, + }, + }) + ); + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.agentLogs.selectLogLevel.errorTitleText', { + defaultMessage: 'Error updating agent logging level', + }), + }); + } + setIsLoading(false); + } + + send(); + }, [notifications, selectedLogLevel, agent.id]); + + return ( + <EuiFlexGroup gutterSize="m" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiFormLabel htmlFor="selectAgentLogLevel"> + <FormattedMessage + id="xpack.fleet.agentLogs.selectLogLevelLabelText" + defaultMessage="Agent logging level" + /> + </EuiFormLabel> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSelect + disabled={isLoading} + compressed={true} + id="selectAgentLogLevel" + value={selectedLogLevel} + onChange={(event) => { + setSelectedLogLevel(event.target.value); + }} + options={LEVEL_VALUES.map((level) => ({ text: level, value: level }))} + /> + </EuiFlexItem> + {agentLogLevel !== selectedLogLevel && ( + <EuiFlexItem grow={false}> + <EuiButtonEmpty + flush="left" + size="xs" + isLoading={isLoading} + disabled={agentLogLevel === selectedLogLevel} + iconType="refresh" + onClick={onClickApply} + > + {isLoading ? ( + <FormattedMessage + id="xpack.fleet.agentLogs.updateButtonLoadingText" + defaultMessage="Applying changes..." + /> + ) : ( + <FormattedMessage + id="xpack.fleet.agentLogs.updateButtonText" + defaultMessage="Apply changes" + /> + )} + </EuiButtonEmpty> + </EuiFlexItem> + )} + </EuiFlexGroup> + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 78cb355318d40..ded1447954aff 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -67,6 +67,8 @@ export { PutAgentReassignResponse, PostBulkAgentReassignRequest, PostBulkAgentReassignResponse, + PostNewAgentActionResponse, + PostNewAgentActionRequest, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest,