mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Logs UI] Register Logs UI Locators (#155156)
## Summary Closes https://github.com/elastic/kibana/issues/104855 This PR creates 2 registered locators: 1. Logs Locator 2. Node Logs Locator With these 2 locators, we now have a typed navigation to the logs UI which also redirects to Discover in serverless mode. ## Testing ### Normal behaviour When Kibana is used as always then on any navigation to the logs UI, the user will be redirected to the stream UI. All links to `link-to` routes should still behave as before. - Launch the Kibana dev environment with `yarn start` - Navigate to Hosts UI - Click the logs tab - Add a filter text in the search bar - Click on the Open in Logs link - Verify that navigation to the Stream view and the state is maintained https://user-images.githubusercontent.com/11225826/234514430-ddc1ffaa-0cb2-4f2a-84e9-6c6230937d9f.mov ### Serverless behaviour When Kibana is used in serverless mode, we want to redirect any user landing to Logs UI to the Discover page - Launch the Kibana dev environment with `yarn serverless-oblt` - Navigate to Hosts UI - Click the logs tab - Add a filter text in the search bar - Click on the Open in Logs link - Verify to be redirected to Discover and that the state is maintained https://user-images.githubusercontent.com/11225826/234514454-dfb2774e-d6f1-4f4c-ba10-77815dc1ae9d.mov ### Next Steps A separate PR will be created to fulfill the below AC - All usages of link-to routes in other apps are replaced with usage of the appropriate locator.
This commit is contained in:
parent
0eb1d0fc39
commit
5d96ef99d7
29 changed files with 871 additions and 666 deletions
|
@ -21,3 +21,6 @@ export const TIEBREAKER_FIELD = '_doc';
|
|||
export const HOST_FIELD = 'host.name';
|
||||
export const CONTAINER_FIELD = 'container.id';
|
||||
export const POD_FIELD = 'kubernetes.pod.uid';
|
||||
|
||||
export const DISCOVER_APP_TARGET = 'discover';
|
||||
export const LOGS_APP_TARGET = 'logs-ui';
|
||||
|
|
|
@ -4,95 +4,16 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { interpret } from 'xstate';
|
||||
import type { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import type { InfraClientStartDeps, InfraClientStartExports } from '../types';
|
||||
import type { LogViewColumnConfiguration, ResolvedLogView } from '../../common/log_views';
|
||||
import {
|
||||
createLogViewStateMachine,
|
||||
DEFAULT_LOG_VIEW,
|
||||
initializeFromUrl,
|
||||
} from '../observability_logs/log_view_state';
|
||||
import { MESSAGE_FIELD, TIMESTAMP_FIELD } from '../../common/constants';
|
||||
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import type { InfraClientStartExports } from '../types';
|
||||
import { getLogViewReferenceFromUrl } from '../observability_logs/log_view_state';
|
||||
|
||||
export const renderApp = (
|
||||
core: CoreStart,
|
||||
plugins: InfraClientStartDeps,
|
||||
pluginStart: InfraClientStartExports,
|
||||
params: AppMountParameters
|
||||
) => {
|
||||
const { discover } = plugins;
|
||||
const { logViews } = pluginStart;
|
||||
|
||||
const machine = createLogViewStateMachine({
|
||||
initialContext: { logViewReference: DEFAULT_LOG_VIEW },
|
||||
logViews: logViews.client,
|
||||
initializeFromUrl: createInitializeFromUrl(core, params),
|
||||
});
|
||||
|
||||
const service = interpret(machine)
|
||||
.onTransition((state) => {
|
||||
if (
|
||||
state.matches('checkingStatus') ||
|
||||
state.matches('resolvedPersistedLogView') ||
|
||||
state.matches('resolvedInlineLogView')
|
||||
) {
|
||||
return redirectToDiscover(discover, state.context.resolvedLogView);
|
||||
} else if (
|
||||
state.matches('loadingFailed') ||
|
||||
state.matches('resolutionFailed') ||
|
||||
state.matches('checkingStatusFailed')
|
||||
) {
|
||||
return redirectToDiscover(discover);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
|
||||
return () => {
|
||||
// Stop machine interpreter after navigation
|
||||
service.stop();
|
||||
};
|
||||
};
|
||||
|
||||
const redirectToDiscover = (discover: DiscoverStart, resolvedLogView?: ResolvedLogView) => {
|
||||
const navigationOptions = { replace: true };
|
||||
|
||||
if (!resolvedLogView) {
|
||||
return discover.locator?.navigate({}, navigationOptions);
|
||||
}
|
||||
|
||||
const columns = parseColumns(resolvedLogView.columns);
|
||||
const dataViewSpec = resolvedLogView.dataViewReference.toSpec();
|
||||
|
||||
return discover.locator?.navigate(
|
||||
{
|
||||
columns,
|
||||
dataViewId: dataViewSpec.id,
|
||||
dataViewSpec,
|
||||
},
|
||||
navigationOptions
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
const parseColumns = (columns: ResolvedLogView['columns']) => {
|
||||
return columns.map(getColumnValue).filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
const getColumnValue = (column: LogViewColumnConfiguration) => {
|
||||
if ('messageColumn' in column) return MESSAGE_FIELD;
|
||||
if ('timestampColumn' in column) return TIMESTAMP_FIELD;
|
||||
if ('fieldColumn' in column) return column.fieldColumn.field;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createInitializeFromUrl = (core: CoreStart, params: AppMountParameters) => {
|
||||
const toastsService = core.notifications.toasts;
|
||||
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
|
@ -101,5 +22,9 @@ const createInitializeFromUrl = (core: CoreStart, params: AppMountParameters) =>
|
|||
useHashQuery: false,
|
||||
});
|
||||
|
||||
return initializeFromUrl({ toastsService, urlStateStorage });
|
||||
const logView = getLogViewReferenceFromUrl({ toastsService, urlStateStorage });
|
||||
|
||||
pluginStart.locators.logsLocator.navigate({ ...(logView ? { logView } : {}) }, { replace: true });
|
||||
|
||||
return () => true;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { LogsLocatorDependencies, LogsLocatorParams } from './logs_locator';
|
||||
|
||||
const DISCOVER_LOGS_LOCATOR_ID = 'DISCOVER_LOGS_LOCATOR';
|
||||
|
||||
export type DiscoverLogsLocator = LocatorPublic<LogsLocatorParams>;
|
||||
|
||||
export class DiscoverLogsLocatorDefinition implements LocatorDefinition<LogsLocatorParams> {
|
||||
public readonly id = DISCOVER_LOGS_LOCATOR_ID;
|
||||
|
||||
constructor(protected readonly deps: LogsLocatorDependencies) {}
|
||||
|
||||
public readonly getLocation = async (params: LogsLocatorParams) => {
|
||||
const { getLocationToDiscover } = await import('./helpers');
|
||||
|
||||
return getLocationToDiscover({ core: this.deps.core, ...params });
|
||||
};
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { NodeLogsLocatorDependencies, NodeLogsLocatorParams } from './node_logs_locator';
|
||||
|
||||
const DISCOVER_NODE_LOGS_LOCATOR_ID = 'DISCOVER_NODE_LOGS_LOCATOR';
|
||||
|
||||
export type DiscoverNodeLogsLocator = LocatorPublic<NodeLogsLocatorParams>;
|
||||
|
||||
export class DiscoverNodeLogsLocatorDefinition implements LocatorDefinition<NodeLogsLocatorParams> {
|
||||
public readonly id = DISCOVER_NODE_LOGS_LOCATOR_ID;
|
||||
|
||||
constructor(protected readonly deps: NodeLogsLocatorDependencies) {}
|
||||
|
||||
public readonly getLocation = async (params: NodeLogsLocatorParams) => {
|
||||
const { createNodeLogsQuery, getLocationToDiscover } = await import('./helpers');
|
||||
|
||||
const { timeRange, logView } = params;
|
||||
const query = createNodeLogsQuery(params);
|
||||
|
||||
return getLocationToDiscover({
|
||||
core: this.deps.core,
|
||||
timeRange,
|
||||
filter: query,
|
||||
logView,
|
||||
});
|
||||
};
|
||||
}
|
151
x-pack/plugins/infra/public/locators/helpers.ts
Normal file
151
x-pack/plugins/infra/public/locators/helpers.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { interpret } from 'xstate';
|
||||
import { waitFor } from 'xstate/lib/waitFor';
|
||||
import { flowRight } from 'lodash';
|
||||
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import type { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { findInventoryFields } from '../../common/inventory_models';
|
||||
import { MESSAGE_FIELD, TIMESTAMP_FIELD } from '../../common/constants';
|
||||
import {
|
||||
createLogViewStateMachine,
|
||||
DEFAULT_LOG_VIEW,
|
||||
replaceLogViewInQueryString,
|
||||
} from '../observability_logs/log_view_state';
|
||||
import { replaceLogFilterInQueryString } from '../observability_logs/log_stream_query_state';
|
||||
import { replaceLogPositionInQueryString } from '../observability_logs/log_stream_position_state/src/url_state_storage_service';
|
||||
import type { TimeRange } from '../../common/time';
|
||||
import type { LogsLocatorParams } from './logs_locator';
|
||||
import type { InfraClientCoreSetup } from '../types';
|
||||
import type {
|
||||
LogViewColumnConfiguration,
|
||||
LogViewReference,
|
||||
ResolvedLogView,
|
||||
} from '../../common/log_views';
|
||||
import type { NodeLogsLocatorParams } from './node_logs_locator';
|
||||
|
||||
interface LocationToDiscoverParams {
|
||||
core: InfraClientCoreSetup;
|
||||
timeRange?: TimeRange;
|
||||
filter?: string;
|
||||
logView?: LogViewReference;
|
||||
}
|
||||
|
||||
export const createNodeLogsQuery = (params: NodeLogsLocatorParams) => {
|
||||
const { nodeType, nodeId, filter } = params;
|
||||
|
||||
const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`;
|
||||
const query = filter ? `(${nodeFilter}) and (${filter})` : nodeFilter;
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const createSearchString = ({
|
||||
time,
|
||||
timeRange,
|
||||
filter = '',
|
||||
logView = DEFAULT_LOG_VIEW,
|
||||
}: LogsLocatorParams) => {
|
||||
return flowRight(
|
||||
replaceLogFilterInQueryString({ language: 'kuery', query: filter }, time, timeRange),
|
||||
replaceLogPositionInQueryString(time),
|
||||
replaceLogViewInQueryString(logView)
|
||||
)('');
|
||||
};
|
||||
|
||||
export const getLocationToDiscover = async ({
|
||||
core,
|
||||
timeRange,
|
||||
filter,
|
||||
logView,
|
||||
}: LocationToDiscoverParams) => {
|
||||
const [, plugins, pluginStart] = await core.getStartServices();
|
||||
const { discover } = plugins;
|
||||
const { logViews } = pluginStart;
|
||||
|
||||
const machine = createLogViewStateMachine({
|
||||
initialContext: { logViewReference: logView || DEFAULT_LOG_VIEW },
|
||||
logViews: logViews.client,
|
||||
});
|
||||
|
||||
const discoverParams: DiscoverAppLocatorParams = {
|
||||
...(timeRange ? { from: timeRange.startTime, to: timeRange.endTime } : {}),
|
||||
...(filter
|
||||
? {
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: filter,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
let discoverLocation;
|
||||
|
||||
const service = interpret(machine).start();
|
||||
const doneState = await waitFor(
|
||||
service,
|
||||
(state) =>
|
||||
state.matches('checkingStatus') ||
|
||||
state.matches('resolvedPersistedLogView') ||
|
||||
state.matches('resolvedInlineLogView') ||
|
||||
state.matches('loadingFailed') ||
|
||||
state.matches('resolutionFailed') ||
|
||||
state.matches('checkingStatusFailed')
|
||||
);
|
||||
|
||||
service.stop();
|
||||
|
||||
if ('resolvedLogView' in doneState.context) {
|
||||
discoverLocation = await constructDiscoverLocation(
|
||||
discover,
|
||||
discoverParams,
|
||||
doneState.context.resolvedLogView
|
||||
);
|
||||
} else {
|
||||
discoverLocation = await constructDiscoverLocation(discover, discoverParams);
|
||||
}
|
||||
|
||||
if (!discoverLocation) {
|
||||
throw new Error('Discover location not found');
|
||||
}
|
||||
|
||||
return discoverLocation;
|
||||
};
|
||||
|
||||
const constructDiscoverLocation = async (
|
||||
discover: DiscoverStart,
|
||||
discoverParams: DiscoverAppLocatorParams,
|
||||
resolvedLogView?: ResolvedLogView
|
||||
) => {
|
||||
if (!resolvedLogView) {
|
||||
return await discover.locator?.getLocation(discoverParams);
|
||||
}
|
||||
|
||||
const columns = parseColumns(resolvedLogView.columns);
|
||||
const dataViewSpec = resolvedLogView.dataViewReference.toSpec();
|
||||
|
||||
return await discover.locator?.getLocation({
|
||||
...discoverParams,
|
||||
columns,
|
||||
dataViewId: dataViewSpec.id,
|
||||
dataViewSpec,
|
||||
});
|
||||
};
|
||||
|
||||
const parseColumns = (columns: ResolvedLogView['columns']) => {
|
||||
return columns.map(getColumnValue).filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
const getColumnValue = (column: LogViewColumnConfiguration) => {
|
||||
if ('messageColumn' in column) return MESSAGE_FIELD;
|
||||
if ('timestampColumn' in column) return TIMESTAMP_FIELD;
|
||||
if ('fieldColumn' in column) return column.fieldColumn.field;
|
||||
|
||||
return null;
|
||||
};
|
21
x-pack/plugins/infra/public/locators/index.ts
Normal file
21
x-pack/plugins/infra/public/locators/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 type { DiscoverLogsLocator } from './discover_logs_locator';
|
||||
import type { DiscoverNodeLogsLocator } from './discover_node_logs_locator';
|
||||
import type { LogsLocator } from './logs_locator';
|
||||
import type { NodeLogsLocator } from './node_logs_locator';
|
||||
|
||||
export * from './discover_logs_locator';
|
||||
export * from './discover_node_logs_locator';
|
||||
export * from './logs_locator';
|
||||
export * from './node_logs_locator';
|
||||
|
||||
export interface InfraLocators {
|
||||
logsLocator: LogsLocator | DiscoverLogsLocator;
|
||||
nodeLogsLocator: NodeLogsLocator | DiscoverNodeLogsLocator;
|
||||
}
|
14
x-pack/plugins/infra/public/locators/locators.mock.ts
Normal file
14
x-pack/plugins/infra/public/locators/locators.mock.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import type { InfraLocators } from '.';
|
||||
|
||||
export const createLocatorMock = (): jest.Mocked<InfraLocators> => ({
|
||||
logsLocator: sharePluginMock.createLocator(),
|
||||
nodeLogsLocator: sharePluginMock.createLocator(),
|
||||
});
|
270
x-pack/plugins/infra/public/locators/locators.test.ts
Normal file
270
x-pack/plugins/infra/public/locators/locators.test.ts
Normal file
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { LogsLocatorDefinition, LogsLocatorDependencies } from './logs_locator';
|
||||
import { NodeLogsLocatorDefinition } from './node_logs_locator';
|
||||
import type { LogsLocatorParams } from './logs_locator';
|
||||
import type { NodeLogsLocatorParams } from './node_logs_locator';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { findInventoryFields } from '../../common/inventory_models';
|
||||
import moment from 'moment';
|
||||
import { DEFAULT_LOG_VIEW } from '../observability_logs/log_view_state';
|
||||
import type { LogViewReference } from '../../common/log_views';
|
||||
|
||||
const setupLogsLocator = async () => {
|
||||
const deps: LogsLocatorDependencies = {
|
||||
core: coreMock.createSetup(),
|
||||
};
|
||||
const logsLocator = new LogsLocatorDefinition(deps);
|
||||
const nodeLogsLocator = new NodeLogsLocatorDefinition(deps);
|
||||
|
||||
return {
|
||||
logsLocator,
|
||||
nodeLogsLocator,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Infra Locators', () => {
|
||||
const APP_ID = 'logs';
|
||||
const nodeType = 'host';
|
||||
const FILTER_QUERY = 'trace.id:1234';
|
||||
const nodeId = uuidv4();
|
||||
const time = 1550671089404;
|
||||
const from = 1676815089000;
|
||||
const to = 1682351734323;
|
||||
|
||||
describe('Logs Locator', () => {
|
||||
it('should create a link to Logs with no state', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
time,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { app, path, state } = await logsLocator.getLocation(params);
|
||||
|
||||
expect(app).toBe(APP_ID);
|
||||
expect(path).toBe(constructUrlSearchString(params));
|
||||
expect(state).toBeDefined();
|
||||
expect(Object.keys(state)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should allow specifying specific logPosition', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
time,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { path } = await logsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific filter', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
time,
|
||||
filter: FILTER_QUERY,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { path } = await logsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific view id', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
time,
|
||||
logView: DEFAULT_LOG_VIEW,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { path } = await logsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific time range', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
time,
|
||||
from,
|
||||
to,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { path } = await logsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return correct structured url', async () => {
|
||||
const params: LogsLocatorParams = {
|
||||
logView: DEFAULT_LOG_VIEW,
|
||||
filter: FILTER_QUERY,
|
||||
time,
|
||||
};
|
||||
const { logsLocator } = await setupLogsLocator();
|
||||
const { app, path, state } = await logsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
|
||||
expect(app).toBe(APP_ID);
|
||||
expect(path).toBe(expected);
|
||||
expect(state).toBeDefined();
|
||||
expect(Object.keys(state)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Node Logs Locator', () => {
|
||||
it('should create a link to Node Logs with no state', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { app, path, state } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
expect(app).toBe(APP_ID);
|
||||
expect(path).toBe(constructUrlSearchString(params));
|
||||
expect(state).toBeDefined();
|
||||
expect(Object.keys(state)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should allow specifying specific logPosition', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { path } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific filter', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
filter: FILTER_QUERY,
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { path } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific view id', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
logView: { ...DEFAULT_LOG_VIEW, logViewId: 'test' },
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { path } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specifying specific time range', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
from,
|
||||
to,
|
||||
logView: DEFAULT_LOG_VIEW,
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { path } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(path).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return correct structured url', async () => {
|
||||
const params: NodeLogsLocatorParams = {
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
logView: DEFAULT_LOG_VIEW,
|
||||
filter: FILTER_QUERY,
|
||||
};
|
||||
const { nodeLogsLocator } = await setupLogsLocator();
|
||||
const { app, path, state } = await nodeLogsLocator.getLocation(params);
|
||||
|
||||
const expected = constructUrlSearchString(params);
|
||||
expect(app).toBe(APP_ID);
|
||||
expect(path).toBe(expected);
|
||||
expect(state).toBeDefined();
|
||||
expect(Object.keys(state)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
export const constructUrlSearchString = (params: Partial<NodeLogsLocatorParams>) => {
|
||||
const { time = 1550671089404, logView } = params;
|
||||
|
||||
return `/stream?logView=${constructLogView(logView)}&logPosition=${constructLogPosition(
|
||||
time
|
||||
)}&logFilter=${constructLogFilter(params)}`;
|
||||
};
|
||||
|
||||
const constructLogView = (logView?: LogViewReference) => {
|
||||
const logViewId =
|
||||
logView && 'logViewId' in logView ? logView.logViewId : DEFAULT_LOG_VIEW.logViewId;
|
||||
|
||||
return `(logViewId:${logViewId},type:log-view-reference)`;
|
||||
};
|
||||
|
||||
const constructLogPosition = (time: number = 1550671089404) => {
|
||||
return `(position:(tiebreaker:0,time:${time}))`;
|
||||
};
|
||||
|
||||
const constructLogFilter = ({
|
||||
nodeType,
|
||||
nodeId,
|
||||
filter,
|
||||
timeRange,
|
||||
time,
|
||||
}: Partial<NodeLogsLocatorParams>) => {
|
||||
let finalFilter = filter || '';
|
||||
|
||||
if (nodeId) {
|
||||
const nodeFilter = `${findInventoryFields(nodeType!).id}: ${nodeId}`;
|
||||
finalFilter = filter ? `(${nodeFilter}) and (${filter})` : nodeFilter;
|
||||
}
|
||||
|
||||
const query = encodeURI(
|
||||
`(query:(language:kuery,query:'${finalFilter}'),refreshInterval:(pause:!t,value:5000)`
|
||||
);
|
||||
|
||||
if (!time) return `${query})`;
|
||||
|
||||
const fromDate = timeRange?.startTime
|
||||
? addHoursToTimestamp(timeRange.startTime, 0)
|
||||
: addHoursToTimestamp(time, -1);
|
||||
|
||||
const toDate = timeRange?.endTime
|
||||
? addHoursToTimestamp(timeRange.endTime, 0)
|
||||
: addHoursToTimestamp(time, 1);
|
||||
|
||||
return `${query},timeRange:(from:'${fromDate}',to:'${toDate}'))`;
|
||||
};
|
||||
|
||||
const addHoursToTimestamp = (timestamp: number, hours: number): string => {
|
||||
return moment(timestamp).add({ hours }).toISOString();
|
||||
};
|
49
x-pack/plugins/infra/public/locators/logs_locator.ts
Normal file
49
x-pack/plugins/infra/public/locators/logs_locator.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LogViewReference } from '../../common/log_views';
|
||||
import type { TimeRange } from '../../common/time';
|
||||
import type { InfraClientCoreSetup } from '../types';
|
||||
|
||||
const LOGS_LOCATOR_ID = 'LOGS_LOCATOR';
|
||||
|
||||
export interface LogsLocatorParams extends SerializableRecord {
|
||||
/** Defines log position */
|
||||
time?: number;
|
||||
/**
|
||||
* Optionally set the time range in the time picker.
|
||||
*/
|
||||
timeRange?: TimeRange;
|
||||
filter?: string;
|
||||
logView?: LogViewReference;
|
||||
}
|
||||
|
||||
export type LogsLocator = LocatorPublic<LogsLocatorParams>;
|
||||
|
||||
export interface LogsLocatorDependencies {
|
||||
core: InfraClientCoreSetup;
|
||||
}
|
||||
|
||||
export class LogsLocatorDefinition implements LocatorDefinition<LogsLocatorParams> {
|
||||
public readonly id = LOGS_LOCATOR_ID;
|
||||
|
||||
constructor(protected readonly deps: LogsLocatorDependencies) {}
|
||||
|
||||
public readonly getLocation = async (params: LogsLocatorParams) => {
|
||||
const { createSearchString } = await import('./helpers');
|
||||
|
||||
const searchString = createSearchString(params);
|
||||
|
||||
return {
|
||||
app: 'logs',
|
||||
path: `/stream?${searchString}`,
|
||||
state: {},
|
||||
};
|
||||
};
|
||||
}
|
41
x-pack/plugins/infra/public/locators/node_logs_locator.ts
Normal file
41
x-pack/plugins/infra/public/locators/node_logs_locator.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { InventoryItemType } from '../../common/inventory_models/types';
|
||||
import type { LogsLocatorDependencies, LogsLocatorParams } from './logs_locator';
|
||||
|
||||
const NODE_LOGS_LOCATOR_ID = 'NODE_LOGS_LOCATOR';
|
||||
|
||||
export interface NodeLogsLocatorParams extends LogsLocatorParams {
|
||||
nodeId: string;
|
||||
nodeType: InventoryItemType;
|
||||
}
|
||||
|
||||
export type NodeLogsLocator = LocatorPublic<NodeLogsLocatorParams>;
|
||||
|
||||
export type NodeLogsLocatorDependencies = LogsLocatorDependencies;
|
||||
|
||||
export class NodeLogsLocatorDefinition implements LocatorDefinition<NodeLogsLocatorParams> {
|
||||
public readonly id = NODE_LOGS_LOCATOR_ID;
|
||||
|
||||
constructor(protected readonly deps: NodeLogsLocatorDependencies) {}
|
||||
|
||||
public readonly getLocation = async (params: NodeLogsLocatorParams) => {
|
||||
const { createNodeLogsQuery, createSearchString } = await import('./helpers');
|
||||
|
||||
const query = createNodeLogsQuery(params);
|
||||
|
||||
const searchString = createSearchString({ ...params, filter: query });
|
||||
|
||||
return {
|
||||
app: 'logs',
|
||||
path: `/stream?${searchString}`,
|
||||
state: {},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createLocatorMock } from './locators/locators.mock';
|
||||
import { createInventoryViewsServiceStartMock } from './services/inventory_views/inventory_views_service.mock';
|
||||
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
|
||||
import { createMetricsExplorerViewsServiceStartMock } from './services/metrics_explorer_views/metrics_explorer_views_service.mock';
|
||||
|
@ -17,6 +18,7 @@ export const createInfraPluginStartMock = () => ({
|
|||
logViews: createLogViewsServiceStartMock(),
|
||||
metricsExplorerViews: createMetricsExplorerViewsServiceStartMock(),
|
||||
telemetry: createTelemetryServiceMock(),
|
||||
locators: createLocatorMock(),
|
||||
ContainerMetricsTable: () => <div />,
|
||||
HostMetricsTable: () => <div />,
|
||||
PodMetricsTable: () => <div />,
|
||||
|
|
|
@ -101,8 +101,8 @@ const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
|
|||
};
|
||||
|
||||
// Used by linkTo components
|
||||
export const replaceLogPositionInQueryString = (time: number) =>
|
||||
Number.isNaN(time)
|
||||
export const replaceLogPositionInQueryString = (time?: number) =>
|
||||
Number.isNaN(time) || time == null
|
||||
? (value: string) => value
|
||||
: replaceStateKeyInQueryString<PositionStateInUrl>(defaultPositionStateKey, {
|
||||
position: {
|
||||
|
|
|
@ -15,7 +15,7 @@ import * as rt from 'io-ts';
|
|||
import { InvokeCreator } from 'xstate';
|
||||
import { DurationInputObject } from 'moment';
|
||||
import moment from 'moment';
|
||||
import { minimalTimeKeyRT } from '../../../../common/time';
|
||||
import { minimalTimeKeyRT, TimeRange } from '../../../../common/time';
|
||||
import { datemathStringRT } from '../../../utils/datemath';
|
||||
import { createPlainError, formatErrors } from '../../../../common/runtime_types';
|
||||
import { replaceStateKeyInQueryString } from '../../../utils/url_state';
|
||||
|
@ -290,21 +290,33 @@ const decodePositionQueryValueFromUrl = (queryValueFromUrl: unknown) => {
|
|||
return legacyPositionStateInUrlRT.decode(queryValueFromUrl);
|
||||
};
|
||||
|
||||
const ONE_HOUR = 3600000;
|
||||
export const replaceLogFilterInQueryString = (query: Query, time?: number) =>
|
||||
export const replaceLogFilterInQueryString = (query: Query, time?: number, timeRange?: TimeRange) =>
|
||||
replaceStateKeyInQueryString<FilterStateInUrl>(defaultFilterStateKey, {
|
||||
query,
|
||||
...(time && !Number.isNaN(time)
|
||||
? {
|
||||
timeRange: {
|
||||
from: new Date(time - ONE_HOUR).toISOString(),
|
||||
to: new Date(time + ONE_HOUR).toISOString(),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...getTimeRange(time, timeRange),
|
||||
refreshInterval: DEFAULT_REFRESH_INTERVAL,
|
||||
});
|
||||
|
||||
const getTimeRange = (time?: number, timeRange?: TimeRange) => {
|
||||
if (timeRange) {
|
||||
return {
|
||||
timeRange: {
|
||||
from: new Date(timeRange.startTime).toISOString(),
|
||||
to: new Date(timeRange.endTime).toISOString(),
|
||||
},
|
||||
};
|
||||
} else if (time) {
|
||||
return {
|
||||
timeRange: {
|
||||
from: getTimeRangeStartFromTime(time),
|
||||
to: getTimeRangeEndFromTime(time),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const defaultTimeRangeFromPositionOffset: DurationInputObject = { hours: 1 };
|
||||
|
||||
const getTimeRangeStartFromTime = (time: number): string =>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
const DEFAULT_LOG_VIEW_ID = 'default';
|
||||
export const DEFAULT_LOG_VIEW_ID = 'default';
|
||||
export const DEFAULT_LOG_VIEW = {
|
||||
type: 'log-view-reference' as const,
|
||||
logViewId: DEFAULT_LOG_VIEW_ID,
|
||||
|
|
|
@ -52,44 +52,53 @@ export const initializeFromUrl =
|
|||
}: LogViewUrlStateDependencies): InvokeCreator<LogViewContext, LogViewEvent> =>
|
||||
(_context, _event) =>
|
||||
(send) => {
|
||||
const logViewQueryValueFromUrl = urlStateStorage.get(logViewKey);
|
||||
const logViewQueryE = decodeLogViewQueryValueFromUrl(logViewQueryValueFromUrl);
|
||||
const logViewReference = getLogViewReferenceFromUrl({
|
||||
logViewKey,
|
||||
sourceIdKey,
|
||||
toastsService,
|
||||
urlStateStorage,
|
||||
});
|
||||
|
||||
const legacySourceIdQueryValueFromUrl = urlStateStorage.get(sourceIdKey);
|
||||
const sourceIdQueryE = decodeSourceIdQueryValueFromUrl(legacySourceIdQueryValueFromUrl);
|
||||
|
||||
if (Either.isLeft(logViewQueryE) || Either.isLeft(sourceIdQueryE)) {
|
||||
withNotifyOnErrors(toastsService).onGetError(
|
||||
createPlainError(
|
||||
formatErrors([
|
||||
...(Either.isLeft(logViewQueryE) ? logViewQueryE.left : []),
|
||||
...(Either.isLeft(sourceIdQueryE) ? sourceIdQueryE.left : []),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
send({
|
||||
type: 'INITIALIZED_FROM_URL',
|
||||
logViewReference: null,
|
||||
});
|
||||
} else {
|
||||
send({
|
||||
type: 'INITIALIZED_FROM_URL',
|
||||
logViewReference: pipe(
|
||||
// Via the legacy sourceId key
|
||||
pipe(
|
||||
sourceIdQueryE.right,
|
||||
Either.fromNullable(null),
|
||||
Either.map(convertSourceIdToReference)
|
||||
),
|
||||
// Via the logView key
|
||||
Either.alt(() => pipe(logViewQueryE.right, Either.fromNullable(null))),
|
||||
Either.fold(identity, identity)
|
||||
),
|
||||
});
|
||||
}
|
||||
send({
|
||||
type: 'INITIALIZED_FROM_URL',
|
||||
logViewReference,
|
||||
});
|
||||
};
|
||||
|
||||
export const getLogViewReferenceFromUrl = ({
|
||||
logViewKey,
|
||||
sourceIdKey,
|
||||
toastsService,
|
||||
urlStateStorage,
|
||||
}: LogViewUrlStateDependencies): LogViewReference | null => {
|
||||
const logViewQueryValueFromUrl = urlStateStorage.get(logViewKey!);
|
||||
const logViewQueryE = decodeLogViewQueryValueFromUrl(logViewQueryValueFromUrl);
|
||||
|
||||
const legacySourceIdQueryValueFromUrl = urlStateStorage.get(sourceIdKey!);
|
||||
const sourceIdQueryE = decodeSourceIdQueryValueFromUrl(legacySourceIdQueryValueFromUrl);
|
||||
|
||||
if (Either.isLeft(logViewQueryE) || Either.isLeft(sourceIdQueryE)) {
|
||||
withNotifyOnErrors(toastsService).onGetError(
|
||||
createPlainError(
|
||||
formatErrors([
|
||||
...(Either.isLeft(logViewQueryE) ? logViewQueryE.left : []),
|
||||
...(Either.isLeft(sourceIdQueryE) ? sourceIdQueryE.left : []),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return pipe(
|
||||
// Via the legacy sourceId key
|
||||
pipe(sourceIdQueryE.right, Either.fromNullable(null), Either.map(convertSourceIdToReference)),
|
||||
// Via the logView key
|
||||
Either.alt(() => pipe(logViewQueryE.right, Either.fromNullable(null))),
|
||||
Either.fold(identity, identity)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Certain navigations within the Logs solution will remove the logView URL key,
|
||||
// we want to ensure the logView key is present in the URL at all times by monitoring for it's removal.
|
||||
export const listenForUrlChanges =
|
||||
|
|
|
@ -1,338 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { Router, Switch } from 'react-router-dom';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useLogView } from '../../hooks/use_log_view';
|
||||
import {
|
||||
createLoadedUseLogViewMock,
|
||||
createLoadingUseLogViewMock,
|
||||
} from '../../hooks/use_log_view.mock';
|
||||
import { LinkToLogsPage } from './link_to_logs';
|
||||
|
||||
jest.mock('../../hooks/use_log_view');
|
||||
const useLogViewMock = useLogView as jest.MockedFunction<typeof useLogView>;
|
||||
const LOG_VIEW_REFERENCE = '(logViewId:default,type:log-view-reference)';
|
||||
const OTHER_LOG_VIEW_REFERENCE = '(logViewId:OTHER_SOURCE,type:log-view-reference)';
|
||||
|
||||
const renderRoutes = (routes: React.ReactElement) => {
|
||||
const history = createMemoryHistory();
|
||||
const services = {
|
||||
http: httpServiceMock.createStartContract(),
|
||||
logViews: {
|
||||
client: {},
|
||||
},
|
||||
observabilityShared: {
|
||||
navigation: {
|
||||
PageTemplate: KibanaPageTemplate,
|
||||
},
|
||||
},
|
||||
};
|
||||
const renderResult = render(
|
||||
<KibanaContextProvider services={services}>
|
||||
<Router history={history}>{routes}</Router>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
history,
|
||||
services,
|
||||
};
|
||||
};
|
||||
|
||||
describe('LinkToLogsPage component', () => {
|
||||
beforeEach(async () => {
|
||||
useLogViewMock.mockImplementation(await createLoadedUseLogViewMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
useLogViewMock.mockRestore();
|
||||
});
|
||||
|
||||
describe('default route', () => {
|
||||
it('redirects to the stream at a given time filtered for a user-defined criterion', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'FILTER_FIELD:FILTER_VALUE'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(position:(tiebreaker:0,time:1550671089404))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('redirects to the stream using a specific source id', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/OTHER_SOURCE');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(OTHER_LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs route', () => {
|
||||
it('redirects to the stream at a given time filtered for a user-defined criterion', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/logs?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'FILTER_FIELD:FILTER_VALUE'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(position:(tiebreaker:0,time:1550671089404))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('redirects to the stream using a specific source id', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/OTHER_SOURCE/logs');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(OTHER_LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host-logs route', () => {
|
||||
it('redirects to the stream filtered for a host', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/host-logs/HOST_NAME');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'host.name: HOST_NAME'),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
||||
it('redirects to the stream at a given time filtered for a host and a user-defined criterion', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push(
|
||||
'/link-to/host-logs/HOST_NAME?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'
|
||||
);
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'(host.name: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(position:(tiebreaker:0,time:1550671089404))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('redirects to the stream filtered for a host using a specific source id', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/OTHER_SOURCE/host-logs/HOST_NAME');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(OTHER_LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'host.name: HOST_NAME'),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
||||
it('renders a loading page while loading the source configuration', async () => {
|
||||
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
|
||||
|
||||
const { history, queryByTestId } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/host-logs/HOST_NAME');
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('container-logs route', () => {
|
||||
it('redirects to the stream filtered for a container', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/container-logs/CONTAINER_ID');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'container.id: CONTAINER_ID'),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
||||
it('redirects to the stream at a given time filtered for a container and a user-defined criterion', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push(
|
||||
'/link-to/container-logs/CONTAINER_ID?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'
|
||||
);
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'(container.id: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(position:(tiebreaker:0,time:1550671089404))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a loading page while loading the source configuration', () => {
|
||||
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
|
||||
|
||||
const { history, queryByTestId } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/container-logs/CONTAINER_ID');
|
||||
|
||||
expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pod-logs route', () => {
|
||||
it('redirects to the stream filtered for a pod', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/pod-logs/POD_UID');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'kubernetes.pod.uid: POD_UID'),refreshInterval:(pause:!t,value:5000))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
||||
it('redirects to the stream at a given time filtered for a pod and a user-defined criterion', () => {
|
||||
const { history } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/pod-logs/POD_UID?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE');
|
||||
|
||||
expect(history.location.pathname).toEqual('/stream');
|
||||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('logView')).toEqual(LOG_VIEW_REFERENCE);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(query:(language:kuery,query:'(kubernetes.pod.uid: POD_UID) and (FILTER_FIELD:FILTER_VALUE)'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(position:(tiebreaker:0,time:1550671089404))"`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a loading page while loading the source configuration', () => {
|
||||
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
|
||||
|
||||
const { history, queryByTestId } = renderRoutes(
|
||||
<Switch>
|
||||
<Route path="/link-to" component={LinkToLogsPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
history.push('/link-to/pod-logs/POD_UID');
|
||||
|
||||
expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,6 +22,11 @@ interface LinkToPageProps {
|
|||
|
||||
const ITEM_TYPES = inventoryModels.map((m) => m.id).join('|');
|
||||
|
||||
/**
|
||||
* @deprecated Link-to routes shouldn't be used anymore
|
||||
* Instead please use locators registered for the infra plugin
|
||||
* LogsLocator & NodeLogsLocator
|
||||
*/
|
||||
export const LinkToLogsPage: React.FC<LinkToPageProps> = (props) => {
|
||||
return (
|
||||
<Switch>
|
||||
|
|
|
@ -1,62 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createLocation } from 'history';
|
||||
import React from 'react';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RedirectToLogs } from './redirect_to_logs';
|
||||
|
||||
describe('RedirectToLogs component', () => {
|
||||
it('renders a redirect with the correct position', () => {
|
||||
const component = shallow(
|
||||
<RedirectToLogs {...createRouteComponentProps('/logs?time=1550671089404')} />
|
||||
);
|
||||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?logView=(logViewId:default,type:log-view-reference)&logPosition=(position:(tiebreaker:0,time:1550671089404))&logFilter=(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders a redirect with the correct user-defined filter', () => {
|
||||
const component = shallow(
|
||||
<RedirectToLogs
|
||||
{...createRouteComponentProps('/logs?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE')}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?logView=(logViewId:default,type:log-view-reference)&logPosition=(position:(tiebreaker:0,time:1550671089404))&logFilter=(query:(language:kuery,query:'FILTER_FIELD:FILTER_VALUE'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'2019-02-20T12:58:09.404Z',to:'2019-02-20T14:58:09.404Z'))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders a redirect with the correct custom source id', () => {
|
||||
const component = shallow(
|
||||
<RedirectToLogs {...createRouteComponentProps('/SOME-OTHER-SOURCE/logs')} />
|
||||
);
|
||||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?logView=(logViewId:default,type:log-view-reference)&logFilter=(query:(language:kuery,query:''),refreshInterval:(pause:!t,value:5000))"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
const createRouteComponentProps = (path: string) => {
|
||||
const location = createLocation(path);
|
||||
return {
|
||||
match: matchPath(location.pathname, { path: '/:sourceId?/logs' }) as any,
|
||||
history: null as any,
|
||||
location,
|
||||
};
|
||||
};
|
|
@ -5,31 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
import { flowRight } from 'lodash';
|
||||
import { replaceLogPositionInQueryString } from '../../observability_logs/log_stream_position_state/src/url_state_storage_service';
|
||||
import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
|
||||
import { replaceLogViewInQueryString } from '../../observability_logs/log_view_state';
|
||||
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
|
||||
import { DEFAULT_LOG_VIEW } from '../../observability_logs/log_view_state';
|
||||
|
||||
type RedirectToLogsType = RouteComponentProps<{}>;
|
||||
export const RedirectToLogs = () => {
|
||||
const { logViewId } = useParams<{ logViewId?: string }>();
|
||||
const location = useLocation();
|
||||
|
||||
interface RedirectToLogsProps extends RedirectToLogsType {
|
||||
match: RouteMatch<{
|
||||
logViewId?: string;
|
||||
}>;
|
||||
}
|
||||
const {
|
||||
services: { locators },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => {
|
||||
const logViewId = match.params.logViewId || 'default';
|
||||
const filter = getFilterFromLocation(location);
|
||||
const time = getTimeFromLocation(location);
|
||||
const searchString = flowRight(
|
||||
replaceLogFilterInQueryString({ language: 'kuery', query: filter }, time),
|
||||
replaceLogPositionInQueryString(time),
|
||||
replaceLogViewInQueryString({ type: 'log-view-reference', logViewId })
|
||||
)('');
|
||||
|
||||
return <Redirect to={`/stream?${searchString}`} />;
|
||||
useEffect(() => {
|
||||
locators.logsLocator.navigate(
|
||||
{
|
||||
time,
|
||||
filter,
|
||||
logView: { ...DEFAULT_LOG_VIEW, logViewId: logViewId || DEFAULT_LOG_VIEW.logViewId },
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [filter, locators.logsLocator, logViewId, time]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -5,21 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { LinkDescriptor } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import { flowRight } from 'lodash';
|
||||
import { findInventoryFields } from '../../../common/inventory_models';
|
||||
import { LinkDescriptor } from '@kbn/observability-plugin/public';
|
||||
import { useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { InventoryItemType } from '../../../common/inventory_models/types';
|
||||
import { LoadingPage } from '../../components/loading_page';
|
||||
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
|
||||
import { useLogView } from '../../hooks/use_log_view';
|
||||
import { replaceLogFilterInQueryString } from '../../observability_logs/log_stream_query_state';
|
||||
import { DEFAULT_LOG_VIEW_ID } from '../../observability_logs/log_view_state';
|
||||
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
|
||||
import { replaceLogPositionInQueryString } from '../../observability_logs/log_stream_position_state/src/url_state_storage_service';
|
||||
import { replaceLogViewInQueryString } from '../../observability_logs/log_view_state';
|
||||
|
||||
type RedirectToNodeLogsType = RouteComponentProps<{
|
||||
nodeId: string;
|
||||
|
@ -29,46 +21,31 @@ type RedirectToNodeLogsType = RouteComponentProps<{
|
|||
|
||||
export const RedirectToNodeLogs = ({
|
||||
match: {
|
||||
params: { nodeId, nodeType, logViewId = 'default' },
|
||||
params: { nodeId, nodeType, logViewId = DEFAULT_LOG_VIEW_ID },
|
||||
},
|
||||
location,
|
||||
}: RedirectToNodeLogsType) => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { isLoading, load } = useLogView({
|
||||
initialLogViewReference: { type: 'log-view-reference', logViewId },
|
||||
logViews: services.logViews.client,
|
||||
});
|
||||
const {
|
||||
services: { locators },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
useMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingPage
|
||||
data-test-subj={`nodeLoadingPage-${nodeType}`}
|
||||
message={i18n.translate('xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage', {
|
||||
defaultMessage: 'Loading {nodeType} logs',
|
||||
values: {
|
||||
nodeType,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`;
|
||||
const userFilter = getFilterFromLocation(location);
|
||||
const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;
|
||||
const filter = getFilterFromLocation(location);
|
||||
const time = getTimeFromLocation(location);
|
||||
|
||||
const searchString = flowRight(
|
||||
replaceLogFilterInQueryString({ language: 'kuery', query: filter }, time),
|
||||
replaceLogPositionInQueryString(time),
|
||||
replaceLogViewInQueryString({ type: 'log-view-reference', logViewId })
|
||||
)('');
|
||||
useEffect(() => {
|
||||
locators.nodeLogsLocator.navigate(
|
||||
{
|
||||
nodeId,
|
||||
nodeType,
|
||||
time,
|
||||
filter,
|
||||
logView: { type: 'log-view-reference', logViewId },
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [filter, locators.nodeLogsLocator, logViewId, nodeId, nodeType, time]);
|
||||
|
||||
return <Redirect to={`/stream?${searchString}`} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getNodeLogsUrl = ({
|
||||
|
|
|
@ -5,47 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { stringify } from 'querystring';
|
||||
import { encode } from '@kbn/rison';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
|
||||
|
||||
interface LogsLinkToStreamProps {
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const LogsLinkToStream = ({
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
query,
|
||||
}: LogsLinkToStreamProps) => {
|
||||
export const LogsLinkToStream = ({ startTime, endTime, query }: LogsLinkToStreamProps) => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { http } = services;
|
||||
|
||||
const queryString = new URLSearchParams(
|
||||
stringify({
|
||||
logPosition: encode({
|
||||
start: new Date(startTimestamp),
|
||||
end: new Date(endTimestamp),
|
||||
streamLive: false,
|
||||
}),
|
||||
logFilter: encode({
|
||||
kind: 'kuery',
|
||||
expression: query,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const viewInLogsUrl = http.basePath.prepend(`/app/logs/stream?${queryString}`);
|
||||
const { locators } = services;
|
||||
|
||||
return (
|
||||
<RedirectAppLinks coreStart={services}>
|
||||
<EuiButtonEmpty
|
||||
href={viewInLogsUrl}
|
||||
href={locators.logsLocator?.getRedirectUrl({
|
||||
time: endTime,
|
||||
timeRange: {
|
||||
startTime,
|
||||
endTime,
|
||||
},
|
||||
filter: query,
|
||||
})}
|
||||
data-test-subj="hostsView-logs-link-to-stream-button"
|
||||
iconType="popout"
|
||||
flush="both"
|
||||
|
|
|
@ -64,7 +64,7 @@ export const LogsTabContent = () => {
|
|||
<LogsSearchBar />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogsLinkToStream startTimestamp={from} endTimestamp={to} query={logsLinkToStreamQuery} />
|
||||
<LogsLinkToStream startTime={from} endTime={to} query={logsLinkToStreamQuery} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { enableInfrastructureHostsView } from '@kbn/observability-plugin/public'
|
|||
import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common';
|
||||
import { BehaviorSubject, combineLatest, from } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { DISCOVER_APP_TARGET, LOGS_APP_TARGET } from '../common/constants';
|
||||
import { defaultLogViewsStaticConfig } from '../common/log_views';
|
||||
import { InfraPublicConfig } from '../common/plugin_config_types';
|
||||
import { createInventoryMetricRuleType } from './alerting/inventory';
|
||||
|
@ -28,6 +29,13 @@ import { createLazyHostMetricsTable } from './components/infrastructure_node_met
|
|||
import { createLazyPodMetricsTable } from './components/infrastructure_node_metrics_tables/pod/create_lazy_pod_metrics_table';
|
||||
import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embeddable';
|
||||
import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory';
|
||||
import {
|
||||
DiscoverLogsLocatorDefinition,
|
||||
DiscoverNodeLogsLocatorDefinition,
|
||||
InfraLocators,
|
||||
LogsLocatorDefinition,
|
||||
NodeLogsLocatorDefinition,
|
||||
} from './locators';
|
||||
import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers';
|
||||
import { registerFeatures } from './register_feature';
|
||||
import { InventoryViewsService } from './services/inventory_views';
|
||||
|
@ -51,6 +59,8 @@ export class Plugin implements InfraClientPluginClass {
|
|||
private logViews: LogViewsService;
|
||||
private metricsExplorerViews: MetricsExplorerViewsService;
|
||||
private telemetry: TelemetryService;
|
||||
private locators?: InfraLocators;
|
||||
private appTarget: string;
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
|
||||
constructor(context: PluginInitializerContext<InfraPublicConfig>) {
|
||||
|
@ -62,6 +72,7 @@ export class Plugin implements InfraClientPluginClass {
|
|||
});
|
||||
this.metricsExplorerViews = new MetricsExplorerViewsService();
|
||||
this.telemetry = new TelemetryService();
|
||||
this.appTarget = this.config.logs.app_target;
|
||||
}
|
||||
|
||||
setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) {
|
||||
|
@ -148,7 +159,21 @@ export class Plugin implements InfraClientPluginClass {
|
|||
new LogStreamEmbeddableFactoryDefinition(core.getStartServices)
|
||||
);
|
||||
|
||||
if (this.config.logs.app_target === 'discover') {
|
||||
// Register Locators
|
||||
let logsLocator = pluginsSetup.share.url.locators.create(new LogsLocatorDefinition({ core }));
|
||||
let nodeLogsLocator = pluginsSetup.share.url.locators.create(
|
||||
new NodeLogsLocatorDefinition({ core })
|
||||
);
|
||||
|
||||
if (this.appTarget === DISCOVER_APP_TARGET) {
|
||||
// Register Locators
|
||||
logsLocator = pluginsSetup.share.url.locators.create(
|
||||
new DiscoverLogsLocatorDefinition({ core })
|
||||
);
|
||||
nodeLogsLocator = pluginsSetup.share.url.locators.create(
|
||||
new DiscoverNodeLogsLocatorDefinition({ core })
|
||||
);
|
||||
|
||||
core.application.register({
|
||||
id: 'logs-to-discover',
|
||||
title: '',
|
||||
|
@ -156,16 +181,15 @@ export class Plugin implements InfraClientPluginClass {
|
|||
appRoute: '/app/logs',
|
||||
mount: async (params: AppMountParameters) => {
|
||||
// mount callback should not use setup dependencies, get start dependencies instead
|
||||
const [coreStart, plugins, pluginStart] = await core.getStartServices();
|
||||
|
||||
const [coreStart, , pluginStart] = await core.getStartServices();
|
||||
const { renderApp } = await import('./apps/discover_app');
|
||||
|
||||
return renderApp(coreStart, plugins, pluginStart, params);
|
||||
return renderApp(coreStart, pluginStart, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.config.logs.app_target === 'logs-ui') {
|
||||
if (this.appTarget === LOGS_APP_TARGET) {
|
||||
core.application.register({
|
||||
id: 'logs',
|
||||
title: i18n.translate('xpack.infra.logs.pluginTitle', {
|
||||
|
@ -291,6 +315,15 @@ export class Plugin implements InfraClientPluginClass {
|
|||
|
||||
// Setup telemetry events
|
||||
this.telemetry.setup({ analytics: core.analytics });
|
||||
|
||||
this.locators = {
|
||||
logsLocator,
|
||||
nodeLogsLocator,
|
||||
};
|
||||
|
||||
return {
|
||||
locators: this.locators,
|
||||
};
|
||||
}
|
||||
|
||||
start(core: InfraClientCoreStart, plugins: InfraClientStartDeps) {
|
||||
|
@ -317,6 +350,7 @@ export class Plugin implements InfraClientPluginClass {
|
|||
logViews,
|
||||
metricsExplorerViews,
|
||||
telemetry,
|
||||
locators: this.locators!,
|
||||
ContainerMetricsTable: createLazyContainerMetricsTable(getStartServices),
|
||||
HostMetricsTable: createLazyHostMetricsTable(getStartServices),
|
||||
PodMetricsTable: createLazyPodMetricsTable(getStartServices),
|
||||
|
|
|
@ -48,15 +48,19 @@ import { InventoryViewsServiceStart } from './services/inventory_views';
|
|||
import { LogViewsServiceStart } from './services/log_views';
|
||||
import { MetricsExplorerViewsServiceStart } from './services/metrics_explorer_views';
|
||||
import { ITelemetryClient } from './services/telemetry';
|
||||
import { InfraLocators } from './locators';
|
||||
|
||||
// Our own setup and start contract values
|
||||
export type InfraClientSetupExports = void;
|
||||
export interface InfraClientSetupExports {
|
||||
locators: InfraLocators;
|
||||
}
|
||||
|
||||
export interface InfraClientStartExports {
|
||||
inventoryViews: InventoryViewsServiceStart;
|
||||
logViews: LogViewsServiceStart;
|
||||
metricsExplorerViews: MetricsExplorerViewsServiceStart;
|
||||
telemetry: ITelemetryClient;
|
||||
locators: InfraLocators;
|
||||
ContainerMetricsTable: (
|
||||
props: UseNodeMetricsTableOptions & Partial<SourceProviderProps>
|
||||
) => JSX.Element;
|
||||
|
|
|
@ -17,7 +17,12 @@ import { handleEsError } from '@kbn/es-ui-shared-plugin/server';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants';
|
||||
import {
|
||||
DISCOVER_APP_TARGET,
|
||||
LOGS_APP_TARGET,
|
||||
LOGS_FEATURE_ID,
|
||||
METRICS_FEATURE_ID,
|
||||
} from '../common/constants';
|
||||
import { defaultLogViewsStaticConfig } from '../common/log_views';
|
||||
import { publicConfigKeys } from '../common/plugin_config_types';
|
||||
import { configDeprecations, getInfraDeprecationsFactory } from './deprecations';
|
||||
|
@ -63,9 +68,12 @@ import { UsageCollector } from './usage/usage_collector';
|
|||
export const config: PluginConfigDescriptor<InfraConfig> = {
|
||||
schema: schema.object({
|
||||
logs: schema.object({
|
||||
app_target: schema.oneOf([schema.literal('logs-ui'), schema.literal('discover')], {
|
||||
defaultValue: 'logs-ui',
|
||||
}),
|
||||
app_target: schema.oneOf(
|
||||
[schema.literal(LOGS_APP_TARGET), schema.literal(DISCOVER_APP_TARGET)],
|
||||
{
|
||||
defaultValue: LOGS_APP_TARGET,
|
||||
}
|
||||
),
|
||||
}),
|
||||
alerting: schema.object({
|
||||
inventory_threshold: schema.object({
|
||||
|
|
|
@ -16977,7 +16977,6 @@
|
|||
"xpack.infra.nodeContextMenu.viewUptimeLink": "{inventoryName} en disponibilité",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeMore": "+{count} en plus",
|
||||
"xpack.infra.parseInterval.errorMessage": "{value} n'est pas une chaîne d'intervalle",
|
||||
"xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage": "Chargement de logs {nodeType}",
|
||||
"xpack.infra.snapshot.missingSnapshotMetricError": "L'agrégation de {metric} pour {nodeType} n'est pas disponible.",
|
||||
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "La valeur recommandée est {defaultValue}",
|
||||
"xpack.infra.sourceConfiguration.metricIndicesRecommendedValue": "La valeur recommandée est {defaultValue}",
|
||||
|
|
|
@ -16976,7 +16976,6 @@
|
|||
"xpack.infra.nodeContextMenu.viewUptimeLink": "アップタイムの{inventoryName}",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeMore": "+ 追加の{count}",
|
||||
"xpack.infra.parseInterval.errorMessage": "{value}は間隔文字列ではありません",
|
||||
"xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage": "{nodeType} ログを読み込み中",
|
||||
"xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType}の{metric}のアグリゲーションを利用できません。",
|
||||
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推奨値は {defaultValue} です",
|
||||
"xpack.infra.sourceConfiguration.metricIndicesRecommendedValue": "推奨値は {defaultValue} です",
|
||||
|
|
|
@ -16978,7 +16978,6 @@
|
|||
"xpack.infra.nodeContextMenu.viewUptimeLink": "Uptime 中的 {inventoryName}",
|
||||
"xpack.infra.nodeDetails.tabs.metadata.seeMore": "+ 另外 {count} 个",
|
||||
"xpack.infra.parseInterval.errorMessage": "{value} 不是时间间隔字符串",
|
||||
"xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage": "正在加载 {nodeType} 日志",
|
||||
"xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType} 的 {metric} 聚合不可用。",
|
||||
"xpack.infra.sourceConfiguration.logIndicesRecommendedValue": "推荐值为 {defaultValue}",
|
||||
"xpack.infra.sourceConfiguration.metricIndicesRecommendedValue": "推荐值为 {defaultValue}",
|
||||
|
|
|
@ -10,7 +10,8 @@ import { URL } from 'url';
|
|||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
const LOG_VIEW_REFERENCE = '(logViewId:default,type:log-view-reference)';
|
||||
const LOG_VIEW_ID = 'testView';
|
||||
const LOG_VIEW_REFERENCE = `(logViewId:${LOG_VIEW_ID},type:log-view-reference)`;
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const pageObjects = getPageObjects(['common']);
|
||||
|
@ -24,36 +25,73 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const traceId = '433b4651687e18be2c6c8e3b11f53d09';
|
||||
|
||||
describe('link-to Logs', function () {
|
||||
it('redirects to the logs app and parses URL search params correctly', async () => {
|
||||
const location = {
|
||||
hash: '',
|
||||
pathname: '/link-to',
|
||||
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
||||
state: undefined,
|
||||
};
|
||||
describe('Redirect to Logs', function () {
|
||||
it('redirects to the logs app and parses URL search params correctly', async () => {
|
||||
const location = {
|
||||
hash: '',
|
||||
pathname: `/link-to/${LOG_VIEW_ID}`,
|
||||
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
||||
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);
|
||||
const documentTitle = await browser.getTitle();
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'infraLogs',
|
||||
location.pathname,
|
||||
location.search,
|
||||
{
|
||||
ensureCurrentUrl: false,
|
||||
}
|
||||
);
|
||||
return await retry.tryForTime(5000, async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const parsedUrl = new URL(currentUrl);
|
||||
const documentTitle = await browser.getTitle();
|
||||
|
||||
expect(parsedUrl.pathname).to.be('/app/logs/stream');
|
||||
expect(parsedUrl.searchParams.get('logFilter')).to.be(
|
||||
`(query:(language:kuery,query:\'trace.id:${traceId}'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'${startDate}',to:'${endDate}'))`
|
||||
expect(parsedUrl.pathname).to.be('/app/logs/stream');
|
||||
expect(parsedUrl.searchParams.get('logFilter')).to.be(
|
||||
`(query:(language:kuery,query:\'trace.id:${traceId}'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'${startDate}',to:'${endDate}'))`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logPosition')).to.be(
|
||||
`(position:(tiebreaker:0,time:${timestamp}))`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logView')).to.be(LOG_VIEW_REFERENCE);
|
||||
expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Redirect to Node Logs', function () {
|
||||
it('redirects to the logs app and parses URL search params correctly', async () => {
|
||||
const nodeId = 1234;
|
||||
const location = {
|
||||
hash: '',
|
||||
pathname: `/link-to/${LOG_VIEW_ID}/pod-logs/${nodeId}`,
|
||||
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
||||
state: undefined,
|
||||
};
|
||||
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'infraLogs',
|
||||
location.pathname,
|
||||
location.search,
|
||||
{
|
||||
ensureCurrentUrl: false,
|
||||
}
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logPosition')).to.be(
|
||||
`(position:(tiebreaker:0,time:${timestamp}))`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logView')).to.be(LOG_VIEW_REFERENCE);
|
||||
expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic');
|
||||
await retry.tryForTime(5000, async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const parsedUrl = new URL(currentUrl);
|
||||
const documentTitle = await browser.getTitle();
|
||||
|
||||
expect(parsedUrl.pathname).to.be('/app/logs/stream');
|
||||
expect(parsedUrl.searchParams.get('logFilter')).to.be(
|
||||
`(query:(language:kuery,query:\'(kubernetes.pod.uid: 1234) and (trace.id:${traceId})\'),refreshInterval:(pause:!t,value:5000),timeRange:(from:'${startDate}',to:'${endDate}'))`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logPosition')).to.be(
|
||||
`(position:(tiebreaker:0,time:${timestamp}))`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logView')).to.be(LOG_VIEW_REFERENCE);
|
||||
expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue