mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[7.x] [APM] Use new platform for toast notifications (#47276) | [APM] Agent configuration phase 2 (#46995) (#47806)
* [APM] Use new platform for toast notifications (#47276) * [APM] Use new platform for toast notifications * fix more tests * remove comment * [APM] Agent configuration phase 2 (#46995) * [APM] Agent Config Management Phase 2 * Add status indicator * Extract TimestampTooltip component * Remove unused StickyTransactionProperties component * Fix snapshot and minor cleanup * Minor cleanup * Display settings conditionally by agent name * Fix client * Format timestamp * Minor design feedback * Clear cache when clicking refresh * Fix test * Revert t() short hand * Fix translations * Add support for “all” option * Fix API tests * Move delete button to footer * Fix snapshots * Add API tests * Fix toasts * Address feedback and ensure order when searching for configs * Fix snapshots * Remove timeout
This commit is contained in:
parent
9b8979424a
commit
0d77169b64
68 changed files with 2037 additions and 1637 deletions
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ALL_OPTION_VALUE = 'ALL_OPTION_VALUE';
|
||||
|
||||
// human-readable label for the option. The "All" option should be translated.
|
||||
// Everything else should be returned verbatim
|
||||
export function getOptionLabel(value: string | undefined) {
|
||||
if (value === undefined || value === ALL_OPTION_VALUE) {
|
||||
return i18n.translate('xpack.apm.settings.agentConf.allOptionLabel', {
|
||||
defaultMessage: 'All'
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function omitAllOption(value: string) {
|
||||
return value === ALL_OPTION_VALUE ? undefined : value;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { transactionMaxSpansRt } from './index';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('transactionMaxSpans', () => {
|
||||
it('does not accept empty values', () => {
|
||||
expect(isRight(transactionMaxSpansRt.decode(undefined))).toBe(false);
|
||||
expect(isRight(transactionMaxSpansRt.decode(null))).toBe(false);
|
||||
expect(isRight(transactionMaxSpansRt.decode(''))).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts both strings and numbers as values', () => {
|
||||
expect(isRight(transactionMaxSpansRt.decode('55'))).toBe(true);
|
||||
expect(isRight(transactionMaxSpansRt.decode(55))).toBe(true);
|
||||
});
|
||||
|
||||
it('checks if the number falls within 0, 32000', () => {
|
||||
expect(isRight(transactionMaxSpansRt.decode(0))).toBe(true);
|
||||
expect(isRight(transactionMaxSpansRt.decode(32000))).toBe(true);
|
||||
expect(isRight(transactionMaxSpansRt.decode(-55))).toBe(false);
|
||||
expect(isRight(transactionMaxSpansRt.decode(NaN))).toBe(false);
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const transactionMaxSpansRt = new t.Type<number, number, unknown>(
|
||||
'transactionMaxSpans',
|
||||
t.number.is,
|
||||
(input, context) => {
|
||||
const value = parseInt(input as string, 10);
|
||||
return value >= 0 && value <= 32000
|
||||
? t.success(value)
|
||||
: t.failure(input, context);
|
||||
},
|
||||
t.identity
|
||||
);
|
|
@ -7,6 +7,12 @@ import { transactionSampleRateRt } from './index';
|
|||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('transactionSampleRateRt', () => {
|
||||
it('does not accept empty values', () => {
|
||||
expect(isRight(transactionSampleRateRt.decode(undefined))).toBe(false);
|
||||
expect(isRight(transactionSampleRateRt.decode(null))).toBe(false);
|
||||
expect(isRight(transactionSampleRateRt.decode(''))).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts both strings and numbers as values', () => {
|
||||
expect(isRight(transactionSampleRateRt.decode('0.5'))).toBe(true);
|
||||
expect(isRight(transactionSampleRateRt.decode(0.5))).toBe(true);
|
||||
|
|
|
@ -10,8 +10,8 @@ export const transactionSampleRateRt = new t.Type<number, number, unknown>(
|
|||
'TransactionSampleRate',
|
||||
t.number.is,
|
||||
(input, context) => {
|
||||
const value = Number(input);
|
||||
return value >= 0 && value <= 1 && Number(value.toFixed(3)) === value
|
||||
const value = parseFloat(input as string);
|
||||
return value >= 0 && value <= 1 && parseFloat(value.toFixed(3)) === value
|
||||
? t.success(value)
|
||||
: t.failure(input, context);
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
logStacktraceTab
|
||||
} from './ErrorTabs';
|
||||
import { Summary } from '../../../shared/Summary';
|
||||
import { TimestampSummaryItem } from '../../../shared/Summary/TimestampSummaryItem';
|
||||
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
|
||||
import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem';
|
||||
import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
|
||||
|
||||
|
@ -113,7 +113,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) {
|
|||
|
||||
<Summary
|
||||
items={[
|
||||
<TimestampSummaryItem time={error.timestamp.us / 1000} />,
|
||||
<TimestampTooltip time={error.timestamp.us / 1000} />,
|
||||
errorUrl && method ? (
|
||||
<HttpInfoSummaryItem
|
||||
url={errorUrl}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { TransactionDetails } from '../../TransactionDetails';
|
|||
import { Home } from '../../Home';
|
||||
import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
|
||||
import { RouteName } from './route_names';
|
||||
import { Settings } from '../../Settings';
|
||||
import { AgentConfigurations } from '../../Settings/AgentConfigurations';
|
||||
import { toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
|
||||
import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams';
|
||||
|
@ -66,7 +66,7 @@ export const routes: BreadcrumbRoute[] = [
|
|||
{
|
||||
exact: true,
|
||||
path: '/settings',
|
||||
component: Settings,
|
||||
component: AgentConfigurations,
|
||||
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
|
||||
defaultMessage: 'Settings'
|
||||
}),
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { startMLJob } from '../../../../../services/rest/ml';
|
||||
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
|
||||
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
|
||||
import { MachineLearningFlyoutView } from './view';
|
||||
import { KibanaCoreContext } from '../../../../../../../observability/public/context/kibana_core';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
|
@ -23,6 +23,8 @@ interface State {
|
|||
}
|
||||
|
||||
export class MachineLearningFlyout extends Component<Props, State> {
|
||||
static contextType = KibanaCoreContext;
|
||||
|
||||
public state: State = {
|
||||
isCreatingJob: false
|
||||
};
|
||||
|
@ -53,6 +55,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public addErrorToast = () => {
|
||||
const core = this.context;
|
||||
const { urlParams } = this.props;
|
||||
const { serviceName } = urlParams;
|
||||
|
||||
|
@ -60,7 +63,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
toastNotifications.addWarning({
|
||||
core.notifications.toasts.addWarning({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
||||
{
|
||||
|
@ -86,6 +89,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
}: {
|
||||
transactionType: string;
|
||||
}) => {
|
||||
const core = this.context;
|
||||
const { urlParams } = this.props;
|
||||
const { serviceName } = urlParams;
|
||||
|
||||
|
@ -93,7 +97,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
core.notifications.toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle',
|
||||
{
|
||||
|
@ -113,17 +117,19 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
)}{' '}
|
||||
<MLJobLink
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View job'
|
||||
}
|
||||
)}
|
||||
</MLJobLink>
|
||||
<KibanaCoreContext.Provider value={core}>
|
||||
<MLJobLink
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View job'
|
||||
}
|
||||
)}
|
||||
</MLJobLink>
|
||||
</KibanaCoreContext.Provider>
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -30,7 +30,6 @@ import { memoize, padLeft, range } from 'lodash';
|
|||
import moment from 'moment-timezone';
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { LegacyCoreStart } from 'src/core/public';
|
||||
import { KibanaCoreContext } from '../../../../../../observability/public';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
|
@ -218,7 +217,9 @@ export class WatcherFlyout extends Component<
|
|||
};
|
||||
|
||||
public addErrorToast = () => {
|
||||
toastNotifications.addWarning({
|
||||
const core = this.context;
|
||||
|
||||
core.notifications.toasts.addWarning({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle',
|
||||
{
|
||||
|
@ -240,7 +241,9 @@ export class WatcherFlyout extends Component<
|
|||
};
|
||||
|
||||
public addSuccessToast = (id: string) => {
|
||||
toastNotifications.addSuccess({
|
||||
const core = this.context;
|
||||
|
||||
core.notifications.toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle',
|
||||
{
|
||||
|
@ -259,16 +262,18 @@ export class WatcherFlyout extends Component<
|
|||
}
|
||||
}
|
||||
)}{' '}
|
||||
<KibanaLink
|
||||
path={`/management/elasticsearch/watcher/watches/watch/${id}`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
|
||||
{
|
||||
defaultMessage: 'View watch'
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
<KibanaCoreContext.Provider value={core}>
|
||||
<KibanaLink
|
||||
path={`/management/elasticsearch/watcher/watches/watch/${id}`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
|
||||
{
|
||||
defaultMessage: 'View watch'
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
</KibanaCoreContext.Provider>
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import React from 'react';
|
||||
import { render, wait, waitForElement } from 'react-testing-library';
|
||||
import 'react-testing-library/cleanup-after-each';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import * as callApmApi from '../../../../services/rest/callApmApi';
|
||||
import { ServiceOverview } from '..';
|
||||
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
|
||||
|
@ -22,16 +21,17 @@ function renderServiceOverview() {
|
|||
return render(<ServiceOverview />);
|
||||
}
|
||||
|
||||
const coreMock = ({
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (path: string) => `/basepath${path}`
|
||||
}
|
||||
},
|
||||
notifications: { toasts: { addWarning: () => {} } }
|
||||
} as unknown) as LegacyCoreStart;
|
||||
|
||||
describe('Service Overview -> View', () => {
|
||||
beforeEach(() => {
|
||||
const coreMock = ({
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (path: string) => `/basepath${path}`
|
||||
}
|
||||
}
|
||||
} as unknown) as LegacyCoreStart;
|
||||
|
||||
// mock urlParams
|
||||
spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({
|
||||
urlParams: {
|
||||
|
@ -141,45 +141,56 @@ describe('Service Overview -> View', () => {
|
|||
expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render upgrade migration notification when legacy data is found, ', async () => {
|
||||
// create spies
|
||||
const toastSpy = jest.spyOn(toastNotifications, 'addWarning');
|
||||
const dataFetchingSpy = jest
|
||||
.spyOn(callApmApi, 'callApmApi')
|
||||
.mockResolvedValue({
|
||||
hasLegacyData: true,
|
||||
hasHistoricalData: true,
|
||||
items: []
|
||||
});
|
||||
describe('when legacy data is found', () => {
|
||||
it('renders an upgrade migration notification', async () => {
|
||||
// create spies
|
||||
const addWarning = jest.spyOn(
|
||||
coreMock.notifications.toasts,
|
||||
'addWarning'
|
||||
);
|
||||
|
||||
renderServiceOverview();
|
||||
const dataFetchingSpy = jest
|
||||
.spyOn(callApmApi, 'callApmApi')
|
||||
.mockResolvedValue({
|
||||
hasLegacyData: true,
|
||||
hasHistoricalData: true,
|
||||
items: []
|
||||
});
|
||||
|
||||
// wait for requests to be made
|
||||
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
|
||||
renderServiceOverview();
|
||||
|
||||
expect(toastSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Legacy data was detected within the selected time range'
|
||||
})
|
||||
);
|
||||
// wait for requests to be made
|
||||
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(addWarning).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'Legacy data was detected within the selected time range'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render upgrade migration notification when legacy data is not found, ', async () => {
|
||||
// create spies
|
||||
const toastSpy = jest.spyOn(toastNotifications, 'addWarning');
|
||||
const dataFetchingSpy = jest
|
||||
.spyOn(callApmApi, 'callApmApi')
|
||||
.mockResolvedValue({
|
||||
hasLegacyData: false,
|
||||
hasHistoricalData: true,
|
||||
items: []
|
||||
});
|
||||
describe('when legacy data is not found', () => {
|
||||
it('does not render an upgrade migration notification', async () => {
|
||||
// create spies
|
||||
const addWarning = jest.spyOn(
|
||||
coreMock.notifications.toasts,
|
||||
'addWarning'
|
||||
);
|
||||
const dataFetchingSpy = jest
|
||||
.spyOn(callApmApi, 'callApmApi')
|
||||
.mockResolvedValue({
|
||||
hasLegacyData: false,
|
||||
hasHistoricalData: true,
|
||||
items: []
|
||||
});
|
||||
|
||||
renderServiceOverview();
|
||||
renderServiceOverview();
|
||||
|
||||
// wait for requests to be made
|
||||
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
|
||||
// wait for requests to be made
|
||||
await wait(() => expect(dataFetchingSpy).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(toastSpy).not.toHaveBeenCalled();
|
||||
expect(addWarning).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import url from 'url';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { NoServicesMessage } from './NoServicesMessage';
|
||||
|
@ -48,7 +47,8 @@ export function ServiceOverview() {
|
|||
useEffect(() => {
|
||||
if (data.hasLegacyData && !hasDisplayedToast) {
|
||||
hasDisplayedToast = true;
|
||||
toastNotifications.addWarning({
|
||||
|
||||
core.notifications.toasts.addWarning({
|
||||
title: i18n.translate('xpack.apm.serviceOverview.toastTitle', {
|
||||
defaultMessage:
|
||||
'Legacy data was detected within the selected time range'
|
||||
|
@ -77,7 +77,7 @@ export function ServiceOverview() {
|
|||
)
|
||||
});
|
||||
}
|
||||
}, [data.hasLegacyData, core.http.basePath]);
|
||||
}, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]);
|
||||
|
||||
useTrackPageview({ app: 'apm', path: 'services_overview' });
|
||||
useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 });
|
||||
|
|
|
@ -1,386 +0,0 @@
|
|||
/*
|
||||
* 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 { isRight } from 'fp-ts/lib/Either';
|
||||
import { transactionSampleRateRt } from '../../../../../common/runtime_types/transaction_sample_rate_rt';
|
||||
import { AddSettingFlyoutBody } from './AddSettingFlyoutBody';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
import { callApmApi } from '../../../../services/rest/callApmApi';
|
||||
import { trackEvent } from '../../../../../../infra/public/hooks/use_track_metric';
|
||||
import { Config } from '..';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSubmit: () => void;
|
||||
isOpen: boolean;
|
||||
selectedConfig: Config | null;
|
||||
}
|
||||
|
||||
export function AddSettingsFlyout({
|
||||
onClose,
|
||||
isOpen,
|
||||
onSubmit,
|
||||
selectedConfig
|
||||
}: Props) {
|
||||
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<string>(
|
||||
selectedConfig
|
||||
? selectedConfig.settings.transaction_sample_rate.toString()
|
||||
: ''
|
||||
);
|
||||
const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher(
|
||||
() =>
|
||||
callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/services'
|
||||
}),
|
||||
[],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
const { data: environments = [], status: environmentStatus } = useFetcher(
|
||||
() => {
|
||||
if (serviceName) {
|
||||
return callApmApi({
|
||||
pathname:
|
||||
'/api/apm/settings/agent-configuration/services/{serviceName}/environments',
|
||||
params: {
|
||||
path: { serviceName }
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate));
|
||||
|
||||
const isSelectedEnvironmentValid = environments.some(
|
||||
env =>
|
||||
env.name === environment && (Boolean(selectedConfig) || env.available)
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="s" onClose={onClose} ownFocus={true}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
{selectedConfig ? (
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.editConfigTitle',
|
||||
{
|
||||
defaultMessage: 'Edit configuration'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
) : (
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.createConfigTitle',
|
||||
{
|
||||
defaultMessage: 'Create configuration'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.betaCallOutTitle',
|
||||
{
|
||||
defaultMessage: 'APM Agent Configuration (BETA)'
|
||||
}
|
||||
)}
|
||||
iconType="iInCircle"
|
||||
color="warning"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.betaCallOutText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Please note only sample rate configuration is supported in this first version. We will extend support for agent 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.agentConf.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: parseFloat(sampleRate),
|
||||
configurationId: selectedConfig
|
||||
? selectedConfig.id
|
||||
: undefined
|
||||
});
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Save configuration'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
||||
async function deleteConfig(selectedConfig: Config) {
|
||||
try {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
path: { configurationId: selectedConfig.id }
|
||||
}
|
||||
});
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.deleteConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration was deleted'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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.agentConf.deleteConfigFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration could not be deleted'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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;
|
||||
}) {
|
||||
trackEvent({ app: 'apm', name: 'save_agent_configuration' });
|
||||
|
||||
try {
|
||||
if (isNaN(sampleRate) || !serviceName) {
|
||||
throw new Error('Missing arguments');
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
settings: {
|
||||
transaction_sample_rate: sampleRate
|
||||
},
|
||||
service: {
|
||||
name: serviceName,
|
||||
environment:
|
||||
environment === ENVIRONMENT_NOT_DEFINED ? undefined : environment
|
||||
}
|
||||
};
|
||||
|
||||
if (configurationId) {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
method: 'PUT',
|
||||
params: {
|
||||
path: { configurationId },
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.editConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration edited'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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 callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/new',
|
||||
method: 'POST',
|
||||
params: {
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.createConfigSucceededTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration created!'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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.agentConf.editConfigFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration could not be edited'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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.agentConf.createConfigFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration could not be created'
|
||||
}
|
||||
),
|
||||
text: (
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.createConfigFailedText"
|
||||
defaultMessage="Something went wrong when creating a configuration for {serviceName}. Error: {errorMessage}"
|
||||
values={{
|
||||
serviceName: `"${serviceName}"`,
|
||||
errorMessage: `"${error.message}"`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,277 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
EuiFieldText,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiHorizontalRule,
|
||||
EuiText
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
import { SelectWithPlaceholder } from '../../../shared/SelectWithPlaceholder';
|
||||
import { Config } from '..';
|
||||
|
||||
const selectPlaceholderLabel = `- ${i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.selectPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select'
|
||||
}
|
||||
)} -`;
|
||||
|
||||
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: (env: string | undefined) => void;
|
||||
serviceName?: string;
|
||||
setServiceName: (env: string | undefined) => void;
|
||||
sampleRate: string;
|
||||
setSampleRate: (env: string) => void;
|
||||
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.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel',
|
||||
{
|
||||
defaultMessage: 'Not set'
|
||||
}
|
||||
)
|
||||
: name,
|
||||
value: name
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<form>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Service'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel',
|
||||
{
|
||||
defaultMessage: 'Name'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText',
|
||||
{
|
||||
defaultMessage: 'Choose the service you want to configure.'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SelectWithPlaceholder
|
||||
placeholder={selectPlaceholderLabel}
|
||||
isLoading={serviceNamesStatus === 'loading'}
|
||||
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.agentConf.flyOut.serviceEnvironmentSelectLabel',
|
||||
{
|
||||
defaultMessage: 'Environment'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Only a single environment per configuration is supported.'
|
||||
}
|
||||
)}
|
||||
error={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Must select a valid environment to save a configuration.'
|
||||
}
|
||||
)}
|
||||
isInvalid={
|
||||
!(
|
||||
selectedConfig ||
|
||||
(!selectedConfig &&
|
||||
environment &&
|
||||
isSelectedEnvironmentValid &&
|
||||
environmentStatus === 'success') ||
|
||||
isEmpty(sampleRate)
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectWithPlaceholder
|
||||
placeholder={selectPlaceholderLabel}
|
||||
isLoading={environmentStatus === 'loading'}
|
||||
options={environmentOptions}
|
||||
value={environment}
|
||||
disabled={!serviceName || Boolean(selectedConfig)}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setEnvironment(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.configurationSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Configuration'
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel',
|
||||
{
|
||||
defaultMessage: 'Transaction sample rate'
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.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.agentConf.flyOut.sampleRateConfigurationInputErrorText',
|
||||
{
|
||||
defaultMessage: 'Sample rate must be between 0.000 and 1'
|
||||
}
|
||||
)}
|
||||
isInvalid={
|
||||
!(
|
||||
(Boolean(selectedConfig) &&
|
||||
(isEmpty(sampleRate) || isSampleRateValid)) ||
|
||||
(!selectedConfig &&
|
||||
(!(serviceName || environment) ||
|
||||
(isEmpty(sampleRate) || isSampleRateValid)))
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText',
|
||||
{
|
||||
defaultMessage: 'Set sample rate'
|
||||
}
|
||||
)}
|
||||
value={sampleRate}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setSampleRate(e.target.value);
|
||||
}}
|
||||
disabled={!(serviceName && environment) && !selectedConfig}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{selectedConfig ? (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<EuiText color="danger">
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Delete configuration'
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.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.agentConf.flyOut.deleteConfigurationButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
</form>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { NotificationsStart } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Config } from '../index';
|
||||
import { callApmApi } from '../../../../../services/rest/callApmApi';
|
||||
import { getOptionLabel } from '../../../../../../common/agent_configuration_constants';
|
||||
import { useKibanaCore } from '../../../../../../../observability/public';
|
||||
|
||||
interface Props {
|
||||
onDeleted: () => void;
|
||||
selectedConfig: Config;
|
||||
}
|
||||
|
||||
export function DeleteButton({ onDeleted, selectedConfig }: Props) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const {
|
||||
notifications: { toasts }
|
||||
} = useKibanaCore();
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
isLoading={isDeleting}
|
||||
iconSide="right"
|
||||
onClick={async () => {
|
||||
setIsDeleting(true);
|
||||
await deleteConfig(selectedConfig, toasts);
|
||||
setIsDeleting(false);
|
||||
onDeleted();
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.buttonLabel',
|
||||
{ defaultMessage: 'Delete' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteConfig(
|
||||
selectedConfig: Config,
|
||||
toasts: NotificationsStart['toasts']
|
||||
) {
|
||||
try {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
path: { configurationId: selectedConfig.id }
|
||||
}
|
||||
});
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle',
|
||||
{ defaultMessage: 'Configuration was deleted' }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText',
|
||||
{
|
||||
defaultMessage:
|
||||
'You have successfully deleted a configuration for "{serviceName}". It will take some time to propagate to the agents.',
|
||||
values: { serviceName: getOptionLabel(selectedConfig.service.name) }
|
||||
}
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle',
|
||||
{ defaultMessage: 'Configuration could not be deleted' }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Something went wrong when deleting a configuration for "{serviceName}". Error: "{errorMessage}"',
|
||||
values: {
|
||||
serviceName: getOptionLabel(selectedConfig.service.name),
|
||||
errorMessage: error.message
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 { EuiTitle, EuiSpacer, EuiFormRow, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder';
|
||||
import { useFetcher } from '../../../../../hooks/useFetcher';
|
||||
import { callApmApi } from '../../../../../services/rest/callApmApi';
|
||||
import {
|
||||
getOptionLabel,
|
||||
omitAllOption
|
||||
} from '../../../../../../common/agent_configuration_constants';
|
||||
|
||||
const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder',
|
||||
{ defaultMessage: 'Select' }
|
||||
)} -`;
|
||||
|
||||
interface Props {
|
||||
isReadOnly: boolean;
|
||||
serviceName: string;
|
||||
setServiceName: (env: string) => void;
|
||||
environment: string;
|
||||
setEnvironment: (env: string) => void;
|
||||
}
|
||||
|
||||
export function ServiceSection({
|
||||
isReadOnly,
|
||||
serviceName,
|
||||
setServiceName,
|
||||
environment,
|
||||
setEnvironment
|
||||
}: Props) {
|
||||
const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher(
|
||||
() => {
|
||||
if (!isReadOnly) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/services',
|
||||
forceCache: true
|
||||
});
|
||||
}
|
||||
},
|
||||
[isReadOnly],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
const { data: environments = [], status: environmentStatus } = useFetcher(
|
||||
() => {
|
||||
if (!isReadOnly && serviceName) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/environments',
|
||||
params: { query: { serviceName: omitAllOption(serviceName) } }
|
||||
});
|
||||
}
|
||||
},
|
||||
[isReadOnly, serviceName],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const ALREADY_CONFIGURED_TRANSLATED = i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption',
|
||||
{ defaultMessage: 'already configured' }
|
||||
);
|
||||
|
||||
const serviceNameOptions = serviceNames.map(name => ({
|
||||
text: getOptionLabel(name),
|
||||
value: name
|
||||
}));
|
||||
const environmentOptions = environments.map(
|
||||
({ name, alreadyConfigured }) => ({
|
||||
disabled: alreadyConfigured,
|
||||
text: `${getOptionLabel(name)} ${
|
||||
alreadyConfigured ? `(${ALREADY_CONFIGURED_TRANSLATED})` : ''
|
||||
}`,
|
||||
value: name
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.title',
|
||||
{ defaultMessage: 'Service' }
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel',
|
||||
{ defaultMessage: 'Name' }
|
||||
)}
|
||||
helpText={
|
||||
!isReadOnly &&
|
||||
i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText',
|
||||
{ defaultMessage: 'Choose the service you want to configure.' }
|
||||
)
|
||||
}
|
||||
>
|
||||
{isReadOnly ? (
|
||||
<EuiText>{getOptionLabel(serviceName)}</EuiText>
|
||||
) : (
|
||||
<SelectWithPlaceholder
|
||||
placeholder={SELECT_PLACEHOLDER_LABEL}
|
||||
isLoading={serviceNamesStatus === 'loading'}
|
||||
options={serviceNameOptions}
|
||||
value={serviceName}
|
||||
disabled={serviceNamesStatus === 'loading'}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setServiceName(e.target.value);
|
||||
setEnvironment('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel',
|
||||
{ defaultMessage: 'Environment' }
|
||||
)}
|
||||
helpText={
|
||||
!isReadOnly &&
|
||||
i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Only a single environment per configuration is supported.'
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{isReadOnly ? (
|
||||
<EuiText>{getOptionLabel(environment)}</EuiText>
|
||||
) : (
|
||||
<SelectWithPlaceholder
|
||||
placeholder={SELECT_PLACEHOLDER_LABEL}
|
||||
isLoading={environmentStatus === 'loading'}
|
||||
options={environmentOptions}
|
||||
value={environment}
|
||||
disabled={!serviceName || environmentStatus === 'loading'}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setEnvironment(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFieldNumber
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder';
|
||||
|
||||
interface Props {
|
||||
isRumService: boolean;
|
||||
|
||||
// sampleRate
|
||||
sampleRate: string;
|
||||
setSampleRate: (value: string) => void;
|
||||
isSampleRateValid?: boolean;
|
||||
|
||||
// captureBody
|
||||
captureBody: string;
|
||||
setCaptureBody: (value: string) => void;
|
||||
|
||||
// transactionMaxSpans
|
||||
transactionMaxSpans: string;
|
||||
setTransactionMaxSpans: (value: string) => void;
|
||||
isTransactionMaxSpansValid?: boolean;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
isRumService,
|
||||
|
||||
// sampleRate
|
||||
sampleRate,
|
||||
setSampleRate,
|
||||
isSampleRateValid,
|
||||
|
||||
// captureBody
|
||||
captureBody,
|
||||
setCaptureBody,
|
||||
|
||||
// transactionMaxSpans
|
||||
transactionMaxSpans,
|
||||
setTransactionMaxSpans,
|
||||
isTransactionMaxSpansValid
|
||||
}: Props) {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.title',
|
||||
{ defaultMessage: 'Options' }
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.sampleRateConfigurationInputLabel',
|
||||
{ defaultMessage: 'Transaction sample rate' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.sampleRateConfigurationInputHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose a rate between 0.000 and 1.0. Default is 1.0 (100% of traces).'
|
||||
}
|
||||
)}
|
||||
error={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.sampleRateConfigurationInputErrorText',
|
||||
{ defaultMessage: 'Sample rate must be between 0.000 and 1' }
|
||||
)}
|
||||
isInvalid={!isEmpty(sampleRate) && !isSampleRateValid}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.sampleRateConfigurationInputPlaceholderText',
|
||||
{ defaultMessage: 'Set sample rate' }
|
||||
)}
|
||||
value={sampleRate}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setSampleRate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{!isRumService && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputLabel',
|
||||
{ defaultMessage: 'Capture body' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'For transactions that are HTTP requests, the agent can optionally capture the request body (e.g. POST variables). Default is "off".'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SelectWithPlaceholder
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputPlaceholderText',
|
||||
{ defaultMessage: 'Select option' }
|
||||
)}
|
||||
options={[
|
||||
{ text: 'off' },
|
||||
{ text: 'errors' },
|
||||
{ text: 'transactions' },
|
||||
{ text: 'all' }
|
||||
]}
|
||||
value={captureBody}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setCaptureBody(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
{!isRumService && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.transactionMaxSpansConfigInputLabel',
|
||||
{ defaultMessage: 'Transaction max spans' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.transactionMaxSpansConfigInputHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Limits the amount of spans that are recorded per transaction. Default is 500.'
|
||||
}
|
||||
)}
|
||||
error={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.transactionMaxSpansConfigInputErrorText',
|
||||
{ defaultMessage: 'Must be between 0 and 32000' }
|
||||
)}
|
||||
isInvalid={
|
||||
!isEmpty(transactionMaxSpans) && !isTransactionMaxSpansValid
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyOut.settingsSection.transactionMaxSpansConfigInputPlaceholderText',
|
||||
{ defaultMessage: 'Set transaction max spans' }
|
||||
)}
|
||||
value={
|
||||
transactionMaxSpans === '' ? '' : Number(transactionMaxSpans)
|
||||
}
|
||||
min={0}
|
||||
max={32000}
|
||||
onChange={e => {
|
||||
e.preventDefault();
|
||||
setTransactionMaxSpans(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiForm,
|
||||
EuiPortal,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiSpacer
|
||||
} from '@elastic/eui';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt';
|
||||
import { callApmApi } from '../../../../../services/rest/callApmApi';
|
||||
import { Config } from '../index';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { ServiceSection } from './ServiceSection';
|
||||
import { DeleteButton } from './DeleteButton';
|
||||
import { transactionMaxSpansRt } from '../../../../../../common/runtime_types/transaction_max_spans_rt';
|
||||
import { useFetcher } from '../../../../../hooks/useFetcher';
|
||||
import { isRumAgentName } from '../../../../../../common/agent_name';
|
||||
import { ALL_OPTION_VALUE } from '../../../../../../common/agent_configuration_constants';
|
||||
import { saveConfig } from './saveConfig';
|
||||
import { useKibanaCore } from '../../../../../../../observability/public';
|
||||
|
||||
const defaultSettings = {
|
||||
TRANSACTION_SAMPLE_RATE: '1.0',
|
||||
CAPTURE_BODY: 'off',
|
||||
TRANSACTION_MAX_SPANS: '500'
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
onDeleted: () => void;
|
||||
selectedConfig: Config | null;
|
||||
}
|
||||
|
||||
export function AddEditFlyout({
|
||||
onClose,
|
||||
onSaved,
|
||||
onDeleted,
|
||||
selectedConfig
|
||||
}: Props) {
|
||||
const {
|
||||
notifications: { toasts }
|
||||
} = useKibanaCore();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// config conditions (service)
|
||||
const [serviceName, setServiceName] = useState<string>(
|
||||
selectedConfig ? selectedConfig.service.name || ALL_OPTION_VALUE : ''
|
||||
);
|
||||
const [environment, setEnvironment] = useState<string>(
|
||||
selectedConfig ? selectedConfig.service.environment || ALL_OPTION_VALUE : ''
|
||||
);
|
||||
|
||||
const { data: { agentName } = { agentName: undefined } } = useFetcher(
|
||||
() => {
|
||||
if (serviceName === ALL_OPTION_VALUE) {
|
||||
return { agentName: undefined };
|
||||
}
|
||||
|
||||
if (serviceName) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/agent_name',
|
||||
params: { query: { serviceName } }
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
// config settings
|
||||
const [sampleRate, setSampleRate] = useState<string>(
|
||||
(
|
||||
idx(selectedConfig, _ => _.settings.transaction_sample_rate) ||
|
||||
defaultSettings.TRANSACTION_SAMPLE_RATE
|
||||
).toString()
|
||||
);
|
||||
const [captureBody, setCaptureBody] = useState<string>(
|
||||
idx(selectedConfig, _ => _.settings.capture_body) ||
|
||||
defaultSettings.CAPTURE_BODY
|
||||
);
|
||||
const [transactionMaxSpans, setTransactionMaxSpans] = useState<string>(
|
||||
(
|
||||
idx(selectedConfig, _ => _.settings.transaction_max_spans) ||
|
||||
defaultSettings.TRANSACTION_MAX_SPANS
|
||||
).toString()
|
||||
);
|
||||
|
||||
const isRumService = isRumAgentName(agentName);
|
||||
const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate));
|
||||
const isTransactionMaxSpansValid = isRight(
|
||||
transactionMaxSpansRt.decode(transactionMaxSpans)
|
||||
);
|
||||
|
||||
const isFormValid =
|
||||
!!serviceName &&
|
||||
!!environment &&
|
||||
isSampleRateValid &&
|
||||
// captureBody and isTransactionMaxSpansValid are required except if service is RUM
|
||||
(isRumService || (!!captureBody && isTransactionMaxSpansValid)) &&
|
||||
// agent name is required, except if serviceName is "all"
|
||||
(serviceName === ALL_OPTION_VALUE || agentName !== undefined);
|
||||
|
||||
const handleSubmitEvent = async (
|
||||
event:
|
||||
| React.FormEvent<HTMLFormElement>
|
||||
| React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
event.preventDefault();
|
||||
setIsSaving(true);
|
||||
|
||||
await saveConfig({
|
||||
serviceName,
|
||||
environment,
|
||||
sampleRate,
|
||||
captureBody,
|
||||
transactionMaxSpans,
|
||||
configurationId: selectedConfig ? selectedConfig.id : undefined,
|
||||
agentName,
|
||||
toasts
|
||||
});
|
||||
setIsSaving(false);
|
||||
onSaved();
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout size="s" onClose={onClose} ownFocus={true}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{selectedConfig
|
||||
? i18n.translate(
|
||||
'xpack.apm.settings.agentConf.editConfigTitle',
|
||||
{ defaultMessage: 'Edit configuration' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.createConfigTitle',
|
||||
{ defaultMessage: 'Create configuration' }
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiText size="s">
|
||||
This allows you to fine-tune your agent configuration directly in
|
||||
Kibana. Best of all, changes are automatically propagated to your
|
||||
APM agents so there’s no need to redeploy.
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiForm>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<form
|
||||
onKeyPress={e => {
|
||||
const didClickEnter = e.which === 13;
|
||||
if (didClickEnter) {
|
||||
handleSubmitEvent(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ServiceSection
|
||||
isReadOnly={Boolean(selectedConfig)}
|
||||
//
|
||||
// environment
|
||||
environment={environment}
|
||||
setEnvironment={setEnvironment}
|
||||
//
|
||||
// serviceName
|
||||
serviceName={serviceName}
|
||||
setServiceName={setServiceName}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<SettingsSection
|
||||
isRumService={isRumService}
|
||||
//
|
||||
// sampleRate
|
||||
sampleRate={sampleRate}
|
||||
setSampleRate={setSampleRate}
|
||||
isSampleRateValid={isSampleRateValid}
|
||||
//
|
||||
// captureBody
|
||||
captureBody={captureBody}
|
||||
setCaptureBody={setCaptureBody}
|
||||
//
|
||||
// transactionMaxSpans
|
||||
transactionMaxSpans={transactionMaxSpans}
|
||||
setTransactionMaxSpans={setTransactionMaxSpans}
|
||||
isTransactionMaxSpansValid={isTransactionMaxSpansValid}
|
||||
/>
|
||||
</form>
|
||||
</EuiForm>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
{selectedConfig ? (
|
||||
<DeleteButton
|
||||
selectedConfig={selectedConfig}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.cancelButtonLabel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
type="submit"
|
||||
fill
|
||||
isLoading={isSaving}
|
||||
iconSide="right"
|
||||
isDisabled={!isFormValid}
|
||||
onClick={handleSubmitEvent}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.saveConfigurationButtonLabel',
|
||||
{ defaultMessage: 'Save' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { NotificationsStart } from 'kibana/public';
|
||||
import { trackEvent } from '../../../../../../../infra/public/hooks/use_track_metric';
|
||||
import { isRumAgentName } from '../../../../../../common/agent_name';
|
||||
import {
|
||||
getOptionLabel,
|
||||
omitAllOption
|
||||
} from '../../../../../../common/agent_configuration_constants';
|
||||
import { callApmApi } from '../../../../../services/rest/callApmApi';
|
||||
|
||||
interface Settings {
|
||||
transaction_sample_rate: number;
|
||||
capture_body?: string;
|
||||
transaction_max_spans?: number;
|
||||
}
|
||||
|
||||
export async function saveConfig({
|
||||
serviceName,
|
||||
environment,
|
||||
sampleRate,
|
||||
captureBody,
|
||||
transactionMaxSpans,
|
||||
configurationId,
|
||||
agentName,
|
||||
toasts
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
sampleRate: string;
|
||||
captureBody: string;
|
||||
transactionMaxSpans: string;
|
||||
configurationId?: string;
|
||||
agentName?: string;
|
||||
toasts: NotificationsStart['toasts'];
|
||||
}) {
|
||||
trackEvent({ app: 'apm', name: 'save_agent_configuration' });
|
||||
|
||||
try {
|
||||
const settings: Settings = {
|
||||
transaction_sample_rate: Number(sampleRate)
|
||||
};
|
||||
|
||||
if (!isRumAgentName(agentName)) {
|
||||
settings.capture_body = captureBody;
|
||||
settings.transaction_max_spans = Number(transactionMaxSpans);
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
agent_name: agentName,
|
||||
service: {
|
||||
name: omitAllOption(serviceName),
|
||||
environment: omitAllOption(environment)
|
||||
},
|
||||
settings
|
||||
};
|
||||
|
||||
if (configurationId) {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
method: 'PUT',
|
||||
params: {
|
||||
path: { configurationId },
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/new',
|
||||
method: 'POST',
|
||||
params: {
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.saveConfig.succeeded.title',
|
||||
{ defaultMessage: 'Configuration saved' }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.saveConfig.succeeded.text',
|
||||
{
|
||||
defaultMessage:
|
||||
'The configuration for "{serviceName}" was saved. It will take some time to propagate to the agents.',
|
||||
values: { serviceName: getOptionLabel(serviceName) }
|
||||
}
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.saveConfig.failed.title',
|
||||
{ defaultMessage: 'Configuration could not be saved' }
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.saveConfig.failed.text',
|
||||
{
|
||||
defaultMessage:
|
||||
'Something went wrong when saving the configuration for "{serviceName}". Error: "{errorMessage}"',
|
||||
values: {
|
||||
serviceName: getOptionLabel(serviceName),
|
||||
errorMessage: error.message
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,17 +5,26 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiHealth,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FETCH_STATUS } from '../../../hooks/useFetcher';
|
||||
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
||||
import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt';
|
||||
import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
|
||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
||||
import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations';
|
||||
import { Config } from '.';
|
||||
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
|
||||
import { px, units } from '../../../../style/variables';
|
||||
import { getOptionLabel } from '../../../../../common/agent_configuration_constants';
|
||||
|
||||
export function SettingsList({
|
||||
export function AgentConfigurationList({
|
||||
status,
|
||||
data,
|
||||
setIsFlyoutOpen,
|
||||
|
@ -23,21 +32,44 @@ export function SettingsList({
|
|||
}: {
|
||||
status: FETCH_STATUS;
|
||||
data: AgentConfigurationListAPIResponse;
|
||||
setIsFlyoutOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedConfig: React.Dispatch<React.SetStateAction<Config | null>>;
|
||||
setIsFlyoutOpen: (val: boolean) => void;
|
||||
setSelectedConfig: (val: Config | null) => void;
|
||||
}) {
|
||||
const columns: Array<ITableColumn<Config>> = [
|
||||
{
|
||||
field: 'applied_by_agent',
|
||||
align: 'center',
|
||||
width: px(units.double),
|
||||
name: '',
|
||||
sortable: true,
|
||||
render: (isApplied: boolean) => (
|
||||
<EuiToolTip
|
||||
content={
|
||||
isApplied
|
||||
? i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.appliedTooltipMessage',
|
||||
{ defaultMessage: 'Applied by at least one agent' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.notAppliedTooltipMessage',
|
||||
{ defaultMessage: 'Not yet applied by any agents' }
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiHealth color={isApplied ? 'success' : theme.euiColorLightShade} />
|
||||
</EuiToolTip>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'service.name',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Service name'
|
||||
}
|
||||
{ defaultMessage: 'Service name' }
|
||||
),
|
||||
sortable: true,
|
||||
render: (_, config: Config) => (
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
|
@ -45,7 +77,7 @@ export function SettingsList({
|
|||
setIsFlyoutOpen(true);
|
||||
}}
|
||||
>
|
||||
{config.service.name}
|
||||
{getOptionLabel(config.service.name)}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
},
|
||||
|
@ -53,50 +85,64 @@ export function SettingsList({
|
|||
field: 'service.environment',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.environmentColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Service environment'
|
||||
}
|
||||
{ defaultMessage: 'Service environment' }
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: string) => value
|
||||
render: (value: string) => getOptionLabel(value)
|
||||
},
|
||||
{
|
||||
field: 'settings.transaction_sample_rate',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.sampleRateColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Sample rate'
|
||||
}
|
||||
{ defaultMessage: 'Sample rate' }
|
||||
),
|
||||
dataType: 'number',
|
||||
sortable: true,
|
||||
render: (value: number) => value
|
||||
},
|
||||
{
|
||||
field: 'settings.capture_body',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.captureBodyColumnLabel',
|
||||
{ defaultMessage: 'Capture body' }
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: string) => value
|
||||
},
|
||||
{
|
||||
field: 'settings.transaction_max_spans',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.transactionMaxSpansColumnLabel',
|
||||
{ defaultMessage: 'Transaction max spans' }
|
||||
),
|
||||
dataType: 'number',
|
||||
sortable: true,
|
||||
render: (value: number) => value
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
field: '@timestamp',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.lastUpdatedColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Last updated'
|
||||
}
|
||||
{ defaultMessage: 'Last updated' }
|
||||
),
|
||||
sortable: true,
|
||||
render: (value: number) => (value ? moment(value).fromNow() : null)
|
||||
render: (value: number) => (
|
||||
<TimestampTooltip time={value} precision="minutes" />
|
||||
)
|
||||
},
|
||||
{
|
||||
width: px(units.double),
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.editButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit'
|
||||
}
|
||||
{ defaultMessage: 'Edit' }
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.editButtonDescription',
|
||||
{
|
||||
defaultMessage: 'Edit this config'
|
||||
}
|
||||
{ defaultMessage: 'Edit this config' }
|
||||
),
|
||||
icon: 'pencil',
|
||||
color: 'primary',
|
||||
|
@ -137,10 +183,8 @@ export function SettingsList({
|
|||
actions={
|
||||
<EuiButton color="primary" fill onClick={() => setIsFlyoutOpen(true)}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.createConfigButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create configuration'
|
||||
}
|
||||
'xpack.apm.settings.agentConf.configTable.createConfigButtonLabel',
|
||||
{ defaultMessage: 'Create configuration' }
|
||||
)}
|
||||
</EuiButton>
|
||||
}
|
||||
|
@ -154,7 +198,7 @@ export function SettingsList({
|
|||
<>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configTable.failurePromptText',
|
||||
'xpack.apm.settings.agentConf.configTable.configTable.failurePromptText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.'
|
||||
|
@ -165,26 +209,23 @@ export function SettingsList({
|
|||
}
|
||||
/>
|
||||
);
|
||||
const hasConfigurations = !isEmpty(data);
|
||||
|
||||
if (status === 'failure') {
|
||||
return failurePrompt;
|
||||
}
|
||||
if (status === 'success') {
|
||||
if (hasConfigurations) {
|
||||
return (
|
||||
<ManagedTable
|
||||
noItemsMessage={<LoadingStatePrompt />}
|
||||
columns={columns}
|
||||
items={data}
|
||||
initialSortField="service.name"
|
||||
initialSortDirection="asc"
|
||||
initialPageSize={50}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return emptyStatePrompt;
|
||||
}
|
||||
|
||||
if (status === 'success' && isEmpty(data)) {
|
||||
return emptyStatePrompt;
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<ManagedTable
|
||||
noItemsMessage={<LoadingStatePrompt />}
|
||||
columns={columns}
|
||||
items={data}
|
||||
initialSortField="service.name"
|
||||
initialSortDirection="asc"
|
||||
initialPageSize={50}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiButton
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations';
|
||||
import { callApmApi } from '../../../../services/rest/callApmApi';
|
||||
import { HomeLink } from '../../../shared/Links/apm/HomeLink';
|
||||
import { AgentConfigurationList } from './AgentConfigurationList';
|
||||
import { useTrackPageview } from '../../../../../../infra/public';
|
||||
import { AddEditFlyout } from './AddEditFlyout';
|
||||
|
||||
export type Config = AgentConfigurationListAPIResponse[0];
|
||||
|
||||
export function AgentConfigurations() {
|
||||
const { data = [], status, refetch } = useFetcher(
|
||||
() => callApmApi({ pathname: `/api/apm/settings/agent-configuration` }),
|
||||
[],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
const [selectedConfig, setSelectedConfig] = useState<Config | null>(null);
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
|
||||
|
||||
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
|
||||
useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 });
|
||||
|
||||
const hasConfigurations = !isEmpty(data);
|
||||
|
||||
const onClose = () => {
|
||||
setSelectedConfig(null);
|
||||
setIsFlyoutOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFlyoutOpen && (
|
||||
<AddEditFlyout
|
||||
selectedConfig={selectedConfig}
|
||||
onClose={onClose}
|
||||
onSaved={() => {
|
||||
onClose();
|
||||
refetch();
|
||||
}}
|
||||
onDeleted={() => {
|
||||
onClose();
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
{i18n.translate('xpack.apm.settings.agentConf.pageTitle', {
|
||||
defaultMessage: 'Settings'
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HomeLink>
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="arrowLeft">
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.returnToOverviewLinkLabel',
|
||||
{ defaultMessage: 'Return to overview' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</HomeLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configurationsPanelTitle',
|
||||
{ defaultMessage: 'Agent remote configuration' }
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
{hasConfigurations ? (
|
||||
<CreateConfigurationButton onClick={() => setIsFlyoutOpen(true)} />
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<AgentConfigurationList
|
||||
status={status}
|
||||
data={data}
|
||||
setIsFlyoutOpen={setIsFlyoutOpen}
|
||||
setSelectedConfig={setSelectedConfig}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateConfigurationButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={onClick}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.createConfigButtonLabel',
|
||||
{ defaultMessage: 'Create configuration' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiPanel,
|
||||
EuiBetaBadge,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiButton,
|
||||
EuiLink
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations';
|
||||
import { AddSettingsFlyout } from './AddSettings/AddSettingFlyout';
|
||||
import { callApmApi } from '../../../services/rest/callApmApi';
|
||||
import { HomeLink } from '../../shared/Links/apm/HomeLink';
|
||||
import { SettingsList } from './SettingsList';
|
||||
import { useTrackPageview } from '../../../../../infra/public';
|
||||
|
||||
export type Config = AgentConfigurationListAPIResponse[0];
|
||||
|
||||
export function Settings() {
|
||||
const { data = [], status, refresh } = useFetcher(
|
||||
() =>
|
||||
callApmApi({
|
||||
pathname: `/api/apm/settings/agent-configuration`
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const [selectedConfig, setSelectedConfig] = useState<Config | null>(null);
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
|
||||
|
||||
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
|
||||
useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 });
|
||||
|
||||
const RETURN_TO_OVERVIEW_LINK_LABEL = i18n.translate(
|
||||
'xpack.apm.settings.agentConf.returnToOverviewLinkLabel',
|
||||
{
|
||||
defaultMessage: 'Return to overview'
|
||||
}
|
||||
);
|
||||
|
||||
const hasConfigurations = !isEmpty(data);
|
||||
|
||||
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.agentConf.pageTitle', {
|
||||
defaultMessage: 'Settings'
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HomeLink>
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="arrowLeft">
|
||||
{RETURN_TO_OVERVIEW_LINK_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</HomeLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.agentConf.configurationsPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Configurations'
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.betaBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Beta'
|
||||
}
|
||||
)}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.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.agentConf.createConfigButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create configuration'
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.apm.settings.agentConf.betaCallOutTitle',
|
||||
{
|
||||
defaultMessage: 'APM Agent Configuration (BETA)'
|
||||
}
|
||||
)}
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.agentConf.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.agentConf.agentConfigDocsLinkLabel',
|
||||
{ defaultMessage: 'Learn more in our docs.' }
|
||||
)}
|
||||
</EuiLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<SettingsList
|
||||
status={status}
|
||||
data={data}
|
||||
setIsFlyoutOpen={setIsFlyoutOpen}
|
||||
setSelectedConfig={setSelectedConfig}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_RESULT,
|
||||
URL_FULL,
|
||||
USER_ID,
|
||||
TRANSACTION_PAGE_URL
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { asTime } from '../../../../utils/formatters';
|
||||
import {
|
||||
IStickyProperty,
|
||||
StickyProperties
|
||||
} from '../../../shared/StickyProperties';
|
||||
import { ErrorCountBadge } from './ErrorCountBadge';
|
||||
import { isRumAgentName } from '../../../../../common/agent_name';
|
||||
import { fontSize } from '../../../../style/variables';
|
||||
import { PercentOfParent } from './PercentOfParent';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
totalDuration?: number;
|
||||
errorCount: number;
|
||||
}
|
||||
|
||||
const ErrorTitle = styled.span`
|
||||
font-size: ${fontSize};
|
||||
`;
|
||||
|
||||
export function StickyTransactionProperties({
|
||||
transaction,
|
||||
totalDuration,
|
||||
errorCount
|
||||
}: Props) {
|
||||
const timestamp = transaction['@timestamp'];
|
||||
|
||||
const isRumAgent = isRumAgentName(transaction.agent.name);
|
||||
const { urlFieldName, urlValue } = isRumAgent
|
||||
? {
|
||||
urlFieldName: TRANSACTION_PAGE_URL,
|
||||
urlValue: idx(transaction, _ => _.transaction.page.url)
|
||||
}
|
||||
: {
|
||||
urlFieldName: URL_FULL,
|
||||
urlValue: idx(transaction, _ => _.url.full)
|
||||
};
|
||||
|
||||
const duration = transaction.transaction.duration.us;
|
||||
|
||||
const noErrorsText = i18n.translate(
|
||||
'xpack.apm.transactionDetails.errorsNone',
|
||||
{
|
||||
defaultMessage: 'None'
|
||||
}
|
||||
);
|
||||
|
||||
const stickyProperties: IStickyProperty[] = [
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.timestampLabel', {
|
||||
defaultMessage: 'Timestamp'
|
||||
}),
|
||||
fieldName: '@timestamp',
|
||||
val: timestamp,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: urlFieldName,
|
||||
label: 'URL',
|
||||
val: urlValue || NOT_AVAILABLE_LABEL,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.durationLabel', {
|
||||
defaultMessage: 'Duration'
|
||||
}),
|
||||
fieldName: TRANSACTION_DURATION,
|
||||
val: asTime(duration),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.percentOfTraceLabel',
|
||||
{
|
||||
defaultMessage: '% of trace'
|
||||
}
|
||||
),
|
||||
val: (
|
||||
<PercentOfParent
|
||||
duration={duration}
|
||||
totalDuration={totalDuration}
|
||||
parentType="trace"
|
||||
/>
|
||||
),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.resultLabel', {
|
||||
defaultMessage: 'Result'
|
||||
}),
|
||||
fieldName: TRANSACTION_RESULT,
|
||||
val: idx(transaction, _ => _.transaction.result) || NOT_AVAILABLE_LABEL,
|
||||
width: '14%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.errorsOverviewLabel',
|
||||
{
|
||||
defaultMessage: 'Errors'
|
||||
}
|
||||
),
|
||||
val: errorCount ? (
|
||||
<>
|
||||
<ErrorCountBadge>{errorCount}</ErrorCountBadge>
|
||||
<ErrorTitle>
|
||||
|
||||
{i18n.translate('xpack.apm.transactionDetails.errorsOverviewLink', {
|
||||
values: { errorCount },
|
||||
defaultMessage:
|
||||
'{errorCount, plural, one {Related error} other {Related errors}}'
|
||||
})}
|
||||
</ErrorTitle>
|
||||
</>
|
||||
) : (
|
||||
noErrorsText
|
||||
),
|
||||
width: '18%'
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.transactionDetails.userIdLabel', {
|
||||
defaultMessage: 'User ID'
|
||||
}),
|
||||
fieldName: USER_ID,
|
||||
val: idx(transaction, _ => _.user.id) || NOT_AVAILABLE_LABEL,
|
||||
truncated: true,
|
||||
width: '18%'
|
||||
}
|
||||
];
|
||||
|
||||
const { user_agent: userAgent } = transaction;
|
||||
|
||||
if (userAgent) {
|
||||
const { os, device } = userAgent;
|
||||
const width = '25%';
|
||||
stickyProperties.push({
|
||||
label: i18n.translate('xpack.apm.transactionDetails.userAgentLabel', {
|
||||
defaultMessage: 'User agent'
|
||||
}),
|
||||
val: [userAgent.name, userAgent.version].filter(Boolean).join(' '),
|
||||
truncated: true,
|
||||
width
|
||||
});
|
||||
|
||||
if (os) {
|
||||
stickyProperties.push({
|
||||
label: i18n.translate('xpack.apm.transactionDetails.userAgentOsLabel', {
|
||||
defaultMessage: 'User agent OS'
|
||||
}),
|
||||
val: os.full || os.name,
|
||||
truncated: true,
|
||||
width
|
||||
});
|
||||
}
|
||||
|
||||
if (device) {
|
||||
stickyProperties.push({
|
||||
label: i18n.translate(
|
||||
'xpack.apm.transactionDetails.userAgentDeviceLabel',
|
||||
{
|
||||
defaultMessage: 'User agent device'
|
||||
}
|
||||
),
|
||||
val: device.name,
|
||||
width
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
|
@ -24,7 +24,7 @@ import styled from 'styled-components';
|
|||
import { idx } from '@kbn/elastic-idx';
|
||||
import { px, units } from '../../../../../../../style/variables';
|
||||
import { Summary } from '../../../../../../shared/Summary';
|
||||
import { TimestampSummaryItem } from '../../../../../../shared/Summary/TimestampSummaryItem';
|
||||
import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip';
|
||||
import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
|
@ -143,7 +143,7 @@ export function SpanFlyout({
|
|||
<EuiSpacer size="m" />
|
||||
<Summary
|
||||
items={[
|
||||
<TimestampSummaryItem time={span.timestamp.us / 1000} />,
|
||||
<TimestampTooltip time={span.timestamp.us / 1000} />,
|
||||
<DurationSummaryItem
|
||||
duration={span.span.duration.us}
|
||||
totalDuration={totalDuration}
|
||||
|
|
|
@ -22,12 +22,18 @@ import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTra
|
|||
import { fromQuery } from '../../../shared/Links/url_helpers';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { UrlParamsProvider } from '../../../../context/UrlParamsContext';
|
||||
import { KibanaCoreContext } from '../../../../../../observability/public';
|
||||
import { LegacyCoreStart } from 'kibana/public';
|
||||
|
||||
jest.spyOn(history, 'push');
|
||||
jest.spyOn(history, 'replace');
|
||||
|
||||
jest.mock('ui/kfetch');
|
||||
|
||||
const coreMock = ({
|
||||
notifications: { toasts: { addWarning: () => {} } }
|
||||
} as unknown) as LegacyCoreStart;
|
||||
|
||||
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||
/* eslint-disable no-console */
|
||||
const originalError = console.error;
|
||||
|
@ -59,11 +65,13 @@ function setup({
|
|||
.mockReturnValue(serviceTransactionTypes);
|
||||
|
||||
return render(
|
||||
<Router history={history}>
|
||||
<UrlParamsProvider>
|
||||
<TransactionOverview />
|
||||
</UrlParamsProvider>
|
||||
</Router>
|
||||
<KibanaCoreContext.Provider value={coreMock}>
|
||||
<Router history={history}>
|
||||
<UrlParamsProvider>
|
||||
<TransactionOverview />
|
||||
</UrlParamsProvider>
|
||||
</Router>
|
||||
</KibanaCoreContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import { mount } from 'enzyme';
|
|||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
jest.mock('ui/kfetch');
|
||||
|
||||
const mockHistoryPush = jest.spyOn(history, 'push');
|
||||
const mockRefreshTimeRange = jest.fn();
|
||||
const MockUrlParamsProvider: React.FC<{
|
||||
|
|
|
@ -11,6 +11,7 @@ import { fromQuery, toQuery } from '../Links/url_helpers';
|
|||
import { history } from '../../../utils/history';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { clearCache } from '../../../services/rest/callApi';
|
||||
|
||||
export function DatePicker() {
|
||||
const location = useLocation();
|
||||
|
@ -113,6 +114,7 @@ export function DatePicker() {
|
|||
refreshInterval={refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={({ start, end }) => {
|
||||
clearCache();
|
||||
refreshTimeRange({ rangeFrom: start, rangeTo: end });
|
||||
}}
|
||||
onRefreshChange={onRefreshChange}
|
||||
|
|
|
@ -15,11 +15,6 @@ describe('StickyProperties', () => {
|
|||
|
||||
it('should render entire component', () => {
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
fieldName: '@timestamp',
|
||||
val: 1536405447640
|
||||
},
|
||||
{
|
||||
fieldName: URL_FULL,
|
||||
label: 'URL',
|
||||
|
@ -51,22 +46,6 @@ describe('StickyProperties', () => {
|
|||
});
|
||||
|
||||
describe('values', () => {
|
||||
it('should render timestamp when fieldName is `@timestamp`', () => {
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'My Timestamp',
|
||||
fieldName: '@timestamp',
|
||||
val: 1536405447640
|
||||
}
|
||||
];
|
||||
|
||||
const wrapper = shallow(
|
||||
<StickyProperties stickyProperties={stickyProperties} />
|
||||
).find('TimestampValue');
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render numbers', () => {
|
||||
const stickyProperties = [
|
||||
{
|
||||
|
|
|
@ -20,35 +20,6 @@ exports[`StickyProperties should render entire component 1`] = `
|
|||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PropertyLabel>
|
||||
<EuiToolTip
|
||||
content={
|
||||
<styled.span>
|
||||
@timestamp
|
||||
</styled.span>
|
||||
}
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
<span>
|
||||
Timestamp
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
</PropertyLabel>
|
||||
<TimestampValue
|
||||
timestamp={1536405447640}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PropertyLabel>
|
||||
<EuiToolTip
|
||||
|
@ -77,7 +48,7 @@ exports[`StickyProperties should render entire component 1`] = `
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="2"
|
||||
key="1"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
|
@ -106,7 +77,7 @@ exports[`StickyProperties should render entire component 1`] = `
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="3"
|
||||
key="2"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
|
@ -135,7 +106,7 @@ exports[`StickyProperties should render entire component 1`] = `
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="4"
|
||||
key="3"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
|
@ -164,9 +135,3 @@ exports[`StickyProperties should render entire component 1`] = `
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`StickyProperties values should render timestamp when fieldName is \`@timestamp\` 1`] = `
|
||||
<TimestampValue
|
||||
timestamp={1536405447640}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -7,10 +7,8 @@
|
|||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
import {
|
||||
fontFamilyCode,
|
||||
fontSizes,
|
||||
|
@ -42,10 +40,6 @@ const PropertyLabel = styled.div`
|
|||
`;
|
||||
PropertyLabel.displayName = 'PropertyLabel';
|
||||
|
||||
const PropertyValueDimmed = styled.span`
|
||||
color: ${theme.euiColorMediumShade};
|
||||
`;
|
||||
|
||||
const propertyValueLineHeight = 1.2;
|
||||
const PropertyValue = styled.div`
|
||||
display: inline-block;
|
||||
|
@ -59,20 +53,6 @@ const PropertyValueTruncated = styled.span`
|
|||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
function TimestampValue({ timestamp }: { timestamp: Date }) {
|
||||
const time = moment(timestamp);
|
||||
const timeAgo = timestamp ? time.fromNow() : NOT_AVAILABLE_LABEL;
|
||||
const timestampFull = timestamp
|
||||
? time.format('MMMM Do YYYY, HH:mm:ss.SSS')
|
||||
: NOT_AVAILABLE_LABEL;
|
||||
|
||||
return (
|
||||
<PropertyValue>
|
||||
{timeAgo} <PropertyValueDimmed>({timestampFull})</PropertyValueDimmed>
|
||||
</PropertyValue>
|
||||
);
|
||||
}
|
||||
|
||||
function getPropertyLabel({ fieldName, label }: Partial<IStickyProperty>) {
|
||||
if (fieldName) {
|
||||
return (
|
||||
|
@ -92,10 +72,6 @@ function getPropertyValue({
|
|||
fieldName,
|
||||
truncated = false
|
||||
}: Partial<IStickyProperty>) {
|
||||
if (fieldName === '@timestamp') {
|
||||
return <TimestampValue timestamp={val as Date} />;
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
return (
|
||||
<EuiToolTip content={String(val)}>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiToolTip } from '@elastic/eui';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
interface Props {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const TimestampSummaryItem = (props: Props) => {
|
||||
const time = moment.tz(props.time, moment.tz.guess());
|
||||
const relativeTimeLabel = time.fromNow();
|
||||
const absoluteTimeLabel = time.format('MMM Do YYYY HH:mm:ss.SSS zz');
|
||||
|
||||
return (
|
||||
<EuiToolTip content={absoluteTimeLabel}>
|
||||
<>{relativeTimeLabel}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export { TimestampSummaryItem };
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import { idx } from '@kbn/elastic-idx';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { Summary } from './';
|
||||
import { TimestampSummaryItem } from './TimestampSummaryItem';
|
||||
import { TimestampTooltip } from '../TimestampTooltip';
|
||||
import { DurationSummaryItem } from './DurationSummaryItem';
|
||||
import { ErrorCountSummaryItem } from './ErrorCountSummaryItem';
|
||||
import { isRumAgentName } from '../../../../common/agent_name';
|
||||
|
@ -47,7 +47,7 @@ const TransactionSummary = ({
|
|||
errorCount
|
||||
}: Props) => {
|
||||
const items = [
|
||||
<TimestampSummaryItem time={transaction.timestamp.us / 1000} />,
|
||||
<TimestampTooltip time={transaction.timestamp.us / 1000} />,
|
||||
<DurationSummaryItem
|
||||
duration={transaction.transaction.duration.us}
|
||||
totalDuration={totalDuration}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { EuiToolTip } from '@elastic/eui';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* timestamp in milliseconds
|
||||
*/
|
||||
time: number;
|
||||
precision?: 'days' | 'minutes' | 'milliseconds';
|
||||
}
|
||||
|
||||
function getPreciseTime(precision: Props['precision']) {
|
||||
switch (precision) {
|
||||
case 'days':
|
||||
return '';
|
||||
case 'minutes':
|
||||
return ', HH:mm';
|
||||
default:
|
||||
return ', HH:mm.SSS';
|
||||
}
|
||||
}
|
||||
|
||||
export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) {
|
||||
const momentTime = moment.tz(time, moment.tz.guess());
|
||||
const relativeTimeLabel = momentTime.fromNow();
|
||||
const absoluteTimeLabel = momentTime.format(
|
||||
`MMM Do YYYY${getPreciseTime(precision)} (ZZ zz)`
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={absoluteTimeLabel}>
|
||||
<>{relativeTimeLabel}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -8,6 +8,8 @@ import React from 'react';
|
|||
import { render } from 'react-testing-library';
|
||||
import { delay, tick } from '../utils/testHelpers';
|
||||
import { useFetcher } from './useFetcher';
|
||||
import { KibanaCoreContext } from '../../../observability/public/context/kibana_core';
|
||||
import { LegacyCoreStart } from 'kibana/public';
|
||||
|
||||
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||
/* eslint-disable no-console */
|
||||
|
@ -19,6 +21,19 @@ afterAll(() => {
|
|||
console.error = originalError;
|
||||
});
|
||||
|
||||
// Wrap the hook with a provider so it can useKibanaCore
|
||||
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<KibanaCoreContext.Provider
|
||||
value={
|
||||
({
|
||||
notifications: { toasts: { addWarning: () => {} } }
|
||||
} as unknown) as LegacyCoreStart
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</KibanaCoreContext.Provider>
|
||||
);
|
||||
|
||||
async function asyncFn(name: string, ms: number) {
|
||||
await delay(ms);
|
||||
return `Hello from ${name}`;
|
||||
|
@ -54,7 +69,8 @@ describe('when simulating race condition', () => {
|
|||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<MyComponent name="John" ms={500} renderFn={renderSpy} />
|
||||
<MyComponent name="John" ms={500} renderFn={renderSpy} />,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
rerender(<MyComponent name="Peter" ms={100} renderFn={renderSpy} />);
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
import { cleanup, renderHook } from 'react-hooks-testing-library';
|
||||
import { delay } from '../utils/testHelpers';
|
||||
import { useFetcher } from './useFetcher';
|
||||
import { KibanaCoreContext } from '../../../observability/public/context/kibana_core';
|
||||
import { LegacyCoreStart } from 'kibana/public';
|
||||
import React from 'react';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
|
@ -20,6 +23,19 @@ afterAll(() => {
|
|||
console.error = originalError;
|
||||
});
|
||||
|
||||
// Wrap the hook with a provider so it can useKibanaCore
|
||||
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<KibanaCoreContext.Provider
|
||||
value={
|
||||
({
|
||||
notifications: { toasts: { addWarning: () => {} } }
|
||||
} as unknown) as LegacyCoreStart
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</KibanaCoreContext.Provider>
|
||||
);
|
||||
|
||||
describe('useFetcher', () => {
|
||||
describe('when resolving after 500ms', () => {
|
||||
let hook: ReturnType<typeof renderHook>;
|
||||
|
@ -29,14 +45,14 @@ describe('useFetcher', () => {
|
|||
await delay(500);
|
||||
return 'response from hook';
|
||||
}
|
||||
hook = renderHook(() => useFetcher(() => fn(), []));
|
||||
hook = renderHook(() => useFetcher(() => fn(), []), { wrapper });
|
||||
});
|
||||
|
||||
it('should have loading spinner initally', async () => {
|
||||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -47,7 +63,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -59,7 +75,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'response from hook',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
});
|
||||
|
@ -73,14 +89,14 @@ describe('useFetcher', () => {
|
|||
await delay(500);
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
hook = renderHook(() => useFetcher(() => fn(), []));
|
||||
hook = renderHook(() => useFetcher(() => fn(), []), { wrapper });
|
||||
});
|
||||
|
||||
it('should have loading spinner initally', async () => {
|
||||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -91,7 +107,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
@ -103,7 +119,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: expect.any(Error),
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'failure'
|
||||
});
|
||||
});
|
||||
|
@ -112,19 +128,21 @@ describe('useFetcher', () => {
|
|||
describe('when a hook already has data', () => {
|
||||
it('should show "first response" while loading "second response"', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const hook = renderHook(
|
||||
({ callback, args }) => useFetcher(callback, args),
|
||||
{
|
||||
initialProps: {
|
||||
callback: async () => 'first response',
|
||||
args: ['a']
|
||||
}
|
||||
},
|
||||
wrapper
|
||||
}
|
||||
);
|
||||
expect(hook.result.current).toEqual({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
|
||||
|
@ -134,7 +152,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'first response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
|
@ -153,7 +171,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'first response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'loading'
|
||||
});
|
||||
|
||||
|
@ -164,7 +182,7 @@ describe('useFetcher', () => {
|
|||
expect(hook.result.current).toEqual({
|
||||
data: 'second response',
|
||||
error: undefined,
|
||||
refresh: expect.any(Function),
|
||||
refetch: expect.any(Function),
|
||||
status: 'success'
|
||||
});
|
||||
});
|
||||
|
@ -176,7 +194,8 @@ describe('useFetcher', () => {
|
|||
initialProps: {
|
||||
callback: async () => 'data response',
|
||||
args: ['a']
|
||||
}
|
||||
},
|
||||
wrapper
|
||||
}
|
||||
);
|
||||
await hook.waitForNextUpdate();
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import React, { useContext, useEffect, useState, useMemo } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext';
|
||||
import { useComponentId } from './useComponentId';
|
||||
import { KFetchError } from '../../../../../../src/legacy/ui/public/kfetch/kfetch_error';
|
||||
import { useKibanaCore } from '../../../observability/public';
|
||||
|
||||
export enum FETCH_STATUS {
|
||||
LOADING = 'loading',
|
||||
|
@ -31,7 +31,7 @@ export function useFetcher<TState>(
|
|||
options?: {
|
||||
preservePreviousData?: boolean;
|
||||
}
|
||||
): Result<TState> & { refresh: () => void };
|
||||
): Result<TState> & { refetch: () => void };
|
||||
|
||||
// To avoid infinite rescursion when infering the type of `TState` `initialState` must be given if `prevResult` is consumed
|
||||
export function useFetcher<TState>(
|
||||
|
@ -41,7 +41,7 @@ export function useFetcher<TState>(
|
|||
preservePreviousData?: boolean;
|
||||
initialState: TState;
|
||||
}
|
||||
): Result<TState> & { refresh: () => void };
|
||||
): Result<TState> & { refetch: () => void };
|
||||
|
||||
export function useFetcher(
|
||||
fn: Function,
|
||||
|
@ -51,6 +51,11 @@ export function useFetcher(
|
|||
initialState?: unknown;
|
||||
} = {}
|
||||
) {
|
||||
const {
|
||||
notifications: {
|
||||
toasts: { addWarning }
|
||||
}
|
||||
} = useKibanaCore();
|
||||
const { preservePreviousData = true } = options;
|
||||
const id = useComponentId();
|
||||
const { dispatchStatus } = useContext(LoadingIndicatorContext);
|
||||
|
@ -93,7 +98,7 @@ export function useFetcher(
|
|||
} catch (e) {
|
||||
const err = e as KFetchError;
|
||||
if (!didCancel) {
|
||||
toastNotifications.addWarning({
|
||||
addWarning({
|
||||
title: i18n.translate('xpack.apm.fetcher.error.title', {
|
||||
defaultMessage: `Error while fetching resource`
|
||||
}),
|
||||
|
@ -141,14 +146,13 @@ export function useFetcher(
|
|||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...result,
|
||||
refresh: () => {
|
||||
refetch: () => {
|
||||
// this will invalidate the deps to `useEffect` and will result in a new request
|
||||
setCounter(count => count + 1);
|
||||
}
|
||||
}),
|
||||
[result]
|
||||
);
|
||||
};
|
||||
}, [result]);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import * as kfetchModule from 'ui/kfetch';
|
||||
import { mockNow } from '../../utils/testHelpers';
|
||||
import { _clearCache, callApi } from '../rest/callApi';
|
||||
import { clearCache, callApi } from '../rest/callApi';
|
||||
import { SessionStorageMock } from './SessionStorageMock';
|
||||
|
||||
jest.mock('ui/kfetch');
|
||||
|
@ -24,7 +24,7 @@ describe('callApi', () => {
|
|||
|
||||
afterEach(() => {
|
||||
kfetchSpy.mockClear();
|
||||
_clearCache();
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe('apm_debug', () => {
|
||||
|
|
|
@ -31,12 +31,12 @@ function fetchOptionsWithDebug(fetchOptions: KFetchOptions) {
|
|||
|
||||
const cache = new LRU<string, any>({ max: 100, maxAge: 1000 * 60 * 60 });
|
||||
|
||||
export function _clearCache() {
|
||||
export function clearCache() {
|
||||
cache.reset();
|
||||
}
|
||||
|
||||
export async function callApi<T = void>(
|
||||
fetchOptions: KFetchOptions,
|
||||
fetchOptions: KFetchOptions & { forceCache?: boolean },
|
||||
options?: KFetchKibanaOptions
|
||||
): Promise<T> {
|
||||
const cacheKey = getCacheKey(fetchOptions);
|
||||
|
@ -57,7 +57,11 @@ export async function callApi<T = void>(
|
|||
|
||||
// only cache items that has a time range with `start` and `end` params,
|
||||
// and where `end` is not a timestamp in the future
|
||||
function isCachable(fetchOptions: KFetchOptions) {
|
||||
function isCachable(fetchOptions: KFetchOptions & { forceCache?: boolean }) {
|
||||
if (fetchOptions.forceCache) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!(fetchOptions.query && fetchOptions.query.start && fetchOptions.query.end)
|
||||
) {
|
||||
|
|
|
@ -110,7 +110,7 @@ Object {
|
|||
"myIndex",
|
||||
"myIndex",
|
||||
],
|
||||
"terminate_after": 1,
|
||||
"terminateAfter": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -9,18 +9,14 @@ import {
|
|||
SERVICE_AGENT_NAME,
|
||||
SERVICE_NAME
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../typings/common';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
export type ServiceAgentNameAPIResponse = PromiseReturnType<
|
||||
typeof getServiceAgentName
|
||||
>;
|
||||
export async function getServiceAgentName(serviceName: string, setup: Setup) {
|
||||
const { start, end, client, config } = setup;
|
||||
|
||||
const params = {
|
||||
terminate_after: 1,
|
||||
terminateAfter: 1,
|
||||
index: [
|
||||
config.get<string>('apm_oss.errorIndices'),
|
||||
config.get<string>('apm_oss.transactionIndices'),
|
||||
|
|
|
@ -9,13 +9,9 @@ import {
|
|||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../typings/common';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
export type ServiceTransactionTypesAPIResponse = PromiseReturnType<
|
||||
typeof getServiceTransactionTypes
|
||||
>;
|
||||
export async function getServiceTransactionTypes(
|
||||
serviceName: string,
|
||||
setup: Setup
|
||||
|
|
|
@ -7,7 +7,6 @@ Object {
|
|||
"environments": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ENVIRONMENT_NOT_DEFINED",
|
||||
"size": 100,
|
||||
},
|
||||
},
|
||||
|
@ -53,21 +52,47 @@ Object {
|
|||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
"service.name": Object {
|
||||
"value": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.environment": "bar",
|
||||
"service.environment": Object {
|
||||
"value": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 1,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
|
@ -78,25 +103,40 @@ Object {
|
|||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
"service.name": Object {
|
||||
"value": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 1,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
|
@ -109,7 +149,7 @@ Object {
|
|||
"services": Object {
|
||||
"terms": Object {
|
||||
"field": "service.name",
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -145,8 +185,8 @@ Object {
|
|||
"environments": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ENVIRONMENT_NOT_DEFINED",
|
||||
"size": 100,
|
||||
"missing": "ALL_OPTION_VALUE",
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,16 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface AgentConfigurationIntake {
|
||||
settings: {
|
||||
transaction_sample_rate: number;
|
||||
};
|
||||
export interface AgentConfiguration {
|
||||
'@timestamp': number;
|
||||
applied_by_agent?: boolean;
|
||||
etag?: string;
|
||||
agent_name?: string;
|
||||
service: {
|
||||
name: string;
|
||||
name?: string;
|
||||
environment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentConfiguration extends AgentConfigurationIntake {
|
||||
'@timestamp': number;
|
||||
settings: {
|
||||
transaction_sample_rate?: number;
|
||||
capture_body?: string;
|
||||
transaction_max_spans?: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { InternalCoreSetup } from 'src/core/server';
|
||||
import Boom from 'boom';
|
||||
import { CallCluster } from '../../../../../../../../src/legacy/core_plugins/elasticsearch';
|
||||
|
||||
export async function createApmAgentConfigurationIndex(
|
||||
core: InternalCoreSetup
|
||||
|
@ -19,57 +19,84 @@ export async function createApmAgentConfigurationIndex(
|
|||
'admin'
|
||||
);
|
||||
const indexExists = await callWithInternalUser('indices.exists', { index });
|
||||
const result = indexExists
|
||||
? await updateExistingIndex(index, callWithInternalUser)
|
||||
: await createNewIndex(index, callWithInternalUser);
|
||||
|
||||
if (!indexExists) {
|
||||
const result = await callWithInternalUser('indices.create', {
|
||||
index,
|
||||
body: {
|
||||
settings: {
|
||||
'index.auto_expand_replicas': '0-1'
|
||||
},
|
||||
mappings: {
|
||||
properties: {
|
||||
'@timestamp': {
|
||||
type: 'date'
|
||||
},
|
||||
settings: {
|
||||
properties: {
|
||||
transaction_sample_rate: {
|
||||
type: 'scaled_float',
|
||||
scaling_factor: 1000,
|
||||
ignore_malformed: true,
|
||||
coerce: false
|
||||
}
|
||||
}
|
||||
},
|
||||
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 Agent Configuration index '${index}'`
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
console.error(err.stack);
|
||||
throw Boom.boomify(err, { statusCode: 500 });
|
||||
}
|
||||
if (!result.acknowledged) {
|
||||
const resultError =
|
||||
result && result.error && JSON.stringify(result.error);
|
||||
throw new Error(
|
||||
`Unable to create APM Agent Configuration index '${index}': ${resultError}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Could not create APM Agent configuration:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function createNewIndex(index: string, callWithInternalUser: CallCluster) {
|
||||
return callWithInternalUser('indices.create', {
|
||||
index,
|
||||
body: {
|
||||
settings: { 'index.auto_expand_replicas': '0-1' },
|
||||
mappings: { properties: mappingProperties }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Necessary for migration reasons
|
||||
// Added in 7.5: `capture_body`, `transaction_max_spans`, `applied_by_agent`, `agent_name` and `etag`
|
||||
function updateExistingIndex(index: string, callWithInternalUser: CallCluster) {
|
||||
return callWithInternalUser('indices.putMapping', {
|
||||
index,
|
||||
body: { properties: mappingProperties }
|
||||
});
|
||||
}
|
||||
|
||||
const mappingProperties = {
|
||||
'@timestamp': {
|
||||
type: 'date'
|
||||
},
|
||||
service: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
},
|
||||
environment: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
properties: {
|
||||
transaction_sample_rate: {
|
||||
type: 'scaled_float',
|
||||
scaling_factor: 1000,
|
||||
ignore_malformed: true,
|
||||
coerce: false
|
||||
},
|
||||
capture_body: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
},
|
||||
transaction_max_spans: {
|
||||
type: 'short'
|
||||
}
|
||||
}
|
||||
},
|
||||
applied_by_agent: {
|
||||
type: 'boolean'
|
||||
},
|
||||
agent_name: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
},
|
||||
etag: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 hash from 'object-hash';
|
||||
import { IndexDocumentParams } from 'elasticsearch';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { AgentConfiguration } from './configuration_types';
|
||||
|
||||
export async function createOrUpdateConfiguration({
|
||||
configurationId,
|
||||
configuration,
|
||||
setup
|
||||
}: {
|
||||
configurationId?: string;
|
||||
configuration: Omit<
|
||||
AgentConfiguration,
|
||||
'@timestamp' | 'applied_by_agent' | 'etag'
|
||||
>;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params: IndexDocumentParams<AgentConfiguration> = {
|
||||
type: '_doc',
|
||||
refresh: true,
|
||||
index: config.get<string>('apm_oss.apmAgentConfigurationIndex'),
|
||||
body: {
|
||||
agent_name: configuration.agent_name,
|
||||
service: {
|
||||
name: configuration.service.name,
|
||||
environment: configuration.service.environment
|
||||
},
|
||||
settings: configuration.settings,
|
||||
'@timestamp': Date.now(),
|
||||
applied_by_agent: false,
|
||||
etag: hash(configuration)
|
||||
}
|
||||
};
|
||||
|
||||
// by specifying an id elasticsearch will delete the previous doc and insert the updated doc
|
||||
if (configurationId) {
|
||||
params.id = configurationId;
|
||||
}
|
||||
|
||||
return client.index(params);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { idx } from '@kbn/elastic-idx';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { SERVICE_AGENT_NAME } from '../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export async function getAgentNameByService({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
terminateAfter: 1,
|
||||
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: {
|
||||
agent_names: {
|
||||
terms: { field: SERVICE_AGENT_NAME, size: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { aggregations } = await client.search(params);
|
||||
const agentName = idx(aggregations, _ => _.agent_names.buckets[0].key);
|
||||
return { agentName };
|
||||
}
|
|
@ -11,17 +11,22 @@ import {
|
|||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants';
|
||||
|
||||
export async function getAllEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
serviceName: string | undefined;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
// omit filter for service.name if "All" option is selected
|
||||
const serviceNameFilter = serviceName
|
||||
? [{ term: { [SERVICE_NAME]: serviceName } }]
|
||||
: [];
|
||||
|
||||
const params = {
|
||||
index: [
|
||||
config.get<string>('apm_oss.metricsIndices'),
|
||||
|
@ -36,7 +41,7 @@ export async function getAllEnvironments({
|
|||
{
|
||||
terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }
|
||||
},
|
||||
{ term: { [SERVICE_NAME]: serviceName } }
|
||||
...serviceNameFilter
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -44,7 +49,6 @@ export async function getAllEnvironments({
|
|||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED,
|
||||
size: 100
|
||||
}
|
||||
}
|
||||
|
@ -54,5 +58,6 @@ export async function getAllEnvironments({
|
|||
|
||||
const resp = await client.search(params);
|
||||
const buckets = idx(resp.aggregations, _ => _.environments.buckets) || [];
|
||||
return buckets.map(bucket => bucket.key);
|
||||
const environments = buckets.map(bucket => bucket.key);
|
||||
return [ALL_OPTION_VALUE, ...environments];
|
||||
}
|
||||
|
|
|
@ -10,32 +10,32 @@ import {
|
|||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||
import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants';
|
||||
|
||||
export async function getUnavailableEnvironments({
|
||||
export async function getExistingEnvironmentsForService({
|
||||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
serviceName: string | undefined;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const bool = serviceName
|
||||
? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] }
|
||||
: { must_not: [{ exists: { field: SERVICE_NAME } }] };
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.apmAgentConfigurationIndex'),
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { [SERVICE_NAME]: serviceName } }]
|
||||
}
|
||||
},
|
||||
query: { bool },
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED,
|
||||
size: 100
|
||||
missing: ALL_OPTION_VALUE,
|
||||
size: 50
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
import { getAllEnvironments } from './get_all_environments';
|
||||
import { Setup } from '../../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../../typings/common';
|
||||
import { getUnavailableEnvironments } from './get_unavailable_environments';
|
||||
import { getExistingEnvironmentsForService } from './get_existing_environments_for_service';
|
||||
|
||||
export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType<
|
||||
typeof getEnvironments
|
||||
|
@ -17,18 +17,18 @@ export async function getEnvironments({
|
|||
serviceName,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
serviceName: string | undefined;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const [allEnvironments, unavailableEnvironments] = await Promise.all([
|
||||
const [allEnvironments, existingEnvironments] = await Promise.all([
|
||||
getAllEnvironments({ serviceName, setup }),
|
||||
getUnavailableEnvironments({ serviceName, setup })
|
||||
getExistingEnvironmentsForService({ serviceName, setup })
|
||||
]);
|
||||
|
||||
return allEnvironments.map(environment => {
|
||||
return {
|
||||
name: environment,
|
||||
available: !unavailableEnvironments.includes(environment)
|
||||
alreadyConfigured: existingEnvironments.includes(environment)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration_constants';
|
||||
|
||||
export type AgentConfigurationServicesAPIResponse = PromiseReturnType<
|
||||
typeof getServiceNames
|
||||
|
@ -37,7 +38,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) {
|
|||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 100
|
||||
size: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,5 +47,6 @@ export async function getServiceNames({ setup }: { setup: Setup }) {
|
|||
|
||||
const resp = await client.search(params);
|
||||
const buckets = idx(resp.aggregations, _ => _.services.buckets) || [];
|
||||
return buckets.map(bucket => bucket.key).sort();
|
||||
const serviceNames = buckets.map(bucket => bucket.key).sort();
|
||||
return [ALL_OPTION_VALUE, ...serviceNames];
|
||||
}
|
||||
|
|
|
@ -5,30 +5,28 @@
|
|||
*/
|
||||
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { AgentConfigurationIntake } from './configuration_types';
|
||||
import { AgentConfiguration } from './configuration_types';
|
||||
|
||||
export type CreateAgentConfigurationAPIResponse = PromiseReturnType<
|
||||
typeof createConfiguration
|
||||
>;
|
||||
export async function createConfiguration({
|
||||
configuration,
|
||||
export async function markAppliedByAgent({
|
||||
id,
|
||||
body,
|
||||
setup
|
||||
}: {
|
||||
configuration: AgentConfigurationIntake;
|
||||
id: string;
|
||||
body: AgentConfiguration;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
type: '_doc',
|
||||
refresh: true,
|
||||
index: config.get<string>('apm_oss.apmAgentConfigurationIndex'),
|
||||
id, // by specifying the `id` elasticsearch will do an "upsert"
|
||||
body: {
|
||||
'@timestamp': Date.now(),
|
||||
...configuration
|
||||
...body,
|
||||
applied_by_agent: true
|
||||
}
|
||||
};
|
||||
|
||||
return client.index(params);
|
||||
return client.index<AgentConfiguration>(params);
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { getAllEnvironments } from './get_environments/get_all_environments';
|
||||
import { getUnavailableEnvironments } from './get_environments/get_unavailable_environments';
|
||||
import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service';
|
||||
import { getServiceNames } from './get_service_names';
|
||||
import { listConfigurations } from './list_configurations';
|
||||
import { searchConfigurations } from './search';
|
||||
|
@ -34,7 +34,7 @@ describe('agent configuration queries', () => {
|
|||
|
||||
it('fetches unavailable environments', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getUnavailableEnvironments({
|
||||
getExistingEnvironmentsForService({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 const searchMocks = {
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
skipped: 0,
|
||||
failed: 0
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 3,
|
||||
relation: 'eq'
|
||||
},
|
||||
max_score: 0.9808292,
|
||||
hits: [
|
||||
{
|
||||
_index: '.apm-agent-configuration',
|
||||
_type: '_doc',
|
||||
_id: '-aQHsm0BxZLczArvNQYW',
|
||||
_score: 0.9808292,
|
||||
_source: {
|
||||
service: {
|
||||
environment: 'production'
|
||||
},
|
||||
settings: {
|
||||
transaction_sample_rate: 0.3
|
||||
},
|
||||
'@timestamp': 1570649879829,
|
||||
applied_by_agent: false,
|
||||
etag: 'c511f4c1df457371c4446c9c4925662e18726f51'
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: '.apm-agent-configuration',
|
||||
_type: '_doc',
|
||||
_id: '-KQHsm0BxZLczArvNAb0',
|
||||
_score: 0.18232156,
|
||||
_source: {
|
||||
service: {
|
||||
name: 'my_service'
|
||||
},
|
||||
settings: {
|
||||
transaction_sample_rate: 0.2
|
||||
},
|
||||
'@timestamp': 1570649879795,
|
||||
applied_by_agent: false,
|
||||
etag: 'a13cd8fee5a2fcc2ae773a60a4deaf7f76b90a65'
|
||||
}
|
||||
},
|
||||
{
|
||||
_index: '.apm-agent-configuration',
|
||||
_type: '_doc',
|
||||
_id: '96QHsm0BxZLczArvNAbD',
|
||||
_score: 0.0,
|
||||
_source: {
|
||||
service: {},
|
||||
settings: {
|
||||
transaction_sample_rate: 0.1
|
||||
},
|
||||
'@timestamp': 1570649879743,
|
||||
applied_by_agent: false,
|
||||
etag: 'c7f4ba16f00a9c9bf3c49024c5b6d4632ff05ff5'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { searchConfigurations } from './search';
|
||||
import { searchMocks } from './search.mocks';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
|
||||
describe('search configurations', () => {
|
||||
it('should return configuration by matching on service.name', async () => {
|
||||
const res = await searchConfigurations({
|
||||
serviceName: 'my_service',
|
||||
environment: 'production',
|
||||
setup: ({
|
||||
config: { get: () => '' },
|
||||
client: { search: async () => searchMocks }
|
||||
} as unknown) as Setup
|
||||
});
|
||||
|
||||
expect(res!._source.service).toEqual({ name: 'my_service' });
|
||||
expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.2 });
|
||||
});
|
||||
|
||||
it('should return configuration by matching on "production" env', async () => {
|
||||
const res = await searchConfigurations({
|
||||
serviceName: 'non_existing_service',
|
||||
environment: 'production',
|
||||
setup: ({
|
||||
config: { get: () => '' },
|
||||
client: { search: async () => searchMocks }
|
||||
} as unknown) as Setup
|
||||
});
|
||||
|
||||
expect(res!._source.service).toEqual({ environment: 'production' });
|
||||
expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.3 });
|
||||
});
|
||||
});
|
|
@ -4,8 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ESFilter } from 'elasticsearch';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { ESSearchHit } from 'elasticsearch';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
|
@ -13,9 +12,6 @@ import {
|
|||
import { Setup } from '../../helpers/setup_request';
|
||||
import { AgentConfiguration } from './configuration_types';
|
||||
|
||||
export type SearchAgentConfigurationsAPIResponse = PromiseReturnType<
|
||||
typeof searchConfigurations
|
||||
>;
|
||||
export async function searchConfigurations({
|
||||
serviceName,
|
||||
environment,
|
||||
|
@ -27,26 +23,61 @@ export async function searchConfigurations({
|
|||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const filters: ESFilter[] = [{ term: { [SERVICE_NAME]: serviceName } }];
|
||||
// sorting order
|
||||
// 1. exact match: service.name AND service.environment (eg. opbeans-node / production)
|
||||
// 2. Partial match: service.name and no service.environment (eg. opbeans-node / All)
|
||||
// 3. Partial match: service.environment and no service.name (eg. All / production)
|
||||
// 4. Catch all: no service.name and no service.environment (eg. All / All)
|
||||
|
||||
if (environment) {
|
||||
filters.push({ term: { [SERVICE_ENVIRONMENT]: environment } });
|
||||
} else {
|
||||
filters.push({
|
||||
bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } }
|
||||
});
|
||||
}
|
||||
const environmentFilter = environment
|
||||
? [{ term: { [SERVICE_ENVIRONMENT]: { value: environment } } }]
|
||||
: [];
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.apmAgentConfigurationIndex'),
|
||||
body: {
|
||||
size: 1,
|
||||
query: {
|
||||
bool: { filter: filters }
|
||||
bool: {
|
||||
minimum_should_match: 2,
|
||||
should: [
|
||||
{ term: { [SERVICE_NAME]: { value: serviceName } } },
|
||||
...environmentFilter,
|
||||
{ bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } },
|
||||
{ bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search<AgentConfiguration>(params);
|
||||
return resp.hits.hits[0];
|
||||
const { hits } = resp.hits;
|
||||
|
||||
const exactMatch = hits.find(
|
||||
hit =>
|
||||
hit._source.service.name === serviceName &&
|
||||
hit._source.service.environment === environment
|
||||
);
|
||||
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
const matchWithServiceName = hits.find(
|
||||
hit => hit._source.service.name === serviceName
|
||||
);
|
||||
|
||||
if (matchWithServiceName) {
|
||||
return matchWithServiceName;
|
||||
}
|
||||
|
||||
const matchWithEnvironment = hits.find(
|
||||
hit => hit._source.service.environment === environment
|
||||
);
|
||||
|
||||
if (matchWithEnvironment) {
|
||||
return matchWithEnvironment;
|
||||
}
|
||||
|
||||
return resp.hits.hits[0] as ESSearchHit<AgentConfiguration> | undefined;
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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 { AgentConfigurationIntake } from './configuration_types';
|
||||
|
||||
export type UpdateAgentConfigurationAPIResponse = PromiseReturnType<
|
||||
typeof updateConfiguration
|
||||
>;
|
||||
export async function updateConfiguration({
|
||||
configurationId,
|
||||
configuration,
|
||||
setup
|
||||
}: {
|
||||
configurationId: string;
|
||||
configuration: AgentConfigurationIntake;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { client, config } = setup;
|
||||
|
||||
const params = {
|
||||
type: '_doc',
|
||||
id: configurationId,
|
||||
refresh: true,
|
||||
index: config.get<string>('apm_oss.apmAgentConfigurationIndex'),
|
||||
body: {
|
||||
'@timestamp': Date.now(),
|
||||
...configuration
|
||||
}
|
||||
};
|
||||
|
||||
return client.index(params);
|
||||
}
|
|
@ -11,14 +11,9 @@ import {
|
|||
TRANSACTION_DURATION,
|
||||
TRANSACTION_TYPE
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
|
||||
export type TransactionAvgDurationByCountryAPIResponse = PromiseReturnType<
|
||||
typeof getTransactionAvgDurationByCountry
|
||||
>;
|
||||
|
||||
export async function getTransactionAvgDurationByCountry({
|
||||
setup,
|
||||
serviceName
|
||||
|
|
|
@ -16,16 +16,11 @@ import {
|
|||
TRANSACTION_BREAKDOWN_COUNT,
|
||||
PROCESSOR_EVENT
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
|
||||
import { MAX_KPIS, COLORS } from './constants';
|
||||
|
||||
export type TransactionBreakdownAPIResponse = PromiseReturnType<
|
||||
typeof getTransactionBreakdown
|
||||
>;
|
||||
|
||||
export async function getTransactionBreakdown({
|
||||
setup,
|
||||
serviceName,
|
||||
|
|
|
@ -10,12 +10,10 @@ import {
|
|||
TRACE_ID,
|
||||
TRANSACTION_ID
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
|
||||
export type TransactionAPIResponse = PromiseReturnType<typeof getTransaction>;
|
||||
export async function getTransaction(
|
||||
transactionId: string,
|
||||
traceId: string,
|
||||
|
|
|
@ -11,14 +11,10 @@ import {
|
|||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../typings/common';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
|
||||
|
||||
export type EnvironmentUIFilterAPIResponse = PromiseReturnType<
|
||||
typeof getEnvironments
|
||||
>;
|
||||
export async function getEnvironments(setup: Setup, serviceName?: string) {
|
||||
const { start, end, client, config } = setup;
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ import {
|
|||
deleteAgentConfigurationRoute,
|
||||
listAgentConfigurationEnvironmentsRoute,
|
||||
listAgentConfigurationServicesRoute,
|
||||
updateAgentConfigurationRoute
|
||||
updateAgentConfigurationRoute,
|
||||
agentConfigurationAgentNameRoute
|
||||
} from './settings';
|
||||
import { metricsChartsRoute } from './metrics';
|
||||
import { serviceNodesRoute } from './service_nodes';
|
||||
|
@ -48,14 +49,22 @@ import { createApi } from './create_api';
|
|||
|
||||
const createApmApi = () => {
|
||||
const api = createApi()
|
||||
// index pattern
|
||||
.add(indexPatternRoute)
|
||||
|
||||
// Errors
|
||||
.add(errorDistributionRoute)
|
||||
.add(errorGroupsRoute)
|
||||
.add(errorsRoute)
|
||||
|
||||
// Services
|
||||
.add(serviceAgentNameRoute)
|
||||
.add(serviceTransactionTypesRoute)
|
||||
.add(servicesRoute)
|
||||
.add(serviceNodeMetadataRoute)
|
||||
|
||||
// Agent configuration
|
||||
.add(agentConfigurationAgentNameRoute)
|
||||
.add(agentConfigurationRoute)
|
||||
.add(agentConfigurationSearchRoute)
|
||||
.add(createAgentConfigurationRoute)
|
||||
|
@ -63,15 +72,23 @@ const createApmApi = () => {
|
|||
.add(listAgentConfigurationEnvironmentsRoute)
|
||||
.add(listAgentConfigurationServicesRoute)
|
||||
.add(updateAgentConfigurationRoute)
|
||||
|
||||
// Metrics
|
||||
.add(metricsChartsRoute)
|
||||
.add(serviceNodesRoute)
|
||||
|
||||
// Traces
|
||||
.add(tracesRoute)
|
||||
.add(tracesByIdRoute)
|
||||
|
||||
// Transaction groups
|
||||
.add(transactionGroupsBreakdownRoute)
|
||||
.add(transactionGroupsChartsRoute)
|
||||
.add(transactionGroupsDistributionRoute)
|
||||
.add(transactionGroupsRoute)
|
||||
.add(transactionGroupsAvgDurationByCountry)
|
||||
|
||||
// UI filters
|
||||
.add(errorGroupsLocalFiltersRoute)
|
||||
.add(metricsLocalFiltersRoute)
|
||||
.add(servicesLocalFiltersRoute)
|
||||
|
|
|
@ -7,14 +7,16 @@
|
|||
import * as t from 'io-ts';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getServiceNames } from '../lib/settings/agent_configuration/get_service_names';
|
||||
import { createConfiguration } from '../lib/settings/agent_configuration/create_configuration';
|
||||
import { updateConfiguration } from '../lib/settings/agent_configuration/update_configuration';
|
||||
import { createOrUpdateConfiguration } from '../lib/settings/agent_configuration/create_or_update_configuration';
|
||||
import { searchConfigurations } from '../lib/settings/agent_configuration/search';
|
||||
import { listConfigurations } from '../lib/settings/agent_configuration/list_configurations';
|
||||
import { getEnvironments } from '../lib/settings/agent_configuration/get_environments';
|
||||
import { deleteConfiguration } from '../lib/settings/agent_configuration/delete_configuration';
|
||||
import { createRoute } from './create_route';
|
||||
import { transactionSampleRateRt } from '../../common/runtime_types/transaction_sample_rate_rt';
|
||||
import { transactionMaxSpansRt } from '../../common/runtime_types/transaction_max_spans_rt';
|
||||
import { getAgentNameByService } from '../lib/settings/agent_configuration/get_agent_name_by_service';
|
||||
import { markAppliedByAgent } from '../lib/settings/agent_configuration/mark_applied_by_agent';
|
||||
|
||||
// get list of configurations
|
||||
export const agentConfigurationRoute = createRoute(core => ({
|
||||
|
@ -56,36 +58,46 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({
|
|||
}
|
||||
}));
|
||||
|
||||
const agentPayloadRt = t.type({
|
||||
settings: t.type({
|
||||
transaction_sample_rate: transactionSampleRateRt
|
||||
const agentPayloadRt = t.intersection([
|
||||
t.partial({ agent_name: t.string }),
|
||||
t.type({
|
||||
service: t.intersection([
|
||||
t.partial({ name: t.string }),
|
||||
t.partial({ environment: t.string })
|
||||
])
|
||||
}),
|
||||
service: t.intersection([
|
||||
t.type({
|
||||
name: t.string
|
||||
}),
|
||||
t.partial({
|
||||
environments: t.array(t.string)
|
||||
})
|
||||
])
|
||||
});
|
||||
t.type({
|
||||
settings: t.intersection([
|
||||
t.partial({ transaction_sample_rate: transactionSampleRateRt }),
|
||||
t.partial({ capture_body: t.string }),
|
||||
t.partial({ transaction_max_spans: transactionMaxSpansRt })
|
||||
])
|
||||
})
|
||||
]);
|
||||
|
||||
// get environments for service
|
||||
export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({
|
||||
path:
|
||||
'/api/apm/settings/agent-configuration/services/{serviceName}/environments',
|
||||
path: '/api/apm/settings/agent-configuration/environments',
|
||||
params: {
|
||||
path: t.type({
|
||||
serviceName: t.string
|
||||
})
|
||||
query: t.partial({ serviceName: t.string })
|
||||
},
|
||||
handler: async (req, { path }) => {
|
||||
handler: async (req, { query }) => {
|
||||
const setup = await setupRequest(req);
|
||||
const { serviceName } = path;
|
||||
return await getEnvironments({
|
||||
serviceName,
|
||||
setup
|
||||
});
|
||||
const { serviceName } = query;
|
||||
return await getEnvironments({ serviceName, setup });
|
||||
}
|
||||
}));
|
||||
|
||||
// get agentName for service
|
||||
export const agentConfigurationAgentNameRoute = createRoute(() => ({
|
||||
path: '/api/apm/settings/agent-configuration/agent_name',
|
||||
params: {
|
||||
query: t.type({ serviceName: t.string })
|
||||
},
|
||||
handler: async (req, { query }) => {
|
||||
const setup = await setupRequest(req);
|
||||
const { serviceName } = query;
|
||||
return await getAgentNameByService({ serviceName, setup });
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -97,16 +109,13 @@ export const createAgentConfigurationRoute = createRoute(() => ({
|
|||
},
|
||||
handler: async (req, { body }) => {
|
||||
const setup = await setupRequest(req);
|
||||
return await createConfiguration({
|
||||
configuration: body,
|
||||
setup
|
||||
});
|
||||
return await createOrUpdateConfiguration({ configuration: body, setup });
|
||||
}
|
||||
}));
|
||||
|
||||
export const updateAgentConfigurationRoute = createRoute(() => ({
|
||||
method: 'PUT',
|
||||
path: `/api/apm/settings/agent-configuration/{configurationId}`,
|
||||
path: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
params: {
|
||||
path: t.type({
|
||||
configurationId: t.string
|
||||
|
@ -116,7 +125,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({
|
|||
handler: async (req, { path, body }) => {
|
||||
const setup = await setupRequest(req);
|
||||
const { configurationId } = path;
|
||||
return await updateConfiguration({
|
||||
return await createOrUpdateConfiguration({
|
||||
configurationId,
|
||||
configuration: body,
|
||||
setup
|
||||
|
@ -124,7 +133,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({
|
|||
}
|
||||
}));
|
||||
|
||||
// Lookup single configuration
|
||||
// Lookup single configuration (used by APM Server)
|
||||
export const agentConfigurationSearchRoute = createRoute(core => ({
|
||||
method: 'POST',
|
||||
path: '/api/apm/settings/agent-configuration/search',
|
||||
|
@ -133,7 +142,8 @@ export const agentConfigurationSearchRoute = createRoute(core => ({
|
|||
service: t.intersection([
|
||||
t.type({ name: t.string }),
|
||||
t.partial({ environment: t.string })
|
||||
])
|
||||
]),
|
||||
etag: t.string
|
||||
})
|
||||
},
|
||||
handler: async (req, { body }, h) => {
|
||||
|
@ -148,6 +158,11 @@ export const agentConfigurationSearchRoute = createRoute(core => ({
|
|||
return h.response().code(404);
|
||||
}
|
||||
|
||||
// update `applied_by_agent` field if etags match
|
||||
if (body.etag === config._source.etag && !config._source.applied_by_agent) {
|
||||
markAppliedByAgent({ id: config._id, body: config._source, setup });
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -115,6 +115,7 @@ export type Client<TRouteState> = <
|
|||
: undefined
|
||||
>(
|
||||
options: Omit<KFetchOptions, 'query' | 'body' | 'pathname' | 'method'> & {
|
||||
forceCache?: boolean;
|
||||
pathname: TPath;
|
||||
} & (TMethod extends 'GET' ? { method?: TMethod } : { method: TMethod }) &
|
||||
// Makes sure params can only be set when types were defined
|
||||
|
|
|
@ -6,15 +6,11 @@
|
|||
|
||||
import { StringMap, IndexAsString } from './common';
|
||||
|
||||
export interface BoolQuery {
|
||||
must_not: Array<Record<string, any>>;
|
||||
should: Array<Record<string, any>>;
|
||||
filter: Array<Record<string, any>>;
|
||||
}
|
||||
|
||||
declare module 'elasticsearch' {
|
||||
// extending SearchResponse to be able to have typed aggregations
|
||||
|
||||
type ESSearchHit<T> = SearchResponse<T>['hits']['hits'][0];
|
||||
|
||||
type AggregationType =
|
||||
| 'date_histogram'
|
||||
| 'histogram'
|
||||
|
|
|
@ -3425,14 +3425,9 @@
|
|||
"xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "トレースログを表示",
|
||||
"xpack.apm.transactionActionMenu.viewInUptime": "監視ステータスを表示",
|
||||
"xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示",
|
||||
"xpack.apm.transactionDetails.durationLabel": "期間",
|
||||
"xpack.apm.transactionDetails.errorsNone": "なし",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLabel": "エラー",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {関連エラー} other {関連エラー}}",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {1 件の関連エラーを表示} other {# 件の関連エラーを表示}}",
|
||||
"xpack.apm.transactionDetails.notFoundLabel": "トランザクションが見つかりませんでした。",
|
||||
"xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした",
|
||||
"xpack.apm.transactionDetails.percentOfTraceLabel": "トレースの %",
|
||||
"xpack.apm.transactionDetails.resultLabel": "結果",
|
||||
"xpack.apm.transactionDetails.serviceLabel": "サービス",
|
||||
"xpack.apm.transactionDetails.servicesTitle": "サービス",
|
||||
|
@ -3442,7 +3437,6 @@
|
|||
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "ナビゲーションタイミング",
|
||||
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "スタックトレース",
|
||||
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "ディスカバリでスパンを表示",
|
||||
"xpack.apm.transactionDetails.timestampLabel": "タイムスタンプ",
|
||||
"xpack.apm.transactionDetails.transactionLabel": "トランザクション",
|
||||
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "このバケットに利用可能なサンプルがありません",
|
||||
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# request} 1 {# 件のリクエスト} other {# 件のリクエスト}}",
|
||||
|
@ -3454,7 +3448,6 @@
|
|||
"xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "このトランザクションを報告した APM エージェントが、構成に基づき {dropped} 個以上のスパンをドロップしました。",
|
||||
"xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "ドロップされたスパンの詳細。",
|
||||
"xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "トランザクションの詳細",
|
||||
"xpack.apm.transactionDetails.userIdLabel": "ユーザー ID",
|
||||
"xpack.apm.transactionDetails.viewFullTraceButtonLabel": "完全なトレースを表示",
|
||||
"xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "現在完全なトレースが表示されています",
|
||||
"xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル",
|
||||
|
@ -3495,10 +3488,6 @@
|
|||
"xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "国ごとの平均ページ読み込み時間の分布",
|
||||
"xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "平均ページ読み込み時間:",
|
||||
"xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} ページの読み込み",
|
||||
"xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "詳細については、当社のドキュメントをご覧ください。",
|
||||
"xpack.apm.settings.agentConf.betaBadgeLabel": "ベータ",
|
||||
"xpack.apm.settings.agentConf.betaBadgeText": "この機能は開発中です。フィードバックがある場合は、ディスカッションフォーラムをご利用ください。",
|
||||
"xpack.apm.settings.agentConf.betaCallOutTitle": "APM エージェント構成 (ベータ)",
|
||||
"xpack.apm.settings.agentConf.configTable.editButtonDescription": "この構成を編集します",
|
||||
"xpack.apm.settings.agentConf.configTable.editButtonLabel": "編集",
|
||||
"xpack.apm.settings.agentConf.configTable.emptyPromptText": "変更しましょう。直接 Kibana からエージェント構成を微調整できます。再展開する必要はありません。まず、最初の構成を作成します。",
|
||||
|
@ -3509,40 +3498,6 @@
|
|||
"xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "サービス名",
|
||||
"xpack.apm.settings.agentConf.configurationsPanelTitle": "構成",
|
||||
"xpack.apm.settings.agentConf.createConfigButtonLabel": "構成の作成",
|
||||
"xpack.apm.settings.agentConf.createConfigFailedText": "{serviceName}の構成を作成するときに問題が発生しました。エラー: {errorMessage}",
|
||||
"xpack.apm.settings.agentConf.createConfigFailedTitle": "構成を作成できませんでした",
|
||||
"xpack.apm.settings.agentConf.createConfigSucceededText": "{serviceName}の構成を正常に作成しました。エージェントに反映するには、少し時間がかかります。",
|
||||
"xpack.apm.settings.agentConf.createConfigSucceededTitle": "構成が作成されました。",
|
||||
"xpack.apm.settings.agentConf.deleteConfigFailedText": "{serviceName}の構成を削除するときに問題が発生しました。エラー: {errorMessage}",
|
||||
"xpack.apm.settings.agentConf.deleteConfigFailedTitle": "構成を削除できませんでした",
|
||||
"xpack.apm.settings.agentConf.deleteConfigSucceededText": "{serviceName}の構成を正常に削除しました。エージェントに反映するには、少し時間がかかります。",
|
||||
"xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "構成が削除されました",
|
||||
"xpack.apm.settings.agentConf.editConfigFailedText": "{serviceName}の構成を編集するときに問題が発生しました。エラー: {errorMessage}",
|
||||
"xpack.apm.settings.agentConf.editConfigFailedTitle": "構成を編集できませんでした",
|
||||
"xpack.apm.settings.agentConf.editConfigSucceededText": "{serviceName}の構成を正常に編集しました。エージェントに反映するには、少し時間がかかります。",
|
||||
"xpack.apm.settings.agentConf.editConfigSucceededTitle": "構成が編集されました",
|
||||
"xpack.apm.settings.agentConf.flyOut.betaCallOutText": "この最初のバージョンでは、サンプルレート構成のみがサポートされます。今後のリリースで、エージェントのサポートを拡張します。不具合があることを認識してください。",
|
||||
"xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM エージェント構成 (ベータ)",
|
||||
"xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "キャンセル",
|
||||
"xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "構成",
|
||||
"xpack.apm.settings.agentConf.flyOut.createConfigTitle": "構成の作成",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "削除",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "この構成を削除する場合、APM サーバーと同期するまで、エージェントは、既存の構成を使用し続けます。",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "構成の削除",
|
||||
"xpack.apm.settings.agentConf.flyOut.editConfigTitle": "構成の編集",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "サンプルレートは 0.000 ~ 1 の範囲でなければなりません",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "0.000 ~ 1.0 の範囲のレートを選択してください。既定の構成は 1.0 (100% のトレース) です。",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "トランザクションサンプルレート",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "サンプルレートの設定",
|
||||
"xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "構成の保存",
|
||||
"xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "選択してください",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未設定",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "構成を保存するには、有効な環境を選択する必要があります。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "構成ごとに 1 つの環境のみがサポートされます。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "環境",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "構成するサービスを選択してください。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名前",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "サービス",
|
||||
"xpack.apm.settings.agentConf.pageTitle": "設定",
|
||||
"xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "概要に戻る",
|
||||
"xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません",
|
||||
|
|
|
@ -3426,14 +3426,9 @@
|
|||
"xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "显示跟踪日志",
|
||||
"xpack.apm.transactionActionMenu.viewInUptime": "查看监测状态",
|
||||
"xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档",
|
||||
"xpack.apm.transactionDetails.durationLabel": "持续时间",
|
||||
"xpack.apm.transactionDetails.errorsNone": "无",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLabel": "错误",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {相关错误} other {相关错误}}",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}",
|
||||
"xpack.apm.transactionDetails.notFoundLabel": "未找到任何事务。",
|
||||
"xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯",
|
||||
"xpack.apm.transactionDetails.percentOfTraceLabel": "追溯的 %",
|
||||
"xpack.apm.transactionDetails.resultLabel": "结果",
|
||||
"xpack.apm.transactionDetails.serviceLabel": "服务",
|
||||
"xpack.apm.transactionDetails.servicesTitle": "服务",
|
||||
|
@ -3443,7 +3438,6 @@
|
|||
"xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "导航定时",
|
||||
"xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "堆栈追溯",
|
||||
"xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "在 Discover 中查看跨度",
|
||||
"xpack.apm.transactionDetails.timestampLabel": "时间戳",
|
||||
"xpack.apm.transactionDetails.transactionLabel": "事务",
|
||||
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "此存储桶没有可用样例",
|
||||
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# 个请求} one {# 个请求} other {# 个请求}}",
|
||||
|
@ -3455,7 +3449,6 @@
|
|||
"xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "报告此事务的 APM 代理基于其配置丢弃了 {dropped} 个跨度。",
|
||||
"xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "详细了解丢弃的跨度。",
|
||||
"xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "事务详情",
|
||||
"xpack.apm.transactionDetails.userIdLabel": "用户 ID",
|
||||
"xpack.apm.transactionDetails.viewFullTraceButtonLabel": "查看完整追溯信息",
|
||||
"xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "当前正在查看完整追溯信息",
|
||||
"xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位",
|
||||
|
@ -3496,11 +3489,6 @@
|
|||
"xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "页面加载平均时长分布(按国家/地区)",
|
||||
"xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "页面加载平均时长:",
|
||||
"xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} 个页面加载",
|
||||
"xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "在我们的文档中详细了解。",
|
||||
"xpack.apm.settings.agentConf.betaBadgeLabel": "公测版",
|
||||
"xpack.apm.settings.agentConf.betaBadgeText": "此功能仍在开发之中。如果您有反馈,请在我们的“讨论”论坛中提供。",
|
||||
"xpack.apm.settings.agentConf.betaCallOutText": "我们很高兴让您第一时间了解 APM 代理配置。{agentConfigDocsLink}",
|
||||
"xpack.apm.settings.agentConf.betaCallOutTitle": "APM 代理配置(公测版)",
|
||||
"xpack.apm.settings.agentConf.configTable.editButtonDescription": "编辑此配置",
|
||||
"xpack.apm.settings.agentConf.configTable.editButtonLabel": "编辑",
|
||||
"xpack.apm.settings.agentConf.configTable.emptyPromptText": "让我们改动一下!可以直接从 Kibana 微调代理配置,无需重新部署。首先创建您的第一个配置。",
|
||||
|
@ -3511,40 +3499,6 @@
|
|||
"xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "服务名称",
|
||||
"xpack.apm.settings.agentConf.configurationsPanelTitle": "配置",
|
||||
"xpack.apm.settings.agentConf.createConfigButtonLabel": "创建配置",
|
||||
"xpack.apm.settings.agentConf.createConfigFailedText": "为 {serviceName} 创建配置时出现问题。错误:{errorMessage}",
|
||||
"xpack.apm.settings.agentConf.createConfigFailedTitle": "配置无法创建",
|
||||
"xpack.apm.settings.agentConf.createConfigSucceededText": "您已成功为 {serviceName} 创建配置。将花费一些时间才能传播到代理。",
|
||||
"xpack.apm.settings.agentConf.createConfigSucceededTitle": "配置已创建!",
|
||||
"xpack.apm.settings.agentConf.deleteConfigFailedText": "为 {serviceName} 删除配置时出现问题。错误:{errorMessage}",
|
||||
"xpack.apm.settings.agentConf.deleteConfigFailedTitle": "配置无法删除",
|
||||
"xpack.apm.settings.agentConf.deleteConfigSucceededText": "您已成功为 {serviceName} 删除配置。将花费一些时间才能传播到代理。",
|
||||
"xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "配置已删除",
|
||||
"xpack.apm.settings.agentConf.editConfigFailedText": "编辑 {serviceName} 的配置时出现问题。错误:{errorMessage}",
|
||||
"xpack.apm.settings.agentConf.editConfigFailedTitle": "配置无法编辑",
|
||||
"xpack.apm.settings.agentConf.editConfigSucceededText": "您已成功编辑 {serviceName} 的配置。将花费一些时间才能传播到代理。",
|
||||
"xpack.apm.settings.agentConf.editConfigSucceededTitle": "配置已编辑",
|
||||
"xpack.apm.settings.agentConf.flyOut.betaCallOutText": "请注意,在此第一版中仅支持采样速率配置。我们将在未来的版本中提供代理配置的支持。请注意故障。",
|
||||
"xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM 代理配置(公测版)",
|
||||
"xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "取消",
|
||||
"xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "配置",
|
||||
"xpack.apm.settings.agentConf.flyOut.createConfigTitle": "创建配置",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "删除",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "如果您希望删除此配置,请注意,代理将继续使用现有配置,直至它们与 APM Server 同步。",
|
||||
"xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "删除配置",
|
||||
"xpack.apm.settings.agentConf.flyOut.editConfigTitle": "编辑配置",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "采样速率必须介于 0.000 和 1 之间",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "选择 0.000 和 1.0 之间的速率。默认配置为 1.0(跟踪的 100%)。",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "事务采样速率",
|
||||
"xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "设置采样速率",
|
||||
"xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "保存配置",
|
||||
"xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "选择",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未设置",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "必须选择有效的环境,才能保存配置。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "每个配置仅支持单个环境。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "环境",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "选择要配置的服务。",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名称",
|
||||
"xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "服务",
|
||||
"xpack.apm.settings.agentConf.pageTitle": "设置",
|
||||
"xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "返回至概览",
|
||||
"xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪",
|
||||
|
|
174
x-pack/test/api_integration/apis/apm/agent_configuration.ts
Normal file
174
x-pack/test/api_integration/apis/apm/agent_configuration.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function agentConfigurationTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
|
||||
function searchConfigurations(configuration: any) {
|
||||
return supertest
|
||||
.post(`/api/apm/settings/agent-configuration/search`)
|
||||
.send(configuration)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
}
|
||||
|
||||
let createdConfigIds: any[] = [];
|
||||
async function createConfiguration(configuration: any) {
|
||||
const res = await supertest
|
||||
.post(`/api/apm/settings/agent-configuration/new`)
|
||||
.send(configuration)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
createdConfigIds.push(res.body._id);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function deleteCreatedConfigurations() {
|
||||
const promises = Promise.all(createdConfigIds.map(deleteConfiguration));
|
||||
createdConfigIds = [];
|
||||
return promises;
|
||||
}
|
||||
|
||||
function deleteConfiguration(configurationId: string) {
|
||||
return supertest
|
||||
.delete(`/api/apm/settings/agent-configuration/${configurationId}`)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
}
|
||||
|
||||
describe('agent configuration', () => {
|
||||
describe('when creating four configurations', () => {
|
||||
before(async () => {
|
||||
log.debug('creating agent configuration');
|
||||
|
||||
// all / all
|
||||
await createConfiguration({
|
||||
service: {},
|
||||
settings: { transaction_sample_rate: 0.1 },
|
||||
});
|
||||
|
||||
// my_service / all
|
||||
await createConfiguration({
|
||||
service: { name: 'my_service' },
|
||||
settings: { transaction_sample_rate: 0.2 },
|
||||
});
|
||||
|
||||
// all / production
|
||||
await createConfiguration({
|
||||
service: { environment: 'production' },
|
||||
settings: { transaction_sample_rate: 0.3 },
|
||||
});
|
||||
|
||||
// all / production
|
||||
await createConfiguration({
|
||||
service: { environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.4 },
|
||||
});
|
||||
|
||||
// my_service / production
|
||||
await createConfiguration({
|
||||
service: { name: 'my_service', environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.5 },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.debug('deleting agent configurations');
|
||||
await deleteCreatedConfigurations();
|
||||
});
|
||||
|
||||
const agentsRequests = [
|
||||
{
|
||||
service: { name: 'non_existing_service', environment: 'non_existing_env' },
|
||||
expectedSettings: { transaction_sample_rate: 0.1 },
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'production' },
|
||||
expectedSettings: { transaction_sample_rate: 0.2 },
|
||||
},
|
||||
{
|
||||
service: { name: 'non_existing_service', environment: 'production' },
|
||||
expectedSettings: { transaction_sample_rate: 0.3 },
|
||||
},
|
||||
{
|
||||
service: { name: 'non_existing_service', environment: 'development' },
|
||||
expectedSettings: { transaction_sample_rate: 0.4 },
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'development' },
|
||||
expectedSettings: { transaction_sample_rate: 0.5 },
|
||||
},
|
||||
];
|
||||
|
||||
for (const agentRequest of agentsRequests) {
|
||||
it(`${agentRequest.service.name} / ${agentRequest.service.environment}`, async () => {
|
||||
const { statusCode, body } = await searchConfigurations({
|
||||
service: agentRequest.service,
|
||||
etag: 'abc',
|
||||
});
|
||||
|
||||
expect(statusCode).to.equal(200);
|
||||
expect(body._source.settings).to.eql(agentRequest.expectedSettings);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('when an agent retrieves a configuration', () => {
|
||||
before(async () => {
|
||||
log.debug('creating agent configuration');
|
||||
|
||||
await createConfiguration({
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.9 },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.debug('deleting agent configurations');
|
||||
await deleteCreatedConfigurations();
|
||||
});
|
||||
|
||||
it(`should have 'applied_by_agent=false' on first request`, async () => {
|
||||
const { body } = await searchConfigurations({
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39',
|
||||
});
|
||||
|
||||
expect(body._source.applied_by_agent).to.be(false);
|
||||
});
|
||||
|
||||
it(`should have 'applied_by_agent=true' on second request`, async () => {
|
||||
async function getAppliedByAgent() {
|
||||
const { body } = await searchConfigurations({
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39',
|
||||
});
|
||||
|
||||
return body._source.applied_by_agent;
|
||||
}
|
||||
|
||||
// wait until `applied_by_agent` has been updated in elasticsearch
|
||||
expect(await waitFor(getAppliedByAgent)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitFor(cb: () => Promise<boolean>, retries = 50): Promise<boolean> {
|
||||
if (retries === 0) {
|
||||
throw new Error(`Maximum number of retries reached`);
|
||||
}
|
||||
|
||||
const res = await cb();
|
||||
if (!res) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return waitFor(cb, retries - 1);
|
||||
}
|
||||
return res;
|
||||
}
|
|
@ -133,7 +133,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
req: {
|
||||
method: 'post',
|
||||
url: `/api/apm/settings/agent-configuration/search`,
|
||||
body: { service: { name: 'test-service' } },
|
||||
body: { service: { name: 'test-service' }, etag: 'abc' },
|
||||
},
|
||||
expectForbidden: expect404,
|
||||
expectResponse: expect200,
|
||||
|
|
|
@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('APM', () => {
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
loadTestFile(require.resolve('./agent_configuration'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue