[UII] Allow to reset log level for agents >= 8.15.0 (#183434)

‼️ Should be reverted if
https://github.com/elastic/elastic-agent/issues/4747 does not make
8.15.0.

## Summary

Resolves #180778 

This PR allows agent log level to be reset back to the level set on its
policy (or if not set, simply the default agent level, see
https://github.com/elastic/elastic-agent/pull/3090).

To achieve this, this PR:
- Allows `null` to be passed for the log level settings action, i.e.:

```
POST kbn:/api/fleet/agents/<AGENT_ID>/actions
{"action":{"type":"SETTINGS","data":{"log_level": null}}}
```
- Enables the agent policy log level setting implemented in
https://github.com/elastic/kibana/pull/180607
- Always show `Apply changes` on the agent details > Logs tab
- For agents >= 8.15.0, always show `Reset to policy` on the agent
details > Logs tab
- Ensures both buttons are disabled if user does not have access to
write to agents

<img width="1254" alt="image"
src="bcdf763e-2053-4071-9aa8-8bcb57b8fee1">

<img width="1267" alt="image"
src="182ac54d-d5ad-435f-9376-70bb24f288f9">

### Caveats
1. The reported agent log level is not accurate if agent is using the
level from its policy and does not have a log level set on its own level
(https://github.com/elastic/elastic-agent/issues/4747), so the initial
selection on the agent log level could be wrong
2. We have no way to tell where the log level came from
(https://github.com/elastic/elastic-agent/issues/4748), so that's why
`Apply changes` and `Reset to policy` are always shown

### Testing
Use the latest `8.15.0-SNAPSHOT` for agents or fleet server to test this
change

### Checklist

Delete any items that are not applicable to this PR.

- [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
This commit is contained in:
Jen Huang 2024-05-16 08:41:31 -07:00 committed by GitHub
parent b2118427f1
commit 4c80f262db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 244 additions and 121 deletions

View file

@ -39,17 +39,10 @@ export const DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT = 750;
export const AGENTLESS_POLICY_ID = 'agentless'; // the policy id defined here: https://github.com/elastic/project-controller/blob/main/internal/project/security/security_kibana_config.go#L86
export const AGENT_LOG_LEVELS = {
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
DEBUG: 'debug',
info: 'info',
debug: 'debug',
warning: 'warning',
error: 'error',
};
export const DEFAULT_LOG_LEVEL = AGENT_LOG_LEVELS.INFO;
export const agentLoggingLevels = {
Info: 'info',
Debug: 'debug',
Warning: 'warning',
Error: 'error',
} as const;
export const DEFAULT_LOG_LEVEL = AGENT_LOG_LEVELS.info;

View file

@ -7294,6 +7294,7 @@
"properties": {
"log_level": {
"type": "string",
"nullable": true,
"enum": [
"debug",
"info",

View file

@ -4674,6 +4674,7 @@ components:
properties:
log_level:
type: string
nullable: true
enum:
- debug
- info

View file

@ -19,6 +19,7 @@ oneOf:
properties:
log_level:
type: string
nullable: true
enum:
- debug
- info

View file

@ -4,11 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCode } from '@elastic/eui';
import { z } from 'zod';
import { agentLoggingLevels } from '../constants';
import { AGENT_LOG_LEVELS, DEFAULT_LOG_LEVEL } from '../constants';
import type { SettingsConfig } from './types';
@ -130,20 +133,19 @@ export const AGENT_POLICY_ADVANCED_SETTINGS: SettingsConfig[] = [
},
{
name: 'agent.logging.level',
hidden: true,
title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.agentLoggingLevelTitle', {
defaultMessage: 'Agent logging level',
}),
description: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingLevelDescription',
{
defaultMessage:
'Sets the log level for all the agents on the policy. The default log level is "info".',
}
description: (
<FormattedMessage
id="xpack.fleet.settings.agentPolicyAdvanced.agentLoggingLevelDescription"
defaultMessage="Sets the log level for all the agents on the policy. The default log level is {level}."
values={{ level: <EuiCode>{DEFAULT_LOG_LEVEL}</EuiCode> }}
/>
),
api_field: {
name: 'agent_logging_level',
},
schema: z.nativeEnum(agentLoggingLevels).default(agentLoggingLevels.Info),
schema: z.nativeEnum(AGENT_LOG_LEVELS).default(DEFAULT_LOG_LEVEL),
},
];

View file

@ -65,11 +65,15 @@ describe('AgentLogsUI', () => {
},
} as any);
});
const renderComponent = () => {
const renderComponent = (
opts = {
agentVersion: '8.11.0',
}
) => {
const renderer = createFleetTestRendererMock();
const agent = {
id: 'agent1',
local_metadata: { elastic: { agent: { version: '8.11' } } },
local_metadata: { elastic: { agent: { version: opts.agentVersion, log_level: 'debug' } } },
} as any;
const state = {
datasets: ['elastic_agent'],
@ -125,4 +129,35 @@ describe('AgentLogsUI', () => {
`http://localhost:5620/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2023-20-04T14:00:00.340Z',to:'2023-20-04T14:20:00.340Z'))&_a=(columns:!(event.dataset,message),index:'logs-*',query:(language:kuery,query:'elastic_agent.id:agent1 and (data_stream.dataset:elastic_agent) and (log.level:info or log.level:error)'))`
);
});
it('should show log level dropdown with correct value', () => {
mockStartServices();
const result = renderComponent();
const logLevelDropdown = result.getByTestId('selectAgentLogLevel');
expect(logLevelDropdown.getElementsByTagName('option').length).toBe(4);
expect(logLevelDropdown).toHaveDisplayValue('debug');
});
it('should always show apply log level changes button', () => {
mockStartServices();
const result = renderComponent();
const applyLogLevelBtn = result.getByTestId('applyLogLevelBtn');
expect(applyLogLevelBtn).toBeInTheDocument();
expect(applyLogLevelBtn).not.toHaveAttribute('disabled');
});
it('should hide reset log level button for agents version < 8.15.0', () => {
mockStartServices();
const result = renderComponent();
const resetLogLevelBtn = result.queryByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).not.toBeInTheDocument();
});
it('should show reset log level button for agents version >= 8.15.0', () => {
mockStartServices();
const result = renderComponent({ agentVersion: '8.15.0' });
const resetLogLevelBtn = result.getByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).toBeInTheDocument();
expect(resetLogLevelBtn).not.toHaveAttribute('disabled');
});
});

View file

@ -342,7 +342,10 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SelectLogLevel agent={agent} />
<SelectLogLevel
agent={agent}
agentPolicyLogLevel={agentPolicy?.advanced_settings?.agent_logging_level}
/>
</EuiFlexItem>
</WrapperFlexGroup>
);

View file

@ -9,106 +9,175 @@ 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 semverGte from 'semver/functions/gte';
import type { Agent } from '../../../../../types';
import { sendPostAgentAction, useAuthz, useStartServices } from '../../../../../hooks';
import { AGENT_LOG_LEVELS, DEFAULT_LOG_LEVEL } from '../../../../../../../../common/constants';
const LEVEL_VALUES = Object.values(AGENT_LOG_LEVELS);
export const SelectLogLevel: React.FC<{ agent: Agent; agentPolicyLogLevel?: string }> = memo(
({ agent, agentPolicyLogLevel = DEFAULT_LOG_LEVEL }) => {
const authz = useAuthz();
const { notifications } = useStartServices();
const [isSetLevelLoading, setIsSetLevelLoading] = useState(false);
const [isResetLevelLoading, setIsResetLevelLoading] = useState(false);
const canResetLogLevel = semverGte(
agent.local_metadata?.elastic?.agent?.version,
'8.15.0',
true
);
export const SelectLogLevel: React.FC<{ agent: Agent }> = memo(({ agent }) => {
const authz = useAuthz();
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 [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,
const resetLogLevel = useCallback(() => {
setIsResetLevelLoading(true);
async function send() {
try {
const res = await sendPostAgentAction(agent.id, {
action: {
type: 'SETTINGS',
data: {
log_level: null,
},
},
},
});
if (res.error) {
throw res.error;
});
if (res.error) {
throw res.error;
}
// TODO: reset to an empty state?
setAgentLogLevel(agentPolicyLogLevel);
setSelectedLogLevel(agentPolicyLogLevel);
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.agentLogs.resetLogLevel.successText', {
defaultMessage: `Reset agent logging level to policy`,
})
);
} catch (error) {
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.agentLogs.resetLogLevel.errorTitleText', {
defaultMessage: 'Error resetting agent logging level',
}),
});
}
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',
}),
});
setIsResetLevelLoading(false);
}
setIsLoading(false);
}
send();
}, [notifications, selectedLogLevel, agent.id]);
send();
}, [agent.id, agentPolicyLogLevel, notifications]);
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 || !authz.fleet.allAgents}
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 ? (
const onClickApply = useCallback(() => {
setIsSetLevelLoading(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',
}),
});
}
setIsSetLevelLoading(false);
}
send();
}, [notifications, selectedLogLevel, agent.id]);
return (
<>
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFormLabel htmlFor="selectAgentLogLevel">
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonLoadingText"
defaultMessage="Applying changes..."
id="xpack.fleet.agentLogs.selectLogLevelLabelText"
defaultMessage="Agent logging level"
/>
) : (
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonText"
defaultMessage="Apply changes"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
});
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSelect
disabled={isSetLevelLoading || !authz.fleet.allAgents}
compressed={true}
id="selectAgentLogLevel"
data-test-subj="selectAgentLogLevel"
value={selectedLogLevel}
onChange={(event) => {
setSelectedLogLevel(event.target.value);
}}
options={Object.entries(AGENT_LOG_LEVELS).map(([key, value]) => ({
value,
text: key,
}))}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="both"
size="xs"
disabled={!authz.fleet.allAgents}
isLoading={isSetLevelLoading || isResetLevelLoading}
iconType="check"
onClick={onClickApply}
data-test-subj="applyLogLevelBtn"
>
{isSetLevelLoading ? (
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonLoadingText"
defaultMessage="Applying changes..."
/>
) : (
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonText"
defaultMessage="Apply changes"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
{canResetLogLevel && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="both"
size="xs"
disabled={!authz.fleet.allAgents}
isLoading={isSetLevelLoading || isResetLevelLoading}
iconType="cross"
onClick={resetLogLevel}
data-test-subj="resetLogLevelBtn"
>
<FormattedMessage
id="xpack.fleet.agentLogs.resetLogLevelLabelText"
defaultMessage="Reset to policy"
/>
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
</>
);
}
);

View file

@ -32,12 +32,14 @@ export const NewAgentActionSchema = schema.oneOf([
schema.object({
type: schema.oneOf([schema.literal('SETTINGS')]),
data: schema.object({
log_level: schema.oneOf([
schema.literal('debug'),
schema.literal('info'),
schema.literal('warning'),
schema.literal('error'),
]),
log_level: schema.nullable(
schema.oneOf([
schema.literal('debug'),
schema.literal('info'),
schema.literal('warning'),
schema.literal('error'),
])
),
}),
}),
]);

View file

@ -26,7 +26,7 @@ export default function (providerContext: FtrProviderContext) {
});
describe('POST /agents/{agentId}/actions', () => {
it('should return a 200 if this a valid SETTINGS action request', async () => {
it('should return a 200 if this a SETTINGS action request with a valid log level', async () => {
const { body: apiResponse } = await supertest
.post(`/api/fleet/agents/agent1/actions`)
.set('kbn-xsrf', 'xx')
@ -42,7 +42,23 @@ export default function (providerContext: FtrProviderContext) {
expect(apiResponse.item.data).to.eql({ log_level: 'debug' });
});
it('should return a 400 if this a invalid SETTINGS action request', async () => {
it('should return a 200 if this a SETTINGS action request with null log level', async () => {
const { body: apiResponse } = await supertest
.post(`/api/fleet/agents/agent1/actions`)
.set('kbn-xsrf', 'xx')
.send({
action: {
type: 'SETTINGS',
data: { log_level: null },
},
})
.expect(200);
expect(apiResponse.item.type).to.eql('SETTINGS');
expect(apiResponse.item.data).to.eql({ log_level: null });
});
it('should return a 400 if this a SETTINGS action request with an invalid log level', async () => {
const { body: apiResponse } = await supertest
.post(`/api/fleet/agents/agent1/actions`)
.set('kbn-xsrf', 'xx')