Alert creation and freeform selection (#111883)
Allow selecting any service name, transaction type (where appropriate), and environment when creating and editing rules, both in APM and Stack Management. - Create /internal/apm/suggestions endpoint that uses `terms_enum` - Use combo box for environment, service name, and transaction type with suggestions endpoint on all alerts - Remove "Go to APM" callouts on new alert creation - Wrap calls to `createCallApmApi` in alert triggers with `useEffect` - Use `getEnvironmentLabel` for value in environment field expression - Make all `AlertParams` fields optional (except in latency threshold alert) - Add e2e tests for creating an alert - Remove `NewAlertEmptyPrompt` component and `isNewApmRuleFromStackManagement` helper - Replace `maxServiceEnvironments` and `maxServiceSelections` config options with `maxSuggestions` advanced setting. ![CleanShot 2021-09-28 at 10 35 58](https://user-images.githubusercontent.com/9912/135119948-e247615a-d235-4feb-b197-b803f165ad1e.gif) Fixes #106786
This commit is contained in:
parent
c7e06ab7d3
commit
f4a95f9b97
|
@ -408,12 +408,12 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'text',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'apm:enableSignificantTerms': {
|
||||
'observability:enableInspectEsQueries': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:enableInspectEsQueries': {
|
||||
type: 'boolean',
|
||||
'observability:maxSuggestions': {
|
||||
type: 'integer',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'banners:placement': {
|
||||
|
|
|
@ -34,8 +34,8 @@ export interface UsageStats {
|
|||
'discover:showMultiFields': boolean;
|
||||
'discover:maxDocFieldsDisplayed': number;
|
||||
'securitySolution:rulesTableRefresh': string;
|
||||
'apm:enableSignificantTerms': boolean;
|
||||
'observability:enableInspectEsQueries': boolean;
|
||||
'observability:maxSuggestions': number;
|
||||
'visualize:enableLabs': boolean;
|
||||
'visualization:heatmap:maxBuckets': number;
|
||||
'visualization:colorMapping': string;
|
||||
|
|
|
@ -7629,14 +7629,14 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"apm:enableSignificantTerms": {
|
||||
"observability:enableInspectEsQueries": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:enableInspectEsQueries": {
|
||||
"type": "boolean",
|
||||
"observability:maxSuggestions": {
|
||||
"type": "integer",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
## Feature flags
|
||||
|
||||
To set up a flagged feature, add the name of the feature key (`apm:myFeature`) to [commmon/ui_settings_keys.ts](./common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](./server/ui_settings.ts).
|
||||
|
||||
Test for the feature like:
|
||||
|
||||
```js
|
||||
import { myFeatureEnabled } from '../ui_settings_keys';
|
||||
if (core.uiSettings.get(myFeatureEnabled)) {
|
||||
doStuff();
|
||||
}
|
||||
```
|
||||
|
||||
Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability.
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
describe('Rules', () => {
|
||||
describe('Error count', () => {
|
||||
const ruleName = 'Error count threshold';
|
||||
const comboBoxInputSelector =
|
||||
'.euiPopover__panel-isOpen [data-test-subj=comboBoxSearchInput]';
|
||||
const confirmModalButtonSelector =
|
||||
'.euiModal button[data-test-subj=confirmModalConfirmButton]';
|
||||
const deleteButtonSelector =
|
||||
'[data-test-subj=deleteActionHoverButton]:first';
|
||||
const editButtonSelector = '[data-test-subj=editActionHoverButton]:first';
|
||||
|
||||
describe('when created from APM', () => {
|
||||
describe('when created from Service Inventory', () => {
|
||||
before(() => {
|
||||
cy.loginAsPowerUser();
|
||||
});
|
||||
|
||||
it('creates and updates a rule', () => {
|
||||
// Create a rule in APM
|
||||
cy.visit('/app/apm/services');
|
||||
cy.contains('Alerts and rules').click();
|
||||
cy.contains('Error count').click();
|
||||
cy.contains('Create threshold rule').click();
|
||||
|
||||
// Change the environment to "testing"
|
||||
cy.contains('Environment All').click();
|
||||
cy.get(comboBoxInputSelector).type('testing{enter}');
|
||||
|
||||
// Save, with no actions
|
||||
cy.contains('button:not(:disabled)', 'Save').click();
|
||||
cy.get(confirmModalButtonSelector).click();
|
||||
|
||||
cy.contains(`Created rule "${ruleName}`);
|
||||
|
||||
// Go to Stack Management
|
||||
cy.contains('Alerts and rules').click();
|
||||
cy.contains('Manage rules').click();
|
||||
|
||||
// Edit the rule, changing the environment to "All"
|
||||
cy.get(editButtonSelector).click();
|
||||
cy.contains('Environment testing').click();
|
||||
cy.get(comboBoxInputSelector).type('All{enter}');
|
||||
cy.contains('button:not(:disabled)', 'Save').click();
|
||||
|
||||
cy.contains(`Updated '${ruleName}'`);
|
||||
|
||||
// Wait for the table to be ready for next edit click
|
||||
cy.get('.euiBasicTable').not('.euiBasicTable-loading');
|
||||
|
||||
// Ensure the rule now shows "All" for the environment
|
||||
cy.get(editButtonSelector).click();
|
||||
cy.contains('Environment All');
|
||||
cy.contains('button', 'Cancel').click();
|
||||
|
||||
// Delete the rule
|
||||
cy.get(deleteButtonSelector).click();
|
||||
cy.get(confirmModalButtonSelector).click();
|
||||
|
||||
// Ensure the table is empty
|
||||
cy.contains('Create your first rule');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when created from Stack management', () => {
|
||||
before(() => {
|
||||
cy.loginAsPowerUser();
|
||||
});
|
||||
|
||||
it('creates a rule', () => {
|
||||
// Go to stack management
|
||||
cy.visit('/app/management/insightsAndAlerting/triggersActions/rules');
|
||||
|
||||
// Create a rule
|
||||
cy.contains('button', 'Create rule').click();
|
||||
cy.get('[name=name]').type(ruleName);
|
||||
cy.contains('.euiFlyout button', ruleName).click();
|
||||
|
||||
// Change the environment to "testing"
|
||||
cy.contains('Environment All').click();
|
||||
cy.get(comboBoxInputSelector).type('testing{enter}');
|
||||
|
||||
// Save, with no actions
|
||||
cy.contains('button:not(:disabled)', 'Save').click();
|
||||
cy.get(confirmModalButtonSelector).click();
|
||||
|
||||
cy.contains(`Created rule "${ruleName}`);
|
||||
|
||||
// Wait for the table to be ready for next delete click
|
||||
cy.get('.euiBasicTable').not('.euiBasicTable-loading');
|
||||
|
||||
// Delete the rule
|
||||
cy.get(deleteButtonSelector).click();
|
||||
cy.get(confirmModalButtonSelector).click();
|
||||
|
||||
// Ensure the table is empty
|
||||
cy.contains('Create your first rule');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import React, { useState } from 'react';
|
||||
import { AlertParams, ErrorCountAlertTrigger } from '.';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { AlertMetadata } from '../helper';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
notifications: { toasts: { add: () => {} } },
|
||||
} as unknown as Partial<CoreStart>);
|
||||
|
||||
interface Args {
|
||||
alertParams: AlertParams;
|
||||
metadata?: AlertMetadata;
|
||||
}
|
||||
|
||||
const stories: Meta<{}> = {
|
||||
title: 'alerting/ErrorCountAlertTrigger',
|
||||
component: ErrorCountAlertTrigger,
|
||||
decorators: [
|
||||
(StoryComponent) => {
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<div style={{ width: 400 }}>
|
||||
<StoryComponent />
|
||||
</div>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
export default stories;
|
||||
|
||||
export const CreatingInApmFromInventory: Story<Args> = ({
|
||||
alertParams,
|
||||
metadata,
|
||||
}) => {
|
||||
const [params, setParams] = useState<AlertParams>(alertParams);
|
||||
|
||||
function setAlertParams(property: string, value: any) {
|
||||
setParams({ ...params, [property]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCountAlertTrigger
|
||||
alertParams={params}
|
||||
metadata={metadata}
|
||||
setAlertParams={setAlertParams}
|
||||
setAlertProperty={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CreatingInApmFromInventory.args = {
|
||||
alertParams: {},
|
||||
metadata: {
|
||||
end: '2021-09-10T14:14:04.789Z',
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
serviceName: undefined,
|
||||
start: '2021-09-10T13:59:00.000Z',
|
||||
},
|
||||
};
|
||||
|
||||
export const CreatingInApmFromService: Story<Args> = ({
|
||||
alertParams,
|
||||
metadata,
|
||||
}) => {
|
||||
const [params, setParams] = useState<AlertParams>(alertParams);
|
||||
|
||||
function setAlertParams(property: string, value: any) {
|
||||
setParams({ ...params, [property]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCountAlertTrigger
|
||||
alertParams={params}
|
||||
metadata={metadata}
|
||||
setAlertParams={setAlertParams}
|
||||
setAlertProperty={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CreatingInApmFromService.args = {
|
||||
alertParams: {},
|
||||
metadata: {
|
||||
end: '2021-09-10T14:14:04.789Z',
|
||||
environment: 'testEnvironment',
|
||||
serviceName: 'testServiceName',
|
||||
start: '2021-09-10T13:59:00.000Z',
|
||||
},
|
||||
};
|
||||
|
||||
export const EditingInStackManagement: Story<Args> = ({
|
||||
alertParams,
|
||||
metadata,
|
||||
}) => {
|
||||
const [params, setParams] = useState<AlertParams>(alertParams);
|
||||
|
||||
function setAlertParams(property: string, value: any) {
|
||||
setParams({ ...params, [property]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCountAlertTrigger
|
||||
alertParams={params}
|
||||
metadata={metadata}
|
||||
setAlertParams={setAlertParams}
|
||||
setAlertProperty={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EditingInStackManagement.args = {
|
||||
alertParams: {
|
||||
environment: 'testEnvironment',
|
||||
serviceName: 'testServiceName',
|
||||
threshold: 25,
|
||||
windowSize: 1,
|
||||
windowUnit: 'm',
|
||||
},
|
||||
metadata: undefined,
|
||||
};
|
||||
|
||||
export const CreatingInStackManagement: Story<Args> = ({
|
||||
alertParams,
|
||||
metadata,
|
||||
}) => {
|
||||
const [params, setParams] = useState<AlertParams>(alertParams);
|
||||
|
||||
function setAlertParams(property: string, value: any) {
|
||||
setParams({ ...params, [property]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCountAlertTrigger
|
||||
alertParams={params}
|
||||
metadata={metadata}
|
||||
setAlertParams={setAlertParams}
|
||||
setAlertProperty={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CreatingInStackManagement.args = {
|
||||
alertParams: {},
|
||||
metadata: undefined,
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as stories from './error_count_alert_trigger.stories';
|
||||
import { composeStories } from '@storybook/testing-react';
|
||||
|
||||
const { CreatingInApmFromService } = composeStories(stories);
|
||||
|
||||
describe('ErrorCountAlertTrigger', () => {
|
||||
it('renders', () => {
|
||||
expect(() => render(<CreatingInApmFromService />)).not.toThrowError();
|
||||
});
|
||||
});
|
|
@ -1,51 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { AlertParams, ErrorCountAlertTrigger } from '.';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
notifications: { toasts: { add: () => {} } },
|
||||
} as unknown as Partial<CoreStart>);
|
||||
|
||||
export default {
|
||||
title: 'alerting/ErrorCountAlertTrigger',
|
||||
component: ErrorCountAlertTrigger,
|
||||
decorators: [
|
||||
(Story: React.ComponentClass) => (
|
||||
<KibanaReactContext.Provider>
|
||||
<div style={{ width: 400 }}>
|
||||
<Story />
|
||||
</div>
|
||||
</KibanaReactContext.Provider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export function Example() {
|
||||
const [params, setParams] = useState<AlertParams>({
|
||||
serviceName: 'testServiceName',
|
||||
environment: 'testEnvironment',
|
||||
threshold: 2,
|
||||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
});
|
||||
|
||||
function setAlertParams(property: string, value: any) {
|
||||
setParams({ ...params, [property]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorCountAlertTrigger
|
||||
alertParams={params}
|
||||
setAlertParams={setAlertParams}
|
||||
setAlertProperty={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -7,29 +7,25 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { defaults, omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { asInteger } from '../../../../common/utils/formatters';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
|
||||
import { ChartPreview } from '../chart_preview';
|
||||
import { EnvironmentField, IsAboveField, ServiceField } from '../fields';
|
||||
import {
|
||||
AlertMetadata,
|
||||
getIntervalAndTimeRange,
|
||||
isNewApmRuleFromStackManagement,
|
||||
TimeUnit,
|
||||
} from '../helper';
|
||||
import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt';
|
||||
import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
|
||||
export interface AlertParams {
|
||||
windowSize: number;
|
||||
windowUnit: string;
|
||||
threshold: number;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
windowSize?: number;
|
||||
windowUnit?: TimeUnit;
|
||||
threshold?: number;
|
||||
serviceName?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -40,13 +36,12 @@ interface Props {
|
|||
}
|
||||
|
||||
export function ErrorCountAlertTrigger(props: Props) {
|
||||
const { services } = useKibana();
|
||||
const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
|
||||
|
||||
const { environmentOptions } = useEnvironmentsFetcher({
|
||||
serviceName: metadata?.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
useEffect(() => {
|
||||
createCallApmApi(services as CoreStart);
|
||||
}, [services]);
|
||||
|
||||
const params = defaults(
|
||||
{ ...omit(metadata, ['start', 'end']), ...alertParams },
|
||||
|
@ -87,16 +82,14 @@ export function ErrorCountAlertTrigger(props: Props) {
|
|||
]
|
||||
);
|
||||
|
||||
if (isNewApmRuleFromStackManagement(alertParams, metadata)) {
|
||||
return <NewAlertEmptyPrompt />;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
<ServiceField value={params.serviceName} />,
|
||||
<ServiceField
|
||||
currentValue={params.serviceName}
|
||||
onChange={(value) => setAlertParams('serviceName', value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
onChange={(value) => setAlertParams('environment', value)}
|
||||
/>,
|
||||
<IsAboveField
|
||||
value={params.threshold}
|
||||
|
|
|
@ -7,56 +7,35 @@
|
|||
|
||||
import React from 'react';
|
||||
import { ServiceField, TransactionTypeField } from './fields';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { expectTextsInDocument } from '../../utils/testHelpers';
|
||||
|
||||
describe('alerting fields', () => {
|
||||
describe('Service Field', () => {
|
||||
it('renders with value', () => {
|
||||
const component = render(<ServiceField value="foo" />);
|
||||
const component = render(
|
||||
<ServiceField currentValue="foo" onChange={() => {}} />
|
||||
);
|
||||
expectTextsInDocument(component, ['foo']);
|
||||
});
|
||||
it('renders with All when value is not defined', () => {
|
||||
const component = render(<ServiceField />);
|
||||
const component = render(<ServiceField onChange={() => {}} />);
|
||||
expectTextsInDocument(component, ['All']);
|
||||
});
|
||||
});
|
||||
describe('Transaction Type Field', () => {
|
||||
it('renders select field when multiple options available', () => {
|
||||
const options = [
|
||||
{ text: 'Foo', value: 'foo' },
|
||||
{ text: 'Bar', value: 'bar' },
|
||||
];
|
||||
const { getByText, getByTestId } = render(
|
||||
<TransactionTypeField currentValue="Foo" options={options} />
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByText('Foo'));
|
||||
});
|
||||
|
||||
const selectBar = getByTestId('transactionTypeField');
|
||||
expect(selectBar instanceof HTMLSelectElement).toBeTruthy();
|
||||
const selectOptions = (selectBar as HTMLSelectElement).options;
|
||||
expect(selectOptions.length).toEqual(2);
|
||||
expect(
|
||||
Object.values(selectOptions).map((option) => option.value)
|
||||
).toEqual(['foo', 'bar']);
|
||||
});
|
||||
it('renders read-only field when single option available', () => {
|
||||
const options = [{ text: 'Bar', value: 'bar' }];
|
||||
describe('TransactionTypeField', () => {
|
||||
it('renders', () => {
|
||||
const component = render(
|
||||
<TransactionTypeField currentValue="Bar" options={options} />
|
||||
<TransactionTypeField currentValue="Bar" onChange={() => {}} />
|
||||
);
|
||||
expectTextsInDocument(component, ['Bar']);
|
||||
});
|
||||
it('renders read-only All option when no option available', () => {
|
||||
const component = render(<TransactionTypeField currentValue="" />);
|
||||
expectTextsInDocument(component, ['All']);
|
||||
});
|
||||
|
||||
it('renders current value when available', () => {
|
||||
const component = render(<TransactionTypeField currentValue="foo" />);
|
||||
const component = render(
|
||||
<TransactionTypeField currentValue="foo" onChange={() => {}} />
|
||||
);
|
||||
expectTextsInDocument(component, ['foo']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,59 +5,96 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiComboBoxOptionOption, EuiFieldNumber } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
getEnvironmentLabel,
|
||||
} from '../../../common/environment_filter_values';
|
||||
import { SuggestionsSelect } from '../shared/suggestions_select';
|
||||
import { PopoverExpression } from './service_alert_trigger/popover_expression';
|
||||
|
||||
const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', {
|
||||
const allOptionText = i18n.translate('xpack.apm.alerting.fields.allOption', {
|
||||
defaultMessage: 'All',
|
||||
});
|
||||
const allOption: EuiComboBoxOptionOption<string> = {
|
||||
label: allOptionText,
|
||||
value: allOptionText,
|
||||
};
|
||||
const environmentAllOption: EuiComboBoxOptionOption<string> = {
|
||||
label: ENVIRONMENT_ALL.text,
|
||||
value: ENVIRONMENT_ALL.value,
|
||||
};
|
||||
|
||||
export function ServiceField({ value }: { value?: string }) {
|
||||
export function ServiceField({
|
||||
allowAll = true,
|
||||
currentValue,
|
||||
onChange,
|
||||
}: {
|
||||
allowAll?: boolean;
|
||||
currentValue?: string;
|
||||
onChange: (value?: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<EuiExpression
|
||||
description={i18n.translate('xpack.apm.alerting.fields.service', {
|
||||
<PopoverExpression
|
||||
value={currentValue || allOption.value}
|
||||
title={i18n.translate('xpack.apm.alerting.fields.service', {
|
||||
defaultMessage: 'Service',
|
||||
})}
|
||||
value={value || ALL_OPTION}
|
||||
/>
|
||||
>
|
||||
<SuggestionsSelect
|
||||
allOption={allowAll ? allOption : undefined}
|
||||
customOptionText={i18n.translate(
|
||||
'xpack.apm.serviceNamesSelectCustomOptionText',
|
||||
{
|
||||
defaultMessage: 'Add \\{searchValue\\} as a new service name',
|
||||
}
|
||||
)}
|
||||
defaultValue={currentValue}
|
||||
field={SERVICE_NAME}
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.apm.serviceNamesSelectPlaceholder', {
|
||||
defaultMessage: 'Select service name',
|
||||
})}
|
||||
/>
|
||||
</PopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnvironmentField({
|
||||
currentValue,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
currentValue: string;
|
||||
options: EuiSelectOption[];
|
||||
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
onChange: (value?: string) => void;
|
||||
}) {
|
||||
const title = i18n.translate('xpack.apm.alerting.fields.environment', {
|
||||
defaultMessage: 'Environment',
|
||||
});
|
||||
if (options.length === 1) {
|
||||
return (
|
||||
<EuiExpression
|
||||
description={title}
|
||||
value={currentValue || ENVIRONMENT_ALL.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverExpression value={getEnvironmentLabel(currentValue)} title={title}>
|
||||
<EuiSelect
|
||||
defaultValue={currentValue}
|
||||
options={options}
|
||||
<PopoverExpression
|
||||
value={getEnvironmentLabel(currentValue)}
|
||||
title={i18n.translate('xpack.apm.alerting.fields.environment', {
|
||||
defaultMessage: 'Environment',
|
||||
})}
|
||||
>
|
||||
<SuggestionsSelect
|
||||
allOption={environmentAllOption}
|
||||
customOptionText={i18n.translate(
|
||||
'xpack.apm.environmentsSelectCustomOptionText',
|
||||
{
|
||||
defaultMessage: 'Add \\{searchValue\\} as a new environment',
|
||||
}
|
||||
)}
|
||||
defaultValue={getEnvironmentLabel(currentValue)}
|
||||
field={SERVICE_ENVIRONMENT}
|
||||
onChange={onChange}
|
||||
compressed
|
||||
placeholder={i18n.translate('xpack.apm.environmentsSelectPlaceholder', {
|
||||
defaultMessage: 'Select environment',
|
||||
})}
|
||||
/>
|
||||
</PopoverExpression>
|
||||
);
|
||||
|
@ -65,31 +102,33 @@ export function EnvironmentField({
|
|||
|
||||
export function TransactionTypeField({
|
||||
currentValue,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
currentValue?: string;
|
||||
options?: EuiSelectOption[];
|
||||
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
onChange: (value?: string) => void;
|
||||
}) {
|
||||
const label = i18n.translate('xpack.apm.alerting.fields.type', {
|
||||
defaultMessage: 'Type',
|
||||
});
|
||||
|
||||
if (!options || options.length <= 1) {
|
||||
return (
|
||||
<EuiExpression description={label} value={currentValue || ALL_OPTION} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverExpression value={currentValue} title={label}>
|
||||
<EuiSelect
|
||||
data-test-subj="transactionTypeField"
|
||||
<PopoverExpression value={currentValue || allOption.value} title={label}>
|
||||
<SuggestionsSelect
|
||||
allOption={allOption}
|
||||
customOptionText={i18n.translate(
|
||||
'xpack.apm.transactionTypesSelectCustomOptionText',
|
||||
{
|
||||
defaultMessage: 'Add \\{searchValue\\} as a new transaction type',
|
||||
}
|
||||
)}
|
||||
defaultValue={currentValue}
|
||||
options={options}
|
||||
field={TRANSACTION_TYPE}
|
||||
onChange={onChange}
|
||||
compressed
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.transactionTypesSelectPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select transaction type',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</PopoverExpression>
|
||||
);
|
||||
|
|
|
@ -36,14 +36,3 @@ export function getIntervalAndTimeRange({
|
|||
end: new Date(end).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function isNewApmRuleFromStackManagement(
|
||||
alertParams: any,
|
||||
metadata?: AlertMetadata
|
||||
) {
|
||||
return (
|
||||
alertParams !== undefined &&
|
||||
Object.keys(alertParams).length === 0 &&
|
||||
metadata === undefined
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,47 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @elastic/eui/href-or-on-click */
|
||||
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export function NewAlertEmptyPrompt() {
|
||||
const { services } = useKibana();
|
||||
const apmUrl = services.http?.basePath.prepend('/app/apm');
|
||||
const navigateToUrl = services.application?.navigateToUrl;
|
||||
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
if (apmUrl && navigateToUrl) {
|
||||
navigateToUrl(apmUrl);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
body={i18n.translate('xpack.apm.NewAlertEmptyPrompt.bodyDescription', {
|
||||
defaultMessage:
|
||||
'APM rules cannot be created in Stack Management. Go to APM and use the "Alerts and rules" menu.',
|
||||
})}
|
||||
actions={[
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={true}
|
||||
href={apmUrl}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{i18n.translate('xpack.apm.NewAlertEmptyPrompt.goToApmLinkText', {
|
||||
defaultMessage: 'Go to APM',
|
||||
})}
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -8,14 +8,12 @@
|
|||
import { EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { defaults, map, omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
|
@ -29,13 +27,7 @@ import {
|
|||
ServiceField,
|
||||
TransactionTypeField,
|
||||
} from '../fields';
|
||||
import {
|
||||
AlertMetadata,
|
||||
getIntervalAndTimeRange,
|
||||
isNewApmRuleFromStackManagement,
|
||||
TimeUnit,
|
||||
} from '../helper';
|
||||
import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt';
|
||||
import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
import { PopoverExpression } from '../service_alert_trigger/popover_expression';
|
||||
|
||||
|
@ -81,13 +73,9 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
const { services } = useKibana();
|
||||
const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
|
||||
|
||||
createCallApmApi(services as CoreStart);
|
||||
|
||||
const transactionTypes = useServiceTransactionTypesFetcher({
|
||||
serviceName: metadata?.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
useEffect(() => {
|
||||
createCallApmApi(services as CoreStart);
|
||||
}, [services]);
|
||||
|
||||
const params = defaults(
|
||||
{
|
||||
|
@ -100,16 +88,9 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
windowSize: 5,
|
||||
windowUnit: 'm',
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
transactionType: transactionTypes[0],
|
||||
}
|
||||
);
|
||||
|
||||
const { environmentOptions } = useEnvironmentsFetcher({
|
||||
serviceName: params.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
|
||||
const { data } = useFetcher(
|
||||
(callApmApi) => {
|
||||
const { interval, start, end } = getIntervalAndTimeRange({
|
||||
|
@ -160,25 +141,19 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
/>
|
||||
);
|
||||
|
||||
if (isNewApmRuleFromStackManagement(alertParams, metadata)) {
|
||||
return <NewAlertEmptyPrompt />;
|
||||
}
|
||||
|
||||
if (!params.serviceName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
<ServiceField value={params.serviceName} />,
|
||||
<ServiceField
|
||||
allowAll={false}
|
||||
currentValue={params.serviceName}
|
||||
onChange={(value) => setAlertParams('serviceName', value)}
|
||||
/>,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
onChange={(value) => setAlertParams('transactionType', value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
onChange={(value) => setAlertParams('environment', value)}
|
||||
/>,
|
||||
<PopoverExpression
|
||||
value={params.aggregationType}
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { defaults, omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { ANOMALY_SEVERITY } from '../../../../common/ml_constants';
|
||||
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
EnvironmentField,
|
||||
ServiceField,
|
||||
TransactionTypeField,
|
||||
} from '../fields';
|
||||
import { AlertMetadata, isNewApmRuleFromStackManagement } from '../helper';
|
||||
import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt';
|
||||
import { AlertMetadata } from '../helper';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
import { PopoverExpression } from '../service_alert_trigger/popover_expression';
|
||||
import {
|
||||
|
@ -27,16 +27,16 @@ import {
|
|||
} from './select_anomaly_severity';
|
||||
|
||||
interface AlertParams {
|
||||
anomalySeverityType:
|
||||
anomalySeverityType?:
|
||||
| ANOMALY_SEVERITY.CRITICAL
|
||||
| ANOMALY_SEVERITY.MAJOR
|
||||
| ANOMALY_SEVERITY.MINOR
|
||||
| ANOMALY_SEVERITY.WARNING;
|
||||
environment: string;
|
||||
environment?: string;
|
||||
serviceName?: string;
|
||||
transactionType?: string;
|
||||
windowSize: number;
|
||||
windowUnit: string;
|
||||
windowSize?: number;
|
||||
windowUnit?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -47,13 +47,12 @@ interface Props {
|
|||
}
|
||||
|
||||
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
||||
const { services } = useKibana();
|
||||
const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
|
||||
|
||||
const transactionTypes = useServiceTransactionTypesFetcher({
|
||||
serviceName: metadata?.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
useEffect(() => {
|
||||
createCallApmApi(services as CoreStart);
|
||||
}, [services]);
|
||||
|
||||
const params = defaults(
|
||||
{
|
||||
|
@ -68,27 +67,18 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
|||
}
|
||||
);
|
||||
|
||||
const { environmentOptions } = useEnvironmentsFetcher({
|
||||
serviceName: params.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
|
||||
if (isNewApmRuleFromStackManagement(alertParams, metadata)) {
|
||||
return <NewAlertEmptyPrompt />;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
<ServiceField value={params.serviceName} />,
|
||||
<ServiceField
|
||||
currentValue={params.serviceName}
|
||||
onChange={(value) => setAlertParams('serviceName', value)}
|
||||
/>,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
onChange={(value) => setAlertParams('transactionType', value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
onChange={(value) => setAlertParams('environment', value)}
|
||||
/>,
|
||||
<PopoverExpression
|
||||
value={<AnomalySeverity type={params.anomalySeverityType} />}
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
*/
|
||||
|
||||
import { defaults, omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { asPercent } from '../../../../common/utils/formatters';
|
||||
import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
|
||||
import { ChartPreview } from '../chart_preview';
|
||||
|
@ -23,22 +21,16 @@ import {
|
|||
ServiceField,
|
||||
TransactionTypeField,
|
||||
} from '../fields';
|
||||
import {
|
||||
AlertMetadata,
|
||||
getIntervalAndTimeRange,
|
||||
isNewApmRuleFromStackManagement,
|
||||
TimeUnit,
|
||||
} from '../helper';
|
||||
import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt';
|
||||
import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
|
||||
interface AlertParams {
|
||||
windowSize: number;
|
||||
windowUnit: string;
|
||||
threshold: number;
|
||||
serviceName: string;
|
||||
transactionType: string;
|
||||
environment: string;
|
||||
windowSize?: number;
|
||||
windowUnit?: string;
|
||||
threshold?: number;
|
||||
serviceName?: string;
|
||||
transactionType?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -52,12 +44,9 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
const { services } = useKibana();
|
||||
const { alertParams, metadata, setAlertParams, setAlertProperty } = props;
|
||||
|
||||
createCallApmApi(services as CoreStart);
|
||||
const transactionTypes = useServiceTransactionTypesFetcher({
|
||||
serviceName: metadata?.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
useEffect(() => {
|
||||
createCallApmApi(services as CoreStart);
|
||||
}, [services]);
|
||||
|
||||
const params = defaults(
|
||||
{ ...omit(metadata, ['start', 'end']), ...alertParams },
|
||||
|
@ -69,12 +58,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
}
|
||||
);
|
||||
|
||||
const { environmentOptions } = useEnvironmentsFetcher({
|
||||
serviceName: params.serviceName,
|
||||
start: metadata?.start,
|
||||
end: metadata?.end,
|
||||
});
|
||||
|
||||
const thresholdAsPercent = (params.threshold ?? 0) / 100;
|
||||
|
||||
const { data } = useFetcher(
|
||||
|
@ -108,21 +91,18 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
]
|
||||
);
|
||||
|
||||
if (isNewApmRuleFromStackManagement(alertParams, metadata)) {
|
||||
return <NewAlertEmptyPrompt />;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
<ServiceField value={params.serviceName} />,
|
||||
<ServiceField
|
||||
currentValue={params.serviceName}
|
||||
onChange={(value) => setAlertParams('serviceName', value)}
|
||||
/>,
|
||||
<TransactionTypeField
|
||||
currentValue={params.transactionType}
|
||||
options={transactionTypes.map((key) => ({ text: key, value: key }))}
|
||||
onChange={(e) => setAlertParams('transactionType', e.target.value)}
|
||||
onChange={(value) => setAlertParams('transactionType', value)}
|
||||
/>,
|
||||
<EnvironmentField
|
||||
currentValue={params.environment}
|
||||
options={environmentOptions}
|
||||
onChange={(e) => setAlertParams('environment', e.target.value)}
|
||||
onChange={(value) => setAlertParams('environment', value)}
|
||||
/>,
|
||||
<IsAboveField
|
||||
value={params.threshold}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
|
||||
interface SuggestionsSelectProps {
|
||||
allOption?: EuiComboBoxOptionOption<string>;
|
||||
customOptionText: string;
|
||||
defaultValue?: string;
|
||||
field: string;
|
||||
onChange: (value?: string) => void;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export function SuggestionsSelect({
|
||||
allOption,
|
||||
customOptionText,
|
||||
defaultValue,
|
||||
field,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: SuggestionsSelectProps) {
|
||||
const allowAll = !!allOption;
|
||||
let defaultOption: EuiComboBoxOptionOption<string> | undefined;
|
||||
|
||||
if (allowAll && !defaultValue) {
|
||||
defaultOption = allOption;
|
||||
}
|
||||
if (defaultValue) {
|
||||
defaultOption = { label: defaultValue, value: defaultValue };
|
||||
}
|
||||
const [selectedOptions, setSelectedOptions] = useState(
|
||||
defaultOption ? [defaultOption] : []
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: {
|
||||
query: { field, string: searchValue },
|
||||
},
|
||||
});
|
||||
},
|
||||
[field, searchValue],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(changedOptions: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setSelectedOptions(changedOptions);
|
||||
if (changedOptions.length === 1) {
|
||||
onChange(
|
||||
changedOptions[0].value
|
||||
? changedOptions[0].value.trim()
|
||||
: changedOptions[0].value
|
||||
);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleCreateOption = useCallback(
|
||||
(value: string) => {
|
||||
handleChange([{ label: value, value }]);
|
||||
},
|
||||
[handleChange]
|
||||
);
|
||||
|
||||
const terms = data?.terms ?? [];
|
||||
|
||||
const options: Array<EuiComboBoxOptionOption<string>> = [
|
||||
...(allOption &&
|
||||
(searchValue === '' ||
|
||||
searchValue.toLowerCase() === allOption.label.toLowerCase())
|
||||
? [allOption]
|
||||
: []),
|
||||
...terms.map((name) => {
|
||||
return { label: name, value: name };
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
async={true}
|
||||
compressed={true}
|
||||
customOptionText={customOptionText}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
onChange={handleChange}
|
||||
onCreateOption={handleCreateOption}
|
||||
onSearchChange={debounce(setSearchValue, 500)}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
selectedOptions={selectedOptions}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
style={{ minWidth: '256px' }}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { createCallApmApi } from '../../../services/rest/createCallApmApi';
|
||||
import { SuggestionsSelect } from './';
|
||||
|
||||
interface Args {
|
||||
allOption: EuiComboBoxOptionOption<string>;
|
||||
customOptionText: string;
|
||||
field: string;
|
||||
placeholder: string;
|
||||
terms: string[];
|
||||
}
|
||||
|
||||
const stories: Meta<Args> = {
|
||||
title: 'shared/SuggestionsSelect',
|
||||
component: SuggestionsSelect,
|
||||
decorators: [
|
||||
(StoryComponent, { args }) => {
|
||||
const { terms } = args;
|
||||
|
||||
const coreMock = {
|
||||
http: {
|
||||
get: () => {
|
||||
return { terms };
|
||||
},
|
||||
},
|
||||
notifications: { toasts: { add: () => {} } },
|
||||
uiSettings: { get: () => true },
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext(coreMock);
|
||||
|
||||
createCallApmApi(coreMock);
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<StoryComponent />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
export default stories;
|
||||
|
||||
export const Example: Story<Args> = ({
|
||||
allOption,
|
||||
customOptionText,
|
||||
field,
|
||||
placeholder,
|
||||
}) => {
|
||||
return (
|
||||
<SuggestionsSelect
|
||||
allOption={allOption}
|
||||
customOptionText={customOptionText}
|
||||
field={field}
|
||||
onChange={() => {}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Example.args = {
|
||||
allOption: { label: 'All the things', value: 'ALL_THE_THINGS' },
|
||||
terms: ['thing1', 'thing2'],
|
||||
customOptionText: 'Add {searchValue} as a new thing',
|
||||
field: 'test.field',
|
||||
placeholder: 'Select thing',
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { composeStories } from '@storybook/testing-react';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import * as stories from './suggestions_select.stories';
|
||||
|
||||
const { Example } = composeStories(stories);
|
||||
|
||||
describe('SuggestionsSelect', () => {
|
||||
it('renders', () => {
|
||||
expect(() => render(<Example />)).not.toThrowError();
|
||||
});
|
||||
});
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
PluginConfigDescriptor,
|
||||
PluginInitializerContext,
|
||||
} from 'src/core/server';
|
||||
import { APMOSSConfig } from 'src/plugins/apm_oss/server';
|
||||
import { APMPlugin } from './plugin';
|
||||
import { maxSuggestions } from '../../observability/common';
|
||||
import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions';
|
||||
import { APMPlugin } from './plugin';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
|
@ -40,8 +41,6 @@ const configSchema = schema.object({
|
|||
),
|
||||
telemetryCollectionEnabled: schema.boolean({ defaultValue: true }),
|
||||
metricsInterval: schema.number({ defaultValue: 30 }),
|
||||
maxServiceEnvironments: schema.number({ defaultValue: 100 }),
|
||||
maxServiceSelection: schema.number({ defaultValue: 50 }),
|
||||
profilingEnabled: schema.boolean({ defaultValue: false }),
|
||||
agent: schema.object({
|
||||
migrations: schema.object({
|
||||
|
@ -52,7 +51,17 @@ const configSchema = schema.object({
|
|||
|
||||
// plugin config
|
||||
export const config: PluginConfigDescriptor<APMXPackConfig> = {
|
||||
deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')],
|
||||
deprecations: ({ deprecate, renameFromRoot }) => [
|
||||
deprecate('enabled', '8.0.0'),
|
||||
renameFromRoot(
|
||||
'xpack.apm.maxServiceEnvironments',
|
||||
`uiSettings.overrides[${maxSuggestions}]`
|
||||
),
|
||||
renameFromRoot(
|
||||
'xpack.apm.maxServiceSelections',
|
||||
`uiSettings.overrides[${maxSuggestions}]`
|
||||
),
|
||||
],
|
||||
exposeToBrowser: {
|
||||
serviceMapEnabled: true,
|
||||
ui: true,
|
||||
|
@ -91,8 +100,6 @@ export function mergeConfigs(
|
|||
'xpack.apm.serviceMapMaxTracesPerRequest':
|
||||
apmConfig.serviceMapMaxTracesPerRequest,
|
||||
'xpack.apm.ui.enabled': apmConfig.ui.enabled,
|
||||
'xpack.apm.maxServiceEnvironments': apmConfig.maxServiceEnvironments,
|
||||
'xpack.apm.maxServiceSelection': apmConfig.maxServiceSelection,
|
||||
'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems,
|
||||
'xpack.apm.ui.transactionGroupBucketSize':
|
||||
apmConfig.ui.transactionGroupBucketSize,
|
||||
|
|
|
@ -15,7 +15,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": undefined,
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -50,7 +50,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ENVIRONMENT_NOT_DEFINED",
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -15,7 +15,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ENVIRONMENT_NOT_DEFINED",
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -59,7 +59,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ENVIRONMENT_NOT_DEFINED",
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -21,9 +21,10 @@ describe('getAllEnvironments', () => {
|
|||
it('fetches all environments', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getAllEnvironments({
|
||||
serviceName: 'test',
|
||||
searchAggregatedTransactions: false,
|
||||
serviceName: 'test',
|
||||
setup,
|
||||
size: 50,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -33,10 +34,11 @@ describe('getAllEnvironments', () => {
|
|||
it('fetches all environments with includeMissing', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getAllEnvironments({
|
||||
includeMissing: true,
|
||||
searchAggregatedTransactions: false,
|
||||
serviceName: 'test',
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
includeMissing: true,
|
||||
size: 50,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -19,22 +19,23 @@ import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregate
|
|||
* It's used in places where we get the list of all possible environments.
|
||||
*/
|
||||
export async function getAllEnvironments({
|
||||
includeMissing = false,
|
||||
searchAggregatedTransactions,
|
||||
serviceName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
includeMissing = false,
|
||||
size,
|
||||
}: {
|
||||
includeMissing?: boolean;
|
||||
searchAggregatedTransactions: boolean;
|
||||
serviceName?: string;
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
includeMissing?: boolean;
|
||||
size: number;
|
||||
}) {
|
||||
const operationName = serviceName
|
||||
? 'get_all_environments_for_service'
|
||||
: 'get_all_environments_for_all_services';
|
||||
|
||||
const { apmEventClient, config } = setup;
|
||||
const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
// omit filter for service.name if "All" option is selected
|
||||
const serviceNameFilter = serviceName
|
||||
|
@ -65,7 +66,7 @@ export async function getAllEnvironments({
|
|||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
size: maxServiceEnvironments,
|
||||
size,
|
||||
...(!serviceName ? { min_doc_count: 0 } : {}),
|
||||
missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined,
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ describe('getEnvironments', () => {
|
|||
setup,
|
||||
serviceName: 'foo',
|
||||
searchAggregatedTransactions: false,
|
||||
size: 50,
|
||||
start: 0,
|
||||
end: 50000,
|
||||
})
|
||||
|
@ -37,6 +38,7 @@ describe('getEnvironments', () => {
|
|||
getEnvironments({
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
size: 50,
|
||||
start: 0,
|
||||
end: 50000,
|
||||
})
|
||||
|
|
|
@ -20,15 +20,17 @@ import { Setup } from '../helpers/setup_request';
|
|||
* filtered by range.
|
||||
*/
|
||||
export async function getEnvironments({
|
||||
setup,
|
||||
serviceName,
|
||||
searchAggregatedTransactions,
|
||||
serviceName,
|
||||
setup,
|
||||
size,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
setup: Setup;
|
||||
serviceName?: string;
|
||||
searchAggregatedTransactions: boolean;
|
||||
size: number;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
|
@ -36,7 +38,7 @@ export async function getEnvironments({
|
|||
? 'get_environments_for_service'
|
||||
: 'get_environments';
|
||||
|
||||
const { apmEventClient, config } = setup;
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const filter = rangeQuery(start, end);
|
||||
|
||||
|
@ -46,8 +48,6 @@ export async function getEnvironments({
|
|||
});
|
||||
}
|
||||
|
||||
const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
events: [
|
||||
|
@ -70,7 +70,7 @@ export async function getEnvironments({
|
|||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED.value,
|
||||
size: maxServiceEnvironments,
|
||||
size,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
TermsEnumRequest,
|
||||
TermsEnumResponse,
|
||||
} from '@elastic/elasticsearch/api/types';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { withApmSpan } from '../../../../utils/with_apm_span';
|
||||
import { Profile } from '../../../../../typings/es_schemas/ui/profile';
|
||||
|
@ -39,6 +43,10 @@ export type APMEventESSearchRequest = Omit<ESSearchRequest, 'index'> & {
|
|||
};
|
||||
};
|
||||
|
||||
export type APMEventESTermsEnumRequest = Omit<TermsEnumRequest, 'index'> & {
|
||||
apm: { events: ProcessorEvent[] };
|
||||
};
|
||||
|
||||
// These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here.
|
||||
// See https://github.com/microsoft/TypeScript/issues/37888
|
||||
type TypeOfProcessorEvent<T extends ProcessorEvent> = {
|
||||
|
@ -124,5 +132,44 @@ export function createApmEventClient({
|
|||
requestParams: searchParams,
|
||||
});
|
||||
},
|
||||
|
||||
async termsEnum(
|
||||
operationName: string,
|
||||
params: APMEventESTermsEnumRequest
|
||||
): Promise<TermsEnumResponse> {
|
||||
const requestType = 'terms_enum';
|
||||
const { index } = unpackProcessorEvents(params, indices);
|
||||
|
||||
return callAsyncWithDebug({
|
||||
cb: () => {
|
||||
const { apm, ...rest } = params;
|
||||
const termsEnumPromise = withApmSpan(operationName, () =>
|
||||
cancelEsRequestOnAbort(
|
||||
esClient.termsEnum({
|
||||
index: Array.isArray(index) ? index.join(',') : index,
|
||||
...rest,
|
||||
}),
|
||||
request
|
||||
)
|
||||
);
|
||||
|
||||
return unwrapEsResponse(termsEnumPromise);
|
||||
},
|
||||
getDebugMessage: () => ({
|
||||
body: getDebugBody({
|
||||
params,
|
||||
requestType,
|
||||
operationName,
|
||||
}),
|
||||
title: getDebugTitle(request),
|
||||
}),
|
||||
isCalledWithInternalUser: false,
|
||||
debug,
|
||||
request,
|
||||
requestType,
|
||||
operationName,
|
||||
requestParams: params,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ESSearchRequest,
|
||||
ESFilter,
|
||||
} from '../../../../../../../../src/core/types/elasticsearch';
|
||||
import { APMEventESSearchRequest } from '.';
|
||||
import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.';
|
||||
import {
|
||||
ApmIndicesConfig,
|
||||
ApmIndicesName,
|
||||
|
@ -28,7 +28,7 @@ const processorEventIndexMap: Record<ProcessorEvent, ApmIndicesName> = {
|
|||
};
|
||||
|
||||
export function unpackProcessorEvents(
|
||||
request: APMEventESSearchRequest,
|
||||
request: APMEventESSearchRequest | APMEventESTermsEnumRequest,
|
||||
indices: ApmIndicesConfig
|
||||
) {
|
||||
const { apm, ...params } = request;
|
||||
|
|
|
@ -99,7 +99,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": undefined,
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -127,7 +127,7 @@ Object {
|
|||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ALL_OPTION_VALUE",
|
||||
"size": 100,
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -15,12 +15,13 @@ import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_
|
|||
export async function getExistingEnvironmentsForService({
|
||||
serviceName,
|
||||
setup,
|
||||
size,
|
||||
}: {
|
||||
serviceName: string | undefined;
|
||||
setup: Setup;
|
||||
size: number;
|
||||
}) {
|
||||
const { internalClient, indices, config } = setup;
|
||||
const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments'];
|
||||
const { internalClient, indices } = setup;
|
||||
|
||||
const bool = serviceName
|
||||
? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] }
|
||||
|
@ -36,7 +37,7 @@ export async function getExistingEnvironmentsForService({
|
|||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ALL_OPTION_VALUE,
|
||||
size: maxServiceEnvironments,
|
||||
size,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -20,15 +20,22 @@ export async function getEnvironments({
|
|||
serviceName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
size,
|
||||
}: {
|
||||
serviceName: string | undefined;
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
size: number;
|
||||
}) {
|
||||
return withApmSpan('get_environments_for_agent_configuration', async () => {
|
||||
const [allEnvironments, existingEnvironments] = await Promise.all([
|
||||
getAllEnvironments({ serviceName, setup, searchAggregatedTransactions }),
|
||||
getExistingEnvironmentsForService({ serviceName, setup }),
|
||||
getAllEnvironments({
|
||||
searchAggregatedTransactions,
|
||||
serviceName,
|
||||
setup,
|
||||
size,
|
||||
}),
|
||||
getExistingEnvironmentsForService({ serviceName, setup, size }),
|
||||
]);
|
||||
|
||||
return [ALL_OPTION_VALUE, ...allEnvironments].map((environment) => {
|
||||
|
|
|
@ -15,15 +15,17 @@ import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggreg
|
|||
export type AgentConfigurationServicesAPIResponse = PromiseReturnType<
|
||||
typeof getServiceNames
|
||||
>;
|
||||
|
||||
export async function getServiceNames({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
size,
|
||||
}: {
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
size: number;
|
||||
}) {
|
||||
const { apmEventClient, config } = setup;
|
||||
const maxServiceSelection = config['xpack.apm.maxServiceSelection'];
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
|
@ -42,8 +44,8 @@ export async function getServiceNames({
|
|||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: maxServiceSelection,
|
||||
min_doc_count: 0,
|
||||
size,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,9 +27,10 @@ describe('agent configuration queries', () => {
|
|||
it('fetches all environments', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getAllEnvironments({
|
||||
searchAggregatedTransactions: false,
|
||||
serviceName: 'foo',
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
size: 50,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -43,6 +44,7 @@ describe('agent configuration queries', () => {
|
|||
getExistingEnvironmentsForService({
|
||||
serviceName: 'foo',
|
||||
setup,
|
||||
size: 50,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -56,6 +58,7 @@ describe('agent configuration queries', () => {
|
|||
getServiceNames({
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
size: 50,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
46
x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts
Normal file
46
x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
export async function getSuggestions({
|
||||
field,
|
||||
searchAggregatedTransactions,
|
||||
setup,
|
||||
size,
|
||||
string,
|
||||
}: {
|
||||
field: string;
|
||||
searchAggregatedTransactions: boolean;
|
||||
setup: Setup;
|
||||
size: number;
|
||||
string: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.termsEnum('get_suggestions', {
|
||||
apm: {
|
||||
events: [
|
||||
getProcessorEventForAggregatedTransactions(
|
||||
searchAggregatedTransactions
|
||||
),
|
||||
ProcessorEvent.error,
|
||||
ProcessorEvent.metric,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
case_insensitive: true,
|
||||
field,
|
||||
size,
|
||||
string,
|
||||
},
|
||||
});
|
||||
|
||||
return { terms: response.terms };
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { maxSuggestions } from '../../../observability/common';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getEnvironments } from '../lib/environments/get_environments';
|
||||
|
@ -26,7 +27,7 @@ const environmentsRoute = createApmServerRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { context, params } = resources;
|
||||
const { serviceName, start, end } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
|
||||
apmEventClient: setup.apmEventClient,
|
||||
|
@ -35,11 +36,14 @@ const environmentsRoute = createApmServerRoute({
|
|||
end,
|
||||
kuery: '',
|
||||
});
|
||||
|
||||
const size = await context.core.uiSettings.client.get<number>(
|
||||
maxSuggestions
|
||||
);
|
||||
const environments = await getEnvironments({
|
||||
setup,
|
||||
serviceName,
|
||||
searchAggregatedTransactions,
|
||||
size,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces';
|
|||
import { transactionRouteRepository } from './transactions';
|
||||
import { APMRouteHandlerResources } from './typings';
|
||||
import { historicalDataRouteRepository } from './historical_data';
|
||||
import { suggestionsRouteRepository } from './suggestions';
|
||||
|
||||
const getTypedGlobalApmServerRouteRepository = () => {
|
||||
const repository = createApmServerRouteRepository()
|
||||
|
@ -45,6 +46,7 @@ const getTypedGlobalApmServerRouteRepository = () => {
|
|||
.merge(serviceMapRouteRepository)
|
||||
.merge(serviceNodeRouteRepository)
|
||||
.merge(serviceRouteRepository)
|
||||
.merge(suggestionsRouteRepository)
|
||||
.merge(traceRouteRepository)
|
||||
.merge(transactionRouteRepository)
|
||||
.merge(alertsChartPreviewRouteRepository)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import * as t from 'io-ts';
|
||||
import Boom from '@hapi/boom';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils';
|
||||
import { maxSuggestions } from '../../../../observability/common';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names';
|
||||
import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration';
|
||||
|
@ -251,9 +252,13 @@ const listAgentConfigurationServicesRoute = createApmServerRoute({
|
|||
start,
|
||||
end,
|
||||
});
|
||||
const size = await resources.context.core.uiSettings.client.get<number>(
|
||||
maxSuggestions
|
||||
);
|
||||
const serviceNames = await getServiceNames({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
setup,
|
||||
size,
|
||||
});
|
||||
|
||||
return { serviceNames };
|
||||
|
@ -269,7 +274,7 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { context, params } = resources;
|
||||
|
||||
const { serviceName, start, end } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
|
||||
|
@ -279,11 +284,14 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({
|
|||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const size = await context.core.uiSettings.client.get<number>(
|
||||
maxSuggestions
|
||||
);
|
||||
const environments = await getEnvironments({
|
||||
serviceName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
size,
|
||||
});
|
||||
|
||||
return { environments };
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import Boom from '@hapi/boom';
|
||||
import { maxSuggestions } from '../../../../observability/common';
|
||||
import { isActivePlatinumLicense } from '../../../common/license_check';
|
||||
import { ML_ERRORS } from '../../../common/anomaly_detection';
|
||||
import { createApmServerRoute } from '../create_apm_server_route';
|
||||
|
@ -92,11 +93,14 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({
|
|||
config: setup.config,
|
||||
kuery: '',
|
||||
});
|
||||
|
||||
const size = await resources.context.core.uiSettings.client.get<number>(
|
||||
maxSuggestions
|
||||
);
|
||||
const environments = await getAllEnvironments({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
includeMissing: true,
|
||||
searchAggregatedTransactions,
|
||||
setup,
|
||||
size,
|
||||
});
|
||||
|
||||
return { environments };
|
||||
|
|
47
x-pack/plugins/apm/server/routes/suggestions.ts
Normal file
47
x-pack/plugins/apm/server/routes/suggestions.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { maxSuggestions } from '../../../observability/common';
|
||||
import { getSuggestions } from '../lib/suggestions/get_suggestions';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
|
||||
const suggestionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: t.partial({
|
||||
query: t.type({ field: t.string, string: t.string }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { context, params } = resources;
|
||||
const { field, string } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
|
||||
apmEventClient: setup.apmEventClient,
|
||||
config: setup.config,
|
||||
kuery: '',
|
||||
});
|
||||
const size = await context.core.uiSettings.client.get<number>(
|
||||
maxSuggestions
|
||||
);
|
||||
const suggestions = await getSuggestions({
|
||||
field,
|
||||
searchAggregatedTransactions,
|
||||
setup,
|
||||
size,
|
||||
string,
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
});
|
||||
|
||||
export const suggestionsRouteRepository =
|
||||
createApmServerRouteRepository().add(suggestionsRoute);
|
|
@ -79,12 +79,6 @@ export async function inspectSearchParams(
|
|||
|
||||
case 'xpack.apm.metricsInterval':
|
||||
return 30;
|
||||
|
||||
case 'xpack.apm.maxServiceEnvironments':
|
||||
return 100;
|
||||
|
||||
case 'xpack.apm.maxServiceSelection':
|
||||
return 50;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export type { AsDuration, AsPercent } from './utils/formatters';
|
||||
export { enableInspectEsQueries, maxSuggestions } from './ui_settings_keys';
|
||||
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export const enableInspectEsQueries = 'observability:enableInspectEsQueries';
|
||||
export const maxSuggestions = 'observability:maxSuggestions';
|
||||
|
|
16
x-pack/plugins/observability/dev_docs/feature_flags.md
Normal file
16
x-pack/plugins/observability/dev_docs/feature_flags.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
## Feature flags and advanced settings
|
||||
|
||||
To set up a flagged feature or other advanced setting, add the name of the feature key (`observability:myFeature`) to [common/ui_settings_keys.ts](../common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](../server/ui_settings.ts).
|
||||
|
||||
Test for the feature like:
|
||||
|
||||
```js
|
||||
import { myFeatureEnabled } from '../ui_settings_keys';
|
||||
if (core.uiSettings.get(myFeatureEnabled)) {
|
||||
doStuff();
|
||||
}
|
||||
```
|
||||
|
||||
In order for telemetry to be collected, the keys and types need to be added in [src/plugins/kibana_usage_collection/server/collectors/management/schema.ts](../../../../src/plugins/kibana_usage_collection/server/collectors/management/schema.ts) and [src/plugins/kibana_usage_collection/server/collectors/management/types.ts](../../../../src/plugins/kibana_usage_collection/server/collectors/management/types.ts).
|
||||
|
||||
Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability.
|
|
@ -9,16 +9,16 @@ import { schema } from '@kbn/config-schema';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { UiSettingsParams } from '../../../../src/core/types';
|
||||
import { observabilityFeatureId } from '../common';
|
||||
import { enableInspectEsQueries } from '../common/ui_settings_keys';
|
||||
import { enableInspectEsQueries, maxSuggestions } from '../common/ui_settings_keys';
|
||||
|
||||
/**
|
||||
* uiSettings definitions for Observability.
|
||||
*/
|
||||
export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
|
||||
export const uiSettings: Record<string, UiSettingsParams<boolean | number>> = {
|
||||
[enableInspectEsQueries]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', {
|
||||
defaultMessage: 'inspect ES queries',
|
||||
defaultMessage: 'Inspect ES queries',
|
||||
}),
|
||||
value: false,
|
||||
description: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentDescription', {
|
||||
|
@ -26,4 +26,15 @@ export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
|
|||
}),
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[maxSuggestions]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.maxSuggestionsUiSettingName', {
|
||||
defaultMessage: 'Maximum suggestions',
|
||||
}),
|
||||
value: 100,
|
||||
description: i18n.translate('xpack.observability.maxSuggestionsUiSettingDescription', {
|
||||
defaultMessage: 'Maximum number of suggestions fetched in autocomplete selection boxes.',
|
||||
}),
|
||||
schema: schema.number(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6193,7 +6193,6 @@
|
|||
"xpack.apm.alertAnnotationCriticalTitle": "重大アラート",
|
||||
"xpack.apm.alertAnnotationNoSeverityTitle": "アラート",
|
||||
"xpack.apm.alertAnnotationWarningTitle": "警告アラート",
|
||||
"xpack.apm.alerting.fields.all_option": "すべて",
|
||||
"xpack.apm.alerting.fields.environment": "環境",
|
||||
"xpack.apm.alerting.fields.service": "サービス",
|
||||
"xpack.apm.alerting.fields.type": "型",
|
||||
|
|
|
@ -6243,7 +6243,6 @@
|
|||
"xpack.apm.alertAnnotationCriticalTitle": "紧急告警",
|
||||
"xpack.apm.alertAnnotationNoSeverityTitle": "告警",
|
||||
"xpack.apm.alertAnnotationWarningTitle": "警告告警",
|
||||
"xpack.apm.alerting.fields.all_option": "全部",
|
||||
"xpack.apm.alerting.fields.environment": "环境",
|
||||
"xpack.apm.alerting.fields.service": "服务",
|
||||
"xpack.apm.alerting.fields.type": "类型",
|
||||
|
|
|
@ -137,6 +137,11 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
|
|||
loadTestFile(require.resolve('./settings/custom_link'));
|
||||
});
|
||||
|
||||
// suggestions
|
||||
describe('suggestions', function () {
|
||||
loadTestFile(require.resolve('./suggestions/suggestions'));
|
||||
});
|
||||
|
||||
// traces
|
||||
describe('traces/top_traces', function () {
|
||||
loadTestFile(require.resolve('./traces/top_traces'));
|
||||
|
|
143
x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts
Normal file
143
x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../plugins/apm/common/elasticsearch_fieldnames';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { registry } from '../../common/registry';
|
||||
|
||||
export default function suggestionsTests({ getService }: FtrProviderContext) {
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
|
||||
registry.when(
|
||||
'suggestions when data is loaded',
|
||||
{ config: 'basic', archives: [archiveName] },
|
||||
() => {
|
||||
describe('with environment', () => {
|
||||
describe('with an empty string parameter', () => {
|
||||
it('returns all environments', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: SERVICE_ENVIRONMENT, string: '' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"production",
|
||||
"testing",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a string parameter', () => {
|
||||
it('returns items matching the string parameter', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: SERVICE_ENVIRONMENT, string: 'pr' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"production",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with service name', () => {
|
||||
describe('with an empty string parameter', () => {
|
||||
it('returns all services', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: SERVICE_NAME, string: '' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"auditbeat",
|
||||
"opbeans-dotnet",
|
||||
"opbeans-go",
|
||||
"opbeans-java",
|
||||
"opbeans-node",
|
||||
"opbeans-python",
|
||||
"opbeans-ruby",
|
||||
"opbeans-rum",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a string parameter', () => {
|
||||
it('returns items matching the string parameter', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: SERVICE_NAME, string: 'aud' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"auditbeat",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with transaction type', () => {
|
||||
describe('with an empty string parameter', () => {
|
||||
it('returns all transaction types', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: TRANSACTION_TYPE, string: '' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"Worker",
|
||||
"celery",
|
||||
"page-load",
|
||||
"request",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a string parameter', () => {
|
||||
it('returns items matching the string parameter', async () => {
|
||||
const { body } = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/suggestions',
|
||||
params: { query: { field: TRANSACTION_TYPE, string: 'w' } },
|
||||
});
|
||||
|
||||
expectSnapshot(body).toMatchInline(`
|
||||
Object {
|
||||
"terms": Array [
|
||||
"Worker",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue