[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:
Felix Stürmer 2023-08-04 19:16:00 +02:00 committed by GitHub
parent 3763a5a134
commit 0069062fb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 218 additions and 66 deletions

View file

@ -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';

View 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));

View file

@ -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>>;

View file

@ -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 <></>;
}

View file

@ -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/**/*"
]
}

View file

@ -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": []
}

View file

@ -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,
}),
])
);

View file

@ -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 {};
}

View file

@ -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;
}

View file

@ -26,5 +26,8 @@
"@kbn/i18n",
"@kbn/management-cards-navigation",
"@kbn/cloud-plugin",
"@kbn/data-plugin",
"@kbn/observability-plugin",
"@kbn/io-ts-utils",
]
}