mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[APM] Extract server type utils to package (#96349)
This commit is contained in:
parent
d9ef5c28d5
commit
bfc940c146
76 changed files with 2821 additions and 1684 deletions
|
@ -131,10 +131,12 @@
|
|||
"@kbn/crypto": "link:packages/kbn-crypto",
|
||||
"@kbn/i18n": "link:packages/kbn-i18n",
|
||||
"@kbn/interpreter": "link:packages/kbn-interpreter",
|
||||
"@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils",
|
||||
"@kbn/legacy-logging": "link:packages/kbn-legacy-logging",
|
||||
"@kbn/logging": "link:packages/kbn-logging",
|
||||
"@kbn/monaco": "link:packages/kbn-monaco",
|
||||
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
|
||||
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
|
||||
"@kbn/std": "link:packages/kbn-std",
|
||||
"@kbn/tinymath": "link:packages/kbn-tinymath",
|
||||
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
|
||||
|
|
13
packages/kbn-io-ts-utils/jest.config.js
Normal file
13
packages/kbn-io-ts-utils/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-io-ts-utils'],
|
||||
};
|
13
packages/kbn-io-ts-utils/package.json
Normal file
13
packages/kbn-io-ts-utils/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@kbn/io-ts-utils",
|
||||
"main": "./target/index.js",
|
||||
"types": "./target/index.d.ts",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
}
|
||||
}
|
11
packages/kbn-io-ts-utils/src/index.ts
Normal file
11
packages/kbn-io-ts-utils/src/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { jsonRt } from './json_rt';
|
||||
export { mergeRt } from './merge_rt';
|
||||
export { strictKeysRt } from './strict_keys_rt';
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
@ -12,9 +13,7 @@ import { Right } from 'fp-ts/lib/Either';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
function getValueOrThrow<TEither extends Either<any, any>>(
|
||||
either: TEither
|
||||
): Right<TEither> {
|
||||
function getValueOrThrow<TEither extends Either<any, any>>(either: TEither): Right<TEither> {
|
||||
const value = pipe(
|
||||
either,
|
||||
fold(() => {
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
|
@ -1,18 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { merge } from './';
|
||||
import { mergeRt } from '.';
|
||||
import { jsonRt } from '../json_rt';
|
||||
|
||||
describe('merge', () => {
|
||||
it('fails on one or more errors', () => {
|
||||
const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]);
|
||||
const type = mergeRt(t.type({ foo: t.string }), t.type({ bar: t.number }));
|
||||
|
||||
const result = type.decode({ foo: '' });
|
||||
|
||||
|
@ -20,10 +21,7 @@ describe('merge', () => {
|
|||
});
|
||||
|
||||
it('merges left to right', () => {
|
||||
const typeBoolean = merge([
|
||||
t.type({ foo: t.string }),
|
||||
t.type({ foo: jsonRt.pipe(t.boolean) }),
|
||||
]);
|
||||
const typeBoolean = mergeRt(t.type({ foo: t.string }), t.type({ foo: jsonRt.pipe(t.boolean) }));
|
||||
|
||||
const resultBoolean = typeBoolean.decode({
|
||||
foo: 'true',
|
||||
|
@ -34,10 +32,7 @@ describe('merge', () => {
|
|||
foo: true,
|
||||
});
|
||||
|
||||
const typeString = merge([
|
||||
t.type({ foo: jsonRt.pipe(t.boolean) }),
|
||||
t.type({ foo: t.string }),
|
||||
]);
|
||||
const typeString = mergeRt(t.type({ foo: jsonRt.pipe(t.boolean) }), t.type({ foo: t.string }));
|
||||
|
||||
const resultString = typeString.decode({
|
||||
foo: 'true',
|
||||
|
@ -50,10 +45,10 @@ describe('merge', () => {
|
|||
});
|
||||
|
||||
it('deeply merges values', () => {
|
||||
const type = merge([
|
||||
const type = mergeRt(
|
||||
t.type({ foo: t.type({ baz: t.string }) }),
|
||||
t.type({ foo: t.type({ bar: t.string }) }),
|
||||
]);
|
||||
t.type({ foo: t.type({ bar: t.string }) })
|
||||
);
|
||||
|
||||
const result = type.decode({
|
||||
foo: {
|
|
@ -1,31 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { merge as lodashMerge } from 'lodash';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { ValuesType } from 'utility-types';
|
||||
|
||||
export type MergeType<
|
||||
T extends t.Any[],
|
||||
U extends ValuesType<T> = ValuesType<T>
|
||||
> = t.Type<U['_A'], U['_O'], U['_I']> & {
|
||||
_tag: 'MergeType';
|
||||
types: T;
|
||||
};
|
||||
type PlainObject = Record<string | number | symbol, any>;
|
||||
|
||||
type DeepMerge<T, U> = U extends PlainObject
|
||||
? T extends PlainObject
|
||||
? Omit<T, keyof U> &
|
||||
{
|
||||
[key in keyof U]: T extends { [k in key]: any } ? DeepMerge<T[key], U[key]> : U[key];
|
||||
}
|
||||
: U
|
||||
: U;
|
||||
|
||||
// this is similar to t.intersection, but does a deep merge
|
||||
// instead of a shallow merge
|
||||
|
||||
export function merge<A extends t.Mixed, B extends t.Mixed>(
|
||||
types: [A, B]
|
||||
): MergeType<[A, B]>;
|
||||
export type MergeType<T1 extends t.Any, T2 extends t.Any> = t.Type<
|
||||
DeepMerge<t.TypeOf<T1>, t.TypeOf<T2>>,
|
||||
DeepMerge<t.OutputOf<T1>, t.OutputOf<T2>>
|
||||
> & {
|
||||
_tag: 'MergeType';
|
||||
types: [T1, T2];
|
||||
};
|
||||
|
||||
export function merge(types: t.Any[]) {
|
||||
export function mergeRt<T1 extends t.Any, T2 extends t.Any>(a: T1, b: T2): MergeType<T1, T2>;
|
||||
|
||||
export function mergeRt(...types: t.Any[]) {
|
||||
const mergeType = new t.Type(
|
||||
'merge',
|
||||
(u): u is unknown => {
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
@ -14,10 +15,7 @@ describe('strictKeysRt', () => {
|
|||
it('correctly and deeply validates object keys', () => {
|
||||
const checks: Array<{ type: t.Type<any>; passes: any[]; fails: any[] }> = [
|
||||
{
|
||||
type: t.intersection([
|
||||
t.type({ foo: t.string }),
|
||||
t.partial({ bar: t.string }),
|
||||
]),
|
||||
type: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.string })]),
|
||||
passes: [{ foo: '' }, { foo: '', bar: '' }],
|
||||
fails: [
|
||||
{ foo: '', unknownKey: '' },
|
||||
|
@ -26,15 +24,9 @@ describe('strictKeysRt', () => {
|
|||
},
|
||||
{
|
||||
type: t.type({
|
||||
path: t.union([
|
||||
t.type({ serviceName: t.string }),
|
||||
t.type({ transactionType: t.string }),
|
||||
]),
|
||||
path: t.union([t.type({ serviceName: t.string }), t.type({ transactionType: t.string })]),
|
||||
}),
|
||||
passes: [
|
||||
{ path: { serviceName: '' } },
|
||||
{ path: { transactionType: '' } },
|
||||
],
|
||||
passes: [{ path: { serviceName: '' } }, { path: { transactionType: '' } }],
|
||||
fails: [
|
||||
{ path: { serviceName: '', unknownKey: '' } },
|
||||
{ path: { transactionType: '', unknownKey: '' } },
|
||||
|
@ -62,9 +54,7 @@ describe('strictKeysRt', () => {
|
|||
|
||||
if (!isRight(result)) {
|
||||
throw new Error(
|
||||
`Expected ${JSON.stringify(
|
||||
value
|
||||
)} to be allowed, but validation failed with ${
|
||||
`Expected ${JSON.stringify(value)} to be allowed, but validation failed with ${
|
||||
result.left[0].message
|
||||
}`
|
||||
);
|
||||
|
@ -76,9 +66,7 @@ describe('strictKeysRt', () => {
|
|||
|
||||
if (!isLeft(result)) {
|
||||
throw new Error(
|
||||
`Expected ${JSON.stringify(
|
||||
value
|
||||
)} to be disallowed, but validation succeeded`
|
||||
`Expected ${JSON.stringify(value)} to be disallowed, but validation succeeded`
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,14 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { either, isRight } from 'fp-ts/lib/Either';
|
||||
import { mapValues, difference, isPlainObject, forEach } from 'lodash';
|
||||
import { MergeType, merge } from '../merge';
|
||||
import { MergeType, mergeRt } from '../merge_rt';
|
||||
|
||||
/*
|
||||
Type that tracks validated keys, and fails when the input value
|
||||
|
@ -21,7 +22,7 @@ type ParsableType =
|
|||
| t.PartialType<any>
|
||||
| t.ExactType<any>
|
||||
| t.InterfaceType<any>
|
||||
| MergeType<any>;
|
||||
| MergeType<any, any>;
|
||||
|
||||
function getKeysInObject<T extends Record<string, unknown>>(
|
||||
object: T,
|
||||
|
@ -32,17 +33,16 @@ function getKeysInObject<T extends Record<string, unknown>>(
|
|||
const ownPrefix = prefix ? `${prefix}.${key}` : key;
|
||||
keys.push(ownPrefix);
|
||||
if (isPlainObject(object[key])) {
|
||||
keys.push(
|
||||
...getKeysInObject(object[key] as Record<string, unknown>, ownPrefix)
|
||||
);
|
||||
keys.push(...getKeysInObject(object[key] as Record<string, unknown>, ownPrefix));
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
function addToContextWhenValidated<
|
||||
T extends t.InterfaceType<any> | t.PartialType<any>
|
||||
>(type: T, prefix: string): T {
|
||||
function addToContextWhenValidated<T extends t.InterfaceType<any> | t.PartialType<any>>(
|
||||
type: T,
|
||||
prefix: string
|
||||
): T {
|
||||
const validate = (input: unknown, context: t.Context) => {
|
||||
const result = type.validate(input, context);
|
||||
const keysType = context[0].type as StrictKeysType;
|
||||
|
@ -50,36 +50,19 @@ function addToContextWhenValidated<
|
|||
throw new Error('Expected a top-level StrictKeysType');
|
||||
}
|
||||
if (isRight(result)) {
|
||||
keysType.trackedKeys.push(
|
||||
...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)
|
||||
);
|
||||
keysType.trackedKeys.push(...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
if (type._tag === 'InterfaceType') {
|
||||
return new t.InterfaceType(
|
||||
type.name,
|
||||
type.is,
|
||||
validate,
|
||||
type.encode,
|
||||
type.props
|
||||
) as T;
|
||||
return new t.InterfaceType(type.name, type.is, validate, type.encode, type.props) as T;
|
||||
}
|
||||
|
||||
return new t.PartialType(
|
||||
type.name,
|
||||
type.is,
|
||||
validate,
|
||||
type.encode,
|
||||
type.props
|
||||
) as T;
|
||||
return new t.PartialType(type.name, type.is, validate, type.encode, type.props) as T;
|
||||
}
|
||||
|
||||
function trackKeysOfValidatedTypes(
|
||||
type: ParsableType | t.Any,
|
||||
prefix: string = ''
|
||||
): t.Any {
|
||||
function trackKeysOfValidatedTypes(type: ParsableType | t.Any, prefix: string = ''): t.Any {
|
||||
if (!('_tag' in type)) {
|
||||
return type;
|
||||
}
|
||||
|
@ -89,27 +72,24 @@ function trackKeysOfValidatedTypes(
|
|||
case 'IntersectionType': {
|
||||
const collectionType = type as t.IntersectionType<t.Any[]>;
|
||||
return t.intersection(
|
||||
collectionType.types.map((rt) =>
|
||||
trackKeysOfValidatedTypes(rt, prefix)
|
||||
) as [t.Any, t.Any]
|
||||
collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
|
||||
);
|
||||
}
|
||||
|
||||
case 'UnionType': {
|
||||
const collectionType = type as t.UnionType<t.Any[]>;
|
||||
return t.union(
|
||||
collectionType.types.map((rt) =>
|
||||
trackKeysOfValidatedTypes(rt, prefix)
|
||||
) as [t.Any, t.Any]
|
||||
collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
|
||||
);
|
||||
}
|
||||
|
||||
case 'MergeType': {
|
||||
const collectionType = type as MergeType<t.Any[]>;
|
||||
return merge(
|
||||
collectionType.types.map((rt) =>
|
||||
trackKeysOfValidatedTypes(rt, prefix)
|
||||
) as [t.Any, t.Any]
|
||||
const collectionType = type as MergeType<t.Any, t.Any>;
|
||||
return mergeRt(
|
||||
...(collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [
|
||||
t.Any,
|
||||
t.Any
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -142,9 +122,7 @@ function trackKeysOfValidatedTypes(
|
|||
case 'ExactType': {
|
||||
const exactType = type as t.ExactType<t.HasProps>;
|
||||
|
||||
return t.exact(
|
||||
trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps
|
||||
);
|
||||
return t.exact(trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps);
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -169,17 +147,11 @@ class StrictKeysType<
|
|||
(input, context) => {
|
||||
this.trackedKeys.length = 0;
|
||||
return either.chain(trackedType.validate(input, context), (i) => {
|
||||
const originalKeys = getKeysInObject(
|
||||
input as Record<string, unknown>
|
||||
);
|
||||
const originalKeys = getKeysInObject(input as Record<string, unknown>);
|
||||
const excessKeys = difference(originalKeys, this.trackedKeys);
|
||||
|
||||
if (excessKeys.length) {
|
||||
return t.failure(
|
||||
i,
|
||||
context,
|
||||
`Excess keys are not allowed: \n${excessKeys.join('\n')}`
|
||||
);
|
||||
return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`);
|
||||
}
|
||||
|
||||
return t.success(i);
|
19
packages/kbn-io-ts-utils/tsconfig.json
Normal file
19
packages/kbn-io-ts-utils/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "./target",
|
||||
"stripInternal": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "../../../../packages/kbn-io-ts-utils/src",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
]
|
||||
}
|
7
packages/kbn-server-route-repository/README.md
Normal file
7
packages/kbn-server-route-repository/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# @kbn/server-route-repository
|
||||
|
||||
Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition.
|
||||
|
||||
## Usage
|
||||
|
||||
TBD
|
13
packages/kbn-server-route-repository/jest.config.js
Normal file
13
packages/kbn-server-route-repository/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-server-route-repository'],
|
||||
};
|
16
packages/kbn-server-route-repository/package.json
Normal file
16
packages/kbn-server-route-repository/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@kbn/server-route-repository",
|
||||
"main": "./target/index.js",
|
||||
"types": "./target/index.d.ts",
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc",
|
||||
"kbn:bootstrap": "yarn build",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kbn/io-ts-utils": "link:../kbn-io-ts-utils"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import {
|
||||
ServerRouteCreateOptions,
|
||||
ServerRouteHandlerResources,
|
||||
RouteParamsRT,
|
||||
ServerRoute,
|
||||
} from './typings';
|
||||
|
||||
export function createServerRouteFactory<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions
|
||||
>(): <
|
||||
TEndpoint extends string,
|
||||
TReturnType,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: ServerRoute<
|
||||
TEndpoint,
|
||||
TRouteParamsRT,
|
||||
TRouteHandlerResources,
|
||||
TReturnType,
|
||||
TRouteCreateOptions
|
||||
>
|
||||
) => ServerRoute<
|
||||
TEndpoint,
|
||||
TRouteParamsRT,
|
||||
TRouteHandlerResources,
|
||||
TReturnType,
|
||||
TRouteCreateOptions
|
||||
> {
|
||||
return (route) => route;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import {
|
||||
ServerRouteHandlerResources,
|
||||
ServerRouteRepository,
|
||||
ServerRouteCreateOptions,
|
||||
} from './typings';
|
||||
|
||||
export function createServerRouteRepository<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources = never,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions = never
|
||||
>(): ServerRouteRepository<TRouteHandlerResources, TRouteCreateOptions, {}> {
|
||||
let routes: Record<string, any> = {};
|
||||
|
||||
return {
|
||||
add(route) {
|
||||
routes = {
|
||||
...routes,
|
||||
[route.endpoint]: route,
|
||||
};
|
||||
|
||||
return this as any;
|
||||
},
|
||||
merge(repository) {
|
||||
routes = {
|
||||
...routes,
|
||||
...Object.fromEntries(repository.getRoutes().map((route) => [route.endpoint, route])),
|
||||
};
|
||||
|
||||
return this as any;
|
||||
},
|
||||
getRoutes: () => Object.values(routes),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { jsonRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { decodeRequestParams } from './decode_request_params';
|
||||
|
||||
describe('decodeRequestParams', () => {
|
||||
it('decodes request params', () => {
|
||||
const decode = () => {
|
||||
return decodeRequestParams(
|
||||
{
|
||||
params: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
body: null,
|
||||
query: {
|
||||
start: '',
|
||||
},
|
||||
},
|
||||
t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.type({
|
||||
start: t.string,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
expect(decode).not.toThrow();
|
||||
|
||||
expect(decode()).toEqual({
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
query: {
|
||||
start: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on excess keys', () => {
|
||||
const decode = () => {
|
||||
return decodeRequestParams(
|
||||
{
|
||||
params: {
|
||||
serviceName: 'opbeans-java',
|
||||
extraKey: '',
|
||||
},
|
||||
body: null,
|
||||
query: {
|
||||
start: '',
|
||||
},
|
||||
},
|
||||
t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.type({
|
||||
start: t.string,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
expect(decode).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Excess keys are not allowed:
|
||||
path.extraKey"
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the decoded output', () => {
|
||||
const decode = () => {
|
||||
return decodeRequestParams(
|
||||
{
|
||||
params: {},
|
||||
query: {
|
||||
_inspect: 'true',
|
||||
},
|
||||
body: null,
|
||||
},
|
||||
t.type({
|
||||
query: t.type({
|
||||
_inspect: jsonRt.pipe(t.boolean),
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
expect(decode).not.toThrow();
|
||||
|
||||
expect(decode()).toEqual({
|
||||
query: {
|
||||
_inspect: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('strips empty params', () => {
|
||||
const decode = () => {
|
||||
return decodeRequestParams(
|
||||
{
|
||||
params: {},
|
||||
query: {},
|
||||
body: {},
|
||||
},
|
||||
t.type({
|
||||
body: t.any,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
expect(decode).not.toThrow();
|
||||
|
||||
expect(decode()).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { omitBy, isPlainObject, isEmpty } from 'lodash';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import Boom from '@hapi/boom';
|
||||
import { strictKeysRt } from '@kbn/io-ts-utils';
|
||||
import { RouteParamsRT } from './typings';
|
||||
|
||||
interface KibanaRequestParams {
|
||||
body: unknown;
|
||||
query: unknown;
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export function decodeRequestParams<T extends RouteParamsRT>(
|
||||
params: KibanaRequestParams,
|
||||
paramsRt: T
|
||||
): t.OutputOf<T> {
|
||||
const paramMap = omitBy(
|
||||
{
|
||||
path: params.params,
|
||||
body: params.body,
|
||||
query: params.query,
|
||||
},
|
||||
(val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val))
|
||||
);
|
||||
|
||||
// decode = validate
|
||||
const result = strictKeysRt(paramsRt).decode(paramMap);
|
||||
|
||||
if (isLeft(result)) {
|
||||
throw Boom.badRequest(PathReporter.report(result)[0]);
|
||||
}
|
||||
|
||||
return result.right;
|
||||
}
|
20
packages/kbn-server-route-repository/src/format_request.ts
Normal file
20
packages/kbn-server-route-repository/src/format_request.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { parseEndpoint } from './parse_endpoint';
|
||||
|
||||
export function formatRequest(endpoint: string, pathParams: Record<string, any> = {}) {
|
||||
const { method, pathname: rawPathname } = parseEndpoint(endpoint);
|
||||
|
||||
// replace template variables with path params
|
||||
const pathname = Object.keys(pathParams).reduce((acc, paramName) => {
|
||||
return acc.replace(`{${paramName}}`, pathParams[paramName]);
|
||||
}, rawPathname);
|
||||
|
||||
return { method, pathname };
|
||||
}
|
24
packages/kbn-server-route-repository/src/index.ts
Normal file
24
packages/kbn-server-route-repository/src/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { createServerRouteRepository } from './create_server_route_repository';
|
||||
export { createServerRouteFactory } from './create_server_route_factory';
|
||||
export { formatRequest } from './format_request';
|
||||
export { parseEndpoint } from './parse_endpoint';
|
||||
export { decodeRequestParams } from './decode_request_params';
|
||||
export { routeValidationObject } from './route_validation_object';
|
||||
export {
|
||||
RouteRepositoryClient,
|
||||
ReturnOf,
|
||||
EndpointOf,
|
||||
ClientRequestParamsOf,
|
||||
DecodedRequestParamsOf,
|
||||
ServerRouteRepository,
|
||||
ServerRoute,
|
||||
RouteParamsRT,
|
||||
} from './typings';
|
22
packages/kbn-server-route-repository/src/parse_endpoint.ts
Normal file
22
packages/kbn-server-route-repository/src/parse_endpoint.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
type Method = 'get' | 'post' | 'put' | 'delete';
|
||||
|
||||
export function parseEndpoint(endpoint: string) {
|
||||
const parts = endpoint.split(' ');
|
||||
|
||||
const method = parts[0].trim().toLowerCase() as Method;
|
||||
const pathname = parts[1].trim();
|
||||
|
||||
if (!['get', 'post', 'put', 'delete'].includes(method)) {
|
||||
throw new Error('Endpoint was not prefixed with a valid HTTP method');
|
||||
}
|
||||
|
||||
return { method, pathname };
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
const anyObject = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
export const routeValidationObject = {
|
||||
// `body` can be null, but `validate` expects non-nullable types
|
||||
// if any validation is defined. Not having validation currently
|
||||
// means we don't get the payload. See
|
||||
// https://github.com/elastic/kibana/issues/50179
|
||||
body: schema.nullable(anyObject),
|
||||
params: anyObject,
|
||||
query: anyObject,
|
||||
};
|
238
packages/kbn-server-route-repository/src/test_types.ts
Normal file
238
packages/kbn-server-route-repository/src/test_types.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { createServerRouteRepository } from './create_server_route_repository';
|
||||
import { decodeRequestParams } from './decode_request_params';
|
||||
import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings';
|
||||
|
||||
function assertType<TShape = never>(value: TShape) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Generic arguments for createServerRouteRepository should be set,
|
||||
// if not, registering routes should not be allowed
|
||||
createServerRouteRepository().add({
|
||||
// @ts-expect-error
|
||||
endpoint: 'any_endpoint',
|
||||
// @ts-expect-error
|
||||
handler: async ({ params }) => {},
|
||||
});
|
||||
|
||||
// If a params codec is not set, its type should not be available in
|
||||
// the request handler.
|
||||
createServerRouteRepository<{}, {}>().add({
|
||||
endpoint: 'endpoint_without_params',
|
||||
handler: async (resources) => {
|
||||
// @ts-expect-error Argument of type '{}' is not assignable to parameter of type '{ params: any; }'.
|
||||
assertType<{ params: any }>(resources);
|
||||
},
|
||||
});
|
||||
|
||||
// If a params codec is set, its type _should_ be available in the
|
||||
// request handler.
|
||||
createServerRouteRepository<{}, {}>().add({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: async (resources) => {
|
||||
assertType<{ params: { path: { serviceName: string } } }>(resources);
|
||||
},
|
||||
});
|
||||
|
||||
// Resources should be passed to the request handler.
|
||||
createServerRouteRepository<{ context: { getSpaceId: () => string } }, {}>().add({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: async ({ context }) => {
|
||||
const spaceId = context.getSpaceId();
|
||||
assertType<string>(spaceId);
|
||||
},
|
||||
});
|
||||
|
||||
// Create options are available when registering a route.
|
||||
createServerRouteRepository<{}, { options: { tags: string[] } }>().add({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
handler: async (resources) => {
|
||||
assertType<{ params: { path: { serviceName: string } } }>(resources);
|
||||
},
|
||||
});
|
||||
|
||||
const repository = createServerRouteRepository<{}, {}>()
|
||||
.add({
|
||||
endpoint: 'endpoint_without_params',
|
||||
handler: async () => {
|
||||
return {
|
||||
noParamsForMe: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
.add({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: async () => {
|
||||
return {
|
||||
yesParamsForMe: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
.add({
|
||||
endpoint: 'endpoint_with_optional_params',
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: async () => {
|
||||
return {
|
||||
someParamsForMe: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
type TestRepository = typeof repository;
|
||||
|
||||
// EndpointOf should return all valid endpoints of a repository
|
||||
|
||||
assertType<Array<EndpointOf<TestRepository>>>([
|
||||
'endpoint_with_params',
|
||||
'endpoint_without_params',
|
||||
'endpoint_with_optional_params',
|
||||
]);
|
||||
|
||||
// @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"'
|
||||
assertType<Array<EndpointOf<TestRepository>>>(['this_endpoint_does_not_exist']);
|
||||
|
||||
// ReturnOf should return the return type of a request handler.
|
||||
|
||||
assertType<ReturnOf<TestRepository, 'endpoint_without_params'>>({
|
||||
noParamsForMe: true,
|
||||
});
|
||||
|
||||
const noParamsInvalid: ReturnOf<TestRepository, 'endpoint_without_params'> = {
|
||||
// @ts-expect-error type '{ paramsForMe: boolean; }' is not assignable to type '{ noParamsForMe: boolean; }'.
|
||||
paramsForMe: true,
|
||||
};
|
||||
|
||||
// RouteRepositoryClient
|
||||
|
||||
type TestClient = RouteRepositoryClient<TestRepository, { timeout: number }>;
|
||||
|
||||
const client: TestClient = {} as any;
|
||||
|
||||
// It should respect any additional create options.
|
||||
|
||||
// @ts-expect-error Property 'timeout' is missing
|
||||
client({
|
||||
endpoint: 'endpoint_without_params',
|
||||
});
|
||||
|
||||
client({
|
||||
endpoint: 'endpoint_without_params',
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
// It does not allow params for routes without a params codec
|
||||
client({
|
||||
endpoint: 'endpoint_without_params',
|
||||
// @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type
|
||||
params: {},
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
// It requires params for routes with a params codec
|
||||
client({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: {
|
||||
// @ts-expect-error property 'serviceName' is missing in type '{}'
|
||||
path: {},
|
||||
},
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
// Params are optional if the codec has no required keys
|
||||
client({
|
||||
endpoint: 'endpoint_with_optional_params',
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
// If optional, an error will still occur if the params do not match
|
||||
client({
|
||||
endpoint: 'endpoint_with_optional_params',
|
||||
timeout: 1,
|
||||
params: {
|
||||
// @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type
|
||||
path: '',
|
||||
},
|
||||
});
|
||||
|
||||
// The return type is correctly inferred
|
||||
client({
|
||||
endpoint: 'endpoint_with_params',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: '',
|
||||
},
|
||||
},
|
||||
timeout: 1,
|
||||
}).then((res) => {
|
||||
assertType<{
|
||||
noParamsForMe: boolean;
|
||||
// @ts-expect-error Property 'noParamsForMe' is missing in type
|
||||
}>(res);
|
||||
|
||||
assertType<{
|
||||
yesParamsForMe: boolean;
|
||||
}>(res);
|
||||
});
|
||||
|
||||
// decodeRequestParams should return the type of the codec that is passed
|
||||
assertType<{ path: { serviceName: string } }>(
|
||||
decodeRequestParams(
|
||||
{
|
||||
params: {
|
||||
serviceName: 'serviceName',
|
||||
},
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
},
|
||||
t.type({ path: t.type({ serviceName: t.string }) })
|
||||
)
|
||||
);
|
||||
|
||||
assertType<{ path: { serviceName: boolean } }>(
|
||||
// @ts-expect-error The types of 'path.serviceName' are incompatible between these types.
|
||||
decodeRequestParams(
|
||||
{
|
||||
params: {
|
||||
serviceName: 'serviceName',
|
||||
},
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
},
|
||||
t.type({ path: t.type({ serviceName: t.string }) })
|
||||
)
|
||||
);
|
192
packages/kbn-server-route-repository/src/typings.ts
Normal file
192
packages/kbn-server-route-repository/src/typings.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { RequiredKeys } from 'utility-types';
|
||||
|
||||
type MaybeOptional<T extends { params: Record<string, any> }> = RequiredKeys<
|
||||
T['params']
|
||||
> extends never
|
||||
? { params?: T['params'] }
|
||||
: { params: T['params'] };
|
||||
|
||||
type WithoutIncompatibleMethods<T extends t.Any> = Omit<T, 'encode' | 'asEncoder'> & {
|
||||
encode: t.Encode<any, any>;
|
||||
asEncoder: () => t.Encoder<any, any>;
|
||||
};
|
||||
|
||||
export type RouteParamsRT = WithoutIncompatibleMethods<
|
||||
t.Type<{
|
||||
path?: any;
|
||||
query?: any;
|
||||
body?: any;
|
||||
}>
|
||||
>;
|
||||
|
||||
export interface RouteState {
|
||||
[endpoint: string]: ServerRoute<any, any, any, any, any>;
|
||||
}
|
||||
|
||||
export type ServerRouteHandlerResources = Record<string, any>;
|
||||
export type ServerRouteCreateOptions = Record<string, any>;
|
||||
|
||||
export type ServerRoute<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined,
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TReturnType,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions
|
||||
> = {
|
||||
endpoint: TEndpoint;
|
||||
params?: TRouteParamsRT;
|
||||
handler: ({}: TRouteHandlerResources &
|
||||
(TRouteParamsRT extends RouteParamsRT
|
||||
? DecodedRequestParamsOfType<TRouteParamsRT>
|
||||
: {})) => Promise<TReturnType>;
|
||||
} & TRouteCreateOptions;
|
||||
|
||||
export interface ServerRouteRepository<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources = ServerRouteHandlerResources,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions = ServerRouteCreateOptions,
|
||||
TRouteState extends RouteState = RouteState
|
||||
> {
|
||||
add<
|
||||
TEndpoint extends string,
|
||||
TReturnType,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: ServerRoute<
|
||||
TEndpoint,
|
||||
TRouteParamsRT,
|
||||
TRouteHandlerResources,
|
||||
TReturnType,
|
||||
TRouteCreateOptions
|
||||
>
|
||||
): ServerRouteRepository<
|
||||
TRouteHandlerResources,
|
||||
TRouteCreateOptions,
|
||||
TRouteState &
|
||||
{
|
||||
[key in TEndpoint]: ServerRoute<
|
||||
TEndpoint,
|
||||
TRouteParamsRT,
|
||||
TRouteHandlerResources,
|
||||
TReturnType,
|
||||
TRouteCreateOptions
|
||||
>;
|
||||
}
|
||||
>;
|
||||
merge<
|
||||
TServerRouteRepository extends ServerRouteRepository<
|
||||
TRouteHandlerResources,
|
||||
TRouteCreateOptions
|
||||
>
|
||||
>(
|
||||
repository: TServerRouteRepository
|
||||
): TServerRouteRepository extends ServerRouteRepository<
|
||||
TRouteHandlerResources,
|
||||
TRouteCreateOptions,
|
||||
infer TRouteStateToMerge
|
||||
>
|
||||
? ServerRouteRepository<
|
||||
TRouteHandlerResources,
|
||||
TRouteCreateOptions,
|
||||
TRouteState & TRouteStateToMerge
|
||||
>
|
||||
: never;
|
||||
getRoutes: () => Array<
|
||||
ServerRoute<string, RouteParamsRT, TRouteHandlerResources, unknown, TRouteCreateOptions>
|
||||
>;
|
||||
}
|
||||
|
||||
type ClientRequestParamsOfType<
|
||||
TRouteParamsRT extends RouteParamsRT
|
||||
> = TRouteParamsRT extends t.Mixed
|
||||
? MaybeOptional<{
|
||||
params: t.OutputOf<TRouteParamsRT>;
|
||||
}>
|
||||
: {};
|
||||
|
||||
type DecodedRequestParamsOfType<
|
||||
TRouteParamsRT extends RouteParamsRT
|
||||
> = TRouteParamsRT extends t.Mixed
|
||||
? MaybeOptional<{
|
||||
params: t.TypeOf<TRouteParamsRT>;
|
||||
}>
|
||||
: {};
|
||||
|
||||
export type EndpointOf<
|
||||
TServerRouteRepository extends ServerRouteRepository<any, any, any>
|
||||
> = TServerRouteRepository extends ServerRouteRepository<any, any, infer TRouteState>
|
||||
? keyof TRouteState
|
||||
: never;
|
||||
|
||||
export type ReturnOf<
|
||||
TServerRouteRepository extends ServerRouteRepository<any, any, any>,
|
||||
TEndpoint extends EndpointOf<TServerRouteRepository>
|
||||
> = TServerRouteRepository extends ServerRouteRepository<any, any, infer TRouteState>
|
||||
? TEndpoint extends keyof TRouteState
|
||||
? TRouteState[TEndpoint] extends ServerRoute<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
infer TReturnType,
|
||||
ServerRouteCreateOptions
|
||||
>
|
||||
? TReturnType
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type DecodedRequestParamsOf<
|
||||
TServerRouteRepository extends ServerRouteRepository<any, any, any>,
|
||||
TEndpoint extends EndpointOf<TServerRouteRepository>
|
||||
> = TServerRouteRepository extends ServerRouteRepository<any, any, infer TRouteState>
|
||||
? TEndpoint extends keyof TRouteState
|
||||
? TRouteState[TEndpoint] extends ServerRoute<
|
||||
any,
|
||||
infer TRouteParamsRT,
|
||||
any,
|
||||
any,
|
||||
ServerRouteCreateOptions
|
||||
>
|
||||
? TRouteParamsRT extends RouteParamsRT
|
||||
? DecodedRequestParamsOfType<TRouteParamsRT>
|
||||
: {}
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type ClientRequestParamsOf<
|
||||
TServerRouteRepository extends ServerRouteRepository<any, any, any>,
|
||||
TEndpoint extends EndpointOf<TServerRouteRepository>
|
||||
> = TServerRouteRepository extends ServerRouteRepository<any, any, infer TRouteState>
|
||||
? TEndpoint extends keyof TRouteState
|
||||
? TRouteState[TEndpoint] extends ServerRoute<
|
||||
any,
|
||||
infer TRouteParamsRT,
|
||||
any,
|
||||
any,
|
||||
ServerRouteCreateOptions
|
||||
>
|
||||
? TRouteParamsRT extends RouteParamsRT
|
||||
? ClientRequestParamsOfType<TRouteParamsRT>
|
||||
: {}
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type RouteRepositoryClient<
|
||||
TServerRouteRepository extends ServerRouteRepository<any, any, any>,
|
||||
TAdditionalClientOptions extends Record<string, any>
|
||||
> = <TEndpoint extends EndpointOf<TServerRouteRepository>>(
|
||||
options: {
|
||||
endpoint: TEndpoint;
|
||||
} & ClientRequestParamsOf<TServerRouteRepository, TEndpoint> &
|
||||
TAdditionalClientOptions
|
||||
) => Promise<ReturnOf<TServerRouteRepository, TEndpoint>>;
|
20
packages/kbn-server-route-repository/tsconfig.json
Normal file
20
packages/kbn-server-route-repository/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "./target",
|
||||
"stripInternal": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"sourceRoot": "../../../../packages/kbn-server-route-repository/src",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
],
|
||||
"noUnusedLocals": false
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -14,7 +14,7 @@ export enum LatencyAggregationType {
|
|||
}
|
||||
|
||||
export const latencyAggregationTypeRt = t.union([
|
||||
t.literal('avg'),
|
||||
t.literal('p95'),
|
||||
t.literal('p99'),
|
||||
t.literal(LatencyAggregationType.avg),
|
||||
t.literal(LatencyAggregationType.p95),
|
||||
t.literal(LatencyAggregationType.p99),
|
||||
]);
|
||||
|
|
|
@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type<number, string, unknown>(
|
|||
? t.failure(input, context)
|
||||
: t.success(epochDate);
|
||||
}),
|
||||
(a) => {
|
||||
const d = new Date(a);
|
||||
return d.toISOString();
|
||||
}
|
||||
(output) => new Date(output).toISOString()
|
||||
);
|
||||
|
|
|
@ -14,15 +14,18 @@ import {
|
|||
act,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import * as apmApi from '../../../../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
getCallApmApiSpy,
|
||||
CallApmApiSpy,
|
||||
} from '../../../../../../services/rest/callApmApiSpy';
|
||||
|
||||
export const removeExternalLinkText = (str: string) =>
|
||||
str.replace(/\(opens in a new tab or window\)/g, '');
|
||||
|
||||
describe('LinkPreview', () => {
|
||||
let callApmApiSpy: jest.SpyInstance<any, any>;
|
||||
let callApmApiSpy: CallApmApiSpy;
|
||||
beforeAll(() => {
|
||||
callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({
|
||||
callApmApiSpy = getCallApmApiSpy().mockResolvedValue({
|
||||
transaction: { id: 'foo' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { fireEvent, render, RenderResult } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { getCallApmApiSpy } from '../../../../../services/rest/callApmApiSpy';
|
||||
import { CustomLinkOverview } from '.';
|
||||
import { License } from '../../../../../../../licensing/common/license';
|
||||
import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
} from '../../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { LicenseContext } from '../../../../../context/license/license_context';
|
||||
import * as hooks from '../../../../../hooks/use_fetcher';
|
||||
import * as apmApi from '../../../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
expectTextsInDocument,
|
||||
expectTextsNotInDocument,
|
||||
|
@ -43,7 +43,7 @@ function getMockAPMContext({ canSave }: { canSave: boolean }) {
|
|||
|
||||
describe('CustomLink', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({});
|
||||
getCallApmApiSpy().mockResolvedValue({});
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
|
|
|
@ -22,9 +22,12 @@ import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_b
|
|||
import { renderWithTheme } from '../../../utils/testHelpers';
|
||||
import { ServiceOverview } from './';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import * as callApmApiModule from '../../../services/rest/createCallApmApi';
|
||||
import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context';
|
||||
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
|
||||
import {
|
||||
getCallApmApiSpy,
|
||||
getCreateCallApmApiSpy,
|
||||
} from '../../../services/rest/callApmApiSpy';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
|
@ -83,10 +86,10 @@ describe('ServiceOverview', () => {
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const calls = {
|
||||
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': {
|
||||
error_groups: [],
|
||||
error_groups: [] as any[],
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': {
|
||||
transactionGroups: [],
|
||||
transactionGroups: [] as any[],
|
||||
totalTransactionGroups: 0,
|
||||
isAggregationAccurate: true,
|
||||
},
|
||||
|
@ -95,19 +98,17 @@ describe('ServiceOverview', () => {
|
|||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
jest
|
||||
.spyOn(callApmApiModule, 'createCallApmApi')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const callApmApi = jest
|
||||
.spyOn(callApmApiModule, 'callApmApi')
|
||||
.mockImplementation(({ endpoint }) => {
|
||||
const callApmApiSpy = getCallApmApiSpy().mockImplementation(
|
||||
({ endpoint }) => {
|
||||
const response = calls[endpoint as keyof typeof calls];
|
||||
|
||||
return response
|
||||
? Promise.resolve(response)
|
||||
: Promise.reject(`Response for ${endpoint} is not defined`);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any);
|
||||
jest
|
||||
.spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown')
|
||||
.mockReturnValue({
|
||||
|
@ -124,7 +125,7 @@ describe('ServiceOverview', () => {
|
|||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(callApmApi).toHaveBeenCalledTimes(Object.keys(calls).length)
|
||||
expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length)
|
||||
);
|
||||
|
||||
expect((await findAllByText('Latency')).length).toBeGreaterThan(0);
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
fetchObservabilityOverviewPageData,
|
||||
getHasData,
|
||||
} from './apm_observability_overview_fetchers';
|
||||
import * as createCallApmApi from './createCallApmApi';
|
||||
import { getCallApmApiSpy } from './callApmApiSpy';
|
||||
|
||||
describe('Observability dashboard data', () => {
|
||||
const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
|
||||
const callApmApiMock = getCallApmApiSpy();
|
||||
const params = {
|
||||
absoluteTime: {
|
||||
start: moment('2020-07-02T13:25:11.629Z').valueOf(),
|
||||
|
@ -84,7 +84,7 @@ describe('Observability dashboard data', () => {
|
|||
callApmApiMock.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
serviceCount: 0,
|
||||
transactionPerMinute: { value: null, timeseries: [] },
|
||||
transactionPerMinute: { value: null, timeseries: [] as any },
|
||||
})
|
||||
);
|
||||
const response = await fetchObservabilityOverviewPageData(params);
|
||||
|
|
24
x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
Normal file
24
x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as createCallApmApi from './createCallApmApi';
|
||||
import type { AbstractAPMClient } from './createCallApmApi';
|
||||
|
||||
export type CallApmApiSpy = jest.SpyInstance<
|
||||
Promise<any>,
|
||||
Parameters<AbstractAPMClient>
|
||||
>;
|
||||
|
||||
export type CreateCallApmApiSpy = jest.SpyInstance<AbstractAPMClient>;
|
||||
|
||||
export const getCreateCallApmApiSpy = () =>
|
||||
(jest.spyOn(
|
||||
createCallApmApi,
|
||||
'createCallApmApi'
|
||||
) as unknown) as CreateCallApmApiSpy;
|
||||
export const getCallApmApiSpy = () =>
|
||||
(jest.spyOn(createCallApmApi, 'callApmApi') as unknown) as CallApmApiSpy;
|
|
@ -6,30 +6,68 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { parseEndpoint } from '../../../common/apm_api/parse_endpoint';
|
||||
import * as t from 'io-ts';
|
||||
import type {
|
||||
ClientRequestParamsOf,
|
||||
EndpointOf,
|
||||
ReturnOf,
|
||||
RouteRepositoryClient,
|
||||
ServerRouteRepository,
|
||||
ServerRoute,
|
||||
} from '@kbn/server-route-repository';
|
||||
import { formatRequest } from '@kbn/server-route-repository/target/format_request';
|
||||
import { FetchOptions } from '../../../common/fetch_options';
|
||||
import { callApi } from './callApi';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import type { APMAPI } from '../../../server/routes/create_apm_api';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import type { Client } from '../../../server/routes/typings';
|
||||
|
||||
export type APMClient = Client<APMAPI['_S']>;
|
||||
export type AutoAbortedAPMClient = Client<APMAPI['_S'], { abortable: false }>;
|
||||
import type {
|
||||
APMServerRouteRepository,
|
||||
InspectResponse,
|
||||
APMRouteHandlerResources,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../server';
|
||||
|
||||
export type APMClientOptions = Omit<
|
||||
FetchOptions,
|
||||
'query' | 'body' | 'pathname' | 'signal'
|
||||
> & {
|
||||
endpoint: string;
|
||||
signal: AbortSignal | null;
|
||||
params?: {
|
||||
body?: any;
|
||||
query?: Record<string, any>;
|
||||
path?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
export type APMClient = RouteRepositoryClient<
|
||||
APMServerRouteRepository,
|
||||
APMClientOptions
|
||||
>;
|
||||
|
||||
export type AutoAbortedAPMClient = RouteRepositoryClient<
|
||||
APMServerRouteRepository,
|
||||
Omit<APMClientOptions, 'signal'>
|
||||
>;
|
||||
|
||||
export type APIReturnType<
|
||||
TEndpoint extends EndpointOf<APMServerRouteRepository>
|
||||
> = ReturnOf<APMServerRouteRepository, TEndpoint> & {
|
||||
_inspect?: InspectResponse;
|
||||
};
|
||||
|
||||
export type APIEndpoint = EndpointOf<APMServerRouteRepository>;
|
||||
|
||||
export type APIClientRequestParamsOf<
|
||||
TEndpoint extends EndpointOf<APMServerRouteRepository>
|
||||
> = ClientRequestParamsOf<APMServerRouteRepository, TEndpoint>;
|
||||
|
||||
export type AbstractAPMRepository = ServerRouteRepository<
|
||||
APMRouteHandlerResources,
|
||||
{},
|
||||
Record<
|
||||
string,
|
||||
ServerRoute<string, t.Mixed | undefined, APMRouteHandlerResources, any, {}>
|
||||
>
|
||||
>;
|
||||
|
||||
export type AbstractAPMClient = RouteRepositoryClient<
|
||||
AbstractAPMRepository,
|
||||
APMClientOptions
|
||||
>;
|
||||
|
||||
export let callApmApi: APMClient = () => {
|
||||
throw new Error(
|
||||
'callApmApi has to be initialized before used. Call createCallApmApi first.'
|
||||
|
@ -37,9 +75,13 @@ export let callApmApi: APMClient = () => {
|
|||
};
|
||||
|
||||
export function createCallApmApi(core: CoreStart | CoreSetup) {
|
||||
callApmApi = ((options: APMClientOptions) => {
|
||||
const { endpoint, params, ...opts } = options;
|
||||
const { method, pathname } = parseEndpoint(endpoint, params?.path);
|
||||
callApmApi = ((options) => {
|
||||
const { endpoint, ...opts } = options;
|
||||
const { params } = (options as unknown) as {
|
||||
params?: Partial<Record<string, any>>;
|
||||
};
|
||||
|
||||
const { method, pathname } = formatRequest(endpoint, params?.path);
|
||||
|
||||
return callApi(core, {
|
||||
...opts,
|
||||
|
@ -50,10 +92,3 @@ export function createCallApmApi(core: CoreStart | CoreSetup) {
|
|||
});
|
||||
}) as APMClient;
|
||||
}
|
||||
|
||||
// infer return type from API
|
||||
export type APIReturnType<
|
||||
TPath extends keyof APMAPI['_S']
|
||||
> = APMAPI['_S'][TPath] extends { ret: any }
|
||||
? APMAPI['_S'][TPath]['ret']
|
||||
: unknown;
|
||||
|
|
|
@ -120,5 +120,9 @@ export function mergeConfigs(
|
|||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new APMPlugin(initContext);
|
||||
|
||||
export { APMPlugin, APMPluginSetup } from './plugin';
|
||||
export { APMPlugin } from './plugin';
|
||||
export { APMPluginSetup } from './types';
|
||||
export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository';
|
||||
export { InspectResponse, APMRouteHandlerResources } from './routes/typings';
|
||||
|
||||
export type { ProcessorEvent } from '../common/processor_event';
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { omit } from 'lodash';
|
||||
import chalk from 'chalk';
|
||||
import { KibanaRequest } from '../../../../../../../src/core/server';
|
||||
import { inspectableEsQueriesMap } from '../../../routes/create_api';
|
||||
import { inspectableEsQueriesMap } from '../../../routes/register_routes';
|
||||
|
||||
function formatObj(obj: Record<string, any>) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
|
||||
import {
|
||||
CreateIndexRequest,
|
||||
|
@ -13,7 +12,7 @@ import {
|
|||
IndexRequest,
|
||||
} from '@elastic/elasticsearch/api/types';
|
||||
import { unwrapEsResponse } from '../../../../../../observability/server';
|
||||
import { APMRequestHandlerContext } from '../../../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../../../routes/typings';
|
||||
import {
|
||||
ESSearchResponse,
|
||||
ESSearchRequest,
|
||||
|
@ -31,11 +30,9 @@ export type APMInternalClient = ReturnType<typeof createInternalESClient>;
|
|||
|
||||
export function createInternalESClient({
|
||||
context,
|
||||
debug,
|
||||
request,
|
||||
}: {
|
||||
context: APMRequestHandlerContext;
|
||||
request: KibanaRequest;
|
||||
}) {
|
||||
}: Pick<APMRouteHandlerResources, 'context' | 'request'> & { debug: boolean }) {
|
||||
const { asInternalUser } = context.core.elasticsearch.client;
|
||||
|
||||
function callEs<T extends { body: any }>({
|
||||
|
@ -53,7 +50,7 @@ export function createInternalESClient({
|
|||
title: getDebugTitle(request),
|
||||
body: getDebugBody(params, requestType),
|
||||
}),
|
||||
debug: context.params.query._inspect,
|
||||
debug,
|
||||
isCalledWithInternalUser: true,
|
||||
request,
|
||||
requestType,
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import { setupRequest } from './setup_request';
|
||||
import { APMConfig } from '../..';
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
|
@ -32,7 +31,7 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
function getMockRequest() {
|
||||
function getMockResources() {
|
||||
const esClientMock = {
|
||||
asCurrentUser: {
|
||||
search: jest.fn().mockResolvedValue({ body: {} }),
|
||||
|
@ -42,7 +41,7 @@ function getMockRequest() {
|
|||
},
|
||||
};
|
||||
|
||||
const mockContext = ({
|
||||
const mockResources = ({
|
||||
config: new Proxy(
|
||||
{},
|
||||
{
|
||||
|
@ -54,65 +53,69 @@ function getMockRequest() {
|
|||
_inspect: false,
|
||||
},
|
||||
},
|
||||
core: {
|
||||
elasticsearch: {
|
||||
client: esClientMock,
|
||||
},
|
||||
uiSettings: {
|
||||
client: {
|
||||
get: jest.fn().mockResolvedValue(false),
|
||||
context: {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
client: esClientMock,
|
||||
},
|
||||
},
|
||||
savedObjects: {
|
||||
client: {
|
||||
get: jest.fn(),
|
||||
uiSettings: {
|
||||
client: {
|
||||
get: jest.fn().mockResolvedValue(false),
|
||||
},
|
||||
},
|
||||
savedObjects: {
|
||||
client: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
ml: undefined,
|
||||
},
|
||||
} as unknown) as APMRequestHandlerContext & {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
client: typeof esClientMock;
|
||||
};
|
||||
uiSettings: {
|
||||
client: {
|
||||
get: jest.Mock<any, any>;
|
||||
request: {
|
||||
url: '',
|
||||
events: {
|
||||
aborted$: {
|
||||
subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as APMRouteHandlerResources & {
|
||||
context: {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
client: typeof esClientMock;
|
||||
};
|
||||
};
|
||||
savedObjects: {
|
||||
client: {
|
||||
get: jest.Mock<any, any>;
|
||||
uiSettings: {
|
||||
client: {
|
||||
get: jest.Mock<any, any>;
|
||||
};
|
||||
};
|
||||
savedObjects: {
|
||||
client: {
|
||||
get: jest.Mock<any, any>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const mockRequest = ({
|
||||
url: '',
|
||||
events: {
|
||||
aborted$: {
|
||||
subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
|
||||
},
|
||||
},
|
||||
} as unknown) as KibanaRequest;
|
||||
|
||||
return { mockContext, mockRequest };
|
||||
return mockResources;
|
||||
}
|
||||
|
||||
describe('setupRequest', () => {
|
||||
describe('with default args', () => {
|
||||
it('calls callWithRequest', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const mockResources = getMockResources();
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
await apmEventClient.search({
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search
|
||||
).toHaveBeenCalledWith({
|
||||
index: ['apm-*'],
|
||||
body: {
|
||||
|
@ -132,14 +135,14 @@ describe('setupRequest', () => {
|
|||
});
|
||||
|
||||
it('calls callWithInternalUser', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const { internalClient } = await setupRequest(mockContext, mockRequest);
|
||||
const mockResources = getMockResources();
|
||||
const { internalClient } = await setupRequest(mockResources);
|
||||
await internalClient.search({
|
||||
index: ['apm-*'],
|
||||
body: { foo: 'bar' },
|
||||
} as any);
|
||||
expect(
|
||||
mockContext.core.elasticsearch.client.asInternalUser.search
|
||||
mockResources.context.core.elasticsearch.client.asInternalUser.search
|
||||
).toHaveBeenCalledWith({
|
||||
index: ['apm-*'],
|
||||
body: {
|
||||
|
@ -151,8 +154,8 @@ describe('setupRequest', () => {
|
|||
|
||||
describe('with a bool filter', () => {
|
||||
it('adds a range filter for `observer.version_major` to the existing filter', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const mockResources = getMockResources();
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
await apmEventClient.search({
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
|
@ -162,8 +165,8 @@ describe('setupRequest', () => {
|
|||
},
|
||||
});
|
||||
const params =
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
.calls[0][0];
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search
|
||||
.mock.calls[0][0];
|
||||
expect(params.body).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -178,8 +181,8 @@ describe('setupRequest', () => {
|
|||
});
|
||||
|
||||
it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const mockResources = getMockResources();
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
await apmEventClient.search(
|
||||
{
|
||||
apm: {
|
||||
|
@ -194,8 +197,8 @@ describe('setupRequest', () => {
|
|||
}
|
||||
);
|
||||
const params =
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
.calls[0][0];
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search
|
||||
.mock.calls[0][0];
|
||||
expect(params.body).toEqual({
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -216,15 +219,15 @@ describe('setupRequest', () => {
|
|||
|
||||
describe('without a bool filter', () => {
|
||||
it('adds a range filter for `observer.version_major`', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const mockResources = getMockResources();
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
await apmEventClient.search({
|
||||
apm: {
|
||||
events: [ProcessorEvent.error],
|
||||
},
|
||||
});
|
||||
const params =
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
.calls[0][0];
|
||||
expect(params.body).toEqual({
|
||||
query: {
|
||||
|
@ -241,12 +244,12 @@ describe('without a bool filter', () => {
|
|||
|
||||
describe('with includeFrozen=false', () => {
|
||||
it('sets `ignore_throttled=true`', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const mockResources = getMockResources();
|
||||
|
||||
// mock includeFrozen to return false
|
||||
mockContext.core.uiSettings.client.get.mockResolvedValue(false);
|
||||
mockResources.context.core.uiSettings.client.get.mockResolvedValue(false);
|
||||
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
|
||||
await apmEventClient.search({
|
||||
apm: {
|
||||
|
@ -255,7 +258,7 @@ describe('with includeFrozen=false', () => {
|
|||
});
|
||||
|
||||
const params =
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
.calls[0][0];
|
||||
expect(params.ignore_throttled).toBe(true);
|
||||
});
|
||||
|
@ -263,19 +266,19 @@ describe('with includeFrozen=false', () => {
|
|||
|
||||
describe('with includeFrozen=true', () => {
|
||||
it('sets `ignore_throttled=false`', async () => {
|
||||
const { mockContext, mockRequest } = getMockRequest();
|
||||
const mockResources = getMockResources();
|
||||
|
||||
// mock includeFrozen to return true
|
||||
mockContext.core.uiSettings.client.get.mockResolvedValue(true);
|
||||
mockResources.context.core.uiSettings.client.get.mockResolvedValue(true);
|
||||
|
||||
const { apmEventClient } = await setupRequest(mockContext, mockRequest);
|
||||
const { apmEventClient } = await setupRequest(mockResources);
|
||||
|
||||
await apmEventClient.search({
|
||||
apm: { events: [] },
|
||||
});
|
||||
|
||||
const params =
|
||||
mockContext.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
|
||||
.calls[0][0];
|
||||
expect(params.ignore_throttled).toBe(false);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { APMConfig } from '../..';
|
|||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
|
||||
import { UIFilters } from '../../../typings/ui_filters';
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
import {
|
||||
ApmIndicesConfig,
|
||||
getApmIndices,
|
||||
|
@ -44,7 +44,7 @@ export interface SetupTimeRange {
|
|||
}
|
||||
|
||||
interface SetupRequestParams {
|
||||
query?: {
|
||||
query: {
|
||||
_inspect?: boolean;
|
||||
|
||||
/**
|
||||
|
@ -64,13 +64,19 @@ type InferSetup<TParams extends SetupRequestParams> = Setup &
|
|||
(TParams extends { query: { start: number } } ? { start: number } : {}) &
|
||||
(TParams extends { query: { end: number } } ? { end: number } : {});
|
||||
|
||||
export async function setupRequest<TParams extends SetupRequestParams>(
|
||||
context: APMRequestHandlerContext<TParams>,
|
||||
request: KibanaRequest
|
||||
): Promise<InferSetup<TParams>> {
|
||||
export async function setupRequest<TParams extends SetupRequestParams>({
|
||||
context,
|
||||
params,
|
||||
core,
|
||||
plugins,
|
||||
request,
|
||||
config,
|
||||
logger,
|
||||
}: APMRouteHandlerResources & {
|
||||
params: TParams;
|
||||
}): Promise<InferSetup<TParams>> {
|
||||
return withApmSpan('setup_request', async () => {
|
||||
const { config, logger } = context;
|
||||
const { query } = context.params;
|
||||
const { query } = params;
|
||||
|
||||
const [indices, includeFrozen] = await Promise.all([
|
||||
getApmIndices({
|
||||
|
@ -88,7 +94,7 @@ export async function setupRequest<TParams extends SetupRequestParams>(
|
|||
indices,
|
||||
apmEventClient: createApmEventClient({
|
||||
esClient: context.core.elasticsearch.client.asCurrentUser,
|
||||
debug: context.params.query._inspect,
|
||||
debug: query._inspect,
|
||||
request,
|
||||
indices,
|
||||
options: { includeFrozen },
|
||||
|
@ -96,11 +102,12 @@ export async function setupRequest<TParams extends SetupRequestParams>(
|
|||
internalClient: createInternalESClient({
|
||||
context,
|
||||
request,
|
||||
debug: query._inspect,
|
||||
}),
|
||||
ml:
|
||||
context.plugins.ml && isActivePlatinumLicense(context.licensing.license)
|
||||
plugins.ml && isActivePlatinumLicense(context.licensing.license)
|
||||
? getMlSetup(
|
||||
context.plugins.ml,
|
||||
plugins.ml.setup,
|
||||
context.core.savedObjects.client,
|
||||
request
|
||||
)
|
||||
|
@ -118,8 +125,8 @@ export async function setupRequest<TParams extends SetupRequestParams>(
|
|||
}
|
||||
|
||||
function getMlSetup(
|
||||
ml: Required<APMRequestHandlerContext['plugins']>['ml'],
|
||||
savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'],
|
||||
ml: Required<APMRouteHandlerResources['plugins']>['ml']['setup'],
|
||||
savedObjectsClient: APMRouteHandlerResources['context']['core']['savedObjects']['client'],
|
||||
request: KibanaRequest
|
||||
) {
|
||||
return {
|
||||
|
|
|
@ -8,21 +8,9 @@
|
|||
import { createStaticIndexPattern } from './create_static_index_pattern';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data';
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
|
||||
import { APMConfig } from '../..';
|
||||
|
||||
function getMockContext(config: Record<string, unknown>) {
|
||||
return ({
|
||||
config,
|
||||
core: {
|
||||
savedObjects: {
|
||||
client: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown) as APMRequestHandlerContext;
|
||||
}
|
||||
function getMockSavedObjectsClient() {
|
||||
return ({
|
||||
create: jest.fn(),
|
||||
|
@ -32,13 +20,13 @@ function getMockSavedObjectsClient() {
|
|||
describe('createStaticIndexPattern', () => {
|
||||
it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => {
|
||||
const setup = {} as Setup;
|
||||
const context = getMockContext({
|
||||
'xpack.apm.autocreateApmIndexPattern': false,
|
||||
});
|
||||
|
||||
const savedObjectsClient = getMockSavedObjectsClient();
|
||||
await createStaticIndexPattern(
|
||||
setup,
|
||||
context,
|
||||
{
|
||||
'xpack.apm.autocreateApmIndexPattern': false,
|
||||
} as APMConfig,
|
||||
savedObjectsClient,
|
||||
'default'
|
||||
);
|
||||
|
@ -47,9 +35,6 @@ describe('createStaticIndexPattern', () => {
|
|||
|
||||
it(`should not create index pattern if no APM data is found`, async () => {
|
||||
const setup = {} as Setup;
|
||||
const context = getMockContext({
|
||||
'xpack.apm.autocreateApmIndexPattern': true,
|
||||
});
|
||||
|
||||
// does not have APM data
|
||||
jest
|
||||
|
@ -60,7 +45,9 @@ describe('createStaticIndexPattern', () => {
|
|||
|
||||
await createStaticIndexPattern(
|
||||
setup,
|
||||
context,
|
||||
{
|
||||
'xpack.apm.autocreateApmIndexPattern': true,
|
||||
} as APMConfig,
|
||||
savedObjectsClient,
|
||||
'default'
|
||||
);
|
||||
|
@ -69,9 +56,6 @@ describe('createStaticIndexPattern', () => {
|
|||
|
||||
it(`should create index pattern`, async () => {
|
||||
const setup = {} as Setup;
|
||||
const context = getMockContext({
|
||||
'xpack.apm.autocreateApmIndexPattern': true,
|
||||
});
|
||||
|
||||
// does have APM data
|
||||
jest
|
||||
|
@ -82,7 +66,9 @@ describe('createStaticIndexPattern', () => {
|
|||
|
||||
await createStaticIndexPattern(
|
||||
setup,
|
||||
context,
|
||||
{
|
||||
'xpack.apm.autocreateApmIndexPattern': true,
|
||||
} as APMConfig,
|
||||
savedObjectsClient,
|
||||
'default'
|
||||
);
|
||||
|
|
|
@ -12,20 +12,18 @@ import {
|
|||
} from '../../../../../../src/plugins/apm_oss/server';
|
||||
import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js';
|
||||
import { withApmSpan } from '../../utils/with_apm_span';
|
||||
import { getApmIndexPatternTitle } from './get_apm_index_pattern_title';
|
||||
|
||||
export async function createStaticIndexPattern(
|
||||
setup: Setup,
|
||||
context: APMRequestHandlerContext,
|
||||
config: APMRouteHandlerResources['config'],
|
||||
savedObjectsClient: InternalSavedObjectsClient,
|
||||
spaceId: string | undefined
|
||||
): Promise<boolean> {
|
||||
return withApmSpan('create_static_index_pattern', async () => {
|
||||
const { config } = context;
|
||||
|
||||
// don't autocreate APM index pattern if it's been disabled via the config
|
||||
if (!config['xpack.apm.autocreateApmIndexPattern']) {
|
||||
return false;
|
||||
|
@ -39,7 +37,7 @@ export async function createStaticIndexPattern(
|
|||
}
|
||||
|
||||
try {
|
||||
const apmIndexPatternTitle = getApmIndexPatternTitle(context);
|
||||
const apmIndexPatternTitle = getApmIndexPatternTitle(config);
|
||||
await withApmSpan('create_index_pattern_saved_object', () =>
|
||||
savedObjectsClient.create(
|
||||
'index-pattern',
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
|
||||
export function getApmIndexPatternTitle(context: APMRequestHandlerContext) {
|
||||
return context.config['apm_oss.indexPattern'];
|
||||
export function getApmIndexPatternTitle(
|
||||
config: APMRouteHandlerResources['config']
|
||||
) {
|
||||
return config['apm_oss.indexPattern'];
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
IndexPatternsFetcher,
|
||||
FieldDescriptor,
|
||||
} from '../../../../../../src/plugins/data/server';
|
||||
import { APMRequestHandlerContext } from '../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
import { withApmSpan } from '../../utils/with_apm_span';
|
||||
|
||||
export interface IndexPatternTitleAndFields {
|
||||
|
@ -20,12 +20,12 @@ export interface IndexPatternTitleAndFields {
|
|||
|
||||
// TODO: this is currently cached globally. In the future we might want to cache this per user
|
||||
export const getDynamicIndexPattern = ({
|
||||
config,
|
||||
context,
|
||||
}: {
|
||||
context: APMRequestHandlerContext;
|
||||
}) => {
|
||||
logger,
|
||||
}: Pick<APMRouteHandlerResources, 'logger' | 'config' | 'context'>) => {
|
||||
return withApmSpan('get_dynamic_index_pattern', async () => {
|
||||
const indexPatternTitle = context.config['apm_oss.indexPattern'];
|
||||
const indexPatternTitle = config['apm_oss.indexPattern'];
|
||||
|
||||
const indexPatternsFetcher = new IndexPatternsFetcher(
|
||||
context.core.elasticsearch.client.asCurrentUser
|
||||
|
@ -50,7 +50,7 @@ export const getDynamicIndexPattern = ({
|
|||
} catch (e) {
|
||||
const notExists = e.output?.statusCode === 404;
|
||||
if (notExists) {
|
||||
context.logger.error(
|
||||
logger.error(
|
||||
`Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist`
|
||||
);
|
||||
return;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
APM_INDICES_SAVED_OBJECT_ID,
|
||||
} from '../../../../common/apm_saved_object_constants';
|
||||
import { APMConfig } from '../../..';
|
||||
import { APMRequestHandlerContext } from '../../../routes/typings';
|
||||
import { APMRouteHandlerResources } from '../../../routes/typings';
|
||||
import { withApmSpan } from '../../../utils/with_apm_span';
|
||||
|
||||
type ISavedObjectsClient = Pick<SavedObjectsClient, 'get'>;
|
||||
|
@ -91,9 +91,8 @@ const APM_UI_INDICES: ApmIndicesName[] = [
|
|||
|
||||
export async function getApmIndexSettings({
|
||||
context,
|
||||
}: {
|
||||
context: APMRequestHandlerContext;
|
||||
}) {
|
||||
config,
|
||||
}: Pick<APMRouteHandlerResources, 'context' | 'config'>) {
|
||||
let apmIndicesSavedObject: PromiseReturnType<typeof getApmIndicesSavedObject>;
|
||||
try {
|
||||
apmIndicesSavedObject = await getApmIndicesSavedObject(
|
||||
|
@ -106,7 +105,7 @@ export async function getApmIndexSettings({
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
const apmIndicesConfig = getApmIndicesConfig(context.config);
|
||||
const apmIndicesConfig = getApmIndicesConfig(config);
|
||||
|
||||
return APM_UI_INDICES.map((configurationName) => ({
|
||||
configurationName,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import {
|
||||
CoreSetup,
|
||||
|
@ -16,22 +16,10 @@ import {
|
|||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from 'src/core/server';
|
||||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { mapValues } from 'lodash';
|
||||
import { APMConfig, APMXPackConfig } from '.';
|
||||
import { mergeConfigs } from './index';
|
||||
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
|
||||
import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
|
||||
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
|
||||
import { ActionsPlugin } from '../../actions/server';
|
||||
import { AlertingPlugin } from '../../alerting/server';
|
||||
import { CloudSetup } from '../../cloud/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { MlPluginSetup } from '../../ml/server';
|
||||
import { ObservabilityPluginSetup } from '../../observability/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { TaskManagerSetupContract } from '../../task_manager/server';
|
||||
import { APM_FEATURE, registerFeaturesUsage } from './feature';
|
||||
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
|
||||
import { createApmTelemetry } from './lib/apm_telemetry';
|
||||
|
@ -40,23 +28,29 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_
|
|||
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
|
||||
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
|
||||
import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index';
|
||||
import { createApmApi } from './routes/create_apm_api';
|
||||
import { apmIndices, apmTelemetry } from './saved_objects';
|
||||
import { createElasticCloudInstructions } from './tutorial/elastic_cloud';
|
||||
import { uiSettings } from './ui_settings';
|
||||
import type { ApmPluginRequestHandlerContext } from './routes/typings';
|
||||
import type {
|
||||
ApmPluginRequestHandlerContext,
|
||||
APMRouteHandlerResources,
|
||||
} from './routes/typings';
|
||||
import {
|
||||
APMPluginSetup,
|
||||
APMPluginSetupDependencies,
|
||||
APMPluginStartDependencies,
|
||||
} from './types';
|
||||
import { registerRoutes } from './routes/register_routes';
|
||||
import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository';
|
||||
|
||||
export interface APMPluginSetup {
|
||||
config$: Observable<APMConfig>;
|
||||
getApmIndices: () => ReturnType<typeof getApmIndices>;
|
||||
createApmEventClient: (params: {
|
||||
debug?: boolean;
|
||||
request: KibanaRequest;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
}) => Promise<ReturnType<typeof createApmEventClient>>;
|
||||
}
|
||||
|
||||
export class APMPlugin implements Plugin<APMPluginSetup> {
|
||||
export class APMPlugin
|
||||
implements
|
||||
Plugin<
|
||||
APMPluginSetup,
|
||||
void,
|
||||
APMPluginSetupDependencies,
|
||||
APMPluginStartDependencies
|
||||
> {
|
||||
private currentConfig?: APMConfig;
|
||||
private logger?: Logger;
|
||||
constructor(private readonly initContext: PluginInitializerContext) {
|
||||
|
@ -64,22 +58,8 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
|
|||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
plugins: {
|
||||
spaces?: SpacesPluginSetup;
|
||||
apmOss: APMOSSPluginSetup;
|
||||
home: HomeServerPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
cloud?: CloudSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
taskManager?: TaskManagerSetupContract;
|
||||
alerting?: AlertingPlugin['setup'];
|
||||
actions?: ActionsPlugin['setup'];
|
||||
observability?: ObservabilityPluginSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
ml?: MlPluginSetup;
|
||||
}
|
||||
core: CoreSetup<APMPluginStartDependencies>,
|
||||
plugins: Omit<APMPluginSetupDependencies, 'core'>
|
||||
) {
|
||||
this.logger = this.initContext.logger.get();
|
||||
const config$ = this.initContext.config.create<APMXPackConfig>();
|
||||
|
@ -101,11 +81,13 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
|
|||
});
|
||||
}
|
||||
|
||||
this.currentConfig = mergeConfigs(
|
||||
const currentConfig = mergeConfigs(
|
||||
plugins.apmOss.config,
|
||||
this.initContext.config.get<APMXPackConfig>()
|
||||
);
|
||||
|
||||
this.currentConfig = currentConfig;
|
||||
|
||||
if (
|
||||
plugins.taskManager &&
|
||||
plugins.usageCollection &&
|
||||
|
@ -122,8 +104,8 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
|
|||
}
|
||||
|
||||
const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider();
|
||||
plugins.home.tutorials.unregisterTutorial(ossTutorialProvider);
|
||||
plugins.home.tutorials.registerTutorial(() => {
|
||||
plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider);
|
||||
plugins.home?.tutorials.registerTutorial(() => {
|
||||
const ossPart = ossTutorialProvider({});
|
||||
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
|
||||
ossPart.artifacts.application = {
|
||||
|
@ -147,10 +129,26 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
|
|||
|
||||
registerFeaturesUsage({ licensingPlugin: plugins.licensing });
|
||||
|
||||
createApmApi().init(core, {
|
||||
config$: mergedConfig$,
|
||||
logger: this.logger!,
|
||||
plugins,
|
||||
registerRoutes({
|
||||
core: {
|
||||
setup: core,
|
||||
start: () => core.getStartServices().then(([coreStart]) => coreStart),
|
||||
},
|
||||
logger: this.logger,
|
||||
config: currentConfig,
|
||||
repository: getGlobalApmServerRouteRepository(),
|
||||
plugins: mapValues(plugins, (value, key) => {
|
||||
return {
|
||||
setup: value,
|
||||
start: () =>
|
||||
core.getStartServices().then((services) => {
|
||||
const [, pluginsStartContracts] = services;
|
||||
return pluginsStartContracts[
|
||||
key as keyof APMPluginStartDependencies
|
||||
];
|
||||
}),
|
||||
};
|
||||
}) as APMRouteHandlerResources['plugins'],
|
||||
});
|
||||
|
||||
const boundGetApmIndices = async () =>
|
||||
|
|
|
@ -10,7 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ
|
|||
import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count';
|
||||
import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { createRoute } from '../create_route';
|
||||
import { createApmServerRoute } from '../create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
|
||||
import { rangeRt } from '../default_api_types';
|
||||
|
||||
const alertParamsRt = t.intersection([
|
||||
|
@ -29,13 +30,14 @@ const alertParamsRt = t.intersection([
|
|||
|
||||
export type AlertParams = t.TypeOf<typeof alertParamsRt>;
|
||||
|
||||
export const transactionErrorRateChartPreview = createRoute({
|
||||
const transactionErrorRateChartPreview = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate',
|
||||
params: t.type({ query: alertParamsRt }),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { _inspect, ...alertParams } = params.query;
|
||||
|
||||
const errorRateChartPreview = await getTransactionErrorRateChartPreview({
|
||||
setup,
|
||||
|
@ -46,13 +48,16 @@ export const transactionErrorRateChartPreview = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionErrorCountChartPreview = createRoute({
|
||||
const transactionErrorCountChartPreview = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count',
|
||||
params: t.type({ query: alertParamsRt }),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const { _inspect, ...alertParams } = params.query;
|
||||
|
||||
const errorCountChartPreview = await getTransactionErrorCountChartPreview({
|
||||
setup,
|
||||
alertParams,
|
||||
|
@ -62,13 +67,16 @@ export const transactionErrorCountChartPreview = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionDurationChartPreview = createRoute({
|
||||
const transactionDurationChartPreview = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration',
|
||||
params: t.type({ query: alertParamsRt }),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { _inspect, ...alertParams } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { params } = resources;
|
||||
|
||||
const { _inspect, ...alertParams } = params.query;
|
||||
|
||||
const latencyChartPreview = await getTransactionDurationChartPreview({
|
||||
alertParams,
|
||||
|
@ -78,3 +86,9 @@ export const transactionDurationChartPreview = createRoute({
|
|||
return { latencyChartPreview };
|
||||
},
|
||||
});
|
||||
|
||||
export const alertsChartPreviewRouteRepository = createApmServerRouteRepository()
|
||||
.add(transactionErrorRateChartPreview)
|
||||
.add(transactionDurationChartPreview)
|
||||
.add(transactionErrorCountChartPreview)
|
||||
.add(transactionDurationChartPreview);
|
||||
|
|
|
@ -14,7 +14,8 @@ import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overal
|
|||
import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions';
|
||||
import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
|
||||
|
||||
const INVALID_LICENSE = i18n.translate(
|
||||
|
@ -25,7 +26,7 @@ const INVALID_LICENSE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const correlationsLatencyDistributionRoute = createRoute({
|
||||
const correlationsLatencyDistributionRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/correlations/latency/overall_distribution',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -40,18 +41,19 @@ export const correlationsLatencyDistributionRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
return getOverallLatencyDistribution({
|
||||
environment,
|
||||
|
@ -64,7 +66,7 @@ export const correlationsLatencyDistributionRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const correlationsForSlowTransactionsRoute = createRoute({
|
||||
const correlationsForSlowTransactionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/correlations/latency/slow_transactions',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -85,11 +87,13 @@ export const correlationsForSlowTransactionsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -100,7 +104,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
|
|||
fieldNames,
|
||||
maxLatency,
|
||||
distributionInterval,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
return getCorrelationsForSlowTransactions({
|
||||
environment,
|
||||
|
@ -117,7 +121,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const correlationsErrorDistributionRoute = createRoute({
|
||||
const correlationsErrorDistributionRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/correlations/errors/overall_timeseries',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -132,18 +136,20 @@ export const correlationsErrorDistributionRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { params, context } = resources;
|
||||
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
return getOverallErrorTimeseries({
|
||||
environment,
|
||||
|
@ -156,7 +162,7 @@ export const correlationsErrorDistributionRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const correlationsForFailedTransactionsRoute = createRoute({
|
||||
const correlationsForFailedTransactionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/correlations/errors/failed_transactions',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -174,11 +180,12 @@ export const correlationsForFailedTransactionsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -186,7 +193,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({
|
|||
transactionType,
|
||||
transactionName,
|
||||
fieldNames,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
return getCorrelationsForFailedTransactions({
|
||||
environment,
|
||||
|
@ -199,3 +206,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const correlationsRouteRepository = createApmServerRouteRepository()
|
||||
.add(correlationsLatencyDistributionRoute)
|
||||
.add(correlationsForSlowTransactionsRoute)
|
||||
.add(correlationsErrorDistributionRoute)
|
||||
.add(correlationsForFailedTransactionsRoute);
|
||||
|
|
|
@ -1,368 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { createApi } from './index';
|
||||
import { CoreSetup, Logger } from 'src/core/server';
|
||||
import { RouteParamsRT } from '../typings';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { APMConfig } from '../..';
|
||||
import { jsonRt } from '../../../common/runtime_types/json_rt';
|
||||
|
||||
const getCoreMock = () => {
|
||||
const get = jest.fn();
|
||||
const post = jest.fn();
|
||||
const put = jest.fn();
|
||||
const createRouter = jest.fn().mockReturnValue({
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
});
|
||||
|
||||
const mock = {} as CoreSetup;
|
||||
|
||||
return {
|
||||
mock: {
|
||||
...mock,
|
||||
http: {
|
||||
...mock.http,
|
||||
createRouter,
|
||||
},
|
||||
},
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
createRouter,
|
||||
context: {
|
||||
measure: () => undefined,
|
||||
config$: new BehaviorSubject({} as APMConfig),
|
||||
logger: ({
|
||||
error: jest.fn(),
|
||||
} as unknown) as Logger,
|
||||
plugins: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const initApi = (params?: RouteParamsRT) => {
|
||||
const { mock, context, createRouter, get, post } = getCoreMock();
|
||||
const handlerMock = jest.fn();
|
||||
createApi()
|
||||
.add(() => ({
|
||||
endpoint: 'GET /foo',
|
||||
params,
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: handlerMock,
|
||||
}))
|
||||
.init(mock, context);
|
||||
|
||||
const routeHandler = get.mock.calls[0][1];
|
||||
|
||||
const responseMock = {
|
||||
ok: jest.fn(),
|
||||
custom: jest.fn(),
|
||||
};
|
||||
|
||||
const simulateRequest = (requestMock: any) => {
|
||||
return routeHandler(
|
||||
{},
|
||||
{
|
||||
// stub default values
|
||||
params: {},
|
||||
query: {},
|
||||
body: null,
|
||||
...requestMock,
|
||||
},
|
||||
responseMock
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
simulateRequest,
|
||||
handlerMock,
|
||||
createRouter,
|
||||
get,
|
||||
post,
|
||||
responseMock,
|
||||
};
|
||||
};
|
||||
|
||||
describe('createApi', () => {
|
||||
it('registers a route with the server', () => {
|
||||
const { mock, context, createRouter, post, get, put } = getCoreMock();
|
||||
|
||||
createApi()
|
||||
.add(() => ({
|
||||
endpoint: 'GET /foo',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add(() => ({
|
||||
endpoint: 'POST /bar',
|
||||
params: t.type({
|
||||
body: t.string,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add(() => ({
|
||||
endpoint: 'PUT /baz',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => ({}),
|
||||
}))
|
||||
.add({
|
||||
endpoint: 'GET /qux',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => ({}),
|
||||
})
|
||||
.init(mock, context);
|
||||
|
||||
expect(createRouter).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(get).toHaveBeenCalledTimes(2);
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect(put).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(get.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
path: '/foo',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(get.mock.calls[1][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
path: '/qux',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(post.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
path: '/bar',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(put.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
path: '/baz',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validating', () => {
|
||||
describe('_inspect', () => {
|
||||
it('allows _inspect=true', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi();
|
||||
await simulateRequest({ query: { _inspect: 'true' } });
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
expect(params).toEqual({ query: { _inspect: true } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// responds with ok
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(responseMock.ok).toHaveBeenCalledWith({
|
||||
body: { _inspect: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects _inspect=1', async () => {
|
||||
const { simulateRequest, responseMock } = initApi();
|
||||
await simulateRequest({ query: { _inspect: 1 } });
|
||||
|
||||
// responds with error handler
|
||||
expect(responseMock.ok).not.toHaveBeenCalled();
|
||||
expect(responseMock.custom).toHaveBeenCalledWith({
|
||||
body: {
|
||||
attributes: { _inspect: [] },
|
||||
message:
|
||||
'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
|
||||
},
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows omitting _inspect', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi();
|
||||
await simulateRequest({ query: {} });
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
expect(params).toEqual({ query: { _inspect: false } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// responds with ok
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(responseMock.ok).toHaveBeenCalledWith({ body: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if unknown parameters are provided', async () => {
|
||||
const { simulateRequest, responseMock } = initApi();
|
||||
|
||||
await simulateRequest({
|
||||
query: { _inspect: true, extra: '' },
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulateRequest({
|
||||
body: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates path parameters', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
path: t.type({
|
||||
foo: t.string,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
|
||||
expect(params).toEqual({
|
||||
path: {
|
||||
foo: 'bar',
|
||||
},
|
||||
query: {
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
params: {
|
||||
bar: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 9,
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulateRequest({
|
||||
params: {
|
||||
foo: 'bar',
|
||||
extra: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates body parameters', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
body: t.string,
|
||||
})
|
||||
);
|
||||
|
||||
await simulateRequest({
|
||||
body: '',
|
||||
});
|
||||
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
|
||||
expect(params).toEqual({
|
||||
body: '',
|
||||
query: {
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
body: null,
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('validates query parameters', async () => {
|
||||
const { simulateRequest, handlerMock, responseMock } = initApi(
|
||||
t.type({
|
||||
query: t.type({
|
||||
bar: t.string,
|
||||
filterNames: jsonRt.pipe(t.array(t.string)),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await simulateRequest({
|
||||
query: {
|
||||
bar: '',
|
||||
_inspect: 'true',
|
||||
filterNames: JSON.stringify(['hostName', 'agentName']),
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(responseMock.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].context.params;
|
||||
|
||||
expect(params).toEqual({
|
||||
query: {
|
||||
bar: '',
|
||||
_inspect: true,
|
||||
filterNames: ['hostName', 'agentName'],
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
query: {
|
||||
bar: '',
|
||||
foo: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseMock.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,185 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash';
|
||||
import Boom from '@hapi/boom';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import * as t from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { KibanaRequest, RouteRegistrar } from 'src/core/server';
|
||||
import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
|
||||
import agent from 'elastic-apm-node';
|
||||
import { parseMethod } from '../../../common/apm_api/parse_endpoint';
|
||||
import { merge } from '../../../common/runtime_types/merge';
|
||||
import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt';
|
||||
import { APMConfig } from '../..';
|
||||
import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings';
|
||||
import { jsonRt } from '../../../common/runtime_types/json_rt';
|
||||
import type { ApmPluginRequestHandlerContext } from '../typings';
|
||||
|
||||
const inspectRt = t.exact(
|
||||
t.partial({
|
||||
query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
|
||||
})
|
||||
);
|
||||
|
||||
type RouteOrRouteFactoryFn = Parameters<ServerAPI<{}>['add']>[0];
|
||||
|
||||
const isNotEmpty = (val: any) =>
|
||||
val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val));
|
||||
|
||||
export const inspectableEsQueriesMap = new WeakMap<
|
||||
KibanaRequest,
|
||||
InspectResponse
|
||||
>();
|
||||
|
||||
export function createApi() {
|
||||
const routes: RouteOrRouteFactoryFn[] = [];
|
||||
const api: ServerAPI<{}> = {
|
||||
_S: {},
|
||||
add(route) {
|
||||
routes.push((route as unknown) as RouteOrRouteFactoryFn);
|
||||
return this as any;
|
||||
},
|
||||
init(core, { config$, logger, plugins }) {
|
||||
const router = core.http.createRouter();
|
||||
|
||||
let config = {} as APMConfig;
|
||||
|
||||
config$.subscribe((val) => {
|
||||
config = val;
|
||||
});
|
||||
|
||||
routes.forEach((routeOrFactoryFn) => {
|
||||
const route =
|
||||
typeof routeOrFactoryFn === 'function'
|
||||
? routeOrFactoryFn(core)
|
||||
: routeOrFactoryFn;
|
||||
|
||||
const { params, endpoint, options, handler } = route;
|
||||
|
||||
const [method, path] = endpoint.split(' ');
|
||||
const typedRouterMethod = parseMethod(method);
|
||||
|
||||
// For all runtime types with props, we create an exact
|
||||
// version that will strip all keys that are unvalidated.
|
||||
const anyObject = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
(router[typedRouterMethod] as RouteRegistrar<
|
||||
typeof typedRouterMethod,
|
||||
ApmPluginRequestHandlerContext
|
||||
>)(
|
||||
{
|
||||
path,
|
||||
options,
|
||||
validate: {
|
||||
// `body` can be null, but `validate` expects non-nullable types
|
||||
// if any validation is defined. Not having validation currently
|
||||
// means we don't get the payload. See
|
||||
// https://github.com/elastic/kibana/issues/50179
|
||||
body: schema.nullable(anyObject),
|
||||
params: anyObject,
|
||||
query: anyObject,
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
if (agent.isStarted()) {
|
||||
agent.addLabels({
|
||||
plugin: 'apm',
|
||||
});
|
||||
}
|
||||
|
||||
// init debug queries
|
||||
inspectableEsQueriesMap.set(request, []);
|
||||
|
||||
try {
|
||||
const validParams = validateParams(request, params);
|
||||
const data = await handler({
|
||||
request,
|
||||
context: {
|
||||
...context,
|
||||
plugins,
|
||||
params: validParams,
|
||||
config,
|
||||
logger,
|
||||
},
|
||||
});
|
||||
|
||||
const body = { ...data };
|
||||
if (validParams.query._inspect) {
|
||||
body._inspect = inspectableEsQueriesMap.get(request);
|
||||
}
|
||||
|
||||
// cleanup
|
||||
inspectableEsQueriesMap.delete(request);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
const opts = {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: error.message,
|
||||
attributes: {
|
||||
_inspect: inspectableEsQueriesMap.get(request),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
opts.statusCode = error.output.statusCode;
|
||||
}
|
||||
|
||||
if (error instanceof RequestAbortedError) {
|
||||
opts.statusCode = 499;
|
||||
opts.body.message = 'Client closed request';
|
||||
}
|
||||
|
||||
return response.custom(opts);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function validateParams(
|
||||
request: KibanaRequest,
|
||||
params: RouteParamsRT | undefined
|
||||
) {
|
||||
const paramsRt = params ? merge([params, inspectRt]) : inspectRt;
|
||||
const paramMap = pickBy(
|
||||
{
|
||||
path: request.params,
|
||||
body: request.body,
|
||||
query: {
|
||||
_inspect: 'false',
|
||||
// @ts-ignore
|
||||
...request.query,
|
||||
},
|
||||
},
|
||||
isNotEmpty
|
||||
);
|
||||
|
||||
const result = strictKeysRt(paramsRt).decode(paramMap);
|
||||
|
||||
if (isLeft(result)) {
|
||||
throw Boom.badRequest(PathReporter.report(result)[0]);
|
||||
}
|
||||
|
||||
// Only return values for parameters that have runtime types,
|
||||
// but always include query as _inspect is always set even if
|
||||
// it's not defined in the route.
|
||||
return mergeLodash(
|
||||
{ query: { _inspect: false } },
|
||||
pickBy(result.right, isNotEmpty)
|
||||
);
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
staticIndexPatternRoute,
|
||||
dynamicIndexPatternRoute,
|
||||
apmIndexPatternTitleRoute,
|
||||
} from './index_pattern';
|
||||
import { createApi } from './create_api';
|
||||
import { environmentsRoute } from './environments';
|
||||
import {
|
||||
errorDistributionRoute,
|
||||
errorGroupsRoute,
|
||||
errorsRoute,
|
||||
} from './errors';
|
||||
import {
|
||||
serviceAgentNameRoute,
|
||||
serviceTransactionTypesRoute,
|
||||
servicesRoute,
|
||||
serviceNodeMetadataRoute,
|
||||
serviceAnnotationsRoute,
|
||||
serviceAnnotationsCreateRoute,
|
||||
serviceErrorGroupsPrimaryStatisticsRoute,
|
||||
serviceErrorGroupsComparisonStatisticsRoute,
|
||||
serviceThroughputRoute,
|
||||
serviceDependenciesRoute,
|
||||
serviceMetadataDetailsRoute,
|
||||
serviceMetadataIconsRoute,
|
||||
serviceInstancesPrimaryStatisticsRoute,
|
||||
serviceInstancesComparisonStatisticsRoute,
|
||||
serviceProfilingStatisticsRoute,
|
||||
serviceProfilingTimelineRoute,
|
||||
} from './services';
|
||||
import {
|
||||
agentConfigurationRoute,
|
||||
getSingleAgentConfigurationRoute,
|
||||
agentConfigurationSearchRoute,
|
||||
deleteAgentConfigurationRoute,
|
||||
listAgentConfigurationEnvironmentsRoute,
|
||||
listAgentConfigurationServicesRoute,
|
||||
createOrUpdateAgentConfigurationRoute,
|
||||
agentConfigurationAgentNameRoute,
|
||||
} from './settings/agent_configuration';
|
||||
import {
|
||||
apmIndexSettingsRoute,
|
||||
apmIndicesRoute,
|
||||
saveApmIndicesRoute,
|
||||
} from './settings/apm_indices';
|
||||
import { metricsChartsRoute } from './metrics';
|
||||
import { serviceNodesRoute } from './service_nodes';
|
||||
import {
|
||||
tracesRoute,
|
||||
tracesByIdRoute,
|
||||
rootTransactionByTraceIdRoute,
|
||||
} from './traces';
|
||||
import {
|
||||
correlationsLatencyDistributionRoute,
|
||||
correlationsForSlowTransactionsRoute,
|
||||
correlationsErrorDistributionRoute,
|
||||
correlationsForFailedTransactionsRoute,
|
||||
} from './correlations';
|
||||
import {
|
||||
transactionChartsBreakdownRoute,
|
||||
transactionChartsDistributionRoute,
|
||||
transactionChartsErrorRateRoute,
|
||||
transactionGroupsRoute,
|
||||
transactionGroupsPrimaryStatisticsRoute,
|
||||
transactionLatencyChartsRoute,
|
||||
transactionThroughputChartsRoute,
|
||||
transactionGroupsComparisonStatisticsRoute,
|
||||
} from './transactions';
|
||||
import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
|
||||
import {
|
||||
createCustomLinkRoute,
|
||||
updateCustomLinkRoute,
|
||||
deleteCustomLinkRoute,
|
||||
listCustomLinksRoute,
|
||||
customLinkTransactionRoute,
|
||||
} from './settings/custom_link';
|
||||
import {
|
||||
observabilityOverviewHasDataRoute,
|
||||
observabilityOverviewRoute,
|
||||
} from './observability_overview';
|
||||
import {
|
||||
anomalyDetectionJobsRoute,
|
||||
createAnomalyDetectionJobsRoute,
|
||||
anomalyDetectionEnvironmentsRoute,
|
||||
} from './settings/anomaly_detection';
|
||||
import {
|
||||
rumHasDataRoute,
|
||||
rumClientMetricsRoute,
|
||||
rumJSErrors,
|
||||
rumLongTaskMetrics,
|
||||
rumOverviewLocalFiltersRoute,
|
||||
rumPageLoadDistBreakdownRoute,
|
||||
rumPageLoadDistributionRoute,
|
||||
rumPageViewsTrendRoute,
|
||||
rumServicesRoute,
|
||||
rumUrlSearch,
|
||||
rumVisitorsBreakdownRoute,
|
||||
rumWebCoreVitals,
|
||||
} from './rum_client';
|
||||
import {
|
||||
transactionErrorRateChartPreview,
|
||||
transactionErrorCountChartPreview,
|
||||
transactionDurationChartPreview,
|
||||
} from './alerts/chart_preview';
|
||||
|
||||
const createApmApi = () => {
|
||||
const api = createApi()
|
||||
// index pattern
|
||||
.add(staticIndexPatternRoute)
|
||||
.add(dynamicIndexPatternRoute)
|
||||
.add(apmIndexPatternTitleRoute)
|
||||
|
||||
// Environments
|
||||
.add(environmentsRoute)
|
||||
|
||||
// Errors
|
||||
.add(errorDistributionRoute)
|
||||
.add(errorGroupsRoute)
|
||||
.add(errorsRoute)
|
||||
|
||||
// Services
|
||||
.add(serviceAgentNameRoute)
|
||||
.add(serviceTransactionTypesRoute)
|
||||
.add(servicesRoute)
|
||||
.add(serviceNodeMetadataRoute)
|
||||
.add(serviceAnnotationsRoute)
|
||||
.add(serviceAnnotationsCreateRoute)
|
||||
.add(serviceErrorGroupsPrimaryStatisticsRoute)
|
||||
.add(serviceThroughputRoute)
|
||||
.add(serviceDependenciesRoute)
|
||||
.add(serviceMetadataDetailsRoute)
|
||||
.add(serviceMetadataIconsRoute)
|
||||
.add(serviceInstancesPrimaryStatisticsRoute)
|
||||
.add(serviceInstancesComparisonStatisticsRoute)
|
||||
.add(serviceErrorGroupsComparisonStatisticsRoute)
|
||||
.add(serviceProfilingTimelineRoute)
|
||||
.add(serviceProfilingStatisticsRoute)
|
||||
|
||||
// Agent configuration
|
||||
.add(getSingleAgentConfigurationRoute)
|
||||
.add(agentConfigurationAgentNameRoute)
|
||||
.add(agentConfigurationRoute)
|
||||
.add(agentConfigurationSearchRoute)
|
||||
.add(deleteAgentConfigurationRoute)
|
||||
.add(listAgentConfigurationEnvironmentsRoute)
|
||||
.add(listAgentConfigurationServicesRoute)
|
||||
.add(createOrUpdateAgentConfigurationRoute)
|
||||
|
||||
// Correlations
|
||||
.add(correlationsLatencyDistributionRoute)
|
||||
.add(correlationsForSlowTransactionsRoute)
|
||||
.add(correlationsErrorDistributionRoute)
|
||||
.add(correlationsForFailedTransactionsRoute)
|
||||
|
||||
// APM indices
|
||||
.add(apmIndexSettingsRoute)
|
||||
.add(apmIndicesRoute)
|
||||
.add(saveApmIndicesRoute)
|
||||
|
||||
// Metrics
|
||||
.add(metricsChartsRoute)
|
||||
.add(serviceNodesRoute)
|
||||
|
||||
// Traces
|
||||
.add(tracesRoute)
|
||||
.add(tracesByIdRoute)
|
||||
.add(rootTransactionByTraceIdRoute)
|
||||
|
||||
// Transactions
|
||||
.add(transactionChartsBreakdownRoute)
|
||||
.add(transactionChartsDistributionRoute)
|
||||
.add(transactionChartsErrorRateRoute)
|
||||
.add(transactionGroupsRoute)
|
||||
.add(transactionGroupsPrimaryStatisticsRoute)
|
||||
.add(transactionLatencyChartsRoute)
|
||||
.add(transactionThroughputChartsRoute)
|
||||
.add(transactionGroupsComparisonStatisticsRoute)
|
||||
|
||||
// Service map
|
||||
.add(serviceMapRoute)
|
||||
.add(serviceMapServiceNodeRoute)
|
||||
|
||||
// Custom links
|
||||
.add(createCustomLinkRoute)
|
||||
.add(updateCustomLinkRoute)
|
||||
.add(deleteCustomLinkRoute)
|
||||
.add(listCustomLinksRoute)
|
||||
.add(customLinkTransactionRoute)
|
||||
|
||||
// Observability dashboard
|
||||
.add(observabilityOverviewHasDataRoute)
|
||||
.add(observabilityOverviewRoute)
|
||||
|
||||
// Anomaly detection
|
||||
.add(anomalyDetectionJobsRoute)
|
||||
.add(createAnomalyDetectionJobsRoute)
|
||||
.add(anomalyDetectionEnvironmentsRoute)
|
||||
|
||||
// User Experience app api routes
|
||||
.add(rumOverviewLocalFiltersRoute)
|
||||
.add(rumPageViewsTrendRoute)
|
||||
.add(rumPageLoadDistributionRoute)
|
||||
.add(rumPageLoadDistBreakdownRoute)
|
||||
.add(rumClientMetricsRoute)
|
||||
.add(rumServicesRoute)
|
||||
.add(rumVisitorsBreakdownRoute)
|
||||
.add(rumWebCoreVitals)
|
||||
.add(rumJSErrors)
|
||||
.add(rumUrlSearch)
|
||||
.add(rumLongTaskMetrics)
|
||||
.add(rumHasDataRoute)
|
||||
|
||||
// Alerting
|
||||
.add(transactionErrorCountChartPreview)
|
||||
.add(transactionDurationChartPreview)
|
||||
.add(transactionErrorRateChartPreview);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
export type APMAPI = ReturnType<typeof createApmApi>;
|
||||
|
||||
export { createApmApi };
|
13
x-pack/plugins/apm/server/routes/create_apm_server_route.ts
Normal file
13
x-pack/plugins/apm/server/routes/create_apm_server_route.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { createServerRouteFactory } from '@kbn/server-route-repository';
|
||||
import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
|
||||
|
||||
export const createApmServerRoute = createServerRouteFactory<
|
||||
APMRouteHandlerResources,
|
||||
APMRouteCreateOptions
|
||||
>();
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { createServerRouteRepository } from '@kbn/server-route-repository';
|
||||
import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
|
||||
|
||||
export function createApmServerRouteRepository() {
|
||||
return createServerRouteRepository<
|
||||
APMRouteHandlerResources,
|
||||
APMRouteCreateOptions
|
||||
>();
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { HandlerReturn, Route, RouteParamsRT } from './typings';
|
||||
|
||||
export function createRoute<
|
||||
TEndpoint extends string,
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
): Route<TEndpoint, TRouteParamsRT, TReturn>;
|
||||
|
||||
export function createRoute<
|
||||
TEndpoint extends string,
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route: (core: CoreSetup) => Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
): (core: CoreSetup) => Route<TEndpoint, TRouteParamsRT, TReturn>;
|
||||
|
||||
export function createRoute(routeOrFactoryFn: Function | object) {
|
||||
return routeOrFactoryFn;
|
||||
}
|
|
@ -9,10 +9,11 @@ import * as t from 'io-ts';
|
|||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getEnvironments } from '../lib/environments/get_environments';
|
||||
import { createRoute } from './create_route';
|
||||
import { rangeRt } from './default_api_types';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
|
||||
export const environmentsRoute = createRoute({
|
||||
const environmentsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/environments',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -23,9 +24,10 @@ export const environmentsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -39,3 +41,7 @@ export const environmentsRoute = createRoute({
|
|||
return { environments };
|
||||
},
|
||||
});
|
||||
|
||||
export const environmentsRouteRepository = createApmServerRouteRepository().add(
|
||||
environmentsRoute
|
||||
);
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { getErrorDistribution } from '../lib/errors/distribution/get_distribution';
|
||||
import { getErrorGroupSample } from '../lib/errors/get_error_group_sample';
|
||||
import { getErrorGroups } from '../lib/errors/get_error_groups';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
|
||||
export const errorsRoute = createRoute({
|
||||
const errorsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/errors',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -30,9 +31,9 @@ export const errorsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { params } = context;
|
||||
handler: async (resources) => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const { serviceName } = params.path;
|
||||
const { environment, kuery, sortField, sortDirection } = params.query;
|
||||
|
||||
|
@ -49,7 +50,7 @@ export const errorsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const errorGroupsRoute = createRoute({
|
||||
const errorGroupsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -59,10 +60,11 @@ export const errorGroupsRoute = createRoute({
|
|||
query: t.intersection([environmentRt, kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName, groupId } = context.params.path;
|
||||
const { environment, kuery } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const { serviceName, groupId } = params.path;
|
||||
const { environment, kuery } = params.query;
|
||||
|
||||
return getErrorGroupSample({
|
||||
environment,
|
||||
|
@ -74,7 +76,7 @@ export const errorGroupsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const errorDistributionRoute = createRoute({
|
||||
const errorDistributionRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -90,9 +92,9 @@ export const errorDistributionRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { params } = context;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { environment, kuery, groupId } = params.query;
|
||||
return getErrorDistribution({
|
||||
|
@ -104,3 +106,8 @@ export const errorDistributionRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const errorsRouteRepository = createApmServerRouteRepository()
|
||||
.add(errorsRoute)
|
||||
.add(errorGroupsRoute)
|
||||
.add(errorDistributionRoute);
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 type {
|
||||
ServerRouteRepository,
|
||||
ReturnOf,
|
||||
EndpointOf,
|
||||
} from '@kbn/server-route-repository';
|
||||
import { PickByValue } from 'utility-types';
|
||||
import { alertsChartPreviewRouteRepository } from './alerts/chart_preview';
|
||||
import { correlationsRouteRepository } from './correlations';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { environmentsRouteRepository } from './environments';
|
||||
import { errorsRouteRepository } from './errors';
|
||||
import { indexPatternRouteRepository } from './index_pattern';
|
||||
import { metricsRouteRepository } from './metrics';
|
||||
import { observabilityOverviewRouteRepository } from './observability_overview';
|
||||
import { rumRouteRepository } from './rum_client';
|
||||
import { serviceRouteRepository } from './services';
|
||||
import { serviceMapRouteRepository } from './service_map';
|
||||
import { serviceNodeRouteRepository } from './service_nodes';
|
||||
import { agentConfigurationRouteRepository } from './settings/agent_configuration';
|
||||
import { anomalyDetectionRouteRepository } from './settings/anomaly_detection';
|
||||
import { apmIndicesRouteRepository } from './settings/apm_indices';
|
||||
import { customLinkRouteRepository } from './settings/custom_link';
|
||||
import { traceRouteRepository } from './traces';
|
||||
import { transactionRouteRepository } from './transactions';
|
||||
import { APMRouteHandlerResources } from './typings';
|
||||
|
||||
const getTypedGlobalApmServerRouteRepository = () => {
|
||||
const repository = createApmServerRouteRepository()
|
||||
.merge(indexPatternRouteRepository)
|
||||
.merge(environmentsRouteRepository)
|
||||
.merge(errorsRouteRepository)
|
||||
.merge(metricsRouteRepository)
|
||||
.merge(observabilityOverviewRouteRepository)
|
||||
.merge(rumRouteRepository)
|
||||
.merge(serviceMapRouteRepository)
|
||||
.merge(serviceNodeRouteRepository)
|
||||
.merge(serviceRouteRepository)
|
||||
.merge(traceRouteRepository)
|
||||
.merge(transactionRouteRepository)
|
||||
.merge(alertsChartPreviewRouteRepository)
|
||||
.merge(correlationsRouteRepository)
|
||||
.merge(agentConfigurationRouteRepository)
|
||||
.merge(anomalyDetectionRouteRepository)
|
||||
.merge(apmIndicesRouteRepository)
|
||||
.merge(customLinkRouteRepository);
|
||||
|
||||
return repository;
|
||||
};
|
||||
|
||||
const getGlobalApmServerRouteRepository = () => {
|
||||
return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository<APMRouteHandlerResources>;
|
||||
};
|
||||
|
||||
export type APMServerRouteRepository = ReturnType<
|
||||
typeof getTypedGlobalApmServerRouteRepository
|
||||
>;
|
||||
|
||||
// Ensure no APIs return arrays (or, by proxy, the any type),
|
||||
// to guarantee compatibility with _inspect.
|
||||
|
||||
type CompositeEndpoint = EndpointOf<APMServerRouteRepository>;
|
||||
|
||||
type EndpointReturnTypes = {
|
||||
[Endpoint in CompositeEndpoint]: ReturnOf<APMServerRouteRepository, Endpoint>;
|
||||
};
|
||||
|
||||
type ArrayLikeReturnTypes = PickByValue<EndpointReturnTypes, any[]>;
|
||||
|
||||
type ViolatingEndpoints = keyof ArrayLikeReturnTypes;
|
||||
|
||||
function assertType<T = never, U extends T = never>() {}
|
||||
|
||||
// if any endpoint has an array-like return type, the assertion below will fail
|
||||
assertType<never, ViolatingEndpoints>();
|
||||
|
||||
export { getGlobalApmServerRouteRepository };
|
|
@ -6,49 +6,67 @@
|
|||
*/
|
||||
|
||||
import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
|
||||
import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title';
|
||||
import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
|
||||
export const staticIndexPatternRoute = createRoute((core) => ({
|
||||
const staticIndexPatternRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/index_pattern/static',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const {
|
||||
request,
|
||||
core,
|
||||
plugins: { spaces },
|
||||
config,
|
||||
} = resources;
|
||||
|
||||
const [setup, savedObjectsClient] = await Promise.all([
|
||||
setupRequest(context, request),
|
||||
getInternalSavedObjectsClient(core),
|
||||
setupRequest(resources),
|
||||
core
|
||||
.start()
|
||||
.then((coreStart) => coreStart.savedObjects.createInternalRepository()),
|
||||
]);
|
||||
|
||||
const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request);
|
||||
const spaceId = spaces?.setup.spacesService.getSpaceId(request);
|
||||
|
||||
const didCreateIndexPattern = await createStaticIndexPattern(
|
||||
setup,
|
||||
context,
|
||||
config,
|
||||
savedObjectsClient,
|
||||
spaceId
|
||||
);
|
||||
|
||||
return { created: didCreateIndexPattern };
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
export const dynamicIndexPatternRoute = createRoute({
|
||||
const dynamicIndexPatternRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/index_pattern/dynamic',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
const dynamicIndexPattern = await getDynamicIndexPattern({ context });
|
||||
handler: async ({ context, config, logger }) => {
|
||||
const dynamicIndexPattern = await getDynamicIndexPattern({
|
||||
context,
|
||||
config,
|
||||
logger,
|
||||
});
|
||||
return { dynamicIndexPattern };
|
||||
},
|
||||
});
|
||||
|
||||
export const apmIndexPatternTitleRoute = createRoute({
|
||||
const indexPatternTitleRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/index_pattern/title',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
handler: async ({ config }) => {
|
||||
return {
|
||||
indexPatternTitle: getApmIndexPatternTitle(context),
|
||||
indexPatternTitle: getApmIndexPatternTitle(config),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const indexPatternRouteRepository = createApmServerRouteRepository()
|
||||
.add(staticIndexPatternRoute)
|
||||
.add(dynamicIndexPatternRoute)
|
||||
.add(indexPatternTitleRoute);
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import * as t from 'io-ts';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
|
||||
|
||||
export const metricsChartsRoute = createRoute({
|
||||
const metricsChartsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -30,9 +31,9 @@ export const metricsChartsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { params } = context;
|
||||
handler: async (resources) => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const { serviceName } = params.path;
|
||||
const { agentName, environment, kuery, serviceNodeName } = params.query;
|
||||
return await getMetricsChartDataByAgent({
|
||||
|
@ -45,3 +46,7 @@ export const metricsChartsRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const metricsRouteRepository = createApmServerRouteRepository().add(
|
||||
metricsChartsRoute
|
||||
);
|
||||
|
|
|
@ -10,30 +10,32 @@ import { setupRequest } from '../lib/helpers/setup_request';
|
|||
import { getServiceCount } from '../lib/observability_overview/get_service_count';
|
||||
import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute';
|
||||
import { getHasData } from '../lib/observability_overview/has_data';
|
||||
import { createRoute } from './create_route';
|
||||
import { rangeRt } from './default_api_types';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { withApmSpan } from '../utils/with_apm_span';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
|
||||
export const observabilityOverviewHasDataRoute = createRoute({
|
||||
const observabilityOverviewHasDataRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_data',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const res = await getHasData({ setup });
|
||||
return { hasData: res };
|
||||
},
|
||||
});
|
||||
|
||||
export const observabilityOverviewRoute = createRoute({
|
||||
const observabilityOverviewRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/observability_overview',
|
||||
params: t.type({
|
||||
query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { bucketSize } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { bucketSize } = resources.params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -54,3 +56,7 @@ export const observabilityOverviewRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const observabilityOverviewRouteRepository = createApmServerRouteRepository()
|
||||
.add(observabilityOverviewRoute)
|
||||
.add(observabilityOverviewHasDataRoute);
|
||||
|
|
507
x-pack/plugins/apm/server/routes/register_routes/index.test.ts
Normal file
507
x-pack/plugins/apm/server/routes/register_routes/index.test.ts
Normal file
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
* 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 { jsonRt } from '@kbn/io-ts-utils';
|
||||
import { createServerRouteRepository } from '@kbn/server-route-repository';
|
||||
import { ServerRoute } from '@kbn/server-route-repository/target/typings';
|
||||
import * as t from 'io-ts';
|
||||
import { CoreSetup, Logger } from 'src/core/server';
|
||||
import { APMConfig } from '../..';
|
||||
import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings';
|
||||
import { registerRoutes } from './index';
|
||||
|
||||
type RegisterRouteDependencies = Parameters<typeof registerRoutes>[0];
|
||||
|
||||
const getRegisterRouteDependencies = () => {
|
||||
const get = jest.fn();
|
||||
const post = jest.fn();
|
||||
const put = jest.fn();
|
||||
const createRouter = jest.fn().mockReturnValue({
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
});
|
||||
|
||||
const coreSetup = ({
|
||||
http: {
|
||||
createRouter,
|
||||
},
|
||||
} as unknown) as CoreSetup;
|
||||
|
||||
const logger = ({
|
||||
error: jest.fn(),
|
||||
} as unknown) as Logger;
|
||||
|
||||
return {
|
||||
mocks: {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
createRouter,
|
||||
coreSetup,
|
||||
logger,
|
||||
},
|
||||
dependencies: ({
|
||||
core: {
|
||||
setup: coreSetup,
|
||||
},
|
||||
logger,
|
||||
config: {} as APMConfig,
|
||||
plugins: {},
|
||||
} as unknown) as RegisterRouteDependencies,
|
||||
};
|
||||
};
|
||||
|
||||
const getRepository = () =>
|
||||
createServerRouteRepository<
|
||||
APMRouteHandlerResources,
|
||||
APMRouteCreateOptions
|
||||
>();
|
||||
|
||||
const initApi = (
|
||||
routes: Array<
|
||||
ServerRoute<
|
||||
any,
|
||||
t.Any,
|
||||
APMRouteHandlerResources,
|
||||
any,
|
||||
APMRouteCreateOptions
|
||||
>
|
||||
>
|
||||
) => {
|
||||
const { mocks, dependencies } = getRegisterRouteDependencies();
|
||||
|
||||
let repository = getRepository();
|
||||
|
||||
routes.forEach((route) => {
|
||||
repository = repository.add(route);
|
||||
});
|
||||
|
||||
registerRoutes({
|
||||
...dependencies,
|
||||
repository,
|
||||
});
|
||||
|
||||
const responseMock = {
|
||||
ok: jest.fn(),
|
||||
custom: jest.fn(),
|
||||
};
|
||||
|
||||
const simulateRequest = (request: {
|
||||
method: 'get' | 'post' | 'put';
|
||||
pathname: string;
|
||||
params?: Record<string, unknown>;
|
||||
body?: unknown;
|
||||
query?: Record<string, unknown>;
|
||||
}) => {
|
||||
const [, registeredRouteHandler] =
|
||||
mocks[request.method].mock.calls.find((call) => {
|
||||
return call[0].path === request.pathname;
|
||||
}) ?? [];
|
||||
|
||||
const result = registeredRouteHandler(
|
||||
{},
|
||||
{
|
||||
params: {},
|
||||
query: {},
|
||||
body: null,
|
||||
...request,
|
||||
},
|
||||
responseMock
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
simulateRequest,
|
||||
mocks: {
|
||||
...mocks,
|
||||
response: responseMock,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('createApi', () => {
|
||||
it('registers a route with the server', () => {
|
||||
const {
|
||||
mocks: { createRouter, get, post, put },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => ({}),
|
||||
},
|
||||
{
|
||||
endpoint: 'POST /bar',
|
||||
params: t.type({
|
||||
body: t.string,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async () => ({}),
|
||||
},
|
||||
{
|
||||
endpoint: 'PUT /baz',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => ({}),
|
||||
},
|
||||
{
|
||||
endpoint: 'GET /qux',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async () => ({}),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(createRouter).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(get).toHaveBeenCalledTimes(2);
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect(put).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(get.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
path: '/foo',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(get.mock.calls[1][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
path: '/qux',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(post.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
path: '/bar',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
|
||||
expect(put.mock.calls[0][0]).toEqual({
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
path: '/baz',
|
||||
validate: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validating', () => {
|
||||
describe('_inspect', () => {
|
||||
it('allows _inspect=true', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
handler: handlerMock,
|
||||
},
|
||||
]);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: { _inspect: 'true' },
|
||||
});
|
||||
|
||||
// responds with ok
|
||||
expect(response.custom).not.toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].params;
|
||||
expect(params).toEqual({ query: { _inspect: true } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(response.ok).toHaveBeenCalledWith({
|
||||
body: { _inspect: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects _inspect=1', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
handler: handlerMock,
|
||||
},
|
||||
]);
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: { _inspect: 1 },
|
||||
});
|
||||
|
||||
// responds with error handler
|
||||
expect(response.ok).not.toHaveBeenCalled();
|
||||
expect(response.custom).toHaveBeenCalledWith({
|
||||
body: {
|
||||
attributes: { _inspect: [] },
|
||||
message:
|
||||
'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
|
||||
},
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows omitting _inspect', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{ endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock },
|
||||
]);
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: {},
|
||||
});
|
||||
|
||||
// responds with ok
|
||||
expect(response.custom).not.toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].params;
|
||||
expect(params).toEqual({ query: { _inspect: false } });
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(response.ok).toHaveBeenCalledWith({ body: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if unknown parameters are provided', async () => {
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{ endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() },
|
||||
]);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: { _inspect: 'true', extra: '' },
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
body: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates path parameters', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: { tags: [] },
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
foo: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: handlerMock,
|
||||
},
|
||||
]);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(response.ok).toHaveBeenCalledTimes(1);
|
||||
expect(response.custom).not.toHaveBeenCalled();
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].params;
|
||||
|
||||
expect(params).toEqual({
|
||||
path: {
|
||||
foo: 'bar',
|
||||
},
|
||||
query: {
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
params: {
|
||||
bar: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(1);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
params: {
|
||||
foo: 9,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(2);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
params: {
|
||||
foo: 'bar',
|
||||
extra: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('validates body parameters', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
params: t.type({
|
||||
body: t.string,
|
||||
}),
|
||||
handler: handlerMock,
|
||||
},
|
||||
]);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
body: '',
|
||||
});
|
||||
|
||||
expect(response.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(response.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].params;
|
||||
|
||||
expect(params).toEqual({
|
||||
body: '',
|
||||
query: {
|
||||
_inspect: false,
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
body: null,
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('validates query parameters', async () => {
|
||||
const handlerMock = jest.fn();
|
||||
const {
|
||||
simulateRequest,
|
||||
mocks: { response },
|
||||
} = initApi([
|
||||
{
|
||||
endpoint: 'GET /foo',
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
bar: t.string,
|
||||
filterNames: jsonRt.pipe(t.array(t.string)),
|
||||
}),
|
||||
}),
|
||||
handler: handlerMock,
|
||||
},
|
||||
]);
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: {
|
||||
bar: '',
|
||||
_inspect: 'true',
|
||||
filterNames: JSON.stringify(['hostName', 'agentName']),
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).not.toHaveBeenCalled();
|
||||
expect(handlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(response.ok).toHaveBeenCalledTimes(1);
|
||||
|
||||
const params = handlerMock.mock.calls[0][0].params;
|
||||
|
||||
expect(params).toEqual({
|
||||
query: {
|
||||
bar: '',
|
||||
_inspect: true,
|
||||
filterNames: ['hostName', 'agentName'],
|
||||
},
|
||||
});
|
||||
|
||||
await simulateRequest({
|
||||
method: 'get',
|
||||
pathname: '/foo',
|
||||
query: {
|
||||
bar: '',
|
||||
foo: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.custom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
143
x-pack/plugins/apm/server/routes/register_routes/index.ts
Normal file
143
x-pack/plugins/apm/server/routes/register_routes/index.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import * as t from 'io-ts';
|
||||
import { KibanaRequest, RouteRegistrar } from 'src/core/server';
|
||||
import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
|
||||
import agent from 'elastic-apm-node';
|
||||
import { ServerRouteRepository } from '@kbn/server-route-repository';
|
||||
import { merge } from 'lodash';
|
||||
import {
|
||||
decodeRequestParams,
|
||||
parseEndpoint,
|
||||
routeValidationObject,
|
||||
} from '@kbn/server-route-repository';
|
||||
import { mergeRt, jsonRt } from '@kbn/io-ts-utils';
|
||||
import { pickKeys } from '../../../common/utils/pick_keys';
|
||||
import { APMRouteHandlerResources, InspectResponse } from '../typings';
|
||||
import type { ApmPluginRequestHandlerContext } from '../typings';
|
||||
|
||||
const inspectRt = t.exact(
|
||||
t.partial({
|
||||
query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
|
||||
})
|
||||
);
|
||||
|
||||
export const inspectableEsQueriesMap = new WeakMap<
|
||||
KibanaRequest,
|
||||
InspectResponse
|
||||
>();
|
||||
|
||||
export function registerRoutes({
|
||||
core,
|
||||
repository,
|
||||
plugins,
|
||||
logger,
|
||||
config,
|
||||
}: {
|
||||
core: APMRouteHandlerResources['core'];
|
||||
plugins: APMRouteHandlerResources['plugins'];
|
||||
logger: APMRouteHandlerResources['logger'];
|
||||
repository: ServerRouteRepository<APMRouteHandlerResources>;
|
||||
config: APMRouteHandlerResources['config'];
|
||||
}) {
|
||||
const routes = repository.getRoutes();
|
||||
|
||||
const router = core.setup.http.createRouter();
|
||||
|
||||
routes.forEach((route) => {
|
||||
const { params, endpoint, options, handler } = route;
|
||||
|
||||
const { method, pathname } = parseEndpoint(endpoint);
|
||||
|
||||
(router[method] as RouteRegistrar<
|
||||
typeof method,
|
||||
ApmPluginRequestHandlerContext
|
||||
>)(
|
||||
{
|
||||
path: pathname,
|
||||
options,
|
||||
validate: routeValidationObject,
|
||||
},
|
||||
async (context, request, response) => {
|
||||
if (agent.isStarted()) {
|
||||
agent.addLabels({
|
||||
plugin: 'apm',
|
||||
});
|
||||
}
|
||||
|
||||
// init debug queries
|
||||
inspectableEsQueriesMap.set(request, []);
|
||||
|
||||
try {
|
||||
const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt;
|
||||
|
||||
const validatedParams = decodeRequestParams(
|
||||
pickKeys(request, 'params', 'body', 'query'),
|
||||
runtimeType
|
||||
);
|
||||
|
||||
const data: Record<string, any> | undefined | null = (await handler({
|
||||
request,
|
||||
context,
|
||||
config,
|
||||
logger,
|
||||
core,
|
||||
plugins,
|
||||
params: merge(
|
||||
{
|
||||
query: {
|
||||
_inspect: false,
|
||||
},
|
||||
},
|
||||
validatedParams
|
||||
),
|
||||
})) as any;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
throw new Error('Return type cannot be an array');
|
||||
}
|
||||
|
||||
const body = validatedParams.query?._inspect
|
||||
? {
|
||||
...data,
|
||||
_inspect: inspectableEsQueriesMap.get(request),
|
||||
}
|
||||
: { ...data };
|
||||
|
||||
// cleanup
|
||||
inspectableEsQueriesMap.delete(request);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
const opts = {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: error.message,
|
||||
attributes: {
|
||||
_inspect: inspectableEsQueriesMap.get(request),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
opts.statusCode = error.output.statusCode;
|
||||
}
|
||||
|
||||
if (error instanceof RequestAbortedError) {
|
||||
opts.statusCode = 499;
|
||||
opts.body.message = 'Client closed request';
|
||||
}
|
||||
|
||||
return response.custom(opts);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { jsonRt } from '../../common/runtime_types/json_rt';
|
||||
import { jsonRt } from '@kbn/io-ts-utils';
|
||||
import { LocalUIFilterName } from '../../common/ui_filter';
|
||||
import {
|
||||
Setup,
|
||||
|
@ -28,9 +28,10 @@ import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters
|
|||
import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config';
|
||||
import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions';
|
||||
import { Projection } from '../projections/typings';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { rangeRt } from './default_api_types';
|
||||
import { APMRequestHandlerContext } from './typings';
|
||||
import { APMRouteHandlerResources } from './typings';
|
||||
|
||||
export const percentileRangeRt = t.partial({
|
||||
minPercentile: t.string,
|
||||
|
@ -45,18 +46,18 @@ const uxQueryRt = t.intersection([
|
|||
t.partial({ urlQuery: t.string, percentile: t.string }),
|
||||
]);
|
||||
|
||||
export const rumClientMetricsRoute = createRoute({
|
||||
const rumClientMetricsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum/client-metrics',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery, percentile },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getClientMetrics({
|
||||
setup,
|
||||
|
@ -66,18 +67,18 @@ export const rumClientMetricsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumPageLoadDistributionRoute = createRoute({
|
||||
const rumPageLoadDistributionRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/page-load-distribution',
|
||||
params: t.type({
|
||||
query: t.intersection([uxQueryRt, percentileRangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { minPercentile, maxPercentile, urlQuery },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
const pageLoadDistribution = await getPageLoadDistribution({
|
||||
setup,
|
||||
|
@ -90,7 +91,7 @@ export const rumPageLoadDistributionRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumPageLoadDistBreakdownRoute = createRoute({
|
||||
const rumPageLoadDistBreakdownRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -100,12 +101,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { minPercentile, maxPercentile, breakdown, urlQuery },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
const pageLoadDistBreakdown = await getPageLoadDistBreakdown({
|
||||
setup,
|
||||
|
@ -119,18 +120,18 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumPageViewsTrendRoute = createRoute({
|
||||
const rumPageViewsTrendRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/page-view-trends',
|
||||
params: t.type({
|
||||
query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { breakdowns, urlQuery },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getPageViewTrends({
|
||||
setup,
|
||||
|
@ -140,32 +141,32 @@ export const rumPageViewsTrendRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumServicesRoute = createRoute({
|
||||
const rumServicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/services',
|
||||
params: t.type({
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const rumServices = await getRumServices({ setup });
|
||||
return { rumServices };
|
||||
},
|
||||
});
|
||||
|
||||
export const rumVisitorsBreakdownRoute = createRoute({
|
||||
const rumVisitorsBreakdownRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/visitor-breakdown',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getVisitorBreakdown({
|
||||
setup,
|
||||
|
@ -174,18 +175,18 @@ export const rumVisitorsBreakdownRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumWebCoreVitals = createRoute({
|
||||
const rumWebCoreVitals = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/web-core-vitals',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery, percentile },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getWebCoreVitals({
|
||||
setup,
|
||||
|
@ -195,18 +196,18 @@ export const rumWebCoreVitals = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumLongTaskMetrics = createRoute({
|
||||
const rumLongTaskMetrics = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/long-task-metrics',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery, percentile },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getLongTaskMetrics({
|
||||
setup,
|
||||
|
@ -216,24 +217,24 @@ export const rumLongTaskMetrics = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumUrlSearch = createRoute({
|
||||
const rumUrlSearch = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/url-search',
|
||||
params: t.type({
|
||||
query: uxQueryRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { urlQuery, percentile },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) });
|
||||
},
|
||||
});
|
||||
|
||||
export const rumJSErrors = createRoute({
|
||||
const rumJSErrors = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/rum-client/js-errors',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -244,12 +245,12 @@ export const rumJSErrors = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
query: { pageSize, pageIndex, urlQuery },
|
||||
} = context.params;
|
||||
} = resources.params;
|
||||
|
||||
return getJSErrors({
|
||||
setup,
|
||||
|
@ -260,14 +261,14 @@ export const rumJSErrors = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const rumHasDataRoute = createRoute({
|
||||
const rumHasDataRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
|
||||
params: t.type({
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
return await hasRumData({ setup });
|
||||
},
|
||||
});
|
||||
|
@ -309,21 +310,22 @@ function createLocalFiltersRoute<
|
|||
>;
|
||||
queryRt: TQueryRT;
|
||||
}) {
|
||||
return createRoute({
|
||||
return createApmServerRoute({
|
||||
endpoint,
|
||||
params: t.type({
|
||||
query: t.intersection([localUiBaseQueryRt, queryRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { uiFilters } = setup;
|
||||
const { query } = context.params;
|
||||
|
||||
const { query } = resources.params;
|
||||
|
||||
const { filterNames } = query;
|
||||
const projection = await getProjection({
|
||||
query,
|
||||
context,
|
||||
resources,
|
||||
setup,
|
||||
});
|
||||
|
||||
|
@ -339,7 +341,7 @@ function createLocalFiltersRoute<
|
|||
});
|
||||
}
|
||||
|
||||
export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
|
||||
const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
|
||||
endpoint: 'GET /api/apm/rum/local_filters',
|
||||
getProjection: async ({ setup }) => {
|
||||
return getRumPageLoadTransactionsProjection({
|
||||
|
@ -357,9 +359,23 @@ type GetProjection<
|
|||
> = ({
|
||||
query,
|
||||
setup,
|
||||
context,
|
||||
resources,
|
||||
}: {
|
||||
query: t.TypeOf<TQueryRT>;
|
||||
setup: Setup & SetupTimeRange;
|
||||
context: APMRequestHandlerContext;
|
||||
resources: APMRouteHandlerResources;
|
||||
}) => Promise<TProjection> | TProjection;
|
||||
|
||||
export const rumRouteRepository = createApmServerRouteRepository()
|
||||
.add(rumClientMetricsRoute)
|
||||
.add(rumPageLoadDistributionRoute)
|
||||
.add(rumPageLoadDistBreakdownRoute)
|
||||
.add(rumPageViewsTrendRoute)
|
||||
.add(rumServicesRoute)
|
||||
.add(rumVisitorsBreakdownRoute)
|
||||
.add(rumWebCoreVitals)
|
||||
.add(rumLongTaskMetrics)
|
||||
.add(rumUrlSearch)
|
||||
.add(rumJSErrors)
|
||||
.add(rumHasDataRoute)
|
||||
.add(rumOverviewLocalFiltersRoute);
|
||||
|
|
|
@ -11,13 +11,14 @@ import { invalidLicenseMessage } from '../../common/service_map';
|
|||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getServiceMap } from '../lib/service_map/get_service_map';
|
||||
import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { environmentRt, rangeRt } from './default_api_types';
|
||||
import { notifyFeatureUsage } from '../feature';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { isActivePlatinumLicense } from '../../common/license_check';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
|
||||
export const serviceMapRoute = createRoute({
|
||||
const serviceMapRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/service-map',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
|
@ -29,8 +30,9 @@ export const serviceMapRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
if (!context.config['xpack.apm.serviceMapEnabled']) {
|
||||
handler: async (resources) => {
|
||||
const { config, context, params, logger } = resources;
|
||||
if (!config['xpack.apm.serviceMapEnabled']) {
|
||||
throw Boom.notFound();
|
||||
}
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
|
@ -42,11 +44,10 @@ export const serviceMapRoute = createRoute({
|
|||
featureName: 'serviceMaps',
|
||||
});
|
||||
|
||||
const logger = context.logger;
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const {
|
||||
query: { serviceName, environment },
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -61,7 +62,7 @@ export const serviceMapRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceMapServiceNodeRoute = createRoute({
|
||||
const serviceMapServiceNodeRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/service-map/service/{serviceName}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -70,19 +71,21 @@ export const serviceMapServiceNodeRoute = createRoute({
|
|||
query: t.intersection([environmentRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
if (!context.config['xpack.apm.serviceMapEnabled']) {
|
||||
handler: async (resources) => {
|
||||
const { config, context, params } = resources;
|
||||
|
||||
if (!config['xpack.apm.serviceMapEnabled']) {
|
||||
throw Boom.notFound();
|
||||
}
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(invalidLicenseMessage);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment },
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -96,3 +99,7 @@ export const serviceMapServiceNodeRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceMapRouteRepository = createApmServerRouteRepository()
|
||||
.add(serviceMapRoute)
|
||||
.add(serviceMapServiceNodeRoute);
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getServiceNodes } from '../lib/service_nodes';
|
||||
import { rangeRt, kueryRt } from './default_api_types';
|
||||
|
||||
export const serviceNodesRoute = createRoute({
|
||||
const serviceNodesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -20,9 +21,9 @@ export const serviceNodesRoute = createRoute({
|
|||
query: t.intersection([kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { params } = context;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery } = params.query;
|
||||
|
||||
|
@ -30,3 +31,7 @@ export const serviceNodesRoute = createRoute({
|
|||
return { serviceNodes };
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceNodeRouteRepository = createApmServerRouteRepository().add(
|
||||
serviceNodesRoute
|
||||
);
|
||||
|
|
|
@ -6,15 +6,12 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { jsonRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { uniq } from 'lodash';
|
||||
import {
|
||||
LatencyAggregationType,
|
||||
latencyAggregationTypeRt,
|
||||
} from '../../common/latency_aggregation_types';
|
||||
import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types';
|
||||
import { ProfilingValueType } from '../../common/profiling';
|
||||
import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt';
|
||||
import { jsonRt } from '../../common/runtime_types/json_rt';
|
||||
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
|
@ -35,7 +32,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser
|
|||
import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline';
|
||||
import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate';
|
||||
import { withApmSpan } from '../utils/with_apm_span';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import {
|
||||
comparisonRangeRt,
|
||||
environmentRt,
|
||||
|
@ -43,15 +41,16 @@ import {
|
|||
rangeRt,
|
||||
} from './default_api_types';
|
||||
|
||||
export const servicesRoute = createRoute({
|
||||
const servicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services',
|
||||
params: t.type({
|
||||
query: t.intersection([environmentRt, kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { environment, kuery } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, logger } = resources;
|
||||
const { environment, kuery } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -61,21 +60,22 @@ export const servicesRoute = createRoute({
|
|||
kuery,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
logger: context.logger,
|
||||
logger,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceMetadataDetailsRoute = createRoute({
|
||||
const serviceMetadataDetailsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/details',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: rangeRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -89,16 +89,17 @@ export const serviceMetadataDetailsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceMetadataIconsRoute = createRoute({
|
||||
const serviceMetadataIconsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: rangeRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -112,7 +113,7 @@ export const serviceMetadataIconsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceAgentNameRoute = createRoute({
|
||||
const serviceAgentNameRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/agent_name',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -121,9 +122,10 @@ export const serviceAgentNameRoute = createRoute({
|
|||
query: rangeRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -136,7 +138,7 @@ export const serviceAgentNameRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceTransactionTypesRoute = createRoute({
|
||||
const serviceTransactionTypesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/transaction_types',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -145,9 +147,11 @@ export const serviceTransactionTypesRoute = createRoute({
|
|||
query: rangeRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
return getServiceTransactionTypes({
|
||||
serviceName,
|
||||
setup,
|
||||
|
@ -158,7 +162,7 @@ export const serviceTransactionTypesRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceNodeMetadataRoute = createRoute({
|
||||
const serviceNodeMetadataRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
|
||||
params: t.type({
|
||||
|
@ -169,10 +173,11 @@ export const serviceNodeMetadataRoute = createRoute({
|
|||
query: t.intersection([kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName, serviceNodeName } = context.params.path;
|
||||
const { kuery } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName, serviceNodeName } = params.path;
|
||||
const { kuery } = params.query;
|
||||
|
||||
return getServiceNodeMetadata({
|
||||
kuery,
|
||||
|
@ -183,7 +188,7 @@ export const serviceNodeMetadataRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceAnnotationsRoute = createRoute({
|
||||
const serviceAnnotationsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -192,12 +197,13 @@ export const serviceAnnotationsRoute = createRoute({
|
|||
query: t.intersection([environmentRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
const { environment } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, plugins, context, request, logger } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { environment } = params.query;
|
||||
|
||||
const { observability } = context.plugins;
|
||||
const { observability } = plugins;
|
||||
|
||||
const [
|
||||
annotationsClient,
|
||||
|
@ -205,7 +211,7 @@ export const serviceAnnotationsRoute = createRoute({
|
|||
] = await Promise.all([
|
||||
observability
|
||||
? withApmSpan('get_scoped_annotations_client', () =>
|
||||
observability.getScopedAnnotationsClient(context, request)
|
||||
observability.setup.getScopedAnnotationsClient(context, request)
|
||||
)
|
||||
: undefined,
|
||||
getSearchAggregatedTransactions(setup),
|
||||
|
@ -218,12 +224,12 @@ export const serviceAnnotationsRoute = createRoute({
|
|||
serviceName,
|
||||
annotationsClient,
|
||||
client: context.core.elasticsearch.client.asCurrentUser,
|
||||
logger: context.logger,
|
||||
logger,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceAnnotationsCreateRoute = createRoute({
|
||||
const serviceAnnotationsCreateRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/services/{serviceName}/annotation',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
|
@ -250,12 +256,17 @@ export const serviceAnnotationsCreateRoute = createRoute({
|
|||
}),
|
||||
]),
|
||||
}),
|
||||
handler: async ({ request, context }) => {
|
||||
const { observability } = context.plugins;
|
||||
handler: async (resources) => {
|
||||
const {
|
||||
request,
|
||||
context,
|
||||
plugins: { observability },
|
||||
params,
|
||||
} = resources;
|
||||
|
||||
const annotationsClient = observability
|
||||
? await withApmSpan('get_scoped_annotations_client', () =>
|
||||
observability.getScopedAnnotationsClient(context, request)
|
||||
observability.setup.getScopedAnnotationsClient(context, request)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
@ -263,7 +274,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
|
|||
throw Boom.notFound();
|
||||
}
|
||||
|
||||
const { body, path } = context.params;
|
||||
const { body, path } = params;
|
||||
|
||||
return withApmSpan('create_annotation', () =>
|
||||
annotationsClient.create({
|
||||
|
@ -283,7 +294,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
|
||||
const serviceErrorGroupsPrimaryStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics',
|
||||
params: t.type({
|
||||
|
@ -300,13 +311,14 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { kuery, transactionType, environment },
|
||||
} = context.params;
|
||||
} = params;
|
||||
return getServiceErrorGroupPrimaryStatistics({
|
||||
kuery,
|
||||
serviceName,
|
||||
|
@ -317,7 +329,7 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
|
||||
const serviceErrorGroupsComparisonStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics',
|
||||
params: t.type({
|
||||
|
@ -337,8 +349,9 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
|
@ -351,7 +364,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
|
|||
comparisonStart,
|
||||
comparisonEnd,
|
||||
},
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
return getServiceErrorGroupPeriods({
|
||||
environment,
|
||||
|
@ -367,7 +380,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceThroughputRoute = createRoute({
|
||||
const serviceThroughputRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/throughput',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -382,16 +395,17 @@ export const serviceThroughputRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -432,7 +446,7 @@ export const serviceThroughputRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceInstancesPrimaryStatisticsRoute = createRoute({
|
||||
const serviceInstancesPrimaryStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics',
|
||||
params: t.type({
|
||||
|
@ -450,12 +464,16 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
const { environment, kuery, transactionType } = context.params.query;
|
||||
const latencyAggregationType = (context.params.query
|
||||
.latencyAggregationType as unknown) as LatencyAggregationType;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
latencyAggregationType,
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -479,7 +497,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceInstancesComparisonStatisticsRoute = createRoute({
|
||||
const serviceInstancesComparisonStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics',
|
||||
params: t.type({
|
||||
|
@ -500,9 +518,10 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -511,9 +530,8 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
|
|||
comparisonEnd,
|
||||
serviceNodeIds,
|
||||
numBuckets,
|
||||
} = context.params.query;
|
||||
const latencyAggregationType = (context.params.query
|
||||
.latencyAggregationType as unknown) as LatencyAggregationType;
|
||||
latencyAggregationType,
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -535,7 +553,7 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceDependenciesRoute = createRoute({
|
||||
const serviceDependenciesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -552,11 +570,11 @@ export const serviceDependenciesRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
const { serviceName } = context.params.path;
|
||||
const { environment, numBuckets } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { environment, numBuckets } = params.query;
|
||||
|
||||
const serviceDependencies = await getServiceDependencies({
|
||||
serviceName,
|
||||
|
@ -569,7 +587,7 @@ export const serviceDependenciesRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceProfilingTimelineRoute = createRoute({
|
||||
const serviceProfilingTimelineRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -580,13 +598,13 @@ export const serviceProfilingTimelineRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery },
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
const profilingTimeline = await getServiceProfilingTimeline({
|
||||
kuery,
|
||||
|
@ -599,7 +617,7 @@ export const serviceProfilingTimelineRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceProfilingStatisticsRoute = createRoute({
|
||||
const serviceProfilingStatisticsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -625,13 +643,15 @@ export const serviceProfilingStatisticsRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { params, logger } = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery, valueType },
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
return getServiceProfilingStatistics({
|
||||
kuery,
|
||||
|
@ -639,7 +659,25 @@ export const serviceProfilingStatisticsRoute = createRoute({
|
|||
environment,
|
||||
valueType,
|
||||
setup,
|
||||
logger: context.logger,
|
||||
logger,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceRouteRepository = createApmServerRouteRepository()
|
||||
.add(servicesRoute)
|
||||
.add(serviceMetadataDetailsRoute)
|
||||
.add(serviceMetadataIconsRoute)
|
||||
.add(serviceAgentNameRoute)
|
||||
.add(serviceTransactionTypesRoute)
|
||||
.add(serviceNodeMetadataRoute)
|
||||
.add(serviceAnnotationsRoute)
|
||||
.add(serviceAnnotationsCreateRoute)
|
||||
.add(serviceErrorGroupsPrimaryStatisticsRoute)
|
||||
.add(serviceErrorGroupsComparisonStatisticsRoute)
|
||||
.add(serviceThroughputRoute)
|
||||
.add(serviceInstancesPrimaryStatisticsRoute)
|
||||
.add(serviceInstancesComparisonStatisticsRoute)
|
||||
.add(serviceDependenciesRoute)
|
||||
.add(serviceProfilingTimelineRoute)
|
||||
.add(serviceProfilingStatisticsRoute);
|
||||
|
|
|
@ -16,7 +16,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f
|
|||
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 { createApmServerRoute } from '../create_apm_server_route';
|
||||
import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service';
|
||||
import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent';
|
||||
import {
|
||||
|
@ -24,34 +24,37 @@ import {
|
|||
agentConfigurationIntakeRt,
|
||||
} from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt';
|
||||
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
|
||||
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
|
||||
|
||||
// get list of configurations
|
||||
export const agentConfigurationRoute = createRoute({
|
||||
const agentConfigurationRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const configurations = await listConfigurations({ setup });
|
||||
return { configurations };
|
||||
},
|
||||
});
|
||||
|
||||
// get a single configuration
|
||||
export const getSingleAgentConfigurationRoute = createRoute({
|
||||
const getSingleAgentConfigurationRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/view',
|
||||
params: t.partial({
|
||||
query: serviceRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { name, environment } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, logger } = resources;
|
||||
|
||||
const { name, environment } = params.query;
|
||||
|
||||
const service = { name, environment };
|
||||
const config = await findExactConfiguration({ service, setup });
|
||||
|
||||
if (!config) {
|
||||
context.logger.info(
|
||||
logger.info(
|
||||
`Config was not found for ${service.name}/${service.environment}`
|
||||
);
|
||||
|
||||
|
@ -63,7 +66,7 @@ export const getSingleAgentConfigurationRoute = createRoute({
|
|||
});
|
||||
|
||||
// delete configuration
|
||||
export const deleteAgentConfigurationRoute = createRoute({
|
||||
const deleteAgentConfigurationRoute = createApmServerRoute({
|
||||
endpoint: 'DELETE /api/apm/settings/agent-configuration',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
|
@ -73,20 +76,22 @@ export const deleteAgentConfigurationRoute = createRoute({
|
|||
service: serviceRt,
|
||||
}),
|
||||
}),
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { service } = context.params.body;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, logger } = resources;
|
||||
|
||||
const { service } = params.body;
|
||||
|
||||
const config = await findExactConfiguration({ service, setup });
|
||||
if (!config) {
|
||||
context.logger.info(
|
||||
logger.info(
|
||||
`Config was not found for ${service.name}/${service.environment}`
|
||||
);
|
||||
|
||||
throw Boom.notFound();
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
logger.info(
|
||||
`Deleting config ${service.name}/${service.environment} (${config._id})`
|
||||
);
|
||||
|
||||
|
@ -98,7 +103,7 @@ export const deleteAgentConfigurationRoute = createRoute({
|
|||
});
|
||||
|
||||
// create/update configuration
|
||||
export const createOrUpdateAgentConfigurationRoute = createRoute({
|
||||
const createOrUpdateAgentConfigurationRoute = createApmServerRoute({
|
||||
endpoint: 'PUT /api/apm/settings/agent-configuration',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
|
@ -107,9 +112,10 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
|
|||
t.partial({ query: t.partial({ overwrite: toBooleanRt }) }),
|
||||
t.type({ body: agentConfigurationIntakeRt }),
|
||||
]),
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { body, query } = context.params;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, logger } = resources;
|
||||
const { body, query } = params;
|
||||
|
||||
// if the config already exists, it is fetched and updated
|
||||
// this is to avoid creating two configs with identical service params
|
||||
|
@ -125,13 +131,13 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
|
|||
);
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
logger.info(
|
||||
`${config ? 'Updating' : 'Creating'} config ${body.service.name}/${
|
||||
body.service.environment
|
||||
}`
|
||||
);
|
||||
|
||||
return await createOrUpdateConfiguration({
|
||||
await createOrUpdateConfiguration({
|
||||
configurationId: config?._id,
|
||||
configurationIntake: body,
|
||||
setup,
|
||||
|
@ -147,35 +153,35 @@ const searchParamsRt = t.intersection([
|
|||
export type AgentConfigSearchParams = t.TypeOf<typeof searchParamsRt>;
|
||||
|
||||
// Lookup single configuration (used by APM Server)
|
||||
export const agentConfigurationSearchRoute = createRoute({
|
||||
const agentConfigurationSearchRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/settings/agent-configuration/search',
|
||||
params: t.type({
|
||||
body: searchParamsRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { params, logger } = resources;
|
||||
|
||||
const {
|
||||
service,
|
||||
etag,
|
||||
mark_as_applied_by_agent: markAsAppliedByAgent,
|
||||
} = context.params.body;
|
||||
} = params.body;
|
||||
|
||||
const setup = await setupRequest(context, request);
|
||||
const setup = await setupRequest(resources);
|
||||
const config = await searchConfigurations({
|
||||
service,
|
||||
setup,
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
context.logger.debug(
|
||||
logger.debug(
|
||||
`[Central configuration] Config was not found for ${service.name}/${service.environment}`
|
||||
);
|
||||
throw Boom.notFound();
|
||||
}
|
||||
|
||||
context.logger.info(
|
||||
`Config was found for ${service.name}/${service.environment}`
|
||||
);
|
||||
logger.info(`Config was found for ${service.name}/${service.environment}`);
|
||||
|
||||
// update `applied_by_agent` field
|
||||
// when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags)
|
||||
|
@ -197,11 +203,11 @@ export const agentConfigurationSearchRoute = createRoute({
|
|||
*/
|
||||
|
||||
// get list of services
|
||||
export const listAgentConfigurationServicesRoute = createRoute({
|
||||
const listAgentConfigurationServicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/services',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -215,15 +221,17 @@ export const listAgentConfigurationServicesRoute = createRoute({
|
|||
});
|
||||
|
||||
// get environments for service
|
||||
export const listAgentConfigurationEnvironmentsRoute = createRoute({
|
||||
const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/environments',
|
||||
params: t.partial({
|
||||
query: t.partial({ serviceName: t.string }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const { serviceName } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -239,16 +247,27 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({
|
|||
});
|
||||
|
||||
// get agentName for service
|
||||
export const agentConfigurationAgentNameRoute = createRoute({
|
||||
const agentConfigurationAgentNameRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration/agent_name',
|
||||
params: t.type({
|
||||
query: t.type({ serviceName: t.string }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.query;
|
||||
const agentName = await getAgentNameByService({ serviceName, setup });
|
||||
return { agentName };
|
||||
},
|
||||
});
|
||||
|
||||
export const agentConfigurationRouteRepository = createApmServerRouteRepository()
|
||||
.add(agentConfigurationRoute)
|
||||
.add(getSingleAgentConfigurationRoute)
|
||||
.add(deleteAgentConfigurationRoute)
|
||||
.add(createOrUpdateAgentConfigurationRoute)
|
||||
.add(agentConfigurationSearchRoute)
|
||||
.add(listAgentConfigurationServicesRoute)
|
||||
.add(listAgentConfigurationEnvironmentsRoute)
|
||||
.add(agentConfigurationAgentNameRoute);
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as t from 'io-ts';
|
|||
import Boom from '@hapi/boom';
|
||||
import { isActivePlatinumLicense } from '../../../common/license_check';
|
||||
import { ML_ERRORS } from '../../../common/anomaly_detection';
|
||||
import { createRoute } from '../create_route';
|
||||
import { createApmServerRoute } from '../create_apm_server_route';
|
||||
import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs';
|
||||
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
|
@ -18,15 +18,17 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
|
|||
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
|
||||
import { notifyFeatureUsage } from '../../feature';
|
||||
import { withApmSpan } from '../../utils/with_apm_span';
|
||||
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
|
||||
|
||||
// get ML anomaly detection jobs for each environment
|
||||
export const anomalyDetectionJobsRoute = createRoute({
|
||||
const anomalyDetectionJobsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/anomaly-detection/jobs',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:ml:canGetJobs'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { context, logger } = resources;
|
||||
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
|
||||
|
@ -34,7 +36,7 @@ export const anomalyDetectionJobsRoute = createRoute({
|
|||
|
||||
const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () =>
|
||||
Promise.all([
|
||||
getAnomalyDetectionJobs(setup, context.logger),
|
||||
getAnomalyDetectionJobs(setup, logger),
|
||||
hasLegacyJobs(setup),
|
||||
])
|
||||
);
|
||||
|
@ -47,7 +49,7 @@ export const anomalyDetectionJobsRoute = createRoute({
|
|||
});
|
||||
|
||||
// create new ML anomaly detection jobs for each given environment
|
||||
export const createAnomalyDetectionJobsRoute = createRoute({
|
||||
const createAnomalyDetectionJobsRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/settings/anomaly-detection/jobs',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'],
|
||||
|
@ -57,15 +59,17 @@ export const createAnomalyDetectionJobsRoute = createRoute({
|
|||
environments: t.array(t.string),
|
||||
}),
|
||||
}),
|
||||
handler: async ({ context, request }) => {
|
||||
const { environments } = context.params.body;
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const { params, context, logger } = resources;
|
||||
const { environments } = params.body;
|
||||
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
if (!isActivePlatinumLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
|
||||
}
|
||||
|
||||
await createAnomalyDetectionJobs(setup, environments, context.logger);
|
||||
await createAnomalyDetectionJobs(setup, environments, logger);
|
||||
|
||||
notifyFeatureUsage({
|
||||
licensingPlugin: context.licensing,
|
||||
|
@ -77,11 +81,11 @@ export const createAnomalyDetectionJobsRoute = createRoute({
|
|||
});
|
||||
|
||||
// get all available environments to create anomaly detection jobs for
|
||||
export const anomalyDetectionEnvironmentsRoute = createRoute({
|
||||
const anomalyDetectionEnvironmentsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/anomaly-detection/environments',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -96,3 +100,8 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({
|
|||
return { environments };
|
||||
},
|
||||
});
|
||||
|
||||
export const anomalyDetectionRouteRepository = createApmServerRouteRepository()
|
||||
.add(anomalyDetectionJobsRoute)
|
||||
.add(createAnomalyDetectionJobsRoute)
|
||||
.add(anomalyDetectionEnvironmentsRoute);
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { createRoute } from '../create_route';
|
||||
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
|
||||
import { createApmServerRoute } from '../create_apm_server_route';
|
||||
import {
|
||||
getApmIndices,
|
||||
getApmIndexSettings,
|
||||
|
@ -14,29 +15,30 @@ import {
|
|||
import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices';
|
||||
|
||||
// get list of apm indices and values
|
||||
export const apmIndexSettingsRoute = createRoute({
|
||||
const apmIndexSettingsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/apm-index-settings',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
const apmIndexSettings = await getApmIndexSettings({ context });
|
||||
handler: async ({ config, context }) => {
|
||||
const apmIndexSettings = await getApmIndexSettings({ config, context });
|
||||
return { apmIndexSettings };
|
||||
},
|
||||
});
|
||||
|
||||
// get apm indices configuration object
|
||||
export const apmIndicesRoute = createRoute({
|
||||
const apmIndicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/apm-indices',
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, config } = resources;
|
||||
return await getApmIndices({
|
||||
savedObjectsClient: context.core.savedObjects.client,
|
||||
config: context.config,
|
||||
config,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// save ui indices
|
||||
export const saveApmIndicesRoute = createRoute({
|
||||
const saveApmIndicesRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/settings/apm-indices/save',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
|
@ -53,9 +55,15 @@ export const saveApmIndicesRoute = createRoute({
|
|||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}),
|
||||
}),
|
||||
handler: async ({ context }) => {
|
||||
const { body } = context.params;
|
||||
handler: async (resources) => {
|
||||
const { params, context } = resources;
|
||||
const { body } = params;
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
return await saveApmIndices(savedObjectsClient, body);
|
||||
},
|
||||
});
|
||||
|
||||
export const apmIndicesRouteRepository = createApmServerRouteRepository()
|
||||
.add(apmIndexSettingsRoute)
|
||||
.add(apmIndicesRoute)
|
||||
.add(saveApmIndicesRoute);
|
||||
|
|
|
@ -21,35 +21,40 @@ import {
|
|||
import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link';
|
||||
import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
|
||||
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
|
||||
import { createRoute } from '../create_route';
|
||||
import { createApmServerRoute } from '../create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
|
||||
|
||||
export const customLinkTransactionRoute = createRoute({
|
||||
const customLinkTransactionRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/custom_links/transaction',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.partial({
|
||||
query: filterOptionsRt,
|
||||
}),
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { query } = context.params;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { query } = params;
|
||||
// picks only the items listed in FILTER_OPTIONS
|
||||
const filters = pick(query, FILTER_OPTIONS);
|
||||
return await getTransaction({ setup, filters });
|
||||
},
|
||||
});
|
||||
|
||||
export const listCustomLinksRoute = createRoute({
|
||||
const listCustomLinksRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/settings/custom_links',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.partial({
|
||||
query: filterOptionsRt,
|
||||
}),
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
if (!isActiveGoldLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const { query } = context.params;
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { query } = params;
|
||||
|
||||
// picks only the items listed in FILTER_OPTIONS
|
||||
const filters = pick(query, FILTER_OPTIONS);
|
||||
const customLinks = await listCustomLinks({ setup, filters });
|
||||
|
@ -57,29 +62,30 @@ export const listCustomLinksRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const createCustomLinkRoute = createRoute({
|
||||
const createCustomLinkRoute = createApmServerRoute({
|
||||
endpoint: 'POST /api/apm/settings/custom_links',
|
||||
params: t.type({
|
||||
body: payloadRt,
|
||||
}),
|
||||
options: { tags: ['access:apm', 'access:apm_write'] },
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
if (!isActiveGoldLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const customLink = context.params.body;
|
||||
const res = await createOrUpdateCustomLink({ customLink, setup });
|
||||
const setup = await setupRequest(resources);
|
||||
const customLink = params.body;
|
||||
|
||||
notifyFeatureUsage({
|
||||
licensingPlugin: context.licensing,
|
||||
featureName: 'customLinks',
|
||||
});
|
||||
return res;
|
||||
|
||||
await createOrUpdateCustomLink({ customLink, setup });
|
||||
},
|
||||
});
|
||||
|
||||
export const updateCustomLinkRoute = createRoute({
|
||||
const updateCustomLinkRoute = createApmServerRoute({
|
||||
endpoint: 'PUT /api/apm/settings/custom_links/{id}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -90,23 +96,26 @@ export const updateCustomLinkRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { params, context } = resources;
|
||||
|
||||
if (!isActiveGoldLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const { id } = context.params.path;
|
||||
const customLink = context.params.body;
|
||||
const res = await createOrUpdateCustomLink({
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { id } = params.path;
|
||||
const customLink = params.body;
|
||||
|
||||
await createOrUpdateCustomLink({
|
||||
customLinkId: id,
|
||||
customLink,
|
||||
setup,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteCustomLinkRoute = createRoute({
|
||||
const deleteCustomLinkRoute = createApmServerRoute({
|
||||
endpoint: 'DELETE /api/apm/settings/custom_links/{id}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -116,12 +125,14 @@ export const deleteCustomLinkRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm', 'access:apm_write'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
handler: async (resources) => {
|
||||
const { context, params } = resources;
|
||||
|
||||
if (!isActiveGoldLicense(context.licensing.license)) {
|
||||
throw Boom.forbidden(INVALID_LICENSE);
|
||||
}
|
||||
const setup = await setupRequest(context, request);
|
||||
const { id } = context.params.path;
|
||||
const setup = await setupRequest(resources);
|
||||
const { id } = params.path;
|
||||
const res = await deleteCustomLink({
|
||||
customLinkId: id,
|
||||
setup,
|
||||
|
@ -129,3 +140,10 @@ export const deleteCustomLinkRoute = createRoute({
|
|||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
export const customLinkRouteRepository = createApmServerRouteRepository()
|
||||
.add(customLinkTransactionRoute)
|
||||
.add(listCustomLinksRoute)
|
||||
.add(createCustomLinkRoute)
|
||||
.add(updateCustomLinkRoute)
|
||||
.add(deleteCustomLinkRoute);
|
||||
|
|
|
@ -9,20 +9,22 @@ import * as t from 'io-ts';
|
|||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getTrace } from '../lib/traces/get_trace';
|
||||
import { getTransactionGroupList } from '../lib/transaction_groups';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
|
||||
export const tracesRoute = createRoute({
|
||||
const tracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/traces',
|
||||
params: t.type({
|
||||
query: t.intersection([environmentRt, kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { environment, kuery } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { environment, kuery } = params.query;
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
@ -34,7 +36,7 @@ export const tracesRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const tracesByIdRoute = createRoute({
|
||||
const tracesByIdRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/traces/{traceId}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -43,13 +45,16 @@ export const tracesByIdRoute = createRoute({
|
|||
query: rangeRt,
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
return getTrace(context.params.path.traceId, setup);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const { traceId } = params.path;
|
||||
return getTrace(traceId, setup);
|
||||
},
|
||||
});
|
||||
|
||||
export const rootTransactionByTraceIdRoute = createRoute({
|
||||
const rootTransactionByTraceIdRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/traces/{traceId}/root_transaction',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -57,9 +62,15 @@ export const rootTransactionByTraceIdRoute = createRoute({
|
|||
}),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const { traceId } = context.params.path;
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const { params } = resources;
|
||||
const { traceId } = params.path;
|
||||
const setup = await setupRequest(resources);
|
||||
return getRootTransactionByTraceId(traceId, setup);
|
||||
},
|
||||
});
|
||||
|
||||
export const traceRouteRepository = createApmServerRouteRepository()
|
||||
.add(tracesByIdRoute)
|
||||
.add(tracesRoute)
|
||||
.add(rootTransactionByTraceIdRoute);
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { jsonRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import {
|
||||
LatencyAggregationType,
|
||||
latencyAggregationTypeRt,
|
||||
} from '../../common/latency_aggregation_types';
|
||||
import { jsonRt } from '../../common/runtime_types/json_rt';
|
||||
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
|
||||
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
|
@ -23,7 +23,8 @@ import { getLatencyPeriods } from '../lib/transactions/get_latency_charts';
|
|||
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
|
||||
import { getTransactionGroupList } from '../lib/transaction_groups';
|
||||
import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate';
|
||||
import { createRoute } from './create_route';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { createApmServerRouteRepository } from './create_apm_server_route_repository';
|
||||
import {
|
||||
comparisonRangeRt,
|
||||
environmentRt,
|
||||
|
@ -35,7 +36,7 @@ import {
|
|||
* Returns a list of transactions grouped by name
|
||||
* //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/
|
||||
*/
|
||||
export const transactionGroupsRoute = createRoute({
|
||||
const transactionGroupsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -49,10 +50,11 @@ export const transactionGroupsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
const { environment, kuery, transactionType } = context.params.query;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { environment, kuery, transactionType } = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -72,7 +74,7 @@ export const transactionGroupsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionGroupsPrimaryStatisticsRoute = createRoute({
|
||||
const transactionGroupsPrimaryStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics',
|
||||
params: t.type({
|
||||
|
@ -90,8 +92,9 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const { params } = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -100,7 +103,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
|
|||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery, latencyAggregationType, transactionType },
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
return getServiceTransactionGroups({
|
||||
environment,
|
||||
|
@ -109,12 +112,12 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
|
|||
serviceName,
|
||||
searchAggregatedTransactions,
|
||||
transactionType,
|
||||
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
|
||||
latencyAggregationType,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionGroupsComparisonStatisticsRoute = createRoute({
|
||||
const transactionGroupsComparisonStatisticsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics',
|
||||
params: t.type({
|
||||
|
@ -135,13 +138,15 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
|
|||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
);
|
||||
|
||||
const { params } = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
|
@ -154,7 +159,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
|
|||
comparisonStart,
|
||||
comparisonEnd,
|
||||
},
|
||||
} = context.params;
|
||||
} = params;
|
||||
|
||||
return await getServiceTransactionGroupComparisonStatisticsPeriods({
|
||||
environment,
|
||||
|
@ -165,14 +170,14 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
|
|||
searchAggregatedTransactions,
|
||||
transactionType,
|
||||
numBuckets,
|
||||
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
|
||||
latencyAggregationType,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionLatencyChartsRoute = createRoute({
|
||||
const transactionLatencyChartsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -188,10 +193,11 @@ export const transactionLatencyChartsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const logger = context.logger;
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params, logger } = resources;
|
||||
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -200,7 +206,7 @@ export const transactionLatencyChartsRoute = createRoute({
|
|||
latencyAggregationType,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -242,7 +248,7 @@ export const transactionLatencyChartsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionThroughputChartsRoute = createRoute({
|
||||
const transactionThroughputChartsRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/transactions/charts/throughput',
|
||||
params: t.type({
|
||||
|
@ -258,15 +264,17 @@ export const transactionThroughputChartsRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
transactionName,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -284,7 +292,7 @@ export const transactionThroughputChartsRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionChartsDistributionRoute = createRoute({
|
||||
const transactionChartsDistributionRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/transactions/charts/distribution',
|
||||
params: t.type({
|
||||
|
@ -306,9 +314,10 @@ export const transactionChartsDistributionRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -316,7 +325,7 @@ export const transactionChartsDistributionRoute = createRoute({
|
|||
transactionName,
|
||||
transactionId = '',
|
||||
traceId = '',
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||
setup
|
||||
|
@ -336,7 +345,7 @@ export const transactionChartsDistributionRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionChartsBreakdownRoute = createRoute({
|
||||
const transactionChartsBreakdownRoute = createApmServerRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
|
@ -351,15 +360,17 @@ export const transactionChartsBreakdownRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
} = context.params.query;
|
||||
} = params.query;
|
||||
|
||||
return getTransactionBreakdown({
|
||||
environment,
|
||||
|
@ -372,7 +383,7 @@ export const transactionChartsBreakdownRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const transactionChartsErrorRateRoute = createRoute({
|
||||
const transactionChartsErrorRateRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate',
|
||||
params: t.type({
|
||||
|
@ -386,9 +397,10 @@ export const transactionChartsErrorRateRoute = createRoute({
|
|||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { params } = context;
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
|
@ -416,3 +428,13 @@ export const transactionChartsErrorRateRoute = createRoute({
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionRouteRepository = createApmServerRouteRepository()
|
||||
.add(transactionGroupsRoute)
|
||||
.add(transactionGroupsPrimaryStatisticsRoute)
|
||||
.add(transactionGroupsComparisonStatisticsRoute)
|
||||
.add(transactionLatencyChartsRoute)
|
||||
.add(transactionThroughputChartsRoute)
|
||||
.add(transactionChartsDistributionRoute)
|
||||
.add(transactionChartsBreakdownRoute)
|
||||
.add(transactionChartsErrorRateRoute);
|
||||
|
|
|
@ -5,27 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import t, { Encode, Encoder } from 'io-ts';
|
||||
import {
|
||||
CoreSetup,
|
||||
KibanaRequest,
|
||||
RequestHandlerContext,
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
CoreStart,
|
||||
} from 'src/core/server';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RequiredKeys, DeepPartial } from 'utility-types';
|
||||
import { SpacesPluginStart } from '../../../spaces/server';
|
||||
import { ObservabilityPluginSetup } from '../../../observability/server';
|
||||
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { MlPluginSetup } from '../../../ml/server';
|
||||
import { FetchOptions } from '../../common/fetch_options';
|
||||
import { APMConfig } from '..';
|
||||
import { APMPluginDependencies } from '../types';
|
||||
|
||||
export type HandlerReturn = Record<string, any>;
|
||||
|
||||
interface InspectQueryParam {
|
||||
query: { _inspect: boolean };
|
||||
export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
|
||||
licensing: LicensingApiRequestHandlerContext;
|
||||
}
|
||||
|
||||
export type InspectResponse = Array<{
|
||||
|
@ -36,141 +28,53 @@ export type InspectResponse = Array<{
|
|||
esError: Error;
|
||||
}>;
|
||||
|
||||
export interface RouteParams {
|
||||
path?: Record<string, unknown>;
|
||||
query?: Record<string, unknown>;
|
||||
body?: any;
|
||||
export interface APMRouteCreateOptions {
|
||||
options: {
|
||||
tags: Array<
|
||||
| 'access:apm'
|
||||
| 'access:apm_write'
|
||||
| 'access:ml:canGetJobs'
|
||||
| 'access:ml:canCreateJob'
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
type WithoutIncompatibleMethods<T extends t.Any> = Omit<
|
||||
T,
|
||||
'encode' | 'asEncoder'
|
||||
> & { encode: Encode<any, any>; asEncoder: () => Encoder<any, any> };
|
||||
|
||||
export type RouteParamsRT = WithoutIncompatibleMethods<t.Type<RouteParams>>;
|
||||
|
||||
export type RouteHandler<
|
||||
TParamsRT extends RouteParamsRT | undefined,
|
||||
TReturn extends HandlerReturn
|
||||
> = (kibanaContext: {
|
||||
context: APMRequestHandlerContext<
|
||||
(TParamsRT extends RouteParamsRT ? t.TypeOf<TParamsRT> : {}) &
|
||||
InspectQueryParam
|
||||
>;
|
||||
export interface APMRouteHandlerResources {
|
||||
request: KibanaRequest;
|
||||
}) => Promise<TReturn extends any[] ? never : TReturn>;
|
||||
|
||||
interface RouteOptions {
|
||||
tags: Array<
|
||||
| 'access:apm'
|
||||
| 'access:apm_write'
|
||||
| 'access:ml:canGetJobs'
|
||||
| 'access:ml:canCreateJob'
|
||||
>;
|
||||
}
|
||||
|
||||
export interface Route<
|
||||
TEndpoint extends string,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined,
|
||||
TReturn extends HandlerReturn
|
||||
> {
|
||||
endpoint: TEndpoint;
|
||||
options: RouteOptions;
|
||||
params?: TRouteParamsRT;
|
||||
handler: RouteHandler<TRouteParamsRT, TReturn>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
|
||||
licensing: LicensingApiRequestHandlerContext;
|
||||
}
|
||||
|
||||
export type APMRequestHandlerContext<
|
||||
TRouteParams = {}
|
||||
> = ApmPluginRequestHandlerContext & {
|
||||
params: TRouteParams & InspectQueryParam;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
params: {
|
||||
query: {
|
||||
_inspect: boolean;
|
||||
};
|
||||
};
|
||||
config: APMConfig;
|
||||
logger: Logger;
|
||||
core: {
|
||||
setup: CoreSetup;
|
||||
start: () => Promise<CoreStart>;
|
||||
};
|
||||
plugins: {
|
||||
spaces?: SpacesPluginStart;
|
||||
observability?: ObservabilityPluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
ml?: MlPluginSetup;
|
||||
};
|
||||
};
|
||||
|
||||
export interface RouteState {
|
||||
[endpoint: string]: {
|
||||
params?: RouteParams;
|
||||
ret: any;
|
||||
[key in keyof APMPluginDependencies]: {
|
||||
setup: Required<APMPluginDependencies>[key]['setup'];
|
||||
start: () => Promise<Required<APMPluginDependencies>[key]['start']>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerAPI<TRouteState extends RouteState> {
|
||||
_S: TRouteState;
|
||||
add<
|
||||
TEndpoint extends string,
|
||||
TReturn extends HandlerReturn,
|
||||
TRouteParamsRT extends RouteParamsRT | undefined = undefined
|
||||
>(
|
||||
route:
|
||||
| Route<TEndpoint, TRouteParamsRT, TReturn>
|
||||
| ((core: CoreSetup) => Route<TEndpoint, TRouteParamsRT, TReturn>)
|
||||
): ServerAPI<
|
||||
TRouteState &
|
||||
{
|
||||
[key in TEndpoint]: {
|
||||
params: TRouteParamsRT;
|
||||
ret: TReturn & { _inspect?: InspectResponse };
|
||||
};
|
||||
}
|
||||
>;
|
||||
init: (
|
||||
core: CoreSetup,
|
||||
context: {
|
||||
config$: Observable<APMConfig>;
|
||||
logger: Logger;
|
||||
plugins: {
|
||||
observability?: ObservabilityPluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
ml?: MlPluginSetup;
|
||||
};
|
||||
}
|
||||
) => void;
|
||||
}
|
||||
|
||||
type MaybeOptional<T extends { params: Record<string, any> }> = RequiredKeys<
|
||||
T['params']
|
||||
> extends never
|
||||
? { params?: T['params'] }
|
||||
: { params: T['params'] };
|
||||
|
||||
export type MaybeParams<
|
||||
TRouteState,
|
||||
TEndpoint extends keyof TRouteState & string
|
||||
> = TRouteState[TEndpoint] extends { params: t.Any }
|
||||
? MaybeOptional<{
|
||||
params: t.OutputOf<TRouteState[TEndpoint]['params']> &
|
||||
DeepPartial<InspectQueryParam>;
|
||||
}>
|
||||
: {};
|
||||
|
||||
export type Client<
|
||||
TRouteState,
|
||||
TOptions extends { abortable: boolean } = { abortable: true }
|
||||
> = <TEndpoint extends keyof TRouteState & string>(
|
||||
options: Omit<
|
||||
FetchOptions,
|
||||
'query' | 'body' | 'pathname' | 'method' | 'signal'
|
||||
> & {
|
||||
forceCache?: boolean;
|
||||
endpoint: TEndpoint;
|
||||
} & MaybeParams<TRouteState, TEndpoint> &
|
||||
(TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
|
||||
) => Promise<
|
||||
TRouteState[TEndpoint] extends { ret: any }
|
||||
? TRouteState[TEndpoint]['ret']
|
||||
: unknown
|
||||
>;
|
||||
// export type Client<
|
||||
// TRouteState,
|
||||
// TOptions extends { abortable: boolean } = { abortable: true }
|
||||
// > = <TEndpoint extends keyof TRouteState & string>(
|
||||
// options: Omit<
|
||||
// FetchOptions,
|
||||
// 'query' | 'body' | 'pathname' | 'method' | 'signal'
|
||||
// > & {
|
||||
// forceCache?: boolean;
|
||||
// endpoint: TEndpoint;
|
||||
// } & MaybeParams<TRouteState, TEndpoint> &
|
||||
// (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
|
||||
// ) => Promise<
|
||||
// TRouteState[TEndpoint] extends { ret: any }
|
||||
// ? TRouteState[TEndpoint]['ret']
|
||||
// : unknown
|
||||
// >;
|
||||
|
|
164
x-pack/plugins/apm/server/types.ts
Normal file
164
x-pack/plugins/apm/server/types.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 { ValuesType } from 'utility-types';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server';
|
||||
import {
|
||||
PluginSetup as DataPluginSetup,
|
||||
PluginStart as DataPluginStart,
|
||||
} from '../../../../src/plugins/data/server';
|
||||
import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
|
||||
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
|
||||
import {
|
||||
HomeServerPluginSetup,
|
||||
HomeServerPluginStart,
|
||||
} from '../../../../src/plugins/home/server';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
|
||||
import { ActionsPlugin } from '../../actions/server';
|
||||
import { AlertingPlugin } from '../../alerting/server';
|
||||
import { CloudSetup } from '../../cloud/server';
|
||||
import {
|
||||
PluginSetupContract as FeaturesPluginSetup,
|
||||
PluginStartContract as FeaturesPluginStart,
|
||||
} from '../../features/server';
|
||||
import {
|
||||
LicensingPluginSetup,
|
||||
LicensingPluginStart,
|
||||
} from '../../licensing/server';
|
||||
import { MlPluginSetup, MlPluginStart } from '../../ml/server';
|
||||
import { ObservabilityPluginSetup } from '../../observability/server';
|
||||
import {
|
||||
SecurityPluginSetup,
|
||||
SecurityPluginStart,
|
||||
} from '../../security/server';
|
||||
import {
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '../../task_manager/server';
|
||||
import { APMConfig } from '.';
|
||||
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
|
||||
import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { ApmPluginRequestHandlerContext } from './routes/typings';
|
||||
|
||||
export interface APMPluginSetup {
|
||||
config$: Observable<APMConfig>;
|
||||
getApmIndices: () => ReturnType<typeof getApmIndices>;
|
||||
createApmEventClient: (params: {
|
||||
debug?: boolean;
|
||||
request: KibanaRequest;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
}) => Promise<ReturnType<typeof createApmEventClient>>;
|
||||
}
|
||||
|
||||
interface DependencyMap {
|
||||
core: {
|
||||
setup: CoreSetup;
|
||||
start: CoreStart;
|
||||
};
|
||||
spaces: {
|
||||
setup: SpacesPluginSetup;
|
||||
start: SpacesPluginStart;
|
||||
};
|
||||
apmOss: {
|
||||
setup: APMOSSPluginSetup;
|
||||
start: undefined;
|
||||
};
|
||||
home: {
|
||||
setup: HomeServerPluginSetup;
|
||||
start: HomeServerPluginStart;
|
||||
};
|
||||
licensing: {
|
||||
setup: LicensingPluginSetup;
|
||||
start: LicensingPluginStart;
|
||||
};
|
||||
cloud: {
|
||||
setup: CloudSetup;
|
||||
start: undefined;
|
||||
};
|
||||
usageCollection: {
|
||||
setup: UsageCollectionSetup;
|
||||
start: undefined;
|
||||
};
|
||||
taskManager: {
|
||||
setup: TaskManagerSetupContract;
|
||||
start: TaskManagerStartContract;
|
||||
};
|
||||
alerting: {
|
||||
setup: AlertingPlugin['setup'];
|
||||
start: AlertingPlugin['start'];
|
||||
};
|
||||
actions: {
|
||||
setup: ActionsPlugin['setup'];
|
||||
start: ActionsPlugin['start'];
|
||||
};
|
||||
observability: {
|
||||
setup: ObservabilityPluginSetup;
|
||||
start: undefined;
|
||||
};
|
||||
features: {
|
||||
setup: FeaturesPluginSetup;
|
||||
start: FeaturesPluginStart;
|
||||
};
|
||||
security: {
|
||||
setup: SecurityPluginSetup;
|
||||
start: SecurityPluginStart;
|
||||
};
|
||||
ml: {
|
||||
setup: MlPluginSetup;
|
||||
start: MlPluginStart;
|
||||
};
|
||||
data: {
|
||||
setup: DataPluginSetup;
|
||||
start: DataPluginStart;
|
||||
};
|
||||
}
|
||||
|
||||
const requiredDependencies = [
|
||||
'features',
|
||||
'apmOss',
|
||||
'data',
|
||||
'licensing',
|
||||
'triggersActionsUi',
|
||||
'embeddable',
|
||||
'infra',
|
||||
] as const;
|
||||
|
||||
const optionalDependencies = [
|
||||
'spaces',
|
||||
'cloud',
|
||||
'usageCollection',
|
||||
'taskManager',
|
||||
'actions',
|
||||
'alerting',
|
||||
'observability',
|
||||
'security',
|
||||
'ml',
|
||||
'home',
|
||||
'maps',
|
||||
] as const;
|
||||
|
||||
type RequiredDependencies = Pick<
|
||||
DependencyMap,
|
||||
ValuesType<typeof requiredDependencies> & keyof DependencyMap
|
||||
>;
|
||||
|
||||
type OptionalDependencies = Partial<
|
||||
Pick<
|
||||
DependencyMap,
|
||||
ValuesType<typeof optionalDependencies> & keyof DependencyMap
|
||||
>
|
||||
>;
|
||||
|
||||
export type APMPluginDependencies = RequiredDependencies & OptionalDependencies;
|
||||
|
||||
export type APMPluginSetupDependencies = {
|
||||
[key in keyof APMPluginDependencies]: Required<APMPluginDependencies>[key]['setup'];
|
||||
};
|
||||
|
||||
export type APMPluginStartDependencies = {
|
||||
[key in keyof APMPluginDependencies]: Required<APMPluginDependencies>[key]['start'];
|
||||
};
|
|
@ -8,24 +8,25 @@
|
|||
import { format } from 'url';
|
||||
import supertest from 'supertest';
|
||||
import request from 'superagent';
|
||||
import { MaybeParams } from '../../../plugins/apm/server/routes/typings';
|
||||
import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint';
|
||||
import { APMAPI } from '../../../plugins/apm/server/routes/create_apm_api';
|
||||
import type { APIReturnType } from '../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import type {
|
||||
APIReturnType,
|
||||
APIEndpoint,
|
||||
APIClientRequestParamsOf,
|
||||
} from '../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
|
||||
export function createApmApiSupertest(st: supertest.SuperTest<supertest.Test>) {
|
||||
return async <TPath extends keyof APMAPI['_S']>(
|
||||
return async <TEndpoint extends APIEndpoint>(
|
||||
options: {
|
||||
endpoint: TPath;
|
||||
} & MaybeParams<APMAPI['_S'], TPath>
|
||||
endpoint: TEndpoint;
|
||||
} & APIClientRequestParamsOf<TEndpoint> & { params?: { query?: { _inspect?: boolean } } }
|
||||
): Promise<{
|
||||
status: number;
|
||||
body: APIReturnType<TPath>;
|
||||
body: APIReturnType<TEndpoint>;
|
||||
}> => {
|
||||
const { endpoint } = options;
|
||||
|
||||
// @ts-expect-error
|
||||
const params = 'params' in options ? options.params : {};
|
||||
const params = 'params' in options ? (options.params as Record<string, any>) : {};
|
||||
|
||||
const { method, pathname } = parseEndpoint(endpoint, params?.path);
|
||||
const url = format({ pathname, query: params?.query });
|
||||
|
|
|
@ -81,7 +81,6 @@ export default function customLinksTests({ getService }: FtrProviderContext) {
|
|||
it('for agent configs', async () => {
|
||||
const { status, body } = await supertestRead({
|
||||
endpoint: 'GET /api/apm/settings/agent-configuration',
|
||||
// @ts-expect-error
|
||||
params: {
|
||||
query: {
|
||||
_inspect: true,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|||
import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { registry } from '../../common/registry';
|
||||
import { createApmApiSupertest } from '../../common/apm_api_supertest';
|
||||
import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const apmApiSupertest = createApmApiSupertest(getService('supertest'));
|
||||
|
@ -31,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
params: {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
latencyAggregationType: 'avg',
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
start,
|
||||
end,
|
||||
transactionType: 'request',
|
||||
|
@ -61,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
params: {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
latencyAggregationType: 'avg',
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
start,
|
||||
end,
|
||||
transactionType: 'request',
|
||||
|
@ -130,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
params: {
|
||||
path: { serviceName: 'opbeans-ruby' },
|
||||
query: {
|
||||
latencyAggregationType: 'avg',
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
start,
|
||||
end,
|
||||
transactionType: 'request',
|
||||
|
|
|
@ -2680,6 +2680,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/io-ts-utils@link:packages/kbn-io-ts-utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/legacy-logging@link:packages/kbn-legacy-logging":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -2712,6 +2716,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/server-route-repository@link:packages/kbn-server-route-repository":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/std@link:packages/kbn-std":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue