[7.x] [Logs UI] Add customizable columns to source configuration (#34916) (#36067)

Backports the following commits to 7.x:
 - [Logs UI] Add customizable columns to source configuration  (#34916)
This commit is contained in:
Felix Stürmer 2019-05-06 10:39:26 +02:00 committed by GitHub
parent 9a765673a2
commit f0ecf26b28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 3677 additions and 1926 deletions

View file

@ -20,4 +20,33 @@ export const sharedFragments = {
updatedAt
}
`,
InfraLogEntryFields: gql`
fragment InfraLogEntryFields on InfraLogEntry {
gid
key {
time
tiebreaker
}
columns {
... on InfraLogEntryTimestampColumn {
timestamp
}
... on InfraLogEntryMessageColumn {
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
}
... on InfraLogEntryFieldColumn {
field
value
}
}
}
`,
};

View file

@ -53,6 +53,8 @@ export interface InfraSourceConfiguration {
logAlias: string;
/** The field mapping to use for this source */
fields: InfraSourceFields;
/** The columns to use for log display */
logColumns: InfraSourceLogColumn[];
}
/** A mapping of semantic fields to their document counterparts */
export interface InfraSourceFields {
@ -69,6 +71,35 @@ export interface InfraSourceFields {
/** The field to use as a timestamp for metrics and logs */
timestamp: string;
}
/** The built-in timestamp log column */
export interface InfraSourceTimestampLogColumn {
timestampColumn: InfraSourceTimestampLogColumnAttributes;
}
export interface InfraSourceTimestampLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** The built-in message log column */
export interface InfraSourceMessageLogColumn {
messageColumn: InfraSourceMessageLogColumnAttributes;
}
export interface InfraSourceMessageLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** A log column containing a field value */
export interface InfraSourceFieldLogColumn {
fieldColumn: InfraSourceFieldLogColumnAttributes;
}
export interface InfraSourceFieldLogColumnAttributes {
/** A unique id for the column */
id: string;
/** The field name this column refers to */
field: string;
}
/** The status of an infrastructure data source */
export interface InfraSourceStatus {
/** Whether the configured metric alias exists */
@ -143,6 +174,16 @@ export interface InfraLogEntry {
gid: string;
/** The source id */
source: string;
/** The columns used for rendering the log entry */
columns: InfraLogEntryColumn[];
}
/** A special built-in column that contains the log entry's timestamp */
export interface InfraLogEntryTimestampColumn {
/** The timestamp */
timestamp: number;
}
/** A special built-in column that contains the log entry's constructed message */
export interface InfraLogEntryMessageColumn {
/** A list of the formatted log entry segments */
message: InfraLogMessageSegment[];
}
@ -155,11 +196,18 @@ export interface InfraLogMessageFieldSegment {
/** A list of highlighted substrings of the value */
highlights: string[];
}
/** A segment of the log entry message that was derived from a field */
/** A segment of the log entry message that was derived from a string literal */
export interface InfraLogMessageConstantSegment {
/** The segment's message */
constant: string;
}
/** A column that contains the value of a field of the log entry */
export interface InfraLogEntryFieldColumn {
/** The field name of the column */
field: string;
/** The value of the field in the log entry */
value: string;
}
/** A consecutive sequence of log summary buckets */
export interface InfraLogSummaryInterval {
/** The millisecond timestamp corresponding to the start of the interval covered by the summary */
@ -246,20 +294,15 @@ export interface InfraDataPoint {
export interface Mutation {
/** Create a new source of infrastructure data */
createSource: CreateSourceResult;
/** Modify an existing source using the given sequence of update operations */
createSource: UpdateSourceResult;
/** Modify an existing source */
updateSource: UpdateSourceResult;
/** Delete a source of infrastructure data */
deleteSource: DeleteSourceResult;
}
/** The result of a successful source creation */
export interface CreateSourceResult {
/** The source that was created */
source: InfraSource;
}
/** The result of a sequence of source update operations */
/** The result of a successful source update */
export interface UpdateSourceResult {
/** The source after the operations were performed */
/** The source that was updated */
source: InfraSource;
}
/** The result of a source deletion operations */
@ -298,10 +341,10 @@ export interface InfraSnapshotMetricInput {
/** The type of metric */
type: InfraSnapshotMetricType;
}
/** The source to be created */
export interface CreateSourceInput {
/** The properties to update the source with */
export interface UpdateSourceInput {
/** The name of the data source */
name: string;
name?: string | null;
/** A description of the data source */
description?: string | null;
/** The alias to read metric data from */
@ -309,10 +352,12 @@ export interface CreateSourceInput {
/** The alias to read log data from */
logAlias?: string | null;
/** The field mapping to use for this source */
fields?: CreateSourceFieldsInput | null;
fields?: UpdateSourceFieldsInput | null;
/** The log columns to display for this source */
logColumns?: UpdateSourceLogColumnInput[] | null;
}
/** The mapping of semantic fields of the source to be created */
export interface CreateSourceFieldsInput {
export interface UpdateSourceFieldsInput {
/** The field to identify a container by */
container?: string | null;
/** The fields to identify a host by */
@ -324,46 +369,28 @@ export interface CreateSourceFieldsInput {
/** The field to use as a timestamp for metrics and logs */
timestamp?: string | null;
}
/** The update operations to be performed */
export interface UpdateSourceInput {
/** The name update operation to be performed */
setName?: UpdateSourceNameInput | null;
/** The description update operation to be performed */
setDescription?: UpdateSourceDescriptionInput | null;
/** The alias update operation to be performed */
setAliases?: UpdateSourceAliasInput | null;
/** The field update operation to be performed */
setFields?: UpdateSourceFieldsInput | null;
/** One of the log column types to display for this source */
export interface UpdateSourceLogColumnInput {
/** A custom field log column */
fieldColumn?: UpdateSourceFieldLogColumnInput | null;
/** A built-in message log column */
messageColumn?: UpdateSourceMessageLogColumnInput | null;
/** A built-in timestamp log column */
timestampColumn?: UpdateSourceTimestampLogColumnInput | null;
}
/** A name update operation */
export interface UpdateSourceNameInput {
/** The new name to be set */
name: string;
export interface UpdateSourceFieldLogColumnInput {
id: string;
field: string;
}
/** A description update operation */
export interface UpdateSourceDescriptionInput {
/** The new description to be set */
description: string;
export interface UpdateSourceMessageLogColumnInput {
id: string;
}
/** An alias update operation */
export interface UpdateSourceAliasInput {
/** The new log index pattern or alias to bet set */
logAlias?: string | null;
/** The new metric index pattern or alias to bet set */
metricAlias?: string | null;
}
/** A field update operations */
export interface UpdateSourceFieldsInput {
/** The new container field to be set */
container?: string | null;
/** The new host field to be set */
host?: string | null;
/** The new pod field to be set */
pod?: string | null;
/** The new tiebreaker field to be set */
tiebreaker?: string | null;
/** The new timestamp field to be set */
timestamp?: string | null;
export interface UpdateSourceTimestampLogColumnInput {
id: string;
}
// ====================================================
@ -442,13 +469,13 @@ export interface CreateSourceMutationArgs {
/** The id of the source */
id: string;
source: CreateSourceInput;
sourceProperties: UpdateSourceInput;
}
export interface UpdateSourceMutationArgs {
/** The id of the source */
id: string;
/** A sequence of update operations */
changes: UpdateSourceInput[];
/** The properties to update the source with */
sourceProperties: UpdateSourceInput;
}
export interface DeleteSourceMutationArgs {
/** The id of the source */
@ -515,6 +542,18 @@ export enum InfraMetric {
// Unions
// ====================================================
/** All known log column types */
export type InfraSourceLogColumn =
| InfraSourceTimestampLogColumn
| InfraSourceMessageLogColumn
| InfraSourceFieldLogColumn;
/** A column of a log entry */
export type InfraLogEntryColumn =
| InfraLogEntryTimestampColumn
| InfraLogEntryMessageColumn
| InfraLogEntryFieldColumn;
/** A segment of the log entry message */
export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment;
@ -708,7 +747,7 @@ export namespace MetricsQuery {
export namespace CreateSourceConfigurationMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
sourceProperties: UpdateSourceInput;
};
export type Mutation = {
@ -718,7 +757,7 @@ export namespace CreateSourceConfigurationMutation {
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
__typename?: 'UpdateSourceResult';
source: Source;
};
@ -763,7 +802,7 @@ export namespace SourceQuery {
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
sourceProperties: UpdateSourceInput;
};
export type Mutation = {
@ -891,41 +930,7 @@ export namespace LogEntries {
export type End = InfraTimeKeyFields.Fragment;
export type Entries = {
__typename?: 'InfraLogEntry';
gid: string;
key: Key;
message: Message[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Message =
| InfraLogMessageFieldSegmentInlineFragment
| InfraLogMessageConstantSegmentInlineFragment;
export type InfraLogMessageFieldSegmentInlineFragment = {
__typename?: 'InfraLogMessageFieldSegment';
field: string;
value: string;
};
export type InfraLogMessageConstantSegmentInlineFragment = {
__typename?: 'InfraLogMessageConstantSegment';
constant: string;
};
export type Entries = InfraLogEntryFields.Fragment;
}
export namespace SourceConfigurationFields {
@ -941,6 +946,8 @@ export namespace SourceConfigurationFields {
metricAlias: string;
fields: Fields;
logColumns: LogColumns[];
};
export type Fields = {
@ -958,6 +965,49 @@ export namespace SourceConfigurationFields {
timestamp: string;
};
export type LogColumns =
| InfraSourceTimestampLogColumnInlineFragment
| InfraSourceMessageLogColumnInlineFragment
| InfraSourceFieldLogColumnInlineFragment;
export type InfraSourceTimestampLogColumnInlineFragment = {
__typename?: 'InfraSourceTimestampLogColumn';
timestampColumn: TimestampColumn;
};
export type TimestampColumn = {
__typename?: 'InfraSourceTimestampLogColumnAttributes';
id: string;
};
export type InfraSourceMessageLogColumnInlineFragment = {
__typename?: 'InfraSourceMessageLogColumn';
messageColumn: MessageColumn;
};
export type MessageColumn = {
__typename?: 'InfraSourceMessageLogColumnAttributes';
id: string;
};
export type InfraSourceFieldLogColumnInlineFragment = {
__typename?: 'InfraSourceFieldLogColumn';
fieldColumn: FieldColumn;
};
export type FieldColumn = {
__typename?: 'InfraSourceFieldLogColumnAttributes';
id: string;
field: string;
};
}
export namespace SourceStatusFields {
@ -1005,3 +1055,66 @@ export namespace InfraSourceFields {
updatedAt?: number | null;
};
}
export namespace InfraLogEntryFields {
export type Fragment = {
__typename?: 'InfraLogEntry';
gid: string;
key: Key;
columns: Columns[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Columns =
| InfraLogEntryTimestampColumnInlineFragment
| InfraLogEntryMessageColumnInlineFragment
| InfraLogEntryFieldColumnInlineFragment;
export type InfraLogEntryTimestampColumnInlineFragment = {
__typename?: 'InfraLogEntryTimestampColumn';
timestamp: number;
};
export type InfraLogEntryMessageColumnInlineFragment = {
__typename?: 'InfraLogEntryMessageColumn';
message: Message[];
};
export type Message =
| InfraLogMessageFieldSegmentInlineFragment
| InfraLogMessageConstantSegmentInlineFragment;
export type InfraLogMessageFieldSegmentInlineFragment = {
__typename?: 'InfraLogMessageFieldSegment';
field: string;
value: string;
};
export type InfraLogMessageConstantSegmentInlineFragment = {
__typename?: 'InfraLogMessageConstantSegment';
constant: string;
};
export type InfraLogEntryFieldColumnInlineFragment = {
__typename?: 'InfraLogEntryFieldColumn';
field: string;
value: string;
};
}

View file

@ -5,4 +5,3 @@
*/
export * from './log_entry';
export * from './log_entry_list';

View file

@ -5,12 +5,9 @@
*/
import { TimeKey } from '../time';
import { InfraLogEntry } from '../graphql/types';
export interface LogEntry {
gid: string;
origin: LogEntryOrigin;
fields: LogEntryFields;
}
export type LogEntry = InfraLogEntry;
export interface LogEntryOrigin {
id: string;
@ -18,15 +15,7 @@ export interface LogEntryOrigin {
type: string;
}
export interface LogEntryFields extends LogEntryTime {
message: string;
}
export type LogEntryTime = TimeKey;
// export interface LogEntryTime {
// tiebreaker: number;
// time: number;
// }
export interface LogEntryFieldsMapping {
message: string;
@ -34,14 +23,6 @@ export interface LogEntryFieldsMapping {
time: string;
}
export function getLogEntryKey(entry: LogEntry) {
return {
gid: entry.gid,
tiebreaker: entry.fields.tiebreaker,
time: entry.fields.time,
};
}
export function isEqual(time1: LogEntryTime, time2: LogEntryTime) {
return time1.time === time2.time && time1.tiebreaker === time2.tiebreaker;
}

View file

@ -1,43 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
getLogEntryKey,
isEqual,
isLess,
isLessOrEqual,
LogEntry,
LogEntryTime,
} from './log_entry';
export type LogEntryList = LogEntry[];
export function getIndexNearLogEntry(logEntries: LogEntryList, key: LogEntryTime, highest = false) {
let minIndex = 0;
let maxIndex = logEntries.length;
let currentIndex: number;
let currentKey: LogEntryTime;
while (minIndex < maxIndex) {
currentIndex = (minIndex + maxIndex) >>> 1; // eslint-disable-line no-bitwise
currentKey = getLogEntryKey(logEntries[currentIndex]);
if ((highest ? isLessOrEqual : isLess)(currentKey, key)) {
minIndex = currentIndex + 1;
} else {
maxIndex = currentIndex;
}
}
return maxIndex;
}
export function getIndexOfLogEntry(logEntries: LogEntry[], key: LogEntryTime) {
const index = getIndexNearLogEntry(logEntries, key);
const logEntry = logEntries[index];
return logEntry && isEqual(key, getLogEntryKey(logEntry)) ? index : null;
}

View file

@ -1,202 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraSourceConfiguration } from './graphql/types';
import { convertChangeToUpdater } from './source_configuration';
const initialConfiguration: InfraSourceConfiguration = {
name: 'INITIAL_NAME',
description: 'INITIAL_DESCRIPTION',
logAlias: 'INITIAL_LOG_ALIAS',
metricAlias: 'INITIAL_METRIC_ALIAS',
fields: {
container: 'INITIAL_CONTAINER_FIELD',
host: 'INITIAL_HOST_FIELD',
message: ['INITIAL_MESSAGE_FIELD'],
pod: 'INITIAL_POD_FIELD',
tiebreaker: 'INITIAL_TIEBREAKER_FIELD',
timestamp: 'INITIAL_TIMESTAMP_FIELD',
},
};
describe('infrastructure source configuration', () => {
describe('convertChangeToUpdater function', () => {
it('creates a no-op updater for an empty change', () => {
const updateConfiguration = convertChangeToUpdater({});
expect(updateConfiguration(initialConfiguration)).toEqual(initialConfiguration);
});
describe('setName operation', () => {
it('creates a name updater', () => {
const updateConfiguration = convertChangeToUpdater({
setName: {
name: 'CHANGED_NAME',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
name: 'CHANGED_NAME',
});
});
it('creates a name updater even for an empty string', () => {
const updateConfiguration = convertChangeToUpdater({
setName: {
name: '',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
name: '',
});
});
});
describe('setDescription operation', () => {
it('creates a description updater', () => {
const updateConfiguration = convertChangeToUpdater({
setDescription: {
description: '',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
description: '',
});
});
it('creates a description updater even for an empty string', () => {
const updateConfiguration = convertChangeToUpdater({
setDescription: {
description: '',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
description: '',
});
});
});
describe('setAliases operation', () => {
it('creates a partial alias updater for a partial `setAliases` operation', () => {
const updateConfiguration = convertChangeToUpdater({
setAliases: {
logAlias: 'CHANGED_LOG_ALIAS',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
logAlias: 'CHANGED_LOG_ALIAS',
});
});
it('creates a complete alias updater for a complete `setAliases` operation', () => {
const updateConfiguration = convertChangeToUpdater({
setAliases: {
logAlias: 'CHANGED_LOG_ALIAS',
metricAlias: 'CHANGED_METRIC_ALIAS',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
logAlias: 'CHANGED_LOG_ALIAS',
metricAlias: 'CHANGED_METRIC_ALIAS',
});
});
it('creates an alias updater even for empty strings', () => {
const updateConfiguration = convertChangeToUpdater({
setAliases: {
logAlias: '',
metricAlias: '',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
logAlias: '',
metricAlias: '',
});
});
});
describe('setFields operation', () => {
it('creates a partial field updater for a partial `setFields` operation', () => {
const updateConfiguration = convertChangeToUpdater({
setFields: {
host: 'CHANGED_HOST',
timestamp: 'CHANGED_TIMESTAMP',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
fields: {
...initialConfiguration.fields,
host: 'CHANGED_HOST',
timestamp: 'CHANGED_TIMESTAMP',
},
});
});
it('creates a complete field updater for a complete `setFields` operation', () => {
const updateConfiguration = convertChangeToUpdater({
setFields: {
container: 'CHANGED_CONTAINER',
host: 'CHANGED_HOST',
pod: 'CHANGED_POD',
tiebreaker: 'CHANGED_TIEBREAKER',
timestamp: 'CHANGED_TIMESTAMP',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
fields: {
...initialConfiguration.fields,
container: 'CHANGED_CONTAINER',
host: 'CHANGED_HOST',
pod: 'CHANGED_POD',
tiebreaker: 'CHANGED_TIEBREAKER',
timestamp: 'CHANGED_TIMESTAMP',
},
});
});
it('creates a field updater even for empty strings', () => {
const updateConfiguration = convertChangeToUpdater({
setFields: {
container: '',
host: '',
pod: '',
tiebreaker: '',
timestamp: '',
},
});
expect(updateConfiguration(initialConfiguration)).toEqual({
...initialConfiguration,
fields: {
...initialConfiguration.fields,
container: '',
host: '',
pod: '',
tiebreaker: '',
timestamp: '',
},
});
});
});
});
});

View file

@ -1,47 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { InfraSourceConfiguration, UpdateSourceInput } from './graphql/types';
export const convertChangeToUpdater = (change: UpdateSourceInput) => <
C extends InfraSourceConfiguration
>(
configuration: C
): C => {
const updaters: Array<(c: C) => C> = [
c => (change.setName ? { ...c, name: change.setName.name } : c),
c => (change.setDescription ? { ...c, description: change.setDescription.description } : c),
c =>
change.setAliases
? {
...c,
metricAlias: defaultTo(c.metricAlias, change.setAliases.metricAlias),
logAlias: defaultTo(c.logAlias, change.setAliases.logAlias),
}
: c,
c =>
change.setFields
? {
...c,
fields: {
container: defaultTo(c.fields.container, change.setFields.container),
host: defaultTo(c.fields.host, change.setFields.host),
message: c.fields.message,
pod: defaultTo(c.fields.pod, change.setFields.pod),
tiebreaker: defaultTo(c.fields.tiebreaker, change.setFields.tiebreaker),
timestamp: defaultTo(c.fields.timestamp, change.setFields.timestamp),
},
}
: c,
];
return updaters.reduce(
(updatedConfiguration, updater) => updater(updatedConfiguration),
configuration
);
};
const defaultTo = <T>(defaultValue: T, maybeValue: T | undefined | null): T =>
typeof maybeValue === 'undefined' || maybeValue === null ? defaultValue : maybeValue;

View file

@ -5,27 +5,19 @@
*/
import moment from 'moment';
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { useKibanaUiSetting } from '../utils/use_kibana_ui_setting';
interface FormattedTimeProps {
time: number; // Unix time (milliseconds)
fallbackFormat?: string;
}
const getFormattedTime = (
time: FormattedTimeProps['time'],
time: number,
userFormat: string | undefined,
fallbackFormat: string = 'Y-MM-DD HH:mm:ss.SSS'
) => {
return userFormat ? moment(time).format(userFormat) : moment(time).format(fallbackFormat);
};
export const FormattedTime: React.FunctionComponent<FormattedTimeProps> = ({
time,
fallbackFormat,
}) => {
export const useFormattedTime = (time: number, fallbackFormat?: string) => {
const [dateFormat] = useKibanaUiSetting('dateFormat');
const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [
getFormattedTime,
@ -34,5 +26,5 @@ export const FormattedTime: React.FunctionComponent<FormattedTimeProps> = ({
fallbackFormat,
]);
return <span>{formattedTime}</span>;
return formattedTime;
};

View file

@ -6,29 +6,28 @@
import { bisector } from 'd3-array';
import { getLogEntryKey, LogEntry } from '../../../../common/log_entry';
import { SearchResult } from '../../../../common/log_search_result';
import { compareToTimeKey, TimeKey } from '../../../../common/time';
import { LogEntry } from '../../../utils/log_entry';
export type StreamItem = LogEntryStreamItem;
export interface LogEntryStreamItem {
kind: 'logEntry';
logEntry: LogEntry;
searchResult: SearchResult | undefined;
}
export function getStreamItemTimeKey(item: StreamItem) {
switch (item.kind) {
case 'logEntry':
return getLogEntryKey(item.logEntry);
return item.logEntry.key;
}
}
export function getStreamItemId(item: StreamItem) {
const { time, tiebreaker, gid } = getStreamItemTimeKey(item);
return `${time}:${tiebreaker}:${gid}`;
switch (item.kind) {
case 'logEntry':
return `${item.logEntry.key.time}:${item.logEntry.key.tiebreaker}:${item.logEntry.gid}`;
}
}
export function parseStreamItemId(id: string) {

View file

@ -5,44 +5,48 @@
*/
import { darken, transparentize } from 'polished';
import * as React from 'react';
import { css } from 'styled-components';
import React, { memo } from 'react';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { tintOrShade } from '../../../utils/styles';
import { useFormattedTime } from '../../formatted_time';
import { LogTextStreamItemField } from './item_field';
interface LogTextStreamItemDateFieldProps {
children: React.ReactNode;
dataTestSubj?: string;
hasHighlights: boolean;
isHighlighted: boolean;
isHovered: boolean;
scale: TextScale;
isHighlighted: boolean;
time: number;
}
export class LogTextStreamItemDateField extends React.PureComponent<
LogTextStreamItemDateFieldProps,
{}
> {
public render() {
const { children, hasHighlights, isHovered, isHighlighted, scale } = this.props;
export const LogTextStreamItemDateField = memo<LogTextStreamItemDateFieldProps>(
({ dataTestSubj, hasHighlights, isHighlighted, isHovered, scale, time }) => {
const formattedTime = useFormattedTime(time);
return (
<LogTextStreamItemDateFieldWrapper
data-test-subj={dataTestSubj}
hasHighlights={hasHighlights}
isHovered={isHovered}
isHighlighted={isHighlighted}
scale={scale}
>
{children}
{formattedTime}
</LogTextStreamItemDateFieldWrapper>
);
}
}
);
const highlightedFieldStyle = css`
background-color: ${props =>
tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorSecondary, 0.15)};
tintOrShade(
props.theme.eui.euiTextColor as any,
props.theme.eui.euiColorSecondary as any,
0.15
)};
border-color: ${props => props.theme.eui.euiColorSecondary};
`;
@ -67,7 +71,6 @@ const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
color: ${props => props.theme.eui.euiColorDarkShade};
white-space: pre;
padding: 0 ${props => props.theme.eui.paddingSizes.l};
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};

View file

@ -18,5 +18,5 @@ export const LogTextStreamItemField = euiStyled.div.attrs<{
[switchProp.default]: props.theme.eui.euiFontSize,
})};
line-height: ${props => props.theme.eui.euiLineHeight};
padding: 2px ${props => props.theme.eui.euiSize} 2px 0;
padding: 2px ${props => props.theme.eui.paddingSizes.m};
`;

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { darken, transparentize } from 'polished';
import React, { useMemo } from 'react';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { LogTextStreamItemField } from './item_field';
interface LogTextStreamItemFieldFieldProps {
dataTestSubj?: string;
encodedValue: string;
isHighlighted: boolean;
isHovered: boolean;
scale: TextScale;
}
export const LogTextStreamItemFieldField: React.FunctionComponent<
LogTextStreamItemFieldFieldProps
> = ({ dataTestSubj, encodedValue, isHighlighted, isHovered, scale }) => {
const value = useMemo(() => JSON.parse(encodedValue), [encodedValue]);
return (
<LogTextStreamItemFieldFieldWrapper
data-test-subj={dataTestSubj}
isHighlighted={isHighlighted}
isHovered={isHovered}
scale={scale}
>
{value}
</LogTextStreamItemFieldFieldWrapper>
);
};
const hoveredFieldStyle = css`
background-color: ${props =>
props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)};
`;
const LogTextStreamItemFieldFieldWrapper = LogTextStreamItemField.extend.attrs<{
isHighlighted: boolean;
isHovered: boolean;
}>({})`
flex: 1 0 0%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
background-color: ${props => props.theme.eui.euiColorEmptyShade};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
`;

View file

@ -5,70 +5,72 @@
*/
import { darken, transparentize } from 'polished';
import * as React from 'react';
import React, { useMemo } from 'react';
import euiStyled, { css } from '../../../../../../common/eui_styled_components';
// import euiStyled, { css } from '../../../../../../common/eui_styled_components';
import { css } from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { tintOrShade } from '../../../utils/styles';
import { LogTextStreamItemField } from './item_field';
import {
isConstantSegment,
isFieldSegment,
LogEntryMessageSegment,
} from '../../../utils/log_entry';
interface LogTextStreamItemMessageFieldProps {
children: string;
highlights: string[];
dataTestSubj?: string;
segments: LogEntryMessageSegment[];
isHovered: boolean;
isWrapped: boolean;
scale: TextScale;
isHighlighted: boolean;
}
export class LogTextStreamItemMessageField extends React.PureComponent<
LogTextStreamItemMessageFieldProps,
{}
> {
public render() {
const { children, highlights, isHovered, isHighlighted, isWrapped, scale } = this.props;
export const LogTextStreamItemMessageField: React.FunctionComponent<
LogTextStreamItemMessageFieldProps
> = ({ dataTestSubj, isHighlighted, isHovered, isWrapped, scale, segments }) => {
const message = useMemo(() => segments.map(formatMessageSegment).join(''), [segments]);
const hasHighlights = highlights.length > 0;
const content = hasHighlights ? renderHighlightFragments(children, highlights) : children;
return (
<LogTextStreamItemMessageFieldWrapper
hasHighlights={hasHighlights}
isHovered={isHovered}
isHighlighted={isHighlighted}
isWrapped={isWrapped}
scale={scale}
>
{content}
</LogTextStreamItemMessageFieldWrapper>
);
}
}
const renderHighlightFragments = (text: string, highlights: string[]): React.ReactNode[] => {
const renderedHighlights = highlights.reduce(
({ lastFragmentEnd, renderedFragments }, highlight) => {
const fragmentStart = text.indexOf(highlight, lastFragmentEnd);
return {
lastFragmentEnd: fragmentStart + highlight.length,
renderedFragments: [
...renderedFragments,
text.slice(lastFragmentEnd, fragmentStart),
<HighlightSpan key={fragmentStart}>{highlight}</HighlightSpan>,
],
};
},
{
lastFragmentEnd: 0,
renderedFragments: [],
} as {
lastFragmentEnd: number;
renderedFragments: React.ReactNode[];
}
return (
<LogTextStreamItemMessageFieldWrapper
data-test-subj={dataTestSubj}
hasHighlights={false}
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={isWrapped}
scale={scale}
>
{message}
</LogTextStreamItemMessageFieldWrapper>
);
return [...renderedHighlights.renderedFragments, text.slice(renderedHighlights.lastFragmentEnd)];
};
// const renderHighlightFragments = (text: string, highlights: string[]): React.ReactNode[] => {
// const renderedHighlights = highlights.reduce(
// ({ lastFragmentEnd, renderedFragments }, highlight) => {
// const fragmentStart = text.indexOf(highlight, lastFragmentEnd);
// return {
// lastFragmentEnd: fragmentStart + highlight.length,
// renderedFragments: [
// ...renderedFragments,
// text.slice(lastFragmentEnd, fragmentStart),
// <HighlightSpan key={fragmentStart}>{highlight}</HighlightSpan>,
// ],
// };
// },
// {
// lastFragmentEnd: 0,
// renderedFragments: [],
// } as {
// lastFragmentEnd: number;
// renderedFragments: React.ReactNode[];
// }
// );
//
// return [...renderedHighlights.renderedFragments, text.slice(renderedHighlights.lastFragmentEnd)];
// };
const highlightedFieldStyle = css`
background-color: ${props =>
tintOrShade(
@ -102,18 +104,29 @@ const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs
isHighlighted: boolean;
isWrapped?: boolean;
}>({})`
flex-grow: 1;
flex: 5 0 0%
text-overflow: ellipsis;
background-color: ${props => props.theme.eui.euiColorEmptyShade};
padding-left: 0;
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered || props.isHighlighted ? hoveredFieldStyle : '')};
${props => (props.isWrapped ? wrappedFieldStyle : unwrappedFieldStyle)};
`;
const HighlightSpan = euiStyled.span`
display: inline-block;
background-color: ${props => props.theme.eui.euiColorSecondary};
color: ${props => props.theme.eui.euiColorGhost};
font-weight: ${props => props.theme.eui.euiFontWeightMedium};
`;
// const HighlightSpan = euiStyled.span`
// display: inline-block;
// background-color: ${props => props.theme.eui.euiColorSecondary};
// color: ${props => props.theme.eui.euiColorGhost};
// font-weight: ${props => props.theme.eui.euiFontWeightMedium};
// `;
const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string => {
if (isFieldSegment(messageSegment)) {
return messageSegment.value;
} else if (isConstantSegment(messageSegment)) {
return messageSegment.constant;
}
return 'failed to format message';
};

View file

@ -26,7 +26,6 @@ export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
<LogTextStreamLogEntryItemView
boundingBoxRef={ref}
logEntry={item.logEntry}
searchResult={item.searchResult}
scale={scale}
wrap={wrap}
openFlyoutWithItem={openFlyoutWithItem}

View file

@ -5,116 +5,125 @@
*/
import { darken, transparentize } from 'polished';
import * as React from 'react';
import React, { useState, useCallback, Fragment } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
import euiStyled from '../../../../../../common/eui_styled_components';
import { LogEntry } from '../../../../common/log_entry';
import { SearchResult } from '../../../../common/log_search_result';
import {
LogEntry,
isFieldColumn,
isMessageColumn,
isTimestampColumn,
} from '../../../utils/log_entry';
import { TextScale } from '../../../../common/log_text_scale';
import { FormattedTime } from '../../formatted_time';
import { LogTextStreamItemDateField } from './item_date_field';
import { LogTextStreamItemFieldField } from './item_field_field';
import { LogTextStreamItemMessageField } from './item_message_field';
interface LogTextStreamLogEntryItemViewProps {
boundingBoxRef?: React.Ref<Element>;
isHighlighted: boolean;
intl: InjectedIntl;
logEntry: LogEntry;
searchResult?: SearchResult;
openFlyoutWithItem: (id: string) => void;
scale: TextScale;
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
intl: InjectedIntl;
isHighlighted: boolean;
}
interface LogTextStreamLogEntryItemViewState {
isHovered: boolean;
}
export const LogTextStreamLogEntryItemView = injectI18n(
class extends React.PureComponent<
LogTextStreamLogEntryItemViewProps,
LogTextStreamLogEntryItemViewState
> {
public readonly state = {
isHovered: false,
};
({
boundingBoxRef,
isHighlighted,
intl,
logEntry,
openFlyoutWithItem,
scale,
wrap,
}: LogTextStreamLogEntryItemViewProps) => {
const [isHovered, setIsHovered] = useState(false);
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
isHovered: true,
});
};
const setItemIsHovered = useCallback(() => {
setIsHovered(true);
}, []);
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
isHovered: false,
});
};
const setItemIsNotHovered = useCallback(() => {
setIsHovered(false);
}, []);
public handleClick: React.MouseEventHandler<HTMLButtonElement> = () => {
this.props.openFlyoutWithItem(this.props.logEntry.gid);
};
const openFlyout = useCallback(() => openFlyoutWithItem(logEntry.gid), [
openFlyoutWithItem,
logEntry.gid,
]);
public render() {
const {
intl,
boundingBoxRef,
logEntry,
scale,
searchResult,
wrap,
isHighlighted,
} = this.props;
const { isHovered } = this.state;
const viewDetailsLabel = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
defaultMessage: 'View Details',
});
return (
<LogTextStreamLogEntryItemDiv
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<LogTextStreamItemDateField
hasHighlights={!!searchResult}
isHovered={isHovered}
isHighlighted={isHighlighted}
scale={scale}
>
<FormattedTime time={logEntry.fields.time} />
</LogTextStreamItemDateField>
<LogTextStreamIconDiv isHovered={isHovered} isHighlighted={isHighlighted}>
{isHovered ? (
<EuiToolTip content={viewDetailsLabel}>
<EuiButtonIcon
onClick={this.handleClick}
iconType="expand"
aria-label={viewDetailsLabel}
return (
<LogTextStreamLogEntryItemDiv
data-test-subj="streamEntry logTextStreamEntry"
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={setItemIsHovered}
onMouseLeave={setItemIsNotHovered}
>
{logEntry.columns.map((column, columnIndex) => {
if (isTimestampColumn(column)) {
return (
<LogTextStreamItemDateField
dataTestSubj="logColumn timestampLogColumn"
hasHighlights={false}
isHighlighted={isHighlighted}
isHovered={isHovered}
key={`${columnIndex}`}
scale={scale}
time={column.timestamp}
/>
);
} else if (isMessageColumn(column)) {
const viewDetailsLabel = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
defaultMessage: 'View Details',
});
return (
<Fragment key={`${columnIndex}`}>
<LogTextStreamIconDiv isHighlighted={isHighlighted} isHovered={isHovered}>
{isHovered ? (
<EuiToolTip content={viewDetailsLabel}>
<EuiButtonIcon
onClick={openFlyout}
iconType="expand"
aria-label={viewDetailsLabel}
/>
</EuiToolTip>
) : (
<EmptyIcon />
)}
</LogTextStreamIconDiv>
<LogTextStreamItemMessageField
dataTestSubj="logColumn messageLogColumn"
isHighlighted={isHighlighted}
isHovered={isHovered}
isWrapped={wrap}
scale={scale}
segments={column.message}
/>
</EuiToolTip>
) : (
<EmptyIcon />
)}
</LogTextStreamIconDiv>
<LogTextStreamItemMessageField
highlights={searchResult ? searchResult.matches.message || [] : []}
isHovered={isHovered}
isHighlighted={isHighlighted}
isWrapped={wrap}
scale={scale}
>
{logEntry.fields.message}
</LogTextStreamItemMessageField>
</LogTextStreamLogEntryItemDiv>
);
}
</Fragment>
);
} else if (isFieldColumn(column)) {
return (
<LogTextStreamItemFieldField
dataTestSubj={`logColumn fieldLogColumn fieldLogColumn:${column.field}`}
encodedValue={column.value}
isHighlighted={isHighlighted}
isHovered={isHovered}
key={`${columnIndex}`}
scale={scale}
/>
);
}
})}
</LogTextStreamLogEntryItemDiv>
);
}
);

View file

@ -5,11 +5,13 @@
*/
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import * as React from 'react';
import React from 'react';
import euiStyled from '../../../../../../common/eui_styled_components';
import { TextScale } from '../../../../common/log_text_scale';
import { TimeKey } from '../../../../common/time';
import { callWithoutRepeats } from '../../../utils/handlers';
import { AutoSizer } from '../../auto_sizer';
import { NoData } from '../../empty_states';
import { InfraLoadingPanel } from '../../loading';
import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item';
@ -19,8 +21,6 @@ import { MeasurableItemView } from './measurable_item_view';
import { VerticalScrollPanel } from './vertical_scroll_panel';
interface ScrollableLogTextStreamViewProps {
height: number;
width: number;
items: StreamItem[];
scale: TextScale;
wrap: boolean;
@ -92,8 +92,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
public render() {
const {
items,
height,
width,
scale,
wrap,
isReloading,
@ -107,90 +105,95 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
} = this.props;
const { targetId } = this.state;
const hasItems = items.length > 0;
if (isReloading && !hasItems) {
return (
<InfraLoadingPanel
height={height}
width={width}
text={
<FormattedMessage
id="xpack.infra.logs.scrollableLogTextStreamView.loadingEntriesLabel"
defaultMessage="Loading entries"
/>
}
/>
);
} else if (!hasItems) {
return (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageTitle',
defaultMessage: 'There are no log messages to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageDescription',
defaultMessage: 'Try adjusting your filter.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.checkForNewDataButtonLabel',
defaultMessage: 'Check for new data',
})}
onRefetch={this.handleReload}
testString="logsNoDataPrompt"
/>
);
} else {
return (
<VerticalScrollPanel
height={height}
width={width}
onVisibleChildrenChange={this.handleVisibleChildrenChange}
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
>
{registerChild => (
<>
<LogTextStreamLoadingItemView
alignment="bottom"
isLoading={isLoadingMore}
hasMore={hasMoreBeforeStart}
isStreaming={false}
lastStreamingUpdate={null}
return (
<ScrollableLogTextStreamViewWrapper>
{isReloading && !hasItems ? (
<InfraLoadingPanel
width="100%"
height="100%"
text={
<FormattedMessage
id="xpack.infra.logs.scrollableLogTextStreamView.loadingEntriesLabel"
defaultMessage="Loading entries"
/>
{items.map(item => (
<MeasurableItemView
register={registerChild}
registrationKey={getStreamItemId(item)}
key={getStreamItemId(item)}
}
/>
) : !hasItems ? (
<NoData
titleText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageTitle',
defaultMessage: 'There are no log messages to display.',
})}
bodyText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.noLogMessageDescription',
defaultMessage: 'Try adjusting your filter.',
})}
refetchText={intl.formatMessage({
id: 'xpack.infra.logs.emptyView.checkForNewDataButtonLabel',
defaultMessage: 'Check for new data',
})}
onRefetch={this.handleReload}
testString="logsNoDataPrompt"
/>
) : (
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<ScrollPanelSizeProbe innerRef={measureRef}>
<VerticalScrollPanel
height={height}
width={width}
onVisibleChildrenChange={this.handleVisibleChildrenChange}
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
>
{measureRef => (
<LogTextStreamItemView
openFlyoutWithItem={this.handleOpenFlyout}
ref={measureRef}
item={item}
scale={scale}
wrap={wrap}
isHighlighted={
highlightedItem ? item.logEntry.gid === highlightedItem : false
}
/>
{registerChild => (
<>
<LogTextStreamLoadingItemView
alignment="bottom"
isLoading={isLoadingMore}
hasMore={hasMoreBeforeStart}
isStreaming={false}
lastStreamingUpdate={null}
/>
{items.map(item => (
<MeasurableItemView
register={registerChild}
registrationKey={getStreamItemId(item)}
key={getStreamItemId(item)}
>
{itemMeasureRef => (
<LogTextStreamItemView
openFlyoutWithItem={this.handleOpenFlyout}
ref={itemMeasureRef}
item={item}
scale={scale}
wrap={wrap}
isHighlighted={
highlightedItem ? item.logEntry.gid === highlightedItem : false
}
/>
)}
</MeasurableItemView>
))}
<LogTextStreamLoadingItemView
alignment="top"
isLoading={isStreaming || isLoadingMore}
hasMore={hasMoreAfterEnd}
isStreaming={isStreaming}
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
onLoadMore={this.handleLoadNewerItems}
/>
</>
)}
</MeasurableItemView>
))}
<LogTextStreamLoadingItemView
alignment="top"
isLoading={isStreaming || isLoadingMore}
hasMore={hasMoreAfterEnd}
isStreaming={isStreaming}
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
onLoadMore={this.handleLoadNewerItems}
/>
</>
)}
</VerticalScrollPanel>
);
}
</VerticalScrollPanel>
</ScrollPanelSizeProbe>
)}
</AutoSizer>
)}
</ScrollableLogTextStreamViewWrapper>
);
}
private handleOpenFlyout = (id: string) => {
@ -242,3 +245,16 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
}
export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamViewClass);
const ScrollableLogTextStreamViewWrapper = euiStyled.div`
overflow: hidden;
display: flex;
flex: 1 1 0%;
flex-direction: column;
align-items: stretch;
`;
const ScrollPanelSizeProbe = euiStyled.div`
overflow: hidden;
flex: 1 1 0%;
`;

View file

@ -0,0 +1,172 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
import { Option } from '@elastic/eui/src/components/selectable/types';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { LogColumnConfiguration } from '../../utils/source_configuration';
import { euiStyled } from '../../../../../common/eui_styled_components';
interface SelectableColumnOption {
optionProps: Option;
columnConfiguration: LogColumnConfiguration;
}
export const AddLogColumnButtonAndPopover: React.FunctionComponent<{
addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void;
availableFields: string[];
isDisabled?: boolean;
}> = ({ addLogColumn, availableFields, isDisabled }) => {
const [isOpen, openPopover, closePopover] = usePopoverVisibilityState(false);
const availableColumnOptions = useMemo<SelectableColumnOption[]>(
() => [
{
optionProps: {
append: <BuiltinBadge />,
'data-test-subj': 'addTimestampLogColumn',
// this key works around EuiSelectable using a lowercased label as
// key, which leads to conflicts with field names
key: 'timestamp',
label: 'Timestamp',
},
columnConfiguration: {
timestampColumn: {
id: uuidv4(),
},
},
},
{
optionProps: {
'data-test-subj': 'addMessageLogColumn',
append: <BuiltinBadge />,
// this key works around EuiSelectable using a lowercased label as
// key, which leads to conflicts with field names
key: 'message',
label: 'Message',
},
columnConfiguration: {
messageColumn: {
id: uuidv4(),
},
},
},
...availableFields.map<SelectableColumnOption>(field => ({
optionProps: {
'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`,
// this key works around EuiSelectable using a lowercased label as
// key, which leads to conflicts with fields that only differ in the
// case (e.g. the metricbeat mongodb module)
key: `field-${field}`,
label: field,
},
columnConfiguration: {
fieldColumn: {
id: uuidv4(),
field,
},
},
})),
],
[availableFields]
);
const availableOptions = useMemo<Option[]>(
() => availableColumnOptions.map(availableColumnOption => availableColumnOption.optionProps),
[availableColumnOptions]
);
const handleColumnSelection = useCallback(
(selectedOptions: Option[]) => {
closePopover();
const selectedOptionIndex = selectedOptions.findIndex(
selectedOption => selectedOption.checked === 'on'
);
const selectedOption = availableColumnOptions[selectedOptionIndex];
addLogColumn(selectedOption.columnConfiguration);
},
[addLogColumn, availableColumnOptions]
);
return (
<EuiPopover
anchorPosition="downRight"
button={
<EuiButton
data-test-subj="addLogColumnButton"
isDisabled={isDisabled}
iconType="plusInCircle"
onClick={openPopover}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.addLogColumnButtonLabel"
defaultMessage="Add Column"
/>
</EuiButton>
}
closePopover={closePopover}
id="addLogColumn"
isOpen={isOpen}
ownFocus
panelPaddingSize="none"
>
<EuiSelectable
height={600}
listProps={selectableListProps}
onChange={handleColumnSelection}
options={availableOptions}
searchable
searchProps={searchProps}
singleSelection
>
{(list, search) => (
<SelectableContent data-test-subj="addLogColumnPopover">
<EuiPopoverTitle>{search}</EuiPopoverTitle>
{list}
</SelectableContent>
)}
</EuiSelectable>
</EuiPopover>
);
};
const searchProps = {
'data-test-subj': 'fieldSearchInput',
};
const selectableListProps = {
showIcons: false,
};
const usePopoverVisibilityState = (initialState: boolean) => {
const [isOpen, setIsOpen] = useState(initialState);
const closePopover = useCallback(() => setIsOpen(false), []);
const openPopover = useCallback(() => setIsOpen(true), []);
return useMemo<[typeof isOpen, typeof openPopover, typeof closePopover]>(
() => [isOpen, openPopover, closePopover],
[isOpen, openPopover, closePopover]
);
};
const BuiltinBadge: React.FunctionComponent = () => (
<EuiBadge>
<FormattedMessage
id="xpack.infra.sourceConfiguration.builtInColumnBadgeLabel"
defaultMessage="Built-in"
/>
</EuiBadge>
);
const SelectableContent = euiStyled.div`
width: 400px;
`;

View file

@ -5,10 +5,10 @@
*/
import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { InputFieldProps } from './source_configuration_form_state';
import { InputFieldProps } from './input_fields';
interface FieldsConfigurationPanelProps {
containerFieldProps: InputFieldProps;

View file

@ -0,0 +1,185 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { createInputFieldProps, validateInputFieldNotEmpty } from './input_fields';
interface FormState {
name: string;
description: string;
metricAlias: string;
logAlias: string;
containerField: string;
hostField: string;
messageField: string[];
podField: string;
tiebreakerField: string;
timestampField: string;
}
type FormStateChanges = Partial<FormState>;
export const useIndicesConfigurationFormState = ({
initialFormState = defaultFormState,
}: {
initialFormState?: FormState;
}) => {
const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({});
const resetForm = useCallback(() => setFormStateChanges({}), []);
const formState = useMemo(
() => ({
...initialFormState,
...formStateChanges,
}),
[initialFormState, formStateChanges]
);
const nameFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.name),
name: 'name',
onChange: name => setFormStateChanges(changes => ({ ...changes, name })),
value: formState.name,
}),
[formState.name]
);
const logAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.logAlias),
name: 'logAlias',
onChange: logAlias => setFormStateChanges(changes => ({ ...changes, logAlias })),
value: formState.logAlias,
}),
[formState.logAlias]
);
const metricAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.metricAlias),
name: 'metricAlias',
onChange: metricAlias => setFormStateChanges(changes => ({ ...changes, metricAlias })),
value: formState.metricAlias,
}),
[formState.metricAlias]
);
const containerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.containerField),
name: `containerField`,
onChange: containerField =>
setFormStateChanges(changes => ({ ...changes, containerField })),
value: formState.containerField,
}),
[formState.containerField]
);
const hostFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.hostField),
name: `hostField`,
onChange: hostField => setFormStateChanges(changes => ({ ...changes, hostField })),
value: formState.hostField,
}),
[formState.hostField]
);
const podFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.podField),
name: `podField`,
onChange: podField => setFormStateChanges(changes => ({ ...changes, podField })),
value: formState.podField,
}),
[formState.podField]
);
const tiebreakerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.tiebreakerField),
name: `tiebreakerField`,
onChange: tiebreakerField =>
setFormStateChanges(changes => ({ ...changes, tiebreakerField })),
value: formState.tiebreakerField,
}),
[formState.tiebreakerField]
);
const timestampFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.timestampField),
name: `timestampField`,
onChange: timestampField =>
setFormStateChanges(changes => ({ ...changes, timestampField })),
value: formState.timestampField,
}),
[formState.timestampField]
);
const fieldProps = useMemo(
() => ({
name: nameFieldProps,
logAlias: logAliasFieldProps,
metricAlias: metricAliasFieldProps,
containerField: containerFieldFieldProps,
hostField: hostFieldFieldProps,
podField: podFieldFieldProps,
tiebreakerField: tiebreakerFieldFieldProps,
timestampField: timestampFieldFieldProps,
}),
[
nameFieldProps,
logAliasFieldProps,
metricAliasFieldProps,
containerFieldFieldProps,
hostFieldFieldProps,
podFieldFieldProps,
tiebreakerFieldFieldProps,
timestampFieldFieldProps,
]
);
const errors = useMemo(
() =>
Object.values(fieldProps).reduce<ReactNode[]>(
(accumulatedErrors, { error }) => [...accumulatedErrors, ...error],
[]
),
[fieldProps]
);
const isFormValid = useMemo(() => errors.length <= 0, [errors]);
const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]);
return {
errors,
fieldProps,
formState,
formStateChanges,
isFormDirty,
isFormValid,
resetForm,
};
};
const defaultFormState: FormState = {
name: '',
description: '',
logAlias: '',
metricAlias: '',
containerField: '',
hostField: '',
messageField: [],
podField: '',
tiebreakerField: '',
timestampField: '',
};

View file

@ -5,10 +5,10 @@
*/
import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { InputFieldProps } from './source_configuration_form_state';
import { InputFieldProps } from './input_fields';
interface IndicesConfigurationPanelProps {
isLoading: boolean;

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
export interface InputFieldProps<
Value extends string = string,
FieldElement extends HTMLInputElement = HTMLInputElement
> {
error: React.ReactNode[];
isInvalid: boolean;
name: string;
onChange?: React.ChangeEventHandler<FieldElement>;
value?: Value;
}
export type FieldErrorMessage = string | JSX.Element;
export const createInputFieldProps = <
Value extends string = string,
FieldElement extends HTMLInputElement = HTMLInputElement
>({
errors,
name,
onChange,
value,
}: {
errors: FieldErrorMessage[];
name: string;
onChange: (newValue: string) => void;
value: Value;
}): InputFieldProps<Value, FieldElement> => ({
error: errors,
isInvalid: errors.length > 0,
name,
onChange: (evt: React.ChangeEvent<FieldElement>) => onChange(evt.currentTarget.value),
value,
});
export const validateInputFieldNotEmpty = (value: string) =>
value === ''
? [
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldEmptyErrorMessage"
defaultMessage="The field must not be empty"
/>,
]
: [];

View file

@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
LogColumnConfiguration,
isTimestampLogColumnConfiguration,
isMessageLogColumnConfiguration,
TimestampLogColumnConfiguration,
MessageLogColumnConfiguration,
FieldLogColumnConfiguration,
} from '../../utils/source_configuration';
export interface TimestampLogColumnConfigurationProps {
logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn'];
remove: () => void;
type: 'timestamp';
}
export interface MessageLogColumnConfigurationProps {
logColumnConfiguration: MessageLogColumnConfiguration['messageColumn'];
remove: () => void;
type: 'message';
}
export interface FieldLogColumnConfigurationProps {
logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn'];
remove: () => void;
type: 'field';
}
export type LogColumnConfigurationProps =
| TimestampLogColumnConfigurationProps
| MessageLogColumnConfigurationProps
| FieldLogColumnConfigurationProps;
interface FormState {
logColumns: LogColumnConfiguration[];
}
type FormStateChanges = Partial<FormState>;
export const useLogColumnsConfigurationFormState = ({
initialFormState = defaultFormState,
}: {
initialFormState?: FormState;
}) => {
const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({});
const resetForm = useCallback(() => setFormStateChanges({}), []);
const formState = useMemo(
() => ({
...initialFormState,
...formStateChanges,
}),
[initialFormState, formStateChanges]
);
const logColumnConfigurationProps = useMemo<LogColumnConfigurationProps[]>(
() =>
formState.logColumns.map(
(logColumn): LogColumnConfigurationProps => {
const remove = () =>
setFormStateChanges(changes => ({
...changes,
logColumns: formState.logColumns.filter(item => item !== logColumn),
}));
if (isTimestampLogColumnConfiguration(logColumn)) {
return {
logColumnConfiguration: logColumn.timestampColumn,
remove,
type: 'timestamp',
};
} else if (isMessageLogColumnConfiguration(logColumn)) {
return {
logColumnConfiguration: logColumn.messageColumn,
remove,
type: 'message',
};
} else {
return {
logColumnConfiguration: logColumn.fieldColumn,
remove,
type: 'field',
};
}
}
),
[formState.logColumns]
);
const addLogColumn = useCallback(
(logColumnConfiguration: LogColumnConfiguration) =>
setFormStateChanges(changes => ({
...changes,
logColumns: [...formState.logColumns, logColumnConfiguration],
})),
[formState.logColumns]
);
const errors = useMemo(
() =>
logColumnConfigurationProps.length <= 0
? [
<FormattedMessage
id="xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage"
defaultMessage="The log column list must not be empty."
/>,
]
: [],
[logColumnConfigurationProps]
);
const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]);
const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]);
return {
addLogColumn,
errors,
logColumnConfigurationProps,
formState,
formStateChanges,
isFormDirty,
isFormValid,
resetForm,
};
};
const defaultFormState: FormState = {
logColumns: [],
};

View file

@ -0,0 +1,217 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiButtonIcon,
EuiEmptyPrompt,
EuiForm,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { AddLogColumnButtonAndPopover } from './add_log_column_popover';
import {
FieldLogColumnConfigurationProps,
LogColumnConfigurationProps,
} from './log_columns_configuration_form_state';
import { LogColumnConfiguration } from '../../utils/source_configuration';
interface LogColumnsConfigurationPanelProps {
availableFields: string[];
isLoading: boolean;
logColumnConfiguration: LogColumnConfigurationProps[];
addLogColumn: (logColumn: LogColumnConfiguration) => void;
}
export const LogColumnsConfigurationPanel: React.FunctionComponent<
LogColumnsConfigurationPanelProps
> = ({ addLogColumn, availableFields, isLoading, logColumnConfiguration }) => (
<EuiForm>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle">
<h3>
<FormattedMessage
id="xpack.infra.sourceConfiguration.logColumnsSectionTitle"
defaultMessage="Columns"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddLogColumnButtonAndPopover
addLogColumn={addLogColumn}
availableFields={availableFields}
isDisabled={isLoading}
/>
</EuiFlexItem>
</EuiFlexGroup>
{logColumnConfiguration.length > 0 ? (
logColumnConfiguration.map((column, index) => (
<LogColumnConfigurationPanel
logColumnConfigurationProps={column}
key={`logColumnConfigurationPanel-${index}`}
/>
))
) : (
<LogColumnConfigurationEmptyPrompt />
)}
</EuiForm>
);
interface LogColumnConfigurationPanelProps {
logColumnConfigurationProps: LogColumnConfigurationProps;
}
const LogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({
logColumnConfigurationProps,
}) => (
<>
<EuiSpacer size="m" />
{logColumnConfigurationProps.type === 'timestamp' ? (
<TimestampLogColumnConfigurationPanel
logColumnConfigurationProps={logColumnConfigurationProps}
/>
) : logColumnConfigurationProps.type === 'message' ? (
<MessageLogColumnConfigurationPanel
logColumnConfigurationProps={logColumnConfigurationProps}
/>
) : (
<FieldLogColumnConfigurationPanel logColumnConfigurationProps={logColumnConfigurationProps} />
)}
</>
);
const TimestampLogColumnConfigurationPanel: React.FunctionComponent<
LogColumnConfigurationPanelProps
> = ({ logColumnConfigurationProps }) => (
<ExplainedLogColumnConfigurationPanel
fieldName="Timestamp"
helpText={
<FormattedMessage
tagName="span"
id="xpack.infra.sourceConfiguration.timestampLogColumnDescription"
defaultMessage="This built-in field shows the log entry's time as determined by the {timestampSetting} field setting."
values={{
timestampSetting: <code>timestamp</code>,
}}
/>
}
removeColumn={logColumnConfigurationProps.remove}
/>
);
const MessageLogColumnConfigurationPanel: React.FunctionComponent<
LogColumnConfigurationPanelProps
> = ({ logColumnConfigurationProps }) => (
<ExplainedLogColumnConfigurationPanel
fieldName="Message"
helpText={
<FormattedMessage
tagName="span"
id="xpack.infra.sourceConfiguration.messageLogColumnDescription"
defaultMessage="This built-in field shows the log entry message as derived from the document fields."
/>
}
removeColumn={logColumnConfigurationProps.remove}
/>
);
const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
logColumnConfigurationProps: FieldLogColumnConfigurationProps;
}> = ({
logColumnConfigurationProps: {
logColumnConfiguration: { field },
remove,
},
}) => (
<EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldLogColumnTitle"
defaultMessage="Field"
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<code>{field}</code>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RemoveLogColumnButton onClick={remove} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
fieldName: React.ReactNode;
helpText: React.ReactNode;
removeColumn: () => void;
}> = ({ fieldName, helpText, removeColumn }) => (
<EuiPanel
data-test-subj={`logColumnPanel builtInLogColumnPanel builtInLogColumnPanel:${fieldName}`}
>
<EuiFlexGroup>
<EuiFlexItem grow={1}>{fieldName}</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiText size="s" color="subdued">
{helpText}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RemoveLogColumnButton onClick={removeColumn} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
const RemoveLogColumnButton = injectI18n<{
onClick?: () => void;
}>(({ intl, onClick }) => {
const removeColumnLabel = intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel',
defaultMessage: 'Remove this column',
});
return (
<EuiButtonIcon
aria-label={removeColumnLabel}
color="danger"
data-test-subj="removeLogColumnButton"
iconType="trash"
onClick={onClick}
title={removeColumnLabel}
/>
);
});
const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => (
<EuiEmptyPrompt
iconType="list"
title={
<h2>
<FormattedMessage
id="xpack.infra.sourceConfiguration.noLogColumnsTitle"
defaultMessage="No columns"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.infra.sourceConfiguration.noLogColumnsDescription"
defaultMessage="Add a column to this list using the button above."
/>
</p>
}
/>
);

View file

@ -5,10 +5,10 @@
*/
import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { InputFieldProps } from './source_configuration_form_state';
import { InputFieldProps } from './input_fields';
interface NameConfigurationPanelProps {
isLoading: boolean;
@ -22,7 +22,7 @@ export const NameConfigurationPanel = ({
nameFieldProps,
}: NameConfigurationPanelProps) => (
<EuiForm>
<EuiTitle size="s">
<EuiTitle size="s" data-test-subj="sourceConfigurationNameSectionTitle">
<h3>
<FormattedMessage
id="xpack.infra.sourceConfiguration.nameSectionTitle"

View file

@ -7,215 +7,240 @@
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTabbedContent,
EuiTabbedContentTab,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react';
import React, { useCallback, useContext, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { Source } from '../../containers/source';
import { FieldsConfigurationPanel } from './fields_configuration_panel';
import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
const noop = () => undefined;
interface SourceConfigurationFlyoutProps {
intl: InjectedIntl;
shouldAllowEdit: boolean;
}
export const SourceConfigurationFlyout: React.FunctionComponent<
SourceConfigurationFlyoutProps
> = props => {
const { shouldAllowEdit } = props;
const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context);
export const SourceConfigurationFlyout = injectI18n(
({ intl, shouldAllowEdit }: SourceConfigurationFlyoutProps) => {
const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context);
const {
createSourceConfiguration,
source,
sourceExists,
isLoading,
updateSourceConfiguration,
} = useContext(Source.Context);
const {
createSourceConfiguration,
source,
sourceExists,
isLoading,
updateSourceConfiguration,
} = useContext(Source.Context);
const availableFields = useMemo(
() => (source && source.status ? source.status.indexFields.map(field => field.name) : []),
[source]
);
const configuration = source && source.configuration;
const initialFormState = useMemo(
() =>
configuration
? {
name: configuration.name,
description: configuration.description,
fields: {
container: configuration.fields.container,
host: configuration.fields.host,
message: configuration.fields.message,
pod: configuration.fields.pod,
tiebreaker: configuration.fields.tiebreaker,
timestamp: configuration.fields.timestamp,
},
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
}
: defaultFormState,
[configuration]
);
const {
addLogColumn,
indicesConfigurationProps,
logColumnConfigurationProps,
errors,
resetForm,
isFormDirty,
isFormValid,
formState,
formStateChanges,
} = useSourceConfigurationFormState(source && source.configuration);
const {
fieldProps,
formState,
isFormDirty,
isFormValid,
resetForm,
updates,
} = useSourceConfigurationFormState({
initialFormState,
});
const persistUpdates = useCallback(
async () => {
if (sourceExists) {
await updateSourceConfiguration(formStateChanges);
} else {
await createSourceConfiguration(formState);
}
resetForm();
},
[
sourceExists,
updateSourceConfiguration,
createSourceConfiguration,
resetForm,
formState,
formStateChanges,
]
);
const persistUpdates = useCallback(
async () => {
if (sourceExists) {
await updateSourceConfiguration(updates);
} else {
await createSourceConfiguration(formState);
}
resetForm();
},
[sourceExists, updateSourceConfiguration, createSourceConfiguration, resetForm, formState]
);
if (!isVisible || !source || !source.configuration) {
return null;
}
if (!isVisible || !configuration) {
return null;
}
const tabs: EuiTabbedContentTab[] = [
{
id: 'indicesAndFieldsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle',
defaultMessage: 'Indices and fields',
}),
content: (
<>
<EuiSpacer />
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!shouldAllowEdit}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!shouldAllowEdit}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={indicesConfigurationProps.containerField}
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!shouldAllowEdit}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
</>
),
},
{
id: 'logsTab',
name: intl.formatMessage({
id: 'xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle',
defaultMessage: 'Log Columns',
}),
content: (
<>
<EuiSpacer />
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
/>
</>
),
},
];
return (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
data-test-subj="sourceConfigurationFlyout"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
{shouldAllowEdit ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
) : (
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationReadonlyTitle"
defaultMessage="View source configuration"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<NameConfigurationPanel
isLoading={isLoading}
readOnly={!shouldAllowEdit}
nameFieldProps={fieldProps.name}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
readOnly={!shouldAllowEdit}
logAliasFieldProps={fieldProps.logAlias}
metricAliasFieldProps={fieldProps.metricAlias}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={fieldProps.containerField}
hostFieldProps={fieldProps.hostField}
isLoading={isLoading}
readOnly={!shouldAllowEdit}
podFieldProps={fieldProps.podField}
tiebreakerFieldProps={fieldProps.tiebreakerField}
timestampFieldProps={fieldProps.timestampField}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{!isFormDirty ? (
<EuiButtonEmpty
data-test-subj="closeFlyoutButton"
iconType="cross"
isDisabled={isLoading}
onClick={() => hide()}
>
return (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
data-test-subj="sourceConfigurationFlyout"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
{shouldAllowEdit ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
data-test-subj="discardAndCloseFlyoutButton"
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
hide();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem />
{shouldAllowEdit && (
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
data-test-subj="updateSourceConfigurationButton"
color="primary"
isDisabled={!isFormDirty || !isFormValid}
fill
onClick={persistUpdates}
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationReadonlyTitle"
defaultMessage="View source configuration"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTabbedContent tabs={tabs} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
{errors.length > 0 ? (
<>
<EuiCallOut color="danger">
<ul>
{errors.map((error, errorIndex) => (
<li key={errorIndex}>{error}</li>
))}
</ul>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null}
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{!isFormDirty ? (
<EuiButtonEmpty
data-test-subj="closeFlyoutButton"
iconType="cross"
isDisabled={isLoading}
onClick={() => hide()}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButton>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
data-test-subj="discardAndCloseFlyoutButton"
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
hide();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
const defaultFormState = {
name: '',
description: '',
fields: {
container: '',
host: '',
message: [],
pod: '',
tiebreaker: '',
timestamp: '',
},
logAlias: '',
metricAlias: '',
};
<EuiFlexItem />
{shouldAllowEdit && (
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
data-test-subj="updateSourceConfigurationButton"
color="primary"
isDisabled={!isFormDirty || !isFormValid}
fill
onClick={persistUpdates}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
/>
</EuiButton>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
);

View file

@ -4,251 +4,117 @@
* you may not use this file except in compliance with the Elastic License.
*/
import mergeAll from 'lodash/fp/mergeAll';
import React, { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { convertChangeToUpdater } from '../../../common/source_configuration';
import { UpdateSourceInput } from '../../graphql/types';
import { useIndicesConfigurationFormState } from './indices_configuration_form_state';
import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state';
import { SourceConfiguration } from '../../utils/source_configuration';
export interface InputFieldProps<
Value extends string = string,
FieldElement extends HTMLInputElement = HTMLInputElement
> {
error: React.ReactNode[];
isInvalid: boolean;
name: string;
onChange?: React.ChangeEventHandler<FieldElement>;
value?: Value;
}
export const useSourceConfigurationFormState = (configuration?: SourceConfiguration) => {
const indicesConfigurationFormState = useIndicesConfigurationFormState({
initialFormState: useMemo(
() =>
configuration
? {
name: configuration.name,
description: configuration.description,
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
containerField: configuration.fields.container,
hostField: configuration.fields.host,
messageField: configuration.fields.message,
podField: configuration.fields.pod,
tiebreakerField: configuration.fields.tiebreaker,
timestampField: configuration.fields.timestamp,
}
: undefined,
[configuration]
),
});
type FieldErrorMessage = string | JSX.Element;
const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({
initialFormState: useMemo(
() =>
configuration
? {
logColumns: configuration.logColumns,
}
: undefined,
[configuration]
),
});
interface FormState {
name: string;
description: string;
metricAlias: string;
logAlias: string;
fields: {
container: string;
host: string;
message: string[];
pod: string;
tiebreaker: string;
timestamp: string;
};
}
export const useSourceConfigurationFormState = ({
initialFormState,
}: {
initialFormState: FormState;
}) => {
const [updates, setUpdates] = useState<UpdateSourceInput[]>([]);
const addOrCombineLastUpdate = useCallback(
(newUpdate: UpdateSourceInput) =>
setUpdates(currentUpdates => [
...currentUpdates.slice(0, -1),
...maybeCombineUpdates(currentUpdates[currentUpdates.length - 1], newUpdate),
]),
[setUpdates]
const errors = useMemo(
() => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors],
[indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors]
);
const resetForm = useCallback(() => setUpdates([]), []);
const formState = useMemo(
() =>
updates
.map(convertChangeToUpdater)
.reduce((state, updater) => updater(state), initialFormState),
[updates, initialFormState]
const resetForm = useCallback(
() => {
indicesConfigurationFormState.resetForm();
logColumnsConfigurationFormState.resetForm();
},
[indicesConfigurationFormState.resetForm, logColumnsConfigurationFormState.formState]
);
const nameFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.name),
name: 'name',
onChange: name => addOrCombineLastUpdate({ setName: { name } }),
value: formState.name,
}),
[formState.name, addOrCombineLastUpdate]
);
const logAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.logAlias),
name: 'logAlias',
onChange: logAlias => addOrCombineLastUpdate({ setAliases: { logAlias } }),
value: formState.logAlias,
}),
[formState.logAlias, addOrCombineLastUpdate]
);
const metricAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.metricAlias),
name: 'metricAlias',
onChange: metricAlias => addOrCombineLastUpdate({ setAliases: { metricAlias } }),
value: formState.metricAlias,
}),
[formState.metricAlias, addOrCombineLastUpdate]
);
const containerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.container),
name: `containerField`,
onChange: value => addOrCombineLastUpdate({ setFields: { container: value } }),
value: formState.fields.container,
}),
[formState.fields.container, addOrCombineLastUpdate]
);
const hostFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.host),
name: `hostField`,
onChange: value => addOrCombineLastUpdate({ setFields: { host: value } }),
value: formState.fields.host,
}),
[formState.fields.host, addOrCombineLastUpdate]
);
const podFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.pod),
name: `podField`,
onChange: value => addOrCombineLastUpdate({ setFields: { pod: value } }),
value: formState.fields.pod,
}),
[formState.fields.pod, addOrCombineLastUpdate]
);
const tiebreakerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.tiebreaker),
name: `tiebreakerField`,
onChange: value => addOrCombineLastUpdate({ setFields: { tiebreaker: value } }),
value: formState.fields.tiebreaker,
}),
[formState.fields.tiebreaker, addOrCombineLastUpdate]
);
const timestampFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.timestamp),
name: `timestampField`,
onChange: value => addOrCombineLastUpdate({ setFields: { timestamp: value } }),
value: formState.fields.timestamp,
}),
[formState.fields.timestamp, addOrCombineLastUpdate]
);
const fieldProps = useMemo(
() => ({
name: nameFieldProps,
logAlias: logAliasFieldProps,
metricAlias: metricAliasFieldProps,
containerField: containerFieldFieldProps,
hostField: hostFieldFieldProps,
podField: podFieldFieldProps,
tiebreakerField: tiebreakerFieldFieldProps,
timestampField: timestampFieldFieldProps,
}),
[
nameFieldProps,
logAliasFieldProps,
metricAliasFieldProps,
containerFieldFieldProps,
hostFieldFieldProps,
podFieldFieldProps,
tiebreakerFieldFieldProps,
timestampFieldFieldProps,
]
const isFormDirty = useMemo(
() => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty,
[indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty]
);
const isFormValid = useMemo(
() => Object.values(fieldProps).every(({ error }) => error.length <= 0),
[fieldProps]
() => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid,
[indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid]
);
const isFormDirty = useMemo(() => updates.length > 0, [updates]);
const formState = useMemo(
() => ({
name: indicesConfigurationFormState.formState.name,
description: indicesConfigurationFormState.formState.description,
logAlias: indicesConfigurationFormState.formState.logAlias,
metricAlias: indicesConfigurationFormState.formState.metricAlias,
fields: {
container: indicesConfigurationFormState.formState.containerField,
host: indicesConfigurationFormState.formState.hostField,
pod: indicesConfigurationFormState.formState.podField,
tiebreaker: indicesConfigurationFormState.formState.tiebreakerField,
timestamp: indicesConfigurationFormState.formState.timestampField,
},
logColumns: logColumnsConfigurationFormState.formState.logColumns,
}),
[indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState]
);
const formStateChanges = useMemo(
() => ({
name: indicesConfigurationFormState.formStateChanges.name,
description: indicesConfigurationFormState.formStateChanges.description,
logAlias: indicesConfigurationFormState.formStateChanges.logAlias,
metricAlias: indicesConfigurationFormState.formStateChanges.metricAlias,
fields: {
container: indicesConfigurationFormState.formStateChanges.containerField,
host: indicesConfigurationFormState.formStateChanges.hostField,
pod: indicesConfigurationFormState.formStateChanges.podField,
tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField,
timestamp: indicesConfigurationFormState.formStateChanges.timestampField,
},
logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns,
}),
[
indicesConfigurationFormState.formStateChanges,
logColumnsConfigurationFormState.formStateChanges,
]
);
return {
fieldProps,
addLogColumn: logColumnsConfigurationFormState.addLogColumn,
errors,
formState,
formStateChanges,
isFormDirty,
isFormValid,
indicesConfigurationProps: indicesConfigurationFormState.fieldProps,
logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps,
resetForm,
updates,
};
};
const createInputFieldProps = <
Value extends string = string,
FieldElement extends HTMLInputElement = HTMLInputElement
>({
errors,
name,
onChange,
value,
}: {
errors: FieldErrorMessage[];
name: string;
onChange: (newValue: string) => void;
value: Value;
}): InputFieldProps<Value, FieldElement> => ({
error: errors,
isInvalid: errors.length > 0,
name,
onChange: (evt: React.ChangeEvent<FieldElement>) => onChange(evt.currentTarget.value),
value,
});
const validateInputFieldNotEmpty = (value: string) =>
value === ''
? [
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldEmptyErrorMessage"
defaultMessage="The field must not be empty"
/>,
]
: [];
/**
* Tries to combine the given updates by naively checking whether they can be
* merged into one update.
*
* This is only judged to be the case when all of the following conditions are
* met:
*
* 1. The update only contains one operation.
* 2. The operation is the same on in both updates.
* 3. The operation is known to be safe to combine.
*/
const maybeCombineUpdates = (
firstUpdate: UpdateSourceInput | undefined,
secondUpdate: UpdateSourceInput
): UpdateSourceInput[] => {
if (!firstUpdate) {
return [secondUpdate];
}
const firstKeys = Object.keys(firstUpdate);
const secondKeys = Object.keys(secondUpdate);
const isSingleOperation = firstKeys.length === secondKeys.length && firstKeys.length === 1;
const isSameOperation = firstKeys[0] === secondKeys[0];
// to guard against future operations, which might not be safe to merge naively
const isMergeableOperation = mergeableOperations.indexOf(firstKeys[0]) > -1;
if (isSingleOperation && isSameOperation && isMergeableOperation) {
return [mergeAll([firstUpdate, secondUpdate])];
}
return [firstUpdate, secondUpdate];
};
const mergeableOperations = ['setName', 'setDescription', 'setAliases', 'setFields'];

View file

@ -7,9 +7,8 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { SearchResult } from '../../../common/log_search_result';
import { logEntriesActions, logEntriesSelectors, logPositionSelectors, State } from '../../store';
import { LogEntry, LogEntryMessageSegment } from '../../utils/log_entry';
import { LogEntry } from '../../utils/log_entry';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
@ -49,27 +48,7 @@ const selectItems = createSelector(
)
);
const createLogEntryStreamItem = (logEntry: LogEntry, searchResult?: SearchResult) => ({
const createLogEntryStreamItem = (logEntry: LogEntry) => ({
kind: 'logEntry' as 'logEntry',
logEntry: {
gid: logEntry.gid,
origin: {
id: logEntry.gid,
index: '',
type: '',
},
fields: {
time: logEntry.key.time,
tiebreaker: logEntry.key.tiebreaker,
message: logEntry.message.map(formatMessageSegment).join(''),
},
},
searchResult,
logEntry,
});
const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string =>
messageSegment.__typename === 'InfraLogMessageFieldSegment'
? messageSegment.value
: messageSegment.__typename === 'InfraLogMessageConstantSegment'
? messageSegment.constant
: 'failed to format message';

View file

@ -15,9 +15,9 @@ import {
export const createSourceMutation = gql`
mutation CreateSourceConfigurationMutation(
$sourceId: ID!
$sourceConfiguration: CreateSourceInput!
$sourceProperties: UpdateSourceInput!
) {
createSource(id: $sourceId, source: $sourceConfiguration) {
createSource(id: $sourceId, sourceProperties: $sourceProperties) {
source {
...InfraSourceFields
configuration {

View file

@ -9,7 +9,6 @@ import { useEffect, useMemo, useState } from 'react';
import {
CreateSourceConfigurationMutation,
CreateSourceInput,
SourceQuery,
UpdateSourceInput,
UpdateSourceMutation,
@ -51,7 +50,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
const [createSourceConfigurationRequest, createSourceConfiguration] = useTrackedPromise(
{
createPromise: async (newSourceConfiguration: CreateSourceInput) => {
createPromise: async (sourceProperties: UpdateSourceInput) => {
if (!apolloClient) {
throw new DependencyError(
'Failed to create source configuration: No apollo client available.'
@ -66,21 +65,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
fetchPolicy: 'no-cache',
variables: {
sourceId,
sourceConfiguration: {
name: newSourceConfiguration.name,
description: newSourceConfiguration.description,
metricAlias: newSourceConfiguration.metricAlias,
logAlias: newSourceConfiguration.logAlias,
fields: newSourceConfiguration.fields
? {
container: newSourceConfiguration.fields.container,
host: newSourceConfiguration.fields.host,
pod: newSourceConfiguration.fields.pod,
tiebreaker: newSourceConfiguration.fields.tiebreaker,
timestamp: newSourceConfiguration.fields.timestamp,
}
: undefined,
},
sourceProperties,
},
});
},
@ -95,7 +80,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
{
createPromise: async (changes: UpdateSourceInput[]) => {
createPromise: async (sourceProperties: UpdateSourceInput) => {
if (!apolloClient) {
throw new DependencyError(
'Failed to update source configuration: No apollo client available.'
@ -110,7 +95,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
fetchPolicy: 'no-cache',
variables: {
sourceId,
changes,
sourceProperties,
},
});
},

View file

@ -20,6 +20,24 @@ export const sourceConfigurationFieldsFragment = gql`
tiebreaker
timestamp
}
logColumns {
... on InfraSourceTimestampLogColumn {
timestampColumn {
id
}
}
... on InfraSourceMessageLogColumn {
messageColumn {
id
}
}
... on InfraSourceFieldLogColumn {
fieldColumn {
id
field
}
}
}
}
`;

View file

@ -13,8 +13,8 @@ import {
} from './source_fields_fragment.gql_query';
export const updateSourceMutation = gql`
mutation UpdateSourceMutation($sourceId: ID = "default", $changes: [UpdateSourceInput!]!) {
updateSource(id: $sourceId, changes: $changes) {
mutation UpdateSourceMutation($sourceId: ID = "default", $sourceProperties: UpdateSourceInput!) {
updateSource(id: $sourceId, sourceProperties: $sourceProperties) {
source {
...InfraSourceFields
configuration {

View file

@ -7,14 +7,14 @@
import React, { useContext } from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { CreateSourceInput, SourceQuery, UpdateSourceInput } from '../../graphql/types';
import { SourceQuery, UpdateSourceInput } from '../../graphql/types';
import { RendererFunction } from '../../utils/typed_react';
import { Source } from '../source';
interface WithSourceProps {
children: RendererFunction<{
configuration?: SourceQuery.Query['source']['configuration'];
create: (sourceConfiguration: CreateSourceInput) => Promise<any> | undefined;
create: (sourceProperties: UpdateSourceInput) => Promise<any> | undefined;
derivedIndexPattern: StaticIndexPattern;
exists?: boolean;
hasFailed: boolean;
@ -25,7 +25,7 @@ interface WithSourceProps {
metricAlias?: string;
metricIndicesExist?: boolean;
sourceId: string;
update: (changes: UpdateSourceInput[]) => Promise<any> | undefined;
update: (sourceProperties: UpdateSourceInput) => Promise<any> | undefined;
version?: string;
}>;
}

View file

@ -514,6 +514,26 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "logColumns",
"description": "The columns to use for log display",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "UNION", "name": "InfraSourceLogColumn", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -612,6 +632,182 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "UNION",
"name": "InfraSourceLogColumn",
"description": "All known log column types",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{ "kind": "OBJECT", "name": "InfraSourceTimestampLogColumn", "ofType": null },
{ "kind": "OBJECT", "name": "InfraSourceMessageLogColumn", "ofType": null },
{ "kind": "OBJECT", "name": "InfraSourceFieldLogColumn", "ofType": null }
]
},
{
"kind": "OBJECT",
"name": "InfraSourceTimestampLogColumn",
"description": "The built-in timestamp log column",
"fields": [
{
"name": "timestampColumn",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "InfraSourceTimestampLogColumnAttributes",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceTimestampLogColumnAttributes",
"description": "",
"fields": [
{
"name": "id",
"description": "A unique id for the column",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceMessageLogColumn",
"description": "The built-in message log column",
"fields": [
{
"name": "messageColumn",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "InfraSourceMessageLogColumnAttributes",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceMessageLogColumnAttributes",
"description": "",
"fields": [
{
"name": "id",
"description": "A unique id for the column",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceFieldLogColumn",
"description": "A log column containing a field value",
"fields": [
{
"name": "fieldColumn",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "InfraSourceFieldLogColumnAttributes",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceFieldLogColumnAttributes",
"description": "",
"fields": [
{
"name": "id",
"description": "A unique id for the column",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "field",
"description": "The field name this column refers to",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraSourceStatus",
@ -1134,6 +1330,74 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "columns",
"description": "The columns used for rendering the log entry",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "UNION", "name": "InfraLogEntryColumn", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "UNION",
"name": "InfraLogEntryColumn",
"description": "A column of a log entry",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{ "kind": "OBJECT", "name": "InfraLogEntryTimestampColumn", "ofType": null },
{ "kind": "OBJECT", "name": "InfraLogEntryMessageColumn", "ofType": null },
{ "kind": "OBJECT", "name": "InfraLogEntryFieldColumn", "ofType": null }
]
},
{
"kind": "OBJECT",
"name": "InfraLogEntryTimestampColumn",
"description": "A special built-in column that contains the log entry's timestamp",
"fields": [
{
"name": "timestamp",
"description": "The timestamp",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraLogEntryMessageColumn",
"description": "A special built-in column that contains the log entry's constructed message",
"fields": [
{
"name": "message",
"description": "A list of the formatted log entry segments",
@ -1231,7 +1495,7 @@
{
"kind": "OBJECT",
"name": "InfraLogMessageConstantSegment",
"description": "A segment of the log entry message that was derived from a field",
"description": "A segment of the log entry message that was derived from a string literal",
"fields": [
{
"name": "constant",
@ -1251,6 +1515,41 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraLogEntryFieldColumn",
"description": "A column that contains the value of a field of the log entry",
"fields": [
{
"name": "field",
"description": "The field name of the column",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": "The value of the field in the log entry",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraLogSummaryInterval",
@ -2067,12 +2366,12 @@
"defaultValue": null
},
{
"name": "source",
"name": "sourceProperties",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "INPUT_OBJECT", "name": "CreateSourceInput", "ofType": null }
"ofType": { "kind": "INPUT_OBJECT", "name": "UpdateSourceInput", "ofType": null }
},
"defaultValue": null
}
@ -2080,14 +2379,14 @@
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "CreateSourceResult", "ofType": null }
"ofType": { "kind": "OBJECT", "name": "UpdateSourceResult", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateSource",
"description": "Modify an existing source using the given sequence of update operations",
"description": "Modify an existing source",
"args": [
{
"name": "id",
@ -2100,24 +2399,12 @@
"defaultValue": null
},
{
"name": "changes",
"description": "A sequence of update operations",
"name": "sourceProperties",
"description": "The properties to update the source with",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateSourceInput",
"ofType": null
}
}
}
"ofType": { "kind": "INPUT_OBJECT", "name": "UpdateSourceInput", "ofType": null }
},
"defaultValue": null
}
@ -2161,18 +2448,14 @@
},
{
"kind": "INPUT_OBJECT",
"name": "CreateSourceInput",
"description": "The source to be created",
"name": "UpdateSourceInput",
"description": "The properties to update the source with",
"fields": null,
"inputFields": [
{
"name": "name",
"description": "The name of the data source",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
@ -2196,7 +2479,25 @@
{
"name": "fields",
"description": "The field mapping to use for this source",
"type": { "kind": "INPUT_OBJECT", "name": "CreateSourceFieldsInput", "ofType": null },
"type": { "kind": "INPUT_OBJECT", "name": "UpdateSourceFieldsInput", "ofType": null },
"defaultValue": null
},
{
"name": "logColumns",
"description": "The log columns to display for this source",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateSourceLogColumnInput",
"ofType": null
}
}
},
"defaultValue": null
}
],
@ -2206,7 +2507,7 @@
},
{
"kind": "INPUT_OBJECT",
"name": "CreateSourceFieldsInput",
"name": "UpdateSourceFieldsInput",
"description": "The mapping of semantic fields of the source to be created",
"fields": null,
"inputFields": [
@ -2245,61 +2546,40 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CreateSourceResult",
"description": "The result of a successful source creation",
"fields": [
{
"name": "source",
"description": "The source that was created",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "InfraSource", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSourceInput",
"description": "The update operations to be performed",
"name": "UpdateSourceLogColumnInput",
"description": "One of the log column types to display for this source",
"fields": null,
"inputFields": [
{
"name": "setName",
"description": "The name update operation to be performed",
"type": { "kind": "INPUT_OBJECT", "name": "UpdateSourceNameInput", "ofType": null },
"defaultValue": null
},
{
"name": "setDescription",
"description": "The description update operation to be performed",
"name": "fieldColumn",
"description": "A custom field log column",
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdateSourceDescriptionInput",
"name": "UpdateSourceFieldLogColumnInput",
"ofType": null
},
"defaultValue": null
},
{
"name": "setAliases",
"description": "The alias update operation to be performed",
"type": { "kind": "INPUT_OBJECT", "name": "UpdateSourceAliasInput", "ofType": null },
"name": "messageColumn",
"description": "A built-in message log column",
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdateSourceMessageLogColumnInput",
"ofType": null
},
"defaultValue": null
},
{
"name": "setFields",
"description": "The field update operation to be performed",
"type": { "kind": "INPUT_OBJECT", "name": "UpdateSourceFieldsInput", "ofType": null },
"name": "timestampColumn",
"description": "A built-in timestamp log column",
"type": {
"kind": "INPUT_OBJECT",
"name": "UpdateSourceTimestampLogColumnInput",
"ofType": null
},
"defaultValue": null
}
],
@ -2309,13 +2589,23 @@
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSourceNameInput",
"description": "A name update operation",
"name": "UpdateSourceFieldLogColumnInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "name",
"description": "The new name to be set",
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"defaultValue": null
},
{
"name": "field",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
@ -2330,17 +2620,17 @@
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSourceDescriptionInput",
"description": "A description update operation",
"name": "UpdateSourceMessageLogColumnInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "description",
"description": "The new description to be set",
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"defaultValue": null
}
@ -2351,61 +2641,18 @@
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSourceAliasInput",
"description": "An alias update operation",
"name": "UpdateSourceTimestampLogColumnInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "logAlias",
"description": "The new log index pattern or alias to bet set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "metricAlias",
"description": "The new metric index pattern or alias to bet set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSourceFieldsInput",
"description": "A field update operations",
"fields": null,
"inputFields": [
{
"name": "container",
"description": "The new container field to be set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "host",
"description": "The new host field to be set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "pod",
"description": "The new pod field to be set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "tiebreaker",
"description": "The new tiebreaker field to be set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
{
"name": "timestamp",
"description": "The new timestamp field to be set",
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"defaultValue": null
}
],
@ -2416,11 +2663,11 @@
{
"kind": "OBJECT",
"name": "UpdateSourceResult",
"description": "The result of a sequence of source update operations",
"description": "The result of a successful source update",
"fields": [
{
"name": "source",
"description": "The source after the operations were performed",
"description": "The source that was updated",
"args": [],
"type": {
"kind": "NON_NULL",

View file

@ -53,6 +53,8 @@ export interface InfraSourceConfiguration {
logAlias: string;
/** The field mapping to use for this source */
fields: InfraSourceFields;
/** The columns to use for log display */
logColumns: InfraSourceLogColumn[];
}
/** A mapping of semantic fields to their document counterparts */
export interface InfraSourceFields {
@ -69,6 +71,35 @@ export interface InfraSourceFields {
/** The field to use as a timestamp for metrics and logs */
timestamp: string;
}
/** The built-in timestamp log column */
export interface InfraSourceTimestampLogColumn {
timestampColumn: InfraSourceTimestampLogColumnAttributes;
}
export interface InfraSourceTimestampLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** The built-in message log column */
export interface InfraSourceMessageLogColumn {
messageColumn: InfraSourceMessageLogColumnAttributes;
}
export interface InfraSourceMessageLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** A log column containing a field value */
export interface InfraSourceFieldLogColumn {
fieldColumn: InfraSourceFieldLogColumnAttributes;
}
export interface InfraSourceFieldLogColumnAttributes {
/** A unique id for the column */
id: string;
/** The field name this column refers to */
field: string;
}
/** The status of an infrastructure data source */
export interface InfraSourceStatus {
/** Whether the configured metric alias exists */
@ -143,6 +174,16 @@ export interface InfraLogEntry {
gid: string;
/** The source id */
source: string;
/** The columns used for rendering the log entry */
columns: InfraLogEntryColumn[];
}
/** A special built-in column that contains the log entry's timestamp */
export interface InfraLogEntryTimestampColumn {
/** The timestamp */
timestamp: number;
}
/** A special built-in column that contains the log entry's constructed message */
export interface InfraLogEntryMessageColumn {
/** A list of the formatted log entry segments */
message: InfraLogMessageSegment[];
}
@ -155,11 +196,18 @@ export interface InfraLogMessageFieldSegment {
/** A list of highlighted substrings of the value */
highlights: string[];
}
/** A segment of the log entry message that was derived from a field */
/** A segment of the log entry message that was derived from a string literal */
export interface InfraLogMessageConstantSegment {
/** The segment's message */
constant: string;
}
/** A column that contains the value of a field of the log entry */
export interface InfraLogEntryFieldColumn {
/** The field name of the column */
field: string;
/** The value of the field in the log entry */
value: string;
}
/** A consecutive sequence of log summary buckets */
export interface InfraLogSummaryInterval {
/** The millisecond timestamp corresponding to the start of the interval covered by the summary */
@ -246,20 +294,15 @@ export interface InfraDataPoint {
export interface Mutation {
/** Create a new source of infrastructure data */
createSource: CreateSourceResult;
/** Modify an existing source using the given sequence of update operations */
createSource: UpdateSourceResult;
/** Modify an existing source */
updateSource: UpdateSourceResult;
/** Delete a source of infrastructure data */
deleteSource: DeleteSourceResult;
}
/** The result of a successful source creation */
export interface CreateSourceResult {
/** The source that was created */
source: InfraSource;
}
/** The result of a sequence of source update operations */
/** The result of a successful source update */
export interface UpdateSourceResult {
/** The source after the operations were performed */
/** The source that was updated */
source: InfraSource;
}
/** The result of a source deletion operations */
@ -298,10 +341,10 @@ export interface InfraSnapshotMetricInput {
/** The type of metric */
type: InfraSnapshotMetricType;
}
/** The source to be created */
export interface CreateSourceInput {
/** The properties to update the source with */
export interface UpdateSourceInput {
/** The name of the data source */
name: string;
name?: string | null;
/** A description of the data source */
description?: string | null;
/** The alias to read metric data from */
@ -309,10 +352,12 @@ export interface CreateSourceInput {
/** The alias to read log data from */
logAlias?: string | null;
/** The field mapping to use for this source */
fields?: CreateSourceFieldsInput | null;
fields?: UpdateSourceFieldsInput | null;
/** The log columns to display for this source */
logColumns?: UpdateSourceLogColumnInput[] | null;
}
/** The mapping of semantic fields of the source to be created */
export interface CreateSourceFieldsInput {
export interface UpdateSourceFieldsInput {
/** The field to identify a container by */
container?: string | null;
/** The fields to identify a host by */
@ -324,46 +369,28 @@ export interface CreateSourceFieldsInput {
/** The field to use as a timestamp for metrics and logs */
timestamp?: string | null;
}
/** The update operations to be performed */
export interface UpdateSourceInput {
/** The name update operation to be performed */
setName?: UpdateSourceNameInput | null;
/** The description update operation to be performed */
setDescription?: UpdateSourceDescriptionInput | null;
/** The alias update operation to be performed */
setAliases?: UpdateSourceAliasInput | null;
/** The field update operation to be performed */
setFields?: UpdateSourceFieldsInput | null;
/** One of the log column types to display for this source */
export interface UpdateSourceLogColumnInput {
/** A custom field log column */
fieldColumn?: UpdateSourceFieldLogColumnInput | null;
/** A built-in message log column */
messageColumn?: UpdateSourceMessageLogColumnInput | null;
/** A built-in timestamp log column */
timestampColumn?: UpdateSourceTimestampLogColumnInput | null;
}
/** A name update operation */
export interface UpdateSourceNameInput {
/** The new name to be set */
name: string;
export interface UpdateSourceFieldLogColumnInput {
id: string;
field: string;
}
/** A description update operation */
export interface UpdateSourceDescriptionInput {
/** The new description to be set */
description: string;
export interface UpdateSourceMessageLogColumnInput {
id: string;
}
/** An alias update operation */
export interface UpdateSourceAliasInput {
/** The new log index pattern or alias to bet set */
logAlias?: string | null;
/** The new metric index pattern or alias to bet set */
metricAlias?: string | null;
}
/** A field update operations */
export interface UpdateSourceFieldsInput {
/** The new container field to be set */
container?: string | null;
/** The new host field to be set */
host?: string | null;
/** The new pod field to be set */
pod?: string | null;
/** The new tiebreaker field to be set */
tiebreaker?: string | null;
/** The new timestamp field to be set */
timestamp?: string | null;
export interface UpdateSourceTimestampLogColumnInput {
id: string;
}
// ====================================================
@ -442,13 +469,13 @@ export interface CreateSourceMutationArgs {
/** The id of the source */
id: string;
source: CreateSourceInput;
sourceProperties: UpdateSourceInput;
}
export interface UpdateSourceMutationArgs {
/** The id of the source */
id: string;
/** A sequence of update operations */
changes: UpdateSourceInput[];
/** The properties to update the source with */
sourceProperties: UpdateSourceInput;
}
export interface DeleteSourceMutationArgs {
/** The id of the source */
@ -515,6 +542,18 @@ export enum InfraMetric {
// Unions
// ====================================================
/** All known log column types */
export type InfraSourceLogColumn =
| InfraSourceTimestampLogColumn
| InfraSourceMessageLogColumn
| InfraSourceFieldLogColumn;
/** A column of a log entry */
export type InfraLogEntryColumn =
| InfraLogEntryTimestampColumn
| InfraLogEntryMessageColumn
| InfraLogEntryFieldColumn;
/** A segment of the log entry message */
export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment;
@ -708,7 +747,7 @@ export namespace MetricsQuery {
export namespace CreateSourceConfigurationMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
sourceProperties: UpdateSourceInput;
};
export type Mutation = {
@ -718,7 +757,7 @@ export namespace CreateSourceConfigurationMutation {
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
__typename?: 'UpdateSourceResult';
source: Source;
};
@ -763,7 +802,7 @@ export namespace SourceQuery {
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
sourceProperties: UpdateSourceInput;
};
export type Mutation = {
@ -891,41 +930,7 @@ export namespace LogEntries {
export type End = InfraTimeKeyFields.Fragment;
export type Entries = {
__typename?: 'InfraLogEntry';
gid: string;
key: Key;
message: Message[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Message =
| InfraLogMessageFieldSegmentInlineFragment
| InfraLogMessageConstantSegmentInlineFragment;
export type InfraLogMessageFieldSegmentInlineFragment = {
__typename?: 'InfraLogMessageFieldSegment';
field: string;
value: string;
};
export type InfraLogMessageConstantSegmentInlineFragment = {
__typename?: 'InfraLogMessageConstantSegment';
constant: string;
};
export type Entries = InfraLogEntryFields.Fragment;
}
export namespace SourceConfigurationFields {
@ -941,6 +946,8 @@ export namespace SourceConfigurationFields {
metricAlias: string;
fields: Fields;
logColumns: LogColumns[];
};
export type Fields = {
@ -958,6 +965,49 @@ export namespace SourceConfigurationFields {
timestamp: string;
};
export type LogColumns =
| InfraSourceTimestampLogColumnInlineFragment
| InfraSourceMessageLogColumnInlineFragment
| InfraSourceFieldLogColumnInlineFragment;
export type InfraSourceTimestampLogColumnInlineFragment = {
__typename?: 'InfraSourceTimestampLogColumn';
timestampColumn: TimestampColumn;
};
export type TimestampColumn = {
__typename?: 'InfraSourceTimestampLogColumnAttributes';
id: string;
};
export type InfraSourceMessageLogColumnInlineFragment = {
__typename?: 'InfraSourceMessageLogColumn';
messageColumn: MessageColumn;
};
export type MessageColumn = {
__typename?: 'InfraSourceMessageLogColumnAttributes';
id: string;
};
export type InfraSourceFieldLogColumnInlineFragment = {
__typename?: 'InfraSourceFieldLogColumn';
fieldColumn: FieldColumn;
};
export type FieldColumn = {
__typename?: 'InfraSourceFieldLogColumnAttributes';
id: string;
field: string;
};
}
export namespace SourceStatusFields {
@ -1005,3 +1055,66 @@ export namespace InfraSourceFields {
updatedAt?: number | null;
};
}
export namespace InfraLogEntryFields {
export type Fragment = {
__typename?: 'InfraLogEntry';
gid: string;
key: Key;
columns: Columns[];
};
export type Key = {
__typename?: 'InfraTimeKey';
time: number;
tiebreaker: number;
};
export type Columns =
| InfraLogEntryTimestampColumnInlineFragment
| InfraLogEntryMessageColumnInlineFragment
| InfraLogEntryFieldColumnInlineFragment;
export type InfraLogEntryTimestampColumnInlineFragment = {
__typename?: 'InfraLogEntryTimestampColumn';
timestamp: number;
};
export type InfraLogEntryMessageColumnInlineFragment = {
__typename?: 'InfraLogEntryMessageColumn';
message: Message[];
};
export type Message =
| InfraLogMessageFieldSegmentInlineFragment
| InfraLogMessageConstantSegmentInlineFragment;
export type InfraLogMessageFieldSegmentInlineFragment = {
__typename?: 'InfraLogMessageFieldSegment';
field: string;
value: string;
};
export type InfraLogMessageConstantSegmentInlineFragment = {
__typename?: 'InfraLogMessageConstantSegment';
constant: string;
};
export type InfraLogEntryFieldColumnInlineFragment = {
__typename?: 'InfraLogEntryFieldColumn';
field: string;
value: string;
};
}

View file

@ -25,6 +25,7 @@ import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kiba
export function compose(): InfraFrontendLibs {
const cache = new InMemoryCache({
addTypename: false,
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),

View file

@ -30,7 +30,7 @@ import { Source } from '../../containers/source';
import { LogsToolbar } from './page_toolbar';
export const LogsPageLogsContent: React.FunctionComponent = () => {
const { derivedIndexPattern } = useContext(Source.Context);
const { derivedIndexPattern, sourceId, version } = useContext(Source.Context);
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
const {
setFlyoutVisibility,
@ -50,10 +50,10 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogPosition>
{({ jumpToTargetPosition, stopLiveStreaming }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) =>
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<WithLogPosition>
{({ jumpToTargetPosition, stopLiveStreaming }) =>
flyoutVisible ? (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
@ -68,98 +68,74 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
/>
) : null
}
</WithLogFilter>
)}
</WithLogPosition>
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ filterQuery }) => (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<LogPageEventStreamColumn innerRef={measureRef}>
<WithLogPosition>
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
height={height}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
width={width}
wrap={textWrap}
setFlyoutItem={setFlyoutId}
setFlyoutVisibility={setFlyoutVisibility}
highlightedItem={surroundingLogsId ? surroundingLogsId : null}
/>
)}
</WithStreamItems>
)}
</WithLogPosition>
</LogPageEventStreamColumn>
)}
</AutoSizer>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => (
<LogMinimap
height={height}
width={width}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithLogPosition>
)}
</WithSummary>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
</WithLogPosition>
)}
</WithLogFilter>
<PageContent key={`${sourceId}-${version}`}>
<WithLogPosition>
{({ isAutoReloading, jumpToTargetPosition, reportVisiblePositions, targetPosition }) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
wrap={textWrap}
setFlyoutItem={setFlyoutId}
setFlyoutVisibility={setFlyoutVisibility}
highlightedItem={surroundingLogsId ? surroundingLogsId : null}
/>
)}
</WithStreamItems>
)}
</WithLogPosition>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => (
<LogMinimap
height={height}
width={width}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithLogPosition>
)}
</WithSummary>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
</>
);
};
const LogPageEventStreamColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const LogPageMinimapColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;

View file

@ -33,24 +33,12 @@ export const logEntriesQuery = gql`
hasMoreBefore
hasMoreAfter
entries {
gid
key {
time
tiebreaker
}
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;

View file

@ -7,11 +7,18 @@
import { bisector } from 'd3-array';
import { compareToTimeKey, getIndexAtTimeKey, TimeKey } from '../../../common/time';
import { LogEntries as LogEntriesQuery } from '../../graphql/types';
import { InfraLogEntryFields } from '../../graphql/types';
export type LogEntry = LogEntriesQuery.Entries;
export type LogEntry = InfraLogEntryFields.Fragment;
export type LogEntryMessageSegment = LogEntriesQuery.Message;
export type LogEntryColumn = InfraLogEntryFields.Columns;
export type LogEntryMessageColumn = InfraLogEntryFields.InfraLogEntryMessageColumnInlineFragment;
export type LogEntryTimestampColumn = InfraLogEntryFields.InfraLogEntryTimestampColumnInlineFragment;
export type LogEntryFieldColumn = InfraLogEntryFields.InfraLogEntryFieldColumnInlineFragment;
export type LogEntryMessageSegment = InfraLogEntryFields.Message;
export type LogEntryConstantMessageSegment = InfraLogEntryFields.InfraLogMessageConstantSegmentInlineFragment;
export type LogEntryFieldMessageSegment = InfraLogEntryFields.InfraLogMessageFieldSegmentInlineFragment;
export const getLogEntryKey = (entry: LogEntry) => entry.key;
@ -26,3 +33,20 @@ export const getLogEntryAtTime = (entries: LogEntry[], time: TimeKey) => {
return entryIndex !== null ? entries[entryIndex] : null;
};
export const isTimestampColumn = (column: LogEntryColumn): column is LogEntryTimestampColumn =>
'timestamp' in column;
export const isMessageColumn = (column: LogEntryColumn): column is LogEntryMessageColumn =>
'message' in column;
export const isFieldColumn = (column: LogEntryColumn): column is LogEntryFieldColumn =>
'field' in column;
export const isConstantSegment = (
segment: LogEntryMessageSegment
): segment is LogEntryConstantMessageSegment => 'constant' in segment;
export const isFieldSegment = (
segment: LogEntryMessageSegment
): segment is LogEntryFieldMessageSegment => 'field' in segment && 'value' in segment;

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SourceConfigurationFields } from '../graphql/types';
export type SourceConfiguration = SourceConfigurationFields.Fragment;
export type LogColumnConfiguration = SourceConfigurationFields.LogColumns;
export type FieldLogColumnConfiguration = SourceConfigurationFields.InfraSourceFieldLogColumnInlineFragment;
export type MessageLogColumnConfiguration = SourceConfigurationFields.InfraSourceMessageLogColumnInlineFragment;
export type TimestampLogColumnConfiguration = SourceConfigurationFields.InfraSourceTimestampLogColumnInlineFragment;
export const isFieldLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is FieldLogColumnConfiguration => 'fieldColumn' in logColumnConfiguration;
export const isMessageLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is MessageLogColumnConfiguration =>
'messageColumn' in logColumnConfiguration;
export const isTimestampLogColumnConfiguration = (
logColumnConfiguration: LogColumnConfiguration
): logColumnConfiguration is TimestampLogColumnConfiguration =>
'timestampColumn' in logColumnConfiguration;

View file

@ -4,13 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { failure } from 'io-ts/lib/PathReporter';
import {
InfraLogEntryColumn,
InfraLogEntryFieldColumn,
InfraLogEntryMessageColumn,
InfraLogEntryTimestampColumn,
InfraLogMessageConstantSegment,
InfraLogMessageFieldSegment,
InfraLogMessageSegment,
InfraSourceResolvers,
} from '../../graphql/types';
import { InfraLogEntriesDomain } from '../../lib/domains/log_entries_domain';
import { SourceConfigurationRuntimeType } from '../../lib/sources';
import { UsageCollector } from '../../usage/usage_collector';
import { parseFilterQuery } from '../../utils/serialized_query';
import { ChildResolverOf, InfraResolverOf } from '../../utils/typed_resolvers';
@ -45,6 +52,15 @@ export const createLogEntriesResolvers = (libs: {
logSummaryBetween: InfraSourceLogSummaryBetweenResolver;
logItem: InfraSourceLogItem;
};
InfraLogEntryColumn: {
__resolveType(
logEntryColumn: InfraLogEntryColumn
):
| 'InfraLogEntryTimestampColumn'
| 'InfraLogEntryMessageColumn'
| 'InfraLogEntryFieldColumn'
| null;
};
InfraLogMessageSegment: {
__resolveType(
messageSegment: InfraLogMessageSegment
@ -122,11 +138,34 @@ export const createLogEntriesResolvers = (libs: {
};
},
async logItem(source, args, { req }) {
return await libs.logEntries.getLogItem(req, args.id, source.configuration);
const sourceConfiguration = SourceConfigurationRuntimeType.decode(
source.configuration
).getOrElseL(errors => {
throw new Error(failure(errors).join('\n'));
});
return await libs.logEntries.getLogItem(req, args.id, sourceConfiguration);
},
},
InfraLogEntryColumn: {
__resolveType(logEntryColumn) {
if (isTimestampColumn(logEntryColumn)) {
return 'InfraLogEntryTimestampColumn';
}
if (isMessageColumn(logEntryColumn)) {
return 'InfraLogEntryMessageColumn';
}
if (isFieldColumn(logEntryColumn)) {
return 'InfraLogEntryFieldColumn';
}
return null;
},
},
InfraLogMessageSegment: {
__resolveType: (messageSegment: InfraLogMessageSegment) => {
__resolveType(messageSegment) {
if (isConstantSegment(messageSegment)) {
return 'InfraLogMessageConstantSegment';
}
@ -140,6 +179,15 @@ export const createLogEntriesResolvers = (libs: {
},
});
const isTimestampColumn = (column: InfraLogEntryColumn): column is InfraLogEntryTimestampColumn =>
'timestamp' in column;
const isMessageColumn = (column: InfraLogEntryColumn): column is InfraLogEntryMessageColumn =>
'message' in column;
const isFieldColumn = (column: InfraLogEntryColumn): column is InfraLogEntryFieldColumn =>
'field' in column && 'value' in column;
const isConstantSegment = (
segment: InfraLogMessageSegment
): segment is InfraLogMessageConstantSegment => 'constant' in segment;

View file

@ -17,7 +17,7 @@ export const logEntriesSchema = gql`
highlights: [String!]!
}
"A segment of the log entry message that was derived from a field"
"A segment of the log entry message that was derived from a string literal"
type InfraLogMessageConstantSegment {
"The segment's message"
constant: String!
@ -26,6 +26,32 @@ export const logEntriesSchema = gql`
"A segment of the log entry message"
union InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment
"A special built-in column that contains the log entry's timestamp"
type InfraLogEntryTimestampColumn {
"The timestamp"
timestamp: Float!
}
"A special built-in column that contains the log entry's constructed message"
type InfraLogEntryMessageColumn {
"A list of the formatted log entry segments"
message: [InfraLogMessageSegment!]!
}
"A column that contains the value of a field of the log entry"
type InfraLogEntryFieldColumn {
"The field name of the column"
field: String!
"The value of the field in the log entry"
value: String!
}
"A column of a log entry"
union InfraLogEntryColumn =
InfraLogEntryTimestampColumn
| InfraLogEntryMessageColumn
| InfraLogEntryFieldColumn
"A log entry"
type InfraLogEntry {
"A unique representation of the log entry's position in the event stream"
@ -34,8 +60,8 @@ export const logEntriesSchema = gql`
gid: String!
"The source id"
source: String!
"A list of the formatted log entry segments"
message: [InfraLogMessageSegment!]!
"The columns used for rendering the log entry"
columns: [InfraLogEntryColumn!]!
}
"A log summary bucket"

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { failure } from 'io-ts/lib/PathReporter';
import { InfraSourceResolvers } from '../../graphql/types';
import { InfraMetricsDomain } from '../../lib/domains/metrics_domain';
import { SourceConfigurationRuntimeType } from '../../lib/sources';
import { UsageCollector } from '../../usage/usage_collector';
import { ChildResolverOf, InfraResolverOf } from '../../utils/typed_resolvers';
import { QuerySourceResolver } from '../sources/resolvers';
@ -28,13 +31,19 @@ export const createMetricResolvers = (
} => ({
InfraSource: {
async metrics(source, args, { req }) {
const sourceConfiguration = SourceConfigurationRuntimeType.decode(
source.configuration
).getOrElseL(errors => {
throw new Error(failure(errors).join('\n'));
});
UsageCollector.countNode(args.nodeType);
const options = {
nodeId: args.nodeId,
nodeType: args.nodeType,
timerange: args.timerange,
metrics: args.metrics,
sourceConfiguration: source.configuration,
sourceConfiguration,
};
return libs.metrics.getMetrics(req, options);
},

View file

@ -4,10 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { convertChangeToUpdater } from '../../../common/source_configuration';
import { InfraSourceResolvers, MutationResolvers, QueryResolvers } from '../../graphql/types';
import { UserInputError } from 'apollo-server-errors';
import { failure } from 'io-ts/lib/PathReporter';
import {
InfraSourceLogColumn,
InfraSourceResolvers,
MutationResolvers,
QueryResolvers,
UpdateSourceLogColumnInput,
} from '../../graphql/types';
import { InfraSourceStatus } from '../../lib/source_status';
import { InfraSources } from '../../lib/sources';
import {
InfraSources,
SavedSourceConfigurationFieldColumnRuntimeType,
SavedSourceConfigurationMessageColumnRuntimeType,
SavedSourceConfigurationTimestampColumnRuntimeType,
SavedSourceConfigurationColumnRuntimeType,
} from '../../lib/sources';
import {
ChildResolverOf,
InfraResolverOf,
@ -59,6 +73,15 @@ export const createSourcesResolvers = (
InfraSource: {
status: InfraSourceStatusResolver;
};
InfraSourceLogColumn: {
__resolveType(
logColumn: InfraSourceLogColumn
):
| 'InfraSourceTimestampLogColumn'
| 'InfraSourceMessageLogColumn'
| 'InfraSourceFieldLogColumn'
| null;
};
Mutation: {
createSource: MutationCreateSourceResolver;
deleteSource: MutationDeleteSourceResolver;
@ -82,14 +105,34 @@ export const createSourcesResolvers = (
return source;
},
},
InfraSourceLogColumn: {
__resolveType(logColumn) {
if (SavedSourceConfigurationTimestampColumnRuntimeType.is(logColumn)) {
return 'InfraSourceTimestampLogColumn';
}
if (SavedSourceConfigurationMessageColumnRuntimeType.is(logColumn)) {
return 'InfraSourceMessageLogColumn';
}
if (SavedSourceConfigurationFieldColumnRuntimeType.is(logColumn)) {
return 'InfraSourceFieldLogColumn';
}
return null;
},
},
Mutation: {
async createSource(root, args, { req }) {
const sourceConfiguration = await libs.sources.createSourceConfiguration(
req,
args.id,
compactObject({
...args.source,
fields: args.source.fields ? compactObject(args.source.fields) : undefined,
...args.sourceProperties,
fields: args.sourceProperties.fields
? compactObject(args.sourceProperties.fields)
: undefined,
logColumns: decodeLogColumns(args.sourceProperties.logColumns),
})
);
@ -105,12 +148,16 @@ export const createSourcesResolvers = (
};
},
async updateSource(root, args, { req }) {
const updaters = args.changes.map(convertChangeToUpdater);
const updatedSourceConfiguration = await libs.sources.updateSourceConfiguration(
req,
args.id,
updaters
compactObject({
...args.sourceProperties,
fields: args.sourceProperties.fields
? compactObject(args.sourceProperties.fields)
: undefined,
logColumns: decodeLogColumns(args.sourceProperties.logColumns),
})
);
return {
@ -133,3 +180,12 @@ const compactObject = <T>(obj: T): CompactObject<T> =>
},
{} as CompactObject<T>
);
const decodeLogColumns = (logColumns?: UpdateSourceLogColumnInput[] | null) =>
logColumns
? logColumns.map(logColumn =>
SavedSourceConfigurationColumnRuntimeType.decode(logColumn).getOrElseL(errors => {
throw new UserInputError(failure(errors).join('\n'));
})
)
: undefined;

View file

@ -36,6 +36,8 @@ export const sourcesSchema = gql`
logAlias: String!
"The field mapping to use for this source"
fields: InfraSourceFields!
"The columns to use for log display"
logColumns: [InfraSourceLogColumn!]!
}
"A mapping of semantic fields to their document counterparts"
@ -54,6 +56,44 @@ export const sourcesSchema = gql`
timestamp: String!
}
"The built-in timestamp log column"
type InfraSourceTimestampLogColumn {
timestampColumn: InfraSourceTimestampLogColumnAttributes!
}
type InfraSourceTimestampLogColumnAttributes {
"A unique id for the column"
id: ID!
}
"The built-in message log column"
type InfraSourceMessageLogColumn {
messageColumn: InfraSourceMessageLogColumnAttributes!
}
type InfraSourceMessageLogColumnAttributes {
"A unique id for the column"
id: ID!
}
"A log column containing a field value"
type InfraSourceFieldLogColumn {
fieldColumn: InfraSourceFieldLogColumnAttributes!
}
type InfraSourceFieldLogColumnAttributes {
"A unique id for the column"
id: ID!
"The field name this column refers to"
field: String!
}
"All known log column types"
union InfraSourceLogColumn =
InfraSourceTimestampLogColumn
| InfraSourceMessageLogColumn
| InfraSourceFieldLogColumn
extend type Query {
"""
Get an infrastructure data source by id.
@ -74,10 +114,10 @@ export const sourcesSchema = gql`
allSources: [InfraSource!]!
}
"The source to be created"
input CreateSourceInput {
"The properties to update the source with"
input UpdateSourceInput {
"The name of the data source"
name: String!
name: String
"A description of the data source"
description: String
"The alias to read metric data from"
@ -85,11 +125,13 @@ export const sourcesSchema = gql`
"The alias to read log data from"
logAlias: String
"The field mapping to use for this source"
fields: CreateSourceFieldsInput
fields: UpdateSourceFieldsInput
"The log columns to display for this source"
logColumns: [UpdateSourceLogColumnInput!]
}
"The mapping of semantic fields of the source to be created"
input CreateSourceFieldsInput {
input UpdateSourceFieldsInput {
"The field to identify a container by"
container: String
"The fields to identify a host by"
@ -102,61 +144,32 @@ export const sourcesSchema = gql`
timestamp: String
}
"The result of a successful source creation"
type CreateSourceResult {
"The source that was created"
source: InfraSource!
"One of the log column types to display for this source"
input UpdateSourceLogColumnInput {
"A custom field log column"
fieldColumn: UpdateSourceFieldLogColumnInput
"A built-in message log column"
messageColumn: UpdateSourceMessageLogColumnInput
"A built-in timestamp log column"
timestampColumn: UpdateSourceTimestampLogColumnInput
}
"The update operations to be performed"
input UpdateSourceInput {
"The name update operation to be performed"
setName: UpdateSourceNameInput
"The description update operation to be performed"
setDescription: UpdateSourceDescriptionInput
"The alias update operation to be performed"
setAliases: UpdateSourceAliasInput
"The field update operation to be performed"
setFields: UpdateSourceFieldsInput
input UpdateSourceFieldLogColumnInput {
id: ID!
field: String!
}
"A name update operation"
input UpdateSourceNameInput {
"The new name to be set"
name: String!
input UpdateSourceMessageLogColumnInput {
id: ID!
}
"A description update operation"
input UpdateSourceDescriptionInput {
"The new description to be set"
description: String!
input UpdateSourceTimestampLogColumnInput {
id: ID!
}
"An alias update operation"
input UpdateSourceAliasInput {
"The new log index pattern or alias to bet set"
logAlias: String
"The new metric index pattern or alias to bet set"
metricAlias: String
}
"A field update operations"
input UpdateSourceFieldsInput {
"The new container field to be set"
container: String
"The new host field to be set"
host: String
"The new pod field to be set"
pod: String
"The new tiebreaker field to be set"
tiebreaker: String
"The new timestamp field to be set"
timestamp: String
}
"The result of a sequence of source update operations"
"The result of a successful source update"
type UpdateSourceResult {
"The source after the operations were performed"
"The source that was updated"
source: InfraSource!
}
@ -168,13 +181,17 @@ export const sourcesSchema = gql`
extend type Mutation {
"Create a new source of infrastructure data"
createSource("The id of the source" id: ID!, source: CreateSourceInput!): CreateSourceResult!
"Modify an existing source using the given sequence of update operations"
createSource(
"The id of the source"
id: ID!
sourceProperties: UpdateSourceInput!
): UpdateSourceResult!
"Modify an existing source"
updateSource(
"The id of the source"
id: ID!
"A sequence of update operations"
changes: [UpdateSourceInput!]!
"The properties to update the source with"
sourceProperties: UpdateSourceInput!
): UpdateSourceResult!
"Delete a source of infrastructure data"
deleteSource("The id of the source" id: ID!): DeleteSourceResult!

View file

@ -81,6 +81,8 @@ export interface InfraSourceConfiguration {
logAlias: string;
/** The field mapping to use for this source */
fields: InfraSourceFields;
/** The columns to use for log display */
logColumns: InfraSourceLogColumn[];
}
/** A mapping of semantic fields to their document counterparts */
export interface InfraSourceFields {
@ -97,6 +99,35 @@ export interface InfraSourceFields {
/** The field to use as a timestamp for metrics and logs */
timestamp: string;
}
/** The built-in timestamp log column */
export interface InfraSourceTimestampLogColumn {
timestampColumn: InfraSourceTimestampLogColumnAttributes;
}
export interface InfraSourceTimestampLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** The built-in message log column */
export interface InfraSourceMessageLogColumn {
messageColumn: InfraSourceMessageLogColumnAttributes;
}
export interface InfraSourceMessageLogColumnAttributes {
/** A unique id for the column */
id: string;
}
/** A log column containing a field value */
export interface InfraSourceFieldLogColumn {
fieldColumn: InfraSourceFieldLogColumnAttributes;
}
export interface InfraSourceFieldLogColumnAttributes {
/** A unique id for the column */
id: string;
/** The field name this column refers to */
field: string;
}
/** The status of an infrastructure data source */
export interface InfraSourceStatus {
/** Whether the configured metric alias exists */
@ -171,6 +202,16 @@ export interface InfraLogEntry {
gid: string;
/** The source id */
source: string;
/** The columns used for rendering the log entry */
columns: InfraLogEntryColumn[];
}
/** A special built-in column that contains the log entry's timestamp */
export interface InfraLogEntryTimestampColumn {
/** The timestamp */
timestamp: number;
}
/** A special built-in column that contains the log entry's constructed message */
export interface InfraLogEntryMessageColumn {
/** A list of the formatted log entry segments */
message: InfraLogMessageSegment[];
}
@ -183,11 +224,18 @@ export interface InfraLogMessageFieldSegment {
/** A list of highlighted substrings of the value */
highlights: string[];
}
/** A segment of the log entry message that was derived from a field */
/** A segment of the log entry message that was derived from a string literal */
export interface InfraLogMessageConstantSegment {
/** The segment's message */
constant: string;
}
/** A column that contains the value of a field of the log entry */
export interface InfraLogEntryFieldColumn {
/** The field name of the column */
field: string;
/** The value of the field in the log entry */
value: string;
}
/** A consecutive sequence of log summary buckets */
export interface InfraLogSummaryInterval {
/** The millisecond timestamp corresponding to the start of the interval covered by the summary */
@ -274,20 +322,15 @@ export interface InfraDataPoint {
export interface Mutation {
/** Create a new source of infrastructure data */
createSource: CreateSourceResult;
/** Modify an existing source using the given sequence of update operations */
createSource: UpdateSourceResult;
/** Modify an existing source */
updateSource: UpdateSourceResult;
/** Delete a source of infrastructure data */
deleteSource: DeleteSourceResult;
}
/** The result of a successful source creation */
export interface CreateSourceResult {
/** The source that was created */
source: InfraSource;
}
/** The result of a sequence of source update operations */
/** The result of a successful source update */
export interface UpdateSourceResult {
/** The source after the operations were performed */
/** The source that was updated */
source: InfraSource;
}
/** The result of a source deletion operations */
@ -326,10 +369,10 @@ export interface InfraSnapshotMetricInput {
/** The type of metric */
type: InfraSnapshotMetricType;
}
/** The source to be created */
export interface CreateSourceInput {
/** The properties to update the source with */
export interface UpdateSourceInput {
/** The name of the data source */
name: string;
name?: string | null;
/** A description of the data source */
description?: string | null;
/** The alias to read metric data from */
@ -337,10 +380,12 @@ export interface CreateSourceInput {
/** The alias to read log data from */
logAlias?: string | null;
/** The field mapping to use for this source */
fields?: CreateSourceFieldsInput | null;
fields?: UpdateSourceFieldsInput | null;
/** The log columns to display for this source */
logColumns?: UpdateSourceLogColumnInput[] | null;
}
/** The mapping of semantic fields of the source to be created */
export interface CreateSourceFieldsInput {
export interface UpdateSourceFieldsInput {
/** The field to identify a container by */
container?: string | null;
/** The fields to identify a host by */
@ -352,46 +397,28 @@ export interface CreateSourceFieldsInput {
/** The field to use as a timestamp for metrics and logs */
timestamp?: string | null;
}
/** The update operations to be performed */
export interface UpdateSourceInput {
/** The name update operation to be performed */
setName?: UpdateSourceNameInput | null;
/** The description update operation to be performed */
setDescription?: UpdateSourceDescriptionInput | null;
/** The alias update operation to be performed */
setAliases?: UpdateSourceAliasInput | null;
/** The field update operation to be performed */
setFields?: UpdateSourceFieldsInput | null;
/** One of the log column types to display for this source */
export interface UpdateSourceLogColumnInput {
/** A custom field log column */
fieldColumn?: UpdateSourceFieldLogColumnInput | null;
/** A built-in message log column */
messageColumn?: UpdateSourceMessageLogColumnInput | null;
/** A built-in timestamp log column */
timestampColumn?: UpdateSourceTimestampLogColumnInput | null;
}
/** A name update operation */
export interface UpdateSourceNameInput {
/** The new name to be set */
name: string;
export interface UpdateSourceFieldLogColumnInput {
id: string;
field: string;
}
/** A description update operation */
export interface UpdateSourceDescriptionInput {
/** The new description to be set */
description: string;
export interface UpdateSourceMessageLogColumnInput {
id: string;
}
/** An alias update operation */
export interface UpdateSourceAliasInput {
/** The new log index pattern or alias to bet set */
logAlias?: string | null;
/** The new metric index pattern or alias to bet set */
metricAlias?: string | null;
}
/** A field update operations */
export interface UpdateSourceFieldsInput {
/** The new container field to be set */
container?: string | null;
/** The new host field to be set */
host?: string | null;
/** The new pod field to be set */
pod?: string | null;
/** The new tiebreaker field to be set */
tiebreaker?: string | null;
/** The new timestamp field to be set */
timestamp?: string | null;
export interface UpdateSourceTimestampLogColumnInput {
id: string;
}
// ====================================================
@ -470,13 +497,13 @@ export interface CreateSourceMutationArgs {
/** The id of the source */
id: string;
source: CreateSourceInput;
sourceProperties: UpdateSourceInput;
}
export interface UpdateSourceMutationArgs {
/** The id of the source */
id: string;
/** A sequence of update operations */
changes: UpdateSourceInput[];
/** The properties to update the source with */
sourceProperties: UpdateSourceInput;
}
export interface DeleteSourceMutationArgs {
/** The id of the source */
@ -543,6 +570,18 @@ export enum InfraMetric {
// Unions
// ====================================================
/** All known log column types */
export type InfraSourceLogColumn =
| InfraSourceTimestampLogColumn
| InfraSourceMessageLogColumn
| InfraSourceFieldLogColumn;
/** A column of a log entry */
export type InfraLogEntryColumn =
| InfraLogEntryTimestampColumn
| InfraLogEntryMessageColumn
| InfraLogEntryFieldColumn;
/** A segment of the log entry message */
export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment;
@ -742,6 +781,8 @@ export namespace InfraSourceConfigurationResolvers {
logAlias?: LogAliasResolver<string, TypeParent, Context>;
/** The field mapping to use for this source */
fields?: FieldsResolver<InfraSourceFields, TypeParent, Context>;
/** The columns to use for log display */
logColumns?: LogColumnsResolver<InfraSourceLogColumn[], TypeParent, Context>;
}
export type NameResolver<
@ -769,6 +810,11 @@ export namespace InfraSourceConfigurationResolvers {
Parent = InfraSourceConfiguration,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type LogColumnsResolver<
R = InfraSourceLogColumn[],
Parent = InfraSourceConfiguration,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A mapping of semantic fields to their document counterparts */
export namespace InfraSourceFieldsResolvers {
@ -818,6 +864,105 @@ export namespace InfraSourceFieldsResolvers {
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** The built-in timestamp log column */
export namespace InfraSourceTimestampLogColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraSourceTimestampLogColumn> {
timestampColumn?: TimestampColumnResolver<
InfraSourceTimestampLogColumnAttributes,
TypeParent,
Context
>;
}
export type TimestampColumnResolver<
R = InfraSourceTimestampLogColumnAttributes,
Parent = InfraSourceTimestampLogColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
export namespace InfraSourceTimestampLogColumnAttributesResolvers {
export interface Resolvers<
Context = InfraContext,
TypeParent = InfraSourceTimestampLogColumnAttributes
> {
/** A unique id for the column */
id?: IdResolver<string, TypeParent, Context>;
}
export type IdResolver<
R = string,
Parent = InfraSourceTimestampLogColumnAttributes,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** The built-in message log column */
export namespace InfraSourceMessageLogColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraSourceMessageLogColumn> {
messageColumn?: MessageColumnResolver<
InfraSourceMessageLogColumnAttributes,
TypeParent,
Context
>;
}
export type MessageColumnResolver<
R = InfraSourceMessageLogColumnAttributes,
Parent = InfraSourceMessageLogColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
export namespace InfraSourceMessageLogColumnAttributesResolvers {
export interface Resolvers<
Context = InfraContext,
TypeParent = InfraSourceMessageLogColumnAttributes
> {
/** A unique id for the column */
id?: IdResolver<string, TypeParent, Context>;
}
export type IdResolver<
R = string,
Parent = InfraSourceMessageLogColumnAttributes,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A log column containing a field value */
export namespace InfraSourceFieldLogColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraSourceFieldLogColumn> {
fieldColumn?: FieldColumnResolver<InfraSourceFieldLogColumnAttributes, TypeParent, Context>;
}
export type FieldColumnResolver<
R = InfraSourceFieldLogColumnAttributes,
Parent = InfraSourceFieldLogColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
export namespace InfraSourceFieldLogColumnAttributesResolvers {
export interface Resolvers<
Context = InfraContext,
TypeParent = InfraSourceFieldLogColumnAttributes
> {
/** A unique id for the column */
id?: IdResolver<string, TypeParent, Context>;
/** The field name this column refers to */
field?: FieldResolver<string, TypeParent, Context>;
}
export type IdResolver<
R = string,
Parent = InfraSourceFieldLogColumnAttributes,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type FieldResolver<
R = string,
Parent = InfraSourceFieldLogColumnAttributes,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** The status of an infrastructure data source */
export namespace InfraSourceStatusResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraSourceStatus> {
@ -1039,8 +1184,8 @@ export namespace InfraLogEntryResolvers {
gid?: GidResolver<string, TypeParent, Context>;
/** The source id */
source?: SourceResolver<string, TypeParent, Context>;
/** A list of the formatted log entry segments */
message?: MessageResolver<InfraLogMessageSegment[], TypeParent, Context>;
/** The columns used for rendering the log entry */
columns?: ColumnsResolver<InfraLogEntryColumn[], TypeParent, Context>;
}
export type KeyResolver<
@ -1058,9 +1203,35 @@ export namespace InfraLogEntryResolvers {
Parent,
Context
>;
export type ColumnsResolver<
R = InfraLogEntryColumn[],
Parent = InfraLogEntry,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A special built-in column that contains the log entry's timestamp */
export namespace InfraLogEntryTimestampColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryTimestampColumn> {
/** The timestamp */
timestamp?: TimestampResolver<number, TypeParent, Context>;
}
export type TimestampResolver<
R = number,
Parent = InfraLogEntryTimestampColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A special built-in column that contains the log entry's constructed message */
export namespace InfraLogEntryMessageColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryMessageColumn> {
/** A list of the formatted log entry segments */
message?: MessageResolver<InfraLogMessageSegment[], TypeParent, Context>;
}
export type MessageResolver<
R = InfraLogMessageSegment[],
Parent = InfraLogEntry,
Parent = InfraLogEntryMessageColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
@ -1091,7 +1262,7 @@ export namespace InfraLogMessageFieldSegmentResolvers {
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A segment of the log entry message that was derived from a field */
/** A segment of the log entry message that was derived from a string literal */
export namespace InfraLogMessageConstantSegmentResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogMessageConstantSegment> {
/** The segment's message */
@ -1104,6 +1275,26 @@ export namespace InfraLogMessageConstantSegmentResolvers {
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A column that contains the value of a field of the log entry */
export namespace InfraLogEntryFieldColumnResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryFieldColumn> {
/** The field name of the column */
field?: FieldResolver<string, TypeParent, Context>;
/** The value of the field in the log entry */
value?: ValueResolver<string, TypeParent, Context>;
}
export type FieldResolver<
R = string,
Parent = InfraLogEntryFieldColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type ValueResolver<
R = string,
Parent = InfraLogEntryFieldColumn,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** A consecutive sequence of log summary buckets */
export namespace InfraLogSummaryIntervalResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogSummaryInterval> {
@ -1371,15 +1562,15 @@ export namespace InfraDataPointResolvers {
export namespace MutationResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = never> {
/** Create a new source of infrastructure data */
createSource?: CreateSourceResolver<CreateSourceResult, TypeParent, Context>;
/** Modify an existing source using the given sequence of update operations */
createSource?: CreateSourceResolver<UpdateSourceResult, TypeParent, Context>;
/** Modify an existing source */
updateSource?: UpdateSourceResolver<UpdateSourceResult, TypeParent, Context>;
/** Delete a source of infrastructure data */
deleteSource?: DeleteSourceResolver<DeleteSourceResult, TypeParent, Context>;
}
export type CreateSourceResolver<
R = CreateSourceResult,
R = UpdateSourceResult,
Parent = never,
Context = InfraContext
> = Resolver<R, Parent, Context, CreateSourceArgs>;
@ -1387,7 +1578,7 @@ export namespace MutationResolvers {
/** The id of the source */
id: string;
source: CreateSourceInput;
sourceProperties: UpdateSourceInput;
}
export type UpdateSourceResolver<
@ -1398,8 +1589,8 @@ export namespace MutationResolvers {
export interface UpdateSourceArgs {
/** The id of the source */
id: string;
/** A sequence of update operations */
changes: UpdateSourceInput[];
/** The properties to update the source with */
sourceProperties: UpdateSourceInput;
}
export type DeleteSourceResolver<
@ -1412,23 +1603,10 @@ export namespace MutationResolvers {
id: string;
}
}
/** The result of a successful source creation */
export namespace CreateSourceResultResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = CreateSourceResult> {
/** The source that was created */
source?: SourceResolver<InfraSource, TypeParent, Context>;
}
export type SourceResolver<
R = InfraSource,
Parent = CreateSourceResult,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
/** The result of a sequence of source update operations */
/** The result of a successful source update */
export namespace UpdateSourceResultResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = UpdateSourceResult> {
/** The source after the operations were performed */
/** The source that was updated */
source?: SourceResolver<InfraSource, TypeParent, Context>;
}

View file

@ -4,20 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import stringify from 'json-stable-stringify';
import { sortBy } from 'lodash';
import { TimeKey } from '../../../../common/time';
import { JsonObject } from '../../../../common/typed_json';
import { InfraLogItem } from '../../../graphql/types';
import {
InfraLogEntry,
InfraLogItem,
InfraLogMessageSegment,
InfraLogSummaryBucket,
} from '../../../graphql/types';
import { InfraDateRangeAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework';
import { InfraSourceConfiguration, InfraSources } from '../../sources';
import {
InfraSourceConfiguration,
InfraSources,
SavedSourceConfigurationFieldColumnRuntimeType,
SavedSourceConfigurationMessageColumnRuntimeType,
SavedSourceConfigurationTimestampColumnRuntimeType,
} from '../../sources';
import { getBuiltinRules } from './builtin_rules';
import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
import { compileFormattingRules } from './message';
import { compileFormattingRules, CompiledLogMessageFormattingRule } from './message';
export class InfraLogEntriesDomain {
constructor(
@ -42,12 +50,15 @@ export class InfraLogEntriesDomain {
}
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const formattingRules = compileFormattingRules(getBuiltinRules(configuration.fields.message));
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
const requiredFields = getRequiredFields(configuration, messageFormattingRules);
const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments(
request,
configuration,
formattingRules.requiredFields,
requiredFields,
key,
'desc',
Math.max(maxCountBefore, 1),
@ -65,7 +76,7 @@ export class InfraLogEntriesDomain {
const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments(
request,
configuration,
formattingRules.requiredFields,
messageFormattingRules.requiredFields,
lastKeyBefore,
'asc',
maxCountAfter,
@ -75,9 +86,11 @@ export class InfraLogEntriesDomain {
return {
entriesBefore: (maxCountBefore > 0 ? documentsBefore : []).map(
convertLogDocumentToEntry(sourceId, formattingRules.format)
convertLogDocumentToEntry(sourceId, configuration.logColumns, messageFormattingRules.format)
),
entriesAfter: documentsAfter.map(
convertLogDocumentToEntry(sourceId, configuration.logColumns, messageFormattingRules.format)
),
entriesAfter: documentsAfter.map(convertLogDocumentToEntry(sourceId, formattingRules.format)),
};
}
@ -90,17 +103,22 @@ export class InfraLogEntriesDomain {
highlightQuery?: string
): Promise<InfraLogEntry[]> {
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
const formattingRules = compileFormattingRules(getBuiltinRules(configuration.fields.message));
const messageFormattingRules = compileFormattingRules(
getBuiltinRules(configuration.fields.message)
);
const requiredFields = getRequiredFields(configuration, messageFormattingRules);
const documents = await this.adapter.getContainedLogEntryDocuments(
request,
configuration,
formattingRules.requiredFields,
requiredFields,
startKey,
endKey,
filterQuery,
highlightQuery
);
const entries = documents.map(convertLogDocumentToEntry(sourceId, formattingRules.format));
const entries = documents.map(
convertLogDocumentToEntry(sourceId, configuration.logColumns, messageFormattingRules.format)
);
return entries;
}
@ -210,12 +228,28 @@ export interface LogEntryDocumentFields {
const convertLogDocumentToEntry = (
sourceId: string,
logColumns: InfraSourceConfiguration['logColumns'],
formatLogMessage: (fields: LogEntryDocumentFields) => InfraLogMessageSegment[]
) => (document: LogEntryDocument): InfraLogEntry => ({
key: document.key,
gid: document.gid,
source: sourceId,
message: formatLogMessage(document.fields),
columns: logColumns.map(logColumn => {
if (SavedSourceConfigurationTimestampColumnRuntimeType.is(logColumn)) {
return {
timestamp: document.key.time,
};
} else if (SavedSourceConfigurationMessageColumnRuntimeType.is(logColumn)) {
return {
message: formatLogMessage(document.fields),
};
} else {
return {
field: logColumn.fieldColumn.field,
value: stringify(document.fields[logColumn.fieldColumn.field] || null),
};
}
}),
});
const convertDateRangeBucketToSummaryBucket = (
@ -225,3 +259,21 @@ const convertDateRangeBucketToSummaryBucket = (
start: bucket.from || 0,
end: bucket.to || 0,
});
const getRequiredFields = (
configuration: InfraSourceConfiguration,
messageFormattingRules: CompiledLogMessageFormattingRule
): string[] => {
const fieldsFromCustomColumns = configuration.logColumns.reduce<string[]>(
(accumulatedFields, logColumn) => {
if (SavedSourceConfigurationFieldColumnRuntimeType.is(logColumn)) {
return [...accumulatedFields, logColumn.fieldColumn.field];
}
return accumulatedFields;
},
[]
);
const fieldsFromFormattingRules = messageFormattingRules.requiredFields;
return Array.from(new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules]));
};

View file

@ -13,7 +13,9 @@ import {
LogMessageFormattingRule,
} from './rule_types';
export function compileFormattingRules(rules: LogMessageFormattingRule[]) {
export function compileFormattingRules(
rules: LogMessageFormattingRule[]
): CompiledLogMessageFormattingRule {
const compiledRules = rules.map(compileRule);
return {
@ -28,7 +30,7 @@ export function compileFormattingRules(rules: LogMessageFormattingRule[]) {
)
)
),
format: (fields: Fields): InfraLogMessageSegment[] => {
format(fields): InfraLogMessageSegment[] {
for (const compiledRule of compiledRules) {
if (compiledRule.fulfillsCondition(fields)) {
return compiledRule.format(fields);
@ -37,6 +39,9 @@ export function compileFormattingRules(rules: LogMessageFormattingRule[]) {
return [];
},
fulfillsCondition() {
return true;
},
};
}
@ -163,18 +168,18 @@ interface Fields {
[fieldName: string]: string | number | object | boolean | null;
}
interface CompiledLogMessageFormattingRule {
export interface CompiledLogMessageFormattingRule {
requiredFields: string[];
fulfillsCondition(fields: Fields): boolean;
format(fields: Fields): InfraLogMessageSegment[];
}
interface CompiledLogMessageFormattingCondition {
export interface CompiledLogMessageFormattingCondition {
conditionFields: string[];
fulfillsCondition(fields: Fields): boolean;
}
interface CompiledLogMessageFormattingInstruction {
export interface CompiledLogMessageFormattingInstruction {
formattingFields: string[];
format(fields: Fields): InfraLogMessageSegment[];
}

View file

@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const defaultSourceConfiguration = {
import { InfraSourceConfiguration } from './types';
export const defaultSourceConfiguration: InfraSourceConfiguration = {
name: 'Default',
description: '',
metricAlias: 'metricbeat-*',
@ -17,4 +19,22 @@ export const defaultSourceConfiguration = {
tiebreaker: '_doc',
timestamp: '@timestamp',
},
logColumns: [
{
timestampColumn: {
id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f',
},
},
{
fieldColumn: {
id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2',
field: 'event.dataset',
},
},
{
messageColumn: {
id: 'b645d6da-824b-4723-9a2a-e8cece1645c0',
},
},
],
};

View file

@ -45,6 +45,35 @@ export const infraSourceConfigurationSavedObjectMappings: {
},
},
},
logColumns: {
type: 'nested',
properties: {
timestampColumn: {
properties: {
id: {
type: 'keyword',
},
},
},
messageColumn: {
properties: {
id: {
type: 'keyword',
},
},
},
fieldColumn: {
properties: {
id: {
type: 'keyword',
},
field: {
type: 'keyword',
},
},
},
},
},
},
},
};

View file

@ -87,7 +87,7 @@ export class InfraSources {
.getScopedSavedObjectsClient(request[internalInfraFrameworkRequest])
.create(
infraSourceConfigurationSavedObjectType,
pickSavedSourceConfiguration(newSourceConfiguration),
pickSavedSourceConfiguration(newSourceConfiguration) as any,
{ id: sourceId }
)
);
@ -110,15 +110,15 @@ export class InfraSources {
public async updateSourceConfiguration(
request: InfraFrameworkRequest,
sourceId: string,
updaters: Array<(configuration: InfraSourceConfiguration) => InfraSourceConfiguration>
sourceProperties: InfraSavedSourceConfiguration
) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
const { configuration, version } = await this.getSourceConfiguration(request, sourceId);
const updatedConfigurationAttributes = updaters.reduce(
(accumulatedConfiguration, updater) => updater(accumulatedConfiguration),
configuration
const updatedSourceConfigurationAttributes = mergeSourceConfiguration(
configuration,
sourceProperties
);
const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
@ -127,7 +127,7 @@ export class InfraSources {
.update(
infraSourceConfigurationSavedObjectType,
sourceId,
pickSavedSourceConfiguration(updatedConfigurationAttributes),
pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
{
version,
}

View file

@ -34,23 +34,56 @@ const SavedSourceConfigurationFieldsRuntimeType = runtimeTypes.partial({
timestamp: runtimeTypes.string,
});
export const SavedSourceConfigurationTimestampColumnRuntimeType = runtimeTypes.type({
timestampColumn: runtimeTypes.type({
id: runtimeTypes.string,
}),
});
export const SavedSourceConfigurationMessageColumnRuntimeType = runtimeTypes.type({
messageColumn: runtimeTypes.type({
id: runtimeTypes.string,
}),
});
export const SavedSourceConfigurationFieldColumnRuntimeType = runtimeTypes.type({
fieldColumn: runtimeTypes.type({
id: runtimeTypes.string,
field: runtimeTypes.string,
}),
});
export const SavedSourceConfigurationColumnRuntimeType = runtimeTypes.union([
SavedSourceConfigurationTimestampColumnRuntimeType,
SavedSourceConfigurationMessageColumnRuntimeType,
SavedSourceConfigurationFieldColumnRuntimeType,
]);
export const SavedSourceConfigurationRuntimeType = runtimeTypes.partial({
name: runtimeTypes.string,
description: runtimeTypes.string,
metricAlias: runtimeTypes.string,
logAlias: runtimeTypes.string,
fields: SavedSourceConfigurationFieldsRuntimeType,
logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType),
});
export interface InfraSavedSourceConfiguration
extends runtimeTypes.TypeOf<typeof SavedSourceConfigurationRuntimeType> {}
export const pickSavedSourceConfiguration = (value: InfraSourceConfiguration) => {
const { container, host, pod, tiebreaker, timestamp } = value.fields;
export const pickSavedSourceConfiguration = (
value: InfraSourceConfiguration
): InfraSavedSourceConfiguration => {
const { name, description, metricAlias, logAlias, fields, logColumns } = value;
const { container, host, pod, tiebreaker, timestamp } = fields;
return {
...value,
name,
description,
metricAlias,
logAlias,
fields: { container, host, pod, tiebreaker, timestamp },
logColumns,
};
};
@ -69,6 +102,7 @@ export const StaticSourceConfigurationRuntimeType = runtimeTypes.partial({
metricAlias: runtimeTypes.string,
logAlias: runtimeTypes.string,
fields: StaticSourceConfigurationFieldsRuntimeType,
logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType),
});
export interface InfraStaticSourceConfiguration
@ -85,6 +119,7 @@ const SourceConfigurationFieldsRuntimeType = runtimeTypes.type({
export const SourceConfigurationRuntimeType = runtimeTypes.type({
...SavedSourceConfigurationRuntimeType.props,
fields: SourceConfigurationFieldsRuntimeType,
logColumns: runtimeTypes.array(SavedSourceConfigurationColumnRuntimeType),
});
export interface InfraSourceConfiguration

View file

@ -8,6 +8,8 @@ export type ElasticsearchMappingOf<Type> = Type extends string
? ElasticsearchStringFieldMapping
: Type extends number
? ElasticsearchNumberFieldMapping
: Type extends object[]
? ElasticsearchNestedFieldMapping<Type>
: Type extends {}
? ElasticsearchObjectFieldMapping<Type>
: never;
@ -29,6 +31,11 @@ export interface ElasticsearchNumberFieldMapping {
| 'date';
}
export interface ElasticsearchNestedFieldMapping<Obj extends object[]> {
type?: 'nested';
properties: { [K in keyof Obj[0]]-?: ElasticsearchMappingOf<Obj[0][K]> };
}
export interface ElasticsearchObjectFieldMapping<Obj extends {}> {
type?: 'object';
properties: { [K in keyof Obj]-?: ElasticsearchMappingOf<Obj[K]> };

View file

@ -7,7 +7,9 @@
import expect from '@kbn/expect';
import { ascending, pairs } from 'd3-array';
import gql from 'graphql-tag';
import { v4 as uuidv4 } from 'uuid';
import { sharedFragments } from '../../../../plugins/infra/common/graphql/shared';
import { InfraTimeKey } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';
@ -40,34 +42,22 @@ const logEntriesAroundQuery = gql`
filterQuery: $filterQuery
) {
start {
time
tiebreaker
...InfraTimeKeyFields
}
end {
time
tiebreaker
...InfraTimeKeyFields
}
hasMoreBefore
hasMoreAfter
entries {
gid
key {
time
tiebreaker
}
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;
const logEntriesBetweenQuery = gql`
@ -80,165 +70,266 @@ const logEntriesBetweenQuery = gql`
id
logEntriesBetween(startKey: $startKey, endKey: $endKey, filterQuery: $filterQuery) {
start {
time
tiebreaker
...InfraTimeKeyFields
}
end {
time
tiebreaker
...InfraTimeKeyFields
}
hasMoreBefore
hasMoreAfter
entries {
gid
key {
time
tiebreaker
}
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;
const logEntriesTests: KbnTestProvider = ({ getService }) => {
const esArchiver = getService('esArchiver');
const client = getService('infraOpsGraphQLClient');
const sourceConfigurationService = getService('infraOpsSourceConfiguration');
describe('log entry apis', () => {
before(() => esArchiver.load('infra/metrics_and_logs'));
after(() => esArchiver.unload('infra/metrics_and_logs'));
describe('logEntriesAround', () => {
it('should return newer and older log entries when present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countBefore: 100,
countAfter: 100,
},
describe('with the default source', () => {
before(() => esArchiver.load('empty_kibana'));
after(() => esArchiver.unload('empty_kibana'));
it('should return newer and older log entries when present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countBefore: 100,
countAfter: 100,
},
});
expect(logEntriesAround).to.have.property('entries');
expect(logEntriesAround.entries).to.have.length(200);
expect(isSorted(ascendingTimeKey)(logEntriesAround.entries)).to.equal(true);
expect(logEntriesAround.hasMoreBefore).to.equal(true);
expect(logEntriesAround.hasMoreAfter).to.equal(true);
});
expect(logEntriesAround).to.have.property('entries');
expect(logEntriesAround.entries).to.have.length(200);
expect(isSorted(ascendingTimeKey)(logEntriesAround.entries)).to.equal(true);
it('should indicate if no older entries are present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: EARLIEST_KEY_WITH_DATA,
countBefore: 100,
countAfter: 100,
},
});
expect(logEntriesAround.hasMoreBefore).to.equal(true);
expect(logEntriesAround.hasMoreAfter).to.equal(true);
expect(logEntriesAround.hasMoreBefore).to.equal(false);
expect(logEntriesAround.hasMoreAfter).to.equal(true);
});
it('should indicate if no newer entries are present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: LATEST_KEY_WITH_DATA,
countBefore: 100,
countAfter: 100,
},
});
expect(logEntriesAround.hasMoreBefore).to.equal(true);
expect(logEntriesAround.hasMoreAfter).to.equal(false);
});
it('should return the default columns', async () => {
const {
data: {
source: {
logEntriesAround: {
entries: [entry],
},
},
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countAfter: 1,
},
});
expect(entry.columns).to.have.length(3);
expect(entry.columns[0]).to.have.property('timestamp');
expect(entry.columns[0].timestamp).to.be.a('number');
expect(entry.columns[1]).to.have.property('field');
expect(entry.columns[1].field).to.be('event.dataset');
expect(entry.columns[1]).to.have.property('value');
expect(JSON.parse)
.withArgs(entry.columns[1].value)
.to.not.throwException();
expect(entry.columns[2]).to.have.property('message');
expect(entry.columns[2].message).to.be.an('array');
expect(entry.columns[2].message.length).to.be.greaterThan(0);
});
});
it('should indicate if no older entries are present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: EARLIEST_KEY_WITH_DATA,
countBefore: 100,
countAfter: 100,
},
describe('with a configured source', () => {
before(async () => {
await esArchiver.load('empty_kibana');
await sourceConfigurationService.createConfiguration('default', {
name: 'Test Source',
logColumns: [
{
timestampColumn: {
id: uuidv4(),
},
},
{
fieldColumn: {
id: uuidv4(),
field: 'host.name',
},
},
{
fieldColumn: {
id: uuidv4(),
field: 'event.dataset',
},
},
{
messageColumn: {
id: uuidv4(),
},
},
],
});
});
after(() => esArchiver.unload('empty_kibana'));
expect(logEntriesAround.hasMoreBefore).to.equal(false);
expect(logEntriesAround.hasMoreAfter).to.equal(true);
});
it('should return the configured columns', async () => {
const {
data: {
source: {
logEntriesAround: {
entries: [entry],
},
},
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countAfter: 1,
},
});
it('should indicate if no newer entries are present', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: LATEST_KEY_WITH_DATA,
countBefore: 100,
countAfter: 100,
},
expect(entry.columns).to.have.length(4);
expect(entry.columns[0]).to.have.property('timestamp');
expect(entry.columns[0].timestamp).to.be.a('number');
expect(entry.columns[1]).to.have.property('field');
expect(entry.columns[1].field).to.be('host.name');
expect(entry.columns[1]).to.have.property('value');
expect(JSON.parse)
.withArgs(entry.columns[1].value)
.to.not.throwException();
expect(entry.columns[2]).to.have.property('field');
expect(entry.columns[2].field).to.be('event.dataset');
expect(entry.columns[2]).to.have.property('value');
expect(JSON.parse)
.withArgs(entry.columns[2].value)
.to.not.throwException();
expect(entry.columns[3]).to.have.property('message');
expect(entry.columns[3].message).to.be.an('array');
expect(entry.columns[3].message.length).to.be.greaterThan(0);
});
expect(logEntriesAround.hasMoreBefore).to.equal(true);
expect(logEntriesAround.hasMoreAfter).to.equal(false);
});
});
describe('logEntriesBetween', () => {
it('should return log entries between the start and end keys', async () => {
const {
data: {
source: { logEntriesBetween },
},
} = await client.query<any>({
query: logEntriesBetweenQuery,
variables: {
startKey: EARLIEST_KEY_WITH_DATA,
endKey: KEY_WITHIN_DATA_RANGE,
},
});
describe('with the default source', () => {
before(() => esArchiver.load('empty_kibana'));
after(() => esArchiver.unload('empty_kibana'));
expect(logEntriesBetween).to.have.property('entries');
expect(logEntriesBetween.entries).to.not.be.empty();
expect(isSorted(ascendingTimeKey)(logEntriesBetween.entries)).to.equal(true);
expect(
ascendingTimeKey(logEntriesBetween.entries[0], { key: EARLIEST_KEY_WITH_DATA })
).to.be.above(-1);
expect(
ascendingTimeKey(logEntriesBetween.entries[logEntriesBetween.entries.length - 1], {
key: KEY_WITHIN_DATA_RANGE,
})
).to.be.below(1);
});
it('should return results consistent with logEntriesAround', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countBefore: 100,
countAfter: 100,
},
});
const {
data: {
source: { logEntriesBetween },
},
} = await client.query<any>({
query: logEntriesBetweenQuery,
variables: {
startKey: {
time: logEntriesAround.start.time,
tiebreaker: logEntriesAround.start.tiebreaker - 1,
it('should return log entries between the start and end keys', async () => {
const {
data: {
source: { logEntriesBetween },
},
endKey: {
time: logEntriesAround.end.time,
tiebreaker: logEntriesAround.end.tiebreaker + 1,
} = await client.query<any>({
query: logEntriesBetweenQuery,
variables: {
startKey: EARLIEST_KEY_WITH_DATA,
endKey: KEY_WITHIN_DATA_RANGE,
},
},
});
expect(logEntriesBetween).to.have.property('entries');
expect(logEntriesBetween.entries).to.not.be.empty();
expect(isSorted(ascendingTimeKey)(logEntriesBetween.entries)).to.equal(true);
expect(
ascendingTimeKey(logEntriesBetween.entries[0], { key: EARLIEST_KEY_WITH_DATA })
).to.be.above(-1);
expect(
ascendingTimeKey(logEntriesBetween.entries[logEntriesBetween.entries.length - 1], {
key: KEY_WITHIN_DATA_RANGE,
})
).to.be.below(1);
});
expect(logEntriesBetween).to.eql(logEntriesAround);
it('should return results consistent with logEntriesAround', async () => {
const {
data: {
source: { logEntriesAround },
},
} = await client.query<any>({
query: logEntriesAroundQuery,
variables: {
timeKey: KEY_WITHIN_DATA_RANGE,
countBefore: 100,
countAfter: 100,
},
});
const {
data: {
source: { logEntriesBetween },
},
} = await client.query<any>({
query: logEntriesBetweenQuery,
variables: {
startKey: {
time: logEntriesAround.start.time,
tiebreaker: logEntriesAround.start.tiebreaker - 1,
},
endKey: {
time: logEntriesAround.end.time,
tiebreaker: logEntriesAround.end.tiebreaker + 1,
},
},
});
expect(logEntriesBetween).to.eql(logEntriesAround);
});
});
});
});

View file

@ -8,6 +8,7 @@ import expect from '@kbn/expect';
import { ascending, pairs } from 'd3-array';
import gql from 'graphql-tag';
import { sharedFragments } from '../../../../plugins/infra/common/graphql/shared';
import { InfraTimeKey } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';
@ -117,34 +118,22 @@ const logEntriesAroundQuery = gql`
filterQuery: $filterQuery
) {
start {
time
tiebreaker
...InfraTimeKeyFields
}
end {
time
tiebreaker
...InfraTimeKeyFields
}
hasMoreBefore
hasMoreAfter
entries {
gid
key {
time
tiebreaker
}
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;
const logEntriesBetweenQuery = gql`
@ -157,34 +146,22 @@ const logEntriesBetweenQuery = gql`
id
logEntriesBetween(startKey: $startKey, endKey: $endKey, filterQuery: $filterQuery) {
start {
time
tiebreaker
...InfraTimeKeyFields
}
end {
time
tiebreaker
...InfraTimeKeyFields
}
hasMoreBefore
hasMoreAfter
entries {
gid
key {
time
tiebreaker
}
message {
... on InfraLogMessageFieldSegment {
field
value
}
... on InfraLogMessageConstantSegment {
constant
}
}
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;
const logSummaryBetweenQuery = gql`

View file

@ -8,8 +8,13 @@ import expect from '@kbn/expect';
import gql from 'graphql-tag';
import { sourceQuery } from '../../../../plugins/infra/public/containers/source/query_source.gql_query';
import {
sourceConfigurationFieldsFragment,
sourceStatusFieldsFragment,
} from '../../../../plugins/infra/public/containers/source/source_fields_fragment.gql_query';
import { SourceQuery } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';
import { sharedFragments } from '../../../../plugins/infra/common/graphql/shared';
const sourcesTests: KbnTestProvider = ({ getService }) => {
const esArchiver = getService('esArchiver');
@ -40,6 +45,10 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(sourceConfiguration.fields.container).to.be('container.id');
expect(sourceConfiguration.fields.host).to.be('host.name');
expect(sourceConfiguration.fields.pod).to.be('kubernetes.pod.uid');
expect(sourceConfiguration.logColumns).to.have.length(3);
expect(sourceConfiguration.logColumns[0]).to.have.key('timestampColumn');
expect(sourceConfiguration.logColumns[1]).to.have.key('fieldColumn');
expect(sourceConfiguration.logColumns[2]).to.have.key('messageColumn');
// test data in x-pack/test/functional/es_archives/infra/data.json.gz
expect(sourceStatus.indexFields.length).to.be(1765);
@ -53,7 +62,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const response = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
description: 'DESCRIPTION',
logAlias: 'filebeat-**',
@ -65,6 +74,13 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
tiebreaker: 'TIEBREAKER',
timestamp: 'TIMESTAMP',
},
logColumns: [
{
messageColumn: {
id: 'MESSAGE_COLUMN',
},
},
],
},
sourceId: 'default',
},
@ -84,6 +100,9 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(configuration.fields.pod).to.be('POD');
expect(configuration.fields.tiebreaker).to.be('TIEBREAKER');
expect(configuration.fields.timestamp).to.be('TIMESTAMP');
expect(configuration.logColumns).to.have.length(1);
expect(configuration.logColumns[0]).to.have.key('messageColumn');
expect(status.logIndicesExist).to.be(true);
expect(status.metricIndicesExist).to.be(true);
});
@ -92,7 +111,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const response = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -113,6 +132,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(configuration.fields.pod).to.be('kubernetes.pod.uid');
expect(configuration.fields.tiebreaker).to.be('_doc');
expect(configuration.fields.timestamp).to.be('@timestamp');
expect(configuration.logColumns).to.have.length(3);
expect(status.logIndicesExist).to.be(true);
expect(status.metricIndicesExist).to.be(true);
});
@ -121,7 +141,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -132,7 +152,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -154,7 +174,7 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
const creationResponse = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -179,11 +199,11 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
});
describe('updateSource mutation', () => {
it('applies multiple updates to an existing source', async () => {
it('applies all top-level field updates to an existing source', async () => {
const creationResponse = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -200,33 +220,12 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
mutation: updateSourceMutation,
variables: {
sourceId: 'default',
changes: [
{
setName: {
name: 'UPDATED_NAME',
},
},
{
setDescription: {
description: 'UPDATED_DESCRIPTION',
},
},
{
setAliases: {
logAlias: 'filebeat-**',
metricAlias: 'metricbeat-**',
},
},
{
setFields: {
container: 'UPDATED_CONTAINER',
host: 'UPDATED_HOST',
pod: 'UPDATED_POD',
tiebreaker: 'UPDATED_TIEBREAKER',
timestamp: 'UPDATED_TIMESTAMP',
},
},
],
sourceProperties: {
name: 'UPDATED_NAME',
description: 'UPDATED_DESCRIPTION',
metricAlias: 'metricbeat-**',
logAlias: 'filebeat-**',
},
},
});
@ -240,20 +239,21 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(configuration.description).to.be('UPDATED_DESCRIPTION');
expect(configuration.metricAlias).to.be('metricbeat-**');
expect(configuration.logAlias).to.be('filebeat-**');
expect(configuration.fields.container).to.be('UPDATED_CONTAINER');
expect(configuration.fields.host).to.be('UPDATED_HOST');
expect(configuration.fields.pod).to.be('UPDATED_POD');
expect(configuration.fields.tiebreaker).to.be('UPDATED_TIEBREAKER');
expect(configuration.fields.timestamp).to.be('UPDATED_TIMESTAMP');
expect(configuration.fields.host).to.be('host.name');
expect(configuration.fields.pod).to.be('kubernetes.pod.uid');
expect(configuration.fields.tiebreaker).to.be('_doc');
expect(configuration.fields.timestamp).to.be('@timestamp');
expect(configuration.fields.container).to.be('container.id');
expect(configuration.logColumns).to.have.length(3);
expect(status.logIndicesExist).to.be(true);
expect(status.metricIndicesExist).to.be(true);
});
it('updates a single alias', async () => {
it('applies a single top-level update to an existing source', async () => {
const creationResponse = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -270,13 +270,9 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
mutation: updateSourceMutation,
variables: {
sourceId: 'default',
changes: [
{
setAliases: {
metricAlias: 'metricbeat-**',
},
},
],
sourceProperties: {
metricAlias: 'metricbeat-**',
},
},
});
@ -292,11 +288,56 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(status.metricIndicesExist).to.be(true);
});
it('updates a single field', async () => {
it('applies a single nested field update to an existing source', async () => {
const creationResponse = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
source: {
sourceProperties: {
name: 'NAME',
fields: {
host: 'HOST',
},
},
sourceId: 'default',
},
});
const { version: initialVersion, updatedAt: createdAt } =
creationResponse.data && creationResponse.data.createSource.source;
expect(initialVersion).to.be.a('string');
expect(createdAt).to.be.greaterThan(0);
const updateResponse = await client.mutate<any>({
mutation: updateSourceMutation,
variables: {
sourceId: 'default',
sourceProperties: {
fields: {
container: 'UPDATED_CONTAINER',
},
},
},
});
const { version, updatedAt, configuration } =
updateResponse.data && updateResponse.data.updateSource.source;
expect(version).to.be.a('string');
expect(version).to.not.be(initialVersion);
expect(updatedAt).to.be.greaterThan(createdAt);
expect(configuration.fields.container).to.be('UPDATED_CONTAINER');
expect(configuration.fields.host).to.be('HOST');
expect(configuration.fields.pod).to.be('kubernetes.pod.uid');
expect(configuration.fields.tiebreaker).to.be('_doc');
expect(configuration.fields.timestamp).to.be('@timestamp');
});
it('applies a log column update to an existing source', async () => {
const creationResponse = await client.mutate<any>({
mutation: createSourceMutation,
variables: {
sourceProperties: {
name: 'NAME',
},
sourceId: 'default',
@ -313,13 +354,16 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
mutation: updateSourceMutation,
variables: {
sourceId: 'default',
changes: [
{
setFields: {
container: 'UPDATED_CONTAINER',
sourceProperties: {
logColumns: [
{
fieldColumn: {
id: 'ADDED_COLUMN_ID',
field: 'ADDED_COLUMN_FIELD',
},
},
},
],
],
},
},
});
@ -329,11 +373,13 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
expect(version).to.be.a('string');
expect(version).to.not.be(initialVersion);
expect(updatedAt).to.be.greaterThan(createdAt);
expect(configuration.fields.container).to.be('UPDATED_CONTAINER');
expect(configuration.fields.host).to.be('host.name');
expect(configuration.fields.pod).to.be('kubernetes.pod.uid');
expect(configuration.fields.tiebreaker).to.be('_doc');
expect(configuration.fields.timestamp).to.be('@timestamp');
expect(configuration.logColumns).to.have.length(1);
expect(configuration.logColumns[0]).to.have.key('fieldColumn');
expect(configuration.logColumns[0].fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID');
expect(configuration.logColumns[0].fieldColumn).to.have.property(
'field',
'ADDED_COLUMN_FIELD'
);
});
});
});
@ -343,32 +389,23 @@ const sourcesTests: KbnTestProvider = ({ getService }) => {
export default sourcesTests;
const createSourceMutation = gql`
mutation createSource($sourceId: ID!, $source: CreateSourceInput!) {
createSource(id: $sourceId, source: $source) {
mutation createSource($sourceId: ID!, $sourceProperties: UpdateSourceInput!) {
createSource(id: $sourceId, sourceProperties: $sourceProperties) {
source {
id
version
updatedAt
...InfraSourceFields
configuration {
name
description
metricAlias
logAlias
fields {
container
host
pod
tiebreaker
timestamp
}
...SourceConfigurationFields
}
status {
logIndicesExist
metricIndicesExist
...SourceStatusFields
}
}
}
}
${sharedFragments.InfraSourceFields}
${sourceConfigurationFieldsFragment}
${sourceStatusFieldsFragment}
`;
const deleteSourceMutation = gql`
@ -380,30 +417,21 @@ const deleteSourceMutation = gql`
`;
const updateSourceMutation = gql`
mutation updateSource($sourceId: ID!, $changes: [UpdateSourceInput!]!) {
updateSource(id: $sourceId, changes: $changes) {
mutation updateSource($sourceId: ID!, $sourceProperties: UpdateSourceInput!) {
updateSource(id: $sourceId, sourceProperties: $sourceProperties) {
source {
id
version
updatedAt
...InfraSourceFields
configuration {
name
description
metricAlias
logAlias
fields {
container
host
pod
tiebreaker
timestamp
}
...SourceConfigurationFields
}
status {
logIndicesExist
metricIndicesExist
...SourceStatusFields
}
}
}
}
${sharedFragments.InfraSourceFields}
${sourceConfigurationFieldsFragment}
${sourceStatusFieldsFragment}
`;

View file

@ -7,6 +7,11 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import {
UpdateSourceInput,
UpdateSourceResult,
} from '../../../../plugins/infra/public/graphql/types';
export interface EsArchiver {
load(name: string): void;
unload(name: string): void;
@ -18,6 +23,13 @@ interface InfraOpsGraphQLClientFactoryOptions {
basePath: string;
}
interface InfraOpsSourceConfigurationService {
createConfiguration(
sourceId: string,
sourceProperties: UpdateSourceInput
): UpdateSourceResult['source']['version'];
}
export interface KbnTestProviderOptions {
getService(name: string): any;
getService(name: 'esArchiver'): EsArchiver;
@ -25,6 +37,7 @@ export interface KbnTestProviderOptions {
getService(
name: 'infraOpsGraphQLClientFactory'
): (options: InfraOpsGraphQLClientFactoryOptions) => ApolloClient<InMemoryCache>;
getService(name: 'infraOpsSourceConfiguration'): InfraOpsSourceConfigurationService;
}
export type KbnTestProvider = (options: KbnTestProviderOptions) => void;

View file

@ -12,6 +12,7 @@ import {
InfraOpsGraphQLClientProvider,
InfraOpsGraphQLClientFactoryProvider,
SiemGraphQLProvider,
InfraOpsSourceConfigurationProvider,
} from './services';
import {
@ -41,6 +42,7 @@ export default async function ({ readConfigFile }) {
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
siemGraphQLClient: SiemGraphQLProvider,
infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider,
es: EsProvider,
esArchiver: kibanaCommonConfig.get('services.esArchiver'),
usageAPI: UsageAPIProvider,

View file

@ -10,3 +10,4 @@ export { SupertestWithoutAuthProvider } from './supertest_without_auth';
export { UsageAPIProvider } from './usage_api';
export { InfraOpsGraphQLClientProvider, InfraOpsGraphQLClientFactoryProvider } from './infraops_graphql_client';
export { SiemGraphQLProvider } from './siem_graphql_client';
export { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration';

View file

@ -39,6 +39,17 @@ export function InfraOpsGraphQLClientFactoryProvider({ getService }) {
introspectionQueryResultData,
}),
}),
defaultOptions: {
query: {
fetchPolicy: 'no-cache'
},
watchQuery: {
fetchPolicy: 'no-cache'
},
mutate: {
fetchPolicy: 'no-cache'
},
},
link: httpLink,
});
};

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
const createSourceMutation = gql`
mutation createSource($sourceId: ID!, $sourceProperties: UpdateSourceInput!) {
createSource(id: $sourceId, sourceProperties: $sourceProperties) {
source {
id
version
configuration {
name
logColumns {
... on InfraSourceTimestampLogColumn {
timestampColumn {
id
}
}
... on InfraSourceMessageLogColumn {
messageColumn {
id
}
}
... on InfraSourceFieldLogColumn {
fieldColumn {
id
field
}
}
}
}
}
}
}
`;
export function InfraOpsSourceConfigurationProvider({ getService }) {
const client = getService('infraOpsGraphQLClient');
const log = getService('log');
return {
async createConfiguration(sourceId, sourceProperties) {
log.debug(`Creating Infra UI source configuration "${sourceId}" with properties ${JSON.stringify(sourceProperties)}`);
const response = await client.mutate({
mutation: createSourceMutation,
variables: {
sourceProperties,
sourceId,
},
});
return response.data.createSource.source.version;
},
};
}

View file

@ -4,13 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) => {
const esArchiver = getService('esArchiver');
const infraLogStream = getService('infraLogStream');
const infraSourceConfigurationFlyout = getService('infraSourceConfigurationFlyout');
const pageObjects = getPageObjects(['common', 'infraLogs']);
const pageObjects = getPageObjects(['infraLogs']);
describe('Logs Page', () => {
before(async () => {
@ -29,12 +32,13 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
});
it('renders the log stream', async () => {
await pageObjects.common.navigateToApp('infraLogs');
await pageObjects.infraLogs.navigateTo();
await pageObjects.infraLogs.getLogStream();
});
it('can change the log indices to a pattern that matches nothing', async () => {
await pageObjects.infraLogs.openSourceConfigurationFlyout();
await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab();
const nameInput = await infraSourceConfigurationFlyout.getNameInput();
await nameInput.clearValueWithKeyboard({ charByChar: true });
@ -54,6 +58,7 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
it('can change the log indices back to a pattern that matches something', async () => {
await pageObjects.infraLogs.openSourceConfigurationFlyout();
await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab();
const logIndicesInput = await infraSourceConfigurationFlyout.getLogIndicesInput();
await logIndicesInput.clearValueWithKeyboard({ charByChar: true });
@ -66,6 +71,44 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
it('renders the log stream again', async () => {
await pageObjects.infraLogs.getLogStream();
});
it('renders the default log columns', async () => {
const logStreamEntries = await infraLogStream.getStreamEntries();
expect(logStreamEntries.length).to.be.greaterThan(0);
const firstLogStreamEntry = logStreamEntries[0];
const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry(
firstLogStreamEntry
);
expect(logStreamEntryColumns).to.have.length(3);
});
it('can change the log columns', async () => {
await pageObjects.infraLogs.openSourceConfigurationFlyout();
await infraSourceConfigurationFlyout.switchToLogsTab();
await infraSourceConfigurationFlyout.removeAllLogColumns();
await infraSourceConfigurationFlyout.addTimestampLogColumn();
await infraSourceConfigurationFlyout.addFieldLogColumn('host.name');
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the changed log columns', async () => {
const logStreamEntries = await infraLogStream.getStreamEntries();
expect(logStreamEntries.length).to.be.greaterThan(0);
const firstLogStreamEntry = logStreamEntries[0];
const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry(
firstLogStreamEntry
);
expect(logStreamEntryColumns).to.have.length(2);
});
});
});
};

View file

@ -39,6 +39,7 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
it('can change the metric indices to a pattern that matches nothing', async () => {
await pageObjects.infraHome.openSourceConfigurationFlyout();
await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab();
const nameInput = await infraSourceConfigurationFlyout.getNameInput();
await nameInput.clearValueWithKeyboard({ charByChar: true });
@ -58,6 +59,7 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
it('can change the log indices back to a pattern that matches something', async () => {
await pageObjects.infraHome.openSourceConfigurationFlyout();
await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab();
const metricIndicesInput = await infraSourceConfigurationFlyout.getMetricIndicesInput();
await metricIndicesInput.clearValueWithKeyboard({ charByChar: true });

View file

@ -59,6 +59,7 @@ import {
UserMenuProvider,
UptimeProvider,
InfraSourceConfigurationFlyoutProvider,
InfraLogStreamProvider,
} from './services';
import {
@ -148,6 +149,7 @@ export default async function ({ readConfigFile }) {
uptime: UptimeProvider,
rollup: RollupPageProvider,
infraSourceConfigurationFlyout: InfraSourceConfigurationFlyoutProvider,
infraLogStream: InfraLogStreamProvider,
},
// just like services, PageObjects are defined as a map of

View file

@ -9,12 +9,18 @@
import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
export function InfraLogsPageProvider({ getService }: KibanaFunctionalTestDefaultProviders) {
export function InfraLogsPageProvider({
getPageObjects,
getService,
}: KibanaFunctionalTestDefaultProviders) {
const testSubjects = getService('testSubjects');
// const find = getService('find');
// const browser = getService('browser');
const pageObjects = getPageObjects(['common']);
return {
async navigateTo() {
await pageObjects.common.navigateToApp('infraLogs');
},
async getLogStream() {
return await testSubjects.find('logStream');
},

View file

@ -13,3 +13,4 @@ export { GrokDebuggerProvider } from './grok_debugger';
export { UserMenuProvider } from './user_menu';
export { UptimeProvider } from './uptime';
export { InfraSourceConfigurationFlyoutProvider } from './infra_source_configuration_flyout';
export { InfraLogStreamProvider } from './infra_log_stream';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper';
export function InfraLogStreamProvider({ getService }: KibanaFunctionalTestDefaultProviders) {
const testSubjects = getService('testSubjects');
return {
async getStreamEntries(): Promise<WebElementWrapper[]> {
return await testSubjects.findAll('streamEntry');
},
async getLogColumnsOfStreamEntry(
entryElement: WebElementWrapper
): Promise<WebElementWrapper[]> {
return await testSubjects.findAllDescendant('logColumn', entryElement);
},
};
}

View file

@ -5,38 +5,106 @@
*/
import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper';
export function InfraSourceConfigurationFlyoutProvider({
getService,
}: KibanaFunctionalTestDefaultProviders) {
const find = getService('find');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
return {
/**
* Tab navigation
*/
async switchToIndicesAndFieldsTab() {
await (await find.descendantDisplayedByCssSelector(
'#indicesAndFieldsTab',
await this.getFlyout()
)).click();
await testSubjects.find('sourceConfigurationNameSectionTitle');
},
async switchToLogsTab() {
await (await find.descendantDisplayedByCssSelector(
'#logsTab',
await this.getFlyout()
)).click();
await testSubjects.find('sourceConfigurationLogColumnsSectionTitle');
},
/**
* Indices and fields
*/
async getNameInput() {
return await testSubjects.find('nameInput');
return await testSubjects.findDescendant('nameInput', await this.getFlyout());
},
async getLogIndicesInput() {
return await testSubjects.find('logIndicesInput');
return await testSubjects.findDescendant('logIndicesInput', await this.getFlyout());
},
async getMetricIndicesInput() {
return await testSubjects.find('metricIndicesInput');
return await testSubjects.findDescendant('metricIndicesInput', await this.getFlyout());
},
/**
* Logs
*/
async getAddLogColumnButton(): Promise<WebElementWrapper> {
return await testSubjects.findDescendant('addLogColumnButton', await this.getFlyout());
},
async getAddLogColumnPopover(): Promise<WebElementWrapper> {
return await testSubjects.find('addLogColumnPopover');
},
async addTimestampLogColumn() {
await (await this.getAddLogColumnButton()).click();
await (await testSubjects.findDescendant(
'addTimestampLogColumn',
await this.getAddLogColumnPopover()
)).click();
},
async addFieldLogColumn(fieldName: string) {
await (await this.getAddLogColumnButton()).click();
const popover = await this.getAddLogColumnPopover();
await (await testSubjects.findDescendant('fieldSearchInput', popover)).type(fieldName);
await (await testSubjects.findDescendant(`addFieldLogColumn:${fieldName}`, popover)).click();
},
async getLogColumnPanels(): Promise<WebElementWrapper[]> {
return await testSubjects.findAllDescendant('logColumnPanel', await this.getFlyout());
},
async removeLogColumn(columnIndex: number) {
const logColumnPanel = (await this.getLogColumnPanels())[columnIndex];
await (await testSubjects.findDescendant('removeLogColumnButton', logColumnPanel)).click();
await testSubjects.waitForDeleted(logColumnPanel);
},
async removeAllLogColumns() {
for (const _ of await this.getLogColumnPanels()) {
await this.removeLogColumn(0);
}
},
/**
* Form and flyout
*/
async getFlyout() {
return await testSubjects.find('sourceConfigurationFlyout');
},
async saveConfiguration() {
await testSubjects.click('updateSourceConfigurationButton');
await (await testSubjects.findDescendant(
'updateSourceConfigurationButton',
await this.getFlyout()
)).click();
await retry.try(async () => {
const element = await testSubjects.find('updateSourceConfigurationButton');
const element = await testSubjects.findDescendant(
'updateSourceConfigurationButton',
await this.getFlyout()
);
return !(await element.isEnabled());
});
},
async closeFlyout() {
const flyout = await testSubjects.find('sourceConfigurationFlyout');
await testSubjects.click('closeFlyoutButton');
const flyout = await this.getFlyout();
await (await testSubjects.findDescendant('closeFlyoutButton', flyout)).click();
await testSubjects.waitForDeleted(flyout);
},
};

View file

@ -5,10 +5,16 @@
*/
import { EsArchiver } from '../../../src/es_archiver';
import { InfraLogStreamProvider } from '../functional/services/infra_log_stream';
import { InfraSourceConfigurationFlyoutProvider } from '../functional/services/infra_source_configuration_flyout';
import { UptimeProvider } from '../functional/services/uptime';
export interface KibanaFunctionalTestDefaultProviders {
getService(serviceName: 'esArchiver'): EsArchiver;
getService(serviceName: 'infraLogStream'): ReturnType<typeof InfraLogStreamProvider>;
getService(
serviceName: 'infraSourceConfigurationFlyout'
): ReturnType<typeof InfraSourceConfigurationFlyoutProvider>;
getService(serviceName: 'uptime'): ReturnType<typeof UptimeProvider>;
getService(serviceName: string): any;
getPageObjects(pageObjectNames: string[]): any;