[Logs Overview] Add a flyout to show category document examples (#194867)

## Summary

Implements https://github.com/elastic/kibana/issues/193450.

## Discover changes ⚠️ 

As part of this we need to render a basic table with the log level and
summary columns, which is technically context aware but only in the
sense we know we want it to be a logs context up front.

The "correct" solution here (or at least from recent conversations) is
to use the saved search embeddable. There is upcoming work planned to
move log stream component usages over to the saved search embeddable.
However, currently this isn't in a place to just be dropped in without
some pretty extensive work. I didn't feel comfortable doing a big push
on that work as a side effort to this work, especially with a loose (if
possible) 8.16 aim for this.

What I've done (and which isn't ideal I appreciate) is used the start
contract of the Discover plugin to export the columns / cells
pre-wrapped with the Discover services. It's not ideal in the sense of
dependencies, but technically Discover doesn't use logs shared. I
considered Discover shared but that's for registering functionality for
Discover, rather than the other way around.

Eventually we'll be able to remove this and convert over to the new
solution. I'm all ears to a better solution, but there's a big mismatch
between the needs here and dropping in something that exists currently.
Thankfully the changeset for Discover is small if we're happy to keep
this temporarily.

Edit: I've made some notes here:
https://github.com/elastic/logs-dev/issues/111#issuecomment-2411096251

Edit: New package added here:
c290819c1c

## Overview

From a high level:

- Adds a new state machine for handling "details" to show in the flyout
(document examples now, plus details and a timeline later).

- Hooks this up to a flyout expanded from the categories table.

- Provides linking to Discover to view documents from the category in
the flyout.

I've also left some comments inline.

## UI / UX 

![Screenshot 2024-10-10 at 15 05
21](https://github.com/user-attachments/assets/49b525b1-f730-4e90-9a84-05175edb8c40)


![flyout_open](https://github.com/user-attachments/assets/0995b952-566b-4e09-80cf-20ad94343980)


![discover_link](https://github.com/user-attachments/assets/249ef269-0105-48af-9c81-ebae1cfb1680)

---------

Co-authored-by: Felix Stürmer <felix.stuermer@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Felix Stürmer <weltenwort@users.noreply.github.com>
Co-authored-by: Julia Rechkunova <julia.rechkunova@gmail.com>
This commit is contained in:
Kerry Gallagher 2024-10-24 15:49:27 +01:00 committed by GitHub
parent 48959e769c
commit 6b63f7f631
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 1764 additions and 902 deletions

1
.github/CODEOWNERS vendored
View file

@ -385,6 +385,7 @@ packages/kbn-dev-proc-runner @elastic/kibana-operations
src/plugins/dev_tools @elastic/kibana-management
packages/kbn-dev-utils @elastic/kibana-operations
examples/developer_examples @elastic/appex-sharedux
packages/kbn-discover-contextual-components @elastic/obs-ux-logs-team @elastic/kibana-data-discovery
examples/discover_customization_examples @elastic/kibana-data-discovery
x-pack/plugins/discover_enhanced @elastic/kibana-data-discovery
src/plugins/discover @elastic/kibana-data-discovery

View file

@ -27,7 +27,7 @@
"dataViews": "src/plugins/data_views",
"defaultNavigation": "packages/default-nav",
"devTools": "src/plugins/dev_tools",
"discover": ["src/plugins/discover", "packages/kbn-discover-utils"],
"discover": ["src/plugins/discover", "packages/kbn-discover-utils", "packages/kbn-discover-contextual-components"],
"savedSearch": "src/plugins/saved_search",
"embeddableApi": "src/plugins/embeddable",
"presentationPanel": "src/plugins/presentation_panel",

View file

@ -451,6 +451,7 @@
"@kbn/default-nav-ml": "link:packages/default-nav/ml",
"@kbn/dev-tools-plugin": "link:src/plugins/dev_tools",
"@kbn/developer-examples-plugin": "link:examples/developer_examples",
"@kbn/discover-contextual-components": "link:packages/kbn-discover-contextual-components",
"@kbn/discover-customization-examples-plugin": "link:examples/discover_customization_examples",
"@kbn/discover-enhanced-plugin": "link:x-pack/plugins/discover_enhanced",
"@kbn/discover-plugin": "link:src/plugins/discover",

View file

@ -0,0 +1,3 @@
# @kbn/discover-contextual-components
Houses contextual (e.g. logs) components that are used by Discover.

View file

@ -7,9 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export interface SmartFieldGridColumnOptions {
type: 'smart-field';
smartField: 'content' | 'resource';
fallbackFields: string[];
width?: number;
}
export * from './src';

View 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-discover-contextual-components'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/discover-contextual-components",
"owner": ["@elastic/obs-ux-logs-team", "@elastic/kibana-data-discovery"]
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/discover-contextual-components",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
"sideEffects": false
}

View file

@ -24,7 +24,9 @@ import {
import { css } from '@emotion/react';
import { useBoolean } from '@kbn/react-hooks';
import { euiThemeVars } from '@kbn/ui-theme';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import {
actionFilterForText,
actionFilterOutText,
@ -109,30 +111,32 @@ export function CellActionsPopover({
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiPopoverFooter>
<EuiFlexGroup responsive={false} gutterSize="s" wrap={true}>
<EuiButtonEmpty
key="addToFilterAction"
size="s"
iconType="plusInCircle"
aria-label={actionFilterForText(value)}
onClick={makeFilterHandlerByOperator('+')}
data-test-subj={`dataTableCellAction_addToFilterAction_${property}`}
>
{filterForText}
</EuiButtonEmpty>
<EuiButtonEmpty
key="removeFromFilterAction"
size="s"
iconType="minusInCircle"
aria-label={actionFilterOutText(value)}
onClick={makeFilterHandlerByOperator('-')}
data-test-subj={`dataTableCellAction_removeFromFilterAction_${property}`}
>
{filterOutText}
</EuiButtonEmpty>
</EuiFlexGroup>
</EuiPopoverFooter>
{onFilter ? (
<EuiPopoverFooter>
<EuiFlexGroup responsive={false} gutterSize="s" wrap={true}>
<EuiButtonEmpty
key="addToFilterAction"
size="s"
iconType="plusInCircle"
aria-label={actionFilterForText(value)}
onClick={makeFilterHandlerByOperator('+')}
data-test-subj={`dataTableCellAction_addToFilterAction_${property}`}
>
{filterForText}
</EuiButtonEmpty>
<EuiButtonEmpty
key="removeFromFilterAction"
size="s"
iconType="minusInCircle"
aria-label={actionFilterOutText(value)}
onClick={makeFilterHandlerByOperator('-')}
data-test-subj={`dataTableCellAction_removeFromFilterAction_${property}`}
>
{filterOutText}
</EuiButtonEmpty>
</EuiFlexGroup>
</EuiPopoverFooter>
) : null}
<EuiPopoverFooter>
<EuiCopy textToCopy={value}>
{(copy) => (
@ -158,13 +162,21 @@ export interface FieldBadgeWithActionsProps
icon?: EuiBadgeProps['iconType'];
}
interface FieldBadgeWithActionsDependencies {
core?: CoreStart;
share?: SharePluginStart;
}
export type FieldBadgeWithActionsPropsAndDependencies = FieldBadgeWithActionsProps &
FieldBadgeWithActionsDependencies;
export function FieldBadgeWithActions({
icon,
onFilter,
property,
renderValue,
value,
}: FieldBadgeWithActionsProps) {
}: FieldBadgeWithActionsPropsAndDependencies) {
return (
<CellActionsPopover
onFilter={onFilter}

View file

@ -0,0 +1,12 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './summary_column';
export * from './log_level_badge_cell/log_level_badge_cell';
export * from './service_name_badge_with_actions';

View file

@ -7,12 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { getLogLevelBadgeCell } from './log_level_badge_cell';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__/data_view';
import { DataTableRecord, buildDataTableRecord } from '@kbn/discover-utils';
const renderCell = (logLevelField: string, record: DataTableRecord) => {
const LogLevelBadgeCell = getLogLevelBadgeCell(logLevelField);

View file

@ -9,8 +9,8 @@
import type { CSSObject } from '@emotion/react';
import React from 'react';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table/src/types';
import { LogLevelBadge } from '@kbn/discover-utils';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
const dataTestSubj = 'logLevelBadgeCell';
const badgeCss: CSSObject = { marginTop: '-4px' };
@ -32,3 +32,5 @@ export const getLogLevelBadgeCell =
/>
);
};
export type LogLevelBadgeCell = ReturnType<typeof getLogLevelBadgeCell>;

View file

@ -11,17 +11,20 @@ import React from 'react';
import { getRouterLinkProps } from '@kbn/router-utils';
import { EuiLink } from '@elastic/eui';
import { OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE } from '@kbn/management-settings-ids';
import { SharePublicStart } from '@kbn/share-plugin/public/plugin';
import { useDiscoverServices } from '../../../hooks/use_discover_services';
import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from './cell_actions_popover';
import type { SharePublicStart } from '@kbn/share-plugin/public/plugin';
import {
FieldBadgeWithActions,
FieldBadgeWithActionsProps,
FieldBadgeWithActionsPropsAndDependencies,
} from './cell_actions_popover';
const SERVICE_ENTITY_LOCATOR = 'SERVICE_ENTITY_LOCATOR';
export function ServiceNameBadgeWithActions(props: FieldBadgeWithActionsProps) {
const { share, core } = useDiscoverServices();
const canViewApm = core.application.capabilities.apm?.show || false;
export function ServiceNameBadgeWithActions(props: FieldBadgeWithActionsPropsAndDependencies) {
const { share, core } = props;
const canViewApm = core?.application.capabilities.apm?.show || false;
const isEntityCentricExperienceSettingEnabled = canViewApm
? core.uiSettings.get(OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE)
? core?.uiSettings.get(OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE)
: false;
const derivedPropsForEntityExperience = isEntityCentricExperienceSettingEnabled

View file

@ -14,7 +14,7 @@ import {
getLogDocumentOverview,
getMessageFieldWithFallbacks,
} from '@kbn/discover-utils';
import * as constants from '../../../../../common/data_types/logs/constants';
import { MESSAGE_FIELD } from '@kbn/discover-utils';
import { formatJsonDocumentForContent } from './utils';
interface ContentProps extends DataGridCellValueElementProps {
@ -32,7 +32,7 @@ const LogMessage = ({
value: string;
className: string;
}) => {
const shouldRenderFieldName = field !== constants.MESSAGE_FIELD;
const shouldRenderFieldName = field !== MESSAGE_FIELD;
if (shouldRenderFieldName) {
return (

View file

@ -0,0 +1,13 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './content';
export * from './resource';
export * from './summary_column';
export * from './utils';

View file

@ -8,8 +8,8 @@
*/
import React from 'react';
import { EuiBadge, EuiFlexGroup } from '@elastic/eui';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { CommonProps, EuiBadge, EuiFlexGroup } from '@elastic/eui';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { ResourceFieldDescriptor } from './utils';
const MAX_LIMITED_FIELDS_VISIBLE = 3;
@ -19,6 +19,7 @@ interface ResourceProps {
/* When true, the column will render a predefined number of resources and indicates with a badge how many more we have */
limited?: boolean;
onFilter?: DocViewFilterFn;
css?: CommonProps['css'];
}
export const Resource = ({ fields, limited = false, onFilter, ...props }: ResourceProps) => {

View file

@ -8,41 +8,41 @@
*/
import React from 'react';
import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { render, screen } from '@testing-library/react';
import SummaryColumn, { SummaryColumnFactoryDeps, SummaryColumnProps } from './summary_column';
import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
import * as constants from '../../../../../common/data_types/logs/constants';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { discoverServiceMock } from '../../../../__mocks__/services';
import * as constants from '@kbn/discover-utils/src/data_types/logs/constants';
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
import { coreMock as corePluginMock } from '@kbn/core/public/mocks';
import { DataTableRecord, buildDataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__/data_view';
const renderSummary = (
record: DataTableRecord,
opts: Partial<SummaryColumnProps & SummaryColumnFactoryDeps> = {}
) => {
render(
<KibanaContextProvider services={discoverServiceMock}>
<SummaryColumn
rowIndex={0}
colIndex={0}
columnId="_source"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={record}
dataView={dataViewMock}
fieldFormats={fieldFormatsMock}
setCellProps={() => {}}
closePopover={() => {}}
density={DataGridDensity.COMPACT}
rowHeight={ROWS_HEIGHT_OPTIONS.single}
onFilter={jest.fn()}
shouldShowFieldHandler={() => true}
{...opts}
/>
</KibanaContextProvider>
<SummaryColumn
rowIndex={0}
colIndex={0}
columnId="_source"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={record}
dataView={dataViewMock}
fieldFormats={fieldFormatsMock}
setCellProps={() => {}}
closePopover={() => {}}
density={DataGridDensity.COMPACT}
rowHeight={ROWS_HEIGHT_OPTIONS.single}
onFilter={jest.fn()}
shouldShowFieldHandler={() => true}
core={corePluginMock.createStart()}
share={sharePluginMock.createStartContract()}
{...opts}
/>
);
};

View file

@ -0,0 +1,171 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DataGridDensity, type DataGridCellValueElementProps } from '@kbn/unified-data-table';
import React from 'react';
import { EuiButtonIcon, EuiCodeBlock, EuiFlexGroup, EuiText, EuiTitle } from '@elastic/eui';
import { JsonCodeEditor } from '@kbn/unified-doc-viewer-plugin/public';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import {
ShouldShowFieldInTableHandler,
getLogDocumentOverview,
getMessageFieldWithFallbacks,
} from '@kbn/discover-utils';
import { ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
import { Resource } from './resource';
import { Content } from './content';
import { createResourceFields, formatJsonDocumentForContent } from './utils';
import {
closeCellActionPopoverText,
contentLabel,
jsonLabel,
resourceLabel,
} from '../translations';
export interface SummaryColumnFactoryDeps {
density: DataGridDensity | undefined;
rowHeight: number | undefined;
shouldShowFieldHandler: ShouldShowFieldInTableHandler;
onFilter?: DocViewFilterFn;
core: CoreStart;
share?: SharePluginStart;
}
export type SummaryColumnProps = DataGridCellValueElementProps;
export type AllSummaryColumnProps = SummaryColumnProps & SummaryColumnFactoryDeps;
export const SummaryColumn = (props: AllSummaryColumnProps) => {
const { isDetails } = props;
if (isDetails) {
return <SummaryCellPopover {...props} />;
}
return <SummaryCell {...props} />;
};
// eslint-disable-next-line import/no-default-export
export default SummaryColumn;
const SummaryCell = ({
density: maybeNullishDensity,
rowHeight: maybeNullishRowHeight,
...props
}: AllSummaryColumnProps) => {
const { onFilter, row, share, core } = props;
const density = maybeNullishDensity ?? DataGridDensity.COMPACT;
const isCompressed = density === DataGridDensity.COMPACT;
const rowHeight = maybeNullishRowHeight ?? ROWS_HEIGHT_OPTIONS.single;
const isSingleLine = rowHeight === ROWS_HEIGHT_OPTIONS.single || rowHeight === 1;
const resourceFields = createResourceFields(row, core, share);
const shouldRenderResource = resourceFields.length > 0;
return isSingleLine ? (
<EuiFlexGroup gutterSize="s">
{shouldRenderResource && (
<Resource
fields={resourceFields}
limited={isSingleLine}
onFilter={onFilter}
css={singleLineResourceCss}
/>
)}
<Content {...props} isCompressed={isCompressed} isSingleLine />
</EuiFlexGroup>
) : (
<>
{shouldRenderResource && (
<Resource
fields={resourceFields}
limited={isSingleLine}
onFilter={onFilter}
css={multiLineResourceCss}
/>
)}
<Content {...props} isCompressed={isCompressed} />
</>
);
};
const SummaryCellPopover = (props: AllSummaryColumnProps) => {
const { row, dataView, fieldFormats, onFilter, closePopover, share, core } = props;
const resourceFields = createResourceFields(row, core, share);
const shouldRenderResource = resourceFields.length > 0;
const documentOverview = getLogDocumentOverview(row, { dataView, fieldFormats });
const { field, value } = getMessageFieldWithFallbacks(documentOverview);
const shouldRenderContent = Boolean(field && value);
const shouldRenderSource = !shouldRenderContent;
return (
<EuiFlexGroup direction="column" css={{ position: 'relative', width: 580 }}>
<EuiButtonIcon
aria-label={closeCellActionPopoverText}
data-test-subj="docTableClosePopover"
iconSize="s"
iconType="cross"
size="xs"
onClick={closePopover}
css={{ position: 'absolute', right: 0 }}
/>
{shouldRenderResource && (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiTitle size="xxs">
<span>{resourceLabel}</span>
</EuiTitle>
<Resource fields={resourceFields} onFilter={onFilter} />
</EuiFlexGroup>
)}
<EuiFlexGroup direction="column" gutterSize="s">
<EuiTitle size="xxs">
<span>{contentLabel}</span>
</EuiTitle>
{shouldRenderContent && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiText color="subdued" size="xs">
{field}
</EuiText>
<EuiCodeBlock
overflowHeight={100}
paddingSize="s"
isCopyable
language="txt"
fontSize="s"
>
{value}
</EuiCodeBlock>
</EuiFlexGroup>
)}
{shouldRenderSource && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiText color="subdued" size="xs">
{jsonLabel}
</EuiText>
<JsonCodeEditor json={formatJsonDocumentForContent(row).raw} height={300} />
</EuiFlexGroup>
)}
</EuiFlexGroup>
</EuiFlexGroup>
);
};
const singleLineResourceCss = {
flexGrow: 0,
lineHeight: 'normal',
marginTop: -1,
};
const multiLineResourceCss = { display: 'inline-flex' };

View file

@ -0,0 +1,147 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { dynamic } from '@kbn/shared-ux-utility';
import React from 'react';
import { css } from '@emotion/react';
import { AgentName } from '@kbn/elastic-agent-utils';
import { euiThemeVars } from '@kbn/ui-theme';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import {
AGENT_NAME_FIELD,
CLOUD_INSTANCE_ID_FIELD,
CONTAINER_ID_FIELD,
CONTAINER_NAME_FIELD,
FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT,
HOST_NAME_FIELD,
ORCHESTRATOR_CLUSTER_NAME_FIELD,
ORCHESTRATOR_NAMESPACE_FIELD,
ORCHESTRATOR_RESOURCE_ID_FIELD,
SERVICE_NAME_FIELD,
} from '@kbn/discover-utils';
import { DataTableRecord, getFieldValue } from '@kbn/discover-utils';
import { LogDocument, ResourceFields, getAvailableResourceFields } from '@kbn/discover-utils/src';
import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from '../cell_actions_popover';
import { ServiceNameBadgeWithActions } from '../service_name_badge_with_actions';
/**
* getUnformattedResourceFields definitions
*/
export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => {
const serviceName = getFieldValue(doc, SERVICE_NAME_FIELD);
const hostName = getFieldValue(doc, HOST_NAME_FIELD);
const agentName = getFieldValue(doc, AGENT_NAME_FIELD);
const orchestratorClusterName = getFieldValue(doc, ORCHESTRATOR_CLUSTER_NAME_FIELD);
const orchestratorResourceId = getFieldValue(doc, ORCHESTRATOR_RESOURCE_ID_FIELD);
const orchestratorNamespace = getFieldValue(doc, ORCHESTRATOR_NAMESPACE_FIELD);
const containerName = getFieldValue(doc, CONTAINER_NAME_FIELD);
const containerId = getFieldValue(doc, CONTAINER_ID_FIELD);
const cloudInstanceId = getFieldValue(doc, CLOUD_INSTANCE_ID_FIELD);
return {
[SERVICE_NAME_FIELD]: serviceName,
[HOST_NAME_FIELD]: hostName,
[AGENT_NAME_FIELD]: agentName,
[ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName,
[ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId,
[ORCHESTRATOR_NAMESPACE_FIELD]: orchestratorNamespace,
[CONTAINER_NAME_FIELD]: containerName,
[CONTAINER_ID_FIELD]: containerId,
[CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId,
};
};
/**
* createResourceFields definitions
*/
const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon'));
const resourceCustomComponentsMap: Partial<
Record<keyof ResourceFields, React.ComponentType<FieldBadgeWithActionsProps>>
> = {
[SERVICE_NAME_FIELD]: ServiceNameBadgeWithActions,
};
export interface ResourceFieldDescriptor {
ResourceBadge: React.ComponentType<FieldBadgeWithActionsProps>;
Icon?: () => JSX.Element;
name: keyof ResourceFields;
value: string;
}
export const createResourceFields = (
row: DataTableRecord,
core: CoreStart,
share?: SharePluginStart
): ResourceFieldDescriptor[] => {
const resourceDoc = getUnformattedResourceFields(row as LogDocument);
const availableResourceFields = getAvailableResourceFields(resourceDoc);
const resourceFields = availableResourceFields.map((name) => {
const ResourceBadgeComponent = resourceCustomComponentsMap[name] ?? FieldBadgeWithActions;
const resourceBadgeComponentWithDependencies = (props: FieldBadgeWithActionsProps) => (
<ResourceBadgeComponent {...props} share={share} core={core} />
);
return {
name,
value: resourceDoc[name] as string,
ResourceBadge: resourceBadgeComponentWithDependencies,
...(name === SERVICE_NAME_FIELD && {
Icon: () => (
<AgentIcon
agentName={resourceDoc[AGENT_NAME_FIELD] as AgentName}
size="m"
css={css`
margin-right: ${euiThemeVars.euiSizeXS};
`}
/>
),
}),
};
});
return resourceFields;
};
/**
* formatJsonDocumentForContent definitions
*/
export const formatJsonDocumentForContent = (row: DataTableRecord) => {
const flattenedResult: DataTableRecord['flattened'] = {};
const rawFieldResult: DataTableRecord['raw']['fields'] = {};
const { raw, flattened } = row;
const { fields } = raw;
// We need 2 loops here for flattened and raw.fields. Flattened contains all fields,
// whereas raw.fields only contains certain fields excluding _ignored
for (const fieldName in flattened) {
if (isFieldAllowed(fieldName) && flattened[fieldName]) {
flattenedResult[fieldName] = flattened[fieldName];
}
}
for (const fieldName in fields) {
if (isFieldAllowed(fieldName) && fields[fieldName]) {
rawFieldResult[fieldName] = fields[fieldName];
}
}
return {
...row,
flattened: flattenedResult,
raw: {
...raw,
fields: rawFieldResult,
},
};
};
const isFieldAllowed = (field: string) =>
!FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT.some((prefix) => field.startsWith(prefix));

View file

@ -0,0 +1,72 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
export const jsonLabel = i18n.translate('discover.logs.dataTable.header.popover.json', {
defaultMessage: 'JSON',
});
export const contentLabel = i18n.translate('discover.logs.dataTable.header.popover.content', {
defaultMessage: 'Content',
});
export const resourceLabel = i18n.translate('discover.logs.dataTable.header.popover.resource', {
defaultMessage: 'Resource',
});
export const actionFilterForText = (text: string) =>
i18n.translate('discover.logs.flyoutDetail.value.hover.filterFor', {
defaultMessage: 'Filter for this {value}',
values: {
value: text,
},
});
export const actionFilterOutText = (text: string) =>
i18n.translate('discover.logs.flyoutDetail.value.hover.filterOut', {
defaultMessage: 'Filter out this {value}',
values: {
value: text,
},
});
export const filterOutText = i18n.translate('discover.logs.popoverAction.filterOut', {
defaultMessage: 'Filter out',
});
export const filterForText = i18n.translate('discover.logs.popoverAction.filterFor', {
defaultMessage: 'Filter for',
});
export const copyValueText = i18n.translate('discover.logs.popoverAction.copyValue', {
defaultMessage: 'Copy value',
});
export const copyValueAriaText = (fieldName: string) =>
i18n.translate('discover.logs.popoverAction.copyValueAriaText', {
defaultMessage: 'Copy value of {fieldName}',
values: {
fieldName,
},
});
export const openCellActionPopoverAriaText = i18n.translate(
'discover.logs.popoverAction.openPopover',
{
defaultMessage: 'Open popover',
}
);
export const closeCellActionPopoverText = i18n.translate(
'discover.logs.popoverAction.closePopover',
{
defaultMessage: 'Close popover',
}
);

View file

@ -0,0 +1,16 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { dynamic } from '@kbn/shared-ux-utility';
export * from './data_types/logs/components';
export const LazySummaryColumn = dynamic(
() => import('./data_types/logs/components/summary_column/summary_column')
);

View file

@ -0,0 +1,37 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"@testing-library/jest-dom",
"@testing-library/react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/field-formats-plugin",
"@kbn/discover-utils",
"@kbn/router-utils",
"@kbn/management-settings-ids",
"@kbn/share-plugin",
"@kbn/ui-theme",
"@kbn/unified-data-table",
"@kbn/unified-doc-viewer",
"@kbn/react-hooks",
"@kbn/core-lifecycle-browser",
"@kbn/i18n",
"@kbn/unified-doc-viewer-plugin",
"@kbn/core",
"@kbn/shared-ux-utility",
"@kbn/elastic-agent-utils",
"@kbn/custom-icons",
]
}

View file

@ -52,15 +52,17 @@ export {
getLogLevelCoalescedValue,
getLogLevelCoalescedValueLabel,
LogLevelCoalescedValue,
LogLevelBadge,
getFieldValue,
getVisibleColumns,
canPrependTimeFieldColumn,
DiscoverFlyouts,
dismissAllFlyoutsExceptFor,
dismissFlyouts,
LogLevelBadge,
} from './src';
export type { LogsContextService } from './src';
export * from './src/types';
export * from './src/data_types/logs/constants';

View file

@ -0,0 +1,70 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { fieldConstants } from '../..';
import { SmartFieldGridColumnOptions } from './types';
export * from '../../field_constants';
export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer';
// Virtual column fields
export const CONTENT_FIELD = 'content';
export const RESOURCE_FIELD = 'resource';
// Sizing
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
export const ACTIONS_COLUMN_WIDTH = 80;
export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: RESOURCE_FIELD,
fallbackFields: [fieldConstants.HOST_NAME_FIELD, fieldConstants.SERVICE_NAME_FIELD],
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
};
export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: CONTENT_FIELD,
fallbackFields: [fieldConstants.MESSAGE_FIELD],
};
export const SMART_FALLBACK_FIELDS = {
[CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION,
[RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION,
};
// UI preferences
export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION];
export const DEFAULT_ROWS_PER_PAGE = 100;
// List of prefixes which needs to be filtered out for Display in Content Column
export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [
'_', // Filter fields like '_id', '_score'
'@timestamp',
'agent.',
'elastic_agent.',
'data_stream.',
'ecs.',
'host.',
'container.',
'cloud.',
'kubernetes.',
'orchestrator.',
'log.',
'service.',
];
export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const LOG_LEVEL_FIELDS = ['log.level', 'log_level'];
export const SERVICE_NAME_FIELDS = ['service.name', 'service_name'];
export const AGENT_NAME_FIELD = 'agent.name';

View file

@ -8,7 +8,7 @@
*/
export * from './types';
export * from './components';
export * from './utils';
export * from './logs_context_service';
export * from './components';

View file

@ -86,3 +86,10 @@ export interface StackTraceFields {
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
}
export interface SmartFieldGridColumnOptions {
type: 'smart-field';
smartField: 'content' | 'resource';
fallbackFields: string[];
width?: number;
}

View file

@ -7,8 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ResourceFields } from '@kbn/discover-utils/src';
import * as constants from '../../common/data_types/logs/constants';
import { ResourceFields } from '../../..';
import * as constants from '../constants';
export const getAvailableResourceFields = (resourceDoc: ResourceFields) => {
const resourceFields: Array<keyof ResourceFields> = [

View file

@ -9,3 +9,4 @@
export * from './get_log_level_color';
export * from './get_log_level_coalesed_value';
export * from './get_available_resource_fields';

View file

@ -25,9 +25,9 @@
"@kbn/field-types",
"@kbn/i18n",
"@kbn/core-ui-settings-browser",
"@kbn/ui-theme",
"@kbn/expressions-plugin",
"@kbn/logs-data-access-plugin",
"@kbn/ui-theme",
"@kbn/i18n-react"
]
}

View file

@ -7,64 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { fieldConstants } from '@kbn/discover-utils';
import { SmartFieldGridColumnOptions } from './display_options';
export * from '@kbn/discover-utils/src/field_constants';
export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer';
// Virtual column fields
export const CONTENT_FIELD = 'content';
export const RESOURCE_FIELD = 'resource';
// Sizing
export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
export const ACTIONS_COLUMN_WIDTH = 80;
export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: RESOURCE_FIELD,
fallbackFields: [fieldConstants.HOST_NAME_FIELD, fieldConstants.SERVICE_NAME_FIELD],
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
};
export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = {
type: 'smart-field',
smartField: CONTENT_FIELD,
fallbackFields: [fieldConstants.MESSAGE_FIELD],
};
export const SMART_FALLBACK_FIELDS = {
[CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION,
[RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION,
};
// UI preferences
export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION];
export const DEFAULT_ROWS_PER_PAGE = 100;
// List of prefixes which needs to be filtered out for Display in Content Column
export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [
'_', // Filter fields like '_id', '_score'
'@timestamp',
'agent.',
'elastic_agent.',
'data_stream.',
'ecs.',
'host.',
'container.',
'cloud.',
'kubernetes.',
'orchestrator.',
'log.',
'service.',
];
export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const LOG_LEVEL_FIELDS = ['log.level', 'log_level'];
export const SERVICE_NAME_FIELDS = ['service.name', 'service_name'];
export const AGENT_NAME_FIELD = 'agent.name';
export * from '@kbn/discover-utils/src/data_types/logs/constants';

View file

@ -15,9 +15,10 @@ import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import { css } from '@emotion/react';
import { getFieldValue } from '@kbn/discover-utils';
import { euiThemeVars } from '@kbn/ui-theme';
import { ServiceNameBadgeWithActions } from '@kbn/discover-contextual-components';
import { useDiscoverServices } from '../../../hooks/use_discover_services';
import { CellRenderersExtensionParams } from '../../../context_awareness';
import { AGENT_NAME_FIELD } from '../../../../common/data_types/logs/constants';
import { ServiceNameBadgeWithActions } from './service_name_badge_with_actions';
const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon'));
const dataTestSubj = 'serviceNameCell';
@ -28,6 +29,7 @@ const agentIconStyle = css`
export const getServiceNameCell =
(serviceNameField: string, { actions }: CellRenderersExtensionParams) =>
(props: DataGridCellValueElementProps) => {
const { core, share } = useDiscoverServices();
const serviceNameValue = getFieldValue(props.row, serviceNameField) as string;
const agentName = getFieldValue(props.row, AGENT_NAME_FIELD) as AgentName;
@ -47,6 +49,8 @@ export const getServiceNameCell =
icon={getIcon}
value={serviceNameValue}
property={serviceNameField}
core={core}
share={share}
/>
);
};

View file

@ -8,13 +8,11 @@
*/
import React from 'react';
import { dynamic } from '@kbn/shared-ux-utility';
import { getShouldShowFieldHandler } from '@kbn/discover-utils';
import { DataView } from '@kbn/data-views-plugin/common';
import { SummaryColumnProps } from '@kbn/discover-contextual-components';
import { CellRenderersExtensionParams } from '../../../../context_awareness';
import type { SummaryColumnProps } from './summary_column';
const SummaryColumn = dynamic(() => import('./summary_column'));
import { SummaryColumn } from './summary_column';
export type SummaryColumnGetterDeps = CellRenderersExtensionParams;
@ -22,7 +20,7 @@ export const getSummaryColumn = (params: SummaryColumnGetterDeps) => {
const { actions, dataView, density, rowHeight } = params;
const shouldShowFieldHandler = createGetShouldShowFieldHandler(dataView);
return (props: SummaryColumnProps) => (
return (props: Omit<SummaryColumnProps, 'core' | 'share'>) => (
<SummaryColumn
{...props}
density={density}

View file

@ -7,163 +7,19 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
ROWS_HEIGHT_OPTIONS,
type DataGridCellValueElementProps,
DataGridDensity,
} from '@kbn/unified-data-table';
import { dynamic } from '@kbn/shared-ux-utility';
import React from 'react';
import { EuiButtonIcon, EuiCodeBlock, EuiFlexGroup, EuiText, EuiTitle } from '@elastic/eui';
import {
ShouldShowFieldInTableHandler,
getLogDocumentOverview,
getMessageFieldWithFallbacks,
} from '@kbn/discover-utils';
import { JsonCodeEditor } from '@kbn/unified-doc-viewer-plugin/public';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { Resource } from './resource';
import { Content } from './content';
import {
closeCellActionPopoverText,
contentLabel,
jsonLabel,
resourceLabel,
} from '../translations';
import { createResourceFields, formatJsonDocumentForContent } from './utils';
import { AllSummaryColumnProps } from '@kbn/discover-contextual-components';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export interface SummaryColumnFactoryDeps {
density: DataGridDensity | undefined;
rowHeight: number | undefined;
shouldShowFieldHandler: ShouldShowFieldInTableHandler;
onFilter?: DocViewFilterFn;
}
const LazySummaryColumn = dynamic(
() =>
import(
'@kbn/discover-contextual-components/src/data_types/logs/components/summary_column/summary_column'
)
);
export type SummaryColumnProps = DataGridCellValueElementProps;
const SummaryColumn = (props: SummaryColumnProps & SummaryColumnFactoryDeps) => {
const { isDetails } = props;
if (isDetails) {
return <SummaryCellPopover {...props} />;
}
return <SummaryCell {...props} />;
export const SummaryColumn = (props: Omit<AllSummaryColumnProps, 'core' | 'share'>) => {
const { share, core } = useDiscoverServices();
return <LazySummaryColumn {...props} share={share} core={core} />;
};
// eslint-disable-next-line import/no-default-export
export default SummaryColumn;
const SummaryCell = ({
density: maybeNullishDensity,
rowHeight: maybeNullishRowHeight,
...props
}: SummaryColumnProps & SummaryColumnFactoryDeps) => {
const { onFilter, row } = props;
const density = maybeNullishDensity ?? DataGridDensity.COMPACT;
const isCompressed = density === DataGridDensity.COMPACT;
const rowHeight = maybeNullishRowHeight ?? ROWS_HEIGHT_OPTIONS.single;
const isSingleLine = rowHeight === ROWS_HEIGHT_OPTIONS.single || rowHeight === 1;
const resourceFields = createResourceFields(row);
const shouldRenderResource = resourceFields.length > 0;
return isSingleLine ? (
<EuiFlexGroup gutterSize="s">
{shouldRenderResource && (
<Resource
fields={resourceFields}
limited={isSingleLine}
onFilter={onFilter}
css={singleLineResourceCss}
/>
)}
<Content {...props} isCompressed={isCompressed} isSingleLine />
</EuiFlexGroup>
) : (
<>
{shouldRenderResource && (
<Resource
fields={resourceFields}
limited={isSingleLine}
onFilter={onFilter}
css={multiLineResourceCss}
/>
)}
<Content {...props} isCompressed={isCompressed} />
</>
);
};
const SummaryCellPopover = (props: SummaryColumnProps & SummaryColumnFactoryDeps) => {
const { row, dataView, fieldFormats, onFilter, closePopover } = props;
const resourceFields = createResourceFields(row);
const shouldRenderResource = resourceFields.length > 0;
const documentOverview = getLogDocumentOverview(row, { dataView, fieldFormats });
const { field, value } = getMessageFieldWithFallbacks(documentOverview);
const shouldRenderContent = Boolean(field && value);
const shouldRenderSource = !shouldRenderContent;
return (
<EuiFlexGroup direction="column" css={{ position: 'relative', width: 580 }}>
<EuiButtonIcon
aria-label={closeCellActionPopoverText}
data-test-subj="docTableClosePopover"
iconSize="s"
iconType="cross"
size="xs"
onClick={closePopover}
css={{ position: 'absolute', right: 0 }}
/>
{shouldRenderResource && (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiTitle size="xxs">
<span>{resourceLabel}</span>
</EuiTitle>
<Resource fields={resourceFields} onFilter={onFilter} />
</EuiFlexGroup>
)}
<EuiFlexGroup direction="column" gutterSize="s">
<EuiTitle size="xxs">
<span>{contentLabel}</span>
</EuiTitle>
{shouldRenderContent && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiText color="subdued" size="xs">
{field}
</EuiText>
<EuiCodeBlock
overflowHeight={100}
paddingSize="s"
isCopyable
language="txt"
fontSize="s"
>
{value}
</EuiCodeBlock>
</EuiFlexGroup>
)}
{shouldRenderSource && (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiText color="subdued" size="xs">
{jsonLabel}
</EuiText>
<JsonCodeEditor json={formatJsonDocumentForContent(row).raw} height={300} />
</EuiFlexGroup>
)}
</EuiFlexGroup>
</EuiFlexGroup>
);
};
const singleLineResourceCss = {
flexGrow: 0,
lineHeight: 'normal',
marginTop: -1,
};
const multiLineResourceCss = { display: 'inline-flex' };

View file

@ -1,126 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getFieldValue, LogDocument, ResourceFields } from '@kbn/discover-utils/src';
import { DataTableRecord } from '@kbn/discover-utils';
import { dynamic } from '@kbn/shared-ux-utility';
import React from 'react';
import { css } from '@emotion/react';
import { AgentName } from '@kbn/elastic-agent-utils';
import { euiThemeVars } from '@kbn/ui-theme';
import { getAvailableResourceFields } from '../../../../utils/get_available_resource_fields';
import * as constants from '../../../../../common/data_types/logs/constants';
import { ServiceNameBadgeWithActions } from '../service_name_badge_with_actions';
import { FieldBadgeWithActions, FieldBadgeWithActionsProps } from '../cell_actions_popover';
/**
* getUnformattedResourceFields definitions
*/
export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => {
const serviceName = getFieldValue(doc, constants.SERVICE_NAME_FIELD);
const hostName = getFieldValue(doc, constants.HOST_NAME_FIELD);
const agentName = getFieldValue(doc, constants.AGENT_NAME_FIELD);
const orchestratorClusterName = getFieldValue(doc, constants.ORCHESTRATOR_CLUSTER_NAME_FIELD);
const orchestratorResourceId = getFieldValue(doc, constants.ORCHESTRATOR_RESOURCE_ID_FIELD);
const orchestratorNamespace = getFieldValue(doc, constants.ORCHESTRATOR_NAMESPACE_FIELD);
const containerName = getFieldValue(doc, constants.CONTAINER_NAME_FIELD);
const containerId = getFieldValue(doc, constants.CONTAINER_ID_FIELD);
const cloudInstanceId = getFieldValue(doc, constants.CLOUD_INSTANCE_ID_FIELD);
return {
[constants.SERVICE_NAME_FIELD]: serviceName,
[constants.HOST_NAME_FIELD]: hostName,
[constants.AGENT_NAME_FIELD]: agentName,
[constants.ORCHESTRATOR_CLUSTER_NAME_FIELD]: orchestratorClusterName,
[constants.ORCHESTRATOR_RESOURCE_ID_FIELD]: orchestratorResourceId,
[constants.ORCHESTRATOR_NAMESPACE_FIELD]: orchestratorNamespace,
[constants.CONTAINER_NAME_FIELD]: containerName,
[constants.CONTAINER_ID_FIELD]: containerId,
[constants.CLOUD_INSTANCE_ID_FIELD]: cloudInstanceId,
};
};
/**
* createResourceFields definitions
*/
const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon'));
const resourceCustomComponentsMap: Partial<
Record<keyof ResourceFields, React.ComponentType<FieldBadgeWithActionsProps>>
> = {
[constants.SERVICE_NAME_FIELD]: ServiceNameBadgeWithActions,
};
export interface ResourceFieldDescriptor {
ResourceBadge: React.ComponentType<FieldBadgeWithActionsProps>;
Icon?: () => JSX.Element;
name: keyof ResourceFields;
value: string;
}
export const createResourceFields = (row: DataTableRecord): ResourceFieldDescriptor[] => {
const resourceDoc = getUnformattedResourceFields(row as LogDocument);
const availableResourceFields = getAvailableResourceFields(resourceDoc);
const resourceFields = availableResourceFields.map((name) => ({
name,
value: resourceDoc[name] as string,
ResourceBadge: resourceCustomComponentsMap[name] ?? FieldBadgeWithActions,
...(name === constants.SERVICE_NAME_FIELD && {
Icon: () => (
<AgentIcon
agentName={resourceDoc[constants.AGENT_NAME_FIELD] as AgentName}
size="m"
css={css`
margin-right: ${euiThemeVars.euiSizeXS};
`}
/>
),
}),
}));
return resourceFields;
};
/**
* formatJsonDocumentForContent definitions
*/
export const formatJsonDocumentForContent = (row: DataTableRecord) => {
const flattenedResult: DataTableRecord['flattened'] = {};
const rawFieldResult: DataTableRecord['raw']['fields'] = {};
const { raw, flattened } = row;
const { fields } = raw;
// We need 2 loops here for flattened and raw.fields. Flattened contains all fields,
// whereas raw.fields only contains certain fields excluding _ignored
for (const fieldName in flattened) {
if (isFieldAllowed(fieldName) && flattened[fieldName]) {
flattenedResult[fieldName] = flattened[fieldName];
}
}
for (const fieldName in fields) {
if (isFieldAllowed(fieldName) && fields[fieldName]) {
rawFieldResult[fieldName] = fields[fieldName];
}
}
return {
...row,
flattened: flattenedResult,
raw: {
...raw,
fields: rawFieldResult,
},
};
};
const isFieldAllowed = (field: string) =>
!constants.FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT.some((prefix) => field.startsWith(prefix));

View file

@ -1,305 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
export const flyoutContentLabel = i18n.translate('discover.logs.flyoutDetail.label.message', {
defaultMessage: 'Content breakdown',
});
export const jsonLabel = i18n.translate('discover.logs.dataTable.header.popover.json', {
defaultMessage: 'JSON',
});
export const contentLabel = i18n.translate('discover.logs.dataTable.header.popover.content', {
defaultMessage: 'Content',
});
export const resourceLabel = i18n.translate('discover.logs.dataTable.header.popover.resource', {
defaultMessage: 'Resource',
});
export const actionsLabel = i18n.translate('discover.logs.dataTable.header.popover.actions', {
defaultMessage: 'Actions',
});
export const actionsLabelLowerCase = i18n.translate(
'discover.logs.dataTable.header.popover.actions.lowercase',
{
defaultMessage: 'actions',
}
);
export const flyoutServiceLabel = i18n.translate('discover.logs.flyoutDetail.label.service', {
defaultMessage: 'Service',
});
export const flyoutTraceLabel = i18n.translate('discover.logs.flyoutDetail.label.trace', {
defaultMessage: 'Trace',
});
export const flyoutHostNameLabel = i18n.translate('discover.logs.flyoutDetail.label.hostName', {
defaultMessage: 'Host name',
});
export const serviceInfraAccordionTitle = i18n.translate(
'discover.logs.flyoutDetail.accordion.title.serviceInfra',
{
defaultMessage: 'Service & Infrastructure',
}
);
export const cloudAccordionTitle = i18n.translate(
'discover.logs.flyoutDetail.accordion.title.cloud',
{
defaultMessage: 'Cloud',
}
);
export const otherAccordionTitle = i18n.translate(
'discover.logs.flyoutDetail.accordion.title.other',
{
defaultMessage: 'Other',
}
);
export const flyoutOrchestratorClusterNameLabel = i18n.translate(
'discover.logs.flyoutDetail.label.orchestratorClusterName',
{
defaultMessage: 'Orchestrator cluster Name',
}
);
export const flyoutOrchestratorResourceIdLabel = i18n.translate(
'discover.logs.flyoutDetail.label.orchestratorResourceId',
{
defaultMessage: 'Orchestrator resource ID',
}
);
export const flyoutCloudProviderLabel = i18n.translate(
'discover.logs.flyoutDetail.label.cloudProvider',
{
defaultMessage: 'Cloud provider',
}
);
export const flyoutCloudRegionLabel = i18n.translate(
'discover.logs.flyoutDetail.label.cloudRegion',
{
defaultMessage: 'Cloud region',
}
);
export const flyoutCloudAvailabilityZoneLabel = i18n.translate(
'discover.logs.flyoutDetail.label.cloudAvailabilityZone',
{
defaultMessage: 'Cloud availability zone',
}
);
export const flyoutCloudProjectIdLabel = i18n.translate(
'discover.logs.flyoutDetail.label.cloudProjectId',
{
defaultMessage: 'Cloud project ID',
}
);
export const flyoutCloudInstanceIdLabel = i18n.translate(
'discover.logs.flyoutDetail.label.cloudInstanceId',
{
defaultMessage: 'Cloud instance ID',
}
);
export const flyoutLogPathFileLabel = i18n.translate(
'discover.logs.flyoutDetail.label.logPathFile',
{
defaultMessage: 'Log path file',
}
);
export const flyoutNamespaceLabel = i18n.translate('discover.logs.flyoutDetail.label.namespace', {
defaultMessage: 'Namespace',
});
export const flyoutDatasetLabel = i18n.translate('discover.logs.flyoutDetail.label.dataset', {
defaultMessage: 'Dataset',
});
export const flyoutShipperLabel = i18n.translate('discover.logs.flyoutDetail.label.shipper', {
defaultMessage: 'Shipper',
});
export const actionFilterForText = (text: string) =>
i18n.translate('discover.logs.flyoutDetail.value.hover.filterFor', {
defaultMessage: 'Filter for this {value}',
values: {
value: text,
},
});
export const actionFilterOutText = (text: string) =>
i18n.translate('discover.logs.flyoutDetail.value.hover.filterOut', {
defaultMessage: 'Filter out this {value}',
values: {
value: text,
},
});
export const filterOutText = i18n.translate('discover.logs.popoverAction.filterOut', {
defaultMessage: 'Filter out',
});
export const filterForText = i18n.translate('discover.logs.popoverAction.filterFor', {
defaultMessage: 'Filter for',
});
export const flyoutHoverActionFilterForFieldPresentText = i18n.translate(
'discover.logs.flyoutDetail.value.hover.filterForFieldPresent',
{
defaultMessage: 'Filter for field present',
}
);
export const flyoutHoverActionToggleColumnText = i18n.translate(
'discover.logs.flyoutDetail.value.hover.toggleColumn',
{
defaultMessage: 'Toggle column in table',
}
);
export const flyoutHoverActionCopyToClipboardText = i18n.translate(
'discover.logs.flyoutDetail.value.hover.copyToClipboard',
{
defaultMessage: 'Copy to clipboard',
}
);
export const copyValueText = i18n.translate('discover.logs.popoverAction.copyValue', {
defaultMessage: 'Copy value',
});
export const copyValueAriaText = (fieldName: string) =>
i18n.translate('discover.logs.popoverAction.copyValueAriaText', {
defaultMessage: 'Copy value of {fieldName}',
values: {
fieldName,
},
});
export const flyoutAccordionShowMoreText = (count: number) =>
i18n.translate('discover.logs.flyoutDetail.section.showMore', {
defaultMessage: '+ {hiddenCount} more',
values: {
hiddenCount: count,
},
});
export const openCellActionPopoverAriaText = i18n.translate(
'discover.logs.popoverAction.openPopover',
{
defaultMessage: 'Open popover',
}
);
export const closeCellActionPopoverText = i18n.translate(
'discover.logs.popoverAction.closePopover',
{
defaultMessage: 'Close popover',
}
);
export const contentHeaderTooltipParagraph1 = (
<FormattedMessage
id="discover.logs.dataTable.header.content.tooltip.paragraph1"
defaultMessage="Displays the document's {logLevel} and {message} fields."
values={{
logLevel: <strong>log.level</strong>,
message: <strong>message</strong>,
}}
/>
);
export const contentHeaderTooltipParagraph2 = i18n.translate(
'discover.logs.dataTable.header.content.tooltip.paragraph2',
{
defaultMessage: 'When the message field is empty, one of the following is displayed:',
}
);
export const resourceHeaderTooltipParagraph = i18n.translate(
'discover.logs.dataTable.header.resource.tooltip.paragraph',
{
defaultMessage: "Fields that provide information on the document's source, such as:",
}
);
export const actionsHeaderTooltipParagraph = i18n.translate(
'discover.logs.dataTable.header.actions.tooltip.paragraph',
{
defaultMessage: 'Fields that provide actionable information, such as:',
}
);
export const actionsHeaderTooltipExpandAction = i18n.translate(
'discover.logs.dataTable.header.actions.tooltip.expand',
{ defaultMessage: 'Expand log details' }
);
export const actionsHeaderTooltipDegradedAction = (
<FormattedMessage
id="discover.logs.dataTable.controlColumn.actions.button.degradedDoc"
defaultMessage="Access to degraded doc with {ignoredProperty} field"
values={{
ignoredProperty: (
<EuiCode language="json" transparentBackground>
_ignored
</EuiCode>
),
}}
/>
);
export const actionsHeaderTooltipStacktraceAction = i18n.translate(
'discover.logs.dataTable.header.actions.tooltip.stacktrace',
{ defaultMessage: 'Access to available stacktraces based on:' }
);
export const degradedDocButtonLabelWhenPresent = i18n.translate(
'discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent',
{
defaultMessage:
"This document couldn't be parsed correctly. Not all fields are properly populated",
}
);
export const degradedDocButtonLabelWhenNotPresent = i18n.translate(
'discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent',
{
defaultMessage: 'All fields in this document were parsed correctly',
}
);
export const stacktraceAvailableControlButton = i18n.translate(
'discover.logs.dataTable.controlColumn.actions.button.stacktrace.available',
{
defaultMessage: 'Stacktraces available',
}
);
export const stacktraceNotAvailableControlButton = i18n.translate(
'discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable',
{
defaultMessage: 'Stacktraces not available',
}
);

View file

@ -8,12 +8,12 @@
*/
import { SOURCE_COLUMN } from '@kbn/unified-data-table';
import { getLogLevelBadgeCell } from '@kbn/discover-contextual-components';
import { getSummaryColumn } from '../../../../../components/data_types/logs/summary_column';
import {
LOG_LEVEL_FIELDS,
SERVICE_NAME_FIELDS,
} from '../../../../../../common/data_types/logs/constants';
import { getLogLevelBadgeCell } from '../../../../../components/data_types/logs/log_level_badge_cell';
import { getServiceNameCell } from '../../../../../components/data_types/logs/service_name_cell';
import type { DataSourceProfileProvider } from '../../../../profiles';

View file

@ -96,11 +96,9 @@
"@kbn/observability-ai-assistant-plugin",
"@kbn/fields-metadata-plugin",
"@kbn/security-solution-common",
"@kbn/router-utils",
"@kbn/management-settings-ids",
"@kbn/react-hooks",
"@kbn/logs-data-access-plugin",
"@kbn/core-lifecycle-browser",
"@kbn/discover-contextual-components",
"@kbn/esql-ast"
],
"exclude": [

View file

@ -12,3 +12,4 @@
"optionalPlugins": ["fieldsMetadata"]
}
}

View file

@ -764,6 +764,8 @@
"@kbn/dev-utils/*": ["packages/kbn-dev-utils/*"],
"@kbn/developer-examples-plugin": ["examples/developer_examples"],
"@kbn/developer-examples-plugin/*": ["examples/developer_examples/*"],
"@kbn/discover-contextual-components": ["packages/kbn-discover-contextual-components"],
"@kbn/discover-contextual-components/*": ["packages/kbn-discover-contextual-components/*"],
"@kbn/discover-customization-examples-plugin": ["examples/discover_customization_examples"],
"@kbn/discover-customization-examples-plugin/*": ["examples/discover_customization_examples/*"],
"@kbn/discover-enhanced-plugin": ["x-pack/plugins/discover_enhanced"],

View file

@ -13,11 +13,17 @@ import { i18n } from '@kbn/i18n';
import { getRouterLinkProps } from '@kbn/router-utils';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import React, { useCallback, useMemo } from 'react';
import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import type { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
interface LinkFilter {
filter: QueryDslQueryContainer;
meta?: {
name?: string;
};
}
export interface DiscoverLinkProps {
documentFilters?: QueryDslQueryContainer[];
logsSource: IndexNameLogsSourceConfiguration;
documentFilters?: LinkFilter[];
logsSource: ResolvedIndexNameLogsSourceConfiguration;
timeRange: {
start: string;
end: string;
@ -46,10 +52,10 @@ export const DiscoverLink = React.memo(
filters: documentFilters?.map((filter) =>
buildCustomFilter(
logsSource.indexName,
filter,
filter.filter,
false,
false,
categorizedLogsFilterLabel,
filter.meta?.name ?? categorizedLogsFilterLabel,
FilterStateStore.APP_STATE
)
),

View file

@ -14,7 +14,12 @@ import {
categorizeLogsService,
createCategorizeLogsServiceImplementations,
} from '../../services/categorize_logs_service';
import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import {
categoryDetailsService,
createCategoryDetailsServiceImplementations,
} from '../../services/category_details_service';
import { LogCategory } from '../../types';
import { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import { LogCategoriesErrorContent } from './log_categories_error_content';
import { LogCategoriesLoadingContent } from './log_categories_loading_content';
import {
@ -25,7 +30,7 @@ import {
export interface LogCategoriesProps {
dependencies: LogCategoriesDependencies;
documentFilters?: QueryDslQueryContainer[];
logsSource: IndexNameLogsSourceConfiguration;
logsSource: ResolvedIndexNameLogsSourceConfiguration;
// The time range could be made optional if we want to support an internal
// time range picker
timeRange: {
@ -61,12 +66,49 @@ export const LogCategories: React.FC<LogCategoriesProps> = ({
}
);
const [categoryDetailsServiceState, sendToCategoryDetailsService] = useMachine(
categoryDetailsService.provide(
createCategoryDetailsServiceImplementations({ search: dependencies.search })
),
{
inspect: consoleInspector,
input: {
index: logsSource.indexName,
startTimestamp: timeRange.start,
endTimestamp: timeRange.end,
timeField: logsSource.timestampField,
messageField: logsSource.messageField,
additionalFilters: documentFilters,
dataView: logsSource.dataView,
},
}
);
const cancelOperation = useCallback(() => {
sendToCategorizeLogsService({
type: 'cancel',
});
}, [sendToCategorizeLogsService]);
const closeFlyout = useCallback(() => {
sendToCategoryDetailsService({
type: 'setExpandedCategory',
category: null,
rowIndex: null,
});
}, [sendToCategoryDetailsService]);
const openFlyout = useCallback(
(category: LogCategory | null, rowIndex: number | null) => {
sendToCategoryDetailsService({
type: 'setExpandedCategory',
category,
rowIndex,
});
},
[sendToCategoryDetailsService]
);
if (categorizeLogsServiceState.matches('done')) {
return (
<LogCategoriesResultContent
@ -75,6 +117,9 @@ export const LogCategories: React.FC<LogCategoriesProps> = ({
logCategories={categorizeLogsServiceState.context.categories}
logsSource={logsSource}
timeRange={timeRange}
categoryDetailsServiceState={categoryDetailsServiceState}
onCloseFlyout={closeFlyout}
onOpenFlyout={openFlyout}
/>
);
} else if (categorizeLogsServiceState.matches('failed')) {

View file

@ -8,13 +8,13 @@
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import React from 'react';
import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import React, { useMemo } from 'react';
import type { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import { DiscoverLink } from '../discover_link';
export interface LogCategoriesControlBarProps {
documentFilters?: QueryDslQueryContainer[];
logsSource: IndexNameLogsSourceConfiguration;
logsSource: ResolvedIndexNameLogsSourceConfiguration;
timeRange: {
start: string;
end: string;
@ -28,12 +28,17 @@ export interface LogCategoriesControlBarDependencies {
export const LogCategoriesControlBar: React.FC<LogCategoriesControlBarProps> = React.memo(
({ dependencies, documentFilters, logsSource, timeRange }) => {
const linkFilters = useMemo(
() => documentFilters?.map((filter) => ({ filter })),
[documentFilters]
);
return (
<EuiFlexGroup direction="row" justifyContent="flexEnd" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<DiscoverLink
dependencies={dependencies}
documentFilters={documentFilters}
documentFilters={linkFilters}
logsSource={logsSource}
timeRange={timeRange}
/>

View file

@ -25,10 +25,14 @@ import {
logCategoriesGridColumns,
renderLogCategoriesGridCell,
} from './log_categories_grid_cell';
import { createLogCategoriesGridControlColumns } from './log_categories_grid_control_columns';
export interface LogCategoriesGridProps {
dependencies: LogCategoriesGridDependencies;
logCategories: LogCategory[];
expandedRowIndex: number | null;
onOpenFlyout: (category: LogCategory, rowIndex: number) => void;
onCloseFlyout: () => void;
}
export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
@ -36,6 +40,9 @@ export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
export const LogCategoriesGrid: React.FC<LogCategoriesGridProps> = ({
dependencies,
logCategories,
expandedRowIndex,
onOpenFlyout,
onCloseFlyout,
}) => {
const [gridState, dispatchGridEvent] = useMachine(gridStateService, {
input: {
@ -93,6 +100,11 @@ export const LogCategoriesGrid: React.FC<LogCategoriesGridProps> = ({
onSort: (sortingColumns) =>
dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }),
}}
leadingControlColumns={createLogCategoriesGridControlColumns({
expandedRowIndex,
onOpenFlyout,
onCloseFlyout,
})}
/>
);
};

View file

@ -83,7 +83,7 @@ export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number
const cellContextKey = 'cellContext';
const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
export const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
(cellContextKey in cellContext
? cellContext[cellContextKey]
: {}) as LogCategoriesGridCellContext;

View file

@ -0,0 +1,45 @@
/*
* 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 { EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { LogCategory } from '../../types';
import { createLogCategoriesGridExpandButton } from './log_categories_grid_expand_button';
const DEFAULT_CONTROL_COLUMN_WIDTH = 40;
interface ControlColumnsProps {
expandedRowIndex: number | null;
onOpenFlyout: (category: LogCategory, rowIndex: number) => void;
onCloseFlyout: () => void;
}
export const createLogCategoriesGridControlColumns = (props: ControlColumnsProps) => {
const { expandedRowIndex, onOpenFlyout, onCloseFlyout } = props;
return [
{
id: 'toggleFlyout',
width: DEFAULT_CONTROL_COLUMN_WIDTH,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>
{i18n.translate('xpack.observabilityLogsOverview.controlColumnHeader', {
defaultMessage: 'Control column',
})}
</span>
</EuiScreenReaderOnly>
),
rowCellRender: createLogCategoriesGridExpandButton({
expandedRowIndex,
onOpenFlyout,
onCloseFlyout,
}),
},
];
};

View file

@ -0,0 +1,71 @@
/*
* 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 { EuiButtonIcon, EuiToolTip, RenderCellValue } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback } from 'react';
import { LogCategory } from '../../types';
import { getCellContext } from './log_categories_grid_cell';
interface CreateLogCategoriesGridExpandButtonProps {
expandedRowIndex: number | null;
onOpenFlyout: (category: LogCategory, rowIndex: number) => void;
onCloseFlyout: () => void;
}
export const createLogCategoriesGridExpandButton =
({
expandedRowIndex,
onOpenFlyout,
onCloseFlyout,
}: CreateLogCategoriesGridExpandButtonProps): RenderCellValue =>
(props) => {
const { rowIndex } = props;
const { logCategories } = getCellContext(props);
const logCategory = logCategories[rowIndex];
const isCurrentRowExpanded = expandedRowIndex === rowIndex;
const onClickHandler = useCallback(() => {
if (isCurrentRowExpanded) {
onCloseFlyout();
} else {
onOpenFlyout(logCategory, rowIndex);
}
}, [isCurrentRowExpanded, logCategory, rowIndex]);
return (
<ExpandButton isCurrentRowExpanded={isCurrentRowExpanded} onClickHandler={onClickHandler} />
);
};
interface ExpandButtonProps {
isCurrentRowExpanded: boolean;
onClickHandler: () => void;
}
const ExpandButton: React.FC<ExpandButtonProps> = ({ isCurrentRowExpanded, onClickHandler }) => {
return (
<EuiToolTip content={buttonLabel}>
<EuiButtonIcon
size="xs"
iconSize="s"
aria-label={buttonLabel}
data-test-subj="logsOverviewLogCategoriesGridExpandButton"
onClick={onClickHandler}
color={isCurrentRowExpanded ? 'primary' : 'text'}
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
isSelected={isCurrentRowExpanded}
/>
</EuiToolTip>
);
};
const buttonLabel = i18n.translate(
'xpack.observabilityLogsOverview.logCategoriesGrid.controlColumns.toggleFlyout',
{
defaultMessage: 'Toggle flyout with details',
}
);

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { EuiDataGridColumn, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { EuiDataGridColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import React from 'react';
import { LogCategory } from '../../types';
import { LogCategoryPattern } from '../shared/log_category_pattern';
export const logCategoriesGridPatternColumn = {
id: 'pattern' as const,
@ -27,34 +27,5 @@ export interface LogCategoriesGridPatternCellProps {
export const LogCategoriesGridPatternCell: React.FC<LogCategoriesGridPatternCellProps> = ({
logCategory,
}) => {
const theme = useEuiTheme();
const { euiTheme } = theme;
const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]);
const commonStyle = css`
display: inline-block;
font-family: ${euiTheme.font.familyCode};
margin-right: ${euiTheme.size.xs};
`;
const termStyle = css`
${commonStyle};
`;
const separatorStyle = css`
${commonStyle};
color: ${euiTheme.colors.successText};
`;
return (
<pre>
<div css={separatorStyle}>*</div>
{termsList.map((term, index) => (
<React.Fragment key={index}>
<div css={termStyle}>{term}</div>
<div css={separatorStyle}>*</div>
</React.Fragment>
))}
</pre>
);
return <LogCategoryPattern logCategory={logCategory} />;
};

View file

@ -9,8 +9,14 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { StateFrom } from 'xstate5';
import { categoryDetailsService } from '../../services/category_details_service';
import { LogCategory } from '../../types';
import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import { ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import {
LogCategoriesFlyoutDependencies,
LogCategoryDetailsFlyout,
} from '../log_category_details/log_category_details_flyout';
import {
LogCategoriesControlBar,
LogCategoriesControlBarDependencies,
@ -21,15 +27,19 @@ export interface LogCategoriesResultContentProps {
dependencies: LogCategoriesResultContentDependencies;
documentFilters?: QueryDslQueryContainer[];
logCategories: LogCategory[];
logsSource: IndexNameLogsSourceConfiguration;
logsSource: ResolvedIndexNameLogsSourceConfiguration;
timeRange: {
start: string;
end: string;
};
categoryDetailsServiceState: StateFrom<typeof categoryDetailsService>;
onCloseFlyout: () => void;
onOpenFlyout: (category: LogCategory, rowIndex: number) => void;
}
export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies &
LogCategoriesGridDependencies;
LogCategoriesGridDependencies &
LogCategoriesFlyoutDependencies;
export const LogCategoriesResultContent: React.FC<LogCategoriesResultContentProps> = ({
dependencies,
@ -37,6 +47,9 @@ export const LogCategoriesResultContent: React.FC<LogCategoriesResultContentProp
logCategories,
logsSource,
timeRange,
categoryDetailsServiceState,
onCloseFlyout,
onOpenFlyout,
}) => {
if (logCategories.length === 0) {
return <LogCategoriesEmptyResultContent />;
@ -52,7 +65,24 @@ export const LogCategoriesResultContent: React.FC<LogCategoriesResultContentProp
/>
</EuiFlexItem>
<EuiFlexItem grow>
<LogCategoriesGrid dependencies={dependencies} logCategories={logCategories} />
<LogCategoriesGrid
dependencies={dependencies}
logCategories={logCategories}
expandedRowIndex={categoryDetailsServiceState.context.expandedRowIndex}
onOpenFlyout={onOpenFlyout}
onCloseFlyout={onCloseFlyout}
/>
{categoryDetailsServiceState.context.expandedCategory && (
<LogCategoryDetailsFlyout
logCategory={categoryDetailsServiceState.context.expandedCategory}
onCloseFlyout={onCloseFlyout}
categoryDetailsServiceState={categoryDetailsServiceState}
logsSource={logsSource}
dependencies={dependencies}
documentFilters={documentFilters}
timeRange={timeRange}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);

View 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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
export interface LogCategoryDetailsErrorContentProps {
error?: Error;
title: string;
}
export const LogCategoryDetailsErrorContent: React.FC<LogCategoryDetailsErrorContentProps> = ({
error,
title,
}) => {
return (
<EuiEmptyPrompt
color="danger"
iconType="error"
title={<h2>{title}</h2>}
body={
<EuiCodeBlock className="eui-textLeft" whiteSpace="pre">
<p>{error?.stack ?? error?.toString() ?? unknownErrorDescription}</p>
</EuiCodeBlock>
}
layout="vertical"
/>
);
};
const unknownErrorDescription = i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetails.unknownErrorDescription',
{
defaultMessage: 'An unspecified error occurred.',
}
);

View file

@ -0,0 +1,139 @@
/*
* 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,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiSpacer,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { StateFrom } from 'xstate5';
import { i18n } from '@kbn/i18n';
import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { LogCategory } from '../../types';
import { LogCategoryPattern } from '../shared/log_category_pattern';
import { categoryDetailsService } from '../../services/category_details_service';
import {
LogCategoryDocumentExamplesTable,
LogCategoryDocumentExamplesTableDependencies,
} from './log_category_document_examples_table';
import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
import { LogCategoryDetailsLoadingContent } from './log_category_details_loading_content';
import { LogCategoryDetailsErrorContent } from './log_category_details_error_content';
import { DiscoverLink } from '../discover_link';
import { createCategoryQuery } from '../../services/categorize_logs_service/queries';
export type LogCategoriesFlyoutDependencies = LogCategoryDocumentExamplesTableDependencies;
interface LogCategoryDetailsFlyoutProps {
onCloseFlyout: () => void;
logCategory: LogCategory;
categoryDetailsServiceState: StateFrom<typeof categoryDetailsService>;
dependencies: LogCategoriesFlyoutDependencies;
logsSource: ResolvedIndexNameLogsSourceConfiguration;
documentFilters?: QueryDslQueryContainer[];
timeRange: {
start: string;
end: string;
};
}
export const LogCategoryDetailsFlyout: React.FC<LogCategoryDetailsFlyoutProps> = ({
onCloseFlyout,
logCategory,
categoryDetailsServiceState,
dependencies,
logsSource,
documentFilters,
timeRange,
}) => {
const flyoutTitleId = useGeneratedHtmlId({
prefix: 'flyoutTitle',
});
const linkFilters = useMemo(() => {
return [
...(documentFilters ? documentFilters.map((filter) => ({ filter })) : []),
{
filter: createCategoryQuery(logsSource.messageField)(logCategory.terms),
meta: {
name: i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.discoverLinkFilterName',
{
defaultMessage: 'Category: {terms}',
values: {
terms: logCategory.terms,
},
}
),
},
},
];
}, [documentFilters, logCategory.terms, logsSource.messageField]);
return (
<EuiFlyout ownFocus onClose={() => onCloseFlyout()} aria-labelledby={flyoutTitleId}>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="m">
<h2 id={flyoutTitleId}>
<FormattedMessage
id="xpack.observabilityLogsOverview.logCategoryDetailsFlyout.title"
defaultMessage="Category details"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<LogCategoryPattern logCategory={logCategory} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<DiscoverLink
dependencies={dependencies}
timeRange={timeRange}
logsSource={logsSource}
documentFilters={linkFilters}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{categoryDetailsServiceState.matches({ hasCategory: 'fetchingDocuments' }) ? (
<LogCategoryDetailsLoadingContent
message={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.loadingMessage',
{
defaultMessage: 'Loading latest documents',
}
)}
/>
) : categoryDetailsServiceState.matches({ hasCategory: 'error' }) ? (
<LogCategoryDetailsErrorContent
title={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDetailsFlyout.fetchingDocumentsErrorTitle',
{
defaultMessage: 'Failed to fetch documents',
}
)}
/>
) : (
<LogCategoryDocumentExamplesTable
dependencies={dependencies}
categoryDocuments={categoryDetailsServiceState.context.categoryDocuments}
logsSource={logsSource}
/>
)}
</EuiFlyoutBody>
</EuiFlyout>
);
};

View file

@ -0,0 +1,19 @@
/*
* 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 { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
interface LogCategoryDetailsLoadingContentProps {
message: string;
}
export const LogCategoryDetailsLoadingContent: React.FC<LogCategoryDetailsLoadingContentProps> = ({
message,
}) => {
return <EuiEmptyPrompt icon={<EuiLoadingSpinner size="xl" />} title={<h2>{message}</h2>} />;
};

View 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 { EuiBasicTable, EuiBasicTableColumn, EuiSpacer, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
import moment from 'moment';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { getLogLevelBadgeCell, LazySummaryColumn } from '@kbn/discover-contextual-components';
import type { LogCategoryDocument } from '../../services/category_details_service/types';
import { type ResolvedIndexNameLogsSourceConfiguration } from '../../utils/logs_source';
export interface LogCategoryDocumentExamplesTableDependencies {
core: CoreStart;
uiSettings: SettingsStart;
fieldFormats: FieldFormatsStart;
share: SharePluginStart;
}
export interface LogCategoryDocumentExamplesTableProps {
dependencies: LogCategoryDocumentExamplesTableDependencies;
categoryDocuments: LogCategoryDocument[];
logsSource: ResolvedIndexNameLogsSourceConfiguration;
}
const TimestampCell = ({
dependencies,
timestamp,
}: {
dependencies: LogCategoryDocumentExamplesTableDependencies;
timestamp?: string | number;
}) => {
const dateFormat = useMemo(
() => dependencies.uiSettings.client.get('dateFormat'),
[dependencies.uiSettings.client]
);
if (!timestamp) return null;
if (dateFormat) {
return <>{moment(timestamp).format(dateFormat)}</>;
} else {
return <>{timestamp}</>;
}
};
const LogLevelBadgeCell = getLogLevelBadgeCell('log.level');
export const LogCategoryDocumentExamplesTable: React.FC<LogCategoryDocumentExamplesTableProps> = ({
categoryDocuments,
dependencies,
logsSource,
}) => {
const columns: Array<EuiBasicTableColumn<LogCategoryDocument>> = [
{
field: 'row',
name: 'Timestamp',
width: '25%',
render: (row: any) => {
return (
<TimestampCell
dependencies={dependencies}
timestamp={row.raw[logsSource.timestampField]}
/>
);
},
},
{
field: 'row',
name: 'Log level',
width: '10%',
render: (row: any) => {
return (
<LogLevelBadgeCell
rowIndex={0}
colIndex={0}
columnId="row"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={row}
dataView={logsSource.dataView}
fieldFormats={dependencies.fieldFormats}
setCellProps={() => {}}
closePopover={() => {}}
/>
);
},
},
{
field: 'row',
name: 'Summary',
width: '65%',
render: (row: any) => {
return (
<LazySummaryColumn
rowIndex={0}
colIndex={0}
columnId="_source"
isExpandable={true}
isExpanded={false}
isDetails={false}
row={row}
dataView={logsSource.dataView}
fieldFormats={dependencies.fieldFormats}
setCellProps={() => {}}
closePopover={() => {}}
density={DataGridDensity.COMPACT}
rowHeight={ROWS_HEIGHT_OPTIONS.single}
shouldShowFieldHandler={() => false}
core={dependencies.core}
share={dependencies.share}
/>
);
},
},
];
return (
<>
<EuiText size="xs" color="subdued">
{i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDocumentExamplesTable.documentCountText',
{
defaultMessage: 'Displaying the latest {documentsCount} documents.',
values: {
documentsCount: categoryDocuments.length,
},
}
)}
</EuiText>
<EuiSpacer size="s" />
<EuiBasicTable
tableCaption={i18n.translate(
'xpack.observabilityLogsOverview.logCategoryDocumentExamplesTable.tableCaption',
{
defaultMessage: 'Log category example documents table',
}
)}
items={categoryDocuments}
columns={columns}
/>
</>
);
};

View file

@ -9,6 +9,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source';
import { LogCategories, LogCategoriesDependencies } from '../log_categories';
import { LogsOverviewErrorContent } from './logs_overview_error_content';
@ -26,6 +27,7 @@ export interface LogsOverviewProps {
export type LogsOverviewDependencies = LogCategoriesDependencies & {
logsDataAccess: LogsDataAccessPluginStart;
dataViews: DataViewsContract;
};
export const LogsOverview: React.FC<LogsOverviewProps> = React.memo(
@ -36,8 +38,12 @@ export const LogsOverview: React.FC<LogsOverviewProps> = React.memo(
timeRange,
}) => {
const normalizedLogsSource = useAsync(
() => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource),
[dependencies.logsDataAccess, logsSource]
() =>
normalizeLogsSource({
logsDataAccess: dependencies.logsDataAccess,
dataViewsService: dependencies.dataViews,
})(logsSource),
[dependencies.dataViews, dependencies.logsDataAccess, logsSource]
);
if (normalizedLogsSource.loading) {

View file

@ -0,0 +1,50 @@
/*
* 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';
import { useMemo } from 'react';
import { css } from '@emotion/react';
import React from 'react';
import { getLogCategoryTerms } from '../../utils/log_category';
import { LogCategory } from '../../types';
interface LogCategoryPatternProps {
logCategory: LogCategory;
}
export const LogCategoryPattern: React.FC<LogCategoryPatternProps> = ({ logCategory }) => {
const theme = useEuiTheme();
const { euiTheme } = theme;
const termsList = useMemo(() => getLogCategoryTerms(logCategory), [logCategory]);
const commonStyle = css`
display: inline-block;
font-family: ${euiTheme.font.familyCode};
margin-right: ${euiTheme.size.xs};
`;
const termStyle = css`
${commonStyle};
`;
const separatorStyle = css`
${commonStyle};
color: ${euiTheme.colors.successText};
`;
return (
<pre>
<div css={separatorStyle}>*</div>
{termsList.map((term, index) => (
<React.Fragment key={index}>
<div css={termStyle}>{term}</div>
<div css={separatorStyle}>*</div>
</React.Fragment>
))}
</pre>
);
};

View file

@ -0,0 +1,191 @@
/*
* 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 { MachineImplementationsFrom, assign, setup } from 'xstate5';
import { LogCategory } from '../../types';
import { getPlaceholderFor } from '../../utils/xstate5_utils';
import {
CategoryDetailsServiceDependencies,
LogCategoryDocument,
LogCategoryDetailsParams,
} from './types';
import { getCategoryDocuments } from './category_documents';
export const categoryDetailsService = setup({
types: {
input: {} as LogCategoryDetailsParams,
output: {} as {
categoryDocuments: LogCategoryDocument[] | null;
},
context: {} as {
parameters: LogCategoryDetailsParams;
error?: Error;
expandedRowIndex: number | null;
expandedCategory: LogCategory | null;
categoryDocuments: LogCategoryDocument[];
},
events: {} as
| {
type: 'cancel';
}
| {
type: 'setExpandedCategory';
rowIndex: number | null;
category: LogCategory | null;
},
},
actors: {
getCategoryDocuments: getPlaceholderFor(getCategoryDocuments),
},
actions: {
storeCategory: assign(
({ context, event }, params: { category: LogCategory | null; rowIndex: number | null }) => ({
expandedCategory: params.category,
expandedRowIndex: params.rowIndex,
})
),
storeDocuments: assign(
({ context, event }, params: { categoryDocuments: LogCategoryDocument[] }) => ({
categoryDocuments: params.categoryDocuments,
})
),
storeError: assign((_, params: { error: unknown }) => ({
error: params.error instanceof Error ? params.error : new Error(String(params.error)),
})),
},
guards: {
hasCategory: (_guardArgs, params: { expandedCategory: LogCategory | null }) =>
params.expandedCategory !== null,
hasDocumentExamples: (
_guardArgs,
params: { categoryDocuments: LogCategoryDocument[] | null }
) => params.categoryDocuments !== null && params.categoryDocuments.length > 0,
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
id: 'logCategoryDetails',
context: ({ input }) => ({
expandedCategory: null,
expandedRowIndex: null,
categoryDocuments: [],
parameters: input,
}),
initial: 'idle',
states: {
idle: {
on: {
setExpandedCategory: {
target: 'checkingCategoryState',
actions: [
{
type: 'storeCategory',
params: ({ event }) => event,
},
],
},
},
},
checkingCategoryState: {
always: [
{
guard: {
type: 'hasCategory',
params: ({ event, context }) => {
return {
expandedCategory: context.expandedCategory,
};
},
},
target: '#hasCategory.fetchingDocuments',
},
{ target: 'idle' },
],
},
hasCategory: {
id: 'hasCategory',
initial: 'fetchingDocuments',
on: {
setExpandedCategory: {
target: 'checkingCategoryState',
actions: [
{
type: 'storeCategory',
params: ({ event }) => event,
},
],
},
},
states: {
fetchingDocuments: {
invoke: {
src: 'getCategoryDocuments',
id: 'fetchCategoryDocumentExamples',
input: ({ context }) => ({
...context.parameters,
categoryTerms: context.expandedCategory!.terms,
}),
onDone: [
{
guard: {
type: 'hasDocumentExamples',
params: ({ event }) => {
return event.output;
},
},
target: 'hasData',
actions: [
{
type: 'storeDocuments',
params: ({ event }) => {
return event.output;
},
},
],
},
{
target: 'noData',
actions: [
{
type: 'storeDocuments',
params: ({ event }) => {
return { categoryDocuments: [] };
},
},
],
},
],
onError: {
target: 'error',
actions: [
{
type: 'storeError',
params: ({ event }) => ({ error: event.error }),
},
],
},
},
},
hasData: {},
noData: {},
error: {},
},
},
},
output: ({ context }) => ({
categoryDocuments: context.categoryDocuments,
}),
});
export const createCategoryDetailsServiceImplementations = ({
search,
}: CategoryDetailsServiceDependencies): MachineImplementationsFrom<
typeof categoryDetailsService
> => ({
actors: {
getCategoryDocuments: getCategoryDocuments({ search }),
},
});

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ISearchGeneric } from '@kbn/search-types';
import { fromPromise } from 'xstate5';
import { lastValueFrom } from 'rxjs';
import { flattenHit } from '@kbn/data-service';
import { LogCategoryDocument, LogCategoryDocumentsParams } from './types';
import { createGetLogCategoryDocumentsRequestParams } from './queries';
export const getCategoryDocuments = ({ search }: { search: ISearchGeneric }) =>
fromPromise<
{
categoryDocuments: LogCategoryDocument[];
},
LogCategoryDocumentsParams
>(
async ({
input: {
index,
endTimestamp,
startTimestamp,
timeField,
messageField,
categoryTerms,
additionalFilters = [],
dataView,
},
signal,
}) => {
const requestParams = createGetLogCategoryDocumentsRequestParams({
index,
timeField,
messageField,
startTimestamp,
endTimestamp,
additionalFilters,
categoryTerms,
});
const { rawResponse } = await lastValueFrom(
search({ params: requestParams }, { abortSignal: signal })
);
const categoryDocuments: LogCategoryDocument[] =
rawResponse.hits?.hits.map((hit) => {
return {
row: {
raw: hit._source,
flattened: flattenHit(hit, dataView),
},
};
}) ?? [];
return {
categoryDocuments,
};
}
);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './category_details_service';

View file

@ -0,0 +1,58 @@
/*
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { createCategoryQuery } from '../categorize_logs_service/queries';
export const createGetLogCategoryDocumentsRequestParams = ({
index,
timeField,
messageField,
startTimestamp,
endTimestamp,
additionalFilters = [],
categoryTerms = '',
documentCount = 20,
}: {
startTimestamp: string;
endTimestamp: string;
index: string;
timeField: string;
messageField: string;
additionalFilters?: QueryDslQueryContainer[];
categoryTerms?: string;
documentCount?: number;
}) => {
return {
index,
size: documentCount,
track_total_hits: false,
sort: [{ [timeField]: { order: 'desc' } }],
query: {
bool: {
filter: [
{
exists: {
field: messageField,
},
},
{
range: {
[timeField]: {
gte: startTimestamp,
lte: endTimestamp,
format: 'strict_date_time',
},
},
},
createCategoryQuery(messageField)(categoryTerms),
...additionalFilters,
],
},
},
};
};

View file

@ -0,0 +1,31 @@
/*
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { ISearchGeneric } from '@kbn/search-types';
import { type DataView } from '@kbn/data-views-plugin/common';
import type { DataTableRecord } from '@kbn/discover-utils';
export interface LogCategoryDocument {
row: Pick<DataTableRecord, 'flattened' | 'raw'>;
}
export interface LogCategoryDetailsParams {
additionalFilters: QueryDslQueryContainer[];
endTimestamp: string;
index: string;
messageField: string;
startTimestamp: string;
timeField: string;
dataView: DataView;
}
export interface CategoryDetailsServiceDependencies {
search: ISearchGeneric;
}
export type LogCategoryDocumentsParams = LogCategoryDetailsParams & { categoryTerms: string };

View file

@ -0,0 +1,12 @@
/*
* 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 { LogCategory } from '../types';
export const getLogCategoryTerms = (logCategory: LogCategory) => {
return logCategory.terms.split(' ');
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { type AbstractDataView } from '@kbn/data-views-plugin/common';
import { type DataViewsContract, type DataView } from '@kbn/data-views-plugin/common';
import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
export type LogsSourceConfiguration =
@ -28,33 +28,68 @@ export interface IndexNameLogsSourceConfiguration {
export interface DataViewLogsSourceConfiguration {
type: 'data_view';
dataView: AbstractDataView;
dataView: DataView;
messageField?: string;
}
export type ResolvedIndexNameLogsSourceConfiguration = IndexNameLogsSourceConfiguration & {
dataView: DataView;
};
export const normalizeLogsSource =
({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) =>
async (logsSource: LogsSourceConfiguration): Promise<IndexNameLogsSourceConfiguration> => {
({
logsDataAccess,
dataViewsService,
}: {
logsDataAccess: LogsDataAccessPluginStart;
dataViewsService: DataViewsContract;
}) =>
async (
logsSource: LogsSourceConfiguration
): Promise<ResolvedIndexNameLogsSourceConfiguration> => {
switch (logsSource.type) {
case 'index_name':
return logsSource;
return {
...logsSource,
dataView: await getDataViewForLogSource(logsSource, dataViewsService),
};
case 'shared_setting':
const logSourcesFromSharedSettings =
await logsDataAccess.services.logSourcesService.getLogSources();
return {
type: 'index_name',
const sharedSettingLogsSource = {
type: 'index_name' as const,
indexName: logSourcesFromSharedSettings
.map((logSource) => logSource.indexPattern)
.join(','),
timestampField: logsSource.timestampField ?? '@timestamp',
messageField: logsSource.messageField ?? 'message',
};
case 'data_view':
return {
type: 'index_name',
...sharedSettingLogsSource,
dataView: await getDataViewForLogSource(sharedSettingLogsSource, dataViewsService),
};
case 'data_view':
const dataViewLogsSource = {
type: 'index_name' as const,
indexName: logsSource.dataView.getIndexPattern(),
timestampField: logsSource.dataView.timeFieldName ?? '@timestamp',
messageField: logsSource.messageField ?? 'message',
};
return {
...dataViewLogsSource,
dataView: logsSource.dataView,
};
}
};
// Ad-hoc Data View
const getDataViewForLogSource = async (
logSourceConfiguration: IndexNameLogsSourceConfiguration,
dataViewsService: DataViewsContract
) => {
const dataView = await dataViewsService.create({
title: logSourceConfiguration.indexName,
timeFieldName: logSourceConfiguration.timestampField,
});
return dataView;
};

View file

@ -31,9 +31,15 @@
"@kbn/ml-random-sampler-utils",
"@kbn/zod",
"@kbn/calculate-auto",
"@kbn/discover-plugin",
"@kbn/es-query",
"@kbn/router-utils",
"@kbn/share-plugin",
"@kbn/field-formats-plugin",
"@kbn/data-service",
"@kbn/discover-utils",
"@kbn/discover-plugin",
"@kbn/unified-data-table",
"@kbn/discover-contextual-components",
"@kbn/core-lifecycle-browser",
]
}

View file

@ -11,6 +11,7 @@
"requiredPlugins": [
"charts",
"data",
"fieldFormats",
"dataViews",
"discoverShared",
"logsDataAccess",
@ -21,7 +22,7 @@
"optionalPlugins": [
"observabilityAIAssistant",
],
"requiredBundles": ["kibanaUtils", "kibanaReact"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedDocViewer"],
"extraPublicDirs": ["common"]
}
}

View file

@ -61,6 +61,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
logsDataAccess,
observabilityAIAssistant,
share,
fieldFormats,
} = plugins;
const logViews = this.logViews.start({
@ -71,11 +72,14 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
});
const LogsOverview = createLogsOverview({
core,
charts,
logsDataAccess,
search: data.search.search,
uiSettings: settings,
share,
dataViews,
fieldFormats,
});
if (!observabilityAIAssistant) {

View file

@ -14,6 +14,7 @@ import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/pub
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { LogsSharedLocators } from '../common/locators';
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
import type { SelfContainedLogsOverview } from './components/logs_overview';
@ -44,6 +45,7 @@ export interface LogsSharedClientStartDeps {
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
share: SharePluginStart;
uiActions: UiActionsStart;
fieldFormats: FieldFormatsStart;
}
export type LogsSharedClientCoreSetup = CoreSetup<

View file

@ -48,5 +48,6 @@
"@kbn/observability-logs-overview",
"@kbn/charts-plugin",
"@kbn/core-ui-settings-common",
"@kbn/field-formats-plugin",
]
}

View file

@ -2480,45 +2480,10 @@
"discover.localMenu.saveTitle": "Enregistrer",
"discover.localMenu.shareSearchDescription": "Partager la recherche",
"discover.localMenu.shareTitle": "Partager",
"discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "Accès à un document dégradé avec le champ {ignoredProperty}",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "Tous les champs de ce document ont été analysés correctement",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "Ce document n'a pas pu être analysé correctement. Tous les champs n'ont pas été remplis correctement",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "Traces d'appel disponibles",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "Traces d'appel indisponibles",
"discover.logs.dataTable.header.actions.tooltip.expand": "Développer les détails du log",
"discover.logs.dataTable.header.actions.tooltip.paragraph": "Les champs fournissant des informations exploitables, comme :",
"discover.logs.dataTable.header.actions.tooltip.stacktrace": "L'accès aux traces d'appel disponibles est basé sur :",
"discover.logs.dataTable.header.content.tooltip.paragraph1": "Affiche le {logLevel} du document et les champs {message}.",
"discover.logs.dataTable.header.content.tooltip.paragraph2": "Lorsque le champ de message est vide, l'une des informations suivantes s'affiche :",
"discover.logs.dataTable.header.popover.actions": "Actions",
"discover.logs.dataTable.header.popover.actions.lowercase": "actions",
"discover.logs.dataTable.header.popover.content": "Contenu",
"discover.logs.dataTable.header.popover.resource": "Ressource",
"discover.logs.dataTable.header.resource.tooltip.paragraph": "Les champs fournissant des informations sur la source du document, comme :",
"discover.logs.flyoutDetail.accordion.title.cloud": "Cloud",
"discover.logs.flyoutDetail.accordion.title.other": "Autre",
"discover.logs.flyoutDetail.accordion.title.serviceInfra": "Service et Infrastructure",
"discover.logs.flyoutDetail.label.cloudAvailabilityZone": "Zone de disponibilité du cloud",
"discover.logs.flyoutDetail.label.cloudInstanceId": "ID d'instance du cloud",
"discover.logs.flyoutDetail.label.cloudProjectId": "ID de projet du cloud",
"discover.logs.flyoutDetail.label.cloudProvider": "Fournisseur cloud",
"discover.logs.flyoutDetail.label.cloudRegion": "Région du cloud",
"discover.logs.flyoutDetail.label.dataset": "Ensemble de données",
"discover.logs.flyoutDetail.label.hostName": "Nom d'hôte",
"discover.logs.flyoutDetail.label.logPathFile": "Fichier de chemin d'accès au log",
"discover.logs.flyoutDetail.label.message": "Répartition du contenu",
"discover.logs.flyoutDetail.label.namespace": "Espace de nom",
"discover.logs.flyoutDetail.label.orchestratorClusterName": "Nom de cluster de l'orchestrateur",
"discover.logs.flyoutDetail.label.orchestratorResourceId": "ID de ressource de l'orchestrateur",
"discover.logs.flyoutDetail.label.service": "Service",
"discover.logs.flyoutDetail.label.shipper": "Agent de transfert",
"discover.logs.flyoutDetail.label.trace": "Trace",
"discover.logs.flyoutDetail.section.showMore": "+ {hiddenCount} autres",
"discover.logs.flyoutDetail.value.hover.copyToClipboard": "Copier dans le presse-papiers",
"discover.logs.flyoutDetail.value.hover.filterFor": "Filtrer sur cette {value}",
"discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "Filtrer sur le champ",
"discover.logs.flyoutDetail.value.hover.filterOut": "Exclure cette {value}",
"discover.logs.flyoutDetail.value.hover.toggleColumn": "Afficher/Masquer la colonne dans le tableau",
"discover.logs.popoverAction.closePopover": "Fermer la fenêtre contextuelle",
"discover.logs.popoverAction.copyValue": "Copier la valeur",
"discover.logs.popoverAction.copyValueAriaText": "Copier la valeur de {fieldName}",

View file

@ -2479,45 +2479,10 @@
"discover.localMenu.saveTitle": "保存",
"discover.localMenu.shareSearchDescription": "検索を共有します",
"discover.localMenu.shareTitle": "共有",
"discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "{ignoredProperty}フィールドの劣化したドキュメントにアクセス",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "このドキュメントのすべてのフィールドは正しく解析されました",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "このドキュメントを正しく解析できませんでした。一部のフィールドが正しく入力されていません",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "スタックトレースがあります",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "スタックトレースがありません",
"discover.logs.dataTable.header.actions.tooltip.expand": "ログの詳細を展開",
"discover.logs.dataTable.header.actions.tooltip.paragraph": "次のようなアクショナブルな情報を提供するフィールド:",
"discover.logs.dataTable.header.actions.tooltip.stacktrace": "次に基づいて使用可能なスタックトレースにアクセス:",
"discover.logs.dataTable.header.content.tooltip.paragraph1": "ドキュメントの{logLevel}と{message}フィールドを表示します。",
"discover.logs.dataTable.header.content.tooltip.paragraph2": "メッセージフィールドが空のときには、次のいずれかが表示されます。",
"discover.logs.dataTable.header.popover.actions": "アクション",
"discover.logs.dataTable.header.popover.actions.lowercase": "アクション",
"discover.logs.dataTable.header.popover.content": "コンテンツ",
"discover.logs.dataTable.header.popover.resource": "リソース",
"discover.logs.dataTable.header.resource.tooltip.paragraph": "次のようなドキュメントのソースに関する情報を提供するフィールド:",
"discover.logs.flyoutDetail.accordion.title.cloud": "クラウド",
"discover.logs.flyoutDetail.accordion.title.other": "Other",
"discover.logs.flyoutDetail.accordion.title.serviceInfra": "サービスとインフラストラクチャー",
"discover.logs.flyoutDetail.label.cloudAvailabilityZone": "クラウドアベイラビリティゾーン",
"discover.logs.flyoutDetail.label.cloudInstanceId": "クラウドインスタンスID",
"discover.logs.flyoutDetail.label.cloudProjectId": "クラウドプロジェクトID",
"discover.logs.flyoutDetail.label.cloudProvider": "クラウドプロバイダー",
"discover.logs.flyoutDetail.label.cloudRegion": "クラウドリージョン",
"discover.logs.flyoutDetail.label.dataset": "データセット",
"discover.logs.flyoutDetail.label.hostName": "ホスト名",
"discover.logs.flyoutDetail.label.logPathFile": "ログパスファイル",
"discover.logs.flyoutDetail.label.message": "コンテンツの内訳",
"discover.logs.flyoutDetail.label.namespace": "名前空間",
"discover.logs.flyoutDetail.label.orchestratorClusterName": "オーケストレータークラスター名",
"discover.logs.flyoutDetail.label.orchestratorResourceId": "オーケストレーターリソースID",
"discover.logs.flyoutDetail.label.service": "サービス",
"discover.logs.flyoutDetail.label.shipper": "シッパー",
"discover.logs.flyoutDetail.label.trace": "トレース",
"discover.logs.flyoutDetail.section.showMore": "+ その他{hiddenCount}件",
"discover.logs.flyoutDetail.value.hover.copyToClipboard": "クリップボードにコピー",
"discover.logs.flyoutDetail.value.hover.filterFor": "この{value}でフィルターを適用",
"discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "フィールド表示のフィルター",
"discover.logs.flyoutDetail.value.hover.filterOut": "この{value}を除外",
"discover.logs.flyoutDetail.value.hover.toggleColumn": "表の列を切り替える",
"discover.logs.popoverAction.closePopover": "ポップオーバーを閉じる",
"discover.logs.popoverAction.copyValue": "値をコピー",
"discover.logs.popoverAction.copyValueAriaText": "{fieldName}の値をコピー",

View file

@ -2481,45 +2481,10 @@
"discover.localMenu.saveTitle": "保存",
"discover.localMenu.shareSearchDescription": "共享搜索",
"discover.localMenu.shareTitle": "共享",
"discover.logs.dataTable.controlColumn.actions.button.degradedDoc": "包含 {ignoredProperty} 字段的已降级文档的访问权限",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocNotPresent": "此文档中的所有字段均进行了正确解析",
"discover.logs.dataTable.controlColumn.actions.button.degradedDocPresent": "无法正确解析此文档。并非所有字段都进行了正确填充",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.available": "堆栈跟踪可用",
"discover.logs.dataTable.controlColumn.actions.button.stacktrace.notAvailable": "堆栈跟踪不可用",
"discover.logs.dataTable.header.actions.tooltip.expand": "展开日志详情",
"discover.logs.dataTable.header.actions.tooltip.paragraph": "提供可操作信息的字段,例如:",
"discover.logs.dataTable.header.actions.tooltip.stacktrace": "基于以下项访问可用堆栈跟踪:",
"discover.logs.dataTable.header.content.tooltip.paragraph1": "显示该文档的 {logLevel} 和 {message} 字段。",
"discover.logs.dataTable.header.content.tooltip.paragraph2": "消息字段为空时,将显示以下项之一:",
"discover.logs.dataTable.header.popover.actions": "操作",
"discover.logs.dataTable.header.popover.actions.lowercase": "操作",
"discover.logs.dataTable.header.popover.content": "内容",
"discover.logs.dataTable.header.popover.resource": "资源",
"discover.logs.dataTable.header.resource.tooltip.paragraph": "提供有关文档来源信息的字段,例如:",
"discover.logs.flyoutDetail.accordion.title.cloud": "云",
"discover.logs.flyoutDetail.accordion.title.other": "其他",
"discover.logs.flyoutDetail.accordion.title.serviceInfra": "服务和基础设施",
"discover.logs.flyoutDetail.label.cloudAvailabilityZone": "云可用区",
"discover.logs.flyoutDetail.label.cloudInstanceId": "云实例 ID",
"discover.logs.flyoutDetail.label.cloudProjectId": "云项目 ID",
"discover.logs.flyoutDetail.label.cloudProvider": "云服务提供商",
"discover.logs.flyoutDetail.label.cloudRegion": "云区域",
"discover.logs.flyoutDetail.label.dataset": "数据集",
"discover.logs.flyoutDetail.label.hostName": "主机名",
"discover.logs.flyoutDetail.label.logPathFile": "日志路径文件",
"discover.logs.flyoutDetail.label.message": "内容细目",
"discover.logs.flyoutDetail.label.namespace": "命名空间",
"discover.logs.flyoutDetail.label.orchestratorClusterName": "Orchestrator 集群名称",
"discover.logs.flyoutDetail.label.orchestratorResourceId": "Orchestrator 资源 ID",
"discover.logs.flyoutDetail.label.service": "服务",
"discover.logs.flyoutDetail.label.shipper": "采集器",
"discover.logs.flyoutDetail.label.trace": "跟踪",
"discover.logs.flyoutDetail.section.showMore": "+ 另外 {hiddenCount} 个",
"discover.logs.flyoutDetail.value.hover.copyToClipboard": "复制到剪贴板",
"discover.logs.flyoutDetail.value.hover.filterFor": "筛留此 {value}",
"discover.logs.flyoutDetail.value.hover.filterForFieldPresent": "筛留存在的字段",
"discover.logs.flyoutDetail.value.hover.filterOut": "筛除此 {value}",
"discover.logs.flyoutDetail.value.hover.toggleColumn": "在表中切换列",
"discover.logs.popoverAction.closePopover": "关闭弹出框",
"discover.logs.popoverAction.copyValue": "复制值",
"discover.logs.popoverAction.copyValueAriaText": "复制 {fieldName} 的值",

View file

@ -4793,6 +4793,10 @@
version "0.0.0"
uid ""
"@kbn/discover-contextual-components@link:packages/kbn-discover-contextual-components":
version "0.0.0"
uid ""
"@kbn/discover-customization-examples-plugin@link:examples/discover_customization_examples":
version "0.0.0"
uid ""