[Logs onboarding] Error messages for progress steps (#161599)

This commit is contained in:
Oliver Gupte 2023-07-17 05:48:04 -07:00 committed by GitHub
parent f9adf3bbf7
commit 0d3629f9f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 208 additions and 82 deletions

View file

@ -53,12 +53,13 @@ artifact=elastic-agent-${ELASTIC_AGENT_VERSION}-${os}-${arch}
updateStepProgress() {
local STEPNAME="$1"
local STATUS="$2" # "incomplete" | "complete" | "disabled" | "loading" | "warning" | "danger" | "current"
local MESSAGE=${3:-}
curl --request POST \
--url "${API_ENDPOINT}/custom_logs/${ONBOARDING_ID}/step/${STEPNAME}" \
--header "Authorization: ApiKey ${API_KEY_ENCODED}" \
--header "Content-Type: application/json" \
--header "kbn-xsrf: true" \
--data "{\"status\":\"${STATUS}\"}" \
--data "{\"status\":\"${STATUS}\", \"message\":\"${MESSAGE}\"}" \
--output /dev/null \
--no-progress-meter
}
@ -71,19 +72,18 @@ if [ "$?" -eq 0 ]; then
echo "Downloaded Elastic Agent"
updateStepProgress "ea-download" "complete"
else
updateStepProgress "ea-download" "danger"
updateStepProgress "ea-download" "danger" "Failed to download Elastic Agent, see script output for error."
fail "Failed to download Elastic Agent"
fi
echo "Extracting Elastic Agent"
updateStepProgress "ea-extract" "loading"
tar -xzf ${artifact}.tar.gz
echo ""
if [ "$?" -eq 0 ]; then
echo "Elastic Agent extracted"
updateStepProgress "ea-extract" "complete"
else
updateStepProgress "ea-extract" "danger"
updateStepProgress "ea-extract" "danger" "Failed to extract Elastic Agent, see script output for error."
fail "Failed to extract Elastic Agent"
fi
@ -95,7 +95,7 @@ if [ "$?" -eq 0 ]; then
echo "Elastic Agent installed"
updateStepProgress "ea-install" "complete"
else
updateStepProgress "ea-install" "danger"
updateStepProgress "ea-install" "danger" "Failed to install Elastic Agent, see script output for error."
fail "Failed to install Elastic Agent"
fi
@ -120,8 +120,7 @@ echo "Checking Elastic Agent status"
updateStepProgress "ea-status" "loading"
waitForElasticAgentStatus
if [ "$?" -ne 0 ]; then
updateStepProgress "ea-status" "warning"
exit 1
updateStepProgress "ea-status" "warning" "Unable to determine agent status"
fi
ELASTIC_AGENT_STATE="$(elastic-agent status | grep -m1 State | sed 's/State: //')"
ELASTIC_AGENT_MESSAGE="$(elastic-agent status | grep -m1 Message | sed 's/Message: //')"
@ -130,8 +129,7 @@ if [ "${ELASTIC_AGENT_STATE}" = "HEALTHY" ] && [ "${ELASTIC_AGENT_MESSAGE}" = "R
echo "Download and save configuration to /opt/Elastic/Agent/elastic-agent.yml"
updateStepProgress "ea-status" "complete"
else
updateStepProgress "ea-status" "warning"
exit 1
updateStepProgress "ea-status" "warning" "Expected agent status HEALTHY / Running but got ${ELASTIC_AGENT_STATE} / ${ELASTIC_AGENT_MESSAGE}"
fi
downloadElasticAgentConfig() {
@ -149,12 +147,12 @@ downloadElasticAgentConfig() {
echo "Downloaded elastic-agent.yml"
updateStepProgress "ea-config" "complete"
else
updateStepProgress "ea-config" "warning"
updateStepProgress "ea-config" "warning" "Failed to write elastic-agent.yml on host automatically, try manually setting the configuration"
fail "Failed to download elastic-agent.yml"
fi
}
if [ "${AUTO_DOWNLOAD_CONFIG}" == *"autoDownloadConfig=1"* ]; then
if [ "${AUTO_DOWNLOAD_CONFIG}" == "autoDownloadConfig=1" ]; then
downloadElasticAgentConfig
echo "Done with standalone Elastic Agent setup for custom logs. Look for streaming logs to arrive in Kibana"
else

View file

@ -14,14 +14,11 @@ import {
EuiFlexItem,
EuiSkeletonRectangle,
EuiSpacer,
EuiStep,
EuiSteps,
EuiStepsProps,
EuiSubSteps,
EuiSwitch,
EuiText,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { Buffer } from 'buffer';
import { flatten, zip } from 'lodash';
@ -36,6 +33,7 @@ import {
} from '../../../shared/step_panel';
import { ApiKeyBanner } from './api_key_banner';
import { BackButton } from './back_button';
import { StepStatus } from './step_status';
type ElasticAgentPlatform = 'linux-tar' | 'macos' | 'windows';
export function InstallElasticAgent() {
@ -198,9 +196,8 @@ export function InstallElasticAgent() {
({ id, incompleteTitle, loadingTitle, completedTitle }) => {
const progress = progressData?.progress;
if (progress) {
const stepStatus = progress[
id
] as EuiStepsProps['steps'][number]['status'];
const stepStatus = progress?.[id]
?.status as EuiStepsProps['steps'][number]['status'];
const title =
stepStatus === 'loading'
? loadingTitle
@ -211,6 +208,7 @@ export function InstallElasticAgent() {
title,
children: null,
status: stepStatus ?? ('incomplete' as const),
message: progress?.[id]?.message,
};
}
return {
@ -223,7 +221,8 @@ export function InstallElasticAgent() {
);
const isInstallStarted = progressData?.progress['ea-download'] !== undefined;
const isInstallCompleted = progressData?.progress['ea-status'] === 'complete';
const isInstallCompleted =
progressData?.progress?.['ea-status']?.status === 'complete';
const autoDownloadConfigStep = getStep({
id: 'ea-config',
@ -404,7 +403,7 @@ export function InstallElasticAgent() {
{isInstallStarted && (
<>
<EuiSpacer size="m" />
<EuiSubSteps>
<EuiFlexGroup direction="column" gutterSize="m">
{[
{
id: 'ea-download',
@ -473,36 +472,26 @@ export function InstallElasticAgent() {
),
},
].map((step, index) => {
const { title, status } = getStep(step);
const { title, status, message } = getStep(step);
return (
<EuiStep
key={step.id}
titleSize="xs"
step={index + 1}
title={title}
<StepStatus
status={status}
children={null}
css={css({
'> .euiStep__content': {
paddingBottom: 0,
},
})}
title={title}
message={message}
/>
);
})}
</EuiSubSteps>
</EuiFlexGroup>
</>
)}
</>
),
},
{
title: wizardState.autoDownloadConfig
? autoDownloadConfigStep.title
: i18n.translate(
'xpack.observability_onboarding.installElasticAgent.progress.eaConfig.incompleteTitle',
{ defaultMessage: 'Configure the agent' }
),
title: i18n.translate(
'xpack.observability_onboarding.installElasticAgent.configureStep.title',
{ defaultMessage: 'Configure the Elastic agent' }
),
status:
yamlConfigStatus === FETCH_STATUS.LOADING
? 'loading'
@ -574,6 +563,14 @@ export function InstallElasticAgent() {
{ defaultMessage: 'Download config file' }
)}
</EuiButton>
<EuiSpacer size="m" />
<EuiFlexGroup direction="column">
<StepStatus
status={autoDownloadConfigStep.status}
title={autoDownloadConfigStep.title}
message={autoDownloadConfigStep.message}
/>
</EuiFlexGroup>
</>
),
},

View file

@ -0,0 +1,72 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiStepsProps,
EuiPanel,
EuiText,
EuiCallOut,
EuiLoadingSpinner,
} from '@elastic/eui';
import React from 'react';
export function StepStatus({
status,
title,
message,
}: {
status: EuiStepsProps['steps'][number]['status'];
title: string;
message?: string;
}) {
if (status === 'loading') {
return (
<EuiFlexItem>
<EuiPanel color="transparent">
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">{title}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
);
}
if (status === 'complete') {
return (
<EuiFlexItem>
<EuiCallOut title={title} color="success" iconType="check">
{message}
</EuiCallOut>
</EuiFlexItem>
);
}
if (status === 'danger') {
return (
<EuiFlexItem>
<EuiCallOut title={title} color="danger" iconType="warning">
{message}
</EuiCallOut>
</EuiFlexItem>
);
}
if (status === 'warning') {
return (
<EuiFlexItem>
<EuiCallOut title={title} color="warning" iconType="warning">
{message}
</EuiCallOut>
</EuiFlexItem>
);
}
return null;
}

View file

@ -97,7 +97,10 @@ const createApiKeyRoute = createObservabilityOnboardingServerRoute({
const { id } = await saveObservabilityOnboardingState({
savedObjectsClient,
observabilityOnboardingState: { state } as ObservabilityOnboardingState,
observabilityOnboardingState: {
state: state as ObservabilityOnboardingState['state'],
progress: {},
},
});
return { apiKeyEncoded, onboardingId: id };
@ -145,15 +148,18 @@ const stepProgressUpdateRoute = createObservabilityOnboardingServerRoute({
id: t.string,
name: t.string,
}),
body: t.type({
status: t.string,
}),
body: t.intersection([
t.type({
status: t.string,
}),
t.partial({ message: t.string }),
]),
}),
async handler(resources): Promise<object> {
async handler(resources) {
const {
params: {
path: { id, name },
body: { status },
body: { status, message },
},
core,
} = resources;
@ -186,11 +192,11 @@ const stepProgressUpdateRoute = createObservabilityOnboardingServerRoute({
...observabilityOnboardingState,
progress: {
...observabilityOnboardingState.progress,
[name]: status,
[name]: { status, message },
},
},
});
return { name, status };
return { name, status, message };
},
});
@ -203,7 +209,9 @@ const getProgressRoute = createObservabilityOnboardingServerRoute({
onboardingId: t.string,
}),
}),
async handler(resources): Promise<{ progress: Record<string, string> }> {
async handler(resources): Promise<{
progress: Record<string, { status: string; message?: string }>;
}> {
const {
params: {
path: { onboardingId },
@ -233,7 +241,7 @@ const getProgressRoute = createObservabilityOnboardingServerRoute({
const {
state: { datasetName: dataset, namespace },
} = savedObservabilityOnboardingState;
if (progress['ea-status'] === 'complete') {
if (progress['ea-status']?.status === 'complete') {
try {
const hasLogs = await getHasLogs({
dataset,
@ -241,15 +249,15 @@ const getProgressRoute = createObservabilityOnboardingServerRoute({
esClient,
});
if (hasLogs) {
progress['logs-ingest'] = 'complete';
progress['logs-ingest'] = { status: 'complete' };
} else {
progress['logs-ingest'] = 'loading';
progress['logs-ingest'] = { status: 'loading' };
}
} catch (error) {
progress['logs-ingest'] = 'warning';
progress['logs-ingest'] = { status: 'warning', message: error.message };
}
} else {
progress['logs-ingest'] = 'incomplete';
progress['logs-ingest'] = { status: 'incomplete' };
}
return { progress };

View file

@ -30,7 +30,10 @@ export async function saveObservabilityOnboardingState({
savedObject = await savedObjectsClient.update<ObservabilityOnboardingState>(
OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
savedObjectId,
observabilityOnboardingState
{
state: observabilityOnboardingState.state,
progress: { ...observabilityOnboardingState.progress },
}
);
} else {
savedObject = await savedObjectsClient.create<ObservabilityOnboardingState>(

View file

@ -18,7 +18,13 @@ export interface ObservabilityOnboardingState {
logFilePaths: string[];
namespace: string;
};
progress: Record<string, string>;
progress: Record<
string,
{
status: string;
message?: string;
}
>;
}
export interface SavedObservabilityOnboardingState

View file

@ -95,7 +95,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('incomplete');
const logsIngestProgress = request.body.progress['logs-ingest'];
expect(logsIngestProgress).to.have.property('status', 'incomplete');
});
});
@ -124,7 +125,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('loading');
const logsIngestProgress = request.body.progress['logs-ingest'];
expect(logsIngestProgress).to.have.property('status', 'loading');
});
});
@ -156,7 +158,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('complete');
const logsIngestProgress = request.body.progress['logs-ingest'];
expect(logsIngestProgress).to.have.property('status', 'complete');
});
after(async () => {

View file

@ -76,7 +76,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
id: request.body.onboardingId,
});
expect(savedState.attributes).to.be.eql({ state });
expect(savedState.attributes).to.be.eql({ state, progress: {} });
});
});
});

View file

@ -16,7 +16,17 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const observabilityOnboardingApiClient = getService('observabilityOnboardingApiClient');
async function callApi({ id, name, status }: { id: string; name: string; status: string }) {
async function callApi({
id,
name,
status,
message,
}: {
id: string;
name: string;
status: string;
message?: string;
}) {
return await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/{id}/step/{name}',
params: {
@ -26,28 +36,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
body: {
status,
message,
},
},
});
}
registry.when('Update step progress', { config: 'basic' }, () => {
let onboardingId: string;
before(async () => {
const req = await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/save',
params: {
body: {
name: 'name',
state: {},
},
},
});
onboardingId = req.body.onboardingId;
});
describe("when onboardingId doesn't exists", () => {
it('fails with a 404 error', async () => {
const err = await expectToReject<ObservabilityOnboardingApiError>(
@ -65,21 +60,34 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
describe('when onboardingId exists', () => {
const step = {
name: 'ea-download',
status: 'complete',
};
let onboardingId: string;
before(async () => {
beforeEach(async () => {
const req = await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/save',
params: {
body: {
name: 'name',
state: {},
},
},
});
onboardingId = req.body.onboardingId;
const savedState = await kibanaServer.savedObjects.get({
type: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
id: onboardingId,
});
expect(savedState.attributes.progress?.[step.name]).not.ok();
expect(savedState.attributes.progress).eql({});
});
it('updates step status', async () => {
const step = {
name: 'ea-download',
status: 'complete',
};
const request = await callApi({
id: onboardingId,
...step,
@ -92,7 +100,38 @@ export default function ApiTest({ getService }: FtrProviderContext) {
id: onboardingId,
});
expect(savedState.attributes.progress?.[step.name]).to.be(step.status);
const stepProgress = savedState.attributes.progress?.[step.name];
expect(stepProgress).to.have.property('status', step.status);
});
it('updates step status with message', async () => {
const step = {
name: 'ea-download',
status: 'danger',
message: 'Download failed',
};
const request = await callApi({
id: onboardingId,
...step,
});
expect(request.status).to.be(200);
const savedState = await kibanaServer.savedObjects.get({
type: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
id: onboardingId,
});
const stepProgress = savedState.attributes.progress?.[step.name];
expect(stepProgress).to.have.property('status', step.status);
expect(stepProgress).to.have.property('message', step.message);
});
afterEach(async () => {
await kibanaServer.savedObjects.delete({
type: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
id: onboardingId,
});
});
});
});