mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -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';
|
||||
|
||||
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 { createPlainError, decodeOrThrow, formatErrors, throwErrors } from '@kbn/io-ts-utils';
|
||||
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';
|
||||
import { Errors, Type } 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));
|
||||
export { createPlainError, decodeOrThrow, formatErrors, throwErrors };
|
||||
|
||||
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.
|
||||
*/
|
||||
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 { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export function LandingPage() {
|
||||
const { hasDataMap, isAllRequestsComplete } = useHasData();
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
application: { navigateToUrl, navigateToApp },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
|
||||
if (isAllRequestsComplete) {
|
||||
const { apm, infra_logs: logs } = hasDataMap;
|
||||
const hasApmData = apm?.hasData;
|
||||
const hasLogsData = logs?.hasData;
|
||||
useEffect(() => {
|
||||
if (isAllRequestsComplete) {
|
||||
const { apm, infra_logs: logs } = hasDataMap;
|
||||
const hasApmData = apm?.hasData;
|
||||
const hasLogsData = logs?.hasData;
|
||||
|
||||
if (hasLogsData) {
|
||||
navigateToUrl(basePath.prepend('/app/discover'));
|
||||
} else if (hasApmData) {
|
||||
navigateToUrl(basePath.prepend('/app/apm/services'));
|
||||
} else {
|
||||
navigateToUrl(basePath.prepend('/app/observabilityOnboarding'));
|
||||
if (hasLogsData) {
|
||||
navigateToApp(DISCOVER_APP_ID, {
|
||||
deepLinkId: 'log-explorer',
|
||||
});
|
||||
} else if (hasApmData) {
|
||||
navigateToUrl(basePath.prepend('/app/apm/services'));
|
||||
} else {
|
||||
navigateToUrl(basePath.prepend('/app/observabilityOnboarding'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [basePath, hasDataMap, isAllRequestsComplete, navigateToApp, navigateToUrl]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
|
|
@ -81,7 +81,10 @@
|
|||
"@kbn/stack-alerts-plugin",
|
||||
"@kbn/data-view-editor-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",
|
||||
"id": "@kbn/serverless-observability",
|
||||
"owner": ["@elastic/appex-sharedux", "@elastic/apm-ui"],
|
||||
"owner": [
|
||||
"@elastic/appex-sharedux",
|
||||
"@elastic/apm-ui"
|
||||
],
|
||||
"description": "Serverless customizations for observability.",
|
||||
"plugin": {
|
||||
"id": "serverlessObservability",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "serverless", "observability"],
|
||||
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml", "cloud"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"serverless",
|
||||
"observability"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"serverless",
|
||||
"observability",
|
||||
"observabilityShared",
|
||||
"kibanaReact",
|
||||
"management",
|
||||
"ml",
|
||||
"cloud"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"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 { appIds } from '@kbn/management-cards-navigation';
|
||||
import { getObservabilitySideNavComponent } from './components/side_navigation';
|
||||
import { createObservabilityDashboardRegistration } from './logs_signal/overview_registration';
|
||||
import {
|
||||
ServerlessObservabilityPluginSetup,
|
||||
ServerlessObservabilityPluginStart,
|
||||
|
@ -19,9 +20,20 @@ export class ServerlessObservabilityPlugin
|
|||
implements Plugin<ServerlessObservabilityPluginSetup, ServerlessObservabilityPluginStart>
|
||||
{
|
||||
public setup(
|
||||
_core: CoreSetup,
|
||||
_setupDeps: ServerlessObservabilityPluginSetupDependencies
|
||||
_core: CoreSetup<
|
||||
ServerlessObservabilityPluginStartDependencies,
|
||||
ServerlessObservabilityPluginStart
|
||||
>,
|
||||
setupDeps: ServerlessObservabilityPluginSetupDependencies
|
||||
): ServerlessObservabilityPluginSetup {
|
||||
setupDeps.observability.dashboard.register(
|
||||
createObservabilityDashboardRegistration({
|
||||
search: _core
|
||||
.getStartServices()
|
||||
.then(([_coreStart, startDeps]) => startDeps.data.search.search),
|
||||
})
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
* 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 {
|
||||
ObservabilitySharedPluginSetup,
|
||||
ObservabilitySharedPluginStart,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServerlessObservabilityPluginSetup {}
|
||||
|
@ -20,6 +22,7 @@ export interface ServerlessObservabilityPluginSetup {}
|
|||
export interface ServerlessObservabilityPluginStart {}
|
||||
|
||||
export interface ServerlessObservabilityPluginSetupDependencies {
|
||||
observability: ObservabilityPublicSetup;
|
||||
observabilityShared: ObservabilitySharedPluginSetup;
|
||||
serverless: ServerlessPluginSetup;
|
||||
management: ManagementSetup;
|
||||
|
@ -30,4 +33,5 @@ export interface ServerlessObservabilityPluginStartDependencies {
|
|||
serverless: ServerlessPluginStart;
|
||||
management: ManagementStart;
|
||||
cloud: CloudStart;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
|
|
@ -26,5 +26,8 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/management-cards-navigation",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/observability-plugin",
|
||||
"@kbn/io-ts-utils",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue