Skip to content

Commit

Permalink
[Infra UI] Add APM Metrics Endpoint
Browse files Browse the repository at this point in the history
- Closes #42169
- Adds /api/infra/apm_metrics
- Adds makeInternalRequest method to Framwork library
- Adds scafolding for tests
- Removes duplicate version of Boom
- Adds tests
  • Loading branch information
simianhacker committed Aug 5, 2019
1 parent fcd0d73 commit b57bee7
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 31 deletions.
97 changes: 97 additions & 0 deletions x-pack/legacy/plugins/infra/common/http_api/apm_metrics_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as rt from 'io-ts';
import { InfraWrappableRequest } from '../../server/lib/adapters/framework';
import { InfraNodeTypeRT } from './common';

export const InfraApmMetricsRequestRT = rt.type({
timeRange: rt.type({
min: rt.number,
max: rt.number,
}),
nodeId: rt.string,
nodeType: InfraNodeTypeRT,
sourceId: rt.string,
});

export const InfraApmMetricsDataPointRT = rt.type({
timestamp: rt.number,
value: rt.union([rt.number, rt.null]),
});

export const InfraApmMetricsDataBucketRT = rt.type({
name: rt.string,
data: rt.array(InfraApmMetricsDataPointRT),
});

export const InfraApmMetricsServiceRT = rt.type({
name: rt.string,
transactionsPerMinute: rt.array(InfraApmMetricsDataBucketRT),
responseTimes: rt.array(InfraApmMetricsDataBucketRT),
});

export const InfraApmMetricsRT = rt.type({
services: rt.array(InfraApmMetricsServiceRT),
});

export const APMDataPointRT = rt.type({
x: rt.number,
y: rt.union([rt.number, rt.null]),
});

export const APMTpmBucketsRT = rt.type({
key: rt.string,
dataPoints: rt.array(APMDataPointRT),
});

export const APMChartResponseRT = rt.type({
apmTimeseries: rt.intersection([
rt.type({
responseTimes: rt.type({
avg: rt.array(APMDataPointRT),
p95: rt.array(APMDataPointRT),
p99: rt.array(APMDataPointRT),
}),
tpmBuckets: rt.array(APMTpmBucketsRT),
}),
rt.partial({
overallAvgDuration: rt.number,
}),
]),
});

export const APMServiceResponseRT = rt.type({
hasHistoricalData: rt.boolean,
hasLegacyData: rt.boolean,
items: rt.array(
rt.type({
agentName: rt.string,
avgResponseTime: rt.number,
environments: rt.array(rt.string),
errorsPerMinute: rt.number,
serviceName: rt.string,
transactionsPerMinute: rt.number,
})
),
});

export type InfraApmMetricsRequest = rt.TypeOf<typeof InfraApmMetricsRequestRT>;

export type InfraApmMetricsRequestWrapped = InfraWrappableRequest<InfraApmMetricsRequest>;

export type InfraApmMetrics = rt.TypeOf<typeof InfraApmMetricsRT>;

export type InfraApmMetricsService = rt.TypeOf<typeof InfraApmMetricsServiceRT>;

export type InfraApmMetricsDataBucket = rt.TypeOf<typeof InfraApmMetricsDataBucketRT>;

export type InfraApmMetricsDataPoint = rt.TypeOf<typeof InfraApmMetricsDataPointRT>;

export type APMDataPoint = rt.TypeOf<typeof APMDataPointRT>;

export type APMTpmBuckets = rt.TypeOf<typeof APMTpmBucketsRT>;

export type APMChartResponse = rt.TypeOf<typeof APMChartResponseRT>;
14 changes: 14 additions & 0 deletions x-pack/legacy/plugins/infra/common/http_api/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as rt from 'io-ts';

export const InfraNodeTypeRT = rt.keyof({
host: null,
container: null,
pod: null,
});

export type InfraNodeType = rt.TypeOf<typeof InfraNodeTypeRT>;
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/infra/common/http_api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './search_results_api';
export * from './search_summary_api';
export * from './metadata_api';
export * from './timed_api';
export * from './apm_metrics_api';
11 changes: 2 additions & 9 deletions x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,11 @@

import * as rt from 'io-ts';
import { InfraWrappableRequest } from '../../server/lib/adapters/framework';

export const InfraMetadataNodeTypeRT = rt.keyof({
host: null,
pod: null,
container: null,
});
import { InfraNodeTypeRT } from './common';

export const InfraMetadataRequestRT = rt.type({
nodeId: rt.string,
nodeType: InfraMetadataNodeTypeRT,
nodeType: InfraNodeTypeRT,
sourceId: rt.string,
});

Expand Down Expand Up @@ -98,5 +93,3 @@ export type InfraMetadataMachine = rt.TypeOf<typeof InfraMetadataMachineRT>;
export type InfraMetadataHost = rt.TypeOf<typeof InfraMetadataHostRT>;

export type InfraMetadataOS = rt.TypeOf<typeof InfraMetadataOSRT>;

export type InfraMetadataNodeType = rt.TypeOf<typeof InfraMetadataNodeTypeRT>;
3 changes: 1 addition & 2 deletions x-pack/legacy/plugins/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
},
"dependencies": {
"@types/color": "^3.0.0",
"boom": "7.3.0",
"lodash": "^4.17.13"
}
}
}
2 changes: 2 additions & 0 deletions x-pack/legacy/plugins/infra/server/infra_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InfraBackendLibs } from './lib/infra_types';
import { initLegacyLoggingRoutes } from './logging_legacy';
import { initMetricExplorerRoute } from './routes/metrics_explorer';
import { initMetadataRoute } from './routes/metadata';
import { initApmMetricsRoute } from './routes/apm_metrics';

export const initInfraServer = (libs: InfraBackendLibs) => {
const schema = makeExecutableSchema({
Expand All @@ -35,4 +36,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initIpToHostName(libs);
initMetricExplorerRoute(libs);
initMetadataRoute(libs);
initApmMetricsRoute(libs);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { SearchResponse } from 'elasticsearch';
import { GraphQLSchema } from 'graphql';
import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi';
import { Lifecycle, ResponseToolkit, RouteOptions, ServerInjectResponse } from 'hapi';
import { Legacy } from 'kibana';

import { KibanaConfig } from 'src/legacy/server/kbn_server';
Expand Down Expand Up @@ -55,6 +55,12 @@ export interface InfraBackendFrameworkAdapter {
): Promise<InfraDatabaseSearchResponse>;
getIndexPatternsService(req: InfraFrameworkRequest<any>): Legacy.IndexPatternsService;
getSavedObjectsService(): Legacy.SavedObjectsService;
makeInternalRequest<T extends object>(
req: InfraFrameworkRequest<Legacy.Request>,
path: string,
method: 'POST' | 'GET' | 'PUT' | 'HEAD' | 'DELETE',
payload?: T
): Promise<ServerInjectResponse>;
makeTSVBRequest(
req: InfraFrameworkRequest,
model: InfraMetricModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,35 +147,48 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
return this.server.savedObjects;
}

public async makeTSVBRequest(
public async makeInternalRequest<T extends object>(
req: InfraFrameworkRequest<Legacy.Request>,
model: InfraMetricModel,
timerange: { min: number; max: number },
filters: any[]
path: string,
method: 'POST' | 'GET' | 'PUT' | 'HEAD' | 'DELETE' = 'GET',
payload?: T
) {
const internalRequest = req[internalInfraFrameworkRequest];
const server = internalRequest.server;

// getBasePath returns randomized base path AND spaces path
const basePath = internalRequest.getBasePath();
const url = `${basePath}/api/metrics/vis/data`;

const url = `${basePath}${path}`;
const request = {
url,
method: 'POST',
method,
headers: internalRequest.headers,
payload: {
timerange,
panels: [model],
filters,
},
payload,
};

const res = await server.inject(request);
if (res.statusCode !== 200) {
throw res;
}

return res;
}

public async makeTSVBRequest(
req: InfraFrameworkRequest<Legacy.Request>,
model: InfraMetricModel,
timerange: { min: number; max: number },
filters: any[]
) {
const payload = {
timerange,
panels: [model],
filters,
};
const res = await this.makeInternalRequest(req, '/api/metrics/vis/data', 'POST', payload);
if (res.statusCode !== 200) {
throw res;
}
return res.result as InfraTSVBResponse;
}
}
Expand Down
52 changes: 52 additions & 0 deletions x-pack/legacy/plugins/infra/server/routes/apm_metrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from 'boom';
import { InfraBackendLibs } from '../../lib/infra_types';
import { throwErrors } from '../../../common/runtime_types';
import {
InfraApmMetricsRequestRT,
InfraApmMetricsRequestWrapped,
InfraApmMetrics,
InfraApmMetricsRT,
} from '../../../common/http_api';
import { getApmServices } from './lib/get_apm_services';
import { getApmServiceData } from './lib/get_apm_service_data';

export const initApmMetricsRoute = (libs: InfraBackendLibs) => {
const { framework, sources } = libs;

framework.registerRoute<InfraApmMetricsRequestWrapped, Promise<InfraApmMetrics>>({
method: 'POST',
path: '/api/infra/apm_metrics',
handler: async req => {
try {
const { timeRange, nodeId, nodeType, sourceId } = InfraApmMetricsRequestRT.decode(
req.payload
).getOrElseL(throwErrors(Boom.badRequest));
const { configuration } = await sources.getSourceConfiguration(req, sourceId);
const serviceNames = await getApmServices(
framework,
req,
configuration,
nodeId,
nodeType,
timeRange
);
const services = await Promise.all(
serviceNames.map(name =>
getApmServiceData(framework, req, configuration, name, nodeId, nodeType, timeRange)
)
);
return InfraApmMetricsRT.decode({ services }).getOrElseL(
throwErrors(Boom.badImplementation)
);
} catch (error) {
throw Boom.boomify(error);
}
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import moment from 'moment';
import { Legacy } from 'kibana';
import Boom from 'boom';
import { throwErrors } from '../../../../common/runtime_types';
import { InfraNodeType } from '../../../../common/http_api/common';
import { getIdFieldName } from '../../metadata/lib/get_id_field_name';
import {
InfraApmMetricsService,
APMChartResponseRT,
APMDataPoint,
APMTpmBuckets,
} from '../../../../common/http_api';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
} from '../../../lib/adapters/framework';
import { InfraSourceConfiguration } from '../../../lib/sources';

export const getApmServiceData = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
sourceConfiguration: InfraSourceConfiguration,
service: string,
nodeId: string,
nodeType: InfraNodeType,
timeRange: { min: number; max: number }
): Promise<InfraApmMetricsService> => {
const nodeField = getIdFieldName(sourceConfiguration, nodeType);
const params = new URLSearchParams({
start: moment(timeRange.min).toISOString(),
end: moment(timeRange.max).toISOString(),
transactionType: 'request',
uiFilters: JSON.stringify({ kuery: `${nodeField}: "${nodeId}"` }),
});
const res = await framework.makeInternalRequest(
req as InfraFrameworkRequest<Legacy.Request>,
`/api/apm/services/${service}/transaction_groups/charts?${params.toString()}`,
'GET'
);
if (res.statusCode !== 200) {
throw res;
}
const result = APMChartResponseRT.decode(res.result).getOrElseL(
throwErrors(message => Boom.badImplementation(`Request to APM Failed: ${message}`))
);
const { responseTimes, tpmBuckets } = result.apmTimeseries;
return {
name: service,
transactionsPerMinute: tpmBuckets.map(mapApmBucketToDataBucket),
responseTimes: [
createApmBucket('avg', responseTimes.avg),
createApmBucket('p95', responseTimes.p95),
createApmBucket('p99', responseTimes.p99),
],
};
};

const createApmBucket = (name: string, data: APMDataPoint[]) => {
return {
name,
data: data.map(p => ({ timestamp: p.x, value: p.y })),
};
};

const mapApmBucketToDataBucket = (bucket: APMTpmBuckets) =>
createApmBucket(bucket.key, bucket.dataPoints);
Loading

0 comments on commit b57bee7

Please sign in to comment.