mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Agent remote configuration (#39555)
* [APM] Agent remote configuration UI - Creates apmCm index mapping on plugin initialization. - Links from home page to settings page
This commit is contained in:
parent
2eeea97fa8
commit
0e8b722a25
26 changed files with 1762 additions and 44 deletions
|
@ -38,6 +38,7 @@ export default function apmOss(kibana) {
|
|||
spanIndices: Joi.string().default('apm-*'),
|
||||
metricsIndices: Joi.string().default('apm-*'),
|
||||
onboardingIndices: Joi.string().default('apm-*'),
|
||||
cmIndex: Joi.string().default('.apm-cm')
|
||||
}).default();
|
||||
},
|
||||
|
||||
|
@ -48,7 +49,8 @@ export default function apmOss(kibana) {
|
|||
'transactionIndices',
|
||||
'spanIndices',
|
||||
'metricsIndices',
|
||||
'onboardingIndices'
|
||||
'onboardingIndices',
|
||||
'cmIndex'
|
||||
].map(type => server.config().get(`apm_oss.${type}`))));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ApmHeader } from '../../shared/ApmHeader';
|
||||
|
@ -12,6 +17,7 @@ import { HistoryTabs, IHistoryTab } from '../../shared/HistoryTabs';
|
|||
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
|
||||
import { ServiceOverview } from '../ServiceOverview';
|
||||
import { TraceOverview } from '../TraceOverview';
|
||||
import { APMLink } from '../../shared/Links/APMLink';
|
||||
|
||||
const homeTabs: IHistoryTab[] = [
|
||||
{
|
||||
|
@ -32,6 +38,10 @@ const homeTabs: IHistoryTab[] = [
|
|||
}
|
||||
];
|
||||
|
||||
const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', {
|
||||
defaultMessage: 'Settings'
|
||||
});
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<div>
|
||||
|
@ -42,6 +52,13 @@ export function Home() {
|
|||
<h1>APM</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<APMLink path="/settings">
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="gear">
|
||||
{SETTINGS_LINK_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</APMLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SetupInstructionsLink />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -17,6 +17,23 @@ exports[`Home component should render 1`] = `
|
|||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<APMLink
|
||||
path="/settings"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="gear"
|
||||
size="s"
|
||||
type="button"
|
||||
>
|
||||
Settings
|
||||
</EuiButtonEmpty>
|
||||
</APMLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TransactionDetails } from '../../TransactionDetails';
|
|||
import { Home } from '../Home';
|
||||
import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
|
||||
import { RouteName } from './route_names';
|
||||
import { SettingsList } from '../../Settings/SettingsList';
|
||||
|
||||
interface RouteParams {
|
||||
serviceName: string;
|
||||
|
@ -47,6 +48,15 @@ export const routes: BreadcrumbRoute[] = [
|
|||
}),
|
||||
name: RouteName.SERVICES
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/settings',
|
||||
component: SettingsList,
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
|
||||
defaultMessage: 'Settings'
|
||||
}),
|
||||
name: RouteName.SETTINGS
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/traces',
|
||||
|
|
|
@ -14,5 +14,6 @@ export enum RouteName {
|
|||
ERROR = 'error',
|
||||
METRICS = 'metrics',
|
||||
TRANSACTION_TYPE = 'transaction_type',
|
||||
TRANSACTION_NAME = 'transaction_name'
|
||||
TRANSACTION_NAME = 'transaction_name',
|
||||
SETTINGS = 'settings'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiPortal,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutFooter,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut
|
||||
} from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AddSettingFlyoutBody } from './AddSettingFlyoutBody';
|
||||
import { Config } from '../SettingsList';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import {
|
||||
loadCMServices,
|
||||
loadCMEnvironments,
|
||||
deleteCMConfiguration,
|
||||
updateCMConfiguration,
|
||||
createCMConfiguration
|
||||
} from '../../../../services/rest/apm/settings';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
isOpen: boolean;
|
||||
selectedConfig: Config | null;
|
||||
}
|
||||
|
||||
export function AddSettingsFlyout({
|
||||
onClose,
|
||||
isOpen,
|
||||
onSubmit,
|
||||
selectedConfig
|
||||
}: Props) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [environment, setEnvironment] = useState<string | undefined>(
|
||||
selectedConfig
|
||||
? selectedConfig.service.environment || ENVIRONMENT_NOT_DEFINED
|
||||
: undefined
|
||||
);
|
||||
const [serviceName, setServiceName] = useState<string | undefined>(
|
||||
selectedConfig ? selectedConfig.service.name : undefined
|
||||
);
|
||||
const [sampleRate, setSampleRate] = useState<number>(
|
||||
selectedConfig ? parseFloat(selectedConfig.settings.sample_rate) : NaN
|
||||
);
|
||||
const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher<
|
||||
string[]
|
||||
>(async () => (await loadCMServices()).sort(), [], {
|
||||
preservePreviousResponse: false
|
||||
});
|
||||
const { data: environments = [], status: environmentStatus } = useFetcher<
|
||||
Array<{ name: string; available: boolean }>
|
||||
>(
|
||||
() => {
|
||||
if (serviceName) {
|
||||
return loadCMEnvironments({ serviceName });
|
||||
}
|
||||
},
|
||||
[serviceName],
|
||||
{ preservePreviousResponse: false }
|
||||
);
|
||||
const isSelectedEnvironmentValid = environments.some(
|
||||
env =>
|
||||
env.name === environment && (Boolean(selectedConfig) || env.available)
|
||||
);
|
||||
const isSampleRateValid = sampleRate >= 0 && sampleRate <= 1;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="s" onClose={onClose} ownFocus={true}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
{selectedConfig ? (
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.editConfigTitle',
|
||||
{
|
||||
defaultMessage: 'Edit configuration'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
) : (
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.createConfigTitle',
|
||||
{
|
||||
defaultMessage: 'Create configuration'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.betaCallOutTitle',
|
||||
{
|
||||
defaultMessage: 'APM Agent Configuration (BETA)'
|
||||
}
|
||||
)}
|
||||
iconType="iInCircle"
|
||||
color="warning"
|
||||
>
|
||||
{i18n.translate('xpack.apm.settings.cm.flyOut.betaCallOutText', {
|
||||
defaultMessage:
|
||||
'Please note only sample rate configuration is supported in this first version. We will extend support for central configuration in future releases. Please be aware of bugs.'
|
||||
})}
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<AddSettingFlyoutBody
|
||||
selectedConfig={selectedConfig}
|
||||
onDelete={async () => {
|
||||
if (selectedConfig) {
|
||||
await deleteConfig(selectedConfig);
|
||||
}
|
||||
onSubmit();
|
||||
}}
|
||||
environment={environment}
|
||||
setEnvironment={setEnvironment}
|
||||
serviceName={serviceName}
|
||||
setServiceName={setServiceName}
|
||||
sampleRate={sampleRate}
|
||||
setSampleRate={setSampleRate}
|
||||
serviceNames={serviceNames}
|
||||
serviceNamesStatus={serviceNamesStatus}
|
||||
environments={environments}
|
||||
environmentStatus={environmentStatus}
|
||||
isSampleRateValid={isSampleRateValid}
|
||||
isSelectedEnvironmentValid={isSelectedEnvironmentValid}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel'
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={
|
||||
!(
|
||||
(selectedConfig && isSampleRateValid) ||
|
||||
(!selectedConfig &&
|
||||
serviceName &&
|
||||
environment &&
|
||||
isSelectedEnvironmentValid &&
|
||||
isSampleRateValid)
|
||||
)
|
||||
}
|
||||
onClick={async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
await saveConfig({
|
||||
environment,
|
||||
serviceName,
|
||||
sampleRate,
|
||||
configurationId: selectedConfig
|
||||
? selectedConfig.id
|
||||
: undefined
|
||||
});
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.saveConfigurationButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Save configuration'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
||||
async function deleteConfig(selectedConfig: Config) {
|
||||
try {
|
||||
await deleteCMConfiguration(selectedConfig.id);
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.cm.deleteConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration was deleted'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.deleteConfigSucceededText"
|
||||
defaultMessage="You have successfully deleted a configuration for {serviceName}. It will take some time to propagate to the agents."
|
||||
values={{
|
||||
serviceName: `"${selectedConfig.service.name}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.apm.settings.cm.deleteConfigFailedTitle', {
|
||||
defaultMessage: 'Configuration could not be deleted'
|
||||
}),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.deleteConfigFailedText"
|
||||
defaultMessage="Something went wrong when deleting a configuration for {serviceName}. Error: {errorMessage}"
|
||||
values={{
|
||||
serviceName: `"${selectedConfig.service.name}"`,
|
||||
errorMessage: `"${error.message}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig({
|
||||
sampleRate,
|
||||
serviceName,
|
||||
environment,
|
||||
configurationId
|
||||
}: {
|
||||
sampleRate: number;
|
||||
serviceName: string | undefined;
|
||||
environment: string | undefined;
|
||||
configurationId?: string;
|
||||
}) {
|
||||
try {
|
||||
if (isNaN(sampleRate) || !serviceName) {
|
||||
throw new Error('Missing arguments');
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
settings: {
|
||||
sample_rate: sampleRate.toString(10)
|
||||
},
|
||||
service: {
|
||||
name: serviceName,
|
||||
environment:
|
||||
environment === ENVIRONMENT_NOT_DEFINED ? undefined : environment
|
||||
}
|
||||
};
|
||||
|
||||
if (configurationId) {
|
||||
await updateCMConfiguration(configurationId, configuration);
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.cm.editConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration edited'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.editConfigSucceededText"
|
||||
defaultMessage="You have successfully edited the configuration for {serviceName}. It will take some time to propagate to the agents."
|
||||
values={{
|
||||
serviceName: `"${serviceName}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
await createCMConfiguration(configuration);
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.cm.createConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration created!'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.createConfigSucceededText"
|
||||
defaultMessage="You have successfully created a configuration for {serviceName}. It will take some time to propagate to the agents."
|
||||
values={{
|
||||
serviceName: `"${serviceName}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (configurationId) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.apm.settings.cm.editConfigFailedTitle', {
|
||||
defaultMessage: 'Configuration could not be edited'
|
||||
}),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.editConfigFailedText"
|
||||
defaultMessage="Something went wrong when editing the configuration for {serviceName}. Error: {errorMessage}"
|
||||
values={{
|
||||
serviceName: `"${serviceName}"`,
|
||||
errorMessage: `"${error.message}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate('xpack.apm.settings.cm.createConfigFailedTitle', {
|
||||
defaultMessage: 'Configuration could not be created'
|
||||
}),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.createConfigFailedText"
|
||||
defaultMessage="Something went wrong when creating a configuration for {serviceName}. Error: {errorMessage}"
|
||||
values={{
|
||||
serviceName: `"${serviceName}"`,
|
||||
errorMessage: `"${error.message}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiSelect,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldNumber,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiHorizontalRule,
|
||||
EuiText
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
|
||||
import { Config } from '../SettingsList';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
|
||||
export function AddSettingFlyoutBody({
|
||||
selectedConfig,
|
||||
onDelete,
|
||||
environment,
|
||||
setEnvironment,
|
||||
serviceName,
|
||||
setServiceName,
|
||||
sampleRate,
|
||||
setSampleRate,
|
||||
serviceNames,
|
||||
serviceNamesStatus,
|
||||
environments,
|
||||
environmentStatus,
|
||||
isSampleRateValid,
|
||||
isSelectedEnvironmentValid
|
||||
}: {
|
||||
selectedConfig: Config | null;
|
||||
onDelete: () => void;
|
||||
environment?: string;
|
||||
setEnvironment: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
serviceName?: string;
|
||||
setServiceName: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
sampleRate: number;
|
||||
setSampleRate: React.Dispatch<React.SetStateAction<number>>;
|
||||
serviceNames: string[];
|
||||
serviceNamesStatus?: FETCH_STATUS;
|
||||
environments: Array<{
|
||||
name: string;
|
||||
available: boolean;
|
||||
}>;
|
||||
environmentStatus?: FETCH_STATUS;
|
||||
isSampleRateValid: boolean;
|
||||
isSelectedEnvironmentValid: boolean;
|
||||
}) {
|
||||
const environmentOptions = environments.map(({ name, available }) => ({
|
||||
disabled: !available,
|
||||
text:
|
||||
name === ENVIRONMENT_NOT_DEFINED
|
||||
? i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceEnvironmentNotSetOptionLabel',
|
||||
{
|
||||
defaultMessage: 'Not set'
|
||||
}
|
||||
)
|
||||
: name,
|
||||
value: name
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<form>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Service'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceNameSelectLabel',
|
||||
{
|
||||
defaultMessage: 'Name'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceNameSelectHelpText',
|
||||
{
|
||||
defaultMessage: 'Choose the service you want to configure.'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
isLoading={serviceNamesStatus === 'loading'}
|
||||
hasNoInitialSelection
|
||||
options={serviceNames.map(text => ({ text }))}
|
||||
value={serviceName}
|
||||
disabled={Boolean(selectedConfig)}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setServiceName(e.target.value);
|
||||
setEnvironment(undefined);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceEnvironmentSelectLabel',
|
||||
{
|
||||
defaultMessage: 'Environment'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceEnvironmentSelectHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Only a single environment per configuration is supported.'
|
||||
}
|
||||
)}
|
||||
error={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.serviceEnvironmentSelectErrorText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Must select a valid environement to save a configuration.'
|
||||
}
|
||||
)}
|
||||
isInvalid={
|
||||
!(
|
||||
selectedConfig ||
|
||||
(!selectedConfig &&
|
||||
environment &&
|
||||
isSelectedEnvironmentValid &&
|
||||
environmentStatus === 'success') ||
|
||||
isNaN(sampleRate)
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
key={serviceName} // rerender when serviceName changes to mitigate initial selection bug in EuiSelect
|
||||
isLoading={environmentStatus === 'loading'}
|
||||
hasNoInitialSelection
|
||||
options={environmentOptions}
|
||||
value={environment}
|
||||
disabled={!serviceName || Boolean(selectedConfig)}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setEnvironment(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.configurationSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.sampleRateConfigurationInputLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction sample rate'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.sampleRateConfigurationInputHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose a rate between 0.000 and 1.0. Default configuration is 1.0 (100% of traces).'
|
||||
}
|
||||
)}
|
||||
error={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.sampleRateConfigurationInputErrorText',
|
||||
{
|
||||
defaultMessage: 'Sample rate must be between 0.000 and 1'
|
||||
}
|
||||
)}
|
||||
isInvalid={
|
||||
!(
|
||||
(Boolean(selectedConfig) &&
|
||||
(isNaN(sampleRate) || isSampleRateValid)) ||
|
||||
(!selectedConfig &&
|
||||
(!(serviceName || environment) ||
|
||||
(isNaN(sampleRate) || isSampleRateValid)))
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.001}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.sampleRateConfigurationInputPlaceholderText',
|
||||
{
|
||||
defaultMessage: 'Set sample rate'
|
||||
}
|
||||
)}
|
||||
value={isNaN(sampleRate) ? '' : sampleRate}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setSampleRate(parseFloat(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{selectedConfig ? (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<EuiText color="danger">
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.deleteConfigurationSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Delete configuration'
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.deleteConfigurationSectionText',
|
||||
{
|
||||
defaultMessage:
|
||||
'If you wish to delete this configuration, please be aware that the agents will continue to use the existing configuration until they sync with the APM Server.'
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiButton fill={false} color="danger" onClick={onDelete}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.flyOut.deleteConfigurationButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
</form>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiPanel,
|
||||
EuiBetaBadge,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiLink
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { loadCMList } from '../../../services/rest/apm/settings';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
||||
import { CMListAPIResponse } from '../../../../server/lib/settings/cm/list_configurations';
|
||||
import { AddSettingsFlyout } from './AddSettings/AddSettingFlyout';
|
||||
import { APMLink } from '../../shared/Links/APMLink';
|
||||
|
||||
export type Config = CMListAPIResponse[0];
|
||||
|
||||
export function SettingsList() {
|
||||
const { data = [], refresh } = useFetcher(loadCMList, []);
|
||||
const [selectedConfig, setSelectedConfig] = useState<Config | null>(null);
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
|
||||
|
||||
const COLUMNS: Array<ITableColumn<Config>> = [
|
||||
{
|
||||
field: 'service.name',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.serviceNameColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Service name'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (_, config: Config) => (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSelectedConfig(config);
|
||||
setIsFlyoutOpen(true);
|
||||
}}
|
||||
>
|
||||
{config.service.name}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'service.environment',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.environmentColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Service environment'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: string) => value
|
||||
},
|
||||
{
|
||||
field: 'settings.sample_rate',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.sampleRateColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Sample rate'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: string) => value
|
||||
},
|
||||
{
|
||||
field: '@timestamp',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.lastUpdatedColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Last updated'
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: number) => (value ? moment(value).fromNow() : null)
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.editButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit'
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.editButtonDescription',
|
||||
{
|
||||
defaultMessage: 'Edit this config'
|
||||
}
|
||||
),
|
||||
icon: 'pencil',
|
||||
color: 'primary',
|
||||
type: 'icon',
|
||||
onClick: (config: Config) => {
|
||||
setSelectedConfig(config);
|
||||
setIsFlyoutOpen(true);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const RETURN_TO_OVERVIEW_LINK_LABEL = i18n.translate(
|
||||
'xpack.apm.settings.cm.returnToOverviewLinkLabel',
|
||||
{
|
||||
defaultMessage: 'Return to overview'
|
||||
}
|
||||
);
|
||||
|
||||
const hasConfigurations = !isEmpty(data);
|
||||
|
||||
const emptyState = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="controlsHorizontal"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.emptyPromptTitle',
|
||||
{ defaultMessage: 'No configurations found.' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.configTable.emptyPromptText',
|
||||
{
|
||||
defaultMessage:
|
||||
"Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration."
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton color="primary" fill onClick={() => setIsFlyoutOpen(true)}>
|
||||
{i18n.translate('xpack.apm.settings.cm.createConfigButtonLabel', {
|
||||
defaultMessage: 'Create configuration'
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddSettingsFlyout
|
||||
isOpen={isFlyoutOpen}
|
||||
selectedConfig={selectedConfig}
|
||||
onClose={() => {
|
||||
setSelectedConfig(null);
|
||||
setIsFlyoutOpen(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setSelectedConfig(null);
|
||||
setIsFlyoutOpen(false);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
{i18n.translate('xpack.apm.settings.cm.pageTitle', {
|
||||
defaultMessage: 'Settings'
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<APMLink path="/">
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="arrowLeft">
|
||||
{RETURN_TO_OVERVIEW_LINK_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</APMLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.configurationsPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Configurations'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.apm.settings.cm.betaBadgeLabel', {
|
||||
defaultMessage: 'Beta'
|
||||
})}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.apm.settings.cm.betaBadgeText',
|
||||
{
|
||||
defaultMessage:
|
||||
'This feature is still in development. If you have feedback, please reach out in our Discuss forum.'
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasConfigurations ? (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={() => setIsFlyoutOpen(true)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.createConfigButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create configuration'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.apm.settings.cm.betaCallOutTitle', {
|
||||
defaultMessage: 'APM Agent Configuration (BETA)'
|
||||
})}
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.cm.betaCallOutText"
|
||||
defaultMessage="We're excited to bring you a first look at APM Agent configuration. {agentConfigDocsLink}"
|
||||
values={{
|
||||
agentConfigDocsLink: (
|
||||
<EuiLink href="https://www.elastic.co/guide/en/kibana/current/agent-configuration.html">
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.cm.agentConfigDocsLinkLabel',
|
||||
{ defaultMessage: 'Learn more in our docs.' }
|
||||
)}
|
||||
</EuiLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{hasConfigurations ? (
|
||||
<ManagedTable
|
||||
columns={COLUMNS}
|
||||
items={data}
|
||||
initialSort={{ field: 'service.name', direction: 'asc' }}
|
||||
initialPageSize={50}
|
||||
/>
|
||||
) : (
|
||||
emptyState
|
||||
)}
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -8,11 +8,13 @@ import { EuiBasicTable } from '@elastic/eui';
|
|||
import { sortByOrder } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
|
||||
// TODO: this should really be imported from EUI
|
||||
export interface ITableColumn<T> {
|
||||
field: string;
|
||||
name: string;
|
||||
actions?: StringMap[];
|
||||
field?: string;
|
||||
dataType?: string;
|
||||
align?: string;
|
||||
width?: string;
|
||||
|
@ -35,7 +37,7 @@ interface Props<T> {
|
|||
initialPageSize?: number;
|
||||
hidePerPageOptions?: boolean;
|
||||
initialSort?: {
|
||||
field: Extract<keyof T, string>;
|
||||
field: Required<ITableColumn<T>>['field'];
|
||||
direction: string;
|
||||
};
|
||||
noItemsMessage?: React.ReactNode;
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -46,6 +47,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -57,6 +59,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'response from hook',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
});
|
||||
|
@ -77,6 +80,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -87,6 +91,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -98,6 +103,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: expect.any(Error),
|
||||
refresh: expect.any(Function),
|
||||
status: 'failure'
|
||||
});
|
||||
});
|
||||
|
@ -118,6 +124,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
|
||||
|
@ -127,6 +134,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'first response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
|
@ -145,6 +153,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'first response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
|
||||
|
@ -155,8 +164,40 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'second response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the same object reference when data is unchanged between rerenders', async () => {
|
||||
const hook = renderHook(
|
||||
({ callback, args }) => useFetcher(callback, args),
|
||||
{
|
||||
initialProps: {
|
||||
callback: async () => 'data response',
|
||||
args: ['a']
|
||||
}
|
||||
}
|
||||
);
|
||||
await hook.waitForNextUpdate();
|
||||
const firstResult = hook.result.current;
|
||||
hook.rerender();
|
||||
const secondResult = hook.result.current;
|
||||
|
||||
// assert: subsequent rerender returns the same object reference
|
||||
expect(secondResult === firstResult).toEqual(true);
|
||||
|
||||
hook.rerender({
|
||||
callback: async () => {
|
||||
return 'second response';
|
||||
},
|
||||
args: ['b']
|
||||
});
|
||||
await hook.waitForNextUpdate();
|
||||
const thirdResult = hook.result.current;
|
||||
|
||||
// assert: rerender with different data returns a new object
|
||||
expect(secondResult === thirdResult).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState, useMemo } from 'react';
|
||||
import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext';
|
||||
import { useComponentId } from './useComponentId';
|
||||
|
||||
|
@ -16,8 +16,10 @@ export enum FETCH_STATUS {
|
|||
|
||||
export function useFetcher<Response>(
|
||||
fn: () => Promise<Response> | undefined,
|
||||
useEffectKey: any[]
|
||||
effectKey: any[],
|
||||
options: { preservePreviousResponse?: boolean } = {}
|
||||
) {
|
||||
const { preservePreviousResponse = true } = options;
|
||||
const id = useComponentId();
|
||||
const { dispatchStatus } = useContext(LoadingIndicatorContext);
|
||||
const [result, setResult] = useState<{
|
||||
|
@ -25,47 +27,66 @@ export function useFetcher<Response>(
|
|||
status?: FETCH_STATUS;
|
||||
error?: Error;
|
||||
}>({});
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
useEffect(
|
||||
() => {
|
||||
let didCancel = false;
|
||||
|
||||
dispatchStatus({ id, isLoading: true });
|
||||
setResult({
|
||||
data: result.data, // preserve data from previous state while loading next state
|
||||
status: FETCH_STATUS.LOADING,
|
||||
error: undefined
|
||||
});
|
||||
|
||||
async function doFetch() {
|
||||
try {
|
||||
const data = await fn();
|
||||
if (!didCancel) {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
setResult({
|
||||
data,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
error: undefined
|
||||
});
|
||||
async function doFetch() {
|
||||
const promise = fn();
|
||||
if (!promise) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!didCancel) {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
setResult({
|
||||
data: undefined,
|
||||
status: FETCH_STATUS.FAILURE,
|
||||
error: e
|
||||
});
|
||||
|
||||
dispatchStatus({ id, isLoading: true });
|
||||
|
||||
setResult({
|
||||
data: preservePreviousResponse ? result.data : undefined, // preserve data from previous state while loading next state
|
||||
status: FETCH_STATUS.LOADING,
|
||||
error: undefined
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await promise;
|
||||
if (!didCancel) {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
setResult({
|
||||
data,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
error: undefined
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!didCancel) {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
setResult({
|
||||
data: undefined,
|
||||
status: FETCH_STATUS.FAILURE,
|
||||
error: e
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doFetch();
|
||||
doFetch();
|
||||
|
||||
return () => {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
didCancel = true;
|
||||
};
|
||||
}, useEffectKey);
|
||||
return () => {
|
||||
dispatchStatus({ id, isLoading: false });
|
||||
didCancel = true;
|
||||
};
|
||||
},
|
||||
[...effectKey, counter]
|
||||
);
|
||||
|
||||
return result || {};
|
||||
return useMemo(
|
||||
() => ({
|
||||
...result,
|
||||
refresh: () => {
|
||||
// this will invalidate the effectKey and will result in a new request
|
||||
setCounter(counter + 1);
|
||||
}
|
||||
}),
|
||||
[result]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { CMUpdateConfigurationAPIResponse } from '../../../../server/lib/settings/cm/update_configuration';
|
||||
import { callApi } from '../callApi';
|
||||
import { CentralConfigurationIntake } from '../../../../server/lib/settings/cm/configuration';
|
||||
import { CMServicesAPIResponse } from '../../../../server/lib/settings/cm/get_service_names';
|
||||
import { CMCreateConfigurationAPIResponse } from '../../../../server/lib/settings/cm/create_configuration';
|
||||
import { CMListAPIResponse } from '../../../../server/lib/settings/cm/list_configurations';
|
||||
import { CMEnvironmentsAPIResponse } from '../../../../server/lib/settings/cm/get_environments';
|
||||
|
||||
export async function loadCMServices() {
|
||||
return callApi<CMServicesAPIResponse>({
|
||||
pathname: `/api/apm/settings/cm/services`
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCMEnvironments({
|
||||
serviceName
|
||||
}: {
|
||||
serviceName: string;
|
||||
}) {
|
||||
return callApi<CMEnvironmentsAPIResponse>({
|
||||
pathname: `/api/apm/settings/cm/services/${serviceName}/environments`
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCMConfiguration(
|
||||
configuration: CentralConfigurationIntake
|
||||
) {
|
||||
return callApi<CMCreateConfigurationAPIResponse>({
|
||||
pathname: `/api/apm/settings/cm/new`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(configuration)
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCMConfiguration(
|
||||
configurationId: string,
|
||||
configuration: CentralConfigurationIntake
|
||||
) {
|
||||
return callApi<CMUpdateConfigurationAPIResponse>({
|
||||
pathname: `/api/apm/settings/cm/${configurationId}`,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(configuration)
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCMConfiguration(configId: string) {
|
||||
return callApi({
|
||||
pathname: `/api/apm/settings/cm/${configId}`,
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCMList() {
|
||||
return callApi<CMListAPIResponse>({
|
||||
pathname: `/api/apm/settings/cm`
|
||||
});
|
||||
}
|
|
@ -4,12 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import Joi, { Schema } from 'joi';
|
||||
export const dateValidation = Joi.alternatives()
|
||||
.try(Joi.date().iso(), Joi.number())
|
||||
.required();
|
||||
|
||||
export const withDefaultValidators = (validators = {}) => {
|
||||
export const withDefaultValidators = (
|
||||
validators: { [key: string]: Schema } = {}
|
||||
) => {
|
||||
return Joi.object().keys({
|
||||
_debug: Joi.bool(),
|
||||
start: dateValidation,
|
||||
|
|
19
x-pack/legacy/plugins/apm/server/lib/settings/cm/configuration.d.ts
vendored
Normal file
19
x-pack/legacy/plugins/apm/server/lib/settings/cm/configuration.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface CentralConfigurationIntake {
|
||||
settings: {
|
||||
sample_rate: string;
|
||||
};
|
||||
service: {
|
||||
name: string;
|
||||
environment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CentralConfiguration extends CentralConfigurationIntake {
|
||||
'@timestamp': number;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { Server } from 'hapi';
|
||||
import Boom from 'boom';
|
||||
|
||||
export async function createCmIndex(server: Server) {
|
||||
const index = server.config().get<string>('apm_oss.cmIndex');
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster(
|
||||
'admin'
|
||||
);
|
||||
const indexExists = await callWithInternalUser('indices.exists', { index });
|
||||
if (!indexExists) {
|
||||
const result = await callWithInternalUser('indices.create', {
|
||||
index,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
'@timestamp': {
|
||||
type: 'date'
|
||||
},
|
||||
settings: {
|
||||
properties: {
|
||||
sample_rate: {
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.acknowledged) {
|
||||
const err = new Error(`Unable to create apm settings index '${index}'`);
|
||||
// eslint-disable-next-line
|
||||
console.error(err.stack);
|
||||
throw Boom.boomify(err, { statusCode: 500 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { CentralConfigurationIntake } from './configuration';
|
||||
|
||||
export type CMCreateConfigurationAPIResponse = PromiseReturnType<
|
||||
typeof createConfiguration
|
||||
>;
|
||||
export async function createConfiguration({
|
||||
configuration,
|
||||
setup
|
||||
}: {
|
||||
configuration: CentralConfigurationIntake;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
type: '_doc',
|
||||
refresh: true,
|
||||
index: config.get<string>('apm_oss.cmIndex'),
|
||||
body: {
|
||||
'@timestamp': Date.now(),
|
||||
...configuration
|
||||
}
|
||||
};
|
||||
|
||||
return client.index(params);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../helpers/setup_request';
|
||||
|
||||
export async function deleteConfiguration({
|
||||
configurationId,
|
||||
setup
|
||||
}: {
|
||||
configurationId: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
refresh: 'wait_for',
|
||||
index: config.get<string>('apm_oss.cmIndex'),
|
||||
id: configurationId
|
||||
};
|
||||
|
||||
return client.delete(params);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../../helpers/setup_request';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
|
||||
export async function getAllEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
index: [
|
||||
config.get<string>('apm_oss.metricsIndices'),
|
||||
config.get<string>('apm_oss.errorIndices'),
|
||||
config.get<string>('apm_oss.transactionIndices')
|
||||
],
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }
|
||||
},
|
||||
{ term: { [SERVICE_NAME]: serviceName } }
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED,
|
||||
size: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search(params);
|
||||
const buckets = resp.aggregations.environments.buckets;
|
||||
return buckets.map(bucket => bucket.key);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../../helpers/setup_request';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
|
||||
export async function getUnavailableEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.cmIndex'),
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { [SERVICE_NAME]: serviceName } }]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED,
|
||||
size: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search(params);
|
||||
const buckets = resp.aggregations.environments.buckets;
|
||||
return buckets.map(bucket => bucket.key);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { getAllEnvironments } from './get_all_environments';
|
||||
import { Setup } from '../../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../../typings/common';
|
||||
import { getUnavailableEnvironments } from './get_unavailable_environments';
|
||||
|
||||
export type CMEnvironmentsAPIResponse = PromiseReturnType<
|
||||
typeof getEnvironments
|
||||
>;
|
||||
|
||||
export async function getEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const [allEnvironments, unavailableEnvironments] = await Promise.all([
|
||||
getAllEnvironments({ serviceName, setup }),
|
||||
getUnavailableEnvironments({ serviceName, setup })
|
||||
]);
|
||||
|
||||
return allEnvironments.map(environment => {
|
||||
return {
|
||||
name: environment,
|
||||
available: !unavailableEnvironments.includes(environment)
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export type CMServicesAPIResponse = PromiseReturnType<typeof getServiceNames>;
|
||||
export async function getServiceNames({ setup }: { setup: Setup }) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
index: [
|
||||
config.get<string>('apm_oss.metricsIndices'),
|
||||
config.get<string>('apm_oss.errorIndices'),
|
||||
config.get<string>('apm_oss.transactionIndices')
|
||||
],
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search(params);
|
||||
const buckets = resp.aggregations.services.buckets;
|
||||
return buckets.map(bucket => bucket.key);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { CentralConfiguration } from './configuration';
|
||||
|
||||
export type CMListAPIResponse = PromiseReturnType<typeof listConfigurations>;
|
||||
export async function listConfigurations({ setup }: { setup: Setup }) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.cmIndex')
|
||||
};
|
||||
|
||||
const resp = await client.search<CentralConfiguration>(params);
|
||||
return resp.hits.hits.map(item => ({
|
||||
id: item._id,
|
||||
...item._source
|
||||
}));
|
||||
}
|
52
x-pack/legacy/plugins/apm/server/lib/settings/cm/search.ts
Normal file
52
x-pack/legacy/plugins/apm/server/lib/settings/cm/search.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { ESFilter } from 'elasticsearch';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { CentralConfiguration } from './configuration';
|
||||
|
||||
export type CMSearchAPIResponse = PromiseReturnType<
|
||||
typeof searchConfigurations
|
||||
>;
|
||||
export async function searchConfigurations({
|
||||
serviceName,
|
||||
environment,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const filters: ESFilter[] = [{ term: { [SERVICE_NAME]: serviceName } }];
|
||||
|
||||
if (environment) {
|
||||
filters.push({ term: { [SERVICE_ENVIRONMENT]: environment } });
|
||||
} else {
|
||||
filters.push({
|
||||
bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } }
|
||||
});
|
||||
}
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.cmIndex'),
|
||||
body: {
|
||||
size: 1,
|
||||
query: {
|
||||
bool: { filter: filters }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search<CentralConfiguration>(params);
|
||||
return resp.hits.hits[0];
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { Setup } from '../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { CentralConfigurationIntake } from './configuration';
|
||||
|
||||
export type CMUpdateConfigurationAPIResponse = PromiseReturnType<
|
||||
typeof updateConfiguration
|
||||
>;
|
||||
export async function updateConfiguration({
|
||||
configurationId,
|
||||
configuration,
|
||||
setup
|
||||
}: {
|
||||
configurationId: string;
|
||||
configuration: CentralConfigurationIntake;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
type: '_doc',
|
||||
id: configurationId,
|
||||
refresh: true,
|
||||
index: config.get<string>('apm_oss.cmIndex'),
|
||||
body: {
|
||||
'@timestamp': Date.now(),
|
||||
...configuration
|
||||
}
|
||||
};
|
||||
|
||||
return client.index(params);
|
||||
}
|
|
@ -14,6 +14,7 @@ import { initTracesApi } from '../routes/traces';
|
|||
import { initTransactionGroupsApi } from '../routes/transaction_groups';
|
||||
import { initUIFiltersApi } from '../routes/ui_filters';
|
||||
import { initIndexPatternApi } from '../routes/index_pattern';
|
||||
import { initSettingsApi } from '../routes/settings';
|
||||
|
||||
export class Plugin {
|
||||
public setup(core: InternalCoreSetup) {
|
||||
|
@ -21,6 +22,7 @@ export class Plugin {
|
|||
initTransactionGroupsApi(core);
|
||||
initTracesApi(core);
|
||||
initServicesApi(core);
|
||||
initSettingsApi(core);
|
||||
initErrorsApi(core);
|
||||
initMetricsApi(core);
|
||||
initIndexPatternApi(core);
|
||||
|
|
199
x-pack/legacy/plugins/apm/server/routes/settings.ts
Normal file
199
x-pack/legacy/plugins/apm/server/routes/settings.ts
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { InternalCoreSetup } from 'src/core/server';
|
||||
import Joi from 'joi';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getServiceNames } from '../lib/settings/cm/get_service_names';
|
||||
import { createConfiguration } from '../lib/settings/cm/create_configuration';
|
||||
import { updateConfiguration } from '../lib/settings/cm/update_configuration';
|
||||
import { CentralConfigurationIntake } from '../lib/settings/cm/configuration';
|
||||
import { searchConfigurations } from '../lib/settings/cm/search';
|
||||
import { listConfigurations } from '../lib/settings/cm/list_configurations';
|
||||
import { getEnvironments } from '../lib/settings/cm/get_environments';
|
||||
import { deleteConfiguration } from '../lib/settings/cm/delete_configuration';
|
||||
import { createCmIndex } from '../lib/settings/cm/create_cm_index';
|
||||
|
||||
const defaultErrorHandler = (err: Error) => {
|
||||
// eslint-disable-next-line
|
||||
console.error(err.stack);
|
||||
throw Boom.boomify(err, { statusCode: 400 });
|
||||
};
|
||||
|
||||
export function initSettingsApi(core: InternalCoreSetup) {
|
||||
const { server } = core.http;
|
||||
|
||||
createCmIndex(server);
|
||||
|
||||
// get list of configurations
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `/api/apm/settings/cm`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
return await listConfigurations({
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// delete configuration
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `/api/apm/settings/cm/{configurationId}`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
const { configurationId } = req.params;
|
||||
return await deleteConfiguration({
|
||||
configurationId,
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// get list of services
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `/api/apm/settings/cm/services`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
return await getServiceNames({
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// get environments for service
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `/api/apm/settings/cm/services/{serviceName}/environments`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
const { serviceName } = req.params;
|
||||
return await getEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// create configuration
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `/api/apm/settings/cm/new`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
const configuration = req.payload as CentralConfigurationIntake;
|
||||
return await createConfiguration({
|
||||
configuration,
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// update configuration
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `/api/apm/settings/cm/{configurationId}`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async req => {
|
||||
const setup = setupRequest(req);
|
||||
const { configurationId } = req.params;
|
||||
const configuration = req.payload as CentralConfigurationIntake;
|
||||
return await updateConfiguration({
|
||||
configurationId,
|
||||
configuration,
|
||||
setup
|
||||
}).catch(defaultErrorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
// Lookup single configuration
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `/api/apm/settings/cm/search`,
|
||||
options: {
|
||||
validate: {
|
||||
query: {
|
||||
_debug: Joi.bool()
|
||||
}
|
||||
},
|
||||
tags: ['access:apm']
|
||||
},
|
||||
handler: async (req, h) => {
|
||||
interface Payload {
|
||||
service: {
|
||||
name: string;
|
||||
environment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const setup = setupRequest(req);
|
||||
const payload = req.payload as Payload;
|
||||
const serviceName = payload.service.name;
|
||||
const environment = payload.service.environment;
|
||||
const config = await searchConfigurations({
|
||||
serviceName,
|
||||
environment,
|
||||
setup
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return h.response().code(404);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue