mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Logs+] Fix landing page log data check and redirect (#162662)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gil Raphaelli <graphaelli@gmail.com>
This commit is contained in:
parent
3763a5a134
commit
0069062fb4
10 changed files with 218 additions and 66 deletions
|
@ -30,3 +30,5 @@ export {
|
||||||
} from './src/is_greater_or_equal';
|
} from './src/is_greater_or_equal';
|
||||||
|
|
||||||
export { datemathStringRt } from './src/datemath_string_rt';
|
export { datemathStringRt } from './src/datemath_string_rt';
|
||||||
|
|
||||||
|
export { createPlainError, decodeOrThrow, formatErrors, throwErrors } from './src/decode_or_throw';
|
||||||
|
|
54
packages/kbn-io-ts-utils/src/decode_or_throw.ts
Normal file
54
packages/kbn-io-ts-utils/src/decode_or_throw.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fold } from 'fp-ts/lib/Either';
|
||||||
|
import { identity } from 'fp-ts/lib/function';
|
||||||
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
|
import { Context, Errors, IntersectionType, Type, UnionType, ValidationError } from 'io-ts';
|
||||||
|
|
||||||
|
type ErrorFactory = (message: string) => Error;
|
||||||
|
|
||||||
|
const getErrorPath = ([first, ...rest]: Context): string[] => {
|
||||||
|
if (typeof first === 'undefined') {
|
||||||
|
return [];
|
||||||
|
} else if (first.type instanceof IntersectionType) {
|
||||||
|
const [, ...next] = rest;
|
||||||
|
return getErrorPath(next);
|
||||||
|
} else if (first.type instanceof UnionType) {
|
||||||
|
const [, ...next] = rest;
|
||||||
|
return [first.key, ...getErrorPath(next)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [first.key, ...getErrorPath(rest)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorType = ({ context }: ValidationError) =>
|
||||||
|
context[context.length - 1]?.type?.name ?? 'unknown';
|
||||||
|
|
||||||
|
const formatError = (error: ValidationError) =>
|
||||||
|
error.message ??
|
||||||
|
`in ${getErrorPath(error.context).join('/')}: ${JSON.stringify(
|
||||||
|
error.value
|
||||||
|
)} does not match expected type ${getErrorType(error)}`;
|
||||||
|
|
||||||
|
export const formatErrors = (errors: ValidationError[]) =>
|
||||||
|
`Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`;
|
||||||
|
|
||||||
|
export const createPlainError = (message: string) => new Error(message);
|
||||||
|
|
||||||
|
export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => {
|
||||||
|
throw createError(formatErrors(errors));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeOrThrow =
|
||||||
|
<DecodedValue, EncodedValue, InputValue>(
|
||||||
|
runtimeType: Type<DecodedValue, EncodedValue, InputValue>,
|
||||||
|
createError: ErrorFactory = createPlainError
|
||||||
|
) =>
|
||||||
|
(inputValue: InputValue) =>
|
||||||
|
pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity));
|
|
@ -6,52 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RouteValidationFunction } from '@kbn/core/server';
|
import type { RouteValidationFunction } from '@kbn/core/server';
|
||||||
|
import { createPlainError, decodeOrThrow, formatErrors, throwErrors } from '@kbn/io-ts-utils';
|
||||||
import { fold } from 'fp-ts/lib/Either';
|
import { fold } from 'fp-ts/lib/Either';
|
||||||
import { identity } from 'fp-ts/lib/function';
|
|
||||||
import { pipe } from 'fp-ts/lib/pipeable';
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
import { Context, Errors, IntersectionType, Type, UnionType, ValidationError } from 'io-ts';
|
import { Errors, Type } from 'io-ts';
|
||||||
|
|
||||||
type ErrorFactory = (message: string) => Error;
|
export { createPlainError, decodeOrThrow, formatErrors, throwErrors };
|
||||||
|
|
||||||
const getErrorPath = ([first, ...rest]: Context): string[] => {
|
|
||||||
if (typeof first === 'undefined') {
|
|
||||||
return [];
|
|
||||||
} else if (first.type instanceof IntersectionType) {
|
|
||||||
const [, ...next] = rest;
|
|
||||||
return getErrorPath(next);
|
|
||||||
} else if (first.type instanceof UnionType) {
|
|
||||||
const [, ...next] = rest;
|
|
||||||
return [first.key, ...getErrorPath(next)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [first.key, ...getErrorPath(rest)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorType = ({ context }: ValidationError) =>
|
|
||||||
context[context.length - 1]?.type?.name ?? 'unknown';
|
|
||||||
|
|
||||||
const formatError = (error: ValidationError) =>
|
|
||||||
error.message ??
|
|
||||||
`in ${getErrorPath(error.context).join('/')}: ${JSON.stringify(
|
|
||||||
error.value
|
|
||||||
)} does not match expected type ${getErrorType(error)}`;
|
|
||||||
|
|
||||||
export const formatErrors = (errors: ValidationError[]) =>
|
|
||||||
`Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`;
|
|
||||||
|
|
||||||
export const createPlainError = (message: string) => new Error(message);
|
|
||||||
|
|
||||||
export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => {
|
|
||||||
throw createError(formatErrors(errors));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeOrThrow =
|
|
||||||
<DecodedValue, EncodedValue, InputValue>(
|
|
||||||
runtimeType: Type<DecodedValue, EncodedValue, InputValue>,
|
|
||||||
createError: ErrorFactory = createPlainError
|
|
||||||
) =>
|
|
||||||
(inputValue: InputValue) =>
|
|
||||||
pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity));
|
|
||||||
|
|
||||||
type ValdidationResult<Value> = ReturnType<RouteValidationFunction<Value>>;
|
type ValdidationResult<Value> = ReturnType<RouteValidationFunction<Value>>;
|
||||||
|
|
||||||
|
|
|
@ -4,29 +4,35 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
import { useHasData } from '../../hooks/use_has_data';
|
import { useHasData } from '../../hooks/use_has_data';
|
||||||
import { useKibana } from '../../utils/kibana_react';
|
import { useKibana } from '../../utils/kibana_react';
|
||||||
|
|
||||||
export function LandingPage() {
|
export function LandingPage() {
|
||||||
const { hasDataMap, isAllRequestsComplete } = useHasData();
|
const { hasDataMap, isAllRequestsComplete } = useHasData();
|
||||||
const {
|
const {
|
||||||
application: { navigateToUrl },
|
application: { navigateToUrl, navigateToApp },
|
||||||
http: { basePath },
|
http: { basePath },
|
||||||
} = useKibana().services;
|
} = useKibana().services;
|
||||||
|
|
||||||
if (isAllRequestsComplete) {
|
useEffect(() => {
|
||||||
const { apm, infra_logs: logs } = hasDataMap;
|
if (isAllRequestsComplete) {
|
||||||
const hasApmData = apm?.hasData;
|
const { apm, infra_logs: logs } = hasDataMap;
|
||||||
const hasLogsData = logs?.hasData;
|
const hasApmData = apm?.hasData;
|
||||||
|
const hasLogsData = logs?.hasData;
|
||||||
|
|
||||||
if (hasLogsData) {
|
if (hasLogsData) {
|
||||||
navigateToUrl(basePath.prepend('/app/discover'));
|
navigateToApp(DISCOVER_APP_ID, {
|
||||||
} else if (hasApmData) {
|
deepLinkId: 'log-explorer',
|
||||||
navigateToUrl(basePath.prepend('/app/apm/services'));
|
});
|
||||||
} else {
|
} else if (hasApmData) {
|
||||||
navigateToUrl(basePath.prepend('/app/observabilityOnboarding'));
|
navigateToUrl(basePath.prepend('/app/apm/services'));
|
||||||
|
} else {
|
||||||
|
navigateToUrl(basePath.prepend('/app/observabilityOnboarding'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [basePath, hasDataMap, isAllRequestsComplete, navigateToApp, navigateToUrl]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,10 @@
|
||||||
"@kbn/stack-alerts-plugin",
|
"@kbn/stack-alerts-plugin",
|
||||||
"@kbn/data-view-editor-plugin",
|
"@kbn/data-view-editor-plugin",
|
||||||
"@kbn/actions-plugin",
|
"@kbn/actions-plugin",
|
||||||
"@kbn/core-capabilities-common"
|
"@kbn/core-capabilities-common",
|
||||||
|
"@kbn/deeplinks-analytics"
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,30 @@
|
||||||
{
|
{
|
||||||
"type": "plugin",
|
"type": "plugin",
|
||||||
"id": "@kbn/serverless-observability",
|
"id": "@kbn/serverless-observability",
|
||||||
"owner": ["@elastic/appex-sharedux", "@elastic/apm-ui"],
|
"owner": [
|
||||||
|
"@elastic/appex-sharedux",
|
||||||
|
"@elastic/apm-ui"
|
||||||
|
],
|
||||||
"description": "Serverless customizations for observability.",
|
"description": "Serverless customizations for observability.",
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"id": "serverlessObservability",
|
"id": "serverlessObservability",
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"configPath": ["xpack", "serverless", "observability"],
|
"configPath": [
|
||||||
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml", "cloud"],
|
"xpack",
|
||||||
|
"serverless",
|
||||||
|
"observability"
|
||||||
|
],
|
||||||
|
"requiredPlugins": [
|
||||||
|
"data",
|
||||||
|
"serverless",
|
||||||
|
"observability",
|
||||||
|
"observabilityShared",
|
||||||
|
"kibanaReact",
|
||||||
|
"management",
|
||||||
|
"ml",
|
||||||
|
"cloud"
|
||||||
|
],
|
||||||
"optionalPlugins": [],
|
"optionalPlugins": [],
|
||||||
"requiredBundles": []
|
"requiredBundles": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ISearchGeneric } from '@kbn/data-plugin/public';
|
||||||
|
import type {
|
||||||
|
DataHandler,
|
||||||
|
InfraLogsHasDataResponse,
|
||||||
|
LogsFetchDataResponse,
|
||||||
|
} from '@kbn/observability-plugin/public';
|
||||||
|
import * as rt from 'io-ts';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { decodeOrThrow } from '@kbn/io-ts-utils';
|
||||||
|
|
||||||
|
type InfraLogsDashboardAppName = 'infra_logs';
|
||||||
|
|
||||||
|
// check log data streams that match the naming convention, except for the APM
|
||||||
|
// error stream, because its presence would always mask the "APM only" case
|
||||||
|
const LOG_DATA_INDICES = 'logs-*-*,-logs-apm.error-*';
|
||||||
|
|
||||||
|
export function createObservabilityDashboardRegistration({
|
||||||
|
search,
|
||||||
|
}: {
|
||||||
|
search: Promise<ISearchGeneric>;
|
||||||
|
}): {
|
||||||
|
appName: InfraLogsDashboardAppName;
|
||||||
|
} & DataHandler<InfraLogsDashboardAppName> {
|
||||||
|
return {
|
||||||
|
appName: 'infra_logs',
|
||||||
|
fetchData: fetchObservabilityDashboardData,
|
||||||
|
hasData: hasObservabilityDashboardData({ search }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchObservabilityDashboardData(): Promise<LogsFetchDataResponse> {
|
||||||
|
throw new Error('Overview data fetching has not been implemented for serverless deployments.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasObservabilityDashboardData =
|
||||||
|
({ search }: { search: Promise<ISearchGeneric> }) =>
|
||||||
|
async (): Promise<InfraLogsHasDataResponse> => {
|
||||||
|
const hasData: boolean = await lastValueFrom(
|
||||||
|
(
|
||||||
|
await search
|
||||||
|
)({
|
||||||
|
params: {
|
||||||
|
ignore_unavailable: true,
|
||||||
|
allow_no_indices: true,
|
||||||
|
index: LOG_DATA_INDICES,
|
||||||
|
size: 0,
|
||||||
|
terminate_after: 1,
|
||||||
|
track_total_hits: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).then(
|
||||||
|
({ rawResponse }) => {
|
||||||
|
if (rawResponse._shards.total <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalHits = decodeTotalHits(rawResponse.hits.total);
|
||||||
|
if (typeof totalHits === 'number' ? totalHits > 0 : totalHits.value > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err.status === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to check status of log indices "${LOG_DATA_INDICES}": ${err}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasData,
|
||||||
|
indices: LOG_DATA_INDICES,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeTotalHits = decodeOrThrow(
|
||||||
|
rt.union([
|
||||||
|
rt.number,
|
||||||
|
rt.type({
|
||||||
|
value: rt.number,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
|
@ -8,6 +8,7 @@
|
||||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||||
import { appIds } from '@kbn/management-cards-navigation';
|
import { appIds } from '@kbn/management-cards-navigation';
|
||||||
import { getObservabilitySideNavComponent } from './components/side_navigation';
|
import { getObservabilitySideNavComponent } from './components/side_navigation';
|
||||||
|
import { createObservabilityDashboardRegistration } from './logs_signal/overview_registration';
|
||||||
import {
|
import {
|
||||||
ServerlessObservabilityPluginSetup,
|
ServerlessObservabilityPluginSetup,
|
||||||
ServerlessObservabilityPluginStart,
|
ServerlessObservabilityPluginStart,
|
||||||
|
@ -19,9 +20,20 @@ export class ServerlessObservabilityPlugin
|
||||||
implements Plugin<ServerlessObservabilityPluginSetup, ServerlessObservabilityPluginStart>
|
implements Plugin<ServerlessObservabilityPluginSetup, ServerlessObservabilityPluginStart>
|
||||||
{
|
{
|
||||||
public setup(
|
public setup(
|
||||||
_core: CoreSetup,
|
_core: CoreSetup<
|
||||||
_setupDeps: ServerlessObservabilityPluginSetupDependencies
|
ServerlessObservabilityPluginStartDependencies,
|
||||||
|
ServerlessObservabilityPluginStart
|
||||||
|
>,
|
||||||
|
setupDeps: ServerlessObservabilityPluginSetupDependencies
|
||||||
): ServerlessObservabilityPluginSetup {
|
): ServerlessObservabilityPluginSetup {
|
||||||
|
setupDeps.observability.dashboard.register(
|
||||||
|
createObservabilityDashboardRegistration({
|
||||||
|
search: _core
|
||||||
|
.getStartServices()
|
||||||
|
.then(([_coreStart, startDeps]) => startDeps.data.search.search),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,15 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||||
|
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
|
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||||
|
import { ObservabilityPublicSetup } from '@kbn/observability-plugin/public';
|
||||||
import {
|
import {
|
||||||
ObservabilitySharedPluginSetup,
|
ObservabilitySharedPluginSetup,
|
||||||
ObservabilitySharedPluginStart,
|
ObservabilitySharedPluginStart,
|
||||||
} from '@kbn/observability-shared-plugin/public';
|
} from '@kbn/observability-shared-plugin/public';
|
||||||
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
export interface ServerlessObservabilityPluginSetup {}
|
export interface ServerlessObservabilityPluginSetup {}
|
||||||
|
@ -20,6 +22,7 @@ export interface ServerlessObservabilityPluginSetup {}
|
||||||
export interface ServerlessObservabilityPluginStart {}
|
export interface ServerlessObservabilityPluginStart {}
|
||||||
|
|
||||||
export interface ServerlessObservabilityPluginSetupDependencies {
|
export interface ServerlessObservabilityPluginSetupDependencies {
|
||||||
|
observability: ObservabilityPublicSetup;
|
||||||
observabilityShared: ObservabilitySharedPluginSetup;
|
observabilityShared: ObservabilitySharedPluginSetup;
|
||||||
serverless: ServerlessPluginSetup;
|
serverless: ServerlessPluginSetup;
|
||||||
management: ManagementSetup;
|
management: ManagementSetup;
|
||||||
|
@ -30,4 +33,5 @@ export interface ServerlessObservabilityPluginStartDependencies {
|
||||||
serverless: ServerlessPluginStart;
|
serverless: ServerlessPluginStart;
|
||||||
management: ManagementStart;
|
management: ManagementStart;
|
||||||
cloud: CloudStart;
|
cloud: CloudStart;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,8 @@
|
||||||
"@kbn/i18n",
|
"@kbn/i18n",
|
||||||
"@kbn/management-cards-navigation",
|
"@kbn/management-cards-navigation",
|
||||||
"@kbn/cloud-plugin",
|
"@kbn/cloud-plugin",
|
||||||
|
"@kbn/data-plugin",
|
||||||
|
"@kbn/observability-plugin",
|
||||||
|
"@kbn/io-ts-utils",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue