[APM] Extract server type utils to package (#96349)

This commit is contained in:
Dario Gieselaar 2021-04-08 13:26:43 +02:00 committed by GitHub
parent d9ef5c28d5
commit bfc940c146
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 2821 additions and 1684 deletions

View file

@ -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",

View 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'],
};

View 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"
}
}

View 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';

View file

@ -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(() => {

View file

@ -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';

View file

@ -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: {

View file

@ -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 => {

View file

@ -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`
);
}
});

View file

@ -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);

View 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"
]
}

View 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

View 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'],
};

View 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"
}
}

View file

@ -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;
}

View file

@ -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),
};
}

View file

@ -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({});
});
});

View file

@ -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;
}

View 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 };
}

View 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';

View 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 };
}

View 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 { 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,
};

View 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 }) })
)
);

View 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>>;

View 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"
]
}

View file

@ -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),
]);

View file

@ -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()
);

View file

@ -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' },
});
});

View file

@ -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();

View file

@ -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);

View file

@ -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);

View 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;

View file

@ -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;

View file

@ -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';

View file

@ -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);

View file

@ -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,

View file

@ -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);
});

View file

@ -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 {

View file

@ -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'
);

View file

@ -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',

View file

@ -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'];
}

View file

@ -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;

View file

@ -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,

View file

@ -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 () =>

View file

@ -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);

View file

@ -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);

View file

@ -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);
});
});
});

View file

@ -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)
);
}

View file

@ -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 };

View 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
>();

View file

@ -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
>();
}

View file

@ -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;
}

View file

@ -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
);

View file

@ -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);

View file

@ -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 };

View file

@ -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);

View file

@ -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
);

View file

@ -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);

View 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);
});
});
});

View 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);
}
}
);
});
}

View file

@ -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);

View file

@ -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);

View file

@ -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
);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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
// >;

View 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'];
};

View file

@ -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 });

View file

@ -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,

View file

@ -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',

View file

@ -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 ""