mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* [APM] Support specific fields when creating service groups (#142201) * add support to anomaly rule type to store supported service group fields in alert * address PR feedback and fixes checks * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * add API tests for field validation * fixes linting * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * fixes multi_terms sort order paths, for each rule type query * adds unit tests and moves some source files * fixed back import path * PR feedback * improvements to kuery validation * fixes selecting 'All' in service.name, transaction.type fields when creating/editing APM Rules (#143861) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e82e0a150f
commit
796751e2d3
22 changed files with 700 additions and 83 deletions
67
x-pack/plugins/apm/common/service_groups.test.ts
Normal file
67
x-pack/plugins/apm/common/service_groups.test.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 {
|
||||
isSupportedField,
|
||||
validateServiceGroupKuery,
|
||||
SERVICE_GROUP_SUPPORTED_FIELDS,
|
||||
} from './service_groups';
|
||||
import {
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_DURATION,
|
||||
SERVICE_FRAMEWORK_VERSION,
|
||||
} from './elasticsearch_fieldnames';
|
||||
|
||||
describe('service_groups common utils', () => {
|
||||
describe('isSupportedField', () => {
|
||||
it('should allow supported fields', () => {
|
||||
SERVICE_GROUP_SUPPORTED_FIELDS.map((field) => {
|
||||
expect(isSupportedField(field)).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should reject unsupported fields', () => {
|
||||
const unsupportedFields = [
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_DURATION,
|
||||
SERVICE_FRAMEWORK_VERSION,
|
||||
];
|
||||
unsupportedFields.map((field) => {
|
||||
expect(isSupportedField(field)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('validateServiceGroupKuery', () => {
|
||||
it('should validate supported KQL filter for a service group', () => {
|
||||
const result = validateServiceGroupKuery(
|
||||
`service.name: testbeans* or agent.name: "nodejs"`
|
||||
);
|
||||
expect(result).toHaveProperty('isValidFields', true);
|
||||
expect(result).toHaveProperty('isValidSyntax', true);
|
||||
expect(result).not.toHaveProperty('message');
|
||||
});
|
||||
it('should return validation error when unsupported fields are used', () => {
|
||||
const result = validateServiceGroupKuery(
|
||||
`service.name: testbeans* or agent.name: "nodejs" or transaction.type: request`
|
||||
);
|
||||
expect(result).toHaveProperty('isValidFields', false);
|
||||
expect(result).toHaveProperty('isValidSyntax', true);
|
||||
expect(result).toHaveProperty(
|
||||
'message',
|
||||
'Query filter for service group does not support fields [transaction.type]'
|
||||
);
|
||||
});
|
||||
it('should return parsing error when KQL is incomplete', () => {
|
||||
const result = validateServiceGroupKuery(
|
||||
`service.name: testbeans* or agent.name: "nod`
|
||||
);
|
||||
expect(result).toHaveProperty('isValidFields', false);
|
||||
expect(result).toHaveProperty('isValidSyntax', false);
|
||||
expect(result).toHaveProperty('message');
|
||||
expect(result).not.toBe('');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getKueryFields } from './utils/get_kuery_fields';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
} from './elasticsearch_fieldnames';
|
||||
|
||||
const LABELS = 'labels'; // implies labels.* wildcard
|
||||
|
||||
export const APM_SERVICE_GROUP_SAVED_OBJECT_TYPE = 'apm-service-group';
|
||||
export const SERVICE_GROUP_COLOR_DEFAULT = '#D1DAE7';
|
||||
export const MAX_NUMBER_OF_SERVICE_GROUPS = 500;
|
||||
|
@ -20,3 +32,51 @@ export interface SavedServiceGroup extends ServiceGroup {
|
|||
id: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export const SERVICE_GROUP_SUPPORTED_FIELDS = [
|
||||
AGENT_NAME,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
LABELS,
|
||||
];
|
||||
|
||||
export function isSupportedField(fieldName: string) {
|
||||
return (
|
||||
fieldName.startsWith(LABELS) ||
|
||||
SERVICE_GROUP_SUPPORTED_FIELDS.includes(fieldName)
|
||||
);
|
||||
}
|
||||
|
||||
export function validateServiceGroupKuery(kuery: string): {
|
||||
isValidFields: boolean;
|
||||
isValidSyntax: boolean;
|
||||
message?: string;
|
||||
} {
|
||||
try {
|
||||
const kueryFields = getKueryFields([fromKueryExpression(kuery)]);
|
||||
const unsupportedKueryFields = kueryFields.filter(
|
||||
(fieldName) => !isSupportedField(fieldName)
|
||||
);
|
||||
if (unsupportedKueryFields.length === 0) {
|
||||
return { isValidFields: true, isValidSyntax: true };
|
||||
}
|
||||
return {
|
||||
isValidFields: false,
|
||||
isValidSyntax: true,
|
||||
message: i18n.translate('xpack.apm.serviceGroups.invalidFields.message', {
|
||||
defaultMessage:
|
||||
'Query filter for service group does not support fields [{unsupportedFieldNames}]',
|
||||
values: {
|
||||
unsupportedFieldNames: unsupportedKueryFields.join(', '),
|
||||
},
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isValidFields: false,
|
||||
isValidSyntax: false,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { SERVICE_NODE_NAME_MISSING } from '../service_nodes';
|
||||
|
||||
export function environmentQuery(
|
||||
environment: string
|
||||
environment: string | undefined
|
||||
): QueryDslQueryContainer[] {
|
||||
if (!environment || environment === ENVIRONMENT_ALL.value) {
|
||||
return [];
|
||||
|
|
|
@ -38,7 +38,9 @@ export function ServiceField({
|
|||
})}
|
||||
>
|
||||
<SuggestionsSelect
|
||||
customOptions={allowAll ? [ENVIRONMENT_ALL] : undefined}
|
||||
customOptions={
|
||||
allowAll ? [{ label: allOptionText, value: '' }] : undefined
|
||||
}
|
||||
customOptionText={i18n.translate(
|
||||
'xpack.apm.serviceNamesSelectCustomOptionText',
|
||||
{
|
||||
|
@ -106,7 +108,7 @@ export function TransactionTypeField({
|
|||
return (
|
||||
<PopoverExpression value={currentValue || allOptionText} title={label}>
|
||||
<SuggestionsSelect
|
||||
customOptions={[ENVIRONMENT_ALL]}
|
||||
customOptions={[{ label: allOptionText, value: '' }]}
|
||||
customOptionText={i18n.translate(
|
||||
'xpack.apm.transactionTypesSelectCustomOptionText',
|
||||
{
|
||||
|
|
|
@ -27,6 +27,10 @@ import { KueryBar } from '../../../shared/kuery_bar';
|
|||
import { ServiceListPreview } from './service_list_preview';
|
||||
import type { StagedServiceGroup } from './save_modal';
|
||||
import { getDateRange } from '../../../../context/url_params_context/helpers';
|
||||
import {
|
||||
validateServiceGroupKuery,
|
||||
isSupportedField,
|
||||
} from '../../../../../common/service_groups';
|
||||
|
||||
const CentralizedContainer = styled.div`
|
||||
display: flex;
|
||||
|
@ -39,13 +43,6 @@ const MAX_CONTAINER_HEIGHT = 600;
|
|||
const MODAL_HEADER_HEIGHT = 180;
|
||||
const MODAL_FOOTER_HEIGHT = 80;
|
||||
|
||||
const suggestedFieldsWhitelist = [
|
||||
'agent.name',
|
||||
'service.name',
|
||||
'service.language.name',
|
||||
'service.environment',
|
||||
];
|
||||
|
||||
const Container = styled.div`
|
||||
width: 600px;
|
||||
height: ${MAX_CONTAINER_HEIGHT}px;
|
||||
|
@ -70,6 +67,9 @@ export function SelectServices({
|
|||
}: Props) {
|
||||
const [kuery, setKuery] = useState(serviceGroup?.kuery || '');
|
||||
const [stagedKuery, setStagedKuery] = useState(serviceGroup?.kuery || '');
|
||||
const [kueryValidationMessage, setKueryValidationMessage] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit) {
|
||||
|
@ -78,6 +78,14 @@ export function SelectServices({
|
|||
}
|
||||
}, [isEdit, serviceGroup.kuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stagedKuery) {
|
||||
return;
|
||||
}
|
||||
const { message } = validateServiceGroupKuery(stagedKuery);
|
||||
setKueryValidationMessage(message);
|
||||
}, [stagedKuery]);
|
||||
|
||||
const { start, end } = useMemo(
|
||||
() =>
|
||||
getDateRange({
|
||||
|
@ -122,6 +130,11 @@ export function SelectServices({
|
|||
}
|
||||
)}
|
||||
</EuiText>
|
||||
{kueryValidationMessage && (
|
||||
<EuiText color="danger" size="s">
|
||||
{kueryValidationMessage}
|
||||
</EuiText>
|
||||
)}
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<KueryBar
|
||||
|
@ -144,10 +157,7 @@ export function SelectServices({
|
|||
},
|
||||
} = querySuggestion;
|
||||
|
||||
return (
|
||||
fieldName.startsWith('label') ||
|
||||
suggestedFieldsWhitelist.includes(fieldName)
|
||||
);
|
||||
return isSupportedField(fieldName);
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
APM_SERVICE_GROUP_SAVED_OBJECT_TYPE,
|
||||
MAX_NUMBER_OF_SERVICE_GROUPS,
|
||||
} from '../../../../common/service_groups';
|
||||
import { getKueryFields } from '../../helpers/get_kuery_fields';
|
||||
import { getKueryFields } from '../../../../common/utils/get_kuery_fields';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
AGENT_VERSION,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
IScopedClusterClient,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { alertingEsClient } from '../../alerting_es_client';
|
||||
import {
|
||||
getServiceGroupFields,
|
||||
getServiceGroupFieldsAgg,
|
||||
} from '../get_service_group_fields';
|
||||
import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices';
|
||||
import { RegisterRuleDependencies } from '../../register_apm_rule_types';
|
||||
|
||||
export async function getServiceGroupFieldsForAnomaly({
|
||||
config$,
|
||||
scopedClusterClient,
|
||||
savedObjectsClient,
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
timestamp,
|
||||
bucketSpan,
|
||||
}: {
|
||||
config$: RegisterRuleDependencies['config$'];
|
||||
scopedClusterClient: IScopedClusterClient;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
transactionType: string;
|
||||
timestamp: number;
|
||||
bucketSpan: number;
|
||||
}) {
|
||||
const config = await firstValueFrom(config$);
|
||||
const indices = await getApmIndices({
|
||||
config,
|
||||
savedObjectsClient,
|
||||
});
|
||||
const { transaction: index } = indices;
|
||||
|
||||
const params = {
|
||||
index,
|
||||
body: {
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [TRANSACTION_TYPE]: transactionType } },
|
||||
{ term: { [SERVICE_ENVIRONMENT]: environment } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: timestamp,
|
||||
lte: timestamp + bucketSpan * 1000,
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
...getServiceGroupFieldsAgg({
|
||||
sort: [{ [TRANSACTION_DURATION]: { order: 'desc' as const } }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await alertingEsClient({
|
||||
scopedClusterClient,
|
||||
params,
|
||||
});
|
||||
if (!response.aggregations) {
|
||||
return {};
|
||||
}
|
||||
return getServiceGroupFields(response.aggregations);
|
||||
}
|
|
@ -46,6 +46,7 @@ import { getAlertUrlTransaction } from '../../../../../common/utils/formatters';
|
|||
import { getMLJobs } from '../../../service_map/get_service_anomalies';
|
||||
import { apmActionVariables } from '../../action_variables';
|
||||
import { RegisterRuleDependencies } from '../../register_apm_rule_types';
|
||||
import { getServiceGroupFieldsForAnomaly } from './get_service_group_fields_for_anomaly';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
serviceName: schema.maybe(schema.string()),
|
||||
|
@ -66,6 +67,7 @@ const ruleTypeConfig = RULE_TYPES_CONFIG[ApmRuleType.Anomaly];
|
|||
export function registerAnomalyRuleType({
|
||||
logger,
|
||||
ruleDataClient,
|
||||
config$,
|
||||
alerting,
|
||||
ml,
|
||||
basePath,
|
||||
|
@ -102,6 +104,7 @@ export function registerAnomalyRuleType({
|
|||
if (!ml) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ruleParams = params;
|
||||
const request = {} as KibanaRequest;
|
||||
const { mlAnomalySearch } = ml.mlSystemProvider(
|
||||
|
@ -161,8 +164,14 @@ export function registerAnomalyRuleType({
|
|||
},
|
||||
},
|
||||
},
|
||||
...termQuery('partition_field_value', ruleParams.serviceName),
|
||||
...termQuery('by_field_value', ruleParams.transactionType),
|
||||
...termQuery(
|
||||
'partition_field_value',
|
||||
ruleParams.serviceName,
|
||||
{ queryEmptyString: false }
|
||||
),
|
||||
...termQuery('by_field_value', ruleParams.transactionType, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(
|
||||
'detector_index',
|
||||
getApmMlDetectorIndex(ApmMlDetectorType.txLatency)
|
||||
|
@ -178,7 +187,8 @@ export function registerAnomalyRuleType({
|
|||
{ field: 'by_field_value' },
|
||||
{ field: 'job_id' },
|
||||
],
|
||||
size: 10000,
|
||||
size: 1000,
|
||||
order: { 'latest_score.record_score': 'desc' as const },
|
||||
},
|
||||
aggs: {
|
||||
latest_score: {
|
||||
|
@ -188,6 +198,8 @@ export function registerAnomalyRuleType({
|
|||
{ field: 'partition_field_value' },
|
||||
{ field: 'by_field_value' },
|
||||
{ field: 'job_id' },
|
||||
{ field: 'timestamp' },
|
||||
{ field: 'bucket_span' },
|
||||
] as const),
|
||||
sort: {
|
||||
timestamp: 'desc' as const,
|
||||
|
@ -222,14 +234,35 @@ export function registerAnomalyRuleType({
|
|||
transactionType: latest.by_field_value as string,
|
||||
environment: job.environment,
|
||||
score: latest.record_score as number,
|
||||
timestamp: Date.parse(latest.timestamp as string),
|
||||
bucketSpan: latest.bucket_span as number,
|
||||
};
|
||||
})
|
||||
.filter((anomaly) =>
|
||||
anomaly ? anomaly.score >= threshold : false
|
||||
) ?? [];
|
||||
|
||||
compact(anomalies).forEach((anomaly) => {
|
||||
const { serviceName, environment, transactionType, score } = anomaly;
|
||||
for (const anomaly of compact(anomalies)) {
|
||||
const {
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
score,
|
||||
timestamp,
|
||||
bucketSpan,
|
||||
} = anomaly;
|
||||
|
||||
const eventSourceFields = await getServiceGroupFieldsForAnomaly({
|
||||
config$,
|
||||
scopedClusterClient: services.scopedClusterClient,
|
||||
savedObjectsClient: services.savedObjectsClient,
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
timestamp,
|
||||
bucketSpan,
|
||||
});
|
||||
|
||||
const severityLevel = getSeverity(score);
|
||||
const reasonMessage = formatAnomalyReason({
|
||||
measured: score,
|
||||
|
@ -270,6 +303,7 @@ export function registerAnomalyRuleType({
|
|||
[ALERT_EVALUATION_VALUE]: score,
|
||||
[ALERT_EVALUATION_THRESHOLD]: threshold,
|
||||
[ALERT_REASON]: reasonMessage,
|
||||
...eventSourceFields,
|
||||
},
|
||||
})
|
||||
.scheduleActions(ruleTypeConfig.defaultActionGroupId, {
|
||||
|
@ -281,7 +315,7 @@ export function registerAnomalyRuleType({
|
|||
reason: reasonMessage,
|
||||
viewInAppUrl,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
|
|
|
@ -37,6 +37,10 @@ import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices';
|
|||
import { apmActionVariables } from '../../action_variables';
|
||||
import { alertingEsClient } from '../../alerting_es_client';
|
||||
import { RegisterRuleDependencies } from '../../register_apm_rule_types';
|
||||
import {
|
||||
getServiceGroupFieldsAgg,
|
||||
getServiceGroupFields,
|
||||
} from '../get_service_group_fields';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
windowSize: schema.number(),
|
||||
|
@ -107,7 +111,9 @@ export function registerErrorCountRuleType({
|
|||
},
|
||||
},
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
|
||||
...termQuery(SERVICE_NAME, ruleParams.serviceName),
|
||||
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...environmentQuery(ruleParams.environment),
|
||||
],
|
||||
},
|
||||
|
@ -122,8 +128,10 @@ export function registerErrorCountRuleType({
|
|||
missing: ENVIRONMENT_NOT_DEFINED.value,
|
||||
},
|
||||
],
|
||||
size: 10000,
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
aggs: getServiceGroupFieldsAgg(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -137,13 +145,19 @@ export function registerErrorCountRuleType({
|
|||
const errorCountResults =
|
||||
response.aggregations?.error_counts.buckets.map((bucket) => {
|
||||
const [serviceName, environment] = bucket.key;
|
||||
return { serviceName, environment, errorCount: bucket.doc_count };
|
||||
return {
|
||||
serviceName,
|
||||
environment,
|
||||
errorCount: bucket.doc_count,
|
||||
sourceFields: getServiceGroupFields(bucket),
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
errorCountResults
|
||||
.filter((result) => result.errorCount >= ruleParams.threshold)
|
||||
.forEach((result) => {
|
||||
const { serviceName, environment, errorCount } = result;
|
||||
const { serviceName, environment, errorCount, sourceFields } =
|
||||
result;
|
||||
const alertReason = formatErrorCountReason({
|
||||
serviceName,
|
||||
threshold: ruleParams.threshold,
|
||||
|
@ -176,6 +190,7 @@ export function registerErrorCountRuleType({
|
|||
[ALERT_EVALUATION_VALUE]: errorCount,
|
||||
[ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold,
|
||||
[ALERT_REASON]: alertReason,
|
||||
...sourceFields,
|
||||
},
|
||||
})
|
||||
.scheduleActions(ruleTypeConfig.defaultActionGroupId, {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 {
|
||||
getServiceGroupFields,
|
||||
getServiceGroupFieldsAgg,
|
||||
flattenSourceDoc,
|
||||
} from './get_service_group_fields';
|
||||
|
||||
const mockSourceObj = {
|
||||
service: {
|
||||
name: 'testbeans',
|
||||
environment: 'testing',
|
||||
language: {
|
||||
name: 'typescript',
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
team: 'test',
|
||||
},
|
||||
agent: {
|
||||
name: 'nodejs',
|
||||
},
|
||||
};
|
||||
|
||||
const mockBucket = {
|
||||
source_fields: {
|
||||
hits: {
|
||||
hits: [{ _source: mockSourceObj }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getSourceFields', () => {
|
||||
it('should return a flattened record of fields and values for a given bucket', () => {
|
||||
const result = getServiceGroupFields(mockBucket);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agent.name": "nodejs",
|
||||
"labels.team": "test",
|
||||
"service.environment": "testing",
|
||||
"service.language.name": "typescript",
|
||||
"service.name": "testbeans",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSourceFieldsAgg', () => {
|
||||
it('should create a agg for specific source fields', () => {
|
||||
const agg = getServiceGroupFieldsAgg();
|
||||
expect(agg).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"source_fields": Object {
|
||||
"top_hits": Object {
|
||||
"_source": Object {
|
||||
"includes": Array [
|
||||
"agent.name",
|
||||
"service.name",
|
||||
"service.environment",
|
||||
"service.language.name",
|
||||
"labels",
|
||||
],
|
||||
},
|
||||
"size": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should accept options for top_hits options', () => {
|
||||
const agg = getServiceGroupFieldsAgg({
|
||||
sort: [{ 'transaction.duration.us': { order: 'desc' } }],
|
||||
});
|
||||
expect(agg).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"source_fields": Object {
|
||||
"top_hits": Object {
|
||||
"_source": Object {
|
||||
"includes": Array [
|
||||
"agent.name",
|
||||
"service.name",
|
||||
"service.environment",
|
||||
"service.language.name",
|
||||
"labels",
|
||||
],
|
||||
},
|
||||
"size": 1,
|
||||
"sort": Array [
|
||||
Object {
|
||||
"transaction.duration.us": Object {
|
||||
"order": "desc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenSourceDoc', () => {
|
||||
it('should flatten a given nested object with dot delim paths as keys', () => {
|
||||
const result = flattenSourceDoc(mockSourceObj);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"agent.name": "nodejs",
|
||||
"labels.team": "test",
|
||||
"service.environment": "testing",
|
||||
"service.language.name": "typescript",
|
||||
"service.name": "testbeans",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { AggregationsTopHitsAggregation } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SERVICE_GROUP_SUPPORTED_FIELDS } from '../../../../common/service_groups';
|
||||
|
||||
export interface SourceDoc {
|
||||
[key: string]: string | SourceDoc;
|
||||
}
|
||||
|
||||
export function getServiceGroupFieldsAgg(
|
||||
topHitsOpts: AggregationsTopHitsAggregation = {}
|
||||
) {
|
||||
return {
|
||||
source_fields: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: {
|
||||
includes: SERVICE_GROUP_SUPPORTED_FIELDS,
|
||||
},
|
||||
...topHitsOpts,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface AggResultBucket {
|
||||
source_fields: {
|
||||
hits: {
|
||||
hits: Array<{ _source: any }>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getServiceGroupFields(bucket?: AggResultBucket) {
|
||||
if (!bucket) {
|
||||
return {};
|
||||
}
|
||||
const sourceDoc: SourceDoc =
|
||||
bucket?.source_fields?.hits.hits[0]?._source ?? {};
|
||||
return flattenSourceDoc(sourceDoc);
|
||||
}
|
||||
|
||||
export function flattenSourceDoc(
|
||||
val: SourceDoc | string,
|
||||
path: string[] = []
|
||||
): Record<string, string> {
|
||||
if (typeof val !== 'object') {
|
||||
return { [path.join('.')]: val };
|
||||
}
|
||||
return Object.keys(val).reduce((acc, key) => {
|
||||
const fieldMap = flattenSourceDoc(val[key], [...path, key]);
|
||||
return { ...acc, ...fieldMap };
|
||||
}, {});
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { AggregationType } from '../../../common/rules/apm_rule_types';
|
||||
import { getDurationFieldForTransactions } from '../../lib/helpers/transactions';
|
||||
import { AggregationType } from '../../../../../common/rules/apm_rule_types';
|
||||
import { getDurationFieldForTransactions } from '../../../../lib/helpers/transactions';
|
||||
|
||||
type TransactionDurationField = ReturnType<
|
||||
typeof getDurationFieldForTransactions
|
||||
|
@ -45,3 +45,13 @@ export function averageOrPercentileAgg({
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMultiTermsSortOrder(aggregationType: AggregationType): {
|
||||
order: { [path: string]: 'desc' };
|
||||
} {
|
||||
if (aggregationType === AggregationType.Avg) {
|
||||
return { order: { avgLatency: 'desc' } };
|
||||
}
|
||||
const percentsKey = aggregationType === AggregationType.P95 ? 95 : 99;
|
||||
return { order: { [`pctLatency.${percentsKey}`]: 'desc' } };
|
||||
}
|
|
@ -25,7 +25,7 @@ import {
|
|||
ENVIRONMENT_NOT_DEFINED,
|
||||
getEnvironmentLabel,
|
||||
} from '../../../../../common/environment_filter_values';
|
||||
import { averageOrPercentileAgg } from '../../average_or_percentile_agg';
|
||||
import { averageOrPercentileAgg } from './average_or_percentile_agg';
|
||||
import { APMConfig } from '../../../..';
|
||||
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ describe('registerTransactionDurationRuleType', () => {
|
|||
},
|
||||
},
|
||||
aggregations: {
|
||||
environments: {
|
||||
series: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'ENVIRONMENT_NOT_DEFINED',
|
||||
key: ['opbeans-java', 'ENVIRONMENT_NOT_DEFINED', 'request'],
|
||||
avgLatency: {
|
||||
value: 5500000,
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@kbn/rule-data-utils';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { asDuration } from '@kbn/observability-plugin/common/utils/formatters';
|
||||
import { termQuery } from '@kbn/observability-plugin/server';
|
||||
import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { getAlertUrlTransaction } from '../../../../../common/utils/formatters';
|
||||
|
@ -46,7 +47,14 @@ import { getApmIndices } from '../../../settings/apm_indices/get_apm_indices';
|
|||
import { apmActionVariables } from '../../action_variables';
|
||||
import { alertingEsClient } from '../../alerting_es_client';
|
||||
import { RegisterRuleDependencies } from '../../register_apm_rule_types';
|
||||
import { averageOrPercentileAgg } from '../../average_or_percentile_agg';
|
||||
import {
|
||||
averageOrPercentileAgg,
|
||||
getMultiTermsSortOrder,
|
||||
} from './average_or_percentile_agg';
|
||||
import {
|
||||
getServiceGroupFields,
|
||||
getServiceGroupFieldsAgg,
|
||||
} from '../get_service_group_fields';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
serviceName: schema.string(),
|
||||
|
@ -140,26 +148,37 @@ export function registerTransactionDurationRuleType({
|
|||
...getDocumentTypeFilterForTransactions(
|
||||
searchAggregatedTransactions
|
||||
),
|
||||
{ term: { [SERVICE_NAME]: ruleParams.serviceName } },
|
||||
{
|
||||
term: {
|
||||
[TRANSACTION_TYPE]: ruleParams.transactionType,
|
||||
},
|
||||
},
|
||||
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...environmentQuery(ruleParams.environment),
|
||||
] as QueryDslQueryContainer[],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED.value,
|
||||
series: {
|
||||
multi_terms: {
|
||||
terms: [
|
||||
{ field: SERVICE_NAME },
|
||||
{
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
missing: ENVIRONMENT_NOT_DEFINED.value,
|
||||
},
|
||||
{ field: TRANSACTION_TYPE },
|
||||
],
|
||||
size: 1000,
|
||||
...getMultiTermsSortOrder(ruleParams.aggregationType),
|
||||
},
|
||||
aggs: {
|
||||
...averageOrPercentileAgg({
|
||||
aggregationType: ruleParams.aggregationType,
|
||||
transactionDurationField: field,
|
||||
}),
|
||||
...getServiceGroupFieldsAgg(),
|
||||
},
|
||||
aggs: averageOrPercentileAgg({
|
||||
aggregationType: ruleParams.aggregationType,
|
||||
transactionDurationField: field,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -177,32 +196,40 @@ export function registerTransactionDurationRuleType({
|
|||
// Converts threshold to microseconds because this is the unit used on transactionDuration
|
||||
const thresholdMicroseconds = ruleParams.threshold * 1000;
|
||||
|
||||
const triggeredEnvironmentDurations =
|
||||
response.aggregations.environments.buckets
|
||||
.map((bucket) => {
|
||||
const { key: environment } = bucket;
|
||||
const transactionDuration =
|
||||
'avgLatency' in bucket // only true if ruleParams.aggregationType === 'avg'
|
||||
? bucket.avgLatency.value
|
||||
: bucket.pctLatency.values[0].value;
|
||||
return { transactionDuration, environment };
|
||||
})
|
||||
.filter(
|
||||
({ transactionDuration }) =>
|
||||
transactionDuration !== null &&
|
||||
transactionDuration > thresholdMicroseconds
|
||||
) as Array<{ transactionDuration: number; environment: string }>;
|
||||
const triggeredBuckets = [];
|
||||
for (const bucket of response.aggregations.series.buckets) {
|
||||
const [serviceName, environment, transactionType] = bucket.key;
|
||||
const transactionDuration =
|
||||
'avgLatency' in bucket // only true if ruleParams.aggregationType === 'avg'
|
||||
? bucket.avgLatency.value
|
||||
: bucket.pctLatency.values[0].value;
|
||||
if (
|
||||
transactionDuration !== null &&
|
||||
transactionDuration > thresholdMicroseconds
|
||||
) {
|
||||
triggeredBuckets.push({
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
transactionDuration,
|
||||
sourceFields: getServiceGroupFields(bucket),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const {
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
transactionDuration,
|
||||
} of triggeredEnvironmentDurations) {
|
||||
sourceFields,
|
||||
} of triggeredBuckets) {
|
||||
const durationFormatter = getDurationFormatter(transactionDuration);
|
||||
const transactionDurationFormatted =
|
||||
durationFormatter(transactionDuration).formatted;
|
||||
const reasonMessage = formatTransactionDurationReason({
|
||||
measured: transactionDuration,
|
||||
serviceName: ruleParams.serviceName,
|
||||
serviceName,
|
||||
threshold: thresholdMicroseconds,
|
||||
asDuration,
|
||||
aggregationType: String(ruleParams.aggregationType),
|
||||
|
@ -211,9 +238,9 @@ export function registerTransactionDurationRuleType({
|
|||
});
|
||||
|
||||
const relativeViewInAppUrl = getAlertUrlTransaction(
|
||||
ruleParams.serviceName,
|
||||
serviceName,
|
||||
getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT],
|
||||
ruleParams.transactionType
|
||||
transactionType
|
||||
);
|
||||
|
||||
const viewInAppUrl = basePath.publicBaseUrl
|
||||
|
@ -228,18 +255,19 @@ export function registerTransactionDurationRuleType({
|
|||
environment
|
||||
)}`,
|
||||
fields: {
|
||||
[SERVICE_NAME]: ruleParams.serviceName,
|
||||
[SERVICE_NAME]: serviceName,
|
||||
...getEnvironmentEsField(environment),
|
||||
[TRANSACTION_TYPE]: ruleParams.transactionType,
|
||||
[TRANSACTION_TYPE]: transactionType,
|
||||
[PROCESSOR_EVENT]: ProcessorEvent.transaction,
|
||||
[ALERT_EVALUATION_VALUE]: transactionDuration,
|
||||
[ALERT_EVALUATION_THRESHOLD]: thresholdMicroseconds,
|
||||
[ALERT_REASON]: reasonMessage,
|
||||
...sourceFields,
|
||||
},
|
||||
})
|
||||
.scheduleActions(ruleTypeConfig.defaultActionGroupId, {
|
||||
transactionType: ruleParams.transactionType,
|
||||
serviceName: ruleParams.serviceName,
|
||||
transactionType,
|
||||
serviceName,
|
||||
environment: getEnvironmentLabel(environment),
|
||||
threshold: thresholdMicroseconds,
|
||||
triggerValue: transactionDurationFormatted,
|
||||
|
|
|
@ -44,6 +44,10 @@ import { alertingEsClient } from '../../alerting_es_client';
|
|||
import { RegisterRuleDependencies } from '../../register_apm_rule_types';
|
||||
import { SearchAggregatedTransactionSetting } from '../../../../../common/aggregated_transactions';
|
||||
import { getDocumentTypeFilterForTransactions } from '../../../../lib/helpers/transactions';
|
||||
import {
|
||||
getServiceGroupFields,
|
||||
getServiceGroupFieldsAgg,
|
||||
} from '../get_service_group_fields';
|
||||
|
||||
const paramsSchema = schema.object({
|
||||
windowSize: schema.number(),
|
||||
|
@ -136,8 +140,12 @@ export function registerTransactionErrorRateRuleType({
|
|||
],
|
||||
},
|
||||
},
|
||||
...termQuery(SERVICE_NAME, ruleParams.serviceName),
|
||||
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType),
|
||||
...termQuery(SERVICE_NAME, ruleParams.serviceName, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, {
|
||||
queryEmptyString: false,
|
||||
}),
|
||||
...environmentQuery(ruleParams.environment),
|
||||
],
|
||||
},
|
||||
|
@ -153,13 +161,15 @@ export function registerTransactionErrorRateRuleType({
|
|||
},
|
||||
{ field: TRANSACTION_TYPE },
|
||||
],
|
||||
size: 10000,
|
||||
size: 1000,
|
||||
order: { _count: 'desc' as const },
|
||||
},
|
||||
aggs: {
|
||||
outcomes: {
|
||||
terms: {
|
||||
field: EVENT_OUTCOME,
|
||||
},
|
||||
aggs: getServiceGroupFieldsAgg(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -180,10 +190,10 @@ export function registerTransactionErrorRateRuleType({
|
|||
for (const bucket of response.aggregations.series.buckets) {
|
||||
const [serviceName, environment, transactionType] = bucket.key;
|
||||
|
||||
const failed =
|
||||
bucket.outcomes.buckets.find(
|
||||
(outcomeBucket) => outcomeBucket.key === EventOutcome.failure
|
||||
)?.doc_count ?? 0;
|
||||
const failedOutcomeBucket = bucket.outcomes.buckets.find(
|
||||
(outcomeBucket) => outcomeBucket.key === EventOutcome.failure
|
||||
);
|
||||
const failed = failedOutcomeBucket?.doc_count ?? 0;
|
||||
const succesful =
|
||||
bucket.outcomes.buckets.find(
|
||||
(outcomeBucket) => outcomeBucket.key === EventOutcome.success
|
||||
|
@ -196,13 +206,19 @@ export function registerTransactionErrorRateRuleType({
|
|||
environment,
|
||||
transactionType,
|
||||
errorRate,
|
||||
sourceFields: getServiceGroupFields(failedOutcomeBucket),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results.forEach((result) => {
|
||||
const { serviceName, environment, transactionType, errorRate } =
|
||||
result;
|
||||
const {
|
||||
serviceName,
|
||||
environment,
|
||||
transactionType,
|
||||
errorRate,
|
||||
sourceFields,
|
||||
} = result;
|
||||
const reasonMessage = formatTransactionErrorRateReason({
|
||||
threshold: ruleParams.threshold,
|
||||
measured: errorRate,
|
||||
|
@ -241,6 +257,7 @@ export function registerTransactionErrorRateRuleType({
|
|||
[ALERT_EVALUATION_VALUE]: errorRate,
|
||||
[ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold,
|
||||
[ALERT_REASON]: reasonMessage,
|
||||
...sourceFields,
|
||||
},
|
||||
})
|
||||
.scheduleActions(ruleTypeConfig.defaultActionGroupId, {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import Boom from '@hapi/boom';
|
||||
import { apmServiceGroupMaxNumberOfServices } from '@kbn/observability-plugin/common';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { kueryRt, rangeRt } from '../default_api_types';
|
||||
|
@ -14,7 +15,10 @@ import { getServiceGroup } from './get_service_group';
|
|||
import { saveServiceGroup } from './save_service_group';
|
||||
import { deleteServiceGroup } from './delete_service_group';
|
||||
import { lookupServices } from './lookup_services';
|
||||
import { SavedServiceGroup } from '../../../common/service_groups';
|
||||
import {
|
||||
validateServiceGroupKuery,
|
||||
SavedServiceGroup,
|
||||
} from '../../../common/service_groups';
|
||||
import { getServicesCounts } from './get_services_counts';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
|
||||
|
@ -120,6 +124,12 @@ const serviceGroupSaveRoute = createApmServerRoute({
|
|||
const {
|
||||
savedObjects: { client: savedObjectsClient },
|
||||
} = await context.core;
|
||||
const { isValidFields, isValidSyntax, message } = validateServiceGroupKuery(
|
||||
params.body.kuery
|
||||
);
|
||||
if (!(isValidFields && isValidSyntax)) {
|
||||
throw Boom.badRequest(message);
|
||||
}
|
||||
|
||||
await saveServiceGroup({
|
||||
savedObjectsClient,
|
||||
|
|
|
@ -157,14 +157,14 @@ export async function getServiceAnomalies({
|
|||
|
||||
export async function getMLJobs(
|
||||
anomalyDetectors: ReturnType<MlPluginSetup['anomalyDetectorsProvider']>,
|
||||
environment: string
|
||||
environment?: string
|
||||
) {
|
||||
const jobs = await getMlJobsWithAPMGroup(anomalyDetectors);
|
||||
|
||||
// to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings`
|
||||
// and checking that it is compatable.
|
||||
const mlJobs = jobs.filter((job) => job.version >= 2);
|
||||
if (environment !== ENVIRONMENT_ALL.value) {
|
||||
if (environment && environment !== ENVIRONMENT_ALL.value) {
|
||||
const matchingMLJob = mlJobs.find((job) => job.environment === environment);
|
||||
if (!matchingMLJob) {
|
||||
return [];
|
||||
|
@ -176,7 +176,7 @@ export async function getMLJobs(
|
|||
|
||||
export async function getMLJobIds(
|
||||
anomalyDetectors: ReturnType<MlPluginSetup['anomalyDetectorsProvider']>,
|
||||
environment: string
|
||||
environment?: string
|
||||
) {
|
||||
const mlJobs = await getMLJobs(anomalyDetectors, environment);
|
||||
return mlJobs.map((job) => job.jobId);
|
||||
|
|
|
@ -13,11 +13,16 @@ function isUndefinedOrNull(value: any): value is undefined | null {
|
|||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
interface TermQueryOpts {
|
||||
queryEmptyString: boolean;
|
||||
}
|
||||
|
||||
export function termQuery<T extends string>(
|
||||
field: T,
|
||||
value: string | boolean | number | undefined | null
|
||||
value: string | boolean | number | undefined | null,
|
||||
opts: TermQueryOpts = { queryEmptyString: true }
|
||||
): QueryDslQueryContainer[] {
|
||||
if (isUndefinedOrNull(value)) {
|
||||
if (isUndefinedOrNull(value) || (!opts.queryEmptyString && value === '')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { ApmApiError } from '../../common/apm_api_supertest';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { expectToReject } from '../../common/utils/expect_to_reject';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
async function callApi({
|
||||
serviceGroupId,
|
||||
groupName,
|
||||
kuery,
|
||||
description,
|
||||
color,
|
||||
}: {
|
||||
serviceGroupId?: string;
|
||||
groupName: string;
|
||||
kuery: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}) {
|
||||
const response = await apmApiClient.writeUser({
|
||||
endpoint: 'POST /internal/apm/service-group',
|
||||
params: {
|
||||
query: {
|
||||
serviceGroupId,
|
||||
},
|
||||
body: {
|
||||
groupName,
|
||||
kuery,
|
||||
description,
|
||||
color,
|
||||
},
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
type SavedObjectsFindResults = Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
}>;
|
||||
|
||||
async function deleteServiceGroups() {
|
||||
const response = await supertest
|
||||
.get('/api/saved_objects/_find?type=apm-service-group')
|
||||
.set('kbn-xsrf', 'true');
|
||||
const savedObjects: SavedObjectsFindResults = response.body.saved_objects;
|
||||
const bulkDeleteBody = savedObjects.map(({ id, type }) => ({ id, type }));
|
||||
return supertest
|
||||
.post(`/api/saved_objects/_bulk_delete?force=true`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(bulkDeleteBody);
|
||||
}
|
||||
|
||||
registry.when('Service group create', { config: 'basic', archives: [] }, () => {
|
||||
afterEach(deleteServiceGroups);
|
||||
|
||||
it('creates a new service group', async () => {
|
||||
const response = await callApi({
|
||||
groupName: 'synthbeans',
|
||||
kuery: 'service.name: synth*',
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(Object.keys(response.body).length).to.be(0);
|
||||
});
|
||||
|
||||
it('handles invalid fields with error response', async () => {
|
||||
const err = await expectToReject<ApmApiError>(() =>
|
||||
callApi({
|
||||
groupName: 'synthbeans',
|
||||
kuery: 'service.name: synth* or transaction.type: request',
|
||||
})
|
||||
);
|
||||
|
||||
expect(err.res.status).to.be(400);
|
||||
expect(err.res.body.message).to.contain('transaction.type');
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue