From 298d54781738071b6faa287549db9f6ed8c0d248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 9 Jan 2020 17:31:36 +0100 Subject: [PATCH] Add category examples route --- .../http_api/log_analysis/results/index.ts | 1 + .../results/log_entry_category_examples.ts | 74 ++++++++ .../common/log_analysis/job_parameters.ts | 7 + .../logs/log_analysis/api/ml_api_types.ts | 7 - .../api/ml_get_jobs_summary_api.ts | 8 +- .../logs/log_analysis/api/ml_get_module.ts | 7 +- .../log_analysis/api/ml_setup_module_api.ts | 12 +- .../plugins/infra/server/infra_server.ts | 2 + .../framework/kibana_framework_adapter.ts | 5 + .../infra/server/lib/log_analysis/errors.ts | 23 +++ .../log_entry_categories_analysis.ts | 163 +++++++++++++++++- .../server/lib/log_analysis/queries/common.ts | 8 + .../queries/log_entry_categories.ts | 13 +- .../queries/log_entry_category_examples.ts | 75 ++++++++ .../lib/log_analysis/queries/ml_jobs.ts | 21 +++ .../routes/log_analysis/results/index.ts | 1 + .../results/log_entry_category_examples.ts | 86 +++++++++ 17 files changed, 484 insertions(+), 29 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts create mode 100644 x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts create mode 100644 x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/ml_jobs.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts index d9ca9a96ffe51..15615046bdd6a 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/index.ts @@ -6,4 +6,5 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; +export * from './log_entry_category_examples'; export * from './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts new file mode 100644 index 0000000000000..8d1e0d24349c6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_category_examples.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { + badRequestErrorRT, + forbiddenErrorRT, + timeRangeRT, + routeTimingMetadataRT, +} from '../../shared'; + +export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH = + '/api/infra/log_analysis/results/log_entry_category_examples'; + +/** + * request + */ + +export const getLogEntryCategoryExamplesRequestPayloadRT = rt.type({ + data: rt.type({ + // the category to fetch the examples for + categoryId: rt.number, + // the number of examples to fetch + exampleCount: rt.number, + // the id of the source configuration + sourceId: rt.string, + // the time range to fetch the category examples from + timeRange: timeRangeRT, + }), +}); + +export type GetLogEntryCategoryExamplesRequestPayload = rt.TypeOf< + typeof getLogEntryCategoryExamplesRequestPayloadRT +>; + +/** + * response + */ + +const logEntryCategoryExampleRT = rt.type({ + timestamp: rt.number, + message: rt.string, +}); + +export type LogEntryCategoryExample = rt.TypeOf; + +export const getLogEntryCategoryExamplesSuccessReponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.type({ + examples: rt.array(logEntryCategoryExampleRT), + }), + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogEntryCategoryExamplesSuccessResponsePayload = rt.TypeOf< + typeof getLogEntryCategoryExamplesSuccessReponsePayloadRT +>; + +export const getLogEntryCategoryExamplesResponsePayloadRT = rt.union([ + getLogEntryCategoryExamplesSuccessReponsePayloadRT, + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type GetLogEntryCategoryExamplesReponsePayload = rt.TypeOf< + typeof getLogEntryCategoryExamplesResponsePayloadRT +>; diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts index 8c08e24d8665d..94643e21f1ea6 100644 --- a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts +++ b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts @@ -28,3 +28,10 @@ export const jobSourceConfigurationRT = rt.type({ }); export type JobSourceConfiguration = rt.TypeOf; + +export const jobCustomSettingsRT = rt.partial({ + job_revision: rt.number, + logs_source_config: rt.partial(jobSourceConfigurationRT.props), +}); + +export type JobCustomSettings = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts index 9d4d419ceebe3..ee70edc31d49b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts @@ -6,13 +6,6 @@ import * as rt from 'io-ts'; -import { jobSourceConfigurationRT } from '../../../../../common/log_analysis'; - -export const jobCustomSettingsRT = rt.partial({ - job_revision: rt.number, - logs_source_config: rt.partial(jobSourceConfigurationRT.props), -}); - export const getMlCapabilitiesResponsePayloadRT = rt.type({ capabilities: rt.type({ canGetJobs: rt.boolean, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index a067285026e33..87d6f8ba2f674 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; import { npStart } from 'ui/new_platform'; -import { jobCustomSettingsRT } from './ml_api_types'; -import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -import { getJobId } from '../../../../../common/log_analysis'; + +import { getJobId, jobCustomSettingsRT } from '../../../../../common/log_analysis'; +import { createPlainError, throwErrors } from '../../../../../common/runtime_types'; export const callJobsSummaryAPI = async ( spaceId: string, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts index ae90c7bb84130..639335396e08f 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts @@ -5,12 +5,13 @@ */ import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; import { npStart } from 'ui/new_platform'; -import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -import { jobCustomSettingsRT } from './ml_api_types'; + +import { jobCustomSettingsRT } from '../../../../../common/log_analysis'; +import { createPlainError, throwErrors } from '../../../../../common/runtime_types'; export const callGetMlModuleAPI = async (moduleId: string) => { const response = await npStart.core.http.fetch(`/api/ml/modules/get_module/${moduleId}`, { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 774a521ff1d34..e032021ada736 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -5,12 +5,13 @@ */ import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; import { npStart } from 'ui/new_platform'; -import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -import { getJobIdPrefix } from '../../../../../common/log_analysis'; + +import { getJobIdPrefix, jobCustomSettingsRT } from '../../../../../common/log_analysis'; +import { createPlainError, throwErrors } from '../../../../../common/runtime_types'; export const callSetupMlModuleAPI = async ( moduleId: string, @@ -48,7 +49,10 @@ const setupMlModuleTimeParamsRT = rt.partial({ end: rt.number, }); -const setupMlModuleJobOverridesRT = rt.object; +const setupMlModuleJobOverridesRT = rt.type({ + job_id: rt.string, + custom_settings: jobCustomSettingsRT, +}); export type SetupMlModuleJobOverrides = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 4f290cb05f056..f058b9e52c75b 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -14,6 +14,7 @@ import { InfraBackendLibs } from './lib/infra_types'; import { initGetLogEntryCategoriesRoute, initGetLogEntryCategoryDatasetsRoute, + initGetLogEntryCategoryExamplesRoute, initGetLogEntryRateRoute, initValidateLogAnalysisIndicesRoute, } from './routes/log_analysis'; @@ -45,6 +46,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initIpToHostName(libs); initGetLogEntryCategoriesRoute(libs); initGetLogEntryCategoryDatasetsRoute(libs); + initGetLogEntryCategoryExamplesRoute(libs); initGetLogEntryRateRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 4b1aa774a523f..1a6f4e32352c0 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -176,6 +176,11 @@ export class KibanaFramework { method: 'indices.get' | 'ml.getBuckets', options?: object ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'ml.getJobs', + options?: object + ): Promise; callWithRequest( requestContext: RequestHandlerContext, endpoint: string, diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts index d1c8316ad061b..e07126416f4ce 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/errors.ts @@ -4,9 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable max-classes-per-file */ + export class NoLogAnalysisResultsIndexError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, new.target.prototype); } } + +export class NoLogAnalysisMlJobError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InsufficientLogAnalysisMlJobConfigurationError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class UnknownCategoryError extends Error { + constructor(categoryId: number) { + super(`Unknown ml category ${categoryId}`); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index f2b6c468df69f..f10bdc2f503cf 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -5,11 +5,20 @@ */ import { KibanaRequest, RequestHandlerContext } from '../../../../../../../src/core/server'; -import { getJobId, logEntryCategoriesJobTypes } from '../../../common/log_analysis'; +import { + getJobId, + logEntryCategoriesJobTypes, + jobCustomSettingsRT, +} from '../../../common/log_analysis'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { NoLogAnalysisResultsIndexError } from './errors'; +import { + NoLogAnalysisResultsIndexError, + NoLogAnalysisMlJobError, + InsufficientLogAnalysisMlJobConfigurationError, + UnknownCategoryError, +} from './errors'; import { createLogEntryCategoriesQuery, logEntryCategoriesResponseRT, @@ -25,10 +34,15 @@ import { LogEntryDatasetBucket, logEntryDatasetsResponseRT, } from './queries/log_entry_data_sets'; +import { mlJobsResponseRT, createMlJobsQuery } from './queries/ml_jobs'; import { createTopLogEntryCategoriesQuery, topLogEntryCategoriesResponseRT, } from './queries/top_log_entry_categories'; +import { + createLogEntryCategoryExamplesQuery, + logEntryCategoryExamplesResponseRT, +} from './queries/log_entry_category_examples'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; @@ -175,6 +189,80 @@ export class LogEntryCategoriesAnalysis { }; } + public async getLogEntryCategoryExamples( + requestContext: RequestHandlerContext, + request: KibanaRequest, + sourceId: string, + startTime: number, + endTime: number, + categoryId: number, + exampleCount: number + ) { + const finalizeLogEntryCategoryExamplesSpan = startTracingSpan( + 'get category example log entries' + ); + + const logEntryCategoriesCountJobId = getJobId( + this.libs.framework.getSpaceId(request), + sourceId, + logEntryCategoriesJobTypes[0] + ); + + const { + mlJob, + timing: { spans: fetchMlJobSpans }, + } = await this.fetchMlJob(requestContext, logEntryCategoriesCountJobId); + + const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); + const indices = customSettings?.logs_source_config?.indexPattern; + const timestampField = customSettings?.logs_source_config?.timestampField; + + if (indices == null || timestampField == null) { + throw new InsufficientLogAnalysisMlJobConfigurationError( + `Failed to find index configuration for ml job ${logEntryCategoriesCountJobId}` + ); + } + + const { + logEntryCategoriesById, + timing: { spans: fetchLogEntryCategoriesSpans }, + } = await this.fetchLogEntryCategories(requestContext, logEntryCategoriesCountJobId, [ + categoryId, + ]); + const category = logEntryCategoriesById[categoryId]; + + if (category == null) { + throw new UnknownCategoryError(categoryId); + } + + const { + examples, + timing: { spans: fetchLogEntryCategoryExamplesSpans }, + } = await this.fetchLogEntryCategoryExamples( + requestContext, + indices, + timestampField, + startTime, + endTime, + category._source.terms, + exampleCount + ); + + const logEntryCategoryExamplesSpan = finalizeLogEntryCategoryExamplesSpan(); + + return { + data: examples, + timing: { + spans: [ + logEntryCategoryExamplesSpan, + ...fetchMlJobSpans, + ...fetchLogEntryCategoriesSpans, + ...fetchLogEntryCategoryExamplesSpans, + ], + }, + }; + } + private async fetchTopLogEntryCategories( requestContext: RequestHandlerContext, logEntryCategoriesCountJobId: string, @@ -351,6 +439,77 @@ export class LogEntryCategoriesAnalysis { }, }; } + + private async fetchMlJob( + requestContext: RequestHandlerContext, + logEntryCategoriesCountJobId: string + ) { + const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); + + const { + jobs: [mlJob], + } = decodeOrThrow(mlJobsResponseRT)( + await this.libs.framework.callWithRequest( + requestContext, + 'ml.getJobs', + createMlJobsQuery([logEntryCategoriesCountJobId]) + ) + ); + + const mlGetJobSpan = finalizeMlGetJobSpan(); + + if (mlJob == null) { + throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryCategoriesCountJobId}.`); + } + + return { + mlJob, + timing: { + spans: [mlGetJobSpan], + }, + }; + } + + private async fetchLogEntryCategoryExamples( + requestContext: RequestHandlerContext, + indices: string, + timestampField: string, + startTime: number, + endTime: number, + categoryQuery: string, + exampleCount: number + ) { + const finalizeEsSearchSpan = startTracingSpan('Fetch examples from ES'); + + const { + hits: { hits }, + } = decodeOrThrow(logEntryCategoryExamplesResponseRT)( + await this.libs.framework.callWithRequest( + requestContext, + 'search', + createLogEntryCategoryExamplesQuery( + indices, + timestampField, + startTime, + endTime, + categoryQuery, + exampleCount + ) + ) + ); + + const esSearchSpan = finalizeEsSearchSpan(); + + return { + examples: hits.map(hit => ({ + timestamp: hit.sort[0], + message: hit._source.message, + })), + timing: { + spans: [esSearchSpan], + }, + }; + } } const parseCategoryId = (rawCategoryId: string) => parseInt(rawCategoryId, 10); diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/common.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/common.ts index 92ef4fb4e35c9..f1e68d34fdae3 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/common.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/common.ts @@ -35,3 +35,11 @@ export const createResultTypeFilters = (resultType: 'model_plot' | 'record') => }, }, ]; + +export const createCategoryIdFilters = (categoryIds: number[]) => [ + { + terms: { + category_id: categoryIds, + }, + }, +]; diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts index 63b3632f03784..2681a4c037f5d 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts @@ -7,7 +7,7 @@ import * as rt from 'io-ts'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; -import { defaultRequestParameters, getMlResultIndex } from './common'; +import { defaultRequestParameters, getMlResultIndex, createCategoryIdFilters } from './common'; export const createLogEntryCategoriesQuery = ( logEntryCategoriesJobId: string, @@ -17,16 +17,10 @@ export const createLogEntryCategoriesQuery = ( body: { query: { bool: { - filter: [ - { - terms: { - category_id: categoryIds, - }, - }, - ], + filter: [...createCategoryIdFilters(categoryIds)], }, }, - _source: ['category_id', 'regex'], + _source: ['category_id', 'regex', 'terms'], }, index: getMlResultIndex(logEntryCategoriesJobId), size: categoryIds.length, @@ -36,6 +30,7 @@ export const logEntryCategoryHitRT = rt.type({ _source: rt.type({ category_id: rt.number, regex: rt.string, + terms: rt.string, }), }); diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts new file mode 100644 index 0000000000000..00c3e641bd33e --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; +import { defaultRequestParameters } from './common'; + +export const createLogEntryCategoryExamplesQuery = ( + indices: string, + timestampField: string, + startTime: number, + endTime: number, + categoryQuery: string, + exampleCount: number +) => ({ + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: [ + { + range: { + [timestampField]: { + gte: startTime, + lte: endTime, + }, + }, + }, + { + match: { + message: { + query: categoryQuery, + operator: 'AND', + }, + }, + }, + ], + }, + }, + _source: ['message'], + }, + index: indices, + size: exampleCount, + sort: [ + { + [timestampField]: { + order: 'asc', + }, + }, + ], +}); + +export const logEntryCategoryExampleHitRT = rt.type({ + _source: rt.type({ + message: rt.string, + }), + sort: rt.tuple([rt.number]), +}); + +export type LogEntryCategoryExampleHit = rt.TypeOf; + +export const logEntryCategoryExamplesResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(logEntryCategoryExampleHitRT), + }), + }), +]); + +export type logEntryCategoryExamplesResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/ml_jobs.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/ml_jobs.ts new file mode 100644 index 0000000000000..e361305b3686f --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/queries/ml_jobs.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const createMlJobsQuery = (jobIds: string[]) => ({ + job_id: jobIds, + allow_no_jobs: true, +}); + +export const mlJobRT = rt.type({ + job_id: rt.string, + custom_settings: rt.unknown, +}); + +export const mlJobsResponseRT = rt.type({ + jobs: rt.array(mlJobRT), +}); diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts index d9ca9a96ffe51..15615046bdd6a 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/index.ts @@ -6,4 +6,5 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; +export * from './log_entry_category_examples'; export * from './log_entry_rate'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts new file mode 100644 index 0000000000000..67c6c9f5b9924 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts @@ -0,0 +1,86 @@ +/* + * 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 } from '@kbn/config-schema'; +import Boom from 'boom'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + getLogEntryCategoryExamplesRequestPayloadRT, + getLogEntryCategoryExamplesSuccessReponsePayloadRT, + LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH, +} from '../../../../common/http_api/log_analysis'; +import { throwErrors } from '../../../../common/runtime_types'; +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; + +const anyObject = schema.object({}, { allowUnknowns: true }); + +export const initGetLogEntryCategoryExamplesRoute = ({ + framework, + logEntryCategoriesAnalysis, +}: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH, + validate: { + // short-circuit forced @kbn/config-schema validation so we can do io-ts validation + body: anyObject, + }, + }, + async (requestContext, request, response) => { + const { + data: { + categoryId, + exampleCount, + sourceId, + timeRange: { startTime, endTime }, + }, + } = pipe( + getLogEntryCategoryExamplesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + try { + const { + data: logEntryCategoryExamples, + timing, + } = await logEntryCategoriesAnalysis.getLogEntryCategoryExamples( + requestContext, + request, + sourceId, + startTime, + endTime, + categoryId, + exampleCount + ); + + return response.ok({ + body: getLogEntryCategoryExamplesSuccessReponsePayloadRT.encode({ + data: { + examples: logEntryCategoryExamples, + }, + timing, + }), + }); + } catch (e) { + const { statusCode = 500, message = 'Unknown error occurred' } = e; + + if (e instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message } }); + } + + return response.customError({ + statusCode, + body: { message }, + }); + } + } + ); +};