From bb49c9616705090e1d6d3e7f25ce3fe49c06083d Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Fri, 17 Apr 2020 13:45:26 -0700 Subject: [PATCH] [Metrics UI] Allow users to create alerts from the central Alerts UI (#63803) --- .../infra/common/http_api/source_api.ts | 188 ++++++++++++++++++ .../alerting/metrics/expression.tsx | 111 +++-------- .../infra/public/containers/source/source.tsx | 2 +- .../containers/source/use_source_via_http.ts | 92 +++++++++ .../infra/public/hooks/use_http_request.tsx | 20 +- x-pack/plugins/infra/server/infra_server.ts | 2 + .../infra/server/lib/sources/defaults.ts | 2 +- .../plugins/infra/server/lib/sources/index.ts | 2 +- .../lib/sources/saved_object_mappings.ts | 2 +- .../infra/server/lib/sources/sources.ts | 8 +- .../plugins/infra/server/lib/sources/types.ts | 149 -------------- x-pack/plugins/infra/server/plugin.ts | 2 +- .../infra/server/routes/source/index.ts | 65 ++++++ 13 files changed, 402 insertions(+), 243 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/source_api.ts create mode 100644 x-pack/plugins/infra/public/containers/source/use_source_via_http.ts delete mode 100644 x-pack/plugins/infra/server/lib/sources/types.ts create mode 100644 x-pack/plugins/infra/server/routes/source/index.ts diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts new file mode 100644 index 0000000000000..218f8cebc9869 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/source_api.ts @@ -0,0 +1,188 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as rt from 'io-ts'; +import moment from 'moment'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { chain } from 'fp-ts/lib/Either'; + +export const TimestampFromString = new rt.Type( + 'TimestampFromString', + (input): input is number => typeof input === 'number', + (input, context) => + pipe( + rt.string.validate(input, context), + chain(stringInput => { + const momentValue = moment(stringInput); + return momentValue.isValid() + ? rt.success(momentValue.valueOf()) + : rt.failure(stringInput, context); + }) + ), + output => new Date(output).toISOString() +); + +/** + * Stored source configuration as read from and written to saved objects + */ + +const SavedSourceConfigurationFieldsRuntimeType = rt.partial({ + container: rt.string, + host: rt.string, + pod: rt.string, + tiebreaker: rt.string, + timestamp: rt.string, +}); + +export const SavedSourceConfigurationTimestampColumnRuntimeType = rt.type({ + timestampColumn: rt.type({ + id: rt.string, + }), +}); + +export const SavedSourceConfigurationMessageColumnRuntimeType = rt.type({ + messageColumn: rt.type({ + id: rt.string, + }), +}); + +export const SavedSourceConfigurationFieldColumnRuntimeType = rt.type({ + fieldColumn: rt.type({ + id: rt.string, + field: rt.string, + }), +}); + +export const SavedSourceConfigurationColumnRuntimeType = rt.union([ + SavedSourceConfigurationTimestampColumnRuntimeType, + SavedSourceConfigurationMessageColumnRuntimeType, + SavedSourceConfigurationFieldColumnRuntimeType, +]); + +export const SavedSourceConfigurationRuntimeType = rt.partial({ + name: rt.string, + description: rt.string, + metricAlias: rt.string, + logAlias: rt.string, + fields: SavedSourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export interface InfraSavedSourceConfiguration + extends rt.TypeOf {} + +export const pickSavedSourceConfiguration = ( + value: InfraSourceConfiguration +): InfraSavedSourceConfiguration => { + const { name, description, metricAlias, logAlias, fields, logColumns } = value; + const { container, host, pod, tiebreaker, timestamp } = fields; + + return { + name, + description, + metricAlias, + logAlias, + fields: { container, host, pod, tiebreaker, timestamp }, + logColumns, + }; +}; + +/** + * Static source configuration as read from the configuration file + */ + +const StaticSourceConfigurationFieldsRuntimeType = rt.partial({ + ...SavedSourceConfigurationFieldsRuntimeType.props, + message: rt.array(rt.string), +}); + +export const StaticSourceConfigurationRuntimeType = rt.partial({ + name: rt.string, + description: rt.string, + metricAlias: rt.string, + logAlias: rt.string, + fields: StaticSourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export interface InfraStaticSourceConfiguration + extends rt.TypeOf {} + +/** + * Full source configuration type after all cleanup has been done at the edges + */ + +const SourceConfigurationFieldsRuntimeType = rt.type({ + ...StaticSourceConfigurationFieldsRuntimeType.props, +}); + +export const SourceConfigurationRuntimeType = rt.type({ + ...SavedSourceConfigurationRuntimeType.props, + fields: SourceConfigurationFieldsRuntimeType, + logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), +}); + +export const SourceRuntimeType = rt.intersection([ + rt.type({ + id: rt.string, + origin: rt.keyof({ + fallback: null, + internal: null, + stored: null, + }), + configuration: SourceConfigurationRuntimeType, + }), + rt.partial({ + version: rt.string, + updatedAt: rt.number, + }), +]); + +export interface InfraSourceConfiguration + extends rt.TypeOf {} + +export interface InfraSource extends rt.TypeOf {} + +const SourceStatusFieldRuntimeType = rt.type({ + name: rt.string, + type: rt.string, + searchable: rt.boolean, + aggregatable: rt.boolean, + displayable: rt.boolean, +}); + +const SourceStatusRuntimeType = rt.type({ + logIndicesExist: rt.boolean, + metricIndicesExist: rt.boolean, + indexFields: rt.array(SourceStatusFieldRuntimeType), +}); + +export const SourceResponseRuntimeType = rt.type({ + source: SourceRuntimeType, + status: SourceStatusRuntimeType, +}); + +export type SourceResponse = rt.TypeOf; + +/** + * Saved object type with metadata + */ + +export const SourceConfigurationSavedObjectRuntimeType = rt.intersection([ + rt.type({ + id: rt.string, + attributes: SavedSourceConfigurationRuntimeType, + }), + rt.partial({ + version: rt.string, + updated_at: TimestampFromString, + }), +]); + +export interface SourceConfigurationSavedObject + extends rt.TypeOf {} diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 8353158d3a9c8..0e9da32aaa509 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -17,9 +17,6 @@ import { import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiExpression } from '@elastic/eui'; -import { EuiCallOut } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; import { MetricExpressionParams, Comparator, @@ -41,8 +38,8 @@ import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/ap import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { useSource } from '../../../containers/source'; import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; interface AlertContextMeta { currentOptions?: Partial; @@ -87,7 +84,12 @@ const defaultExpression = { export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; - const { source, createDerivedIndexPattern } = useSource({ sourceId: 'default' }); + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); @@ -208,40 +210,11 @@ export const Expressions: React.FC = props => { setAlertParams('groupBy', md.currentOptions.groupBy); } setAlertParams('sourceId', source?.id); + } else { + setAlertParams('criteria', [defaultExpression]); } }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps - // INFO: If there is metadata, you're in the metrics explorer context - const canAddConditions = !!alertsContext.metadata; - - if (!canAddConditions && !alertParams.criteria) { - return ( - <> - - - {' '} - - - - . - - } - color="warning" - iconType="help" - /> - - - ); - } - return ( <> @@ -258,7 +231,6 @@ export const Expressions: React.FC = props => { alertParams.criteria.map((e, idx) => { return ( 1} fields={derivedIndexPattern.fields} remove={removeExpression} @@ -281,20 +253,18 @@ export const Expressions: React.FC = props => { />
- {canAddConditions && ( - - - - )} + + +
@@ -347,7 +317,6 @@ export const Expressions: React.FC = props => { interface ExpressionRowProps { fields: IFieldType[]; - canEditAggField: boolean; expressionId: number; expression: MetricExpression; errors: IErrorObject; @@ -424,20 +393,17 @@ export const ExpressionRow: React.FC = props => { {aggType !== 'count' && ( - {!props.canEditAggField && } - {props.canEditAggField && ( - ({ - normalizedType: f.type, - name: f.name, - }))} - aggType={aggType} - errors={errors} - onChangeSelectedAggField={updateMetric} - /> - )} + ({ + normalizedType: f.type, + name: f.name, + }))} + aggType={aggType} + errors={errors} + onChangeSelectedAggField={updateMetric} + /> )} @@ -469,19 +435,6 @@ export const ExpressionRow: React.FC = props => { ); }; -export const DisabledAggField = ({ text }: { text: string }) => { - return ( - - ); -}; - export const aggregationType: { [key: string]: any } = { avg: { text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/source/source.tsx index a1310b1f33614..c2206769ef0ef 100644 --- a/x-pack/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/plugins/infra/public/containers/source/source.tsx @@ -21,7 +21,7 @@ import { updateSourceMutation } from './update_source.gql_query'; type Source = SourceQuery.Query['source']; -const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => { +export const pickIndexPattern = (source: Source | undefined, type: 'logs' | 'metrics' | 'both') => { if (!source) { return 'unknown-index'; } diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts new file mode 100644 index 0000000000000..bc6374a6538e3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -0,0 +1,92 @@ +/* + * 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 { useEffect, useMemo } from 'react'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import createContainer from 'constate'; +import { HttpHandler } from 'target/types/core/public/http'; +import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; +import { + SourceResponseRuntimeType, + SourceResponse, + InfraSource, +} from '../../../common/http_api/source_api'; +import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../common/runtime_types'; + +export const pickIndexPattern = ( + source: InfraSource | undefined, + type: 'logs' | 'metrics' | 'both' +) => { + if (!source) { + return 'unknown-index'; + } + if (type === 'logs') { + return source.configuration.logAlias; + } + if (type === 'metrics') { + return source.configuration.metricAlias; + } + return `${source.configuration.logAlias},${source.configuration.metricAlias}`; +}; + +interface Props { + sourceId: string; + type: 'logs' | 'metrics' | 'both'; + fetch?: HttpHandler; + toastWarning?: (input: ToastInput) => void; +} + +export const useSourceViaHttp = ({ + sourceId = 'default', + type = 'both', + fetch, + toastWarning, +}: Props) => { + const decodeResponse = (response: any) => { + return pipe( + SourceResponseRuntimeType.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + `/api/metrics/source/${sourceId}/${type}`, + 'GET', + null, + decodeResponse, + fetch, + toastWarning + ); + + useEffect(() => { + (async () => { + await makeRequest(); + })(); + }, [makeRequest]); + + const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => { + return { + fields: response?.source ? response.status.indexFields : [], + title: pickIndexPattern(response?.source, indexType), + }; + }; + + const source = useMemo(() => { + return response ? { ...response.source, status: response.status } : null; + }, [response]); + + return { + createDerivedIndexPattern, + source, + loading, + error, + }; +}; + +export const SourceViaHttp = createContainer(useSourceViaHttp); +export const [SourceViaHttpProvider, useSourceViaHttpContext] = SourceViaHttp; diff --git a/x-pack/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/plugins/infra/public/hooks/use_http_request.tsx index 50f4a636b48a3..e00abe6380498 100644 --- a/x-pack/plugins/infra/public/hooks/use_http_request.tsx +++ b/x-pack/plugins/infra/public/hooks/use_http_request.tsx @@ -7,28 +7,32 @@ import React, { useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { i18n } from '@kbn/i18n'; +import { HttpHandler } from 'target/types/core/public/http'; +import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; import { useTrackedPromise } from '../utils/use_tracked_promise'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; export function useHTTPRequest( pathname: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD', - body?: string, - decode: (response: any) => Response = response => response + body?: string | null, + decode: (response: any) => Response = response => response, + fetch?: HttpHandler, + toastWarning?: (input: ToastInput) => void ) { const kibana = useKibana(); - const fetch = kibana.services.http?.fetch; - const toasts = kibana.notifications.toasts; + const fetchService = fetch ? fetch : kibana.services.http?.fetch; + const toast = toastWarning ? toastWarning : kibana.notifications.toasts.warning; const [response, setResponse] = useState(null); const [error, setError] = useState(null); const [request, makeRequest] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: () => { - if (!fetch) { + if (!fetchService) { throw new Error('HTTP service is unavailable'); } - return fetch(pathname, { + return fetchService(pathname, { method, body, }); @@ -37,7 +41,7 @@ export function useHTTPRequest( onReject: (e: unknown) => { const err = e as IHttpFetchError; setError(err); - toasts.warning({ + toast({ toastLifeTimeMs: 3000, title: i18n.translate('xpack.infra.useHTTPRequest.error.title', { defaultMessage: `Error while fetching resource`, @@ -67,7 +71,7 @@ export function useHTTPRequest( }); }, }, - [pathname, body, method, fetch, toasts] + [pathname, body, method, fetch, toast] ); const loading = useMemo(() => { diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index fb9dd172bf6ed..88b78dfd3e41c 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -29,6 +29,7 @@ import { initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initSourceRoute } from './routes/source'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ @@ -48,6 +49,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetLogEntryRateRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); + initSourceRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initLogEntriesRoute(libs); initLogEntriesHighlightsRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index b9ead0d169ee6..6b00f59cca1f4 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraSourceConfiguration } from './types'; +import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', diff --git a/x-pack/plugins/infra/server/lib/sources/index.ts b/x-pack/plugins/infra/server/lib/sources/index.ts index 6837f953ea18a..9dcbe02bd064b 100644 --- a/x-pack/plugins/infra/server/lib/sources/index.ts +++ b/x-pack/plugins/infra/server/lib/sources/index.ts @@ -7,4 +7,4 @@ export * from './defaults'; export * from './saved_object_mappings'; export * from './sources'; -export * from './types'; +export * from '../../../common/http_api/source_api'; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts index 973a790eeedaf..e5b230373b7ec 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_mappings.ts @@ -5,7 +5,7 @@ */ import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { InfraSavedSourceConfiguration } from './types'; +import { InfraSavedSourceConfiguration } from '../../../common/http_api/source_api'; export const infraSourceConfigurationSavedObjectType = 'infrastructure-ui-source'; diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index c7ff6c9638204..99e062aa49ccf 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -20,7 +20,8 @@ import { pickSavedSourceConfiguration, SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, -} from './types'; + InfraSource, +} from '../../../common/http_api/source_api'; import { InfraConfig } from '../../../server'; interface Libs { @@ -35,7 +36,10 @@ export class InfraSources { this.libs = libs; } - public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { + public async getSourceConfiguration( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) diff --git a/x-pack/plugins/infra/server/lib/sources/types.ts b/x-pack/plugins/infra/server/lib/sources/types.ts deleted file mode 100644 index 1f850635cf35a..0000000000000 --- a/x-pack/plugins/infra/server/lib/sources/types.ts +++ /dev/null @@ -1,149 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; -import moment from 'moment'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { chain } from 'fp-ts/lib/Either'; - -export const TimestampFromString = new runtimeTypes.Type( - 'TimestampFromString', - (input): input is number => typeof input === 'number', - (input, context) => - pipe( - runtimeTypes.string.validate(input, context), - chain(stringInput => { - const momentValue = moment(stringInput); - return momentValue.isValid() - ? runtimeTypes.success(momentValue.valueOf()) - : runtimeTypes.failure(stringInput, context); - }) - ), - output => new Date(output).toISOString() -); - -/** - * Stored source configuration as read from and written to saved objects - */ - -const SavedSourceConfigurationFieldsRuntimeType = runtimeTypes.partial({ - container: runtimeTypes.string, - host: runtimeTypes.string, - pod: runtimeTypes.string, - tiebreaker: runtimeTypes.string, - timestamp: runtimeTypes.string, -}); - -export const SavedSourceConfigurationTimestampColumnRuntimeType = runtimeTypes.type({ - timestampColumn: runtimeTypes.type({ - id: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationMessageColumnRuntimeType = runtimeTypes.type({ - messageColumn: runtimeTypes.type({ - id: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationFieldColumnRuntimeType = runtimeTypes.type({ - fieldColumn: runtimeTypes.type({ - id: runtimeTypes.string, - field: runtimeTypes.string, - }), -}); - -export const SavedSourceConfigurationColumnRuntimeType = runtimeTypes.union([ - SavedSourceConfigurationTimestampColumnRuntimeType, - SavedSourceConfigurationMessageColumnRuntimeType, - SavedSourceConfigurationFieldColumnRuntimeType, -]); - -export const SavedSourceConfigurationRuntimeType = runtimeTypes.partial({ - name: runtimeTypes.string, - description: runtimeTypes.string, - metricAlias: runtimeTypes.string, - logAlias: runtimeTypes.string, - fields: SavedSourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraSavedSourceConfiguration - extends runtimeTypes.TypeOf {} - -export const pickSavedSourceConfiguration = ( - value: InfraSourceConfiguration -): InfraSavedSourceConfiguration => { - const { name, description, metricAlias, logAlias, fields, logColumns } = value; - const { container, host, pod, tiebreaker, timestamp } = fields; - - return { - name, - description, - metricAlias, - logAlias, - fields: { container, host, pod, tiebreaker, timestamp }, - logColumns, - }; -}; - -/** - * Static source configuration as read from the configuration file - */ - -const StaticSourceConfigurationFieldsRuntimeType = runtimeTypes.partial({ - ...SavedSourceConfigurationFieldsRuntimeType.props, - message: runtimeTypes.array(runtimeTypes.string), -}); - -export const StaticSourceConfigurationRuntimeType = runtimeTypes.partial({ - name: runtimeTypes.string, - description: runtimeTypes.string, - metricAlias: runtimeTypes.string, - logAlias: runtimeTypes.string, - fields: StaticSourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraStaticSourceConfiguration - extends runtimeTypes.TypeOf {} - -/** - * Full source configuration type after all cleanup has been done at the edges - */ - -const SourceConfigurationFieldsRuntimeType = runtimeTypes.type({ - ...StaticSourceConfigurationFieldsRuntimeType.props, -}); - -export const SourceConfigurationRuntimeType = runtimeTypes.type({ - ...SavedSourceConfigurationRuntimeType.props, - fields: SourceConfigurationFieldsRuntimeType, - logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType), -}); - -export interface InfraSourceConfiguration - extends runtimeTypes.TypeOf {} - -/** - * Saved object type with metadata - */ - -export const SourceConfigurationSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedSourceConfigurationRuntimeType, - }), - runtimeTypes.partial({ - version: runtimeTypes.string, - updated_at: TimestampFromString, - }), -]); - -export interface SourceConfigurationSavedObject - extends runtimeTypes.TypeOf {} diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 64fc496f3597e..e3804078604cc 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -26,7 +26,7 @@ import { InfraSources } from './lib/sources'; import { InfraServerPluginDeps } from './lib/adapters/framework'; import { METRICS_FEATURE, LOGS_FEATURE } from './features'; import { UsageCollector } from './usage/usage_collector'; -import { InfraStaticSourceConfiguration } from './lib/sources/types'; +import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { registerAlertTypes } from './lib/alerting'; export const config = { diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts new file mode 100644 index 0000000000000..2f29320d7bb81 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -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 { schema } from '@kbn/config-schema'; +import { SourceResponseRuntimeType } from '../../../common/http_api/source_api'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { InfraIndexType } from '../../graphql/types'; + +const typeToInfraIndexType = (value: string | undefined) => { + switch (value) { + case 'metrics': + return InfraIndexType.METRICS; + case 'logs': + return InfraIndexType.LOGS; + default: + return InfraIndexType.ANY; + } +}; + +export const initSourceRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + + framework.registerRoute( + { + method: 'get', + path: '/api/metrics/source/{sourceId}/{type?}', + validate: { + params: schema.object({ + sourceId: schema.string(), + type: schema.string(), + }), + }, + }, + async (requestContext, request, response) => { + try { + const { type, sourceId } = request.params; + + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + if (!source) { + return response.notFound(); + } + + const status = { + logIndicesExist: await libs.sourceStatus.hasLogIndices(requestContext, sourceId), + metricIndicesExist: await libs.sourceStatus.hasMetricIndices(requestContext, sourceId), + indexFields: await libs.fields.getFields( + requestContext, + sourceId, + typeToInfraIndexType(type) + ), + }; + + return response.ok({ + body: SourceResponseRuntimeType.encode({ source, status }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +};