mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Stabilize agent configuration API (#57767)
This commit is contained in:
parent
7e087633d2
commit
13eacb51f0
23 changed files with 935 additions and 411 deletions
260
docs/apm/api.asciidoc
Normal file
260
docs/apm/api.asciidoc
Normal file
|
@ -0,0 +1,260 @@
|
|||
[role="xpack"]
|
||||
[[apm-api]]
|
||||
== API
|
||||
|
||||
Some APM app features are provided via a REST API:
|
||||
|
||||
* <<agent-config-api>>
|
||||
|
||||
TIP: Kibana provides additional <<api,REST APIs>>,
|
||||
and general information on <<using-api,how to use APIs>>.
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
||||
|
||||
[[agent-config-api]]
|
||||
=== Agent Configuration API
|
||||
|
||||
The Agent configuration API allows you to fine-tune your APM agent configuration,
|
||||
without needing to redeploy your application.
|
||||
|
||||
The following Agent configuration APIs are available:
|
||||
|
||||
* <<apm-update-config>> to create or update an Agent configuration
|
||||
* <<apm-delete-config>> to delete an Agent configuration.
|
||||
* <<apm-list-config>> to list all Agent configurations.
|
||||
* <<apm-search-config>> to search for an Agent configuration.
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
||||
|
||||
[[apm-update-config]]
|
||||
==== Create or update configuration
|
||||
|
||||
[[apm-update-config-req]]
|
||||
===== Request
|
||||
|
||||
`PUT /api/apm/settings/agent-configuration`
|
||||
|
||||
[[apm-update-config-req-body]]
|
||||
===== Request body
|
||||
|
||||
`service`::
|
||||
(required, object) Service identifying the configuration to create or update.
|
||||
|
||||
`name` :::
|
||||
(required, string) Name of service
|
||||
|
||||
`environment` :::
|
||||
(optional, string) Environment of service
|
||||
|
||||
`settings`::
|
||||
(required) Key/value object with settings and their corresponding value.
|
||||
|
||||
`agent_name`::
|
||||
(optional) The agent name is used by the UI to determine which settings to display.
|
||||
|
||||
|
||||
[[apm-update-config-example]]
|
||||
===== Example
|
||||
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
PUT /api/apm/settings/agent-configuration
|
||||
{
|
||||
"service" : {
|
||||
"name" : "frontend",
|
||||
"environment" : "production"
|
||||
},
|
||||
"settings" : {
|
||||
"transaction_sample_rate" : 0.4,
|
||||
"capture_body" : "off",
|
||||
"transaction_max_spans" : 500
|
||||
},
|
||||
"agent_name": "nodejs"
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
||||
|
||||
|
||||
[[apm-delete-config]]
|
||||
==== Delete configuration
|
||||
|
||||
[[apm-delete-config-req]]
|
||||
===== Request
|
||||
|
||||
`DELETE /api/apm/settings/agent-configuration`
|
||||
|
||||
[[apm-delete-config-req-body]]
|
||||
===== Request body
|
||||
`service`::
|
||||
(required, object) Service identifying the configuration to delete
|
||||
|
||||
`name` :::
|
||||
(required, string) Name of service
|
||||
|
||||
`environment` :::
|
||||
(optional, string) Environment of service
|
||||
|
||||
|
||||
[[apm-delete-config-example]]
|
||||
===== Example
|
||||
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
DELETE /api/apm/settings/agent-configuration
|
||||
{
|
||||
"service" : {
|
||||
"name" : "frontend",
|
||||
"environment": "production"
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
||||
|
||||
|
||||
[[apm-list-config]]
|
||||
==== List configuration
|
||||
|
||||
|
||||
[[apm-list-config-req]]
|
||||
===== Request
|
||||
|
||||
`GET /api/apm/settings/agent-configuration`
|
||||
|
||||
[[apm-list-config-body]]
|
||||
===== Response body
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
[
|
||||
{
|
||||
"agent_name": "go",
|
||||
"service": {
|
||||
"name": "opbeans-go",
|
||||
"environment": "production"
|
||||
},
|
||||
"settings": {
|
||||
"transaction_sample_rate": 1,
|
||||
"capture_body": "off",
|
||||
"transaction_max_spans": 200
|
||||
},
|
||||
"@timestamp": 1581934104843,
|
||||
"applied_by_agent": false,
|
||||
"etag": "1e58c178efeebae15c25c539da740d21dee422fc"
|
||||
},
|
||||
{
|
||||
"agent_name": "go",
|
||||
"service": {
|
||||
"name": "opbeans-go"
|
||||
},
|
||||
"settings": {
|
||||
"transaction_sample_rate": 1,
|
||||
"capture_body": "off",
|
||||
"transaction_max_spans": 300
|
||||
},
|
||||
"@timestamp": 1581934111727,
|
||||
"applied_by_agent": false,
|
||||
"etag": "3eed916d3db434d9fb7f039daa681c7a04539a64"
|
||||
},
|
||||
{
|
||||
"agent_name": "nodejs",
|
||||
"service": {
|
||||
"name": "frontend"
|
||||
},
|
||||
"settings": {
|
||||
"transaction_sample_rate": 1,
|
||||
},
|
||||
"@timestamp": 1582031336265,
|
||||
"applied_by_agent": false,
|
||||
"etag": "5080ed25785b7b19f32713681e79f46996801a5b"
|
||||
}
|
||||
]
|
||||
--------------------------------------------------
|
||||
|
||||
[[apm-list-config-example]]
|
||||
===== Example
|
||||
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
GET /api/apm/settings/agent-configuration
|
||||
--------------------------------------------------
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
||||
|
||||
|
||||
[[apm-search-config]]
|
||||
==== Search configuration
|
||||
|
||||
[[apm-search-config-req]]
|
||||
===== Request
|
||||
|
||||
`POST /api/apm/settings/agent-configuration/search`
|
||||
|
||||
[[apm-search-config-req-body]]
|
||||
===== Request body
|
||||
|
||||
`service`::
|
||||
(required, object) Service identifying the configuration.
|
||||
|
||||
`name` :::
|
||||
(required, string) Name of service
|
||||
|
||||
`environment` :::
|
||||
(optional, string) Environment of service
|
||||
|
||||
`etag`::
|
||||
(required) etag is sent by the agent to indicate the etag of the last successfully applied configuration. If the etag matches an existing configuration its `applied_by_agent` property will be set to `true`. Every time a configuration is edited `applied_by_agent` is reset to `false`.
|
||||
|
||||
[[apm-search-config-body]]
|
||||
===== Response body
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"_index": ".apm-agent-configuration",
|
||||
"_id": "CIaqXXABmQCdPphWj8EJ",
|
||||
"_score": 2,
|
||||
"_source": {
|
||||
"agent_name": "nodejs",
|
||||
"service": {
|
||||
"name": "frontend"
|
||||
},
|
||||
"settings": {
|
||||
"transaction_sample_rate": 1,
|
||||
},
|
||||
"@timestamp": 1582031336265,
|
||||
"applied_by_agent": false,
|
||||
"etag": "5080ed25785b7b19f32713681e79f46996801a5b"
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
[[apm-search-config-example]]
|
||||
===== Example
|
||||
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
POST /api/apm/settings/agent-configuration/search
|
||||
{
|
||||
"etag" : "1e58c178efeebae15c25c539da740d21dee422fc",
|
||||
"service" : {
|
||||
"name" : "frontend",
|
||||
"environment": "production"
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
|
||||
////
|
||||
*******************************************************
|
||||
////
|
|
@ -24,3 +24,5 @@ include::getting-started.asciidoc[]
|
|||
include::bottlenecks.asciidoc[]
|
||||
|
||||
include::using-the-apm-ui.asciidoc[]
|
||||
|
||||
include::api.asciidoc[]
|
||||
|
|
|
@ -51,12 +51,18 @@ async function deleteConfig(
|
|||
) {
|
||||
try {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
pathname: '/api/apm/settings/agent-configuration',
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
path: { configurationId: selectedConfig.id }
|
||||
body: {
|
||||
service: {
|
||||
name: selectedConfig.service.name,
|
||||
environment: selectedConfig.service.environment
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle',
|
||||
|
|
|
@ -135,8 +135,8 @@ export function AddEditFlyout({
|
|||
sampleRate,
|
||||
captureBody,
|
||||
transactionMaxSpans,
|
||||
configurationId: selectedConfig ? selectedConfig.id : undefined,
|
||||
agentName,
|
||||
isExistingConfig: Boolean(selectedConfig),
|
||||
toasts,
|
||||
trackApmEvent
|
||||
});
|
||||
|
|
|
@ -27,8 +27,8 @@ export async function saveConfig({
|
|||
sampleRate,
|
||||
captureBody,
|
||||
transactionMaxSpans,
|
||||
configurationId,
|
||||
agentName,
|
||||
isExistingConfig,
|
||||
toasts,
|
||||
trackApmEvent
|
||||
}: {
|
||||
|
@ -38,8 +38,8 @@ export async function saveConfig({
|
|||
sampleRate: string;
|
||||
captureBody: string;
|
||||
transactionMaxSpans: string;
|
||||
configurationId?: string;
|
||||
agentName?: string;
|
||||
isExistingConfig: boolean;
|
||||
toasts: NotificationsStart['toasts'];
|
||||
trackApmEvent: UiTracker;
|
||||
}) {
|
||||
|
@ -64,24 +64,14 @@ export async function saveConfig({
|
|||
settings
|
||||
};
|
||||
|
||||
if (configurationId) {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
method: 'PUT',
|
||||
params: {
|
||||
path: { configurationId },
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration/new',
|
||||
method: 'POST',
|
||||
params: {
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
}
|
||||
await callApmApi({
|
||||
pathname: '/api/apm/settings/agent-configuration',
|
||||
method: 'PUT',
|
||||
params: {
|
||||
query: { overwrite: isExistingConfig },
|
||||
body: configuration
|
||||
}
|
||||
});
|
||||
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
|
|
|
@ -71,6 +71,28 @@ node scripts/jest.js plugins/apm --watch
|
|||
node scripts/jest.js plugins/apm --updateSnapshot
|
||||
```
|
||||
|
||||
### Functional tests
|
||||
|
||||
**Start server**
|
||||
`node scripts/functional_tests_server --config x-pack/test/functional/config.js`
|
||||
|
||||
**Run tests**
|
||||
`node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs'`
|
||||
|
||||
APM tests are located in `x-pack/test/functional/apps/apm`.
|
||||
For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
|
||||
|
||||
### API integration tests
|
||||
|
||||
**Start server**
|
||||
`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js`
|
||||
|
||||
**Run tests**
|
||||
`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'`
|
||||
|
||||
APM tests are located in `x-pack/test/api_integration/apis/apm`.
|
||||
For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
|
||||
|
||||
### Linting
|
||||
|
||||
_Note: Run the following commands from `kibana/`._
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { agentConfigurationIntakeRt } from './index';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('agentConfigurationIntakeRt', () => {
|
||||
it('is valid when required parameters are given', () => {
|
||||
const config = {
|
||||
service: {},
|
||||
settings: {}
|
||||
};
|
||||
|
||||
expect(isConfigValid(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('is valid when required and optional parameters are given', () => {
|
||||
const config = {
|
||||
service: { name: 'my-service', environment: 'my-environment' },
|
||||
settings: {
|
||||
transaction_sample_rate: 0.5,
|
||||
capture_body: 'foo',
|
||||
transaction_max_spans: 10
|
||||
}
|
||||
};
|
||||
|
||||
expect(isConfigValid(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('is invalid when required parameters are not given', () => {
|
||||
const config = {};
|
||||
expect(isConfigValid(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function isConfigValid(config: any) {
|
||||
return isRight(agentConfigurationIntakeRt.decode(config));
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { transactionSampleRateRt } from '../transaction_sample_rate_rt';
|
||||
import { transactionMaxSpansRt } from '../transaction_max_spans_rt';
|
||||
|
||||
export const serviceRt = t.partial({
|
||||
name: t.string,
|
||||
environment: t.string
|
||||
});
|
||||
|
||||
export const agentConfigurationIntakeRt = t.intersection([
|
||||
t.partial({ agent_name: t.string }),
|
||||
t.type({
|
||||
service: serviceRt,
|
||||
settings: t.partial({
|
||||
transaction_sample_rate: transactionSampleRateRt,
|
||||
capture_body: t.string,
|
||||
transaction_max_spans: transactionMaxSpansRt
|
||||
})
|
||||
})
|
||||
]);
|
|
@ -7,9 +7,10 @@
|
|||
/* eslint-disable no-console */
|
||||
import {
|
||||
IndexDocumentParams,
|
||||
IndicesCreateParams,
|
||||
IndicesDeleteParams,
|
||||
SearchParams
|
||||
SearchParams,
|
||||
IndicesCreateParams,
|
||||
DeleteDocumentResponse
|
||||
} from 'elasticsearch';
|
||||
import { cloneDeep, isString, merge, uniqueId } from 'lodash';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
|
@ -188,7 +189,7 @@ export function getESClient(
|
|||
index: <Body>(params: APMIndexDocumentParams<Body>) => {
|
||||
return withTime(() => callMethod('index', params));
|
||||
},
|
||||
delete: (params: IndicesDeleteParams) => {
|
||||
delete: (params: IndicesDeleteParams): Promise<DeleteDocumentResponse> => {
|
||||
return withTime(() => callMethod('delete', params));
|
||||
},
|
||||
indicesCreate: (params: IndicesCreateParams) => {
|
||||
|
|
|
@ -58,8 +58,8 @@ export async function getServiceNodeMetadata({
|
|||
const response = await client.search(query);
|
||||
|
||||
return {
|
||||
host: response.aggregations?.host.buckets[0].key || NOT_AVAILABLE_LABEL,
|
||||
host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL,
|
||||
containerId:
|
||||
response.aggregations?.containerId.buckets[0].key || NOT_AVAILABLE_LABEL
|
||||
response.aggregations?.containerId.buckets[0]?.key || NOT_AVAILABLE_LABEL
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,90 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`agent configuration queries fetches all environments 1`] = `
|
||||
exports[`agent configuration queries findExactConfiguration find configuration by service.environment 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.environment": "bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries findExactConfiguration find configuration by service.name 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries findExactConfiguration find configuration by service.name and service.environment 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.environment": "bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries getAllEnvironments fetches all environments 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
|
@ -41,124 +125,36 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries fetches configurations 1`] = `
|
||||
Object {
|
||||
"index": "myIndex",
|
||||
"size": 200,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries fetches filtered configurations with an environment 1`] = `
|
||||
exports[`agent configuration queries getExistingEnvironmentsForService fetches unavailable environments 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"environments": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ALL_OPTION_VALUE",
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
"filter": Array [
|
||||
Object {
|
||||
"constant_score": Object {
|
||||
"boost": 2,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.name": Object {
|
||||
"value": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"constant_score": Object {
|
||||
"boost": 1,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.environment": Object {
|
||||
"value": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries fetches filtered configurations without an environment 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
Object {
|
||||
"constant_score": Object {
|
||||
"boost": 2,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.name": Object {
|
||||
"value": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries fetches service names 1`] = `
|
||||
exports[`agent configuration queries getServiceNames fetches service names 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
|
@ -194,30 +190,112 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries fetches unavailable environments 1`] = `
|
||||
exports[`agent configuration queries listConfigurations fetches configurations 1`] = `
|
||||
Object {
|
||||
"index": "myIndex",
|
||||
"size": 200,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries searchConfigurations fetches filtered configurations with an environment 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"environments": Object {
|
||||
"terms": Object {
|
||||
"field": "service.environment",
|
||||
"missing": "ALL_OPTION_VALUE",
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
"constant_score": Object {
|
||||
"boost": 2,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"constant_score": Object {
|
||||
"boost": 1,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.environment": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`agent configuration queries searchConfigurations fetches filtered configurations without an environment 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 2,
|
||||
"should": Array [
|
||||
Object {
|
||||
"constant_score": Object {
|
||||
"boost": 2,
|
||||
"filter": Object {
|
||||
"term": Object {
|
||||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.environment",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
|
|
|
@ -4,18 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface AgentConfiguration {
|
||||
import t from 'io-ts';
|
||||
import { agentConfigurationIntakeRt } from '../../../../common/runtime_types/agent_configuration_intake_rt';
|
||||
|
||||
export type AgentConfigurationIntake = t.TypeOf<
|
||||
typeof agentConfigurationIntakeRt
|
||||
>;
|
||||
export type AgentConfiguration = {
|
||||
'@timestamp': number;
|
||||
applied_by_agent?: boolean;
|
||||
etag?: string;
|
||||
agent_name?: string;
|
||||
service: {
|
||||
name?: string;
|
||||
environment?: string;
|
||||
};
|
||||
settings: {
|
||||
transaction_sample_rate?: number;
|
||||
capture_body?: string;
|
||||
transaction_max_spans?: number;
|
||||
};
|
||||
}
|
||||
} & AgentConfigurationIntake;
|
||||
|
|
|
@ -6,19 +6,19 @@
|
|||
|
||||
import hash from 'object-hash';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { AgentConfiguration } from './configuration_types';
|
||||
import {
|
||||
AgentConfiguration,
|
||||
AgentConfigurationIntake
|
||||
} from './configuration_types';
|
||||
import { APMIndexDocumentParams } from '../../helpers/es_client';
|
||||
|
||||
export async function createOrUpdateConfiguration({
|
||||
configurationId,
|
||||
configuration,
|
||||
configurationIntake,
|
||||
setup
|
||||
}: {
|
||||
configurationId?: string;
|
||||
configuration: Omit<
|
||||
AgentConfiguration,
|
||||
'@timestamp' | 'applied_by_agent' | 'etag'
|
||||
>;
|
||||
configurationIntake: AgentConfigurationIntake;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { internalClient, indices } = setup;
|
||||
|
@ -27,15 +27,15 @@ export async function createOrUpdateConfiguration({
|
|||
refresh: true,
|
||||
index: indices.apmAgentConfigurationIndex,
|
||||
body: {
|
||||
agent_name: configuration.agent_name,
|
||||
agent_name: configurationIntake.agent_name,
|
||||
service: {
|
||||
name: configuration.service.name,
|
||||
environment: configuration.service.environment
|
||||
name: configurationIntake.service.name,
|
||||
environment: configurationIntake.service.environment
|
||||
},
|
||||
settings: configuration.settings,
|
||||
settings: configurationIntake.settings,
|
||||
'@timestamp': Date.now(),
|
||||
applied_by_agent: false,
|
||||
etag: hash(configuration)
|
||||
etag: hash(configurationIntake)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { AgentConfiguration } from './configuration_types';
|
||||
import { ESSearchHit } from '../../../../typings/elasticsearch';
|
||||
|
||||
export async function findExactConfiguration({
|
||||
service,
|
||||
setup
|
||||
}: {
|
||||
service: AgentConfiguration['service'];
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { internalClient, indices } = setup;
|
||||
|
||||
const serviceNameFilter = service.name
|
||||
? { term: { [SERVICE_NAME]: service.name } }
|
||||
: { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } };
|
||||
|
||||
const environmentFilter = service.environment
|
||||
? { term: { [SERVICE_ENVIRONMENT]: service.environment } }
|
||||
: { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } };
|
||||
|
||||
const params = {
|
||||
index: indices.apmAgentConfigurationIndex,
|
||||
body: {
|
||||
query: {
|
||||
bool: { filter: [serviceNameFilter, environmentFilter] }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await internalClient.search<AgentConfiguration, typeof params>(
|
||||
params
|
||||
);
|
||||
|
||||
return resp.hits.hits[0] as ESSearchHit<AgentConfiguration> | undefined;
|
||||
}
|
|
@ -48,8 +48,6 @@ export async function getAgentNameByService({
|
|||
};
|
||||
|
||||
const { aggregations } = await client.search(params);
|
||||
const agentName = aggregations?.agent_names.buckets[0].key as
|
||||
| string
|
||||
| undefined;
|
||||
return { agentName };
|
||||
const agentName = aggregations?.agent_names.buckets[0]?.key;
|
||||
return agentName as string | undefined;
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@ import { getAllEnvironments } from './get_environments/get_all_environments';
|
|||
import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service';
|
||||
import { getServiceNames } from './get_service_names';
|
||||
import { listConfigurations } from './list_configurations';
|
||||
import { searchConfigurations } from './search';
|
||||
import { searchConfigurations } from './search_configurations';
|
||||
import {
|
||||
SearchParamsMock,
|
||||
inspectSearchParams
|
||||
} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers';
|
||||
import { findExactConfiguration } from './find_exact_configuration';
|
||||
|
||||
describe('agent configuration queries', () => {
|
||||
let mock: SearchParamsMock;
|
||||
|
@ -21,68 +22,117 @@ describe('agent configuration queries', () => {
|
|||
mock.teardown();
|
||||
});
|
||||
|
||||
it('fetches all environments', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getAllEnvironments({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('getAllEnvironments', () => {
|
||||
it('fetches all environments', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getAllEnvironments({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches unavailable environments', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getExistingEnvironmentsForService({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('getExistingEnvironmentsForService', () => {
|
||||
it('fetches unavailable environments', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getExistingEnvironmentsForService({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches service names', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getServiceNames({
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('getServiceNames', () => {
|
||||
it('fetches service names', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
getServiceNames({
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches configurations', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
listConfigurations({
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('listConfigurations', () => {
|
||||
it('fetches configurations', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
listConfigurations({
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches filtered configurations without an environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
searchConfigurations({
|
||||
serviceName: 'foo',
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('searchConfigurations', () => {
|
||||
it('fetches filtered configurations without an environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
searchConfigurations({
|
||||
service: {
|
||||
name: 'foo'
|
||||
},
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('fetches filtered configurations with an environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
searchConfigurations({
|
||||
service: {
|
||||
name: 'foo',
|
||||
environment: 'bar'
|
||||
},
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches filtered configurations with an environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
searchConfigurations({
|
||||
serviceName: 'foo',
|
||||
environment: 'bar',
|
||||
setup
|
||||
})
|
||||
);
|
||||
describe('findExactConfiguration', () => {
|
||||
it('find configuration by service.name', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
findExactConfiguration({
|
||||
service: { name: 'foo' },
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('find configuration by service.environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
findExactConfiguration({
|
||||
service: { environment: 'bar' },
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('find configuration by service.name and service.environment', async () => {
|
||||
mock = await inspectSearchParams(setup =>
|
||||
findExactConfiguration({
|
||||
service: { name: 'foo', environment: 'bar' },
|
||||
setup
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,29 +12,39 @@ import { Setup } from '../../helpers/setup_request';
|
|||
import { AgentConfiguration } from './configuration_types';
|
||||
|
||||
export async function searchConfigurations({
|
||||
serviceName,
|
||||
environment,
|
||||
service,
|
||||
setup
|
||||
}: {
|
||||
serviceName: string;
|
||||
environment?: string;
|
||||
service: AgentConfiguration['service'];
|
||||
setup: Setup;
|
||||
}) {
|
||||
const { internalClient, indices } = setup;
|
||||
const environmentFilter = environment
|
||||
|
||||
// In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring).
|
||||
// Additionally a boost has been added to service.name to ensure it scores higher.
|
||||
// If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins
|
||||
const serviceNameFilter = service.name
|
||||
? [
|
||||
{
|
||||
constant_score: {
|
||||
filter: { term: { [SERVICE_ENVIRONMENT]: { value: environment } } },
|
||||
filter: { term: { [SERVICE_NAME]: service.name } },
|
||||
boost: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
const environmentFilter = service.environment
|
||||
? [
|
||||
{
|
||||
constant_score: {
|
||||
filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } },
|
||||
boost: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
// In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring)
|
||||
// Additionally a boost has been added to service.name to ensure it scores higher
|
||||
// if there is tie between a config with a matching service.name and a config with a matching environment
|
||||
const params = {
|
||||
index: indices.apmAgentConfigurationIndex,
|
||||
body: {
|
||||
|
@ -42,12 +52,7 @@ export async function searchConfigurations({
|
|||
bool: {
|
||||
minimum_should_match: 2,
|
||||
should: [
|
||||
{
|
||||
constant_score: {
|
||||
filter: { term: { [SERVICE_NAME]: { value: serviceName } } },
|
||||
boost: 2
|
||||
}
|
||||
},
|
||||
...serviceNameFilter,
|
||||
...environmentFilter,
|
||||
{ bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } },
|
||||
{ bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }
|
|
@ -23,11 +23,10 @@ import {
|
|||
import {
|
||||
agentConfigurationRoute,
|
||||
agentConfigurationSearchRoute,
|
||||
createAgentConfigurationRoute,
|
||||
deleteAgentConfigurationRoute,
|
||||
listAgentConfigurationEnvironmentsRoute,
|
||||
listAgentConfigurationServicesRoute,
|
||||
updateAgentConfigurationRoute,
|
||||
createOrUpdateAgentConfigurationRoute,
|
||||
agentConfigurationAgentNameRoute
|
||||
} from './settings/agent_configuration';
|
||||
import {
|
||||
|
@ -83,11 +82,10 @@ const createApmApi = () => {
|
|||
.add(agentConfigurationAgentNameRoute)
|
||||
.add(agentConfigurationRoute)
|
||||
.add(agentConfigurationSearchRoute)
|
||||
.add(createAgentConfigurationRoute)
|
||||
.add(deleteAgentConfigurationRoute)
|
||||
.add(listAgentConfigurationEnvironmentsRoute)
|
||||
.add(listAgentConfigurationServicesRoute)
|
||||
.add(updateAgentConfigurationRoute)
|
||||
.add(createOrUpdateAgentConfigurationRoute)
|
||||
|
||||
// APM indices
|
||||
.add(apmIndexSettingsRoute)
|
||||
|
|
|
@ -9,15 +9,19 @@ import Boom from 'boom';
|
|||
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';
|
||||
import { searchConfigurations } from '../../lib/settings/agent_configuration/search';
|
||||
import { searchConfigurations } from '../../lib/settings/agent_configuration/search_configurations';
|
||||
import { findExactConfiguration } from '../../lib/settings/agent_configuration/find_exact_configuration';
|
||||
import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations';
|
||||
import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments';
|
||||
import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration';
|
||||
import { createRoute } from '../create_route';
|
||||
import { transactionSampleRateRt } from '../../../common/runtime_types/transaction_sample_rate_rt';
|
||||
import { transactionMaxSpansRt } from '../../../common/runtime_types/transaction_max_spans_rt';
|
||||
import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service';
|
||||
import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent';
|
||||
import {
|
||||
serviceRt,
|
||||
agentConfigurationIntakeRt
|
||||
} from '../../../common/runtime_types/agent_configuration_intake_rt';
|
||||
import { jsonRt } from '../../../common/runtime_types/json_rt';
|
||||
|
||||
// get list of configurations
|
||||
export const agentConfigurationRoute = createRoute(core => ({
|
||||
|
@ -31,20 +35,34 @@ export const agentConfigurationRoute = createRoute(core => ({
|
|||
// delete configuration
|
||||
export const deleteAgentConfigurationRoute = createRoute(() => ({
|
||||
method: 'DELETE',
|
||||
path: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
path: '/api/apm/settings/agent-configuration',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write']
|
||||
},
|
||||
params: {
|
||||
path: t.type({
|
||||
configurationId: t.string
|
||||
body: t.type({
|
||||
service: serviceRt
|
||||
})
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { configurationId } = context.params.path;
|
||||
const { service } = context.params.body;
|
||||
|
||||
const config = await findExactConfiguration({ service, setup });
|
||||
if (!config) {
|
||||
context.logger.info(
|
||||
`Config was not found for ${service.name}/${service.environment}`
|
||||
);
|
||||
|
||||
throw Boom.notFound();
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
`Deleting config ${service.name}/${service.environment} (${config._id})`
|
||||
);
|
||||
|
||||
return await deleteConfiguration({
|
||||
configurationId,
|
||||
configurationId: config._id,
|
||||
setup
|
||||
});
|
||||
}
|
||||
|
@ -62,23 +80,6 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({
|
|||
}
|
||||
}));
|
||||
|
||||
const agentPayloadRt = t.intersection([
|
||||
t.partial({ agent_name: t.string }),
|
||||
t.type({
|
||||
service: t.intersection([
|
||||
t.partial({ name: t.string }),
|
||||
t.partial({ environment: t.string })
|
||||
])
|
||||
}),
|
||||
t.type({
|
||||
settings: t.intersection([
|
||||
t.partial({ transaction_sample_rate: transactionSampleRateRt }),
|
||||
t.partial({ capture_body: t.string }),
|
||||
t.partial({ transaction_max_spans: transactionMaxSpansRt })
|
||||
])
|
||||
})
|
||||
]);
|
||||
|
||||
// get environments for service
|
||||
export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({
|
||||
path: '/api/apm/settings/agent-configuration/environments',
|
||||
|
@ -102,55 +103,47 @@ export const agentConfigurationAgentNameRoute = createRoute(() => ({
|
|||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.query;
|
||||
const agentName = await getAgentNameByService({ serviceName, setup });
|
||||
return agentName;
|
||||
return { agentName };
|
||||
}
|
||||
}));
|
||||
|
||||
export const createAgentConfigurationRoute = createRoute(() => ({
|
||||
method: 'POST',
|
||||
path: '/api/apm/settings/agent-configuration/new',
|
||||
params: {
|
||||
body: agentPayloadRt
|
||||
},
|
||||
export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({
|
||||
method: 'PUT',
|
||||
path: '/api/apm/settings/agent-configuration',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write']
|
||||
},
|
||||
params: {
|
||||
query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }),
|
||||
body: agentConfigurationIntakeRt
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const configuration = context.params.body;
|
||||
const { body, query } = context.params;
|
||||
|
||||
// TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764)
|
||||
context.logger.info(
|
||||
`Hitting: /api/apm/settings/agent-configuration/new with ${configuration.service.name}/${configuration.service.environment}`
|
||||
);
|
||||
const res = await createOrUpdateConfiguration({
|
||||
configuration,
|
||||
// if the config already exists, it is fetched and updated
|
||||
// this is to avoid creating two configs with identical service params
|
||||
const config = await findExactConfiguration({
|
||||
service: body.service,
|
||||
setup
|
||||
});
|
||||
context.logger.info(`Created agent configuration`);
|
||||
|
||||
return res;
|
||||
}
|
||||
}));
|
||||
// if the config exists ?overwrite=true is required
|
||||
if (config && !query.overwrite) {
|
||||
throw Boom.badRequest(
|
||||
`A configuration already exists for "${body.service.name}/${body.service.environment}. Use ?overwrite=true to overwrite the existing configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
`${config ? 'Updating' : 'Creating'} config ${body.service.name}/${
|
||||
body.service.environment
|
||||
}`
|
||||
);
|
||||
|
||||
export const updateAgentConfigurationRoute = createRoute(() => ({
|
||||
method: 'PUT',
|
||||
path: '/api/apm/settings/agent-configuration/{configurationId}',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write']
|
||||
},
|
||||
params: {
|
||||
path: t.type({
|
||||
configurationId: t.string
|
||||
}),
|
||||
body: agentPayloadRt
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { configurationId } = context.params.path;
|
||||
return await createOrUpdateConfiguration({
|
||||
configurationId,
|
||||
configuration: context.params.body,
|
||||
configurationId: config?._id,
|
||||
configurationIntake: body,
|
||||
setup
|
||||
});
|
||||
}
|
||||
|
@ -162,41 +155,33 @@ export const agentConfigurationSearchRoute = createRoute(core => ({
|
|||
path: '/api/apm/settings/agent-configuration/search',
|
||||
params: {
|
||||
body: t.type({
|
||||
service: t.intersection([
|
||||
t.type({ name: t.string }),
|
||||
t.partial({ environment: t.string })
|
||||
]),
|
||||
service: serviceRt,
|
||||
etag: t.string
|
||||
})
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const { body } = context.params;
|
||||
|
||||
// TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764)
|
||||
context.logger.info(
|
||||
`Hitting: /api/apm/settings/agent-configuration/search for ${body.service.name}/${body.service.environment}`
|
||||
);
|
||||
const { service, etag } = context.params.body;
|
||||
|
||||
const setup = await setupRequest(context, request);
|
||||
const config = await searchConfigurations({
|
||||
serviceName: body.service.name,
|
||||
environment: body.service.environment,
|
||||
service,
|
||||
setup
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
context.logger.info(
|
||||
`Config was not found for ${body.service.name}/${body.service.environment}`
|
||||
`Config was not found for ${service.name}/${service.environment}`
|
||||
);
|
||||
throw new Boom('Not found', { statusCode: 404 });
|
||||
throw Boom.notFound();
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
`Config was found for ${body.service.name}/${body.service.environment}`
|
||||
`Config was found for ${service.name}/${service.environment}`
|
||||
);
|
||||
|
||||
// update `applied_by_agent` field if etags match
|
||||
if (body.etag === config._source.etag && !config._source.applied_by_agent) {
|
||||
// this happens in the background and doesn't block the response
|
||||
if (etag === config._source.etag && !config._source.applied_by_agent) {
|
||||
markAppliedByAgent({ id: config._id, body: config._source, setup });
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { AgentConfigurationIntake } from '../../../../plugins/apm/server/lib/settings/agent_configuration/configuration_types';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function agentConfigurationTests({ getService }: FtrProviderContext) {
|
||||
|
@ -18,108 +19,122 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
|
|||
.set('kbn-xsrf', 'foo');
|
||||
}
|
||||
|
||||
let createdConfigIds: any[] = [];
|
||||
async function createConfiguration(configuration: any) {
|
||||
async function createConfiguration(config: AgentConfigurationIntake) {
|
||||
log.debug('creating configuration', config.service);
|
||||
const res = await supertest
|
||||
.post(`/api/apm/settings/agent-configuration/new`)
|
||||
.send(configuration)
|
||||
.put(`/api/apm/settings/agent-configuration`)
|
||||
.send(config)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
createdConfigIds.push(res.body._id);
|
||||
throwOnError(res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function deleteCreatedConfigurations() {
|
||||
const promises = Promise.all(createdConfigIds.map(deleteConfiguration));
|
||||
return promises;
|
||||
async function updateConfiguration(config: AgentConfigurationIntake) {
|
||||
log.debug('updating configuration', config.service);
|
||||
const res = await supertest
|
||||
.put(`/api/apm/settings/agent-configuration?overwrite=true`)
|
||||
.send(config)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
throwOnError(res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function deleteConfiguration(configurationId: string) {
|
||||
return supertest
|
||||
.delete(`/api/apm/settings/agent-configuration/${configurationId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.then((response: any) => {
|
||||
createdConfigIds = createdConfigIds.filter(id => id === configurationId);
|
||||
return response;
|
||||
});
|
||||
async function deleteConfiguration({ service }: AgentConfigurationIntake) {
|
||||
log.debug('deleting configuration', service);
|
||||
const res = await supertest
|
||||
.delete(`/api/apm/settings/agent-configuration`)
|
||||
.send({ service })
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
throwOnError(res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function throwOnError(res: any) {
|
||||
const { statusCode, req, body } = res;
|
||||
if (statusCode !== 200) {
|
||||
throw new Error(`
|
||||
Endpoint: ${req.method} ${req.path}
|
||||
Service: ${JSON.stringify(res.request._data.service)}
|
||||
Status code: ${statusCode}
|
||||
Response: ${body.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
describe('agent configuration', () => {
|
||||
describe('when creating one configuration', () => {
|
||||
let createdConfigId: string;
|
||||
const newConfig = {
|
||||
service: {},
|
||||
settings: { transaction_sample_rate: 0.55 },
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
const searchParams = {
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39',
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
log.debug('creating agent configuration');
|
||||
|
||||
// all / all
|
||||
const { body } = await createConfiguration({
|
||||
service: {},
|
||||
settings: { transaction_sample_rate: 0.1 },
|
||||
});
|
||||
|
||||
createdConfigId = body._id;
|
||||
await createConfiguration(newConfig);
|
||||
});
|
||||
|
||||
it('returns the created configuration', async () => {
|
||||
const { statusCode, body } = await searchConfigurations(parameters);
|
||||
|
||||
it('can find the created config', async () => {
|
||||
const { statusCode, body } = await searchConfigurations(searchParams);
|
||||
expect(statusCode).to.equal(200);
|
||||
expect(body._id).to.equal(createdConfigId);
|
||||
expect(body._source.service).to.eql({});
|
||||
expect(body._source.settings).to.eql({ transaction_sample_rate: 0.55 });
|
||||
});
|
||||
|
||||
it('succesfully deletes the configuration', async () => {
|
||||
await deleteConfiguration(createdConfigId);
|
||||
it('can update the created config', async () => {
|
||||
await updateConfiguration({ service: {}, settings: { transaction_sample_rate: 0.85 } });
|
||||
|
||||
const { statusCode } = await searchConfigurations(parameters);
|
||||
const { statusCode, body } = await searchConfigurations(searchParams);
|
||||
expect(statusCode).to.equal(200);
|
||||
expect(body._source.service).to.eql({});
|
||||
expect(body._source.settings).to.eql({ transaction_sample_rate: 0.85 });
|
||||
});
|
||||
|
||||
it('can delete the created config', async () => {
|
||||
await deleteConfiguration(newConfig);
|
||||
const { statusCode } = await searchConfigurations(searchParams);
|
||||
expect(statusCode).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating four configurations', () => {
|
||||
before(async () => {
|
||||
log.debug('creating agent configuration');
|
||||
|
||||
// all / all
|
||||
await createConfiguration({
|
||||
describe('when creating multiple configurations', () => {
|
||||
const configs = [
|
||||
{
|
||||
service: {},
|
||||
settings: { transaction_sample_rate: 0.1 },
|
||||
});
|
||||
|
||||
// my_service / all
|
||||
await createConfiguration({
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service' },
|
||||
settings: { transaction_sample_rate: 0.2 },
|
||||
});
|
||||
|
||||
// all / production
|
||||
await createConfiguration({
|
||||
service: { environment: 'production' },
|
||||
settings: { transaction_sample_rate: 0.3 },
|
||||
});
|
||||
|
||||
// all / production
|
||||
await createConfiguration({
|
||||
service: { environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.4 },
|
||||
});
|
||||
|
||||
// my_service / production
|
||||
await createConfiguration({
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.3 },
|
||||
},
|
||||
{
|
||||
service: { environment: 'production' },
|
||||
settings: { transaction_sample_rate: 0.4 },
|
||||
},
|
||||
{
|
||||
service: { environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.5 },
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
await Promise.all(configs.map(config => createConfiguration(config)));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.debug('deleting agent configurations');
|
||||
await deleteCreatedConfigurations();
|
||||
await Promise.all(configs.map(config => deleteConfiguration(config)));
|
||||
});
|
||||
|
||||
const agentsRequests = [
|
||||
|
@ -127,20 +142,24 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
|
|||
service: { name: 'non_existing_service', environment: 'non_existing_env' },
|
||||
expectedSettings: { transaction_sample_rate: 0.1 },
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'non_existing_env' },
|
||||
expectedSettings: { transaction_sample_rate: 0.2 },
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'production' },
|
||||
expectedSettings: { transaction_sample_rate: 0.2 },
|
||||
},
|
||||
{
|
||||
service: { name: 'non_existing_service', environment: 'production' },
|
||||
service: { name: 'my_service', environment: 'development' },
|
||||
expectedSettings: { transaction_sample_rate: 0.3 },
|
||||
},
|
||||
{
|
||||
service: { name: 'non_existing_service', environment: 'development' },
|
||||
service: { name: 'non_existing_service', environment: 'production' },
|
||||
expectedSettings: { transaction_sample_rate: 0.4 },
|
||||
},
|
||||
{
|
||||
service: { name: 'my_service', environment: 'development' },
|
||||
service: { name: 'non_existing_service', environment: 'development' },
|
||||
expectedSettings: { transaction_sample_rate: 0.5 },
|
||||
},
|
||||
];
|
||||
|
@ -159,18 +178,18 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
|
|||
});
|
||||
|
||||
describe('when an agent retrieves a configuration', () => {
|
||||
const config = {
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.9 },
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
log.debug('creating agent configuration');
|
||||
|
||||
await createConfiguration({
|
||||
service: { name: 'myservice', environment: 'development' },
|
||||
settings: { transaction_sample_rate: 0.9 },
|
||||
});
|
||||
await createConfiguration(config);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
log.debug('deleting agent configurations');
|
||||
await deleteCreatedConfigurations();
|
||||
await deleteConfiguration(config);
|
||||
});
|
||||
|
||||
it(`should have 'applied_by_agent=false' on first request`, async () => {
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
|
@ -14,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const security = getService('security');
|
||||
const spaces = getService('spaces');
|
||||
const log = getService('log');
|
||||
const es = getService('legacyEs');
|
||||
const log = getService('log');
|
||||
|
||||
const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString());
|
||||
const end = encodeURIComponent(new Date().toISOString());
|
||||
|
@ -33,7 +31,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
interface Endpoint {
|
||||
req: {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'delete';
|
||||
method?: 'get' | 'post' | 'delete' | 'put';
|
||||
body?: any;
|
||||
};
|
||||
expectForbidden: (result: any) => void;
|
||||
|
@ -148,7 +146,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
index: '.apm-agent-configuration',
|
||||
});
|
||||
|
||||
console.warn(JSON.stringify(res, null, 2));
|
||||
log.error(JSON.stringify(res, null, 2));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -196,7 +194,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
|
||||
const { statusCode, req } = response;
|
||||
if (statusCode !== 200) {
|
||||
log.debug(`Endpoint: ${req.method} ${req.path}
|
||||
throw new Error(`Endpoint: ${req.method} ${req.path}
|
||||
Status code: ${statusCode}
|
||||
Response: ${response.body.message}`);
|
||||
}
|
||||
|
@ -216,9 +214,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
spaceId?: string;
|
||||
}) {
|
||||
for (const endpoint of endpoints) {
|
||||
console.log(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`);
|
||||
log.info(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`);
|
||||
const result = await executeAsUser(endpoint.req, username, password, spaceId);
|
||||
console.log(`Responded: ${endpoint.req.url}`);
|
||||
log.info(`Responded: ${endpoint.req.url}`);
|
||||
|
||||
try {
|
||||
if (expectation === 'forbidden') {
|
||||
|
@ -244,26 +242,28 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
}
|
||||
|
||||
describe('apm feature controls', () => {
|
||||
let res: any;
|
||||
const config = {
|
||||
service: { name: 'test-service' },
|
||||
settings: { transaction_sample_rate: 0.5 },
|
||||
};
|
||||
before(async () => {
|
||||
console.log(`Creating agent configuration`);
|
||||
res = await executeAsAdmin({
|
||||
method: 'post',
|
||||
url: '/api/apm/settings/agent-configuration/new',
|
||||
body: {
|
||||
service: { name: 'test-service' },
|
||||
settings: { transaction_sample_rate: 0.5 },
|
||||
},
|
||||
log.info(`Creating agent configuration`);
|
||||
await executeAsAdmin({
|
||||
method: 'put',
|
||||
url: '/api/apm/settings/agent-configuration',
|
||||
body: config,
|
||||
});
|
||||
console.log(`Agent configuration created`);
|
||||
log.info(`Agent configuration created`);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
console.log('deleting agent configuration');
|
||||
const configurationId = res.body._id;
|
||||
log.info('deleting agent configuration');
|
||||
await executeAsAdmin({
|
||||
method: 'delete',
|
||||
url: `/api/apm/settings/agent-configuration/${configurationId}`,
|
||||
url: `/api/apm/settings/agent-configuration`,
|
||||
body: {
|
||||
service: config.service,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('APM', () => {
|
||||
describe('APM specs', () => {
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
loadTestFile(require.resolve('./agent_configuration'));
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ loadTestFile }: FtrProviderContext) {
|
||||
describe('APM', function() {
|
||||
describe('APM specs', function() {
|
||||
this.tags('ciGroup6');
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue