[Log Explorer] Add logic to display highlights in Flyout (#170650)

## Summary

Closes https://github.com/elastic/kibana/issues/169504



![Highlights](06f21ac5-38e1-4521-843f-064c16ddd034)

## What's pending

- [ ] FTR Tests

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Achyut Jhunjhunwala 2023-11-10 19:37:34 +01:00 committed by GitHub
parent 02ccb7788a
commit c62df52c04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1457 additions and 55 deletions

View file

@ -89,7 +89,7 @@ pageLoadAssetSize:
licensing: 29004
links: 44490
lists: 22900
logExplorer: 39045
logExplorer: 54342
logsShared: 281060
logstash: 53548
management: 46112

View file

@ -13,6 +13,19 @@ export const HOST_NAME_FIELD = 'host.name';
export const LOG_LEVEL_FIELD = 'log.level';
export const MESSAGE_FIELD = 'message';
export const SERVICE_NAME_FIELD = 'service.name';
export const TRACE_ID_FIELD = 'trace.id';
export const AGENT_NAME_FIELD = 'agent.name';
export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name';
export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id';
export const CLOUD_PROVIDER_FIELD = 'cloud.provider';
export const CLOUD_REGION_FIELD = 'cloud.region';
export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone';
export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id';
export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id';
export const LOG_FILE_PATH_FIELD = 'log.file.path';
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset';
// Sizing
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;

View file

@ -6,42 +6,22 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { LogLevel } from './sub_components/log_level';
import { Timestamp } from './sub_components/timestamp';
import { FlyoutProps, LogDocument } from './types';
import { getDocDetailRenderFlags, useDocDetail } from './use_doc_detail';
import { Message } from './sub_components/message';
import { useDocDetail } from './use_doc_detail';
import { FlyoutHeader } from './flyout_header';
import { FlyoutHighlights } from './flyout_highlights';
export function FlyoutDetail({ dataView, doc }: Pick<FlyoutProps, 'dataView' | 'doc' | 'actions'>) {
export function FlyoutDetail({
dataView,
doc,
actions,
}: Pick<FlyoutProps, 'dataView' | 'doc' | 'actions'>) {
const parsedDoc = useDocDetail(doc as LogDocument, { dataView });
const { hasTimestamp, hasLogLevel, hasMessage, hasBadges, hasFlyoutHeader } =
getDocDetailRenderFlags(parsedDoc);
return hasFlyoutHeader ? (
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="logExplorerFlyoutDetail">
<EuiFlexItem grow={false}>
{hasBadges && (
<EuiFlexGroup responsive={false} gutterSize="m">
{hasLogLevel && (
<EuiFlexItem grow={false}>
<LogLevel level={parsedDoc['log.level']} />
</EuiFlexItem>
)}
{hasTimestamp && (
<EuiFlexItem grow={false}>
<Timestamp timestamp={parsedDoc['@timestamp']} />
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiFlexItem>
{hasMessage && (
<EuiFlexItem grow={false}>
<Message message={parsedDoc.message} />
</EuiFlexItem>
)}
</EuiFlexGroup>
) : null;
return (
<>
<FlyoutHeader doc={parsedDoc} />
<FlyoutHighlights formattedDoc={parsedDoc} flattenedDoc={doc.flattened} actions={actions} />
</>
);
}

View file

@ -0,0 +1,46 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FlyoutDoc } from './types';
import { getDocDetailHeaderRenderFlags } from './use_doc_detail';
import { LogLevel } from './sub_components/log_level';
import { Timestamp } from './sub_components/timestamp';
import { Message } from './sub_components/message';
import * as constants from '../../../common/constants';
export function FlyoutHeader({ doc }: { doc: FlyoutDoc }) {
const { hasTimestamp, hasLogLevel, hasMessage, hasBadges, hasFlyoutHeader } =
getDocDetailHeaderRenderFlags(doc);
return hasFlyoutHeader ? (
<EuiFlexGroup direction="column" gutterSize="m" data-test-subj="logExplorerFlyoutDetail">
<EuiFlexItem grow={false}>
{hasBadges && (
<EuiFlexGroup responsive={false} gutterSize="m">
{hasLogLevel && (
<EuiFlexItem grow={false}>
<LogLevel level={doc[constants.LOG_LEVEL_FIELD]} />
</EuiFlexItem>
)}
{hasTimestamp && (
<EuiFlexItem grow={false}>
<Timestamp timestamp={doc[constants.TIMESTAMP_FIELD]} />
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiFlexItem>
{hasMessage && (
<EuiFlexItem grow={false}>
<Message message={doc[constants.MESSAGE_FIELD]} />
</EuiFlexItem>
)}
</EuiFlexGroup>
) : null;
}

View file

@ -0,0 +1,207 @@
/*
* 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 React from 'react';
import { FlyoutContentActions } from '@kbn/discover-plugin/public';
import { DataTableRecord } from '@kbn/discover-utils/src/types';
import { useMeasure } from 'react-use/lib';
import { FlyoutDoc } from './types';
import * as constants from '../../../common/constants';
import { HighlightField } from './sub_components/highlight_field';
import {
cloudAccordionTitle,
flyoutCloudAvailabilityZoneLabel,
flyoutCloudInstanceIdLabel,
flyoutCloudProjectIdLabel,
flyoutCloudProviderLabel,
flyoutCloudRegionLabel,
flyoutDatasetLabel,
flyoutHostNameLabel,
flyoutLogPathFileLabel,
flyoutNamespaceLabel,
flyoutOrchestratorClusterNameLabel,
flyoutOrchestratorResourceIdLabel,
flyoutServiceLabel,
flyoutShipperLabel,
flyoutTraceLabel,
infraAccordionTitle,
otherAccordionTitle,
serviceAccordionTitle,
} from './translations';
import { HighlightSection } from './sub_components/highlight_section';
import { DiscoverActionsProvider } from '../../hooks/use_discover_action';
import { HighlightContainer } from './sub_components/highlight_container';
import { useFlyoutColumnWidth } from '../../hooks/use_flyouot_column_width';
export function FlyoutHighlights({
formattedDoc,
flattenedDoc,
actions,
}: {
formattedDoc: FlyoutDoc;
flattenedDoc: DataTableRecord['flattened'];
actions: FlyoutContentActions;
}) {
const [ref, dimensions] = useMeasure<HTMLDivElement>();
const { columns, fieldWidth } = useFlyoutColumnWidth(dimensions.width);
return (
<DiscoverActionsProvider value={actions}>
<HighlightContainer ref={ref}>
<HighlightSection title={serviceAccordionTitle} columns={columns}>
{formattedDoc[constants.SERVICE_NAME_FIELD] && (
<HighlightField
label={flyoutServiceLabel}
field={constants.SERVICE_NAME_FIELD}
value={flattenedDoc[constants.SERVICE_NAME_FIELD]}
formattedValue={formattedDoc[constants.SERVICE_NAME_FIELD]}
dataTestSubj="logExplorerFlyoutService"
width={fieldWidth}
/>
)}
{formattedDoc[constants.TRACE_ID_FIELD] && (
<HighlightField
label={flyoutTraceLabel}
field={constants.TRACE_ID_FIELD}
value={flattenedDoc[constants.TRACE_ID_FIELD]}
formattedValue={formattedDoc[constants.TRACE_ID_FIELD]}
dataTestSubj="logExplorerFlyoutTrace"
width={fieldWidth}
/>
)}
</HighlightSection>
<HighlightSection title={infraAccordionTitle} columns={columns}>
{formattedDoc[constants.HOST_NAME_FIELD] && (
<HighlightField
label={flyoutHostNameLabel}
field={constants.HOST_NAME_FIELD}
value={flattenedDoc[constants.HOST_NAME_FIELD]}
formattedValue={formattedDoc[constants.HOST_NAME_FIELD]}
dataTestSubj="logExplorerFlyoutHostName"
width={fieldWidth}
/>
)}
{formattedDoc[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD] && (
<HighlightField
label={flyoutOrchestratorClusterNameLabel}
field={constants.ORCHESTRATOR_CLUSTER_NAME_FIELD}
value={flattenedDoc[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]}
formattedValue={formattedDoc[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]}
dataTestSubj="logExplorerFlyoutClusterName"
width={fieldWidth}
/>
)}
{formattedDoc[constants.ORCHESTRATOR_RESOURCE_ID_FIELD] && (
<HighlightField
label={flyoutOrchestratorResourceIdLabel}
field={constants.ORCHESTRATOR_RESOURCE_ID_FIELD}
value={flattenedDoc[constants.ORCHESTRATOR_RESOURCE_ID_FIELD]}
formattedValue={formattedDoc[constants.ORCHESTRATOR_RESOURCE_ID_FIELD]}
dataTestSubj="logExplorerFlyoutResourceId"
width={fieldWidth}
/>
)}
</HighlightSection>
<HighlightSection title={cloudAccordionTitle} columns={columns}>
{formattedDoc[constants.CLOUD_PROVIDER_FIELD] && (
<HighlightField
label={flyoutCloudProviderLabel}
field={constants.CLOUD_PROVIDER_FIELD}
value={flattenedDoc[constants.CLOUD_PROVIDER_FIELD]}
formattedValue={formattedDoc[constants.CLOUD_PROVIDER_FIELD]}
dataTestSubj="logExplorerFlyoutCloudProvider"
width={fieldWidth}
/>
)}
{formattedDoc[constants.CLOUD_REGION_FIELD] && (
<HighlightField
label={flyoutCloudRegionLabel}
field={constants.CLOUD_REGION_FIELD}
value={flattenedDoc[constants.CLOUD_REGION_FIELD]}
formattedValue={formattedDoc[constants.CLOUD_REGION_FIELD]}
dataTestSubj="logExplorerFlyoutCloudRegion"
width={fieldWidth}
/>
)}
{formattedDoc[constants.CLOUD_AVAILABILITY_ZONE_FIELD] && (
<HighlightField
label={flyoutCloudAvailabilityZoneLabel}
field={constants.CLOUD_AVAILABILITY_ZONE_FIELD}
value={flattenedDoc[constants.CLOUD_AVAILABILITY_ZONE_FIELD]}
formattedValue={formattedDoc[constants.CLOUD_AVAILABILITY_ZONE_FIELD]}
dataTestSubj="logExplorerFlyoutCloudAz"
width={fieldWidth}
/>
)}
{formattedDoc[constants.CLOUD_PROJECT_ID_FIELD] && (
<HighlightField
label={flyoutCloudProjectIdLabel}
field={constants.CLOUD_PROJECT_ID_FIELD}
value={flattenedDoc[constants.CLOUD_PROJECT_ID_FIELD]}
formattedValue={formattedDoc[constants.CLOUD_PROJECT_ID_FIELD]}
dataTestSubj="logExplorerFlyoutCloudProjectId"
width={fieldWidth}
/>
)}
{formattedDoc[constants.CLOUD_INSTANCE_ID_FIELD] && (
<HighlightField
label={flyoutCloudInstanceIdLabel}
field={constants.CLOUD_INSTANCE_ID_FIELD}
value={flattenedDoc[constants.CLOUD_INSTANCE_ID_FIELD]}
formattedValue={formattedDoc[constants.CLOUD_INSTANCE_ID_FIELD]}
dataTestSubj="logExplorerFlyoutCloudInstanceId"
width={fieldWidth}
/>
)}
</HighlightSection>
<HighlightSection title={otherAccordionTitle} showBottomRule={false} columns={columns}>
{formattedDoc[constants.LOG_FILE_PATH_FIELD] && (
<HighlightField
label={flyoutLogPathFileLabel}
field={constants.LOG_FILE_PATH_FIELD}
value={flattenedDoc[constants.LOG_FILE_PATH_FIELD]}
formattedValue={formattedDoc[constants.LOG_FILE_PATH_FIELD]}
dataTestSubj="logExplorerFlyoutLogPathFile"
width={fieldWidth}
/>
)}
{formattedDoc[constants.DATASTREAM_NAMESPACE_FIELD] && (
<HighlightField
label={flyoutNamespaceLabel}
field={constants.DATASTREAM_NAMESPACE_FIELD}
value={flattenedDoc[constants.DATASTREAM_NAMESPACE_FIELD]}
formattedValue={formattedDoc[constants.DATASTREAM_NAMESPACE_FIELD]}
dataTestSubj="logExplorerFlyoutNamespace"
width={fieldWidth}
/>
)}
{formattedDoc[constants.DATASTREAM_DATASET_FIELD] && (
<HighlightField
label={flyoutDatasetLabel}
field={constants.DATASTREAM_DATASET_FIELD}
value={flattenedDoc[constants.DATASTREAM_DATASET_FIELD]}
formattedValue={formattedDoc[constants.DATASTREAM_DATASET_FIELD]}
dataTestSubj="logExplorerFlyoutDataset"
width={fieldWidth}
/>
)}
{formattedDoc[constants.AGENT_NAME_FIELD] && (
<HighlightField
label={flyoutShipperLabel}
field={constants.AGENT_NAME_FIELD}
value={flattenedDoc[constants.AGENT_NAME_FIELD]}
formattedValue={formattedDoc[constants.AGENT_NAME_FIELD]}
dataTestSubj="logExplorerFlyoutLogShipper"
width={fieldWidth}
/>
)}
</HighlightSection>
</HighlightContainer>
</DiscoverActionsProvider>
);
}

View file

@ -0,0 +1,42 @@
/*
* 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 React from 'react';
import { EuiHorizontalRule, EuiPanel } from '@elastic/eui';
interface HighlightContainerProps {
children: React.ReactNode;
}
const hasNonUndefinedSubChild = (children: React.ReactNode[]): boolean => {
return children.some((child) => {
if (React.isValidElement(child)) {
const subChildren = React.Children.toArray(child.props.children);
return subChildren.some((subChild) => subChild !== undefined && subChild !== null);
}
return false;
});
};
export const HighlightContainer = React.forwardRef<HTMLDivElement, HighlightContainerProps>(
({ children }, ref) => {
const validChildren = React.Children.toArray(children).filter(Boolean);
const hasChildren = validChildren.length > 0;
const shouldRender = hasChildren && hasNonUndefinedSubChild(validChildren);
const flexChildren = validChildren.map((child, idx) => <div key={idx}>{child}</div>);
return shouldRender ? (
<div ref={ref}>
<EuiHorizontalRule margin="xs" />
<EuiPanel paddingSize="m" hasShadow={false} hasBorder={true}>
{flexChildren}
</EuiPanel>
</div>
) : null;
}
);

View file

@ -0,0 +1,103 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, copyToClipboard } from '@elastic/eui';
import React, { ReactNode, useMemo, useState } from 'react';
import { HoverAction, HoverActionType } from './hover_action';
import {
flyoutHoverActionFilterForText,
flyoutHoverActionFilterOutText,
flyoutHoverActionFilterForFieldPresentText,
flyoutHoverActionToggleColumnText,
flyoutHoverActionCopyToClipboardText,
} from '../translations';
import { useDiscoverActionsContext } from '../../../hooks/use_discover_action';
interface HighlightFieldProps {
label: string | ReactNode;
field: string;
value: unknown;
formattedValue: string;
dataTestSubj: string;
width: number;
}
export function HighlightField({
label,
field,
value,
formattedValue,
dataTestSubj,
width,
}: HighlightFieldProps) {
const filterForText = flyoutHoverActionFilterForText(value);
const filterOutText = flyoutHoverActionFilterOutText(value);
const actions = useDiscoverActionsContext();
const [columnAdded, setColumnAdded] = useState(false);
const hoverActions: HoverActionType[] = useMemo(
() => [
{
id: 'addToFilterAction',
tooltipContent: filterForText,
iconType: 'plusInCircle',
onClick: () => actions?.addFilter && actions.addFilter(field, value, '+'),
display: true,
},
{
id: 'removeFromFilterAction',
tooltipContent: filterOutText,
iconType: 'minusInCircle',
onClick: () => actions?.addFilter && actions.addFilter(field, value, '-'),
display: true,
},
{
id: 'filterForFieldPresentAction',
tooltipContent: flyoutHoverActionFilterForFieldPresentText,
iconType: 'filter',
onClick: () => actions?.addFilter && actions.addFilter('_exists_', field, '+'),
display: true,
},
{
id: 'toggleColumnAction',
tooltipContent: flyoutHoverActionToggleColumnText,
iconType: 'listAdd',
onClick: () => {
if (actions) {
if (columnAdded) {
actions?.removeColumn?.(field);
} else {
actions?.addColumn?.(field);
}
setColumnAdded(!columnAdded);
}
},
display: true,
},
{
id: 'copyToClipboardAction',
tooltipContent: flyoutHoverActionCopyToClipboardText,
iconType: 'copyClipboard',
onClick: () => copyToClipboard(value as string),
display: true,
},
],
[filterForText, filterOutText, actions, field, value, columnAdded]
);
return formattedValue ? (
<EuiFlexGroup direction="column" gutterSize="none" data-test-subj={dataTestSubj}>
<EuiFlexItem>
<EuiText color="subdued" size="xs">
{label}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<HoverAction displayText={formattedValue} actions={hoverActions} width={width} />
</EuiFlexItem>
</EuiFlexGroup>
) : null;
}

View file

@ -0,0 +1,61 @@
/*
* 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 React from 'react';
import {
EuiAccordion,
EuiFlexGrid,
EuiHorizontalRule,
EuiTitle,
EuiFlexItem,
useGeneratedHtmlId,
} from '@elastic/eui';
interface HighlightSectionProps {
title: string;
children: React.ReactNode;
showBottomRule?: boolean;
columns: 1 | 2 | 3;
}
export function HighlightSection({
title,
children,
showBottomRule = true,
columns,
}: HighlightSectionProps) {
const validChildren = React.Children.toArray(children).filter(Boolean);
const shouldRenderSection = validChildren.length > 0;
const accordionTitle = (
<EuiTitle size="xs">
<p>{title}</p>
</EuiTitle>
);
const flexChildren = validChildren.map((child, idx) => (
<EuiFlexItem key={idx}>{child}</EuiFlexItem>
));
const accordionId = useGeneratedHtmlId({
prefix: title,
});
return shouldRenderSection ? (
<>
<EuiAccordion
id={accordionId}
buttonContent={accordionTitle}
paddingSize="s"
initialIsOpen={true}
data-test-subj={`logExplorerFlyoutHighlightSection${title}`}
>
<EuiFlexGrid columns={columns}>{flexChildren}</EuiFlexGrid>
</EuiAccordion>
{showBottomRule && <EuiHorizontalRule margin="xs" />}
</>
) : null;
}

View file

@ -0,0 +1,86 @@
/*
* 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 React from 'react';
import {
EuiFlexGroup,
EuiToolTip,
EuiButtonIcon,
useEuiTheme,
EuiTextTruncate,
EuiText,
} from '@elastic/eui';
import type { IconType } from '@elastic/eui';
export interface HoverActionType {
id: string;
tooltipContent: string;
iconType: IconType;
onClick: () => void;
display: boolean;
}
interface HoverActionProps {
displayText: string;
actions: HoverActionType[];
width: number;
}
export const HoverAction = ({ displayText, actions, width }: HoverActionProps) => {
const { euiTheme } = useEuiTheme();
return (
<EuiFlexGroup
responsive={false}
alignItems="center"
justifyContent="flexStart"
gutterSize="s"
css={{
':hover, :focus-within': {
'.visibleOnHoverFocus': {
opacity: 1,
visibility: 'visible',
},
},
}}
>
<EuiTextTruncate text={displayText} truncation="end" width={width}>
{(truncatedText: string) => (
<EuiText
// Value returned from formatFieldValue is always sanitized
dangerouslySetInnerHTML={{ __html: truncatedText }}
/>
)}
</EuiTextTruncate>
<EuiFlexGroup
className="visibleOnHoverFocus"
css={{
flexGrow: 0,
flexShrink: 0,
opacity: 0,
visibility: 'hidden',
transition: `opacity ${euiTheme.animation.normal} ${euiTheme.animation.resistance}, visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance}`,
}}
responsive={false}
alignItems="center"
gutterSize="none"
>
{actions.map((action) => (
<EuiToolTip content={action.tooltipContent} key={action.id}>
<EuiButtonIcon
size="xs"
iconType={action.iconType}
aria-label={action.tooltipContent as string}
onClick={() => action.onClick()}
/>
</EuiToolTip>
))}
</EuiFlexGroup>
</EuiFlexGroup>
);
};

View file

@ -10,3 +10,151 @@ import { i18n } from '@kbn/i18n';
export const flyoutMessageLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.message', {
defaultMessage: 'Message',
});
export const flyoutServiceLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.service', {
defaultMessage: 'Service',
});
export const flyoutTraceLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.trace', {
defaultMessage: 'Trace',
});
export const flyoutHostNameLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.hostName', {
defaultMessage: 'Host name',
});
export const serviceAccordionTitle = i18n.translate(
'xpack.logExplorer.flyoutDetail.accordion.title.service',
{
defaultMessage: 'Service',
}
);
export const infraAccordionTitle = i18n.translate(
'xpack.logExplorer.flyoutDetail.accordion.title.infrastructure',
{
defaultMessage: 'Infrastructure',
}
);
export const cloudAccordionTitle = i18n.translate(
'xpack.logExplorer.flyoutDetail.accordion.title.cloud',
{
defaultMessage: 'Cloud',
}
);
export const otherAccordionTitle = i18n.translate(
'xpack.logExplorer.flyoutDetail.accordion.title.other',
{
defaultMessage: 'Other',
}
);
export const flyoutOrchestratorClusterNameLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.orchestratorClusterName',
{
defaultMessage: 'Orchestrator cluster Name',
}
);
export const flyoutOrchestratorResourceIdLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.orchestratorResourceId',
{
defaultMessage: 'Orchestrator resource ID',
}
);
export const flyoutCloudProviderLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.cloudProvider',
{
defaultMessage: 'Cloud provider',
}
);
export const flyoutCloudRegionLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.cloudRegion',
{
defaultMessage: 'Cloud region',
}
);
export const flyoutCloudAvailabilityZoneLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.cloudAvailabilityZone',
{
defaultMessage: 'Cloud availability zone',
}
);
export const flyoutCloudProjectIdLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.cloudProjectId',
{
defaultMessage: 'Cloud project ID',
}
);
export const flyoutCloudInstanceIdLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.cloudInstanceId',
{
defaultMessage: 'Cloud instance ID',
}
);
export const flyoutLogPathFileLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.logPathFile',
{
defaultMessage: 'Log path file',
}
);
export const flyoutNamespaceLabel = i18n.translate(
'xpack.logExplorer.flyoutDetail.label.namespace',
{
defaultMessage: 'Namespace',
}
);
export const flyoutDatasetLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.dataset', {
defaultMessage: 'Dataset',
});
export const flyoutShipperLabel = i18n.translate('xpack.logExplorer.flyoutDetail.label.shipper', {
defaultMessage: 'Shipper',
});
export const flyoutHoverActionFilterForText = (text: unknown) =>
i18n.translate('xpack.logExplorer.flyoutDetail.value.hover.filterFor', {
defaultMessage: 'Filter for this {value}',
values: {
value: text as string,
},
});
export const flyoutHoverActionFilterOutText = (text: unknown) =>
i18n.translate('xpack.logExplorer.flyoutDetail.value.hover.filterOut', {
defaultMessage: 'Filter out this {value}',
values: {
value: text as string,
},
});
export const flyoutHoverActionFilterForFieldPresentText = i18n.translate(
'xpack.logExplorer.flyoutDetail.value.hover.filterForFieldPresent',
{
defaultMessage: 'Filter for field present',
}
);
export const flyoutHoverActionToggleColumnText = i18n.translate(
'xpack.logExplorer.flyoutDetail.value.hover.toggleColumn',
{
defaultMessage: 'Toggle column in table',
}
);
export const flyoutHoverActionCopyToClipboardText = i18n.translate(
'xpack.logExplorer.flyoutDetail.value.hover.copyToClipboard',
{
defaultMessage: 'Copy to clipboard',
}
);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import type { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { FlyoutContentProps } from '@kbn/discover-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils/types';
@ -19,6 +18,21 @@ export interface LogDocument extends DataTableRecord {
'@timestamp': string;
'log.level'?: string;
message?: string;
'host.name'?: string;
'service.name'?: string;
'trace.id'?: string;
'agent.name'?: string;
'orchestrator.cluster.name'?: string;
'orchestrator.resource.id'?: string;
'cloud.provider'?: string;
'cloud.region'?: string;
'cloud.availability_zone'?: string;
'cloud.project.id'?: string;
'cloud.instance.id'?: string;
'log.file.path'?: string;
'data_stream.namespace': string;
'data_stream.dataset': string;
};
}
@ -26,10 +40,19 @@ export interface FlyoutDoc {
'@timestamp': string;
'log.level'?: string;
message?: string;
}
export interface FlyoutHighlightField {
label: string;
value: string;
iconType?: EuiIconType;
'host.name'?: string;
'service.name'?: string;
'trace.id'?: string;
'agent.name'?: string;
'orchestrator.cluster.name'?: string;
'orchestrator.resource.id'?: string;
'cloud.provider'?: string;
'cloud.region'?: string;
'cloud.availability_zone'?: string;
'cloud.project.id'?: string;
'cloud.instance.id'?: string;
'log.file.path'?: string;
'data_stream.namespace': string;
'data_stream.dataset': string;
}

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { formatFieldValue } from '@kbn/discover-utils';
import { LOG_LEVEL_FIELD, MESSAGE_FIELD, TIMESTAMP_FIELD } from '../../../common/constants';
import he from 'he';
import * as constants from '../../../common/constants';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
import { FlyoutDoc, FlyoutProps, LogDocument } from './types';
@ -30,21 +31,59 @@ export function useDocDetail(
);
};
const level = formatField(LOG_LEVEL_FIELD)?.toLowerCase();
const timestamp = formatField(TIMESTAMP_FIELD);
const message = formatField(MESSAGE_FIELD);
// Flyout Headers
const level = formatField(constants.LOG_LEVEL_FIELD)?.toLowerCase();
const timestamp = formatField(constants.TIMESTAMP_FIELD);
const formattedMessage = formatField(constants.MESSAGE_FIELD);
const message = formattedMessage ? he.decode(formattedMessage) : undefined;
// Service Highlights
const serviceName = formatField(constants.SERVICE_NAME_FIELD);
const traceId = formatField(constants.TRACE_ID_FIELD);
// Infrastructure Highlights
const hostname = formatField(constants.HOST_NAME_FIELD);
const orchestratorClusterName = formatField(constants.ORCHESTRATOR_CLUSTER_NAME_FIELD);
const orchestratorResourceId = formatField(constants.ORCHESTRATOR_RESOURCE_ID_FIELD);
// Cloud Highlights
const cloudProvider = formatField(constants.CLOUD_PROVIDER_FIELD);
const cloudRegion = formatField(constants.CLOUD_REGION_FIELD);
const cloudAz = formatField(constants.CLOUD_AVAILABILITY_ZONE_FIELD);
const cloudProjectId = formatField(constants.CLOUD_PROJECT_ID_FIELD);
const cloudInstanceId = formatField(constants.CLOUD_INSTANCE_ID_FIELD);
// Other Highlights
const logFilePath = formatField(constants.LOG_FILE_PATH_FIELD);
const namespace = formatField(constants.DATASTREAM_NAMESPACE_FIELD);
const dataset = formatField(constants.DATASTREAM_DATASET_FIELD);
const agentName = formatField(constants.AGENT_NAME_FIELD);
return {
[LOG_LEVEL_FIELD]: level,
[TIMESTAMP_FIELD]: timestamp,
[MESSAGE_FIELD]: message,
[constants.LOG_LEVEL_FIELD]: level,
[constants.TIMESTAMP_FIELD]: timestamp,
[constants.MESSAGE_FIELD]: message,
[constants.SERVICE_NAME_FIELD]: serviceName,
[constants.TRACE_ID_FIELD]: traceId,
[constants.HOST_NAME_FIELD]: hostname,
[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName,
[constants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId,
[constants.CLOUD_PROVIDER_FIELD]: cloudProvider,
[constants.CLOUD_REGION_FIELD]: cloudRegion,
[constants.CLOUD_AVAILABILITY_ZONE_FIELD]: cloudAz,
[constants.CLOUD_PROJECT_ID_FIELD]: cloudProjectId,
[constants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId,
[constants.LOG_FILE_PATH_FIELD]: logFilePath,
[constants.DATASTREAM_NAMESPACE_FIELD]: namespace,
[constants.DATASTREAM_DATASET_FIELD]: dataset,
[constants.AGENT_NAME_FIELD]: agentName,
};
}
export const getDocDetailRenderFlags = (doc: FlyoutDoc) => {
const hasTimestamp = Boolean(doc['@timestamp']);
const hasLogLevel = Boolean(doc['log.level']);
const hasMessage = Boolean(doc.message);
export const getDocDetailHeaderRenderFlags = (doc: FlyoutDoc) => {
const hasTimestamp = Boolean(doc[constants.TIMESTAMP_FIELD]);
const hasLogLevel = Boolean(doc[constants.LOG_LEVEL_FIELD]);
const hasMessage = Boolean(doc[constants.MESSAGE_FIELD]);
const hasBadges = hasTimestamp || hasLogLevel;

View file

@ -0,0 +1,17 @@
/*
* 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 createContainer from 'constate';
import { FlyoutContentActions } from '@kbn/discover-plugin/public';
interface UseFlyoutActionsDeps {
value: FlyoutContentActions;
}
const useDiscoverActions = ({ value }: UseFlyoutActionsDeps) => value;
export const [DiscoverActionsProvider, useDiscoverActionsContext] =
createContainer(useDiscoverActions);

View file

@ -0,0 +1,26 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
interface FlyoutColumnWidth {
columns: 1 | 2 | 3;
fieldWidth: number;
}
export const useFlyoutColumnWidth = (width: number): FlyoutColumnWidth => {
const { euiTheme } = useEuiTheme();
const numberOfColumns = width > euiTheme.breakpoint.m ? 3 : width > euiTheme.breakpoint.s ? 2 : 1;
const widthFactor = numberOfColumns === 3 ? 2.5 : 2.2;
const fieldWidth = width / (numberOfColumns * widthFactor);
return {
columns: numberOfColumns,
fieldWidth,
};
};

View file

@ -54,9 +54,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
after('clean up archives', async () => {
after('clean up DataStream', async () => {
if (cleanupDataStreamSetup) {
cleanupDataStreamSetup();
await cleanupDataStreamSetup();
}
});

View file

@ -0,0 +1,283 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
const DATASET_NAME = 'flyout';
const NAMESPACE = 'default';
const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`;
const NOW = Date.now();
const sharedDoc = {
time: NOW + 1000,
logFilepath: '/flyout.log',
serviceName: 'frontend-node',
datasetName: DATASET_NAME,
namespace: NAMESPACE,
message: 'full document',
logLevel: 'info',
traceId: 'abcdef',
hostName: 'gke-edge-oblt-pool',
orchestratorClusterId: 'my-cluster-id',
orchestratorClusterName: 'my-cluster-id',
orchestratorResourceId: 'orchestratorResourceId',
cloudProvider: 'gcp',
cloudRegion: 'us-central-1',
cloudAz: 'us-central-1a',
cloudProjectId: 'elastic-project',
cloudInstanceId: 'BgfderflkjTheUiGuy',
agentName: 'node',
};
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dataGrid = getService('dataGrid');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['observabilityLogExplorer']);
describe('Flyout highlight customization', () => {
let cleanupDataStreamSetup: () => Promise<void>;
describe('Service container', () => {
const { serviceName, traceId, ...rest } = sharedDoc;
const docWithoutServiceName = { ...rest, traceId, time: NOW - 1000 };
const docWithoutTraceId = { ...rest, serviceName, time: NOW - 2000 };
const docWithoutServiceContainer = { ...rest, time: NOW - 4000 };
const docs = [
sharedDoc,
docWithoutServiceName,
docWithoutTraceId,
docWithoutServiceContainer,
];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
});
after('clean up DataStream', async () => {
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the service container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.existOrFail('logExplorerFlyoutService');
await testSubjects.existOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
it('should load the service container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.missingOrFail('logExplorerFlyoutService');
await testSubjects.existOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
it('should not load the service container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 3 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.missingOrFail('logExplorerFlyoutService');
await testSubjects.missingOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
});
describe('Infrastructure container', () => {
const { hostName, orchestratorClusterName, orchestratorResourceId, ...rest } = sharedDoc;
const docWithoutHostName = {
...rest,
orchestratorClusterName,
orchestratorResourceId,
time: NOW - 1000,
};
const docWithoutInfrastructureContainer = { ...rest, time: NOW - 2000 };
const docs = [sharedDoc, docWithoutHostName, docWithoutInfrastructureContainer];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
});
after('clean up DataStream', async () => {
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the infrastructure container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.existOrFail('logExplorerFlyoutHostName');
await testSubjects.existOrFail('logExplorerFlyoutClusterName');
await testSubjects.existOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
it('should load the infrastructure container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.missingOrFail('logExplorerFlyoutHostName');
await testSubjects.existOrFail('logExplorerFlyoutClusterName');
await testSubjects.existOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
it('should not load the infrastructure container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.missingOrFail('logExplorerFlyoutHostName');
await testSubjects.missingOrFail('logExplorerFlyoutClusterName');
await testSubjects.missingOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
});
describe('Cloud container', () => {
const { cloudProvider, cloudInstanceId, cloudProjectId, cloudRegion, cloudAz, ...rest } =
sharedDoc;
const docWithoutCloudProviderAndInstanceId = {
...rest,
cloudProjectId,
cloudRegion,
cloudAz,
time: NOW - 1000,
};
const docWithoutCloudContainer = { ...rest, time: NOW - 2000 };
const docs = [sharedDoc, docWithoutCloudProviderAndInstanceId, docWithoutCloudContainer];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
});
after('clean up DataStream', async () => {
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the cloud container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.existOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.existOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.existOrFail('logExplorerFlyoutCloudAz');
await testSubjects.existOrFail('logExplorerFlyoutCloudProjectId');
await testSubjects.existOrFail('logExplorerFlyoutCloudInstanceId');
await dataGrid.closeFlyout();
});
it('should load the cloud container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logExplorerFlyoutCloudInstanceId');
await testSubjects.existOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.existOrFail('logExplorerFlyoutCloudAz');
await testSubjects.existOrFail('logExplorerFlyoutCloudProjectId');
await dataGrid.closeFlyout();
});
it('should not load the cloud container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.missingOrFail('logExplorerFlyoutCloudAz');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProjectId');
await testSubjects.missingOrFail('logExplorerFlyoutCloudInstanceId');
await dataGrid.closeFlyout();
});
});
describe('Other container', () => {
const { logFilepath, agentName, ...rest } = sharedDoc;
const docWithoutLogPathAndAgentName = {
...rest,
time: NOW - 1000,
};
const docs = [sharedDoc, docWithoutLogPathAndAgentName];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
});
after('clean up DataStream', async () => {
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the other container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionOther');
await testSubjects.existOrFail('logExplorerFlyoutLogPathFile');
await testSubjects.existOrFail('logExplorerFlyoutNamespace');
await testSubjects.existOrFail('logExplorerFlyoutDataset');
await testSubjects.existOrFail('logExplorerFlyoutLogShipper');
await dataGrid.closeFlyout();
});
it('should load the other container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionOther');
await testSubjects.missingOrFail('logExplorerFlyoutLogPathFile');
await testSubjects.missingOrFail('logExplorerFlyoutLogShipper');
await testSubjects.existOrFail('logExplorerFlyoutNamespace');
await testSubjects.existOrFail('logExplorerFlyoutDataset');
await dataGrid.closeFlyout();
});
});
});
}

View file

@ -16,5 +16,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./filter_controls'));
loadTestFile(require.resolve('./flyout'));
loadTestFile(require.resolve('./header_menu'));
loadTestFile(require.resolve('./flyout_highlights.ts'));
});
}

View file

@ -406,12 +406,24 @@ export function ObservabilityLogExplorerPageObject({
interface MockLogDoc {
time: number;
logFilepath: string;
logFilepath?: string;
serviceName?: string;
namespace: string;
datasetName: string;
message?: string;
logLevel?: string;
traceId?: string;
hostName?: string;
orchestratorClusterId?: string;
orchestratorClusterName?: string;
orchestratorResourceId?: string;
cloudProvider?: string;
cloudRegion?: string;
cloudAz?: string;
cloudProjectId?: string;
cloudInstanceId?: string;
agentName?: string;
[key: string]: unknown;
}
@ -423,6 +435,17 @@ export function createLogDoc({
datasetName,
message,
logLevel,
traceId,
hostName,
orchestratorClusterId,
orchestratorClusterName,
orchestratorResourceId,
cloudProvider,
cloudRegion,
cloudAz,
cloudProjectId,
cloudInstanceId,
agentName,
...extraFields
}: MockLogDoc) {
return {
@ -452,6 +475,17 @@ export function createLogDoc({
dataset: datasetName,
},
...(logLevel && { 'log.level': logLevel }),
...(traceId && { 'trace.id': traceId }),
...(hostName && { 'host.name': hostName }),
...(orchestratorClusterId && { 'orchestrator.cluster.id': orchestratorClusterId }),
...(orchestratorClusterName && { 'orchestrator.cluster.name': orchestratorClusterName }),
...(orchestratorResourceId && { 'orchestrator.resource.id': orchestratorResourceId }),
...(cloudProvider && { 'cloud.provider': cloudProvider }),
...(cloudRegion && { 'cloud.region': cloudRegion }),
...(cloudAz && { 'cloud.availability_zone': cloudAz }),
...(cloudProjectId && { 'cloud.project.id': cloudProjectId }),
...(cloudInstanceId && { 'cloud.instance.id': cloudInstanceId }),
...(agentName && { 'agent.name': agentName }),
...extraFields,
};
}

View file

@ -0,0 +1,291 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
const DATASET_NAME = 'flyout';
const NAMESPACE = 'default';
const DATA_STREAM_NAME = `logs-${DATASET_NAME}-${NAMESPACE}`;
const NOW = Date.now();
const sharedDoc = {
time: NOW + 1000,
logFilepath: '/flyout.log',
serviceName: 'frontend-node',
datasetName: DATASET_NAME,
namespace: NAMESPACE,
message: 'full document',
logLevel: 'info',
traceId: 'abcdef',
hostName: 'gke-edge-oblt-pool',
orchestratorClusterId: 'my-cluster-id',
orchestratorClusterName: 'my-cluster-id',
orchestratorResourceId: 'orchestratorResourceId',
cloudProvider: 'gcp',
cloudRegion: 'us-central-1',
cloudAz: 'us-central-1a',
cloudProjectId: 'elastic-project',
cloudInstanceId: 'BgfderflkjTheUiGuy',
agentName: 'node',
};
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dataGrid = getService('dataGrid');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['observabilityLogExplorer', 'svlCommonPage']);
describe('Flyout highlight customization', () => {
let cleanupDataStreamSetup: () => Promise<void>;
describe('Service container', () => {
const { serviceName, traceId, ...rest } = sharedDoc;
const docWithoutServiceName = { ...rest, traceId, time: NOW - 1000 };
const docWithoutTraceId = { ...rest, serviceName, time: NOW - 2000 };
const docWithoutServiceContainer = { ...rest, time: NOW - 4000 };
const docs = [
sharedDoc,
docWithoutServiceName,
docWithoutTraceId,
docWithoutServiceContainer,
];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
await PageObjects.svlCommonPage.login();
});
after('clean up DataStream', async () => {
await PageObjects.svlCommonPage.forceLogout();
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the service container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.existOrFail('logExplorerFlyoutService');
await testSubjects.existOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
it('should load the service container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.missingOrFail('logExplorerFlyoutService');
await testSubjects.existOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
it('should not load the service container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 3 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionService');
await testSubjects.missingOrFail('logExplorerFlyoutService');
await testSubjects.missingOrFail('logExplorerFlyoutTrace');
await dataGrid.closeFlyout();
});
});
describe('Infrastructure container', () => {
const { hostName, orchestratorClusterName, orchestratorResourceId, ...rest } = sharedDoc;
const docWithoutHostName = {
...rest,
orchestratorClusterName,
orchestratorResourceId,
time: NOW - 1000,
};
const docWithoutInfrastructureContainer = { ...rest, time: NOW - 2000 };
const docs = [sharedDoc, docWithoutHostName, docWithoutInfrastructureContainer];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
await PageObjects.svlCommonPage.login();
});
after('clean up DataStream', async () => {
await PageObjects.svlCommonPage.forceLogout();
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the infrastructure container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.existOrFail('logExplorerFlyoutHostName');
await testSubjects.existOrFail('logExplorerFlyoutClusterName');
await testSubjects.existOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
it('should load the infrastructure container even when 1 field is missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.missingOrFail('logExplorerFlyoutHostName');
await testSubjects.existOrFail('logExplorerFlyoutClusterName');
await testSubjects.existOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
it('should not load the infrastructure container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionInfrastructure');
await testSubjects.missingOrFail('logExplorerFlyoutHostName');
await testSubjects.missingOrFail('logExplorerFlyoutClusterName');
await testSubjects.missingOrFail('logExplorerFlyoutResourceId');
await dataGrid.closeFlyout();
});
});
describe('Cloud container', () => {
const { cloudProvider, cloudInstanceId, cloudProjectId, cloudRegion, cloudAz, ...rest } =
sharedDoc;
const docWithoutCloudProviderAndInstanceId = {
...rest,
cloudProjectId,
cloudRegion,
cloudAz,
time: NOW - 1000,
};
const docWithoutCloudContainer = { ...rest, time: NOW - 2000 };
const docs = [sharedDoc, docWithoutCloudProviderAndInstanceId, docWithoutCloudContainer];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
await PageObjects.svlCommonPage.login();
});
after('clean up DataStream', async () => {
await PageObjects.svlCommonPage.forceLogout();
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the cloud container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.existOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.existOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.existOrFail('logExplorerFlyoutCloudAz');
await testSubjects.existOrFail('logExplorerFlyoutCloudProjectId');
await testSubjects.existOrFail('logExplorerFlyoutCloudInstanceId');
await dataGrid.closeFlyout();
});
it('should load the cloud container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logExplorerFlyoutCloudInstanceId');
await testSubjects.existOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.existOrFail('logExplorerFlyoutCloudAz');
await testSubjects.existOrFail('logExplorerFlyoutCloudProjectId');
await dataGrid.closeFlyout();
});
it('should not load the cloud container if all fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 2 });
await testSubjects.missingOrFail('logExplorerFlyoutHighlightSectionCloud');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProvider');
await testSubjects.missingOrFail('logExplorerFlyoutCloudRegion');
await testSubjects.missingOrFail('logExplorerFlyoutCloudAz');
await testSubjects.missingOrFail('logExplorerFlyoutCloudProjectId');
await testSubjects.missingOrFail('logExplorerFlyoutCloudInstanceId');
await dataGrid.closeFlyout();
});
});
describe('Other container', () => {
const { logFilepath, agentName, ...rest } = sharedDoc;
const docWithoutLogPathAndAgentName = {
...rest,
time: NOW - 1000,
};
const docs = [sharedDoc, docWithoutLogPathAndAgentName];
before('setup DataStream', async () => {
cleanupDataStreamSetup = await PageObjects.observabilityLogExplorer.setupDataStream(
DATASET_NAME,
NAMESPACE
);
await PageObjects.observabilityLogExplorer.ingestLogEntries(DATA_STREAM_NAME, docs);
await PageObjects.svlCommonPage.login();
});
after('clean up DataStream', async () => {
await PageObjects.svlCommonPage.forceLogout();
if (cleanupDataStreamSetup) {
await cleanupDataStreamSetup();
}
});
beforeEach(async () => {
await PageObjects.observabilityLogExplorer.navigateTo({
from: new Date(NOW - 60_000).toISOString(),
to: new Date(NOW + 60_000).toISOString(),
});
});
it('should load the other container with all fields', async () => {
await dataGrid.clickRowToggle();
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionOther');
await testSubjects.existOrFail('logExplorerFlyoutLogPathFile');
await testSubjects.existOrFail('logExplorerFlyoutNamespace');
await testSubjects.existOrFail('logExplorerFlyoutDataset');
await testSubjects.existOrFail('logExplorerFlyoutLogShipper');
await dataGrid.closeFlyout();
});
it('should load the other container even when some fields are missing', async () => {
await dataGrid.clickRowToggle({ rowIndex: 1 });
await testSubjects.existOrFail('logExplorerFlyoutHighlightSectionOther');
await testSubjects.missingOrFail('logExplorerFlyoutLogPathFile');
await testSubjects.missingOrFail('logExplorerFlyoutLogShipper');
await testSubjects.existOrFail('logExplorerFlyoutNamespace');
await testSubjects.existOrFail('logExplorerFlyoutDataset');
await dataGrid.closeFlyout();
});
});
});
}

View file

@ -16,5 +16,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./filter_controls'));
loadTestFile(require.resolve('./flyout'));
loadTestFile(require.resolve('./header_menu'));
loadTestFile(require.resolve('./header_menu'));
loadTestFile(require.resolve('./flyout_highlights.ts'));
});
}