Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elast
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public';
import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-shared-plugin/public';
import type { SharePublicStart } from '@kbn/share-plugin/public/plugin';
import {
Expand All @@ -21,6 +21,7 @@ import { dynamic } from '@kbn/shared-ux-utility';
import { isDevMode } from '@kbn/xstate-utils';
import type { LogsLocatorParams } from '@kbn/logs-shared-plugin/common';
import { safeDecode } from '@kbn/rison';
import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids';
import { LazyAlertDropdownWrapper } from '../../alerting/log_threshold';
import { HelpCenterContent } from '../../components/help_center_content';
import { useReadOnlyBadge } from '../../hooks/use_readonly_badge';
Expand All @@ -29,6 +30,8 @@ import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'
import { NotFoundPage } from '../404';
import { getLogsAppRoutes } from './routes';

const StreamPage = dynamic(() => import('./stream').then((mod) => ({ default: mod.StreamPage })));

const LogEntryCategoriesPage = dynamic(() =>
import('./log_entry_categories').then((mod) => ({ default: mod.LogEntryCategoriesPage }))
);
Expand All @@ -50,6 +53,8 @@ const StateMachinePlayground = dynamic(() =>
export const LogsPageContent: React.FunctionComponent = () => {
const { application, share } = useKibana<{ share: SharePublicStart }>().services;

const isLogsStreamEnabled: boolean = useUiSetting(OBSERVABILITY_ENABLE_LOGS_STREAM, false);

const uiCapabilities = application?.capabilities;
const onboardingLocator = share?.url.locators.get<ObservabilityOnboardingLocatorParams>(
OBSERVABILITY_ONBOARDING_LOCATOR
Expand All @@ -59,7 +64,7 @@ export const LogsPageContent: React.FunctionComponent = () => {
const enableDeveloperRoutes = isDevMode();

useReadOnlyBadge(!uiCapabilities?.logs?.save);
const routes = getLogsAppRoutes();
const routes = getLogsAppRoutes({ isLogsStreamEnabled });

const settingsLinkProps = useLinkProps({
app: 'logs',
Expand Down Expand Up @@ -93,30 +98,34 @@ export const LogsPageContent: React.FunctionComponent = () => {
)}

<Routes>
<Route
path="/stream"
exact
render={(props) => {
const searchParams = new URLSearchParams(props.location.search);
const logFilterEncoded = searchParams.get('logFilter');
let locatorParams: LogsLocatorParams = {};

if (logFilterEncoded) {
const logFilter = safeDecode(logFilterEncoded) as LogsLocatorParams;
locatorParams = {
timeRange: logFilter?.timeRange,
query: logFilter?.query,
filters: logFilter?.filters,
refreshInterval: logFilter?.refreshInterval,
};
}

share.url.locators
.get<LogsLocatorParams>(ALL_DATASETS_LOCATOR_ID)
?.navigate(locatorParams);
return null;
}}
/>
{routes.stream ? (
<Route path={routes.stream.path} component={StreamPage} />
) : (
<Route
path="/stream"
exact
render={(props) => {
const searchParams = new URLSearchParams(props.location.search);
const logFilterEncoded = searchParams.get('logFilter');
let locatorParams: LogsLocatorParams = {};

if (logFilterEncoded) {
const logFilter = safeDecode(logFilterEncoded) as LogsLocatorParams;
locatorParams = {
timeRange: logFilter?.timeRange,
query: logFilter?.query,
filters: logFilter?.filters,
refreshInterval: logFilter?.refreshInterval,
};
}

share.url.locators
.get<LogsLocatorParams>(ALL_DATASETS_LOCATOR_ID)
?.navigate(locatorParams);
return null;
}}
/>
)}
<Route path={routes.logsAnomalies.path} component={LogEntryRatePage} />
<Route path={routes.logsCategories.path} component={LogEntryCategoriesPage} />
<Route path={routes.settings.path} component={LogsSettingsPage} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* 2.0.
*/

import { logsAnomaliesTitle, logCategoriesTitle, settingsTitle } from '../../translations';
import {
logsAnomaliesTitle,
logCategoriesTitle,
settingsTitle,
streamTitle,
} from '../../translations';

export interface LogsRoute {
id: string;
Expand All @@ -17,9 +22,10 @@ export interface LogsAppRoutes {
logsAnomalies: LogsRoute;
logsCategories: LogsRoute;
settings: LogsRoute;
stream?: LogsRoute;
}

export const getLogsAppRoutes = () => {
export const getLogsAppRoutes = ({ isLogsStreamEnabled }: { isLogsStreamEnabled: boolean }) => {
const routes: LogsAppRoutes = {
logsAnomalies: {
id: 'anomalies',
Expand All @@ -38,5 +44,13 @@ export const getLogsAppRoutes = () => {
},
};

if (isLogsStreamEnabled) {
routes.stream = {
id: 'stream',
title: streamTitle,
path: '/stream',
};
}

return routes;
};
10 changes: 8 additions & 2 deletions x-pack/solutions/observability/plugins/infra/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from '@kbn/observability-shared-plugin/common';
import type { NavigationEntry } from '@kbn/observability-shared-plugin/public';
import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability/constants';
import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids';
import type { InfraPublicConfig } from '../common/plugin_config_types';
import { createInventoryMetricRuleType } from './alerting/inventory';
import { createLogThresholdRuleType } from './alerting/log_threshold';
Expand Down Expand Up @@ -85,6 +86,8 @@ export class Plugin implements InfraClientPluginClass {
}

setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) {
const isLogsStreamEnabled = core.uiSettings.get(OBSERVABILITY_ENABLE_LOGS_STREAM, false);

if (pluginsSetup.home) {
registerFeatures(pluginsSetup.home);
}
Expand Down Expand Up @@ -130,7 +133,7 @@ export class Plugin implements InfraClientPluginClass {
)
);

const logRoutes = getLogsAppRoutes();
const logRoutes = getLogsAppRoutes({ isLogsStreamEnabled });

/** !! Need to be kept in sync with the deepLinks in x-pack/solutions/observability/plugins/infra/public/plugin.ts */
pluginsSetup.observabilityShared.navigation.registerSections(
Expand Down Expand Up @@ -352,10 +355,13 @@ const getLogsNavigationEntries = ({
});
}

// Display Stream nav entry when Logs Stream is enabled
if (routes.stream) entries.push(createNavEntryFromRoute(routes.stream));
// Display always Logs Anomalies and Logs Categories entries
entries.push(createNavEntryFromRoute(routes.logsAnomalies));
entries.push(createNavEntryFromRoute(routes.logsCategories));
entries.push(createNavEntryFromRoute(routes.settings));
// Display Logs Settings entry when Logs Stream is not enabled
if (!routes.stream) entries.push(createNavEntryFromRoute(routes.settings));

return entries;
};
Expand Down
2 changes: 2 additions & 0 deletions x-pack/test/functional/apps/infra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export default ({ loadTestFile }: FtrProviderContext) => {
loadTestFile(require.resolve('./logs/log_entry_categories_tab'));
loadTestFile(require.resolve('./logs/log_entry_rate_tab'));
loadTestFile(require.resolve('./logs/logs_source_configuration'));
loadTestFile(require.resolve('./logs/log_stream_date_nano'));
loadTestFile(require.resolve('./logs/link_to'));
loadTestFile(require.resolve('./logs/log_stream'));
loadTestFile(require.resolve('./logs/ml_job_id_formats/tests'));
});
});
Expand Down
92 changes: 92 additions & 0 deletions x-pack/test/functional/apps/infra/logs/log_stream.ts
Original file line number Diff line number Diff line change
@@ -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 expect from '@kbn/expect';
import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids';
import { URL } from 'url';
import { FtrProviderContext } from '../../../ftr_provider_context';

const SERVICE_ID = '49a18510598271e924253ed2581d7ada';

export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common']);
const retry = getService('retry');
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');

describe('Log stream', function () {
describe('Legacy URL handling', () => {
describe('Correctly handles legacy versions of logFilter', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics');
await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true });
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'
);
await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false });
});
it('Expression and kind', async () => {
const location = {
hash: '',
pathname: '/stream',
search: `logFilter=(expression:'service.id:"${SERVICE_ID}"',kind:kuery)`,
state: undefined,
};

await pageObjects.common.navigateToUrlWithBrowserHistory(
'infraLogs',
location.pathname,
location.search,
{
ensureCurrentUrl: false,
}
);

await retry.tryForTime(5000, async () => {
const currentUrl = await browser.getCurrentUrl();
const parsedUrl = new URL(currentUrl);

expect(parsedUrl.pathname).to.be('/app/logs/stream');
expect(parsedUrl.searchParams.get('logFilter')).to.contain(
`(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')`
);
});
});
it('Top-level query and language', async () => {
const location = {
hash: '',
pathname: '/stream',
search: `logFilter=(query:'service.id:"${SERVICE_ID}"',language:kuery)`,
state: undefined,
};

await pageObjects.common.navigateToUrlWithBrowserHistory(
'infraLogs',
location.pathname,
location.search,
{
ensureCurrentUrl: false,
}
);

await retry.tryForTime(5000, async () => {
const currentUrl = await browser.getCurrentUrl();
const parsedUrl = new URL(currentUrl);

expect(parsedUrl.pathname).to.be('/app/logs/stream');
expect(parsedUrl.searchParams.get('logFilter')).to.contain(
`(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')`
);
});
});
});
});
});
};
91 changes: 91 additions & 0 deletions x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 expect from '@kbn/expect';
import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { DATES } from '../constants';

export default ({ getPageObjects, getService }: FtrProviderContext) => {
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const logsUi = getService('logsUi');
const find = getService('find');
const kibanaServer = getService('kibanaServer');
const logFilter = {
timeRange: {
from: DATES.metricsAndLogs.stream.startWithData,
to: DATES.metricsAndLogs.stream.endWithData,
},
};

describe('Log stream supports nano precision', function () {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/infra/logs_with_nano_date');
await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true });
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/infra/logs_with_nano_date');
await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false });
});

it('should display logs entries containing date_nano timestamps properly ', async () => {
await logsUi.logStreamPage.navigateTo({ logFilter });

const logStreamEntries = await logsUi.logStreamPage.getStreamEntries();

expect(logStreamEntries.length).to.be(4);
});

it('should render timestamp column properly', async () => {
await logsUi.logStreamPage.navigateTo({ logFilter });

await retry.try(async () => {
const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels();
expect(columnHeaderLabels[0]).to.eql('Oct 17, 2018');
});
});

it('should render timestamp column values properly', async () => {
await logsUi.logStreamPage.navigateTo({ logFilter });

const logStreamEntries = await logsUi.logStreamPage.getStreamEntries();

const firstLogStreamEntry = logStreamEntries[0];

const entryTimestamp = await logsUi.logStreamPage.getLogEntryColumnValueByName(
firstLogStreamEntry,
'timestampLogColumn'
);

expect(entryTimestamp).to.be('19:43:22.111');
});

it('should properly render timestamp in flyout with nano precision', async () => {
await logsUi.logStreamPage.navigateTo({ logFilter });

const logStreamEntries = await logsUi.logStreamPage.getStreamEntries();
const firstLogStreamEntry = logStreamEntries[0];

await logsUi.logStreamPage.openLogEntryDetailsFlyout(firstLogStreamEntry);

const cells = await find.allByCssSelector('.euiTableCellContent');

let isFound = false;

for (const cell of cells) {
const cellText = await cell.getVisibleText();
if (cellText === '2018-10-17T19:43:22.111111111Z') {
isFound = true;
return;
}
}

expect(isFound).to.be(true);
});
});
};
Loading