[7.x] Infra server NP shim + config/routing API adoption (#45299) (#52613)

* Infra server NP shim + config/routing API adoption (#45299)
This commit is contained in:
Kerry Gallagher 2019-12-11 12:58:44 +00:00 committed by GitHub
parent 49ee79118d
commit a2df167306
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1268 additions and 1240 deletions

View file

@ -78,7 +78,10 @@ async function getParamsForSearchRequest(
) {
const { uiSettings } = context.core;
const [indices, includeFrozen] = await Promise.all([
getApmIndices(context),
getApmIndices({
savedObjectsClient: context.core.savedObjects.client,
config: context.config
}),
uiSettings.client.get('search:includeFrozen')
]);

View file

@ -50,6 +50,11 @@ function getMockRequest() {
client: {
get: jest.fn().mockResolvedValue(false)
}
},
savedObjects: {
client: {
get: jest.fn()
}
}
}
} as unknown) as APMRequestHandlerContext & {
@ -65,6 +70,11 @@ function getMockRequest() {
get: jest.Mock<any, any>;
};
};
savedObjects: {
client: {
get: jest.Mock<any, any>;
};
};
};
};

View file

@ -73,7 +73,10 @@ export async function setupRequest<TParams extends SetupRequestParams>(
const { config } = context;
const { query } = context.params;
const indices = await getApmIndices(context);
const indices = await getApmIndices({
savedObjectsClient: context.core.savedObjects.client,
config
});
const dynamicIndexPattern = await getDynamicIndexPattern({
context,

View file

@ -54,15 +54,25 @@ export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig {
};
}
export async function getApmIndices(context: APMRequestHandlerContext) {
// export async function getApmIndices(context: APMRequestHandlerContext) {
// return _getApmIndices(context.core, context.config);
// }
export async function getApmIndices({
config,
savedObjectsClient
}: {
config: APMConfig;
savedObjectsClient: SavedObjectsClientContract;
}) {
try {
const apmIndicesSavedObject = await getApmIndicesSavedObject(
context.core.savedObjects.client
savedObjectsClient
);
const apmIndicesConfig = getApmIndicesConfig(context.config);
const apmIndicesConfig = getApmIndicesConfig(config);
return merge({}, apmIndicesConfig, apmIndicesSavedObject);
} catch (error) {
return getApmIndicesConfig(context.config);
return getApmIndicesConfig(config);
}
}

View file

@ -26,7 +26,10 @@ export const apmIndicesRoute = createRoute(() => ({
method: 'GET',
path: '/api/apm/settings/apm-indices',
handler: async ({ context }) => {
return await getApmIndices(context);
return await getApmIndices({
savedObjectsClient: context.core.savedObjects.client,
config: context.config
});
}
}));

View file

@ -5,7 +5,6 @@
*/
import * as rt from 'io-ts';
import { InfraWrappableRequest } from '../../server/lib/adapters/framework';
export const InfraMetadataNodeTypeRT = rt.keyof({
host: null,
@ -67,6 +66,7 @@ export const InfraMetadataInfoRT = rt.partial({
});
const InfraMetadataRequiredRT = rt.type({
id: rt.string,
name: rt.string,
features: rt.array(InfraMetadataFeatureRT),
});
@ -81,8 +81,6 @@ export type InfraMetadata = rt.TypeOf<typeof InfraMetadataRT>;
export type InfraMetadataRequest = rt.TypeOf<typeof InfraMetadataRequestRT>;
export type InfraMetadataWrappedRequest = InfraWrappableRequest<InfraMetadataRequest>;
export type InfraMetadataFeature = rt.TypeOf<typeof InfraMetadataFeatureRT>;
export type InfraMetadataInfo = rt.TypeOf<typeof InfraMetadataInfoRT>;

View file

@ -6,7 +6,6 @@
import * as rt from 'io-ts';
import { InventoryMetricRT, ItemTypeRT } from '../inventory_models/types';
import { InfraWrappableRequest } from '../../server/lib/adapters/framework';
import { InfraTimerangeInputRT } from './snapshot_api';
const NodeDetailsDataPointRT = rt.intersection([
@ -53,6 +52,4 @@ export const NodeDetailsRequestRT = rt.intersection([
// export type NodeDetailsRequest = InfraWrappableRequest<NodesArgs & SourceArgs>;
export type NodeDetailsRequest = rt.TypeOf<typeof NodeDetailsRequestRT>;
export type NodeDetailsWrappedRequest = InfraWrappableRequest<NodeDetailsRequest>;
export type NodeDetailsMetricDataResponse = rt.TypeOf<typeof NodeDetailsMetricDataResponseRT>;

View file

@ -5,7 +5,6 @@
*/
import * as rt from 'io-ts';
import { InfraWrappableRequest } from '../../server/lib/adapters/framework';
import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types';
export const SnapshotNodePathRT = rt.intersection([
@ -64,6 +63,5 @@ export const SnapshotRequestRT = rt.intersection([
]);
export type SnapshotRequest = rt.TypeOf<typeof SnapshotRequestRT>;
export type SnapshotWrappedRequest = InfraWrappableRequest<SnapshotRequest>;
export type SnapshotNode = rt.TypeOf<typeof SnapshotNodeRT>;
export type SnapshotNodeResponse = rt.TypeOf<typeof SnapshotNodeResponseRT>;

View file

@ -7,9 +7,14 @@
import { i18n } from '@kbn/i18n';
import JoiNamespace from 'joi';
import { resolve } from 'path';
import { getConfigSchema, initServerWithKibana } from './server/kibana.index';
import { PluginInitializerContext } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import KbnServer from 'src/legacy/server/kbn_server';
import { getConfigSchema } from './server/kibana.index';
import { savedObjectMappings } from './server/saved_objects';
import { plugin, InfraServerPluginDeps } from './server/new_platform_index';
import { InfraSetup } from '../../../plugins/infra/server';
import { APMPluginContract } from '../../../plugins/apm/server/plugin';
const APP_ID = 'infra';
const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel', {
@ -70,9 +75,52 @@ export function infra(kibana: any) {
config(Joi: typeof JoiNamespace) {
return getConfigSchema(Joi);
},
init(server: any) {
initServerWithKibana(server);
server.addAppLinksToSampleDataset('logs', [
init(legacyServer: any) {
const { newPlatform } = legacyServer as KbnServer;
const { core, plugins } = newPlatform.setup;
const infraSetup = (plugins.infra as unknown) as InfraSetup; // chef's kiss
const initContext = ({
config: infraSetup.__legacy.config,
} as unknown) as PluginInitializerContext;
// NP_TODO: Use real types from the other plugins as they are migrated
const pluginDeps: InfraServerPluginDeps = {
usageCollection: plugins.usageCollection as UsageCollectionSetup,
indexPatterns: {
indexPatternsServiceFactory: legacyServer.indexPatternsServiceFactory,
},
metrics: legacyServer.plugins.metrics,
spaces: plugins.spaces,
features: plugins.features,
// NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that
// originate from the New Platform router (and are very different to the old request object).
// Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just
// the requestContext, this can be removed.
___legacy: {
tsvb: {
elasticsearch: legacyServer.plugins.elasticsearch,
__internals: legacyServer.newPlatform.__internals,
},
},
apm: plugins.apm as APMPluginContract,
};
const infraPluginInstance = plugin(initContext);
infraPluginInstance.setup(core, pluginDeps);
// NP_TODO: EVERYTHING BELOW HERE IS LEGACY
const libs = infraPluginInstance.getLibs();
// NP_TODO how do we replace this? Answer: return from setup function.
legacyServer.expose(
'defineInternalSourceConfiguration',
libs.sources.defineInternalSourceConfiguration.bind(libs.sources)
);
// NP_TODO: How do we move this to new platform?
legacyServer.addAppLinksToSampleDataset('logs', [
{
path: `/app/${APP_ID}#/logs`,
label: logsSampleDataLinkLabel,

View file

@ -24,7 +24,7 @@ import {
const ROOT_ELEMENT_ID = 'react-infra-root';
const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs';
export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter {
export class KibanaFramework implements InfraFrameworkAdapter {
public appState: object;
public kbnVersion?: string;
public timezone?: string;

View file

@ -20,7 +20,7 @@ import { HttpLink } from 'apollo-link-http';
import { withClientState } from 'apollo-link-state';
import { InfraFrontendLibs } from '../lib';
import introspectionQueryResultData from '../../graphql/introspection.json';
import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api';
export function compose(): InfraFrontendLibs {
@ -57,7 +57,7 @@ export function compose(): InfraFrontendLibs {
const infraModule = uiModules.get('app/infa');
const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider);
const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider);
const libs: InfraFrontendLibs = {
apolloClient,

View file

@ -17,7 +17,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import { SchemaLink } from 'apollo-link-schema';
import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools';
import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api';
import { InfraFrontendLibs } from '../lib';
@ -27,7 +27,7 @@ export function compose(): InfraFrontendLibs {
basePath: chrome.getBasePath(),
xsrfToken: chrome.getXsrfToken(),
});
const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider);
const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider);
const typeDefs = `
Query {}
`;

View file

@ -0,0 +1,65 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const METRICS_FEATURE = {
id: 'infrastructure',
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
defaultMessage: 'Infrastructure',
}),
icon: 'infraApp',
navLinkId: 'infra:home',
app: ['infra', 'kibana'],
catalogue: ['infraops'],
privileges: {
all: {
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
read: ['index-pattern'],
},
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],
savedObject: {
all: [],
read: ['infrastructure-ui-source', 'index-pattern'],
},
ui: ['show'],
},
},
};
export const LOGS_FEATURE = {
id: 'logs',
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
defaultMessage: 'Logs',
}),
icon: 'loggingApp',
navLinkId: 'infra:logs',
app: ['infra', 'kibana'],
catalogue: ['infralogging'],
privileges: {
all: {
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
read: [],
},
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],
savedObject: {
all: [],
read: ['infrastructure-ui-source'],
},
ui: ['show'],
},
},
};

View file

@ -30,7 +30,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
typeDefs: schemas,
});
libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema);
libs.framework.registerGraphQLEndpoint('/graphql', schema);
initIpToHostName(libs);
initLogAnalysisGetLogEntryRateRoute(libs);

View file

@ -4,97 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { Server } from 'hapi';
import JoiNamespace from 'joi';
import { initInfraServer } from './infra_server';
import { compose } from './lib/compose/kibana';
import { UsageCollector } from './usage/usage_collector';
import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view';
import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view';
export const initServerWithKibana = (kbnServer: Server) => {
const { usageCollection } = kbnServer.newPlatform.setup.plugins;
const libs = compose(kbnServer);
initInfraServer(libs);
kbnServer.expose(
'defineInternalSourceConfiguration',
libs.sources.defineInternalSourceConfiguration.bind(libs.sources)
);
// Register a function with server to manage the collection of usage stats
UsageCollector.registerUsageCollector(usageCollection);
const xpackMainPlugin = kbnServer.plugins.xpack_main;
xpackMainPlugin.registerFeature({
id: 'infrastructure',
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
defaultMessage: 'Metrics',
}),
icon: 'metricsApp',
navLinkId: 'infra:home',
app: ['infra', 'kibana'],
catalogue: ['infraops'],
privileges: {
all: {
api: ['infra'],
savedObject: {
all: [
'infrastructure-ui-source',
inventoryViewSavedObjectType,
metricsExplorerViewSavedObjectType,
],
read: ['index-pattern'],
},
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],
savedObject: {
all: [],
read: [
'infrastructure-ui-source',
'index-pattern',
inventoryViewSavedObjectType,
metricsExplorerViewSavedObjectType,
],
},
ui: ['show'],
},
},
});
xpackMainPlugin.registerFeature({
id: 'logs',
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
defaultMessage: 'Logs',
}),
icon: 'logsApp',
navLinkId: 'infra:logs',
app: ['infra', 'kibana'],
catalogue: ['infralogging'],
privileges: {
all: {
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
read: [],
},
ui: ['show', 'configureSource', 'save'],
},
read: {
api: ['infra'],
savedObject: {
all: [],
read: ['infrastructure-ui-source'],
},
ui: ['show'],
},
},
});
};
export interface KbnServer extends Server {
usage: any;
}
// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP
export const getConfigSchema = (Joi: typeof JoiNamespace) => {
const InfraDefaultSourceConfigSchema = Joi.object({
metricAlias: Joi.string(),
@ -111,6 +28,7 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => {
}),
});
// NP_TODO: make sure this is all represented in the NP config schema
const InfraRootConfigSchema = Joi.object({
enabled: Joi.boolean().default(true),
query: Joi.object({

View file

@ -1,19 +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;
* you may not use this file except in compliance with the Elastic License.
*/
export interface InfraConfigurationAdapter<
Configuration extends InfraBaseConfiguration = InfraBaseConfiguration
> {
get(): Promise<Configuration>;
}
export interface InfraBaseConfiguration {
enabled: boolean;
query: {
partitionSize: number;
partitionFactor: number;
};
}

View file

@ -1,7 +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;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './adapter_types';

View file

@ -1,16 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types';
export class InfraInmemoryConfigurationAdapter<Configuration extends InfraBaseConfiguration>
implements InfraConfigurationAdapter<Configuration> {
constructor(private readonly configuration: Configuration) {}
public async get() {
return this.configuration;
}
}

View file

@ -1,40 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraKibanaConfigurationAdapter } from './kibana_configuration_adapter';
describe('the InfraKibanaConfigurationAdapter', () => {
test('queries the xpack.infra configuration of the server', async () => {
const mockConfig = {
get: jest.fn(),
};
const configurationAdapter = new InfraKibanaConfigurationAdapter({
config: () => mockConfig,
});
await configurationAdapter.get();
expect(mockConfig.get).toBeCalledWith('xpack.infra');
});
test('applies the query defaults', async () => {
const configurationAdapter = new InfraKibanaConfigurationAdapter({
config: () => ({
get: () => ({}),
}),
});
const configuration = await configurationAdapter.get();
expect(configuration).toMatchObject({
query: {
partitionSize: expect.any(Number),
partitionFactor: expect.any(Number),
},
});
});
});

View file

@ -1,73 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types';
export class InfraKibanaConfigurationAdapter implements InfraConfigurationAdapter {
private readonly server: ServerWithConfig;
constructor(server: any) {
if (!isServerWithConfig(server)) {
throw new Error('Failed to find configuration on server.');
}
this.server = server;
}
public async get() {
const config = this.server.config();
if (!isKibanaConfiguration(config)) {
throw new Error('Failed to access configuration of server.');
}
const configuration = config.get('xpack.infra') || {};
const configurationWithDefaults: InfraBaseConfiguration = {
enabled: true,
query: {
partitionSize: 75,
partitionFactor: 1.2,
...(configuration.query || {}),
},
...configuration,
};
// we assume this to be the configuration because Kibana would have already validated it
return configurationWithDefaults;
}
}
interface ServerWithConfig {
config(): any;
}
function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig {
return (
Joi.validate(
maybeServer,
Joi.object({
config: Joi.func().required(),
}).unknown()
).error === null
);
}
interface KibanaConfiguration {
get(key: string): any;
}
function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration {
return (
Joi.validate(
maybeConfiguration,
Joi.object({
get: Joi.func().required(),
}).unknown()
).error === null
);
}

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraFrameworkRequest } from '../framework';
import { RequestHandlerContext } from 'src/core/server';
export interface FieldsAdapter {
getIndexFields(
req: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
indices: string,
timefield: string
): Promise<IndexFieldDescriptor[]>;

View file

@ -5,11 +5,9 @@
*/
import { startsWith, uniq, first } from 'lodash';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
InfraDatabaseSearchResponse,
} from '../framework';
import { RequestHandlerContext } from 'src/core/server';
import { InfraDatabaseSearchResponse } from '../framework';
import { KibanaFramework } from '../framework/kibana_framework_adapter';
import { FieldsAdapter, IndexFieldDescriptor } from './adapter_types';
import { getAllowedListForPrefix } from '../../../../common/ecs_allowed_list';
import { getAllCompositeData } from '../../../utils/get_all_composite_data';
@ -30,22 +28,26 @@ interface DataSetResponse {
}
export class FrameworkFieldsAdapter implements FieldsAdapter {
private framework: InfraBackendFrameworkAdapter;
private framework: KibanaFramework;
constructor(framework: InfraBackendFrameworkAdapter) {
constructor(framework: KibanaFramework) {
this.framework = framework;
}
public async getIndexFields(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
indices: string,
timefield: string
): Promise<IndexFieldDescriptor[]> {
const indexPatternsService = this.framework.getIndexPatternsService(request);
const indexPatternsService = this.framework.getIndexPatternsService(requestContext);
const response = await indexPatternsService.getFieldsForWildcard({
pattern: indices,
});
const { dataSets, modules } = await this.getDataSetsAndModules(request, indices, timefield);
const { dataSets, modules } = await this.getDataSetsAndModules(
requestContext,
indices,
timefield
);
const allowedList = modules.reduce(
(acc, name) => uniq([...acc, ...getAllowedListForPrefix(name)]),
[] as string[]
@ -58,7 +60,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter {
}
private async getDataSetsAndModules(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
indices: string,
timefield: string
): Promise<{ dataSets: string[]; modules: string[] }> {
@ -109,7 +111,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter {
const buckets = await getAllCompositeData<DataSetResponse, Bucket>(
this.framework,
request,
requestContext,
params,
bucketSelector,
handleAfterKey

View file

@ -4,91 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
import { GraphQLSchema } from 'graphql';
import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi';
import { Legacy } from 'kibana';
import { SearchResponse, GenericParams } from 'elasticsearch';
import { Lifecycle } from 'hapi';
import { ObjectType } from '@kbn/config-schema';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server';
import { APMPluginContract } from '../../../../../../../plugins/apm/server/plugin';
import { KibanaConfig } from 'src/legacy/server/kbn_server';
import { JsonObject } from '../../../../common/typed_json';
import { TSVBMetricModel } from '../../../../common/inventory_models/types';
export const internalInfraFrameworkRequest = Symbol('internalInfraFrameworkRequest');
/* eslint-disable @typescript-eslint/unified-signatures */
export interface InfraBackendFrameworkAdapter {
version: string;
exposeStaticDir(urlPath: string, dir: string): void;
registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void;
registerRoute<RouteRequest extends InfraWrappableRequest, RouteResponse extends InfraResponse>(
route: InfraFrameworkRouteOptions<RouteRequest, RouteResponse>
): void;
callWithRequest<Hit = {}, Aggregation = undefined>(
req: InfraFrameworkRequest,
method: 'search',
options?: object
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>>;
callWithRequest<Hit = {}, Aggregation = undefined>(
req: InfraFrameworkRequest,
method: 'msearch',
options?: object
): Promise<InfraDatabaseMultiResponse<Hit, Aggregation>>;
callWithRequest(
req: InfraFrameworkRequest,
method: 'fieldCaps',
options?: object
): Promise<InfraDatabaseFieldCapsResponse>;
callWithRequest(
req: InfraFrameworkRequest,
method: 'indices.existsAlias',
options?: object
): Promise<boolean>;
callWithRequest(
req: InfraFrameworkRequest,
method: 'indices.getAlias',
options?: object
): Promise<InfraDatabaseGetIndicesAliasResponse>;
callWithRequest(
req: InfraFrameworkRequest,
method: 'indices.get',
options?: object
): Promise<InfraDatabaseGetIndicesResponse>;
callWithRequest(
req: InfraFrameworkRequest,
method: 'ml.getBuckets',
options?: object
): Promise<InfraDatabaseGetIndicesResponse>;
callWithRequest(
req: InfraFrameworkRequest,
method: string,
options?: object
): Promise<InfraDatabaseSearchResponse>;
getIndexPatternsService(req: InfraFrameworkRequest<any>): Legacy.IndexPatternsService;
getSavedObjectsService(): Legacy.SavedObjectsService;
getSpaceId(request: InfraFrameworkRequest<any>): string;
makeTSVBRequest(
req: InfraFrameworkRequest,
model: TSVBMetricModel,
timerange: { min: number; max: number },
filters: JsonObject[]
): Promise<InfraTSVBResponse>;
config(req: InfraFrameworkRequest): KibanaConfig;
}
/* eslint-enable @typescript-eslint/unified-signatures */
export interface InfraFrameworkRequest<
InternalRequest extends InfraWrappableRequest = InfraWrappableRequest
> {
[internalInfraFrameworkRequest]: InternalRequest;
payload: InternalRequest['payload'];
params: InternalRequest['params'];
query: InternalRequest['query'];
// NP_TODO: Compose real types from plugins we depend on, no "any"
export interface InfraServerPluginDeps {
usageCollection: UsageCollectionSetup;
spaces: any;
metrics: {
getVisData: any;
};
indexPatterns: {
indexPatternsServiceFactory: any;
};
features: any;
apm: APMPluginContract;
___legacy: any;
}
export interface InfraWrappableRequest<Payload = any, Params = any, Query = any> {
payload: Payload;
params: Params;
query: Query;
export interface CallWithRequestParams extends GenericParams {
max_concurrent_shard_requests?: number;
name?: string;
index?: string;
ignore_unavailable?: boolean;
allow_no_indices?: boolean;
size?: number;
terminate_after?: number;
fields?: string;
}
export type InfraResponse = Lifecycle.ReturnValue;
@ -98,22 +44,6 @@ export interface InfraFrameworkPluginOptions {
options: any;
}
export interface InfraFrameworkRouteOptions<
RouteRequest extends InfraWrappableRequest,
RouteResponse extends InfraResponse
> {
path: string;
method: string | string[];
vhost?: string;
handler: InfraFrameworkRouteHandler<RouteRequest, RouteResponse>;
options?: Pick<RouteOptions, Exclude<keyof RouteOptions, 'handler'>>;
}
export type InfraFrameworkRouteHandler<
RouteRequest extends InfraWrappableRequest,
RouteResponse extends InfraResponse
> = (request: InfraFrameworkRequest<RouteRequest>, h: ResponseToolkit) => RouteResponse;
export interface InfraDatabaseResponse {
took: number;
timeout: boolean;
@ -232,3 +162,12 @@ export interface InfraTSVBSeries {
}
export type InfraTSVBDataPoint = [number, number];
export type InfraRouteConfig<
params extends ObjectType,
query extends ObjectType,
body extends ObjectType,
method extends RouteMethod
> = {
method: RouteMethod;
} & RouteConfig<params, query, body, method>;

View file

@ -1,117 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import * as GraphiQL from 'apollo-server-module-graphiql';
import Boom from 'boom';
import { Plugin, Request, ResponseToolkit, RouteOptions, Server } from 'hapi';
import { GraphQLOptions, runHttpQuery } from 'apollo-server-core';
export type HapiOptionsFunction = (req: Request) => GraphQLOptions | Promise<GraphQLOptions>;
export interface HapiGraphQLPluginOptions {
path: string;
vhost?: string;
route?: RouteOptions;
graphqlOptions: GraphQLOptions | HapiOptionsFunction;
}
export const graphqlHapi: Plugin<HapiGraphQLPluginOptions> = {
name: 'graphql',
register: (server: Server, options: HapiGraphQLPluginOptions) => {
if (!options || !options.graphqlOptions) {
throw new Error('Apollo Server requires options.');
}
server.route({
options: options.route || {},
handler: async (request: Request, h: ResponseToolkit) => {
try {
const query =
request.method === 'post'
? (request.payload as Record<string, any>)
: (request.query as Record<string, any>);
const gqlResponse = await runHttpQuery([request], {
method: request.method.toUpperCase(),
options: options.graphqlOptions,
query,
});
return h.response(gqlResponse).type('application/json');
} catch (error) {
if ('HttpQueryError' !== error.name) {
const queryError = Boom.boomify(error);
queryError.output.payload.message = error.message;
return queryError;
}
if (error.isGraphQLError === true) {
return h
.response(error.message)
.code(error.statusCode)
.type('application/json');
}
const genericError = new Boom(error.message, { statusCode: error.statusCode });
if (error.headers) {
Object.keys(error.headers).forEach(header => {
genericError.output.headers[header] = error.headers[header];
});
}
// Boom hides the error when status code is 500
genericError.output.payload.message = error.message;
throw genericError;
}
},
method: ['GET', 'POST'],
path: options.path || '/graphql',
vhost: options.vhost || undefined,
});
},
};
export type HapiGraphiQLOptionsFunction = (
req?: Request
) => GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
export interface HapiGraphiQLPluginOptions {
path: string;
route?: any;
graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction;
}
export const graphiqlHapi: Plugin<HapiGraphiQLPluginOptions> = {
name: 'graphiql',
register: (server: Server, options: HapiGraphiQLPluginOptions) => {
if (!options || !options.graphiqlOptions) {
throw new Error('Apollo Server GraphiQL requires options.');
}
server.route({
options: options.route || {},
handler: async (request: Request, h: ResponseToolkit) => {
const graphiqlString = await GraphiQL.resolveGraphiQLString(
request.query,
options.graphiqlOptions,
request
);
return h.response(graphiqlString).type('text/html');
},
method: 'GET',
path: options.path || '/graphiql',
});
},
};

View file

@ -4,116 +4,207 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/array-type */
import { GenericParams } from 'elasticsearch';
import { GraphQLSchema } from 'graphql';
import { Legacy } from 'kibana';
import { KibanaConfig } from 'src/legacy/server/kbn_server';
import { get } from 'lodash';
import { runHttpQuery } from 'apollo-server-core';
import { schema, TypeOf, ObjectType } from '@kbn/config-schema';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
InfraFrameworkRouteOptions,
InfraResponse,
InfraRouteConfig,
InfraTSVBResponse,
InfraWrappableRequest,
internalInfraFrameworkRequest,
InfraServerPluginDeps,
CallWithRequestParams,
InfraDatabaseSearchResponse,
InfraDatabaseMultiResponse,
InfraDatabaseFieldCapsResponse,
InfraDatabaseGetIndicesResponse,
InfraDatabaseGetIndicesAliasResponse,
} from './adapter_types';
import {
graphiqlHapi,
graphqlHapi,
HapiGraphiQLPluginOptions,
HapiGraphQLPluginOptions,
} from './apollo_server_hapi';
import { TSVBMetricModel } from '../../../../common/inventory_models/types';
import {
CoreSetup,
IRouter,
KibanaRequest,
RequestHandlerContext,
KibanaResponseFactory,
RouteMethod,
} from '../../../../../../../../src/core/server';
import { RequestHandler } from '../../../../../../../../src/core/server';
import { InfraConfig } from '../../../../../../../plugins/infra/server';
interface CallWithRequestParams extends GenericParams {
max_concurrent_shard_requests?: number;
}
export class KibanaFramework {
public router: IRouter;
private core: CoreSetup;
public plugins: InfraServerPluginDeps;
export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFrameworkAdapter {
public version: string;
constructor(private server: Legacy.Server) {
this.version = server.config().get('pkg.version');
}
public config(req: InfraFrameworkRequest<Legacy.Request>): KibanaConfig {
const internalRequest = req[internalInfraFrameworkRequest];
return internalRequest.server.config();
}
public exposeStaticDir(urlPath: string, dir: string): void {
this.server.route({
handler: {
directory: {
path: dir,
},
},
method: 'GET',
path: urlPath,
});
}
public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void {
this.server.register<HapiGraphQLPluginOptions>({
options: {
graphqlOptions: (req: Legacy.Request) => ({
context: { req: wrapRequest(req) },
schema,
}),
path: routePath,
route: {
tags: ['access:infra'],
},
},
plugin: graphqlHapi,
});
this.server.register<HapiGraphiQLPluginOptions>({
options: {
graphiqlOptions: request => ({
endpointURL: request ? `${request.getBasePath()}${routePath}` : routePath,
passHeader: `'kbn-version': '${this.version}'`,
}),
path: `${routePath}/graphiql`,
route: {
tags: ['access:infra'],
},
},
plugin: graphiqlHapi,
});
constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) {
this.router = core.http.createRouter();
this.core = core;
this.plugins = plugins;
}
public registerRoute<
RouteRequest extends InfraWrappableRequest,
RouteResponse extends InfraResponse
>(route: InfraFrameworkRouteOptions<RouteRequest, RouteResponse>) {
const wrappedHandler = (request: any, h: Legacy.ResponseToolkit) =>
route.handler(wrapRequest(request), h);
this.server.route({
handler: wrappedHandler,
options: route.options,
method: route.method,
path: route.path,
});
params extends ObjectType = any,
query extends ObjectType = any,
body extends ObjectType = any,
method extends RouteMethod = any
>(
config: InfraRouteConfig<params, query, body, method>,
handler: RequestHandler<params, query, body>
) {
const defaultOptions = {
tags: ['access:infra'],
};
const routeConfig = {
path: config.path,
validate: config.validate,
// Currently we have no use of custom options beyond tags, this can be extended
// beyond defaultOptions if it's needed.
options: defaultOptions,
};
switch (config.method) {
case 'get':
this.router.get(routeConfig, handler);
break;
case 'post':
this.router.post(routeConfig, handler);
break;
case 'delete':
this.router.delete(routeConfig, handler);
break;
case 'put':
this.router.put(routeConfig, handler);
break;
}
}
public async callWithRequest(
req: InfraFrameworkRequest<Legacy.Request>,
public registerGraphQLEndpoint(routePath: string, gqlSchema: GraphQLSchema) {
// These endpoints are validated by GraphQL at runtime and with GraphQL generated types
const body = schema.object({}, { allowUnknowns: true });
type Body = TypeOf<typeof body>;
const routeOptions = {
path: `/api/infra${routePath}`,
validate: {
body,
},
options: {
tags: ['access:infra'],
},
};
async function handler(
context: RequestHandlerContext,
request: KibanaRequest<unknown, unknown, Body>,
response: KibanaResponseFactory
) {
try {
const query =
request.route.method === 'post'
? (request.body as Record<string, any>)
: (request.query as Record<string, any>);
const gqlResponse = await runHttpQuery([context, request], {
method: request.route.method.toUpperCase(),
options: (req: RequestHandlerContext, rawReq: KibanaRequest) => ({
context: { req, rawReq },
schema: gqlSchema,
}),
query,
});
return response.ok({
body: gqlResponse,
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
const errorBody = {
message: error.message,
};
if ('HttpQueryError' !== error.name) {
return response.internalError({
body: errorBody,
});
}
if (error.isGraphQLError === true) {
return response.customError({
statusCode: error.statusCode,
body: errorBody,
headers: {
'Content-Type': 'application/json',
},
});
}
const { headers = [], statusCode = 500 } = error;
return response.customError({
statusCode,
headers,
body: errorBody,
});
// NP_TODO: Do we still need to re-throw this error in this case? if we do, can we
// still call the response.customError method to control the HTTP response?
// throw error;
}
}
this.router.post(routeOptions, handler);
this.router.get(routeOptions, handler);
}
callWithRequest<Hit = {}, Aggregation = undefined>(
requestContext: RequestHandlerContext,
endpoint: 'search',
options?: CallWithRequestParams
): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>>;
callWithRequest<Hit = {}, Aggregation = undefined>(
requestContext: RequestHandlerContext,
endpoint: 'msearch',
options?: CallWithRequestParams
): Promise<InfraDatabaseMultiResponse<Hit, Aggregation>>;
callWithRequest(
requestContext: RequestHandlerContext,
endpoint: 'fieldCaps',
options?: CallWithRequestParams
): Promise<InfraDatabaseFieldCapsResponse>;
callWithRequest(
requestContext: RequestHandlerContext,
endpoint: 'indices.existsAlias',
options?: CallWithRequestParams
): Promise<boolean>;
callWithRequest(
requestContext: RequestHandlerContext,
method: 'indices.getAlias',
options?: object
): Promise<InfraDatabaseGetIndicesAliasResponse>;
callWithRequest(
requestContext: RequestHandlerContext,
method: 'indices.get' | 'ml.getBuckets',
options?: object
): Promise<InfraDatabaseGetIndicesResponse>;
callWithRequest(
requestContext: RequestHandlerContext,
endpoint: string,
params: CallWithRequestParams,
...rest: any[]
options?: CallWithRequestParams
): Promise<InfraDatabaseSearchResponse>;
public async callWithRequest<Hit = {}, Aggregation = undefined>(
requestContext: RequestHandlerContext,
endpoint: string,
params: CallWithRequestParams
) {
const internalRequest = req[internalInfraFrameworkRequest];
const { elasticsearch } = internalRequest.server.plugins;
const { callWithRequest } = elasticsearch.getCluster('data');
const includeFrozen = await internalRequest.getUiSettingsService().get('search:includeFrozen');
const { elasticsearch, uiSettings } = requestContext.core;
const includeFrozen = await uiSettings.client.get('search:includeFrozen');
if (endpoint === 'msearch') {
const maxConcurrentShardRequests = await internalRequest
.getUiSettingsService()
.get('courier:maxConcurrentShardRequests');
const maxConcurrentShardRequests = await uiSettings.client.get(
'courier:maxConcurrentShardRequests'
);
if (maxConcurrentShardRequests > 0) {
params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests };
}
@ -125,95 +216,79 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
}
: {};
const fields = await callWithRequest(
internalRequest,
endpoint,
{
...params,
...frozenIndicesParams,
},
...rest
);
return fields;
return elasticsearch.dataClient.callAsCurrentUser(endpoint, {
...params,
...frozenIndicesParams,
});
}
public getIndexPatternsService(
request: InfraFrameworkRequest<Legacy.Request>
requestContext: RequestHandlerContext
): Legacy.IndexPatternsService {
return this.server.indexPatternsServiceFactory({
return this.plugins.indexPatterns.indexPatternsServiceFactory({
callCluster: async (method: string, args: [GenericParams], ...rest: any[]) => {
const fieldCaps = await this.callWithRequest(
request,
method,
{ ...args, allowNoIndices: true } as GenericParams,
...rest
);
const fieldCaps = await this.callWithRequest(requestContext, method, {
...args,
allowNoIndices: true,
} as GenericParams);
return fieldCaps;
},
});
}
public getSpaceId(request: InfraFrameworkRequest): string {
const spacesPlugin = this.server.plugins.spaces;
public getSpaceId(request: KibanaRequest): string {
const spacesPlugin = this.plugins.spaces;
if (spacesPlugin && typeof spacesPlugin.getSpaceId === 'function') {
return spacesPlugin.getSpaceId(request[internalInfraFrameworkRequest]);
if (
spacesPlugin &&
spacesPlugin.spacesService &&
typeof spacesPlugin.spacesService.getSpaceId === 'function'
) {
return spacesPlugin.spacesService.getSpaceId(request);
} else {
return 'default';
}
}
public getSavedObjectsService() {
return this.server.savedObjects;
}
// NP_TODO: This method needs to no longer require full KibanaRequest
public async makeTSVBRequest(
req: InfraFrameworkRequest<Legacy.Request>,
request: KibanaRequest,
model: TSVBMetricModel,
timerange: { min: number; max: number },
filters: any[]
) {
const internalRequest = req[internalInfraFrameworkRequest];
const server = internalRequest.server;
const getVisData = get(server, 'plugins.metrics.getVisData');
filters: any[],
requestContext: RequestHandlerContext
): Promise<InfraTSVBResponse> {
const { getVisData } = this.plugins.metrics;
if (typeof getVisData !== 'function') {
throw new Error('TSVB is not available');
}
// getBasePath returns randomized base path AND spaces path
const basePath = internalRequest.getBasePath();
const url = `${basePath}/api/metrics/vis/data`;
const url = this.core.http.basePath.prepend('/api/metrics/vis/data');
// For the following request we need a copy of the instnace of the internal request
// but modified for our TSVB request. This will ensure all the instance methods
// are available along with our overriden values
const request = Object.assign(
Object.create(Object.getPrototypeOf(internalRequest)),
internalRequest,
{
url,
method: 'POST',
payload: {
timerange,
panels: [model],
filters,
const requestCopy = Object.assign({}, request, {
url,
method: 'POST',
payload: {
timerange,
panels: [model],
filters,
},
// NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that
// originate from the New Platform router (and are very different to the old request object).
// Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just
// the requestContext, this can be removed.
server: {
plugins: {
elasticsearch: this.plugins.___legacy.tsvb.elasticsearch,
},
}
);
const result = await getVisData(request);
return result as InfraTSVBResponse;
newPlatform: {
__internals: this.plugins.___legacy.tsvb.__internals,
},
},
getUiSettingsService: () => requestContext.core.uiSettings.client,
getSavedObjectsClient: () => requestContext.core.savedObjects.client,
});
return getVisData(requestCopy);
}
}
export function wrapRequest<InternalRequest extends InfraWrappableRequest>(
req: InternalRequest
): InfraFrameworkRequest<InternalRequest> {
const { params, payload, query } = req;
return {
[internalInfraFrameworkRequest]: req,
params,
payload,
query,
};
}

View file

@ -15,6 +15,7 @@ import zip from 'lodash/fp/zip';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity, constant } from 'fp-ts/lib/function';
import { RequestHandlerContext } from 'src/core/server';
import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time';
import { JsonObject } from '../../../../common/typed_json';
import {
@ -24,8 +25,8 @@ import {
LogSummaryBucket,
} from '../../domains/log_entries_domain';
import { InfraSourceConfiguration } from '../../sources';
import { InfraFrameworkRequest, SortedSearchHit } from '../framework';
import { InfraBackendFrameworkAdapter } from '../framework';
import { SortedSearchHit } from '../framework';
import { KibanaFramework } from '../framework/kibana_framework_adapter';
const DAY_MILLIS = 24 * 60 * 60 * 1000;
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS);
@ -39,10 +40,10 @@ interface LogItemHit {
}
export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
constructor(private readonly framework: InfraBackendFrameworkAdapter) {}
constructor(private readonly framework: KibanaFramework) {}
public async getAdjacentLogEntryDocuments(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
fields: string[],
start: TimeKey,
@ -64,7 +65,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
}
const documentsInInterval = await this.getLogEntryDocumentsBetween(
request,
requestContext,
sourceConfiguration,
fields,
intervalStart,
@ -82,7 +83,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
}
public async getContainedLogEntryDocuments(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
fields: string[],
start: TimeKey,
@ -91,7 +92,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
highlightQuery?: LogEntryQuery
): Promise<LogEntryDocument[]> {
const documents = await this.getLogEntryDocumentsBetween(
request,
requestContext,
sourceConfiguration,
fields,
start.time,
@ -106,7 +107,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
}
public async getContainedLogSummaryBuckets(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
start: number,
end: number,
@ -165,7 +166,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
},
};
const response = await this.framework.callWithRequest<any, {}>(request, 'search', query);
const response = await this.framework.callWithRequest<any, {}>(requestContext, 'search', query);
return pipe(
LogSummaryResponseRuntimeType.decode(response),
@ -179,12 +180,12 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
}
public async getLogItem(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
id: string,
sourceConfiguration: InfraSourceConfiguration
) {
const search = (searchOptions: object) =>
this.framework.callWithRequest<LogItemHit, {}>(request, 'search', searchOptions);
this.framework.callWithRequest<LogItemHit, {}>(requestContext, 'search', searchOptions);
const params = {
index: sourceConfiguration.logAlias,
@ -212,7 +213,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
}
private async getLogEntryDocumentsBetween(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
fields: string[],
start: number,
@ -298,7 +299,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
};
const response = await this.framework.callWithRequest<SortedSearchHit>(
request,
requestContext,
'search',
query
);

View file

@ -4,15 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext, KibanaRequest } from 'src/core/server';
import {
InfraMetric,
InfraMetricData,
InfraNodeType,
InfraTimerangeInput,
} from '../../../graphql/types';
import { InfraSourceConfiguration } from '../../sources';
import { InfraFrameworkRequest } from '../framework';
export interface InfraMetricsRequestOptions {
nodeIds: {
@ -27,8 +26,9 @@ export interface InfraMetricsRequestOptions {
export interface InfraMetricsAdapter {
getMetrics(
req: InfraFrameworkRequest,
options: InfraMetricsRequestOptions
requestContext: RequestHandlerContext,
options: InfraMetricsRequestOptions,
request: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request
): Promise<InfraMetricData[]>;
}

View file

@ -6,10 +6,9 @@
import { i18n } from '@kbn/i18n';
import { flatten, get } from 'lodash';
import Boom from 'boom';
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types';
import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework';
import { KibanaFramework } from '../framework/kibana_framework_adapter';
import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types';
import { checkValidNode } from './lib/check_valid_node';
import { metrics } from '../../../../common/inventory_models';
@ -17,15 +16,16 @@ import { TSVBMetricModelCreator } from '../../../../common/inventory_models/type
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
export class KibanaMetricsAdapter implements InfraMetricsAdapter {
private framework: InfraBackendFrameworkAdapter;
private framework: KibanaFramework;
constructor(framework: InfraBackendFrameworkAdapter) {
constructor(framework: KibanaFramework) {
this.framework = framework;
}
public async getMetrics(
req: InfraFrameworkRequest,
options: InfraMetricsRequestOptions
requestContext: RequestHandlerContext,
options: InfraMetricsRequestOptions,
rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request
): Promise<InfraMetricData[]> {
const fields = {
[InfraNodeType.host]: options.sourceConfiguration.fields.host,
@ -35,11 +35,11 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`;
const nodeField = fields[options.nodeType];
const search = <Aggregation>(searchOptions: object) =>
this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions);
this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions);
const validNode = await checkValidNode(search, indexPattern, nodeField, options.nodeIds.nodeId);
if (!validNode) {
throw Boom.notFound(
throw new Error(
i18n.translate('xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage', {
defaultMessage: '{nodeId} does not exist.',
values: {
@ -50,7 +50,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
}
const requests = options.metrics.map(metricId =>
this.makeTSVBRequest(metricId, options, req, nodeField)
this.makeTSVBRequest(metricId, options, rawRequest, nodeField, requestContext)
);
return Promise.all(requests)
@ -92,12 +92,13 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
async makeTSVBRequest(
metricId: InfraMetric,
options: InfraMetricsRequestOptions,
req: InfraFrameworkRequest,
nodeField: string
req: KibanaRequest,
nodeField: string,
requestContext: RequestHandlerContext
) {
const createTSVBModel = get(metrics, ['tsvb', metricId]) as TSVBMetricModelCreator | undefined;
if (!createTSVBModel) {
throw Boom.badRequest(
throw new Error(
i18n.translate('xpack.infra.metrics.missingTSVBModelError', {
defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}',
values: {
@ -121,7 +122,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
);
const calculatedInterval = await calculateMetricInterval(
this.framework,
req,
requestContext,
{
indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`,
timestampField: options.sourceConfiguration.fields.timestamp,
@ -135,7 +136,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
}
if (model.id_type === 'cloud' && !options.nodeIds.cloudId) {
throw Boom.badRequest(
throw new Error(
i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', {
defaultMessage:
'Model for {metricId} requires a cloudId, but none was given for {nodeId}.',
@ -152,6 +153,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
? [{ match: { [model.map_field_to]: id } }]
: [{ match: { [nodeField]: id } }];
return this.framework.makeTSVBRequest(req, model, timerange, filters);
return this.framework.makeTSVBRequest(req, model, timerange, filters, requestContext);
}
}

View file

@ -4,26 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext } from 'src/core/server';
import { InfraSourceStatusAdapter } from '../../source_status';
import {
InfraBackendFrameworkAdapter,
InfraDatabaseGetIndicesResponse,
InfraFrameworkRequest,
} from '../framework';
import { InfraDatabaseGetIndicesResponse } from '../framework';
import { KibanaFramework } from '../framework/kibana_framework_adapter';
export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusAdapter {
constructor(private readonly framework: InfraBackendFrameworkAdapter) {}
constructor(private readonly framework: KibanaFramework) {}
public async getIndexNames(request: InfraFrameworkRequest, aliasName: string) {
public async getIndexNames(requestContext: RequestHandlerContext, aliasName: string) {
const indexMaps = await Promise.all([
this.framework
.callWithRequest(request, 'indices.getAlias', {
.callWithRequest(requestContext, 'indices.getAlias', {
name: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
.catch(withDefaultIfNotFound<InfraDatabaseGetIndicesResponse>({})),
this.framework
.callWithRequest(request, 'indices.get', {
.callWithRequest(requestContext, 'indices.get', {
index: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
@ -36,15 +34,15 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA
);
}
public async hasAlias(request: InfraFrameworkRequest, aliasName: string) {
return await this.framework.callWithRequest(request, 'indices.existsAlias', {
public async hasAlias(requestContext: RequestHandlerContext, aliasName: string) {
return await this.framework.callWithRequest(requestContext, 'indices.existsAlias', {
name: aliasName,
});
}
public async hasIndices(request: InfraFrameworkRequest, indexNames: string) {
public async hasIndices(requestContext: RequestHandlerContext, indexNames: string) {
return await this.framework
.callWithRequest(request, 'search', {
.callWithRequest(requestContext, 'search', {
ignore_unavailable: true,
allow_no_indices: true,
index: indexNames,

View file

@ -3,12 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Server } from 'hapi';
import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter';
import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter';
import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter';
import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter';
import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status';
@ -20,13 +16,14 @@ import { InfraLogAnalysis } from '../log_analysis';
import { InfraSnapshot } from '../snapshot';
import { InfraSourceStatus } from '../source_status';
import { InfraSources } from '../sources';
import { InfraConfig } from '../../../../../../plugins/infra/server';
import { CoreSetup } from '../../../../../../../src/core/server';
import { InfraServerPluginDeps } from '../adapters/framework/adapter_types';
export function compose(server: Server): InfraBackendLibs {
const configuration = new InfraKibanaConfigurationAdapter(server);
const framework = new InfraKibanaBackendFrameworkAdapter(server);
export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) {
const framework = new KibanaFramework(core, config, plugins);
const sources = new InfraSources({
configuration,
savedObjects: framework.getSavedObjectsService(),
config,
});
const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), {
sources,
@ -34,6 +31,7 @@ export function compose(server: Server): InfraBackendLibs {
const snapshot = new InfraSnapshot({ sources, framework });
const logAnalysis = new InfraLogAnalysis({ framework });
// TODO: separate these out individually and do away with "domains" as a temporary group
const domainLibs: InfraDomainLibs = {
fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), {
sources,
@ -45,7 +43,7 @@ export function compose(server: Server): InfraBackendLibs {
};
const libs: InfraBackendLibs = {
configuration,
configuration: config, // NP_TODO: Do we ever use this anywhere?
framework,
logAnalysis,
snapshot,

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext } from 'src/core/server';
import { InfraIndexField, InfraIndexType } from '../../graphql/types';
import { FieldsAdapter } from '../adapters/fields';
import { InfraFrameworkRequest } from '../adapters/framework';
import { InfraSources } from '../sources';
export class InfraFieldsDomain {
@ -16,16 +16,19 @@ export class InfraFieldsDomain {
) {}
public async getFields(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
indexType: InfraIndexType
): Promise<InfraIndexField[]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType);
const includeLogIndices = [InfraIndexType.ANY, InfraIndexType.LOGS].includes(indexType);
const fields = await this.adapter.getIndexFields(
request,
requestContext,
`${includeMetricIndices ? configuration.metricAlias : ''},${
includeLogIndices ? configuration.logAlias : ''
}`,

View file

@ -7,6 +7,7 @@
import stringify from 'json-stable-stringify';
import { sortBy } from 'lodash';
import { RequestHandlerContext } from 'src/core/server';
import { TimeKey } from '../../../../common/time';
import { JsonObject } from '../../../../common/typed_json';
import {
@ -16,7 +17,6 @@ import {
InfraLogSummaryBucket,
InfraLogSummaryHighlightBucket,
} from '../../../graphql/types';
import { InfraFrameworkRequest } from '../../adapters/framework';
import {
InfraSourceConfiguration,
InfraSources,
@ -40,7 +40,7 @@ export class InfraLogEntriesDomain {
) {}
public async getLogEntriesAround(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
key: TimeKey,
maxCountBefore: number,
@ -55,14 +55,17 @@ export class InfraLogEntriesDomain {
};
}
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
const requiredFields = getRequiredFields(configuration, messageFormattingRules);
const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
key,
@ -80,7 +83,7 @@ export class InfraLogEntriesDomain {
};
const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
lastKeyBefore,
@ -101,20 +104,23 @@ export class InfraLogEntriesDomain {
}
public async getLogEntriesBetween(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
startKey: TimeKey,
endKey: TimeKey,
filterQuery?: LogEntryQuery,
highlightQuery?: LogEntryQuery
): Promise<InfraLogEntry[]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
const requiredFields = getRequiredFields(configuration, messageFormattingRules);
const documents = await this.adapter.getContainedLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
startKey,
@ -129,7 +135,7 @@ export class InfraLogEntriesDomain {
}
public async getLogEntryHighlights(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
startKey: TimeKey,
endKey: TimeKey,
@ -140,7 +146,10 @@ export class InfraLogEntriesDomain {
}>,
filterQuery?: LogEntryQuery
): Promise<InfraLogEntry[][]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
@ -158,7 +167,7 @@ export class InfraLogEntriesDomain {
: highlightQuery;
const [documentsBefore, documents, documentsAfter] = await Promise.all([
this.adapter.getAdjacentLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
startKey,
@ -168,7 +177,7 @@ export class InfraLogEntriesDomain {
highlightQuery
),
this.adapter.getContainedLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
startKey,
@ -177,7 +186,7 @@ export class InfraLogEntriesDomain {
highlightQuery
),
this.adapter.getAdjacentLogEntryDocuments(
request,
requestContext,
configuration,
requiredFields,
endKey,
@ -203,16 +212,19 @@ export class InfraLogEntriesDomain {
}
public async getLogSummaryBucketsBetween(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
start: number,
end: number,
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<InfraLogSummaryBucket[]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets(
request,
requestContext,
configuration,
start,
end,
@ -223,7 +235,7 @@ export class InfraLogEntriesDomain {
}
public async getLogSummaryHighlightBucketsBetween(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
start: number,
end: number,
@ -231,7 +243,10 @@ export class InfraLogEntriesDomain {
highlightQueries: string[],
filterQuery?: LogEntryQuery
): Promise<InfraLogSummaryHighlightBucket[][]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const { configuration } = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
@ -248,7 +263,7 @@ export class InfraLogEntriesDomain {
}
: highlightQuery;
const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets(
request,
requestContext,
configuration,
start,
end,
@ -266,11 +281,11 @@ export class InfraLogEntriesDomain {
}
public async getLogItem(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
id: string,
sourceConfiguration: InfraSourceConfiguration
): Promise<InfraLogItem> {
const document = await this.adapter.getLogItem(request, id, sourceConfiguration);
const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration);
const defaultFields = [
{ field: '_index', value: document._index },
{ field: '_id', value: document._id },
@ -300,7 +315,7 @@ interface LogItemHit {
export interface LogEntriesAdapter {
getAdjacentLogEntryDocuments(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
fields: string[],
start: TimeKey,
@ -311,7 +326,7 @@ export interface LogEntriesAdapter {
): Promise<LogEntryDocument[]>;
getContainedLogEntryDocuments(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
fields: string[],
start: TimeKey,
@ -321,7 +336,7 @@ export interface LogEntriesAdapter {
): Promise<LogEntryDocument[]>;
getContainedLogSummaryBuckets(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
start: number,
end: number,
@ -330,7 +345,7 @@ export interface LogEntriesAdapter {
): Promise<LogSummaryBucket[]>;
getLogItem(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
id: string,
source: InfraSourceConfiguration
): Promise<LogItemHit>;

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { InfraMetricData } from '../../graphql/types';
import { InfraFrameworkRequest } from '../adapters/framework/adapter_types';
import { InfraMetricsAdapter, InfraMetricsRequestOptions } from '../adapters/metrics/adapter_types';
export class InfraMetricsDomain {
@ -16,9 +16,10 @@ export class InfraMetricsDomain {
}
public async getMetrics(
req: InfraFrameworkRequest,
options: InfraMetricsRequestOptions
requestContext: RequestHandlerContext,
options: InfraMetricsRequestOptions,
rawRequest: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request
): Promise<InfraMetricData[]> {
return await this.adapter.getMetrics(req, options);
return await this.adapter.getMetrics(requestContext, options, rawRequest);
}
}

View file

@ -5,8 +5,6 @@
*/
import { InfraSourceConfiguration } from '../../public/graphql/types';
import { InfraConfigurationAdapter } from './adapters/configuration';
import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework';
import { InfraFieldsDomain } from './domains/fields_domain';
import { InfraLogEntriesDomain } from './domains/log_entries_domain';
import { InfraMetricsDomain } from './domains/metrics_domain';
@ -14,6 +12,15 @@ import { InfraLogAnalysis } from './log_analysis/log_analysis';
import { InfraSnapshot } from './snapshot';
import { InfraSources } from './sources';
import { InfraSourceStatus } from './source_status';
import { InfraConfig } from '../../../../../plugins/infra/server';
import { KibanaFramework } from './adapters/framework/kibana_framework_adapter';
// NP_TODO: We shouldn't need this context anymore but I am
// not sure how the graphql stuff uses it, so we can't remove it yet
export interface InfraContext {
req: any;
rawReq?: any;
}
export interface InfraDomainLibs {
fields: InfraFieldsDomain;
@ -22,8 +29,8 @@ export interface InfraDomainLibs {
}
export interface InfraBackendLibs extends InfraDomainLibs {
configuration: InfraConfigurationAdapter;
framework: InfraBackendFrameworkAdapter;
configuration: InfraConfig;
framework: KibanaFramework;
logAnalysis: InfraLogAnalysis;
snapshot: InfraSnapshot;
sources: InfraSources;
@ -40,7 +47,3 @@ export interface InfraConfiguration {
default: InfraSourceConfiguration;
};
}
export interface InfraContext {
req: InfraFrameworkRequest;
}

View file

@ -9,7 +9,7 @@ import { map, fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { getJobId } from '../../../common/log_analysis';
import { throwErrors, createPlainError } from '../../../common/runtime_types';
import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework';
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
import { NoLogRateResultsIndexError } from './errors';
import {
logRateModelPlotResponseRT,
@ -17,37 +17,38 @@ import {
LogRateModelPlotBucket,
CompositeTimestampPartitionKey,
} from './queries';
import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server';
const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
export class InfraLogAnalysis {
constructor(
private readonly libs: {
framework: InfraBackendFrameworkAdapter;
framework: KibanaFramework;
}
) {}
public getJobIds(request: InfraFrameworkRequest, sourceId: string) {
public getJobIds(request: KibanaRequest, sourceId: string) {
return {
logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'),
};
}
public async getLogEntryRateBuckets(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
startTime: number,
endTime: number,
bucketDuration: number
bucketDuration: number,
request: KibanaRequest
) {
const logRateJobId = this.getJobIds(request, sourceId).logEntryRate;
let mlModelPlotBuckets: LogRateModelPlotBucket[] = [];
let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined;
while (true) {
const mlModelPlotResponse = await this.libs.framework.callWithRequest(
request,
requestContext,
'search',
createLogEntryRateQuery(
logRateJobId,

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext } from 'src/core/server';
import {
InfraSnapshotGroupbyInput,
InfraSnapshotMetricInput,
@ -12,11 +13,8 @@ import {
InfraNodeType,
InfraSourceConfiguration,
} from '../../graphql/types';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
InfraDatabaseSearchResponse,
} from '../adapters/framework';
import { InfraDatabaseSearchResponse } from '../adapters/framework';
import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter';
import { InfraSources } from '../sources';
import { JsonObject } from '../../../common/typed_json';
@ -48,20 +46,18 @@ export interface InfraSnapshotRequestOptions {
}
export class InfraSnapshot {
constructor(
private readonly libs: { sources: InfraSources; framework: InfraBackendFrameworkAdapter }
) {}
constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {}
public async getNodes(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
options: InfraSnapshotRequestOptions
): Promise<InfraSnapshotNode[]> {
// Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch
// in order to page through the results of their respective composite aggregations.
// Both chains of requests are supposed to run in parallel, and their results be merged
// when they have both been completed.
const groupedNodesPromise = requestGroupedNodes(request, options, this.libs.framework);
const nodeMetricsPromise = requestNodeMetrics(request, options, this.libs.framework);
const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework);
const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework);
const groupedNodeBuckets = await groupedNodesPromise;
const nodeMetricBuckets = await nodeMetricsPromise;
@ -79,9 +75,9 @@ const handleAfterKey = createAfterKeyHandler(
);
const requestGroupedNodes = async (
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
options: InfraSnapshotRequestOptions,
framework: InfraBackendFrameworkAdapter
framework: KibanaFramework
): Promise<InfraSnapshotNodeGroupByBucket[]> => {
const query = {
allowNoIndices: true,
@ -130,13 +126,13 @@ const requestGroupedNodes = async (
return await getAllCompositeData<
InfraSnapshotAggregationResponse,
InfraSnapshotNodeGroupByBucket
>(framework, request, query, bucketSelector, handleAfterKey);
>(framework, requestContext, query, bucketSelector, handleAfterKey);
};
const requestNodeMetrics = async (
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
options: InfraSnapshotRequestOptions,
framework: InfraBackendFrameworkAdapter
framework: KibanaFramework
): Promise<InfraSnapshotNodeMetricsBucket[]> => {
const index =
options.metric.type === 'logRate'
@ -191,7 +187,7 @@ const requestNodeMetrics = async (
return await getAllCompositeData<
InfraSnapshotAggregationResponse,
InfraSnapshotNodeMetricsBucket
>(framework, request, query, bucketSelector, handleAfterKey);
>(framework, requestContext, query, bucketSelector, handleAfterKey);
};
// buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[]

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraFrameworkRequest } from './adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { InfraSources } from './sources';
export class InfraSourceStatus {
@ -14,58 +14,85 @@ export class InfraSourceStatus {
) {}
public async getLogIndexNames(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string
): Promise<string[]> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const indexNames = await this.adapter.getIndexNames(
request,
requestContext,
sourceConfiguration.configuration.logAlias
);
return indexNames;
}
public async getMetricIndexNames(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string
): Promise<string[]> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const indexNames = await this.adapter.getIndexNames(
request,
requestContext,
sourceConfiguration.configuration.metricAlias
);
return indexNames;
}
public async hasLogAlias(request: InfraFrameworkRequest, sourceId: string): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
public async hasLogAlias(
requestContext: RequestHandlerContext,
sourceId: string
): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const hasAlias = await this.adapter.hasAlias(
request,
requestContext,
sourceConfiguration.configuration.logAlias
);
return hasAlias;
}
public async hasMetricAlias(request: InfraFrameworkRequest, sourceId: string): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
public async hasMetricAlias(
requestContext: RequestHandlerContext,
sourceId: string
): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const hasAlias = await this.adapter.hasAlias(
request,
requestContext,
sourceConfiguration.configuration.metricAlias
);
return hasAlias;
}
public async hasLogIndices(request: InfraFrameworkRequest, sourceId: string): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
public async hasLogIndices(
requestContext: RequestHandlerContext,
sourceId: string
): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const hasIndices = await this.adapter.hasIndices(
request,
requestContext,
sourceConfiguration.configuration.logAlias
);
return hasIndices;
}
public async hasMetricIndices(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string
): Promise<boolean> {
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId);
const sourceConfiguration = await this.libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const hasIndices = await this.adapter.hasIndices(
request,
requestContext,
sourceConfiguration.configuration.metricAlias
);
return hasIndices;
@ -73,7 +100,7 @@ export class InfraSourceStatus {
}
export interface InfraSourceStatusAdapter {
getIndexNames(request: InfraFrameworkRequest, aliasName: string): Promise<string[]>;
hasAlias(request: InfraFrameworkRequest, aliasName: string): Promise<boolean>;
hasIndices(request: InfraFrameworkRequest, indexNames: string): Promise<boolean>;
getIndexNames(requestContext: RequestHandlerContext, aliasName: string): Promise<string[]>;
hasAlias(requestContext: RequestHandlerContext, aliasName: string): Promise<boolean>;
hasIndices(requestContext: RequestHandlerContext, indexNames: string): Promise<boolean>;
}

View file

@ -3,34 +3,31 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraInmemoryConfigurationAdapter } from '../adapters/configuration/inmemory_configuration_adapter';
import { InfraSources } from './sources';
describe('the InfraSources lib', () => {
describe('getSourceConfiguration method', () => {
test('returns a source configuration if it exists', async () => {
const sourcesLib = new InfraSources({
configuration: createMockStaticConfiguration({}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
metricAlias: 'METRIC_ALIAS',
logAlias: 'LOG_ALIAS',
fields: {
container: 'CONTAINER',
host: 'HOST',
pod: 'POD',
tiebreaker: 'TIEBREAKER',
timestamp: 'TIMESTAMP',
},
},
}),
config: createMockStaticConfiguration({}),
});
const request: any = Symbol();
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
metricAlias: 'METRIC_ALIAS',
logAlias: 'LOG_ALIAS',
fields: {
container: 'CONTAINER',
host: 'HOST',
pod: 'POD',
tiebreaker: 'TIEBREAKER',
timestamp: 'TIMESTAMP',
},
},
});
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
@ -52,7 +49,7 @@ describe('the InfraSources lib', () => {
test('adds missing attributes from the static configuration to a source configuration', async () => {
const sourcesLib = new InfraSources({
configuration: createMockStaticConfiguration({
config: createMockStaticConfiguration({
default: {
metricAlias: 'METRIC_ALIAS',
logAlias: 'LOG_ALIAS',
@ -64,19 +61,18 @@ describe('the InfraSources lib', () => {
},
},
}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
fields: {
container: 'CONTAINER',
},
},
}),
});
const request: any = Symbol();
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
fields: {
container: 'CONTAINER',
},
},
});
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
@ -98,16 +94,15 @@ describe('the InfraSources lib', () => {
test('adds missing attributes from the default configuration to a source configuration', async () => {
const sourcesLib = new InfraSources({
configuration: createMockStaticConfiguration({}),
savedObjects: createMockSavedObjectsService({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {},
}),
config: createMockStaticConfiguration({}),
});
const request: any = Symbol();
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {},
});
expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({
id: 'TEST_ID',
@ -129,29 +124,30 @@ describe('the InfraSources lib', () => {
});
});
const createMockStaticConfiguration = (sources: any) =>
new InfraInmemoryConfigurationAdapter({
enabled: true,
query: {
partitionSize: 1,
partitionFactor: 1,
},
sources,
});
const createMockSavedObjectsService = (savedObject?: any) => ({
getScopedSavedObjectsClient() {
return {
async get() {
return savedObject;
},
} as any;
},
SavedObjectsClient: {
errors: {
isNotFoundError() {
return typeof savedObject === 'undefined';
},
},
const createMockStaticConfiguration = (sources: any) => ({
enabled: true,
query: {
partitionSize: 1,
partitionFactor: 1,
},
sources,
});
const createRequestContext = (savedObject?: any) => {
return {
core: {
savedObjects: {
client: {
async get() {
return savedObject;
},
errors: {
isNotFoundError() {
return typeof savedObject === 'undefined';
},
},
},
},
},
};
};

View file

@ -6,14 +6,10 @@
import * as runtimeTypes from 'io-ts';
import { failure } from 'io-ts/lib/PathReporter';
import { Legacy } from 'kibana';
import { identity, constant } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { Pick3 } from '../../../common/utility_types';
import { InfraConfigurationAdapter } from '../adapters/configuration';
import { InfraFrameworkRequest, internalInfraFrameworkRequest } from '../adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { defaultSourceConfiguration } from './defaults';
import { NotFoundError } from './errors';
import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings';
@ -25,19 +21,21 @@ import {
SourceConfigurationSavedObjectRuntimeType,
StaticSourceConfigurationRuntimeType,
} from './types';
import { InfraConfig } from '../../../../../../plugins/infra/server';
interface Libs {
config: InfraConfig;
}
export class InfraSources {
private internalSourceConfigurations: Map<string, InfraStaticSourceConfiguration> = new Map();
private readonly libs: Libs;
constructor(
private readonly libs: {
configuration: InfraConfigurationAdapter;
savedObjects: Pick<Legacy.SavedObjectsService, 'getScopedSavedObjectsClient'> &
Pick3<Legacy.SavedObjectsService, 'SavedObjectsClient', 'errors', 'isNotFoundError'>;
}
) {}
constructor(libs: Libs) {
this.libs = libs;
}
public async getSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) {
public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId)
@ -53,7 +51,7 @@ export class InfraSources {
}))
.catch(err =>
err instanceof NotFoundError
? this.getSavedSourceConfiguration(request, sourceId).then(result => ({
? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({
...result,
configuration: mergeSourceConfiguration(
staticDefaultSourceConfiguration,
@ -63,7 +61,7 @@ export class InfraSources {
: Promise.reject(err)
)
.catch(err =>
this.libs.savedObjects.SavedObjectsClient.errors.isNotFoundError(err)
requestContext.core.savedObjects.client.errors.isNotFoundError(err)
? Promise.resolve({
id: sourceId,
version: undefined,
@ -77,10 +75,10 @@ export class InfraSources {
return savedSourceConfiguration;
}
public async getAllSourceConfigurations(request: InfraFrameworkRequest) {
public async getAllSourceConfigurations(requestContext: RequestHandlerContext) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(request);
const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext);
return savedSourceConfigurations.map(savedSourceConfiguration => ({
...savedSourceConfiguration,
@ -92,7 +90,7 @@ export class InfraSources {
}
public async createSourceConfiguration(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
source: InfraSavedSourceConfiguration
) {
@ -104,13 +102,11 @@ export class InfraSources {
);
const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalInfraFrameworkRequest])
.create(
infraSourceConfigurationSavedObjectType,
pickSavedSourceConfiguration(newSourceConfiguration) as any,
{ id: sourceId }
)
await requestContext.core.savedObjects.client.create(
infraSourceConfigurationSavedObjectType,
pickSavedSourceConfiguration(newSourceConfiguration) as any,
{ id: sourceId }
)
);
return {
@ -122,20 +118,21 @@ export class InfraSources {
};
}
public async deleteSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) {
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalInfraFrameworkRequest])
.delete(infraSourceConfigurationSavedObjectType, sourceId);
public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) {
await requestContext.core.savedObjects.client.delete(
infraSourceConfigurationSavedObjectType,
sourceId
);
}
public async updateSourceConfiguration(
request: InfraFrameworkRequest,
requestContext: RequestHandlerContext,
sourceId: string,
sourceProperties: InfraSavedSourceConfiguration
) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
const { configuration, version } = await this.getSourceConfiguration(request, sourceId);
const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId);
const updatedSourceConfigurationAttributes = mergeSourceConfiguration(
configuration,
@ -143,16 +140,14 @@ export class InfraSources {
);
const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalInfraFrameworkRequest])
.update(
infraSourceConfigurationSavedObjectType,
sourceId,
pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
{
version,
}
)
await requestContext.core.savedObjects.client.update(
infraSourceConfigurationSavedObjectType,
sourceId,
pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
{
version,
}
)
);
return {
@ -184,7 +179,6 @@ export class InfraSources {
}
private async getStaticDefaultSourceConfiguration() {
const staticConfiguration = await this.libs.configuration.get();
const staticSourceConfiguration = pipe(
runtimeTypes
.type({
@ -192,7 +186,7 @@ export class InfraSources {
default: StaticSourceConfigurationRuntimeType,
}),
})
.decode(staticConfiguration),
.decode(this.libs.config),
map(({ sources: { default: defaultConfiguration } }) => defaultConfiguration),
fold(constant({}), identity)
);
@ -200,12 +194,11 @@ export class InfraSources {
return mergeSourceConfiguration(defaultSourceConfiguration, staticSourceConfiguration);
}
private async getSavedSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalInfraFrameworkRequest]
);
const savedObject = await savedObjectsClient.get(
private async getSavedSourceConfiguration(
requestContext: RequestHandlerContext,
sourceId: string
) {
const savedObject = await requestContext.core.savedObjects.client.get(
infraSourceConfigurationSavedObjectType,
sourceId
);
@ -213,12 +206,8 @@ export class InfraSources {
return convertSavedObjectToSavedSourceConfiguration(savedObject);
}
private async getAllSavedSourceConfigurations(request: InfraFrameworkRequest) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalInfraFrameworkRequest]
);
const savedObjects = await savedObjectsClient.find({
private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) {
const savedObjects = await requestContext.core.savedObjects.client.find({
type: infraSourceConfigurationSavedObjectType,
});

View file

@ -0,0 +1,16 @@
/*
* 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 { PluginInitializerContext } from 'src/core/server';
import { InfraServerPlugin } from './new_platform_plugin';
import { config, InfraConfig } from '../../../../plugins/infra/server';
import { InfraServerPluginDeps } from './lib/adapters/framework';
export { config, InfraConfig, InfraServerPluginDeps };
export function plugin(context: PluginInitializerContext) {
return new InfraServerPlugin(context);
}

View file

@ -0,0 +1,107 @@
/*
* 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 { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { Server } from 'hapi';
import { InfraConfig } from '../../../../plugins/infra/server';
import { initInfraServer } from './infra_server';
import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types';
import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter';
import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter';
import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status';
import { InfraFieldsDomain } from './lib/domains/fields_domain';
import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain';
import { InfraMetricsDomain } from './lib/domains/metrics_domain';
import { InfraLogAnalysis } from './lib/log_analysis';
import { InfraSnapshot } from './lib/snapshot';
import { InfraSourceStatus } from './lib/source_status';
import { InfraSources } from './lib/sources';
import { InfraServerPluginDeps } from './lib/adapters/framework';
import { METRICS_FEATURE, LOGS_FEATURE } from './features';
import { UsageCollector } from './usage/usage_collector';
export interface KbnServer extends Server {
usage: any;
}
const DEFAULT_CONFIG: InfraConfig = {
enabled: true,
query: {
partitionSize: 75,
partitionFactor: 1.2,
},
};
export class InfraServerPlugin {
public config: InfraConfig = DEFAULT_CONFIG;
public libs: InfraBackendLibs | undefined;
constructor(context: PluginInitializerContext) {
const config$ = context.config.create<InfraConfig>();
config$.subscribe(configValue => {
this.config = {
...DEFAULT_CONFIG,
enabled: configValue.enabled,
query: {
...DEFAULT_CONFIG.query,
...configValue.query,
},
};
});
}
getLibs() {
if (!this.libs) {
throw new Error('libs not set up yet');
}
return this.libs;
}
setup(core: CoreSetup, plugins: InfraServerPluginDeps) {
const framework = new KibanaFramework(core, this.config, plugins);
const sources = new InfraSources({
config: this.config,
});
const sourceStatus = new InfraSourceStatus(
new InfraElasticsearchSourceStatusAdapter(framework),
{
sources,
}
);
const snapshot = new InfraSnapshot({ sources, framework });
const logAnalysis = new InfraLogAnalysis({ framework });
// TODO: separate these out individually and do away with "domains" as a temporary group
const domainLibs: InfraDomainLibs = {
fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), {
sources,
}),
logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), {
sources,
}),
metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)),
};
this.libs = {
configuration: this.config,
framework,
logAnalysis,
snapshot,
sources,
sourceStatus,
...domainLibs,
};
plugins.features.registerFeature(METRICS_FEATURE);
plugins.features.registerFeature(LOGS_FEATURE);
initInfraServer(this.libs);
// Telemetry
UsageCollector.registerUsageCollector(plugins.usageCollection);
}
}

View file

@ -3,18 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { boomify, notFound } from 'boom';
import { first } from 'lodash';
import { schema } from '@kbn/config-schema';
import { InfraBackendLibs } from '../lib/infra_types';
import { InfraWrappableRequest } from '../lib/adapters/framework';
interface IpToHostRequest {
ip: string;
index_pattern: string;
}
type IpToHostWrappedRequest = InfraWrappableRequest<IpToHostRequest>;
export interface IpToHostResponse {
host: string;
@ -28,40 +19,47 @@ interface HostDoc {
};
}
const ipToHostSchema = Joi.object({
ip: Joi.string().required(),
index_pattern: Joi.string().required(),
const ipToHostSchema = schema.object({
ip: schema.string(),
index_pattern: schema.string(),
});
export const initIpToHostName = ({ framework }: InfraBackendLibs) => {
const { callWithRequest } = framework;
framework.registerRoute<IpToHostWrappedRequest, Promise<IpToHostResponse>>({
method: 'POST',
path: '/api/infra/ip_to_host',
options: {
validate: { payload: ipToHostSchema },
framework.registerRoute(
{
method: 'post',
path: '/api/infra/ip_to_host',
validate: {
body: ipToHostSchema,
},
},
handler: async req => {
async (requestContext, { body }, response) => {
try {
const params = {
index: req.payload.index_pattern,
index: body.index_pattern,
body: {
size: 1,
query: {
match: { 'host.ip': req.payload.ip },
match: { 'host.ip': body.ip },
},
_source: ['host.name'],
},
};
const response = await callWithRequest<HostDoc>(req, 'search', params);
if (response.hits.total.value === 0) {
throw notFound('Host with matching IP address not found.');
const { hits } = await callWithRequest<HostDoc>(requestContext, 'search', params);
if (hits.total.value === 0) {
return response.notFound({
body: { message: 'Host with matching IP address not found.' },
});
}
const hostDoc = first(response.hits.hits);
return { host: hostDoc._source.host.name };
} catch (e) {
throw boomify(e);
const hostDoc = first(hits.hits);
return response.ok({ body: { host: hostDoc._source.host.name } });
} catch ({ statusCode = 500, message = 'Unknown error occurred' }) {
return response.customError({
statusCode,
body: { message },
});
}
},
});
}
);
};

View file

@ -8,7 +8,7 @@ import Boom from 'boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import { InfraBackendLibs } from '../../../lib/infra_types';
import {
LOG_ANALYSIS_VALIDATION_INDICES_PATH,
@ -20,64 +20,75 @@ import {
import { throwErrors } from '../../../../common/runtime_types';
const partitionField = 'event.dataset';
const escapeHatch = schema.object({}, { allowUnknowns: true });
export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => {
framework.registerRoute({
method: 'POST',
path: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
handler: async (req, res) => {
const payload = pipe(
validationIndicesRequestPayloadRT.decode(req.payload),
fold(throwErrors(Boom.badRequest), identity)
);
framework.registerRoute(
{
method: 'post',
path: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
validate: { body: escapeHatch },
},
async (requestContext, request, response) => {
try {
const payload = pipe(
validationIndicesRequestPayloadRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { timestampField, indices } = payload.data;
const errors: ValidationIndicesError[] = [];
const { timestampField, indices } = payload.data;
const errors: ValidationIndicesError[] = [];
// Query each pattern individually, to map correctly the errors
await Promise.all(
indices.map(async index => {
const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', {
index,
fields: `${timestampField},${partitionField}`,
});
if (fieldCaps.indices.length === 0) {
errors.push({
error: 'INDEX_NOT_FOUND',
// Query each pattern individually, to map correctly the errors
await Promise.all(
indices.map(async index => {
const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', {
index,
fields: `${timestampField},${partitionField}`,
});
return;
}
([
[timestampField, 'date'],
[partitionField, 'keyword'],
] as const).forEach(([field, fieldType]) => {
const fieldMetadata = fieldCaps.fields[field];
if (fieldMetadata === undefined) {
if (fieldCaps.indices.length === 0) {
errors.push({
error: 'FIELD_NOT_FOUND',
error: 'INDEX_NOT_FOUND',
index,
field,
});
} else {
const fieldTypes = Object.keys(fieldMetadata);
return;
}
if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) {
([
[timestampField, 'date'],
[partitionField, 'keyword'],
] as const).forEach(([field, fieldType]) => {
const fieldMetadata = fieldCaps.fields[field];
if (fieldMetadata === undefined) {
errors.push({
error: `FIELD_NOT_VALID`,
error: 'FIELD_NOT_FOUND',
index,
field,
});
}
}
});
})
);
} else {
const fieldTypes = Object.keys(fieldMetadata);
return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } }));
},
});
if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) {
errors.push({
error: `FIELD_NOT_VALID`,
index,
field,
});
}
}
});
})
);
return response.ok({
body: validationIndicesResponsePayloadRT.encode({ data: { errors } }),
});
} catch (error) {
return response.internalError({
body: error.message,
});
}
}
);
};

View file

@ -9,6 +9,7 @@ import Boom from 'boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import { InfraBackendLibs } from '../../../lib/infra_types';
import {
LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
@ -19,46 +20,58 @@ import {
import { throwErrors } from '../../../../common/runtime_types';
import { NoLogRateResultsIndexError } from '../../../lib/log_analysis';
const anyObject = schema.object({}, { allowUnknowns: true });
export const initLogAnalysisGetLogEntryRateRoute = ({
framework,
logAnalysis,
}: InfraBackendLibs) => {
framework.registerRoute({
method: 'POST',
path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
handler: async (req, res) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
validate: {
// short-circuit forced @kbn/config-schema validation so we can do io-ts validation
body: anyObject,
},
},
async (requestContext, request, response) => {
const payload = pipe(
getLogEntryRateRequestPayloadRT.decode(req.payload),
getLogEntryRateRequestPayloadRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const logEntryRateBuckets = await logAnalysis
.getLogEntryRateBuckets(
req,
try {
const logEntryRateBuckets = await logAnalysis.getLogEntryRateBuckets(
requestContext,
payload.data.sourceId,
payload.data.timeRange.startTime,
payload.data.timeRange.endTime,
payload.data.bucketDuration
)
.catch(err => {
if (err instanceof NoLogRateResultsIndexError) {
throw Boom.boomify(err, { statusCode: 404 });
}
payload.data.bucketDuration,
request
);
throw Boom.boomify(err, { statusCode: ('statusCode' in err && err.statusCode) || 500 });
return response.ok({
body: getLogEntryRateSuccessReponsePayloadRT.encode({
data: {
bucketDuration: payload.data.bucketDuration,
histogramBuckets: logEntryRateBuckets,
totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets),
},
}),
});
return res.response(
getLogEntryRateSuccessReponsePayloadRT.encode({
data: {
bucketDuration: payload.data.bucketDuration,
histogramBuckets: logEntryRateBuckets,
totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets),
},
})
);
},
});
} catch (e) {
const { statusCode = 500, message = 'Unknown error occurred' } = e;
if (e instanceof NoLogRateResultsIndexError) {
return response.notFound({ body: { message } });
}
return response.customError({
statusCode,
body: { message },
});
}
}
);
};
const getTotalNumberOfLogEntries = (

View file

@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom, { boomify } from 'boom';
import { schema } from '@kbn/config-schema';
import Boom from 'boom';
import { get } from 'lodash';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import {
InfraMetadata,
InfraMetadataWrappedRequest,
InfraMetadataFeature,
InfraMetadataRequestRT,
InfraMetadataRT,
@ -24,23 +23,33 @@ import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata';
import { getNodeInfo } from './lib/get_node_info';
import { throwErrors } from '../../../common/runtime_types';
const escapeHatch = schema.object({}, { allowUnknowns: true });
export const initMetadataRoute = (libs: InfraBackendLibs) => {
const { framework } = libs;
framework.registerRoute<InfraMetadataWrappedRequest, Promise<InfraMetadata>>({
method: 'POST',
path: '/api/infra/metadata',
handler: async req => {
framework.registerRoute(
{
method: 'post',
path: '/api/infra/metadata',
validate: {
body: escapeHatch,
},
},
async (requestContext, request, response) => {
try {
const { nodeId, nodeType, sourceId } = pipe(
InfraMetadataRequestRT.decode(req.payload),
InfraMetadataRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId);
const { configuration } = await libs.sources.getSourceConfiguration(
requestContext,
sourceId
);
const metricsMetadata = await getMetricMetadata(
framework,
req,
requestContext,
configuration,
nodeId,
nodeType
@ -49,35 +58,35 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => {
nameToFeature('metrics')
);
const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType);
const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType);
const cloudInstanceId = get<string>(info, 'cloud.instance.id');
const cloudMetricsMetadata = cloudInstanceId
? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId)
? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId)
: { buckets: [] };
const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map(
nameToFeature('metrics')
);
const hasAPM = await hasAPMData(framework, req, configuration, nodeId, nodeType);
const hasAPM = await hasAPMData(framework, requestContext, configuration, nodeId, nodeType);
const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : [];
const id = metricsMetadata.id;
const name = metricsMetadata.name || id;
return pipe(
InfraMetadataRT.decode({
return response.ok({
body: InfraMetadataRT.encode({
id,
name,
features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures],
info,
}),
fold(throwErrors(Boom.badImplementation), identity)
);
});
} catch (error) {
throw boomify(error);
return response.internalError({
body: error.message,
});
}
},
});
}
);
};
const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext } from 'src/core/server';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
InfraMetadataAggregationBucket,
InfraMetadataAggregationResponse,
} from '../../../lib/adapters/framework';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import { InfraSourceConfiguration } from '../../../lib/sources';
import { CLOUD_METRICS_MODULES } from '../../../lib/constants';
@ -18,8 +18,8 @@ export interface InfraCloudMetricsAdapterResponse {
}
export const getCloudMetricsMetadata = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
instanceId: string
): Promise<InfraCloudMetricsAdapterResponse> => {
@ -51,7 +51,7 @@ export const getCloudMetricsMetadata = async (
{
metrics?: InfraMetadataAggregationResponse;
}
>(req, 'search', metricQuery);
>(requestContext, 'search', metricQuery);
const buckets =
response.aggregations && response.aggregations.metrics

View file

@ -5,12 +5,12 @@
*/
import { get } from 'lodash';
import { RequestHandlerContext } from 'src/core/server';
import {
InfraFrameworkRequest,
InfraMetadataAggregationBucket,
InfraBackendFrameworkAdapter,
InfraMetadataAggregationResponse,
} from '../../../lib/adapters/framework';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import { InfraSourceConfiguration } from '../../../lib/sources';
import { getIdFieldName } from './get_id_field_name';
import { NAME_FIELDS } from '../../../lib/constants';
@ -22,8 +22,8 @@ export interface InfraMetricsAdapterResponse {
}
export const getMetricMetadata = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
nodeId: string,
nodeType: 'host' | 'pod' | 'container'
@ -69,7 +69,7 @@ export const getMetricMetadata = async (
metrics?: InfraMetadataAggregationResponse;
nodeName?: InfraMetadataAggregationResponse;
}
>(req, 'search', metricQuery);
>(requestContext, 'search', metricQuery);
const buckets =
response.aggregations && response.aggregations.metrics

View file

@ -5,10 +5,8 @@
*/
import { first } from 'lodash';
import {
InfraFrameworkRequest,
InfraBackendFrameworkAdapter,
} from '../../../lib/adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import { InfraSourceConfiguration } from '../../../lib/sources';
import { InfraNodeType } from '../../../graphql/types';
import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api';
@ -17,8 +15,8 @@ import { CLOUD_METRICS_MODULES } from '../../../lib/constants';
import { getIdFieldName } from './get_id_field_name';
export const getNodeInfo = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
nodeId: string,
nodeType: 'host' | 'pod' | 'container'
@ -31,7 +29,7 @@ export const getNodeInfo = async (
if (nodeType === InfraNodeType.pod) {
const kubernetesNodeName = await getPodNodeName(
framework,
req,
requestContext,
sourceConfiguration,
nodeId,
nodeType
@ -39,7 +37,7 @@ export const getNodeInfo = async (
if (kubernetesNodeName) {
return getNodeInfo(
framework,
req,
requestContext,
sourceConfiguration,
kubernetesNodeName,
InfraNodeType.host
@ -64,7 +62,7 @@ export const getNodeInfo = async (
},
};
const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>(
req,
requestContext,
'search',
params
);

View file

@ -5,16 +5,14 @@
*/
import { first, get } from 'lodash';
import {
InfraFrameworkRequest,
InfraBackendFrameworkAdapter,
} from '../../../lib/adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import { InfraSourceConfiguration } from '../../../lib/sources';
import { getIdFieldName } from './get_id_field_name';
export const getPodNodeName = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
nodeId: string,
nodeType: 'host' | 'pod' | 'container'
@ -40,7 +38,7 @@ export const getPodNodeName = async (
const response = await framework.callWithRequest<
{ _source: { kubernetes: { node: { name: string } } } },
{}
>(req, 'search', params);
>(requestContext, 'search', params);
const firstHit = first(response.hits.hits);
if (firstHit) {
return get(firstHit, '_source.kubernetes.node.name');

View file

@ -4,22 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
InfraFrameworkRequest,
InfraBackendFrameworkAdapter,
} from '../../../lib/adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import { InfraSourceConfiguration } from '../../../lib/sources';
import { getIdFieldName } from './get_id_field_name';
export const hasAPMData = async (
framework: InfraBackendFrameworkAdapter,
req: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
sourceConfiguration: InfraSourceConfiguration,
nodeId: string,
nodeType: 'host' | 'pod' | 'container'
) => {
const config = framework.config(req);
const apmIndex = config.get('apm_oss.transactionIndices') || 'apm-*';
const apmIndices = await framework.plugins.apm.getApmIndices(
requestContext.core.savedObjects.client
);
const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*';
// There is a bug in APM ECS data where host.name is not set.
// This will fixed with: https://github.com/elastic/apm-server/issues/2502
const nodeFieldName =
@ -48,6 +50,6 @@ export const hasAPMData = async (
},
},
};
const response = await framework.callWithRequest<{}, {}>(req, 'search', params);
const response = await framework.callWithRequest<{}, {}>(requestContext, 'search', params);
return response.hits.total.value !== 0;
};

View file

@ -4,42 +4,50 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { boomify } from 'boom';
import { schema } from '@kbn/config-schema';
import { InfraBackendLibs } from '../../lib/infra_types';
import { getGroupings } from './lib/get_groupings';
import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data';
import { metricsExplorerSchema } from './schema';
import { MetricsExplorerResponse, MetricsExplorerWrappedRequest } from './types';
import { MetricsExplorerRequestBody } from './types';
// import { metricsExplorerSchema } from './schema';
// import { MetricsExplorerResponse, MetricsExplorerRequestBody } from './types';
// NP_TODO: need to replace all of this with real types or io-ts or something?
const escapeHatch = schema.object({}, { allowUnknowns: true });
export const initMetricExplorerRoute = (libs: InfraBackendLibs) => {
const { framework } = libs;
const { callWithRequest } = framework;
framework.registerRoute<MetricsExplorerWrappedRequest, Promise<MetricsExplorerResponse>>({
method: 'POST',
path: '/api/infra/metrics_explorer',
options: {
framework.registerRoute(
{
method: 'post',
path: '/api/infra/metrics_explorer',
validate: {
payload: metricsExplorerSchema,
body: escapeHatch,
},
},
handler: async req => {
async (requestContext, request, response) => {
try {
const search = <Aggregation>(searchOptions: object) =>
callWithRequest<{}, Aggregation>(req, 'search', searchOptions);
const options = req.payload;
callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions);
const options = request.body as MetricsExplorerRequestBody; // Need to remove this casting and swap in config-schema demands :(
// First we get the groupings from a composite aggregation
const response = await getGroupings(search, options);
const groupings = await getGroupings(search, options);
// Then we take the results and fill in the data from TSVB with the
// user's custom metrics
const seriesWithMetrics = await Promise.all(
response.series.map(populateSeriesWithTSVBData(req, options, framework))
groupings.series.map(
populateSeriesWithTSVBData(request, options, framework, requestContext)
)
);
return { ...response, series: seriesWithMetrics };
return response.ok({ body: { ...groupings, series: seriesWithMetrics } });
} catch (error) {
throw boomify(error);
return response.internalError({
body: error.message,
});
}
},
});
}
);
};

View file

@ -5,10 +5,10 @@
*/
import { InfraMetricModelMetricType } from '../../../lib/adapters/metrics';
import { MetricsExplorerAggregation, MetricsExplorerRequest } from '../types';
import { MetricsExplorerAggregation, MetricsExplorerRequestBody } from '../types';
import { InfraMetric } from '../../../graphql/types';
import { TSVBMetricModel } from '../../../../common/inventory_models/types';
export const createMetricModel = (options: MetricsExplorerRequest): TSVBMetricModel => {
export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => {
return {
id: InfraMetric.custom,
requires: [],

View file

@ -6,7 +6,7 @@
import { isObject, set } from 'lodash';
import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework';
import { MetricsExplorerRequest, MetricsExplorerResponse } from '../types';
import { MetricsExplorerRequestBody, MetricsExplorerResponse } from '../types';
interface GroupingAggregation {
groupingsCount: {
@ -27,7 +27,7 @@ const EMPTY_RESPONSE = {
export const getGroupings = async (
search: <Aggregation>(options: object) => Promise<InfraDatabaseSearchResponse<{}, Aggregation>>,
options: MetricsExplorerRequest
options: MetricsExplorerRequestBody
): Promise<MetricsExplorerResponse> => {
if (!options.groupBy) {
return EMPTY_RESPONSE;

View file

@ -5,25 +5,23 @@
*/
import { union } from 'lodash';
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
} from '../../../lib/adapters/framework';
import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter';
import {
MetricsExplorerColumnType,
MetricsExplorerRequest,
MetricsExplorerRow,
MetricsExplorerSeries,
MetricsExplorerWrappedRequest,
MetricsExplorerRequestBody,
} from '../types';
import { createMetricModel } from './create_metrics_model';
import { JsonObject } from '../../../../common/typed_json';
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
export const populateSeriesWithTSVBData = (
req: InfraFrameworkRequest<MetricsExplorerWrappedRequest>,
options: MetricsExplorerRequest,
framework: InfraBackendFrameworkAdapter
request: KibanaRequest,
options: MetricsExplorerRequestBody,
framework: KibanaFramework,
requestContext: RequestHandlerContext
) => async (series: MetricsExplorerSeries) => {
// IF there are no metrics selected then we should return an empty result.
if (options.metrics.length === 0) {
@ -57,7 +55,7 @@ export const populateSeriesWithTSVBData = (
const model = createMetricModel(options);
const calculatedInterval = await calculateMetricInterval(
framework,
req,
requestContext,
{
indexPattern: options.indexPattern,
timestampField: options.timerange.field,
@ -78,7 +76,13 @@ export const populateSeriesWithTSVBData = (
}
// Get TSVB results using the model, timerange and filters
const tsvbResults = await framework.makeTSVBRequest(req, model, timerange, filters);
const tsvbResults = await framework.makeTSVBRequest(
request,
model,
timerange,
filters,
requestContext
);
// If there is no data `custom` will not exist.
if (!tsvbResults.custom) {

View file

@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraWrappableRequest } from '../../lib/adapters/framework';
export interface InfraTimerange {
field: string;
from: number;
@ -27,7 +25,7 @@ export interface MetricsExplorerMetric {
field?: string | undefined;
}
export interface MetricsExplorerRequest {
export interface MetricsExplorerRequestBody {
timerange: InfraTimerange;
indexPattern: string;
metrics: MetricsExplorerMetric[];
@ -37,8 +35,6 @@ export interface MetricsExplorerRequest {
filterQuery?: string;
}
export type MetricsExplorerWrappedRequest = InfraWrappableRequest<MetricsExplorerRequest>;
export interface MetricsExplorerPageInfo {
total: number;
afterKey?: string | null;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { boomify } from 'boom';
import { schema } from '@kbn/config-schema';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
@ -13,27 +13,34 @@ import { UsageCollector } from '../../usage/usage_collector';
import { InfraMetricsRequestOptions } from '../../lib/adapters/metrics';
import { InfraNodeType, InfraMetric } from '../../graphql/types';
import {
NodeDetailsWrappedRequest,
NodeDetailsRequestRT,
NodeDetailsMetricDataResponse,
NodeDetailsMetricDataResponseRT,
} from '../../../common/http_api/node_details_api';
import { throwErrors } from '../../../common/runtime_types';
const escapeHatch = schema.object({}, { allowUnknowns: true });
export const initNodeDetailsRoute = (libs: InfraBackendLibs) => {
const { framework } = libs;
framework.registerRoute<NodeDetailsWrappedRequest, Promise<NodeDetailsMetricDataResponse>>({
method: 'POST',
path: '/api/metrics/node_details',
handler: async req => {
const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe(
NodeDetailsRequestRT.decode(req.payload),
fold(throwErrors(Boom.badRequest), identity)
);
framework.registerRoute(
{
method: 'post',
path: '/api/metrics/node_details',
validate: {
body: escapeHatch,
},
},
async (requestContext, request, response) => {
try {
const source = await libs.sources.getSourceConfiguration(req, sourceId);
const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe(
NodeDetailsRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const source = await libs.sources.getSourceConfiguration(requestContext, sourceId);
UsageCollector.countNode(nodeType);
const options: InfraMetricsRequestOptions = {
nodeIds: {
nodeId,
@ -44,13 +51,16 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => {
metrics: metrics as InfraMetric[],
timerange,
};
return {
metrics: await libs.metrics.getMetrics(req, options),
};
} catch (e) {
throw boomify(e);
return response.ok({
body: NodeDetailsMetricDataResponseRT.encode({
metrics: await libs.metrics.getMetrics(requestContext, options, request),
}),
});
} catch (error) {
return response.internalError({
body: error.message,
});
}
},
});
}
);
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { schema } from '@kbn/config-schema';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
@ -12,37 +13,50 @@ import { InfraSnapshotRequestOptions } from '../../lib/snapshot';
import { UsageCollector } from '../../usage/usage_collector';
import { parseFilterQuery } from '../../utils/serialized_query';
import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types';
import {
SnapshotRequestRT,
SnapshotWrappedRequest,
SnapshotNodeResponse,
} from '../../../common/http_api/snapshot_api';
import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api';
import { throwErrors } from '../../../common/runtime_types';
const escapeHatch = schema.object({}, { allowUnknowns: true });
export const initSnapshotRoute = (libs: InfraBackendLibs) => {
const { framework } = libs;
framework.registerRoute<SnapshotWrappedRequest, Promise<SnapshotNodeResponse>>({
method: 'POST',
path: '/api/metrics/snapshot',
handler: async req => {
const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe(
SnapshotRequestRT.decode(req.payload),
fold(throwErrors(Boom.badRequest), identity)
);
const source = await libs.sources.getSourceConfiguration(req, sourceId);
UsageCollector.countNode(nodeType);
const options: InfraSnapshotRequestOptions = {
filterQuery: parseFilterQuery(filterQuery),
// TODO: Use common infra metric and replace graphql type
nodeType: nodeType as InfraNodeType,
groupBy,
sourceConfiguration: source.configuration,
// TODO: Use common infra metric and replace graphql type
metric: metric as InfraSnapshotMetricInput,
timerange,
};
return { nodes: await libs.snapshot.getNodes(req, options) };
framework.registerRoute(
{
method: 'post',
path: '/api/metrics/snapshot',
validate: {
body: escapeHatch,
},
},
});
async (requestContext, request, response) => {
try {
const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe(
SnapshotRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const source = await libs.sources.getSourceConfiguration(requestContext, sourceId);
UsageCollector.countNode(nodeType);
const options: InfraSnapshotRequestOptions = {
filterQuery: parseFilterQuery(filterQuery),
// TODO: Use common infra metric and replace graphql type
nodeType: nodeType as InfraNodeType,
groupBy,
sourceConfiguration: source.configuration,
// TODO: Use common infra metric and replace graphql type
metric: metric as InfraSnapshotMetricInput,
timerange,
};
return response.ok({
body: SnapshotNodeResponseRT.encode({
nodes: await libs.snapshot.getNodes(requestContext, options),
}),
});
} catch (error) {
return response.internalError({
body: error.message,
});
}
}
);
};

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../lib/adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
interface Options {
indexPattern: string;
@ -20,8 +21,8 @@ interface Options {
* This is useful for visualizing metric modules like s3 that only send metrics once per day.
*/
export const calculateMetricInterval = async (
framework: InfraBackendFrameworkAdapter,
request: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
options: Options,
modules: string[]
) => {
@ -64,7 +65,11 @@ export const calculateMetricInterval = async (
},
};
const resp = await framework.callWithRequest<{}, PeriodAggregationData>(request, 'search', query);
const resp = await framework.callWithRequest<{}, PeriodAggregationData>(
requestContext,
'search',
query
);
// if ES doesn't return an aggregations key, something went seriously wrong.
if (!resp.aggregations) {

View file

@ -4,25 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
InfraBackendFrameworkAdapter,
InfraFrameworkRequest,
InfraDatabaseSearchResponse,
} from '../lib/adapters/framework';
import { RequestHandlerContext } from 'src/core/server';
import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
import { InfraDatabaseSearchResponse } from '../lib/adapters/framework';
export const getAllCompositeData = async <
Aggregation = undefined,
Bucket = {},
Options extends object = {}
>(
framework: InfraBackendFrameworkAdapter,
request: InfraFrameworkRequest,
framework: KibanaFramework,
requestContext: RequestHandlerContext,
options: Options,
bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[],
onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options,
previousBuckets: Bucket[] = []
): Promise<Bucket[]> => {
const response = await framework.callWithRequest<{}, Aggregation>(request, 'search', options);
const response = await framework.callWithRequest<{}, Aggregation>(
requestContext,
'search',
options
);
// Nothing available, return the previous buckets.
if (response.hits.total.value === 0) {
@ -45,7 +47,7 @@ export const getAllCompositeData = async <
const newOptions = onAfterKey(options, response);
return getAllCompositeData(
framework,
request,
requestContext,
newOptions,
bucketSelector,
onAfterKey,

View file

@ -3,7 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server';
import {
PluginInitializerContext,
Plugin,
CoreSetup,
SavedObjectsClientContract,
} from 'src/core/server';
import { Observable, combineLatest, AsyncSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'hapi';
@ -11,6 +16,7 @@ import { once } from 'lodash';
import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server';
import { createApmAgentConfigurationIndex } from '../../../legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index';
import { createApmApi } from '../../../legacy/plugins/apm/server/routes/create_apm_api';
import { getApmIndices } from '../../../legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices';
import { APMConfig, mergeConfigs, APMXPackConfig } from '.';
export interface LegacySetup {
@ -20,13 +26,18 @@ export interface LegacySetup {
export interface APMPluginContract {
config$: Observable<APMConfig>;
registerLegacyAPI: (__LEGACY: LegacySetup) => void;
getApmIndices: (
savedObjectsClient: SavedObjectsClientContract
) => ReturnType<typeof getApmIndices>;
}
export class APMPlugin implements Plugin<APMPluginContract> {
legacySetup$: AsyncSubject<LegacySetup>;
currentConfig: APMConfig;
constructor(private readonly initContext: PluginInitializerContext) {
this.initContext = initContext;
this.legacySetup$ = new AsyncSubject();
this.currentConfig = {} as APMConfig;
}
public async setup(
@ -49,6 +60,7 @@ export class APMPlugin implements Plugin<APMPluginContract> {
await new Promise(resolve => {
combineLatest(mergedConfig$, core.elasticsearch.dataClient$).subscribe(
async ([config, dataClient]) => {
this.currentConfig = config;
await createApmAgentConfigurationIndex({
esClient: dataClient,
config,
@ -64,6 +76,9 @@ export class APMPlugin implements Plugin<APMPluginContract> {
this.legacySetup$.next(__LEGACY);
this.legacySetup$.complete();
}),
getApmIndices: async (savedObjectsClient: SavedObjectsClientContract) => {
return getApmIndices({ savedObjectsClient, config: this.currentConfig });
},
};
}

View file

@ -0,0 +1,6 @@
{
"id": "infra",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": true
}

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from 'src/core/server';
import { InfraPlugin } from './plugin';
export const config = {
schema: schema.object({
enabled: schema.maybe(schema.boolean()),
query: schema.object({
partitionSize: schema.maybe(schema.number()),
partitionFactor: schema.maybe(schema.number()),
}),
}),
};
export const plugin = (initContext: PluginInitializerContext) => new InfraPlugin(initContext);
export type InfraConfig = TypeOf<typeof config.schema>;
export { InfraSetup } from './plugin';

View file

@ -0,0 +1,33 @@
/*
* 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 { Plugin, PluginInitializerContext } from 'src/core/server';
export class InfraPlugin implements Plugin<InfraSetup> {
private readonly initContext: PluginInitializerContext;
constructor(initContext: PluginInitializerContext) {
this.initContext = initContext;
}
public setup() {
return {
__legacy: {
config: this.initContext.config,
},
};
}
public start() {}
public stop() {}
}
export interface InfraSetup {
/** @deprecated */
__legacy: {
config: PluginInitializerContext['config'];
};
}

View file

@ -19,7 +19,6 @@ const introspectionQuery = gql`
`;
export default function({ getService }: FtrProviderContext) {
const supertest = getService('supertestWithoutAuth');
const security = getService('security');
const spaces = getService('spaces');
const clientFactory = getService('infraOpsGraphQLClientFactory');
@ -37,18 +36,6 @@ export default function({ getService }: FtrProviderContext) {
expect(result.response.data).to.be.an('object');
};
const expectGraphIQL404 = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 404);
};
const expectGraphIQLResponse = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 200);
};
const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => {
const queryOptions = {
query: introspectionQuery,
@ -70,16 +57,6 @@ export default function({ getService }: FtrProviderContext) {
};
};
const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => {
const basePath = spaceId ? `/s/${spaceId}` : '';
return supertest
.get(`${basePath}/api/infra/graphql/graphiql`)
.auth(username, password)
.then((response: any) => ({ error: undefined, response }))
.catch((error: any) => ({ error, response: undefined }));
};
describe('feature controls', () => {
it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => {
const username = 'logstash_read';
@ -105,9 +82,6 @@ export default function({ getService }: FtrProviderContext) {
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -144,9 +118,6 @@ export default function({ getService }: FtrProviderContext) {
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQLResponse(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -186,9 +157,6 @@ export default function({ getService }: FtrProviderContext) {
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -268,25 +236,16 @@ export default function({ getService }: FtrProviderContext) {
it('user_1 can access APIs in space_1', async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space1Id);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id);
expectGraphIQLResponse(graphQLIResult);
});
it(`user_1 can access APIs in space_2`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space2Id);
expectGraphQLResponse(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id);
expectGraphIQLResponse(graphQLIResult);
});
it(`user_1 can't access APIs in space_3`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space3Id);
expectGraphQL404(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id);
expectGraphIQL404(graphQLIResult);
});
});
});

View file

@ -1,26 +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;
* you may not use this file except in compliance with the Elastic License.
*/
declare module 'rison-node' {
export type RisonValue = null | boolean | number | string | RisonObject | RisonArray;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RisonArray extends Array<RisonValue> {}
export interface RisonObject {
[key: string]: RisonValue;
}
export const decode: (input: string) => RisonValue;
// eslint-disable-next-line @typescript-eslint/camelcase
export const decode_object: (input: string) => RisonObject;
export const encode: <Input extends RisonValue>(input: Input) => string;
// eslint-disable-next-line @typescript-eslint/camelcase
export const encode_object: <Input extends RisonObject>(input: Input) => string;
}