[Logs UI] Store Logs UI settings in a dedicated infrastructure-monitoring-log-view saved object (#125014)

This commit is contained in:
Felix Stürmer 2022-03-31 16:08:01 +02:00 committed by GitHub
parent 30ca2f5c50
commit a736c44e21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 3783 additions and 2771 deletions

View file

@ -56,6 +56,7 @@ const previouslyRegisteredTypes = [
'fleet-preconfiguration-deletion-record',
'graph-workspace',
'index-pattern',
'infrastructure-monitoring-log-view',
'infrastructure-ui-source',
'ingest-agent-policies',
'ingest-outputs',

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const parameters = {
docs: {
source: {
type: 'code', // without this, stories in mdx documents freeze the browser
},
},
};

View file

@ -7,7 +7,7 @@
import * as rt from 'io-ts';
import { logEntryCursorRT, logEntryRT } from '../../log_entry';
import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration';
import { logViewColumnConfigurationRT } from '../../log_views';
export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights';
@ -21,7 +21,7 @@ export const logEntriesHighlightsBaseRequestRT = rt.intersection([
rt.partial({
query: rt.union([rt.string, rt.null]),
size: rt.number,
columns: rt.array(logSourceColumnConfigurationRT),
columns: rt.array(logViewColumnConfigurationRT),
}),
]);

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const LOG_SOURCE_CONFIGURATION_PATH_PREFIX = '/api/infra/log_source_configurations';
export const LOG_SOURCE_CONFIGURATION_PATH = `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/{sourceId}`;
export const getLogSourceConfigurationPath = (sourceId: string) =>
`${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/${sourceId}`;

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { badRequestErrorRT, forbiddenErrorRT, routeTimingMetadataRT } from '../shared';
import { logSourceConfigurationRT } from '../../log_sources/log_source_configuration';
/**
* request
*/
export const getLogSourceConfigurationRequestParamsRT = rt.type({
// the id of the source configuration
sourceId: rt.string,
});
export type GetLogSourceConfigurationRequestParams = rt.TypeOf<
typeof getLogSourceConfigurationRequestParamsRT
>;
/**
* response
*/
export const getLogSourceConfigurationSuccessResponsePayloadRT = rt.intersection([
rt.type({
data: logSourceConfigurationRT,
}),
rt.partial({
timing: routeTimingMetadataRT,
}),
]);
export type GetLogSourceConfigurationSuccessResponsePayload = rt.TypeOf<
typeof getLogSourceConfigurationSuccessResponsePayloadRT
>;
export const getLogSourceConfigurationErrorResponsePayloadRT = rt.union([
badRequestErrorRT,
forbiddenErrorRT,
]);
export type GetLogSourceConfigurationErrorReponsePayload = rt.TypeOf<
typeof getLogSourceConfigurationErrorResponsePayloadRT
>;
export const getLogSourceConfigurationResponsePayloadRT = rt.union([
getLogSourceConfigurationSuccessResponsePayloadRT,
getLogSourceConfigurationErrorResponsePayloadRT,
]);
export type GetLogSourceConfigurationReponsePayload = rt.TypeOf<
typeof getLogSourceConfigurationResponsePayloadRT
>;

View file

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { routeTimingMetadataRT } from '../shared';
import { getLogSourceConfigurationPath, LOG_SOURCE_CONFIGURATION_PATH } from './common';
export const LOG_SOURCE_STATUS_PATH_SUFFIX = 'status';
export const LOG_SOURCE_STATUS_PATH = `${LOG_SOURCE_CONFIGURATION_PATH}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`;
export const getLogSourceStatusPath = (sourceId: string) =>
`${getLogSourceConfigurationPath(sourceId)}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`;
/**
* request
*/
export const getLogSourceStatusRequestParamsRT = rt.type({
// the id of the source configuration
sourceId: rt.string,
});
export type GetLogSourceStatusRequestParams = rt.TypeOf<typeof getLogSourceStatusRequestParamsRT>;
/**
* response
*/
const logIndexFieldRT = rt.strict({
name: rt.string,
type: rt.string,
searchable: rt.boolean,
aggregatable: rt.boolean,
});
export type LogIndexField = rt.TypeOf<typeof logIndexFieldRT>;
const logIndexStatusRT = rt.keyof({
missing: null,
empty: null,
available: null,
});
export type LogIndexStatus = rt.TypeOf<typeof logIndexStatusRT>;
const logSourceStatusRT = rt.strict({
logIndexStatus: logIndexStatusRT,
indices: rt.string,
});
export type LogSourceStatus = rt.TypeOf<typeof logSourceStatusRT>;
export const getLogSourceStatusSuccessResponsePayloadRT = rt.intersection([
rt.type({
data: logSourceStatusRT,
}),
rt.partial({
timing: routeTimingMetadataRT,
}),
]);
export type GetLogSourceStatusSuccessResponsePayload = rt.TypeOf<
typeof getLogSourceStatusSuccessResponsePayloadRT
>;

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { badRequestErrorRT, forbiddenErrorRT } from '../shared';
import { getLogSourceConfigurationSuccessResponsePayloadRT } from './get_log_source_configuration';
import { logSourceConfigurationPropertiesRT } from '../../log_sources/log_source_configuration';
/**
* request
*/
export const patchLogSourceConfigurationRequestParamsRT = rt.type({
// the id of the source configuration
sourceId: rt.string,
});
export type PatchLogSourceConfigurationRequestParams = rt.TypeOf<
typeof patchLogSourceConfigurationRequestParamsRT
>;
const logSourceConfigurationProperiesPatchRT = rt.partial({
...logSourceConfigurationPropertiesRT.type.props,
fields: rt.partial(logSourceConfigurationPropertiesRT.type.props.fields.type.props),
});
export type LogSourceConfigurationPropertiesPatch = rt.TypeOf<
typeof logSourceConfigurationProperiesPatchRT
>;
export const patchLogSourceConfigurationRequestBodyRT = rt.type({
data: logSourceConfigurationProperiesPatchRT,
});
export type PatchLogSourceConfigurationRequestBody = rt.TypeOf<
typeof patchLogSourceConfigurationRequestBodyRT
>;
/**
* response
*/
export const patchLogSourceConfigurationSuccessResponsePayloadRT =
getLogSourceConfigurationSuccessResponsePayloadRT;
export type PatchLogSourceConfigurationSuccessResponsePayload = rt.TypeOf<
typeof patchLogSourceConfigurationSuccessResponsePayloadRT
>;
export const patchLogSourceConfigurationResponsePayloadRT = rt.union([
patchLogSourceConfigurationSuccessResponsePayloadRT,
badRequestErrorRT,
forbiddenErrorRT,
]);
export type PatchLogSourceConfigurationReponsePayload = rt.TypeOf<
typeof patchLogSourceConfigurationResponsePayloadRT
>;

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const LOG_VIEW_URL_PREFIX = '/api/infra/log_views';
export const LOG_VIEW_URL = `${LOG_VIEW_URL_PREFIX}/{logViewId}`;
export const getLogViewUrl = (logViewId: string) => `${LOG_VIEW_URL_PREFIX}/${logViewId}`;

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { logViewRT } from '../../log_views';
export const getLogViewRequestParamsRT = rt.type({
// the id of the log view
logViewId: rt.string,
});
export const getLogViewResponsePayloadRT = rt.type({
data: logViewRT,
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
export * from './get_log_source_configuration';
export * from './get_log_source_status';
export * from './patch_log_source_configuration';
export * from './common';
export { getLogViewUrl, LOG_VIEW_URL } from './common';
export * from './get_log_view';
export * from './put_log_view';

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { logViewAttributesRT, logViewRT } from '../../log_views';
export const putLogViewRequestParamsRT = rt.type({
logViewId: rt.string,
});
export const putLogViewRequestPayloadRT = rt.type({
attributes: rt.partial(logViewAttributesRT.type.props),
});
export type PutLogViewRequestPayload = rt.TypeOf<typeof putLogViewRequestPayloadRT>;
export const putLogViewResponsePayloadRT = rt.type({
data: logViewRT,
});

View file

@ -1,91 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
export const logSourceConfigurationOriginRT = rt.keyof({
fallback: null,
internal: null,
stored: null,
});
export type LogSourceConfigurationOrigin = rt.TypeOf<typeof logSourceConfigurationOriginRT>;
const logSourceFieldsConfigurationRT = rt.strict({
message: rt.array(rt.string),
});
const logSourceCommonColumnConfigurationRT = rt.strict({
id: rt.string,
});
const logSourceTimestampColumnConfigurationRT = rt.strict({
timestampColumn: logSourceCommonColumnConfigurationRT,
});
const logSourceMessageColumnConfigurationRT = rt.strict({
messageColumn: logSourceCommonColumnConfigurationRT,
});
export const logSourceFieldColumnConfigurationRT = rt.strict({
fieldColumn: rt.intersection([
logSourceCommonColumnConfigurationRT,
rt.strict({
field: rt.string,
}),
]),
});
export const logSourceColumnConfigurationRT = rt.union([
logSourceTimestampColumnConfigurationRT,
logSourceMessageColumnConfigurationRT,
logSourceFieldColumnConfigurationRT,
]);
export type LogSourceColumnConfiguration = rt.TypeOf<typeof logSourceColumnConfigurationRT>;
// Kibana index pattern
export const logIndexPatternReferenceRT = rt.type({
type: rt.literal('index_pattern'),
indexPatternId: rt.string,
});
export type LogIndexPatternReference = rt.TypeOf<typeof logIndexPatternReferenceRT>;
// Legacy support
export const logIndexNameReferenceRT = rt.type({
type: rt.literal('index_name'),
indexName: rt.string,
});
export type LogIndexNameReference = rt.TypeOf<typeof logIndexNameReferenceRT>;
export const logIndexReferenceRT = rt.union([logIndexPatternReferenceRT, logIndexNameReferenceRT]);
export type LogIndexReference = rt.TypeOf<typeof logIndexReferenceRT>;
export const logSourceConfigurationPropertiesRT = rt.strict({
name: rt.string,
description: rt.string,
logIndices: logIndexReferenceRT,
fields: logSourceFieldsConfigurationRT,
logColumns: rt.array(logSourceColumnConfigurationRT),
});
export type LogSourceConfigurationProperties = rt.TypeOf<typeof logSourceConfigurationPropertiesRT>;
export const logSourceConfigurationRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
origin: logSourceConfigurationOriginRT,
configuration: logSourceConfigurationPropertiesRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export type LogSourceConfiguration = rt.TypeOf<typeof logSourceConfigurationRT>;

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common';
import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../constants';
import { ResolveLogSourceConfigurationError } from './errors';
import {
LogSourceColumnConfiguration,
LogSourceConfigurationProperties,
} from './log_source_configuration';
export interface ResolvedLogSourceConfiguration {
name: string;
description: string;
indices: string;
timestampField: string;
tiebreakerField: string;
messageField: string[];
fields: DataView['fields'];
runtimeMappings: estypes.MappingRuntimeFields;
columns: LogSourceColumnConfiguration[];
}
export const resolveLogSourceConfiguration = async (
sourceConfiguration: LogSourceConfigurationProperties,
indexPatternsService: DataViewsContract
): Promise<ResolvedLogSourceConfiguration> => {
if (sourceConfiguration.logIndices.type === 'index_name') {
return await resolveLegacyReference(sourceConfiguration, indexPatternsService);
} else {
return await resolveKibanaIndexPatternReference(sourceConfiguration, indexPatternsService);
}
};
const resolveLegacyReference = async (
sourceConfiguration: LogSourceConfigurationProperties,
indexPatternsService: DataViewsContract
): Promise<ResolvedLogSourceConfiguration> => {
if (sourceConfiguration.logIndices.type !== 'index_name') {
throw new Error('This function can only resolve legacy references');
}
const indices = sourceConfiguration.logIndices.indexName;
const fields = await indexPatternsService
.getFieldsForWildcard({
pattern: indices,
allowNoIndex: true,
})
.catch((error) => {
throw new ResolveLogSourceConfigurationError(
`Failed to fetch fields for indices "${indices}": ${error}`,
error
);
});
return {
indices: sourceConfiguration.logIndices.indexName,
timestampField: TIMESTAMP_FIELD,
tiebreakerField: TIEBREAKER_FIELD,
messageField: sourceConfiguration.fields.message,
// @ts-ignore
fields,
runtimeMappings: {},
columns: sourceConfiguration.logColumns,
name: sourceConfiguration.name,
description: sourceConfiguration.description,
};
};
const resolveKibanaIndexPatternReference = async (
sourceConfiguration: LogSourceConfigurationProperties,
indexPatternsService: DataViewsContract
): Promise<ResolvedLogSourceConfiguration> => {
if (sourceConfiguration.logIndices.type !== 'index_pattern') {
throw new Error('This function can only resolve Kibana Index Pattern references');
}
const { indexPatternId } = sourceConfiguration.logIndices;
const indexPattern = await indexPatternsService.get(indexPatternId).catch((error) => {
throw new ResolveLogSourceConfigurationError(
`Failed to fetch index pattern "${indexPatternId}": ${error}`,
error
);
});
return {
indices: indexPattern.title,
timestampField: indexPattern.timeFieldName ?? TIMESTAMP_FIELD,
tiebreakerField: TIEBREAKER_FIELD,
messageField: ['message'],
fields: indexPattern.fields,
runtimeMappings: resolveRuntimeMappings(indexPattern),
columns: sourceConfiguration.logColumns,
name: sourceConfiguration.name,
description: sourceConfiguration.description,
};
};
// this might take other sources of runtime fields into account in the future
const resolveRuntimeMappings = (indexPattern: DataView): estypes.MappingRuntimeFields => {
return indexPattern.getRuntimeMappings();
};

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { defaultSourceConfiguration } from '../source_configuration/defaults';
import { LogViewAttributes, LogViewsStaticConfig } from './types';
export const defaultLogViewId = 'default';
export const defaultLogViewAttributes: LogViewAttributes = {
name: 'Log View',
description: 'A default log view',
logIndices: {
type: 'index_name',
indexName: 'logs-*,filebeat-*',
},
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',
},
},
],
};
export const defaultLogViewsStaticConfig: LogViewsStaticConfig = {
messageFields: defaultSourceConfiguration.fields.message,
};

View file

@ -7,34 +7,34 @@
/* eslint-disable max-classes-per-file */
export class ResolveLogSourceConfigurationError extends Error {
export class ResolveLogViewError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'ResolveLogSourceConfigurationError';
this.name = 'ResolveLogViewError';
}
}
export class FetchLogSourceConfigurationError extends Error {
export class FetchLogViewError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'FetchLogSourceConfigurationError';
this.name = 'FetchLogViewError';
}
}
export class FetchLogSourceStatusError extends Error {
export class FetchLogViewStatusError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'FetchLogSourceStatusError';
this.name = 'FetchLogViewStatusError';
}
}
export class PatchLogSourceConfigurationError extends Error {
export class PutLogViewError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'PatchLogSourceConfigurationError';
this.name = 'PutLogViewError';
}
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export * from './defaults';
export * from './errors';
export * from './log_source_configuration';
export * from './resolved_log_source_configuration';
export * from './resolved_log_view';
export * from './types';

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { defaultLogViewAttributes } from './defaults';
import { LogView, LogViewAttributes, LogViewOrigin } from './types';
export const createLogViewMock = (
id: string,
origin: LogViewOrigin = 'stored',
attributeOverrides: Partial<LogViewAttributes> = {},
updatedAt?: number,
version?: string
): LogView => ({
id,
origin,
attributes: {
...defaultLogViewAttributes,
...attributeOverrides,
},
updatedAt,
version,
});

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataViewsContract, fieldList } from 'src/plugins/data_views/common';
import { createStubDataView } from 'src/plugins/data_views/common/stubs';
import { defaultLogViewsStaticConfig } from './defaults';
import { ResolvedLogView, resolveLogView } from './resolved_log_view';
import { LogViewAttributes } from './types';
export const createResolvedLogViewMock = (
resolvedLogViewOverrides: Partial<ResolvedLogView> = {}
): ResolvedLogView => ({
name: 'LOG VIEW',
description: 'LOG VIEW DESCRIPTION',
indices: 'log-indices-*',
timestampField: 'TIMESTAMP_FIELD',
tiebreakerField: 'TIEBREAKER_FIELD',
messageField: ['MESSAGE_FIELD'],
fields: fieldList(),
runtimeMappings: {
runtime_field: {
type: 'keyword',
script: {
source: 'emit("runtime value")',
},
},
},
columns: [
{ timestampColumn: { id: 'TIMESTAMP_COLUMN_ID' } },
{
fieldColumn: {
id: 'DATASET_COLUMN_ID',
field: 'event.dataset',
},
},
{
messageColumn: { id: 'MESSAGE_COLUMN_ID' },
},
],
...resolvedLogViewOverrides,
});
export const createResolvedLogViewMockFromAttributes = (logViewAttributes: LogViewAttributes) =>
resolveLogView(
logViewAttributes,
{
get: async () => createStubDataView({ spec: {} }),
getFieldsForWildcard: async () => [],
} as unknown as DataViewsContract,
defaultLogViewsStaticConfig
);

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
DataView,
DataViewsContract,
FieldSpec,
} from '../../../../../src/plugins/data_views/common';
import { TIEBREAKER_FIELD, TIMESTAMP_FIELD } from '../constants';
import { ResolveLogViewError } from './errors';
import { LogViewAttributes, LogViewColumnConfiguration, LogViewsStaticConfig } from './types';
export type ResolvedLogViewField = FieldSpec;
export interface ResolvedLogView {
name: string;
description: string;
indices: string;
timestampField: string;
tiebreakerField: string;
messageField: string[];
fields: ResolvedLogViewField[];
runtimeMappings: estypes.MappingRuntimeFields;
columns: LogViewColumnConfiguration[];
}
export const resolveLogView = async (
logViewAttributes: LogViewAttributes,
dataViewsService: DataViewsContract,
config: LogViewsStaticConfig
): Promise<ResolvedLogView> => {
if (logViewAttributes.logIndices.type === 'index_name') {
return await resolveLegacyReference(logViewAttributes, dataViewsService, config);
} else {
return await resolveDataViewReference(logViewAttributes, dataViewsService);
}
};
const resolveLegacyReference = async (
logViewAttributes: LogViewAttributes,
dataViewsService: DataViewsContract,
config: LogViewsStaticConfig
): Promise<ResolvedLogView> => {
if (logViewAttributes.logIndices.type !== 'index_name') {
throw new Error('This function can only resolve legacy references');
}
const indices = logViewAttributes.logIndices.indexName;
const fields = await dataViewsService
.getFieldsForWildcard({
pattern: indices,
allowNoIndex: true,
})
.catch((error) => {
throw new ResolveLogViewError(
`Failed to fetch fields for indices "${indices}": ${error}`,
error
);
});
return {
indices: logViewAttributes.logIndices.indexName,
timestampField: TIMESTAMP_FIELD,
tiebreakerField: TIEBREAKER_FIELD,
messageField: config.messageFields,
fields,
runtimeMappings: {},
columns: logViewAttributes.logColumns,
name: logViewAttributes.name,
description: logViewAttributes.description,
};
};
const resolveDataViewReference = async (
logViewAttributes: LogViewAttributes,
dataViewsService: DataViewsContract
): Promise<ResolvedLogView> => {
if (logViewAttributes.logIndices.type !== 'data_view') {
throw new Error('This function can only resolve Kibana data view references');
}
const { dataViewId } = logViewAttributes.logIndices;
const dataView = await dataViewsService.get(dataViewId).catch((error) => {
throw new ResolveLogViewError(`Failed to fetch data view "${dataViewId}": ${error}`, error);
});
return {
indices: dataView.title,
timestampField: dataView.timeFieldName ?? TIMESTAMP_FIELD,
tiebreakerField: TIEBREAKER_FIELD,
messageField: ['message'],
fields: dataView.fields,
runtimeMappings: resolveRuntimeMappings(dataView),
columns: logViewAttributes.logColumns,
name: logViewAttributes.name,
description: logViewAttributes.description,
};
};
// this might take other sources of runtime fields into account in the future
const resolveRuntimeMappings = (dataView: DataView): estypes.MappingRuntimeFields => {
return dataView.getRuntimeMappings();
};

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
export interface LogViewsStaticConfig {
messageFields: string[];
}
export const logViewOriginRT = rt.keyof({
stored: null,
internal: null,
'infra-source-stored': null,
'infra-source-internal': null,
'infra-source-fallback': null,
});
export type LogViewOrigin = rt.TypeOf<typeof logViewOriginRT>;
// Kibana data views
export const logDataViewReferenceRT = rt.type({
type: rt.literal('data_view'),
dataViewId: rt.string,
});
export type LogDataViewReference = rt.TypeOf<typeof logDataViewReferenceRT>;
// Index name
export const logIndexNameReferenceRT = rt.type({
type: rt.literal('index_name'),
indexName: rt.string,
});
export type LogIndexNameReference = rt.TypeOf<typeof logIndexNameReferenceRT>;
export const logIndexReferenceRT = rt.union([logDataViewReferenceRT, logIndexNameReferenceRT]);
export type LogIndexReference = rt.TypeOf<typeof logIndexReferenceRT>;
const logViewCommonColumnConfigurationRT = rt.strict({
id: rt.string,
});
const logViewTimestampColumnConfigurationRT = rt.strict({
timestampColumn: logViewCommonColumnConfigurationRT,
});
const logViewMessageColumnConfigurationRT = rt.strict({
messageColumn: logViewCommonColumnConfigurationRT,
});
export const logViewFieldColumnConfigurationRT = rt.strict({
fieldColumn: rt.intersection([
logViewCommonColumnConfigurationRT,
rt.strict({
field: rt.string,
}),
]),
});
export const logViewColumnConfigurationRT = rt.union([
logViewTimestampColumnConfigurationRT,
logViewMessageColumnConfigurationRT,
logViewFieldColumnConfigurationRT,
]);
export type LogViewColumnConfiguration = rt.TypeOf<typeof logViewColumnConfigurationRT>;
export const logViewAttributesRT = rt.strict({
name: rt.string,
description: rt.string,
logIndices: logIndexReferenceRT,
logColumns: rt.array(logViewColumnConfigurationRT),
});
export type LogViewAttributes = rt.TypeOf<typeof logViewAttributesRT>;
export const logViewRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
origin: logViewOriginRT,
attributes: logViewAttributesRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
export type LogView = rt.TypeOf<typeof logViewRT>;
export const logViewIndexStatusRT = rt.keyof({
available: null,
empty: null,
missing: null,
unknown: null,
});
export type LogViewIndexStatus = rt.TypeOf<typeof logViewIndexStatusRT>;
export const logViewStatusRT = rt.strict({
index: logViewIndexStatusRT,
});
export type LogViewStatus = rt.TypeOf<typeof logViewStatusRT>;

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface InfraConfig {
alerting: {
inventory_threshold: {
group_by_page_size: number;
};
metric_threshold: {
group_by_page_size: number;
};
};
inventory: {
compositeSize: number;
};
sources?: {
default?: {
fields?: {
message?: string[];
};
};
};
}
export const publicConfigKeys = {
sources: true,
} as const;
export type InfraPublicConfigKey = keyof {
[K in keyof typeof publicConfigKeys as typeof publicConfigKeys[K] extends true ? K : never]: true;
};
export type InfraPublicConfig = Pick<InfraConfig, InfraPublicConfigKey>;

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import * as rt from 'io-ts';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration';
import * as rt from 'io-ts';
import {
logEntryAfterCursorRT,
logEntryBeforeCursorRT,
logEntryCursorRT,
logEntryRT,
} from '../../log_entry';
import { logViewColumnConfigurationRT } from '../../log_views';
import { jsonObjectRT } from '../../typed_json';
import { searchStrategyErrorRT } from '../common/errors';
@ -28,7 +28,7 @@ const logEntriesBaseSearchRequestParamsRT = rt.intersection([
}),
rt.partial({
query: jsonObjectRT,
columns: rt.array(logSourceColumnConfigurationRT),
columns: rt.array(logViewColumnConfigurationRT),
highlightPhrase: rt.string,
}),
]);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LOGS_INDEX_PATTERN, METRICS_INDEX_PATTERN } from '../constants';
import { InfraSourceConfiguration } from './source_configuration';
export const defaultSourceConfiguration: InfraSourceConfiguration = {
name: 'Default',
description: '',
metricAlias: METRICS_INDEX_PATTERN,
logIndices: {
type: 'index_name',
indexName: LOGS_INDEX_PATTERN,
},
fields: {
message: ['message', '@message'],
},
inventoryDefaultView: '0',
metricsExplorerDefaultView: '0',
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',
},
},
],
anomalyThreshold: 50,
};

View file

@ -22,7 +22,6 @@ import * as rt from 'io-ts';
import moment from 'moment';
import { pipe } from 'fp-ts/lib/pipeable';
import { chain } from 'fp-ts/lib/Either';
import { logIndexReferenceRT } from '../log_sources';
export const TimestampFromString = new rt.Type<number, string>(
'TimestampFromString',
@ -103,6 +102,27 @@ export const SourceConfigurationColumnRuntimeType = rt.union([
export type InfraSourceConfigurationColumn = rt.TypeOf<typeof SourceConfigurationColumnRuntimeType>;
/**
* Log indices
*/
// Kibana index pattern
export const logIndexPatternReferenceRT = rt.type({
type: rt.literal('index_pattern'),
indexPatternId: rt.string,
});
export type LogIndexPatternReference = rt.TypeOf<typeof logIndexPatternReferenceRT>;
// Legacy support
export const logIndexNameReferenceRT = rt.type({
type: rt.literal('index_name'),
indexName: rt.string,
});
export type LogIndexNameReference = rt.TypeOf<typeof logIndexNameReferenceRT>;
export const logIndexReferenceRT = rt.union([logIndexPatternReferenceRT, logIndexNameReferenceRT]);
export type LogIndexReference = rt.TypeOf<typeof logIndexReferenceRT>;
/**
* Fields
*/

View file

@ -48,3 +48,5 @@ export type ObjectValues<T> = Array<T[keyof T]>;
export type ObjectEntry<T> = [keyof T, T[keyof T]];
export type ObjectEntries<T> = Array<ObjectEntry<T>>;
export type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Value> ? Value : never;

View file

@ -10,6 +10,7 @@
"embeddable",
"data",
"dataEnhanced",
"dataViews",
"visTypeTimeseries",
"alerting",
"triggersActionsUi",

View file

@ -5,21 +5,21 @@
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import { DataViewField } from 'src/plugins/data_views/common';
import { i18n } from '@kbn/i18n';
import {
EuiPopoverTitle,
EuiFlexItem,
EuiFlexGroup,
EuiPopover,
EuiExpression,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiPopoverTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo, useState } from 'react';
import { FieldSpec } from 'src/plugins/data_views/common';
import { GroupBySelector } from './selector';
interface Props {
selectedGroups?: string[];
fields: DataViewField[];
fields: FieldSpec[];
onChange: (groupBy: string[]) => void;
label?: string;
}

View file

@ -7,12 +7,12 @@
import { EuiComboBox } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { DataViewField } from 'src/plugins/data_views/common';
import { FieldSpec } from 'src/plugins/data_views/common';
interface Props {
selectedGroups?: string[];
onChange: (groupBy: string[]) => void;
fields: DataViewField[];
fields: FieldSpec[];
label: string;
placeholder: string;
}

View file

@ -9,7 +9,7 @@ import React, { useCallback } from 'react';
import { EuiFlexItem, EuiFlexGroup, EuiButtonEmpty, EuiAccordion, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { DataViewField } from 'src/plugins/data_views/common';
import type { ResolvedLogViewField } from '../../../../../common/log_views';
import { Criterion } from './criterion';
import {
PartialRuleParams,
@ -34,7 +34,7 @@ const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCrit
});
interface SharedProps {
fields: DataViewField[];
fields: ResolvedLogViewField[];
criteria?: PartialCriteriaType;
defaultCriterion: PartialCriterionType;
errors: Errors['criteria'];

View file

@ -5,30 +5,29 @@
* 2.0.
*/
import React, { useState, useMemo, useCallback } from 'react';
import {
EuiPopoverTitle,
EuiFlexItem,
EuiFlexGroup,
EuiPopover,
EuiSelect,
EuiFieldNumber,
EuiExpression,
EuiFieldText,
EuiButtonIcon,
EuiFormRow,
EuiComboBox,
EuiExpression,
EuiFieldNumber,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPopover,
EuiPopoverTitle,
EuiSelect,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataViewField } from 'src/plugins/data_views/common';
import { isNumber, isFinite } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types';
import { isFinite, isNumber } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import type { IErrorObject } from '../../../../../../triggers_actions_ui/public';
import {
Comparator,
Criterion as CriterionType,
ComparatorToi18nMap,
Criterion as CriterionType,
} from '../../../../../common/alerting/logs/log_threshold/types';
import type { ResolvedLogViewField } from '../../../../../common/log_views';
const firstCriterionFieldPrefix = i18n.translate(
'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix',
@ -55,7 +54,7 @@ const criterionComparatorValueTitle = i18n.translate(
}
);
const getCompatibleComparatorsForField = (fieldInfo: DataViewField | undefined) => {
const getCompatibleComparatorsForField = (fieldInfo: ResolvedLogViewField | undefined) => {
if (fieldInfo?.type === 'number') {
return [
{ value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] },
@ -83,7 +82,10 @@ const getCompatibleComparatorsForField = (fieldInfo: DataViewField | undefined)
}
};
const getFieldInfo = (fields: DataViewField[], fieldName: string): DataViewField | undefined => {
const getFieldInfo = (
fields: ResolvedLogViewField[],
fieldName: string
): ResolvedLogViewField | undefined => {
return fields.find((field) => {
return field.name === fieldName;
});
@ -91,7 +93,7 @@ const getFieldInfo = (fields: DataViewField[], fieldName: string): DataViewField
interface Props {
idx: number;
fields: DataViewField[];
fields: ResolvedLogViewField[];
criterion: Partial<CriterionType>;
updateCriterion: (idx: number, params: Partial<CriterionType>) => void;
removeCriterion: (idx: number) => void;
@ -117,7 +119,7 @@ export const Criterion: React.FC<Props> = ({
});
}, [fields]);
const fieldInfo: DataViewField | undefined = useMemo(() => {
const fieldInfo: ResolvedLogViewField | undefined = useMemo(() => {
if (criterion.field) {
return getFieldInfo(fields, criterion.field);
} else {

View file

@ -9,30 +9,27 @@ import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eu
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { ResolvedLogViewField } from '../../../../../common/log_views';
import {
RuleTypeParamsExpressionProps,
ForLastExpression,
RuleTypeParamsExpressionProps,
} from '../../../../../../triggers_actions_ui/public';
import {
Comparator,
isOptimizableGroupedThreshold,
isRatioRule,
PartialRuleParams,
PartialCountRuleParams,
PartialCriteria as PartialCriteriaType,
PartialRatioRuleParams,
PartialRuleParams,
ThresholdType,
timeUnitRT,
isOptimizableGroupedThreshold,
} from '../../../../../common/alerting/logs/log_threshold/types';
import { decodeOrThrow } from '../../../../../common/runtime_types';
import { ObjectEntries } from '../../../../../common/utility_types';
import {
LogIndexField,
LogSourceProvider,
useLogSourceContext,
} from '../../../../containers/logs/log_source';
import { useSourceId } from '../../../../containers/source_id';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { LogViewProvider, useLogViewContext } from '../../../../hooks/use_log_view';
import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression';
import { errorsRT } from '../../validation';
import { Criteria } from './criteria';
@ -57,7 +54,7 @@ const DEFAULT_BASE_EXPRESSION = {
const DEFAULT_FIELD = 'log.level';
const createDefaultCriterion = (
availableFields: LogIndexField[],
availableFields: ResolvedLogViewField[],
value: ExpressionCriteria['value']
) =>
availableFields.some((availableField) => availableField.name === DEFAULT_FIELD)
@ -65,7 +62,7 @@ const createDefaultCriterion = (
: { field: undefined, comparator: undefined, value: undefined };
const createDefaultCountRuleParams = (
availableFields: LogIndexField[]
availableFields: ResolvedLogViewField[]
): PartialCountRuleParams => ({
...DEFAULT_BASE_EXPRESSION,
count: {
@ -76,7 +73,7 @@ const createDefaultCountRuleParams = (
});
const createDefaultRatioRuleParams = (
availableFields: LogIndexField[]
availableFields: ResolvedLogViewField[]
): PartialRatioRuleParams => ({
...DEFAULT_BASE_EXPRESSION,
count: {
@ -93,8 +90,10 @@ export const ExpressionEditor: React.FC<
RuleTypeParamsExpressionProps<PartialRuleParams, LogsContextMeta>
> = (props) => {
const isInternal = props.metadata?.isInternal ?? false;
const [sourceId] = useSourceId();
const { http } = useKibana().services;
const [logViewId] = useSourceId();
const {
services: { http, logViews },
} = useKibanaContextForPlugin(); // injected during alert registration
return (
<>
@ -103,42 +102,28 @@ export const ExpressionEditor: React.FC<
<Editor {...props} />
</SourceStatusWrapper>
) : (
<LogSourceProvider
sourceId={sourceId}
fetch={http!.fetch}
indexPatternsService={props.data.indexPatterns}
>
<LogViewProvider logViewId={logViewId} logViews={logViews.client} fetch={http.fetch}>
<SourceStatusWrapper {...props}>
<Editor {...props} />
</SourceStatusWrapper>
</LogSourceProvider>
</LogViewProvider>
)}
</>
);
};
export const SourceStatusWrapper: React.FC = ({ children }) => {
const {
initialize,
loadSource,
isLoadingSourceConfiguration,
hasFailedLoadingSource,
isUninitialized,
} = useLogSourceContext();
useMount(() => {
initialize();
});
const { load, isLoading, hasFailedLoading, isUninitialized } = useLogViewContext();
return (
<>
{isLoadingSourceConfiguration || isUninitialized ? (
{isLoading || isUninitialized ? (
<div>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="l" />
<EuiSpacer size="m" />
</div>
) : hasFailedLoadingSource ? (
) : hasFailedLoading ? (
<EuiCallOut
title={i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusError', {
defaultMessage: 'Sorry, there was a problem loading field information',
@ -146,7 +131,7 @@ export const SourceStatusWrapper: React.FC = ({ children }) => {
color="danger"
iconType="alert"
>
<EuiButton onClick={loadSource} iconType="refresh">
<EuiButton onClick={load} iconType="refresh">
{i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain', {
defaultMessage: 'Try again',
})}
@ -164,7 +149,7 @@ export const Editor: React.FC<RuleTypeParamsExpressionProps<PartialRuleParams, L
) => {
const { setRuleParams, ruleParams, errors } = props;
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
const { sourceId, resolvedSourceConfiguration } = useLogSourceContext();
const { logViewId, resolvedLogView } = useLogViewContext();
const {
criteria: criteriaErrors,
@ -174,24 +159,24 @@ export const Editor: React.FC<RuleTypeParamsExpressionProps<PartialRuleParams, L
} = useMemo(() => decodeOrThrow(errorsRT)(errors), [errors]);
const supportedFields = useMemo(() => {
if (resolvedSourceConfiguration?.fields) {
return resolvedSourceConfiguration.fields.filter((field) => {
if (resolvedLogView?.fields) {
return resolvedLogView.fields.filter((field) => {
return (field.type === 'string' || field.type === 'number') && field.searchable;
});
} else {
return [];
}
}, [resolvedSourceConfiguration]);
}, [resolvedLogView]);
const groupByFields = useMemo(() => {
if (resolvedSourceConfiguration?.fields) {
return resolvedSourceConfiguration.fields.filter((field) => {
if (resolvedLogView?.fields) {
return resolvedLogView.fields.filter((field) => {
return field.type === 'string' && field.aggregatable;
});
} else {
return [];
}
}, [resolvedSourceConfiguration]);
}, [resolvedLogView]);
const updateThreshold = useCallback(
(thresholdParams) => {
@ -276,7 +261,7 @@ export const Editor: React.FC<RuleTypeParamsExpressionProps<PartialRuleParams, L
defaultCriterion={defaultCountAlertParams.criteria[0]}
errors={criteriaErrors}
ruleParams={ruleParams}
sourceId={sourceId}
sourceId={logViewId}
updateCriteria={updateCriteria}
/>
) : null;

View file

@ -6,16 +6,24 @@
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import {
LOG_DOCUMENT_COUNT_RULE_TYPE_ID,
PartialRuleParams,
} from '../../../common/alerting/logs/log_threshold';
import { createLazyComponentWithKibanaContext } from '../../hooks/use_kibana';
import { InfraClientCoreSetup } from '../../types';
import { formatRuleData } from './rule_data_formatters';
import { validateExpression } from './validation';
export function createLogThresholdRuleType(): ObservabilityRuleTypeModel<PartialRuleParams> {
export function createLogThresholdRuleType(
core: InfraClientCoreSetup
): ObservabilityRuleTypeModel<PartialRuleParams> {
const ruleParamsExpression = createLazyComponentWithKibanaContext(
core,
() => import('./components/expression_editor/editor')
);
return {
id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID,
description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', {
@ -25,7 +33,7 @@ export function createLogThresholdRuleType(): ObservabilityRuleTypeModel<Partial
documentationUrl(docLinks) {
return `${docLinks.links.observability.logsThreshold}`;
},
ruleParamsExpression: React.lazy(() => import('./components/expression_editor/editor')),
ruleParamsExpression,
validate: validateExpression,
defaultActionMessage: i18n.translate(
'xpack.infra.logs.alerting.threshold.defaultActionMessage',

View file

@ -6,7 +6,7 @@
*/
import { AppMountParameters, CoreStart } from 'kibana/public';
import React, { useMemo } from 'react';
import React from 'react';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
import {
KibanaContextProvider,
@ -14,11 +14,11 @@ import {
useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
import { createKibanaContextForPlugin } from '../hooks/use_kibana';
import { InfraClientStartDeps } from '../types';
import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider';
import { NavigationWarningPromptProvider } from '../../../observability/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
import { useKibanaContextForPluginProvider } from '../hooks/use_kibana';
import { InfraClientStartDeps, InfraClientStartExports } from '../types';
import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider';
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
export const CommonInfraProviders: React.FC<{
@ -45,6 +45,7 @@ export const CommonInfraProviders: React.FC<{
export interface CoreProvidersProps {
core: CoreStart;
pluginStart: InfraClientStartExports;
plugins: InfraClientStartDeps;
theme$: AppMountParameters['theme$'];
}
@ -52,16 +53,18 @@ export interface CoreProvidersProps {
export const CoreProviders: React.FC<CoreProvidersProps> = ({
children,
core,
pluginStart,
plugins,
theme$,
}) => {
const { Provider: KibanaContextProviderForPlugin } = useMemo(
() => createKibanaContextForPlugin(core, plugins),
[core, plugins]
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
core,
plugins,
pluginStart
);
return (
<KibanaContextProviderForPlugin services={{ ...core, ...plugins }}>
<KibanaContextProviderForPlugin services={{ ...core, ...plugins, ...pluginStart }}>
<core.i18n.Context>
<KibanaThemeProvider theme$={theme$}>{children}</KibanaThemeProvider>
</core.i18n.Context>

View file

@ -16,13 +16,14 @@ import '../index.scss';
import { NotFoundPage } from '../pages/404';
import { LinkToLogsPage } from '../pages/link_to/link_to_logs';
import { LogsPage } from '../pages/logs';
import { InfraClientStartDeps } from '../types';
import { InfraClientStartDeps, InfraClientStartExports } from '../types';
import { CommonInfraProviders, CoreProviders } from './common_providers';
import { prepareMountElement } from './common_styles';
export const renderApp = (
core: CoreStart,
plugins: InfraClientStartDeps,
pluginStart: InfraClientStartExports,
{ element, history, setHeaderActionMenu, theme$ }: AppMountParameters
) => {
const storage = new Storage(window.localStorage);
@ -35,6 +36,7 @@ export const renderApp = (
storage={storage}
history={history}
plugins={plugins}
pluginStart={pluginStart}
setHeaderActionMenu={setHeaderActionMenu}
theme$={theme$}
/>,
@ -49,15 +51,16 @@ export const renderApp = (
const LogsApp: React.FC<{
core: CoreStart;
history: History<unknown>;
pluginStart: InfraClientStartExports;
plugins: InfraClientStartDeps;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
storage: Storage;
theme$: AppMountParameters['theme$'];
}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => {
}> = ({ core, history, pluginStart, plugins, setHeaderActionMenu, storage, theme$ }) => {
const uiCapabilities = core.application.capabilities;
return (
<CoreProviders core={core} plugins={plugins} theme$={theme$}>
<CoreProviders core={core} pluginStart={pluginStart} plugins={plugins} theme$={theme$}>
<CommonInfraProviders
appName="Logs UI"
setHeaderActionMenu={setHeaderActionMenu}

View file

@ -16,7 +16,7 @@ import '../index.scss';
import { NotFoundPage } from '../pages/404';
import { LinkToMetricsPage } from '../pages/link_to/link_to_metrics';
import { InfrastructurePage } from '../pages/metrics';
import { InfraClientStartDeps } from '../types';
import { InfraClientStartDeps, InfraClientStartExports } from '../types';
import { RedirectWithQueryParams } from '../utils/redirect_with_query_params';
import { CommonInfraProviders, CoreProviders } from './common_providers';
import { prepareMountElement } from './common_styles';
@ -24,6 +24,7 @@ import { prepareMountElement } from './common_styles';
export const renderApp = (
core: CoreStart,
plugins: InfraClientStartDeps,
pluginStart: InfraClientStartExports,
{ element, history, setHeaderActionMenu, theme$ }: AppMountParameters
) => {
const storage = new Storage(window.localStorage);
@ -35,6 +36,7 @@ export const renderApp = (
core={core}
history={history}
plugins={plugins}
pluginStart={pluginStart}
setHeaderActionMenu={setHeaderActionMenu}
storage={storage}
theme$={theme$}
@ -50,15 +52,16 @@ export const renderApp = (
const MetricsApp: React.FC<{
core: CoreStart;
history: History<unknown>;
pluginStart: InfraClientStartExports;
plugins: InfraClientStartDeps;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
storage: Storage;
theme$: AppMountParameters['theme$'];
}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => {
}> = ({ core, history, pluginStart, plugins, setHeaderActionMenu, storage, theme$ }) => {
const uiCapabilities = core.application.capabilities;
return (
<CoreProviders core={core} plugins={plugins} theme$={theme$}>
<CoreProviders core={core} pluginStart={pluginStart} plugins={plugins} theme$={theme$}>
<CommonInfraProviders
appName="Metrics UI"
setHeaderActionMenu={setHeaderActionMenu}

View file

@ -13,7 +13,7 @@ import type {
NodeMetricsTableFetchMock,
SourceResponseMock,
} from '../test_helpers';
import { createCoreProvidersPropsMock } from '../test_helpers';
import { createStartServicesAccessorMock } from '../test_helpers';
import { createLazyContainerMetricsTable } from './create_lazy_container_metrics_table';
import IntegratedContainerMetricsTable from './integrated_container_metrics_table';
import { metricByField } from './use_container_metrics_table';
@ -41,8 +41,8 @@ describe('ContainerMetricsTable', () => {
describe('createLazyContainerMetricsTable', () => {
it('should lazily load and render the table', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const LazyContainerMetricsTable = createLazyContainerMetricsTable(coreProvidersPropsMock);
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
const LazyContainerMetricsTable = createLazyContainerMetricsTable(getStartServices);
render(<LazyContainerMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
@ -62,7 +62,7 @@ describe('ContainerMetricsTable', () => {
describe('IntegratedContainerMetricsTable', () => {
it('should render a single row of data', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock);
const { findByText } = render(
<IntegratedContainerMetricsTable

View file

@ -6,26 +6,33 @@
*/
import React, { lazy, Suspense } from 'react';
import type { CoreProvidersProps } from '../../../apps/common_providers';
import { InfraClientStartServices } from '../../../types';
import type { SourceProviderProps, UseNodeMetricsTableOptions } from '../shared';
const LazyIntegratedContainerMetricsTable = lazy(
() => import('./integrated_container_metrics_table')
);
export function createLazyContainerMetricsTable(coreProvidersProps: CoreProvidersProps) {
export function createLazyContainerMetricsTable(getStartServices: () => InfraClientStartServices) {
return ({
timerange,
filterClauseDsl,
sourceId,
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => (
<Suspense fallback={null}>
<LazyIntegratedContainerMetricsTable
{...coreProvidersProps}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => {
const [core, plugins, pluginStart] = getStartServices();
return (
<Suspense fallback={null}>
<LazyIntegratedContainerMetricsTable
core={core}
plugins={plugins}
pluginStart={pluginStart}
theme$={core.theme.theme$}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
};
}

View file

@ -6,24 +6,31 @@
*/
import React, { lazy, Suspense } from 'react';
import type { CoreProvidersProps } from '../../../apps/common_providers';
import { InfraClientStartServices } from '../../../types';
import type { SourceProviderProps, UseNodeMetricsTableOptions } from '../shared';
const LazyIntegratedHostMetricsTable = lazy(() => import('./integrated_host_metrics_table'));
export function createLazyHostMetricsTable(coreProvidersProps: CoreProvidersProps) {
export function createLazyHostMetricsTable(getStartServices: () => InfraClientStartServices) {
return ({
timerange,
filterClauseDsl,
sourceId,
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => (
<Suspense fallback={null}>
<LazyIntegratedHostMetricsTable
{...coreProvidersProps}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => {
const [core, plugins, pluginStart] = getStartServices();
return (
<Suspense fallback={null}>
<LazyIntegratedHostMetricsTable
core={core}
plugins={plugins}
pluginStart={pluginStart}
theme$={core.theme.theme$}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
};
}

View file

@ -13,7 +13,7 @@ import type {
NodeMetricsTableFetchMock,
SourceResponseMock,
} from '../test_helpers';
import { createCoreProvidersPropsMock } from '../test_helpers';
import { createStartServicesAccessorMock } from '../test_helpers';
import { createLazyHostMetricsTable } from './create_lazy_host_metrics_table';
import IntegratedHostMetricsTable from './integrated_host_metrics_table';
import { metricByField } from './use_host_metrics_table';
@ -41,8 +41,8 @@ describe('HostMetricsTable', () => {
describe('createLazyHostMetricsTable', () => {
it('should lazily load and render the table', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const LazyHostMetricsTable = createLazyHostMetricsTable(coreProvidersPropsMock);
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
const LazyHostMetricsTable = createLazyHostMetricsTable(getStartServices);
render(<LazyHostMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
@ -62,7 +62,7 @@ describe('HostMetricsTable', () => {
describe('IntegratedHostMetricsTable', () => {
it('should render a single row of data', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock);
const { findByText } = render(
<IntegratedHostMetricsTable

View file

@ -6,24 +6,31 @@
*/
import React, { lazy, Suspense } from 'react';
import type { CoreProvidersProps } from '../../../apps/common_providers';
import { InfraClientStartServices } from '../../../types';
import type { SourceProviderProps, UseNodeMetricsTableOptions } from '../shared';
const LazyIntegratedPodMetricsTable = lazy(() => import('./integrated_pod_metrics_table'));
export function createLazyPodMetricsTable(coreProvidersProps: CoreProvidersProps) {
export function createLazyPodMetricsTable(getStartServices: () => InfraClientStartServices) {
return ({
timerange,
filterClauseDsl,
sourceId,
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => (
<Suspense fallback={null}>
<LazyIntegratedPodMetricsTable
{...coreProvidersProps}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
}: UseNodeMetricsTableOptions & Partial<SourceProviderProps>) => {
const [core, plugins, pluginStart] = getStartServices();
return (
<Suspense fallback={null}>
<LazyIntegratedPodMetricsTable
core={core}
plugins={plugins}
pluginStart={pluginStart}
theme$={core.theme.theme$}
sourceId={sourceId || 'default'}
timerange={timerange}
filterClauseDsl={filterClauseDsl}
/>
</Suspense>
);
};
}

View file

@ -13,7 +13,7 @@ import type {
NodeMetricsTableFetchMock,
SourceResponseMock,
} from '../test_helpers';
import { createCoreProvidersPropsMock } from '../test_helpers';
import { createStartServicesAccessorMock } from '../test_helpers';
import { createLazyPodMetricsTable } from './create_lazy_pod_metrics_table';
import IntegratedPodMetricsTable from './integrated_pod_metrics_table';
import { metricByField } from './use_pod_metrics_table';
@ -41,8 +41,8 @@ describe('PodMetricsTable', () => {
describe('createLazyPodMetricsTable', () => {
it('should lazily load and render the table', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const LazyPodMetricsTable = createLazyPodMetricsTable(coreProvidersPropsMock);
const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock);
const LazyPodMetricsTable = createLazyPodMetricsTable(getStartServices);
render(<LazyPodMetricsTable timerange={timerange} filterClauseDsl={filterClauseDsl} />);
@ -62,7 +62,7 @@ describe('PodMetricsTable', () => {
describe('IntegratedPodMetricsTable', () => {
it('should render a single row of data', async () => {
const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock);
const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock);
const { findByText } = render(
<IntegratedPodMetricsTable

View file

@ -11,7 +11,11 @@ import { coreMock } from '../../../../../../src/core/public/mocks';
import type { MetricsExplorerResponse } from '../../../common/http_api/metrics_explorer';
import type { MetricsSourceConfigurationResponse } from '../../../common/metrics_sources';
import type { CoreProvidersProps } from '../../apps/common_providers';
import type { InfraClientStartDeps } from '../../types';
import type {
InfraClientStartDeps,
InfraClientStartExports,
InfraClientStartServices,
} from '../../types';
export type SourceResponseMock = DeepPartial<MetricsSourceConfigurationResponse>;
export type DataResponseMock = DeepPartial<MetricsExplorerResponse>;
@ -20,19 +24,26 @@ export type NodeMetricsTableFetchMock = (
options: HttpFetchOptions
) => Promise<SourceResponseMock | DataResponseMock>;
export function createCoreProvidersPropsMock(fetchMock: NodeMetricsTableFetchMock) {
export function createStartServicesAccessorMock(fetchMock: NodeMetricsTableFetchMock) {
const core = coreMock.createStart();
// @ts-expect-error core.http.fetch has overloads, Jest/TypeScript only picks the first definition when mocking
core.http.fetch.mockImplementation(fetchMock);
const coreProvidersPropsMock: CoreProvidersProps = {
core,
pluginStart: {} as InfraClientStartExports,
plugins: {} as InfraClientStartDeps,
theme$: core.theme.theme$,
};
const getStartServices = (): InfraClientStartServices => [
coreProvidersPropsMock.core,
coreProvidersPropsMock.plugins,
coreProvidersPropsMock.pluginStart,
];
return {
coreProvidersPropsMock,
fetch: core.http.fetch,
getStartServices,
};
}

View file

@ -1,158 +1,6 @@
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks';
import { defer, of, Subject } from 'rxjs';
import { delay } from 'rxjs/operators';
import { I18nProvider } from '@kbn/i18n-react';
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { LOG_ENTRIES_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entries';
import { createIndexPatternMock, createIndexPatternsMock } from '../../hooks/use_kibana_index_patterns.mock';
import { DEFAULT_SOURCE_CONFIGURATION } from '../../test_utils/source_configuration';
import { generateFakeEntries, ENTRIES_EMPTY } from '../../test_utils/entries';
import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme';
import { LogStream } from './';
<!-- Prework -->
export const startTimestamp = 1595145600000;
export const endTimestamp = startTimestamp + 15 * 60 * 1000;
export const dataMock = {
indexPatterns: createIndexPatternsMock(500, [
createIndexPatternMock({
id: 'some-test-id',
title: 'mock-index-pattern-*',
timeFieldName: '@timestamp',
fields: [
{
name: '@timestamp',
type: KBN_FIELD_TYPES.DATE,
searchable: true,
aggregatable: true,
},
{
name: 'event.dataset',
type: KBN_FIELD_TYPES.STRING,
searchable: true,
aggregatable: true,
},
{
name: 'host.name',
type: KBN_FIELD_TYPES.STRING,
searchable: true,
aggregatable: true,
},
{
name: 'log.level',
type: KBN_FIELD_TYPES.STRING,
searchable: true,
aggregatable: true,
},
{
name: 'message',
type: KBN_FIELD_TYPES.STRING,
searchable: true,
aggregatable: true,
},
],
})
]),
search: {
search: ({ params }, options) => {
return defer(() => {
switch (options.strategy) {
case LOG_ENTRIES_SEARCH_STRATEGY:
if (params.after?.time === params.endTimestamp || params.before?.time === params.startTimestamp) {
return of({
id: 'EMPTY_FAKE_RESPONSE',
total: 1,
loaded: 1,
isRunning: false,
isPartial: false,
rawResponse: ENTRIES_EMPTY,
});
} else {
const entries = generateFakeEntries(
200,
params.startTimestamp,
params.endTimestamp,
params.columns || DEFAULT_SOURCE_CONFIGURATION.data.configuration.logColumns
);
return of({
id: 'FAKE_RESPONSE',
total: 1,
loaded: 1,
isRunning: false,
isPartial: false,
rawResponse: {
data: {
entries,
topCursor: entries[0].cursor,
bottomCursor: entries[entries.length - 1].cursor,
hasMoreBefore: false,
},
errors: [],
}
});
}
default:
return of({
id: 'FAKE_RESPONSE',
rawResponse: {},
});
}
}).pipe(delay(2000));
},
},
};
export const fetch = async function (url, params) {
switch (url) {
case '/api/infra/log_source_configurations/default':
return DEFAULT_SOURCE_CONFIGURATION;
case '/api/infra/log_source_configurations/default/status':
return {
data: {
logIndexStatus: 'available',
}
};
default:
return {};
}
};
export const uiSettings = {
get: (setting) => {
switch (setting) {
case 'dateFormat':
return 'MMM D, YYYY @ HH:mm:ss.SSS';
case 'dateFormat:scaled':
return [['', 'HH:mm:ss.SSS']];
}
},
get$: () => {
return new Subject();
},
};
export const Template = (args) => <LogStream {...args} />;
<Meta
title="infra/LogStream"
component={LogStream}
decorators={[
(story) => (
<I18nProvider>
<KibanaContextProvider services={{ data: dataMock, http: { fetch }, uiSettings }}>
{story()}
</KibanaContextProvider>
</I18nProvider>
),
decorateWithGlobalStorybookThemeProviders,
]}
/>
<Meta title="infra/LogStream/Overview" />
# Embeddable `<LogStream />` component
@ -187,11 +35,7 @@ const startTimestamp = endTimestamp - 15 * 60 * 1000; // 15 minutes
This will show a list of log entries between the specified timestamps.
<Canvas>
<Story name="Default" args={{ startTimestamp, endTimestamp }}>
{Template.bind({})}
</Story>
</Canvas>
<Story id="infra-logstream--basic-date-range" />
## Query log entries
@ -246,14 +90,7 @@ By default the component will load at the bottom of the list, showing the newest
/>
```
<Canvas>
<Story
name="CenteredView"
args={{ startTimestamp, endTimestamp, center: { time: 1595146275000, tiebreaker: 150 } }}
>
{Template.bind({})}
</Story>
</Canvas>
<Story id="infra-logstream--centered-on-log-entry" />
## Highlight a specific entry
@ -263,11 +100,7 @@ The component can highlight a specific line via the `highlight` prop. It takes t
<LogStream startTimestamp={startTimestamp} endTimestamp={endTimestamp} highlight="entry-197" />
```
<Canvas>
<Story name="HighlightedEntry" args={{ startTimestamp, endTimestamp, highlight: 'entry-197' }}>
{Template.bind({})}
</Story>
</Canvas>
<Story id="infra-logstream--highlighted-log-entry" />
## Column configuration
@ -298,23 +131,7 @@ The easiest way is to specify what columns you want with the `columns` prop.
/>
```
<Canvas>
<Story
name="CustomColumns"
args={{
startTimestamp,
endTimestamp,
columns: [
{ type: 'timestamp' },
{ type: 'field', field: 'log.level' },
{ type: 'field', field: 'host.name' },
{ type: 'message' },
],
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Story id="infra-logstream--custom-columns" />
The rendering of the column headers and the cell contents can also be customized with the following properties:
@ -389,57 +206,25 @@ The rendering of the column headers and the cell contents can also be customized
/>
```
<Canvas>
<Story
name="CustomColumnRendering"
args={{
startTimestamp,
endTimestamp,
columns: [
{ type: 'timestamp', header: 'When?' },
{
type: 'field',
field: 'log.level',
header: false,
width: 24,
render: (value) => {
switch (value) {
case 'debug':
return '🐞';
case 'info':
return '';
case 'warn':
return '⚠️';
case 'error':
return '❌';
}
},
},
{ type: 'message' },
],
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Story id="infra-logstream--custom-column-rendering" />
### With a source configuration
### With a static log view configuration
The infra plugin has the concept of a "source configuration", a collection of settings that apply to the logs and metrics UIs. The component uses the source configuration to determine which indices to query or what columns to show.
The infra plugin has the concept of a "log view", a collection of settings that apply to the logs UI. The component uses the log view to determine which indices to query or what columns to show.
The `<LogStream />` component will use the `"default"` source configuration. If you want to use your own configuration, you need to first create it when you initialize your plugin, and then specify it in the `<LogStream />` component with the `sourceId` prop.
The `<LogStream />` component will use the `"default"` log view. If you want to use your own log view, you need to first create it when you initialize your plugin, and then specify it in the `<LogStream />` component with the `sourceId` prop.
```tsx
// Your `server/plugin.ts`
class MyPlugin {
// ...
setup(core, plugins) {
plugins.infra.defineInternalSourceConfiguration(
'my_source', // ID for your source configuration
plugins.infra.logViews.defineInternalLogView(
'my_log_view', // ID for your log view
{
name: 'some-name',
description: 'some description',
logIndices: { // Also accepts an `index_pattern` type with `indexPatternId`
logIndices: { // Also accepts a `data_view` type with `dataViewId`
type: 'index_name',
indexName: 'some-index',
},
@ -463,4 +248,4 @@ class MyPlugin {
### Setting component height
It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume.
It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume.

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { I18nProvider } from '@kbn/i18n-react';
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme';
import { LogStream, LogStreamProps } from './log_stream';
import { decorateWithKibanaContext } from './log_stream.story_decorators';
const startTimestamp = 1595145600000;
const endTimestamp = startTimestamp + 15 * 60 * 1000;
export default {
title: 'infra/LogStream',
component: LogStream,
decorators: [
(wrappedStory) => <I18nProvider>{wrappedStory()}</I18nProvider>,
decorateWithKibanaContext,
decorateWithGlobalStorybookThemeProviders,
],
parameters: {
layout: 'padded',
},
args: {
startTimestamp,
endTimestamp,
},
} as Meta;
const LogStreamStoryTemplate: Story<LogStreamProps> = (args) => <LogStream {...args} />;
export const BasicDateRange = LogStreamStoryTemplate.bind({});
export const CenteredOnLogEntry = LogStreamStoryTemplate.bind({});
CenteredOnLogEntry.args = {
center: { time: 1595146275000, tiebreaker: 150 },
};
export const HighlightedLogEntry = LogStreamStoryTemplate.bind({});
HighlightedLogEntry.args = {
highlight: 'entry-197',
};
export const CustomColumns = LogStreamStoryTemplate.bind({});
CustomColumns.args = {
columns: [
{ type: 'timestamp' },
{ type: 'field', field: 'log.level' },
{ type: 'field', field: 'host.name' },
{ type: 'message' },
],
};
export const CustomColumnRendering = LogStreamStoryTemplate.bind({});
CustomColumnRendering.args = {
columns: [
{ type: 'timestamp', header: 'When?' },
{
type: 'field',
field: 'log.level',
header: false,
width: 24,
render: (value) => {
switch (value) {
case 'debug':
return '🐞';
case 'info':
return '';
case 'warn':
return '⚠️';
case 'error':
return '❌';
}
},
},
{ type: 'message' },
],
};

View file

@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { StoryContext } from '@storybook/react';
import React from 'react';
import { defer, of, Subject } from 'rxjs';
import { delay } from 'rxjs/operators';
import {
ENHANCED_ES_SEARCH_STRATEGY,
ES_SEARCH_STRATEGY,
FieldSpec,
} from '../../../../../../src/plugins/data/common';
import {
IEsSearchResponse,
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
} from '../../../../../../src/plugins/data/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { getLogViewResponsePayloadRT } from '../../../common/http_api/log_views';
import { defaultLogViewAttributes } from '../../../common/log_views';
import {
LogEntriesSearchResponsePayload,
LOG_ENTRIES_SEARCH_STRATEGY,
} from '../../../common/search_strategies/log_entries/log_entries';
import { ENTRIES_EMPTY, generateFakeEntries } from '../../test_utils/entries';
export const decorateWithKibanaContext = <StoryFnReactReturnType extends React.ReactNode>(
wrappedStory: () => StoryFnReactReturnType,
_storyContext: StoryContext
) => {
const data = {
dataViews: {
getFieldsForWildcard: async (): Promise<FieldSpec[]> => {
return [];
},
},
search: {
search: ({ params }: IKibanaSearchRequest, options?: ISearchOptions) => {
return defer(() => {
switch (options?.strategy) {
case LOG_ENTRIES_SEARCH_STRATEGY:
if (
params.after?.time === params.endTimestamp ||
params.before?.time === params.startTimestamp
) {
return of<IKibanaSearchResponse<LogEntriesSearchResponsePayload>>({
id: 'MOCK_LOG_ENTRIES_RESPONSE',
total: 1,
loaded: 1,
isRunning: false,
isPartial: false,
rawResponse: ENTRIES_EMPTY,
});
} else {
const entries = generateFakeEntries(
200,
params.startTimestamp,
params.endTimestamp,
params.columns || defaultLogViewAttributes.logColumns
);
return of<IKibanaSearchResponse<LogEntriesSearchResponsePayload>>({
id: 'MOCK_LOG_ENTRIES_RESPONSE',
total: 1,
loaded: 1,
isRunning: false,
isPartial: false,
rawResponse: {
data: {
entries,
topCursor: entries[0].cursor,
bottomCursor: entries[entries.length - 1].cursor,
hasMoreBefore: false,
},
errors: [],
},
});
}
case undefined:
case ES_SEARCH_STRATEGY:
case ENHANCED_ES_SEARCH_STRATEGY:
return of<IEsSearchResponse>({
id: 'MOCK_INDEX_CHECK_RESPONSE',
total: 1,
loaded: 1,
isRunning: false,
isPartial: false,
rawResponse: {
_shards: {
failed: 0,
successful: 1,
total: 1,
},
hits: {
hits: [],
total: 1,
},
timed_out: false,
took: 1,
},
});
default:
return of<IKibanaSearchResponse>({
id: 'FAKE_RESPONSE',
rawResponse: {},
});
}
}).pipe(delay(2000));
},
},
};
const http = {
get: async (path: string) => {
switch (path) {
case '/api/infra/log_views/default':
return getLogViewResponsePayloadRT.encode({
data: {
id: 'default',
origin: 'stored',
attributes: defaultLogViewAttributes,
},
});
default:
return {};
}
},
};
const uiSettings = {
get: (setting: string) => {
switch (setting) {
case 'dateFormat':
return 'MMM D, YYYY @ HH:mm:ss.SSS';
case 'dateFormat:scaled':
return [['', 'HH:mm:ss.SSS']];
}
},
get$: () => new Subject(),
};
return (
<KibanaContextProvider services={{ data, http, uiSettings }}>
{wrappedStory()}
</KibanaContextProvider>
);
};

View file

@ -13,8 +13,10 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { LogEntryCursor } from '../../../common/log_entry';
import { useLogSource } from '../../containers/logs/log_source';
import { defaultLogViewsStaticConfig } from '../../../common/log_views';
import { BuiltEsQuery, useLogStream } from '../../containers/logs/log_stream';
import { useLogView } from '../../hooks/use_log_view';
import { LogViewsClient } from '../../services/log_views';
import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration';
import { useKibanaQuerySettings } from '../../utils/use_kibana_query_settings';
import { ScrollableLogTextStreamView } from '../logging/log_text_stream';
@ -97,9 +99,10 @@ export const LogStreamContent: React.FC<LogStreamContentProps> = ({
[columns]
);
// source boilerplate
const { services } = useKibana<LogStreamPluginDeps>();
if (!services?.http?.fetch || !services?.data?.indexPatterns) {
const {
services: { http, data },
} = useKibana<LogStreamPluginDeps>();
if (http == null || data == null) {
throw new Error(
`<LogStream /> cannot access kibana core services.
@ -111,32 +114,37 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac
const kibanaQuerySettings = useKibanaQuerySettings();
const logViews = useMemo(
() => new LogViewsClient(data.dataViews, http, data.search.search, defaultLogViewsStaticConfig),
[data.dataViews, data.search.search, http]
);
const {
derivedIndexPattern,
isLoading: isLoadingSource,
loadSource,
sourceConfiguration,
} = useLogSource({
sourceId,
fetch: services.http.fetch,
indexPatternsService: services.data.indexPatterns,
derivedDataView,
isLoading: isLoadingLogView,
load: loadLogView,
resolvedLogView,
} = useLogView({
logViewId: sourceId,
logViews,
fetch: http.fetch,
});
const parsedQuery = useMemo<BuiltEsQuery | undefined>(() => {
if (typeof query === 'object' && 'bool' in query) {
return mergeBoolQueries(
query,
buildEsQuery(derivedIndexPattern, [], filters ?? [], kibanaQuerySettings)
buildEsQuery(derivedDataView, [], filters ?? [], kibanaQuerySettings)
);
} else {
return buildEsQuery(
derivedIndexPattern,
derivedDataView,
coerceToQueries(query),
filters ?? [],
kibanaQuerySettings
);
}
}, [derivedIndexPattern, filters, kibanaQuerySettings, query]);
}, [derivedDataView, filters, kibanaQuerySettings, query]);
// Internal state
const {
@ -158,8 +166,8 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac
});
const columnConfigurations = useMemo(() => {
return sourceConfiguration ? customColumns ?? sourceConfiguration.configuration.logColumns : [];
}, [sourceConfiguration, customColumns]);
return resolvedLogView ? customColumns ?? resolvedLogView.columns : [];
}, [resolvedLogView, customColumns]);
const streamItems = useMemo(
() =>
@ -173,8 +181,8 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac
// Component lifetime
useEffect(() => {
loadSource();
}, [loadSource]);
loadLogView();
}, [loadLogView]);
useEffect(() => {
fetchEntries();
@ -207,7 +215,7 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac
items={streamItems}
scale="medium"
wrap={true}
isReloading={isLoadingSource || isLoadingEntries}
isReloading={isLoadingLogView || isLoadingEntries}
isLoadingMore={isLoadingMore}
hasMoreBeforeStart={hasMoreBefore}
hasMoreAfterEnd={hasMoreAfter}

View file

@ -18,7 +18,7 @@ import {
} from '../../../../../../src/plugins/embeddable/public';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
import { CoreProviders } from '../../apps/common_providers';
import { InfraClientStartDeps } from '../../types';
import { InfraClientStartDeps, InfraClientStartExports } from '../../types';
import { datemathToEpochMillis } from '../../utils/datemath';
import { LazyLogStreamWrapper } from './lazy_log_stream_wrapper';
@ -38,6 +38,7 @@ export class LogStreamEmbeddable extends Embeddable<LogStreamEmbeddableInput> {
constructor(
private core: CoreStart,
private pluginDeps: InfraClientStartDeps,
private pluginStart: InfraClientStartExports,
initialInput: LogStreamEmbeddableInput,
parent?: IContainer
) {
@ -78,7 +79,12 @@ export class LogStreamEmbeddable extends Embeddable<LogStreamEmbeddableInput> {
}
ReactDOM.render(
<CoreProviders core={this.core} plugins={this.pluginDeps} theme$={this.core.theme.theme$}>
<CoreProviders
core={this.core}
plugins={this.pluginDeps}
pluginStart={this.pluginStart}
theme$={this.core.theme.theme$}
>
<EuiThemeProvider>
<div style={{ width: '100%' }}>
<LazyLogStreamWrapper

View file

@ -5,13 +5,12 @@
* 2.0.
*/
import { StartServicesAccessor } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import {
EmbeddableFactoryDefinition,
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
import { InfraClientStartDeps } from '../../types';
import { InfraClientStartServicesAccessor } from '../../types';
import {
LogStreamEmbeddable,
LogStreamEmbeddableInput,
@ -23,7 +22,7 @@ export class LogStreamEmbeddableFactoryDefinition
{
public readonly type = LOG_STREAM_EMBEDDABLE;
constructor(private getStartServices: StartServicesAccessor<InfraClientStartDeps>) {}
constructor(private getStartServices: InfraClientStartServicesAccessor) {}
public async isEditable() {
const [{ application }] = await this.getStartServices();
@ -31,8 +30,8 @@ export class LogStreamEmbeddableFactoryDefinition
}
public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) {
const [core, plugins] = await this.getStartServices();
return new LogStreamEmbeddable(core, plugins, initialInput, parent);
const [core, plugins, pluginStart] = await this.getStartServices();
return new LogStreamEmbeddable(core, plugins, pluginStart, initialInput, parent);
}
public getDisplayName() {

View file

@ -9,12 +9,12 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common';
import {
FetchLogSourceConfigurationError,
FetchLogSourceStatusError,
ResolveLogSourceConfigurationError,
} from '../../../common/log_sources';
import { useLinkProps } from '../../../../observability/public';
import {
FetchLogViewStatusError,
FetchLogViewError,
ResolveLogViewError,
} from '../../../common/log_views';
import { LogsPageTemplate } from '../../pages/logs/page_template';
export const LogSourceErrorPage: React.FC<{
@ -72,7 +72,7 @@ export const LogSourceErrorPage: React.FC<{
};
const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
if (error instanceof ResolveLogSourceConfigurationError) {
if (error instanceof ResolveLogViewError) {
return (
<LogSourceErrorCallout
title={
@ -97,7 +97,7 @@ const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
)}
</LogSourceErrorCallout>
);
} else if (error instanceof FetchLogSourceConfigurationError) {
} else if (error instanceof FetchLogViewError) {
return (
<LogSourceErrorCallout
title={
@ -110,7 +110,7 @@ const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
{`${error.cause?.message ?? error.message}`}
</LogSourceErrorCallout>
);
} else if (error instanceof FetchLogSourceStatusError) {
} else if (error instanceof FetchLogViewStatusError) {
return (
<LogSourceErrorCallout
title={

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { HttpHandler } from 'src/core/public';
import {
getLogSourceConfigurationPath,
getLogSourceConfigurationSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
import { FetchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callFetchLogSourceConfigurationAPI = async (sourceId: string, fetch: HttpHandler) => {
const response = await fetch(getLogSourceConfigurationPath(sourceId), {
method: 'GET',
}).catch((error) => {
throw new FetchLogSourceConfigurationError(
`Failed to fetch log source configuration "${sourceId}": ${error}`,
error
);
});
return decodeOrThrow(
getLogSourceConfigurationSuccessResponsePayloadRT,
(message: string) =>
new FetchLogSourceConfigurationError(
`Failed to decode log source configuration "${sourceId}": ${message}`
)
)(response);
};

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { HttpHandler } from 'src/core/public';
import {
getLogSourceStatusPath,
getLogSourceStatusSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
import { FetchLogSourceStatusError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpHandler) => {
const response = await fetch(getLogSourceStatusPath(sourceId), {
method: 'GET',
}).catch((error) => {
throw new FetchLogSourceStatusError(
`Failed to fetch status for log source "${sourceId}": ${error}`,
error
);
});
return decodeOrThrow(
getLogSourceStatusSuccessResponsePayloadRT,
(message: string) =>
new FetchLogSourceStatusError(
`Failed to decode status for log source "${sourceId}": ${message}`
)
)(response);
};

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { HttpHandler } from 'src/core/public';
import {
getLogSourceConfigurationPath,
patchLogSourceConfigurationSuccessResponsePayloadRT,
patchLogSourceConfigurationRequestBodyRT,
LogSourceConfigurationPropertiesPatch,
} from '../../../../../common/http_api/log_sources';
import { PatchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callPatchLogSourceConfigurationAPI = async (
sourceId: string,
patchedProperties: LogSourceConfigurationPropertiesPatch,
fetch: HttpHandler
) => {
const response = await fetch(getLogSourceConfigurationPath(sourceId), {
method: 'PATCH',
body: JSON.stringify(
patchLogSourceConfigurationRequestBodyRT.encode({
data: patchedProperties,
})
),
}).catch((error) => {
throw new PatchLogSourceConfigurationError(
`Failed to update log source configuration "${sourceId}": ${error}`,
error
);
});
return decodeOrThrow(
patchLogSourceConfigurationSuccessResponsePayloadRT,
(message: string) =>
new PatchLogSourceConfigurationError(
`Failed to decode log source configuration "${sourceId}": ${message}`
)
)(response);
};

View file

@ -1,85 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogSourceConfiguration, LogSourceStatus, useLogSource } from './log_source';
type CreateUseLogSource = (sourceConfiguration?: { sourceId?: string }) => typeof useLogSource;
const defaultSourceId = 'default';
export const createUninitializedUseLogSourceMock: CreateUseLogSource =
({ sourceId = defaultSourceId } = {}) =>
() => ({
derivedIndexPattern: {
fields: [],
title: 'unknown',
},
hasFailedLoading: false,
hasFailedLoadingSource: false,
hasFailedLoadingSourceStatus: false,
hasFailedResolvingSource: false,
initialize: jest.fn(),
isLoading: false,
isLoadingSourceConfiguration: false,
isLoadingSourceStatus: false,
isResolvingSourceConfiguration: false,
isUninitialized: true,
loadSource: jest.fn(),
loadSourceConfiguration: jest.fn(),
latestLoadSourceFailures: [],
resolveSourceFailureMessage: undefined,
loadSourceStatus: jest.fn(),
sourceConfiguration: undefined,
sourceId,
sourceStatus: undefined,
updateSource: jest.fn(),
resolvedSourceConfiguration: undefined,
loadResolveLogSourceConfiguration: jest.fn(),
});
export const createLoadingUseLogSourceMock: CreateUseLogSource =
({ sourceId = defaultSourceId } = {}) =>
(args) => ({
...createUninitializedUseLogSourceMock({ sourceId })(args),
isLoading: true,
isLoadingSourceConfiguration: true,
isLoadingSourceStatus: true,
isResolvingSourceConfiguration: true,
});
export const createLoadedUseLogSourceMock: CreateUseLogSource =
({ sourceId = defaultSourceId } = {}) =>
(args) => ({
...createUninitializedUseLogSourceMock({ sourceId })(args),
sourceConfiguration: createBasicSourceConfiguration(sourceId),
sourceStatus: {
indices: 'test-index',
logIndexStatus: 'available',
},
});
export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfiguration => ({
id: sourceId,
origin: 'stored',
configuration: {
description: `description for ${sourceId}`,
logIndices: {
type: 'index_pattern',
indexPatternId: 'some-id',
},
logColumns: [],
fields: {
message: ['MESSAGE_FIELD'],
},
name: sourceId,
},
});
export const createAvailableSourceStatus = (): LogSourceStatus => ({
indices: 'test-index',
logIndexStatus: 'available',
});

View file

@ -1,209 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import createContainer from 'constate';
import { useCallback, useMemo, useState } from 'react';
import type { HttpHandler } from 'src/core/public';
import { DataViewsContract } from '../../../../../../../src/plugins/data_views/public';
import {
LogIndexField,
LogSourceConfigurationPropertiesPatch,
LogSourceStatus,
} from '../../../../common/http_api/log_sources';
import {
LogSourceConfiguration,
LogSourceConfigurationProperties,
ResolvedLogSourceConfiguration,
resolveLogSourceConfiguration,
ResolveLogSourceConfigurationError,
} from '../../../../common/log_sources';
import { isRejectedPromiseState, useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration';
import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status';
import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration';
export type {
LogIndexField,
LogSourceConfiguration,
LogSourceConfigurationProperties,
LogSourceConfigurationPropertiesPatch,
LogSourceStatus,
};
export { ResolveLogSourceConfigurationError };
export const useLogSource = ({
sourceId,
fetch,
indexPatternsService,
}: {
sourceId: string;
fetch: HttpHandler;
indexPatternsService: DataViewsContract;
}) => {
const [sourceConfiguration, setSourceConfiguration] = useState<
LogSourceConfiguration | undefined
>(undefined);
const [resolvedSourceConfiguration, setResolvedSourceConfiguration] = useState<
ResolvedLogSourceConfiguration | undefined
>(undefined);
const [sourceStatus, setSourceStatus] = useState<LogSourceStatus | undefined>(undefined);
const [loadSourceConfigurationRequest, loadSourceConfiguration] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return (await callFetchLogSourceConfigurationAPI(sourceId, fetch)).data;
},
onResolve: setSourceConfiguration,
},
[sourceId, fetch, indexPatternsService]
);
const [resolveSourceConfigurationRequest, resolveSourceConfiguration] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (unresolvedSourceConfiguration: LogSourceConfigurationProperties) => {
return await resolveLogSourceConfiguration(
unresolvedSourceConfiguration,
indexPatternsService
);
},
onResolve: setResolvedSourceConfiguration,
},
[indexPatternsService]
);
const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
return (await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch)).data;
},
onResolve: setSourceConfiguration,
},
[sourceId, fetch, indexPatternsService]
);
const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return await callFetchLogSourceStatusAPI(sourceId, fetch);
},
onResolve: ({ data }) => setSourceStatus(data),
},
[sourceId, fetch]
);
const derivedIndexPattern = useMemo(
() => ({
fields: resolvedSourceConfiguration?.fields ?? [],
title: resolvedSourceConfiguration?.indices ?? 'unknown',
}),
[resolvedSourceConfiguration]
);
const isLoadingSourceConfiguration = loadSourceConfigurationRequest.state === 'pending';
const isResolvingSourceConfiguration = resolveSourceConfigurationRequest.state === 'pending';
const isLoadingSourceStatus = loadSourceStatusRequest.state === 'pending';
const isUpdatingSourceConfiguration = updateSourceConfigurationRequest.state === 'pending';
const isLoading =
isLoadingSourceConfiguration ||
isResolvingSourceConfiguration ||
isLoadingSourceStatus ||
isUpdatingSourceConfiguration;
const isUninitialized =
loadSourceConfigurationRequest.state === 'uninitialized' ||
resolveSourceConfigurationRequest.state === 'uninitialized' ||
loadSourceStatusRequest.state === 'uninitialized';
const hasFailedLoadingSource = loadSourceConfigurationRequest.state === 'rejected';
const hasFailedResolvingSource = resolveSourceConfigurationRequest.state === 'rejected';
const hasFailedLoadingSourceStatus = loadSourceStatusRequest.state === 'rejected';
const latestLoadSourceFailures = [
loadSourceConfigurationRequest,
resolveSourceConfigurationRequest,
loadSourceStatusRequest,
]
.filter(isRejectedPromiseState)
.map(({ value }) => (value instanceof Error ? value : new Error(`${value}`)));
const hasFailedLoading = latestLoadSourceFailures.length > 0;
const loadSource = useCallback(async () => {
const loadSourceConfigurationPromise = loadSourceConfiguration();
const loadSourceStatusPromise = loadSourceStatus();
const resolveSourceConfigurationPromise = resolveSourceConfiguration(
(await loadSourceConfigurationPromise).configuration
);
return await Promise.all([
loadSourceConfigurationPromise,
resolveSourceConfigurationPromise,
loadSourceStatusPromise,
]);
}, [loadSourceConfiguration, loadSourceStatus, resolveSourceConfiguration]);
const updateSource = useCallback(
async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
const updatedSourceConfiguration = await updateSourceConfiguration(patchedProperties);
const resolveSourceConfigurationPromise = resolveSourceConfiguration(
updatedSourceConfiguration.configuration
);
const loadSourceStatusPromise = loadSourceStatus();
return await Promise.all([
updatedSourceConfiguration,
resolveSourceConfigurationPromise,
loadSourceStatusPromise,
]);
},
[loadSourceStatus, resolveSourceConfiguration, updateSourceConfiguration]
);
const initialize = useCallback(async () => {
if (!isUninitialized) {
return;
}
return await loadSource();
}, [isUninitialized, loadSource]);
return {
sourceId,
initialize,
isUninitialized,
derivedIndexPattern,
// Failure states
hasFailedLoading,
hasFailedLoadingSource,
hasFailedLoadingSourceStatus,
hasFailedResolvingSource,
latestLoadSourceFailures,
// Loading states
isLoading,
isLoadingSourceConfiguration,
isLoadingSourceStatus,
isResolvingSourceConfiguration,
// Source status (denotes the state of the indices, e.g. missing)
sourceStatus,
loadSourceStatus,
// Source configuration (represents the raw attributes of the source configuration)
loadSource,
sourceConfiguration,
updateSource,
// Resolved source configuration (represents a fully resolved state, you would use this for the vast majority of "read" scenarios)
resolvedSourceConfiguration,
};
};
export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource);

View file

@ -12,8 +12,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import useSetState from 'react-use/lib/useSetState';
import { LogEntry, LogEntryCursor } from '../../../../common/log_entry';
import { LogViewColumnConfiguration } from '../../../../common/log_views';
import { useSubscription } from '../../../utils/use_observable';
import { LogSourceConfigurationProperties } from '../log_source';
import { useFetchLogEntriesAfter } from './use_fetch_log_entries_after';
import { useFetchLogEntriesAround } from './use_fetch_log_entries_around';
import { useFetchLogEntriesBefore } from './use_fetch_log_entries_before';
@ -26,7 +26,7 @@ interface LogStreamProps {
endTimestamp: number;
query?: BuiltEsQuery;
center?: LogEntryCursor;
columns?: LogSourceConfigurationProperties['logColumns'];
columns?: LogViewColumnConfiguration[];
}
interface LogStreamState {

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { JsonObject } from '@kbn/utility-types';
import { useCallback } from 'react';
import { Observable } from 'rxjs';
import { exhaustMap } from 'rxjs/operators';
import { JsonObject } from '@kbn/utility-types';
import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public';
import { LogSourceColumnConfiguration } from '../../../../common/log_sources';
import { LogEntryAfterCursor } from '../../../../common/log_entry';
import { LogViewColumnConfiguration } from '../../../../common/log_views';
import { decodeOrThrow } from '../../../../common/runtime_types';
import {
logEntriesSearchRequestParamsRT,
@ -37,7 +37,7 @@ export const useLogEntriesAfterRequest = ({
sourceId,
startTimestamp,
}: {
columnOverrides?: LogSourceColumnConfiguration[];
columnOverrides?: LogViewColumnConfiguration[];
endTimestamp: number;
highlightPhrase?: string;
query?: LogEntriesSearchRequestQuery;
@ -110,7 +110,7 @@ export const useFetchLogEntriesAfter = ({
sourceId,
startTimestamp,
}: {
columnOverrides?: LogSourceColumnConfiguration[];
columnOverrides?: LogViewColumnConfiguration[];
endTimestamp: number;
highlightPhrase?: string;
query?: LogEntriesSearchRequestQuery;

View file

@ -8,8 +8,8 @@
import { useCallback } from 'react';
import { combineLatest, Observable, ReplaySubject } from 'rxjs';
import { last, map, startWith, switchMap } from 'rxjs/operators';
import { LogSourceColumnConfiguration } from '../../../../common/log_sources';
import { LogEntryCursor } from '../../../../common/log_entry';
import { LogViewColumnConfiguration } from '../../../../common/log_views';
import { LogEntriesSearchRequestQuery } from '../../../../common/search_strategies/log_entries/log_entries';
import { flattenDataSearchResponseDescriptor } from '../../../utils/data_search';
import { useObservable, useObservableState } from '../../../utils/use_observable';
@ -24,7 +24,7 @@ export const useFetchLogEntriesAround = ({
sourceId,
startTimestamp,
}: {
columnOverrides?: LogSourceColumnConfiguration[];
columnOverrides?: LogViewColumnConfiguration[];
endTimestamp: number;
highlightPhrase?: string;
query?: LogEntriesSearchRequestQuery;

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { JsonObject } from '@kbn/utility-types';
import { useCallback } from 'react';
import { Observable } from 'rxjs';
import { exhaustMap } from 'rxjs/operators';
import { JsonObject } from '@kbn/utility-types';
import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public';
import { LogSourceColumnConfiguration } from '../../../../common/log_sources';
import { LogEntryBeforeCursor } from '../../../../common/log_entry';
import { LogViewColumnConfiguration } from '../../../../common/log_views';
import { decodeOrThrow } from '../../../../common/runtime_types';
import {
logEntriesSearchRequestParamsRT,
@ -37,7 +37,7 @@ export const useLogEntriesBeforeRequest = ({
sourceId,
startTimestamp,
}: {
columnOverrides?: LogSourceColumnConfiguration[];
columnOverrides?: LogViewColumnConfiguration[];
endTimestamp: number;
highlightPhrase?: string;
query?: LogEntriesSearchRequestQuery;
@ -109,7 +109,7 @@ export const useFetchLogEntriesBefore = ({
sourceId,
startTimestamp,
}: {
columnOverrides?: LogSourceColumnConfiguration[];
columnOverrides?: LogViewColumnConfiguration[];
endTimestamp: number;
highlightPhrase?: string;
query?: LogEntriesSearchRequestQuery;

View file

@ -7,10 +7,10 @@
import { useContext } from 'react';
import useThrottle from 'react-use/lib/useThrottle';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { RendererFunction } from '../../../utils/typed_react';
import { LogFilterState } from '../log_filter';
import { LogPositionState } from '../log_position';
import { useLogSourceContext } from '../log_source';
import { LogSummaryBuckets, useLogSummary } from './log_summary';
const FETCH_THROTTLE_INTERVAL = 3000;
@ -24,7 +24,7 @@ export const WithSummary = ({
end: number | null;
}>;
}) => {
const { sourceId } = useLogSourceContext();
const { logViewId } = useLogViewContext();
const { filterQuery } = useContext(LogFilterState.Context);
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
@ -33,7 +33,7 @@ export const WithSummary = ({
const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL);
const { buckets, start, end } = useLogSummary(
sourceId,
logViewId,
throttledStartTimestamp,
throttledEndTimestamp,
filterQuery?.serializedQuery ?? null

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart } from '../../../../../src/core/public';
import {
createKibanaReactContext,
KibanaReactContextValue,
useKibana,
} from '../../../../../src/plugins/kibana_react/public';
import { InfraClientStartDeps } from '../types';
export type PluginKibanaContextValue = CoreStart & InfraClientStartDeps;
export const createKibanaContextForPlugin = (core: CoreStart, pluginsStart: InfraClientStartDeps) =>
createKibanaReactContext<PluginKibanaContextValue>({
...core,
...pluginsStart,
});
export const useKibanaContextForPlugin =
useKibana as () => KibanaReactContextValue<PluginKibanaContextValue>;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PropsOf } from '@elastic/eui';
import React, { useMemo } from 'react';
import { CoreStart } from '../../../../../src/core/public';
import {
createKibanaReactContext,
KibanaReactContextValue,
useKibana,
} from '../../../../../src/plugins/kibana_react/public';
import { InfraClientCoreSetup, InfraClientStartDeps, InfraClientStartExports } from '../types';
export type PluginKibanaContextValue = CoreStart & InfraClientStartDeps & InfraClientStartExports;
export const createKibanaContextForPlugin = (
core: CoreStart,
plugins: InfraClientStartDeps,
pluginStart: InfraClientStartExports
) =>
createKibanaReactContext<PluginKibanaContextValue>({
...core,
...plugins,
...pluginStart,
});
export const useKibanaContextForPlugin =
useKibana as () => KibanaReactContextValue<PluginKibanaContextValue>;
export const useKibanaContextForPluginProvider = (
core: CoreStart,
plugins: InfraClientStartDeps,
pluginStart: InfraClientStartExports
) => {
const { Provider } = useMemo(
() => createKibanaContextForPlugin(core, plugins, pluginStart),
[core, pluginStart, plugins]
);
return Provider;
};
export const createLazyComponentWithKibanaContext = <T extends React.ComponentType<any>>(
coreSetup: InfraClientCoreSetup,
lazyComponentFactory: () => Promise<{ default: T }>
) =>
React.lazy(() =>
Promise.all([lazyComponentFactory(), coreSetup.getStartServices()]).then(
([{ default: LazilyLoadedComponent }, [core, plugins, pluginStart]]) => {
const { Provider } = createKibanaContextForPlugin(core, plugins, pluginStart);
return {
default: (props: PropsOf<T>) => (
<Provider>
<LazilyLoadedComponent {...props} />
</Provider>
),
};
}
)
);

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createLogViewMock } from '../../common/log_views/log_view.mock';
import { createResolvedLogViewMockFromAttributes } from '../../common/log_views/resolved_log_view.mock';
import { useLogView } from './use_log_view';
type UseLogView = typeof useLogView;
type IUseLogView = ReturnType<UseLogView>;
const defaultLogViewId = 'default';
export const createUninitializedUseLogViewMock =
(logViewId: string = defaultLogViewId) =>
(): IUseLogView => ({
derivedDataView: {
fields: [],
title: 'unknown',
},
hasFailedLoading: false,
hasFailedLoadingLogView: false,
hasFailedLoadingLogViewStatus: false,
hasFailedResolvingLogView: false,
isLoading: false,
isLoadingLogView: false,
isLoadingLogViewStatus: false,
isResolvingLogView: false,
isUninitialized: true,
latestLoadLogViewFailures: [],
load: jest.fn(),
logView: undefined,
logViewId,
logViewStatus: undefined,
resolvedLogView: undefined,
update: jest.fn(),
});
export const createLoadingUseLogViewMock =
(logViewId: string = defaultLogViewId) =>
(): IUseLogView => ({
...createUninitializedUseLogViewMock(logViewId)(),
isLoading: true,
isLoadingLogView: true,
isLoadingLogViewStatus: true,
isResolvingLogView: true,
});
export const createLoadedUseLogViewMock = async (logViewId: string = defaultLogViewId) => {
const logView = createLogViewMock(logViewId);
const resolvedLogView = await createResolvedLogViewMockFromAttributes(logView.attributes);
return (): IUseLogView => {
return {
...createUninitializedUseLogViewMock(logViewId)(),
logView,
resolvedLogView,
logViewStatus: {
index: 'available',
},
};
};
};

View file

@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import createContainer from 'constate';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { HttpHandler } from 'src/core/public';
import { LogView, LogViewAttributes, LogViewStatus, ResolvedLogView } from '../../common/log_views';
import type { ILogViewsClient } from '../services/log_views';
import { isRejectedPromiseState, useTrackedPromise } from '../utils/use_tracked_promise';
export const useLogView = ({
logViewId,
logViews,
fetch,
}: {
logViewId: string;
logViews: ILogViewsClient;
fetch: HttpHandler;
}) => {
const [logView, setLogView] = useState<LogView | undefined>(undefined);
const [resolvedLogView, setResolvedLogView] = useState<ResolvedLogView | undefined>(undefined);
const [logViewStatus, setLogViewStatus] = useState<LogViewStatus | undefined>(undefined);
const [loadLogViewRequest, loadLogView] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.getLogView.bind(logViews),
onResolve: setLogView,
},
[logViews]
);
const [resolveLogViewRequest, resolveLogView] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.resolveLogView.bind(logViews),
onResolve: setResolvedLogView,
},
[logViews]
);
const [updateLogViewRequest, updateLogView] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.putLogView.bind(logViews),
onResolve: setLogView,
},
[logViews]
);
const [loadLogViewStatusRequest, loadLogViewStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: logViews.getResolvedLogViewStatus.bind(logViews),
onResolve: setLogViewStatus,
},
[logViews]
);
const derivedDataView = useMemo(
() => ({
fields: resolvedLogView?.fields ?? [],
title: resolvedLogView?.indices ?? 'unknown',
}),
[resolvedLogView]
);
const isLoadingLogView = loadLogViewRequest.state === 'pending';
const isResolvingLogView = resolveLogViewRequest.state === 'pending';
const isLoadingLogViewStatus = loadLogViewStatusRequest.state === 'pending';
const isUpdatingLogView = updateLogViewRequest.state === 'pending';
const isLoading =
isLoadingLogView || isResolvingLogView || isLoadingLogViewStatus || isUpdatingLogView;
const isUninitialized = loadLogViewRequest.state === 'uninitialized';
const hasFailedLoadingLogView = loadLogViewRequest.state === 'rejected';
const hasFailedResolvingLogView = resolveLogViewRequest.state === 'rejected';
const hasFailedLoadingLogViewStatus = loadLogViewStatusRequest.state === 'rejected';
const latestLoadLogViewFailures = [
loadLogViewRequest,
resolveLogViewRequest,
loadLogViewStatusRequest,
]
.filter(isRejectedPromiseState)
.map(({ value }) => (value instanceof Error ? value : new Error(`${value}`)));
const hasFailedLoading = latestLoadLogViewFailures.length > 0;
const load = useCallback(async () => {
const loadedLogView = await loadLogView(logViewId);
const resolvedLoadedLogView = await resolveLogView(loadedLogView.attributes);
const resolvedLogViewStatus = await loadLogViewStatus(resolvedLoadedLogView);
return [loadedLogView, resolvedLoadedLogView, resolvedLogViewStatus];
}, [logViewId, loadLogView, loadLogViewStatus, resolveLogView]);
const update = useCallback(
async (logViewAttributes: Partial<LogViewAttributes>) => {
const updatedLogView = await updateLogView(logViewId, logViewAttributes);
const resolvedUpdatedLogView = await resolveLogView(updatedLogView.attributes);
const resolvedLogViewStatus = await loadLogViewStatus(resolvedUpdatedLogView);
return [updatedLogView, resolvedUpdatedLogView, resolvedLogViewStatus];
},
[logViewId, loadLogViewStatus, resolveLogView, updateLogView]
);
useEffect(() => {
load();
}, [load]);
return {
logViewId,
isUninitialized,
derivedDataView,
// Failure states
hasFailedLoading,
hasFailedLoadingLogView,
hasFailedLoadingLogViewStatus,
hasFailedResolvingLogView,
latestLoadLogViewFailures,
// Loading states
isLoading,
isLoadingLogView,
isLoadingLogViewStatus,
isResolvingLogView,
// data
logView,
resolvedLogView,
logViewStatus,
// actions
load,
update,
};
};
export const [LogViewProvider, useLogViewContext] = createContainer(useLogView);

View file

@ -5,21 +5,24 @@
* 2.0.
*/
import { coreMock } from 'src/core/public/mocks';
import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers';
import { CoreStart } from 'kibana/public';
import { InfraClientStartDeps, InfraClientStartExports } from './types';
import moment from 'moment';
import { coreMock } from 'src/core/public/mocks';
import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers';
import { createInfraPluginStartMock } from './mocks';
import { FAKE_OVERVIEW_RESPONSE } from './test_utils';
import { InfraClientStartDeps, InfraClientStartExports } from './types';
function setup() {
const core = coreMock.createStart();
const pluginStart = createInfraPluginStartMock();
const mockedGetStartServices = jest.fn(() => {
const deps = {};
return Promise.resolve([
core as CoreStart,
deps as InfraClientStartDeps,
{} as InfraClientStartExports,
pluginStart,
]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>;
});
return { core, mockedGetStartServices };

View file

@ -15,11 +15,11 @@
import { FetchDataParams, MetricsFetchDataResponse } from '../../observability/public';
import { TopNodesRequest, TopNodesResponse } from '../common/http_api/overview_api';
import { InfraClientCoreSetup } from './types';
import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration';
import { InfraClientStartServicesAccessor } from './types';
export const createMetricsHasData =
(getStartServices: InfraClientCoreSetup['getStartServices']) => async () => {
(getStartServices: InfraClientStartServicesAccessor) => async () => {
const [coreServices] = await getStartServices();
const { http } = coreServices;
const results = await http.get<{
@ -30,7 +30,7 @@ export const createMetricsHasData =
};
export const createMetricsFetchData =
(getStartServices: InfraClientCoreSetup['getStartServices']) =>
(getStartServices: InfraClientStartServicesAccessor) =>
async ({ absoluteTime, intervalString }: FetchDataParams): Promise<MetricsFetchDataResponse> => {
const [coreServices] = await getStartServices();
const { http } = coreServices;

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
import { InfraClientStartExports } from './types';
export const createInfraPluginStartMock = () => ({
logViews: createLogViewsServiceStartMock(),
ContainerMetricsTable: () => <div />,
HostMetricsTable: () => <div />,
PodMetricsTable: () => <div />,
});
export const _ensureTypeCompatibility = (): InfraClientStartExports => createInfraPluginStartMock();

View file

@ -11,22 +11,22 @@ import React from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { httpServiceMock } from 'src/core/public/mocks';
import { KibanaContextProvider, KibanaPageTemplate } from 'src/plugins/kibana_react/public';
import { useLogSource } from '../../containers/logs/log_source';
import { useLogView } from '../../hooks/use_log_view';
import {
createLoadedUseLogSourceMock,
createLoadingUseLogSourceMock,
} from '../../containers/logs/log_source/log_source.mock';
createLoadedUseLogViewMock,
createLoadingUseLogViewMock,
} from '../../hooks/use_log_view.mock';
import { LinkToLogsPage } from './link_to_logs';
jest.mock('../../containers/logs/log_source');
const useLogSourceMock = useLogSource as jest.MockedFunction<typeof useLogSource>;
jest.mock('../../hooks/use_log_view');
const useLogViewMock = useLogView as jest.MockedFunction<typeof useLogView>;
const renderRoutes = (routes: React.ReactElement) => {
const history = createMemoryHistory();
const services = {
http: httpServiceMock.createStartContract(),
data: {
indexPatterns: {},
logViews: {
client: {},
},
observability: {
navigation: {
@ -48,12 +48,12 @@ const renderRoutes = (routes: React.ReactElement) => {
};
describe('LinkToLogsPage component', () => {
beforeEach(() => {
useLogSourceMock.mockImplementation(createLoadedUseLogSourceMock());
beforeEach(async () => {
useLogViewMock.mockImplementation(await createLoadedUseLogViewMock());
});
afterEach(() => {
useLogSourceMock.mockRestore();
useLogViewMock.mockRestore();
});
describe('default route', () => {
@ -199,7 +199,7 @@ describe('LinkToLogsPage component', () => {
});
it('renders a loading page while loading the source configuration', async () => {
useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock());
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
const { history, queryByTestId } = renderRoutes(
<Switch>
@ -209,7 +209,7 @@ describe('LinkToLogsPage component', () => {
history.push('/link-to/host-logs/HOST_NAME');
await waitFor(() => {
expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmpty();
expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmptyDOMElement();
});
});
});
@ -258,7 +258,7 @@ describe('LinkToLogsPage component', () => {
});
it('renders a loading page while loading the source configuration', () => {
useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock());
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
const { history, queryByTestId } = renderRoutes(
<Switch>
@ -268,7 +268,7 @@ describe('LinkToLogsPage component', () => {
history.push('/link-to/container-logs/CONTAINER_ID');
expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmpty();
expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmptyDOMElement();
});
});
@ -314,7 +314,7 @@ describe('LinkToLogsPage component', () => {
});
it('renders a loading page while loading the source configuration', () => {
useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock());
useLogViewMock.mockImplementation(createLoadingUseLogViewMock());
const { history, queryByTestId } = renderRoutes(
<Switch>
@ -324,7 +324,7 @@ describe('LinkToLogsPage component', () => {
history.push('/link-to/pod-logs/POD_UID');
expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmpty();
expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmptyDOMElement();
});
});
});

View file

@ -6,20 +6,20 @@
*/
import { i18n } from '@kbn/i18n';
import { flowRight } from 'lodash';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import useMount from 'react-use/lib/useMount';
import { flowRight } from 'lodash';
import { LinkDescriptor } from '../../../../observability/public';
import { findInventoryFields } from '../../../common/inventory_models';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { LoadingPage } from '../../components/loading_page';
import { replaceLogFilterInQueryString } from '../../containers/logs/log_filter';
import { replaceLogPositionInQueryString } from '../../containers/logs/log_position';
import { useLogSource } from '../../containers/logs/log_source';
import { replaceSourceIdInQueryString } from '../../containers/source_id';
import { LinkDescriptor } from '../../../../observability/public';
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { useLogView } from '../../hooks/use_log_view';
import { getFilterFromLocation, getTimeFromLocation } from './query_params';
type RedirectToNodeLogsType = RouteComponentProps<{
nodeId: string;
@ -34,14 +34,14 @@ export const RedirectToNodeLogs = ({
location,
}: RedirectToNodeLogsType) => {
const { services } = useKibanaContextForPlugin();
const { isLoading, loadSource } = useLogSource({
const { isLoading, load } = useLogView({
fetch: services.http.fetch,
sourceId,
indexPatternsService: services.data.indexPatterns,
logViewId: sourceId,
logViews: services.logViews.client,
});
useMount(() => {
loadSource();
load();
});
if (isLoading) {

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect } from 'react';
import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -21,11 +22,10 @@ import {
import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';
import { LogsPageTemplate } from '../page_template';
import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
import { useLogSourceContext } from '../../../containers/logs/log_source';
const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle', {
defaultMessage: 'Categories',
@ -115,10 +115,10 @@ const CategoriesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
children,
...rest
}) => {
const { sourceStatus } = useLogSourceContext();
const { logViewStatus } = useLogViewContext();
return (
<LogsPageTemplate
hasData={sourceStatus?.logIndexStatus !== 'missing'}
hasData={logViewStatus?.index !== 'missing'}
data-test-subj="logsLogEntryCategoriesPage"
pageHeader={
rest.isEmptyState

View file

@ -10,19 +10,19 @@ import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { useLogViewContext } from '../../../hooks/use_log_view';
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadSourceFailures,
loadSource,
resolvedSourceConfiguration,
sourceId,
} = useLogSourceContext();
latestLoadLogViewFailures,
load,
resolvedLogView,
logViewId,
} = useLogViewContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
@ -31,17 +31,17 @@ export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ child
if (space == null) {
return null;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (resolvedSourceConfiguration != null) {
} else if (resolvedLogView != null) {
return (
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
indexPattern={resolvedLogView.indices}
sourceId={logViewId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
timestampField={resolvedLogView.timestampField}
runtimeMappings={resolvedLogView.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>

View file

@ -11,13 +11,21 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useInterval from 'react-use/lib/useInterval';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { MLJobsAwaitingNodeWarning, ML_PAGES, useMlHref } from '../../../../../ml/public';
import { useTrackPageview } from '../../../../../observability/public';
import { TimeRange } from '../../../../common/time/time_range';
import { CategoryJobNoticesSection } from '../../../components/logging/log_analysis_job_status';
import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results';
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
import { RecreateJobButton } from '../../../components/logging/log_analysis_setup/create_job_button';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
import { TopCategoriesSection } from './sections/top_categories';
import { useLogEntryCategoriesResults } from './use_log_entry_categories_results';
@ -25,15 +33,6 @@ import {
StringTimeRange,
useLogEntryCategoriesResultsUrlState,
} from './use_log_entry_categories_results_url_state';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { LogsPageTemplate } from '../page_template';
import { RecreateJobButton } from '../../../components/logging/log_analysis_setup/create_job_button';
import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results';
import { useMlHref, ML_PAGES } from '../../../../../ml/public';
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
const JOB_STATUS_POLLING_INTERVAL = 30000;
@ -52,7 +51,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<
services: { ml, http },
} = useKibanaContextForPlugin();
const { sourceStatus } = useLogSourceContext();
const { logViewStatus } = useLogViewContext();
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
const {
@ -212,7 +211,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<
endTimestamp={categoryQueryTimeRange.timeRange.endTime}
>
<LogsPageTemplate
hasData={sourceStatus?.logIndexStatus !== 'missing'}
hasData={logViewStatus?.index !== 'missing'}
pageHeader={{
pageTitle,
rightSideItems: [

View file

@ -8,6 +8,7 @@
import { i18n } from '@kbn/i18n';
import React, { memo, useCallback, useEffect } from 'react';
import useInterval from 'react-use/lib/useInterval';
import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -20,14 +21,13 @@ import {
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
import { SubscriptionSplashPage } from '../../../components/subscription_splash_content';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';
import { LogsPageTemplate } from '../page_template';
import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public';
const JOB_STATUS_POLLING_INTERVAL = 30000;
@ -156,10 +156,10 @@ const AnomaliesPageTemplate: React.FC<LazyObservabilityPageTemplateProps> = ({
children,
...rest
}) => {
const { sourceStatus } = useLogSourceContext();
const { logViewStatus } = useLogViewContext();
return (
<LogsPageTemplate
hasData={sourceStatus?.logIndexStatus !== 'missing'}
hasData={logViewStatus?.index !== 'missing'}
data-test-subj="logsLogEntryRatePage"
pageHeader={
rest.isEmptyState

View file

@ -12,19 +12,19 @@ import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { useLogViewContext } from '../../../hooks/use_log_view';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadSourceFailures,
loadSource,
resolvedSourceConfiguration,
sourceId,
} = useLogSourceContext();
latestLoadLogViewFailures,
load,
logViewId,
resolvedLogView,
} = useLogViewContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
@ -35,23 +35,23 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children })
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
} else if (resolvedSourceConfiguration != null) {
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
} else if (resolvedLogView != null) {
return (
<LogFlyout.Provider>
<LogEntryRateModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
indexPattern={resolvedLogView.indices}
sourceId={logViewId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
timestampField={resolvedLogView.timestampField}
runtimeMappings={resolvedLogView.runtimeMappings}
>
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
indexPattern={resolvedLogView.indices}
sourceId={logViewId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
timestampField={resolvedLogView.timestampField}
runtimeMappings={resolvedLogView.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>

View file

@ -6,34 +6,34 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
import type { Query } from '@kbn/es-query';
import moment from 'moment';
import { stringify } from 'query-string';
import React, { useCallback, useMemo } from 'react';
import { encode, RisonValue } from 'rison-node';
import type { Query } from '@kbn/es-query';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
import { useTrackPageview } from '../../../../../observability/public';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { TimeKey } from '../../../../common/time';
import {
CategoryJobNoticesSection,
LogAnalysisJobProblemIndicator,
} from '../../../components/logging/log_analysis_job_status';
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button';
import { useLogAnalysisSetupFlyoutStateContext } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { AnomaliesResults } from './sections/anomalies';
import { useDatasetFiltering } from './use_dataset_filtering';
import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';
import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LogsPageTemplate } from '../page_template';
import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button';
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
export const SORT_DEFAULTS = {
direction: 'desc' as const,
@ -52,7 +52,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{
const navigateToApp = useKibana().services.application?.navigateToApp;
const { sourceId, sourceStatus } = useLogSourceContext();
const { logViewId, logViewStatus } = useLogViewContext();
const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext();
@ -142,7 +142,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{
datasets,
isLoadingDatasets,
} = useLogEntryAnomaliesResults({
sourceId,
sourceId: logViewId,
startTime: timeRange.value.startTime,
endTime: timeRange.value.endTime,
defaultSortOptions: SORT_DEFAULTS,
@ -196,7 +196,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{
return (
<LogsPageTemplate
hasData={sourceStatus?.logIndexStatus !== 'missing'}
hasData={logViewStatus?.index !== 'missing'}
pageHeader={{
pageTitle,
rightSideItems: [<ManageJobsButton onClick={showModuleList} size="s" />],
@ -272,7 +272,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{
logEntryId={flyoutLogEntryId}
onCloseFlyout={closeLogEntryFlyout}
onSetFieldFilter={linkToLogStream}
sourceId={sourceId}
sourceId={logViewId}
/>
) : null}
</LogsPageTemplate>

View file

@ -11,10 +11,10 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import useMount from 'react-use/lib/useMount';
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
import { LogEntryAnomaly, isCategoryAnomaly } from '../../../../../../common/log_analysis';
import { isCategoryAnomaly, LogEntryAnomaly } from '../../../../../../common/log_analysis';
import { TimeRange } from '../../../../../../common/time/time_range';
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
import { useLogSourceContext } from '../../../../../containers/logs/log_source';
import { useLogViewContext } from '../../../../../hooks/use_log_view';
import { useLogEntryExamples } from '../../use_log_entry_examples';
import { LogEntryExampleMessage, LogEntryExampleMessageHeaders } from './log_entry_example';
@ -28,7 +28,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{
anomaly: LogEntryAnomaly;
timeRange: TimeRange;
}> = ({ anomaly, timeRange }) => {
const { sourceId } = useLogSourceContext();
const { logViewId } = useLogViewContext();
const {
getLogEntryExamples,
@ -39,7 +39,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{
dataset: anomaly.dataset,
endTime: anomaly.startTime + anomaly.duration,
exampleCount: EXAMPLE_COUNT,
sourceId,
sourceId: logViewId,
startTime: anomaly.startTime,
categoryId: isCategoryAnomaly(anomaly) ? anomaly.categoryId : undefined,
});

View file

@ -5,41 +5,31 @@
* 2.0.
*/
import { EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui';
import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { Route, Switch } from 'react-router-dom';
import useMount from 'react-use/lib/useMount';
import { AlertDropdown } from '../../alerting/log_threshold';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { HeaderMenuPortal, useLinkProps } from '../../../../observability/public';
import { AlertDropdown } from '../../alerting/log_threshold';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
import { useLogSourceContext } from '../../containers/logs/log_source';
import { useReadOnlyBadge } from '../../hooks/use_readonly_badge';
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params';
import { LogEntryCategoriesPage } from './log_entry_categories';
import { LogEntryRatePage } from './log_entry_rate';
import { LogsSettingsPage } from './settings';
import { StreamPage } from './stream';
import { HeaderMenuPortal } from '../../../../observability/public';
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
import { useLinkProps } from '../../../../observability/public';
import { useReadOnlyBadge } from '../../hooks/use_readonly_badge';
export const LogsPageContent: React.FunctionComponent = () => {
const uiCapabilities = useKibana().services.application?.capabilities;
const { setHeaderActionMenu, theme$ } = useContext(HeaderActionMenuContext);
const { initialize } = useLogSourceContext();
const kibana = useKibana();
useReadOnlyBadge(!uiCapabilities?.logs?.save);
useMount(() => {
initialize();
});
// !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts
const streamTab = {
app: 'logs',

View file

@ -6,21 +6,21 @@
*/
import React from 'react';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis';
import { LogSourceProvider } from '../../containers/logs/log_source';
import { useSourceId } from '../../containers/source_id';
import { useKibanaContextForPlugin } from '../../hooks/use_kibana';
import { LogViewProvider } from '../../hooks/use_log_view';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();
const { services } = useKibanaContextForPlugin();
return (
<LogSourceProvider
sourceId={sourceId}
<LogViewProvider
fetch={services.http.fetch}
indexPatternsService={services.data.indexPatterns}
logViewId={sourceId}
logViews={services.logViews.client}
>
<LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider>
</LogSourceProvider>
</LogViewProvider>
);
};

View file

@ -9,7 +9,7 @@ import { EuiCode, EuiDescribedFormGroup, EuiFieldText, EuiFormRow } from '@elast
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { LogIndexNameReference } from '../../../../common/log_sources';
import { LogIndexNameReference } from '../../../../common/log_views';
import { FormElement } from './form_elements';
import { getFormRowProps, getInputFieldProps } from './form_field_props';
import { FormValidationError } from './validation_errors';

View file

@ -9,7 +9,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useMemo } from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { LogIndexPatternReference } from '../../../../common/log_sources';
import { LogDataViewReference } from '../../../../common/log_views';
import { useLinkProps } from '../../../../../observability/public';
import { FormElement } from './form_elements';
import { getFormRowProps } from './form_field_props';
@ -19,7 +19,7 @@ import { FormValidationError } from './validation_errors';
export const IndexPatternConfigurationPanel: React.FC<{
isLoading: boolean;
isReadOnly: boolean;
indexPatternFormElement: FormElement<LogIndexPatternReference | undefined, FormValidationError>;
indexPatternFormElement: FormElement<LogDataViewReference | undefined, FormValidationError>;
}> = ({ isLoading, isReadOnly, indexPatternFormElement }) => {
useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_pattern' });
useTrackPageview({
@ -29,11 +29,11 @@ export const IndexPatternConfigurationPanel: React.FC<{
});
const changeIndexPatternId = useCallback(
(indexPatternId: string | undefined) => {
if (indexPatternId != null) {
(dataViewId: string | undefined) => {
if (dataViewId != null) {
indexPatternFormElement.updateValue(() => ({
type: 'index_pattern',
indexPatternId,
type: 'data_view',
dataViewId,
}));
} else {
indexPatternFormElement.updateValue(() => undefined);
@ -78,7 +78,7 @@ export const IndexPatternConfigurationPanel: React.FC<{
<IndexPatternSelector
isLoading={isLoading || indexPatternFormElement.validity.validity === 'pending'}
isReadOnly={isReadOnly}
indexPatternId={indexPatternFormElement.value?.indexPatternId}
indexPatternId={indexPatternFormElement.value?.dataViewId}
onChangeIndexPatternId={changeIndexPatternId}
/>
</EuiFormRow>

View file

@ -6,13 +6,13 @@
*/
import { useMemo } from 'react';
import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common';
import { useUiTracker } from '../../../../../observability/public';
import {
LogDataViewReference,
LogIndexNameReference,
logIndexNameReferenceRT,
LogIndexPatternReference,
} from '../../../../common/log_sources';
} from '../../../../common/log_views';
import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common';
import { useUiTracker } from '../../../../../observability/public';
import { useKibanaIndexPatternService } from '../../../hooks/use_kibana_index_patterns';
import { useFormElement } from './form_elements';
import {
@ -21,7 +21,7 @@ import {
validateStringNotEmpty,
} from './validation_errors';
export type LogIndicesFormState = LogIndexNameReference | LogIndexPatternReference | undefined;
export type LogIndicesFormState = LogIndexNameReference | LogDataViewReference | undefined;
export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => {
const indexPatternService = useKibanaIndexPatternService();
@ -37,23 +37,20 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => {
} else if (logIndexNameReferenceRT.is(logIndices)) {
return validateStringNotEmpty('log indices', logIndices.indexName);
} else {
const emptyStringErrors = validateStringNotEmpty(
'log data view',
logIndices.indexPatternId
);
const emptyStringErrors = validateStringNotEmpty('log data view', logIndices.dataViewId);
if (emptyStringErrors.length > 0) {
return emptyStringErrors;
}
const indexPatternErrors = await indexPatternService
.get(logIndices.indexPatternId)
.get(logIndices.dataViewId)
.then(validateIndexPattern, (error): FormValidationError[] => {
if (error instanceof SavedObjectNotFound) {
return [
{
type: 'missing_index_pattern' as const,
indexPatternId: logIndices.indexPatternId,
indexPatternId: logIndices.dataViewId,
},
];
} else {

View file

@ -11,10 +11,10 @@ import React, { useCallback } from 'react';
import { useUiTracker } from '../../../../../observability/public';
import {
logIndexNameReferenceRT,
LogIndexPatternReference,
logIndexPatternReferenceRT,
LogDataViewReference,
logDataViewReferenceRT,
LogIndexReference,
} from '../../../../common/log_sources';
} from '../../../../common/log_views';
import { FormElement, isFormElementForType } from './form_elements';
import { IndexNamesConfigurationPanel } from './index_names_configuration_panel';
import { IndexPatternConfigurationPanel } from './index_pattern_configuration_panel';
@ -28,7 +28,7 @@ export const IndicesConfigurationPanel = React.memo<{
const trackChangeIndexSourceType = useUiTracker({ app: 'infra_logs' });
const changeToIndexPatternType = useCallback(() => {
if (indicesFormElement.initialValue?.type === 'index_pattern') {
if (logDataViewReferenceRT.is(indicesFormElement.initialValue)) {
indicesFormElement.updateValue(() => indicesFormElement.initialValue);
} else {
indicesFormElement.updateValue(() => undefined);
@ -83,11 +83,11 @@ export const IndicesConfigurationPanel = React.memo<{
}
name="dataView"
value="dataView"
checked={isIndexPatternFormElement(indicesFormElement)}
checked={isDataViewFormElement(indicesFormElement)}
onChange={changeToIndexPatternType}
disabled={isReadOnly}
>
{isIndexPatternFormElement(indicesFormElement) && (
{isDataViewFormElement(indicesFormElement) && (
<IndexPatternConfigurationPanel
isLoading={isLoading}
isReadOnly={isReadOnly}
@ -127,9 +127,9 @@ export const IndicesConfigurationPanel = React.memo<{
);
});
const isIndexPatternFormElement = isFormElementForType(
(value): value is LogIndexPatternReference | undefined =>
value == null || logIndexPatternReferenceRT.is(value)
const isDataViewFormElement = isFormElementForType(
(value): value is LogDataViewReference | undefined =>
value == null || logDataViewReferenceRT.is(value)
);
const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is);

View file

@ -6,30 +6,28 @@
*/
import { useMemo } from 'react';
import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source';
import { LogViewAttributes } from '../../../../common/log_views';
import { useCompositeFormElement } from './form_elements';
import { useLogIndicesFormElement } from './indices_configuration_form_state';
import { useLogColumnsFormElement } from './log_columns_configuration_form_state';
import { useNameFormElement } from './name_configuration_form_state';
export const useLogSourceConfigurationFormState = (
configuration?: LogSourceConfigurationProperties
) => {
const nameFormElement = useNameFormElement(configuration?.name ?? '');
export const useLogSourceConfigurationFormState = (logViewAttributes?: LogViewAttributes) => {
const nameFormElement = useNameFormElement(logViewAttributes?.name ?? '');
const logIndicesFormElement = useLogIndicesFormElement(
useMemo(
() =>
configuration?.logIndices ?? {
logViewAttributes?.logIndices ?? {
type: 'index_name',
indexName: '',
},
[configuration]
[logViewAttributes]
)
);
const logColumnsFormElement = useLogColumnsFormElement(
useMemo(() => configuration?.logColumns ?? [], [configuration])
useMemo(() => logViewAttributes?.logColumns ?? [], [logViewAttributes])
);
const sourceConfigurationFormElement = useCompositeFormElement(

View file

@ -17,18 +17,17 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useMemo } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../../observability/public';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
import { Prompt, useTrackPageview } from '../../../../../observability/public';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { Prompt } from '../../../../../observability/public';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { settingsTitle } from '../../../translations';
import { LogsPageTemplate } from '../page_template';
import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';
import { LogSourceConfigurationFormErrors } from './source_configuration_form_errors';
import { useLogSourceConfigurationFormState } from './source_configuration_form_state';
import { LogsPageTemplate } from '../page_template';
import { settingsTitle } from '../../../translations';
export const LogsSettingsPage = () => {
const uiCapabilities = useKibana().services.application?.capabilities;
@ -47,18 +46,12 @@ export const LogsSettingsPage = () => {
},
]);
const {
sourceConfiguration: source,
hasFailedLoadingSource,
isLoading,
isUninitialized,
updateSource,
resolvedSourceConfiguration,
} = useLogSourceContext();
const { logView, hasFailedLoadingLogView, isLoading, isUninitialized, update, resolvedLogView } =
useLogViewContext();
const availableFields = useMemo(
() => resolvedSourceConfiguration?.fields.map((field) => field.name) ?? [],
[resolvedSourceConfiguration]
() => resolvedLogView?.fields.map((field) => field.name) ?? [],
[resolvedLogView]
);
const {
@ -67,22 +60,22 @@ export const LogsSettingsPage = () => {
logIndicesFormElement,
logColumnsFormElement,
nameFormElement,
} = useLogSourceConfigurationFormState(source?.configuration);
} = useLogSourceConfigurationFormState(logView?.attributes);
const persistUpdates = useCallback(async () => {
await updateSource(formState);
await update(formState);
sourceConfigurationFormElement.resetValue();
}, [updateSource, sourceConfigurationFormElement, formState]);
}, [update, sourceConfigurationFormElement, formState]);
const isWriteable = useMemo(
() => shouldAllowEdit && source && source.origin !== 'internal',
[shouldAllowEdit, source]
() => shouldAllowEdit && logView && logView.origin !== 'internal',
[shouldAllowEdit, logView]
);
if ((isLoading || isUninitialized) && !resolvedSourceConfiguration) {
if ((isLoading || isUninitialized) && !resolvedLogView) {
return <SourceLoadingPage />;
}
if (hasFailedLoadingSource) {
if (hasFailedLoadingLogView) {
return null;
}

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogsPageLogsContent } from './page_logs_content';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { LogsPageTemplate } from '../page_template';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public';
import { LogsPageLogsContent } from './page_logs_content';
const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', {
defaultMessage: 'Stream',
@ -24,20 +24,20 @@ export const StreamPageContent: React.FunctionComponent = () => {
hasFailedLoading,
isLoading,
isUninitialized,
loadSource,
latestLoadSourceFailures,
sourceStatus,
} = useLogSourceContext();
latestLoadLogViewFailures,
load,
logViewStatus,
} = useLogViewContext();
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
return <LogSourceErrorPage errors={latestLoadLogViewFailures} onRetry={load} />;
} else {
return (
<LogStreamPageWrapper className={APP_WRAPPER_CLASS}>
<LogsPageTemplate
hasData={sourceStatus?.logIndexStatus !== 'missing'}
hasData={logViewStatus?.index !== 'missing'}
pageHeader={{
pageTitle: streamTitle,
}}

View file

@ -6,9 +6,9 @@
*/
import { EuiSpacer } from '@elastic/eui';
import React, { useContext, useCallback, useMemo, useEffect } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import type { Query } from '@kbn/es-query';
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { LogEntry } from '../../../../common/log_entry';
import { TimeKey } from '../../../../common/time';
@ -25,12 +25,12 @@ import {
} from '../../../containers/logs/log_flyout';
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
import { LogPositionState } from '../../../containers/logs/log_position';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useLogStreamContext } from '../../../containers/logs/log_stream';
import { WithSummary } from '../../../containers/logs/log_summary';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview';
import { useLogViewContext } from '../../../hooks/use_log_view';
import { datemathToEpochMillis, isValidDatemath } from '../../../utils/datemath';
import { LogsToolbar } from './page_toolbar';
import { PageViewLogInContext } from './page_view_log_in_context';
@ -38,7 +38,7 @@ import { PageViewLogInContext } from './page_view_log_in_context';
const PAGE_THRESHOLD = 2;
export const LogsPageLogsContent: React.FunctionComponent = () => {
const { resolvedSourceConfiguration, sourceConfiguration, sourceId } = useLogSourceContext();
const { resolvedLogView, logView, logViewId } = useLogViewContext();
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
const {
surroundingLogsId,
@ -216,14 +216,12 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
logEntryId={flyoutLogEntryId}
onCloseFlyout={closeLogEntryFlyout}
onSetFieldFilter={setFilter}
sourceId={sourceId}
sourceId={logViewId}
/>
) : null}
<PageContent key={`${sourceId}-${sourceConfiguration?.version}`}>
<PageContent key={`${logViewId}-${logView?.version}`}>
<ScrollableLogTextStreamView
columnConfigurations={
(resolvedSourceConfiguration && resolvedSourceConfiguration.columns) || []
}
columnConfigurations={(resolvedLogView && resolvedLogView.columns) || []}
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
isLoadingMore={isLoadingMore}

View file

@ -6,20 +6,20 @@
*/
import React, { useContext } from 'react';
import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights';
import { LogPositionState, WithLogPositionUrlState } from '../../../containers/logs/log_position';
import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
import { LogStreamProvider, useLogStreamContext } from '../../../containers/logs/log_stream';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
import { useLogViewContext } from '../../../hooks/use_log_view';
const LogFilterStateProvider: React.FC = ({ children }) => {
const { derivedIndexPattern } = useLogSourceContext();
const { derivedDataView } = useLogViewContext();
return (
<LogFilterState.Provider indexPattern={derivedIndexPattern}>
<LogFilterState.Provider indexPattern={derivedDataView}>
<WithLogFilterUrlState />
{children}
</LogFilterState.Provider>
@ -28,7 +28,7 @@ const LogFilterStateProvider: React.FC = ({ children }) => {
const ViewLogInContextProvider: React.FC = ({ children }) => {
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
const { sourceId } = useLogSourceContext();
const { logViewId } = useLogViewContext();
if (!startTimestamp || !endTimestamp) {
return null;
@ -38,7 +38,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => {
<ViewLogInContext.Provider
startTimestamp={startTimestamp}
endTimestamp={endTimestamp}
sourceId={sourceId}
sourceId={logViewId}
>
{children}
</ViewLogInContext.Provider>
@ -46,7 +46,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => {
};
const LogEntriesStateProvider: React.FC = ({ children }) => {
const { sourceId } = useLogSourceContext();
const { logViewId } = useLogViewContext();
const { startTimestamp, endTimestamp, targetPosition, isInitialized } = useContext(
LogPositionState.Context
);
@ -65,7 +65,7 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
return (
<LogStreamProvider
sourceId={sourceId}
sourceId={logViewId}
startTimestamp={startTimestamp}
endTimestamp={endTimestamp}
query={filterQuery?.parsedQuery}
@ -77,13 +77,13 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
};
const LogHighlightsStateProvider: React.FC = ({ children }) => {
const { sourceId, sourceConfiguration } = useLogSourceContext();
const { logViewId, logView } = useLogViewContext();
const { topCursor, bottomCursor, entries } = useLogStreamContext();
const { filterQuery } = useContext(LogFilterState.Context);
const highlightsProps = {
sourceId,
sourceVersion: sourceConfiguration?.version,
sourceId: logViewId,
sourceVersion: logView?.version,
entriesStart: topCursor,
entriesEnd: bottomCursor,
centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null,
@ -94,10 +94,10 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => {
};
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const { sourceStatus } = useLogSourceContext();
const { logViewStatus } = useLogViewContext();
// The providers assume the source is loaded, so short-circuit them otherwise
if (sourceStatus?.logIndexStatus === 'missing') {
if (logViewStatus?.index === 'missing') {
return <>{children}</>;
}

View file

@ -6,10 +6,11 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { Query } from '@kbn/es-query';
import { QueryStringInput } from '../../../../../../../src/plugins/data/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu';
import { LogDatepicker } from '../../../components/logging/log_datepicker';
import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu';
@ -19,12 +20,11 @@ import { LogFilterState } from '../../../containers/logs/log_filter';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights';
import { LogPositionState } from '../../../containers/logs/log_position';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
import { useLogViewContext } from '../../../hooks/use_log_view';
export const LogsToolbar = () => {
const { derivedIndexPattern } = useLogSourceContext();
const { derivedDataView } = useLogViewContext();
const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext(
LogViewConfiguration.Context
);
@ -57,7 +57,7 @@ export const LogsToolbar = () => {
<QueryStringInput
disableLanguageSwitcher={true}
iconType="search"
indexPatterns={[derivedIndexPattern]}
indexPatterns={[derivedDataView]}
isInvalid={!isFilterQueryDraftValid}
onChange={(query: Query) => {
setSurroundingLogsId(null);

View file

@ -10,10 +10,11 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public';
import { from } from 'rxjs';
import { map } from 'rxjs/operators';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { defaultLogViewsStaticConfig } from '../common/log_views';
import { InfraPublicConfig } from '../common/plugin_config_types';
import { createInventoryMetricRuleType } from './alerting/inventory';
import { createLogThresholdRuleType } from './alerting/log_threshold';
import { createMetricThresholdRuleType } from './alerting/metric_threshold';
import type { CoreProvidersProps } from './apps/common_providers';
import { createLazyContainerMetricsTable } from './components/infrastructure_node_metrics_tables/container/create_lazy_container_metrics_table';
import { createLazyHostMetricsTable } from './components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table';
import { createLazyPodMetricsTable } from './components/infrastructure_node_metrics_tables/pod/create_lazy_pod_metrics_table';
@ -21,17 +22,29 @@ import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embedd
import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory';
import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers';
import { registerFeatures } from './register_feature';
import { LogViewsService } from './services/log_views';
import {
InfraClientCoreSetup,
InfraClientCoreStart,
InfraClientPluginClass,
InfraClientSetupDeps,
InfraClientStartDeps,
InfraClientStartExports,
InfraClientStartServices,
} from './types';
import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers';
export class Plugin implements InfraClientPluginClass {
constructor(_context: PluginInitializerContext) {}
public config: InfraPublicConfig;
private logViews: LogViewsService;
constructor(context: PluginInitializerContext<InfraPublicConfig>) {
this.config = context.config.get();
this.logViews = new LogViewsService({
messageFields:
this.config.sources?.default?.fields?.message ?? defaultLogViewsStaticConfig.messageFields,
});
}
setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) {
if (pluginsSetup.home) {
@ -42,7 +55,9 @@ export class Plugin implements InfraClientPluginClass {
createInventoryMetricRuleType()
);
pluginsSetup.observability.observabilityRuleTypeRegistry.register(createLogThresholdRuleType());
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createLogThresholdRuleType(core)
);
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createMetricThresholdRuleType()
);
@ -144,10 +159,10 @@ export class Plugin implements InfraClientPluginClass {
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {
// mount callback should not use setup dependencies, get start dependencies instead
const [coreStart, pluginsStart] = await core.getStartServices();
const [coreStart, pluginsStart, pluginStart] = await core.getStartServices();
const { renderApp } = await import('./apps/logs_app');
return renderApp(coreStart, pluginsStart, params);
return renderApp(coreStart, pluginsStart, pluginStart, params);
},
});
@ -186,10 +201,10 @@ export class Plugin implements InfraClientPluginClass {
],
mount: async (params: AppMountParameters) => {
// mount callback should not use setup dependencies, get start dependencies instead
const [coreStart, pluginsStart] = await core.getStartServices();
const [coreStart, pluginsStart, pluginStart] = await core.getStartServices();
const { renderApp } = await import('./apps/metrics_app');
return renderApp(coreStart, pluginsStart, params);
return renderApp(coreStart, pluginsStart, pluginStart, params);
},
});
@ -209,17 +224,22 @@ export class Plugin implements InfraClientPluginClass {
}
start(core: InfraClientCoreStart, plugins: InfraClientStartDeps) {
const coreProvidersProps: CoreProvidersProps = {
core,
plugins,
theme$: core.theme.theme$,
const getStartServices = (): InfraClientStartServices => [core, plugins, startContract];
const logViews = this.logViews.start({
http: core.http,
dataViews: plugins.dataViews,
search: plugins.data.search,
});
const startContract: InfraClientStartExports = {
logViews,
ContainerMetricsTable: createLazyContainerMetricsTable(getStartServices),
HostMetricsTable: createLazyHostMetricsTable(getStartServices),
PodMetricsTable: createLazyPodMetricsTable(getStartServices),
};
return {
ContainerMetricsTable: createLazyContainerMetricsTable(coreProvidersProps),
HostMetricsTable: createLazyHostMetricsTable(coreProvidersProps),
PodMetricsTable: createLazyPodMetricsTable(coreProvidersProps),
};
return startContract;
}
stop() {}

View file

@ -5,5 +5,6 @@
* 2.0.
*/
export * from './configuration';
export * from './status';
export * from './log_views_client';
export * from './log_views_service';
export * from './types';

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ILogViewsClient } from './types';
export const createLogViewsClientMock = (): jest.Mocked<ILogViewsClient> => ({
getLogView: jest.fn(),
getResolvedLogView: jest.fn(),
getResolvedLogViewStatus: jest.fn(),
putLogView: jest.fn(),
resolveLogView: jest.fn(),
});

View file

@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { HttpStart } from 'src/core/public';
import { ISearchGeneric } from 'src/plugins/data/public';
import { DataViewsContract } from 'src/plugins/data_views/public';
import {
getLogViewResponsePayloadRT,
getLogViewUrl,
putLogViewRequestPayloadRT,
} from '../../../common/http_api/log_views';
import {
FetchLogViewError,
FetchLogViewStatusError,
LogView,
LogViewAttributes,
LogViewsStaticConfig,
LogViewStatus,
PutLogViewError,
ResolvedLogView,
resolveLogView,
} from '../../../common/log_views';
import { decodeOrThrow } from '../../../common/runtime_types';
import { ILogViewsClient } from './types';
export class LogViewsClient implements ILogViewsClient {
constructor(
private readonly dataViews: DataViewsContract,
private readonly http: HttpStart,
private readonly search: ISearchGeneric,
private readonly config: LogViewsStaticConfig
) {}
public async getLogView(logViewId: string): Promise<LogView> {
const response = await this.http.get(getLogViewUrl(logViewId)).catch((error) => {
throw new FetchLogViewError(`Failed to fetch log view "${logViewId}": ${error}`);
});
const { data } = decodeOrThrow(
getLogViewResponsePayloadRT,
(message: string) =>
new FetchLogViewError(`Failed to decode log view "${logViewId}": ${message}"`)
)(response);
return data;
}
public async getResolvedLogView(logViewId: string): Promise<ResolvedLogView> {
const logView = await this.getLogView(logViewId);
const resolvedLogView = await this.resolveLogView(logView.attributes);
return resolvedLogView;
}
public async getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise<LogViewStatus> {
const indexStatus = await this.search({
params: {
ignore_unavailable: true,
allow_no_indices: true,
index: resolvedLogView.indices,
size: 0,
terminate_after: 1,
track_total_hits: 1,
},
})
.toPromise()
.then(
({ rawResponse }) => {
if (rawResponse._shards.total <= 0) {
return 'missing' as const;
}
const totalHits = decodeTotalHits(rawResponse.hits.total);
if (typeof totalHits === 'number' ? totalHits > 0 : totalHits.value > 0) {
return 'available' as const;
}
return 'empty' as const;
},
(err) => {
if (err.status === 404) {
return 'missing' as const;
}
throw new FetchLogViewStatusError(
`Failed to check status of log indices of "${resolvedLogView.indices}": ${err}`
);
}
);
return {
index: indexStatus,
};
}
public async putLogView(
logViewId: string,
logViewAttributes: Partial<LogViewAttributes>
): Promise<LogView> {
const response = await this.http
.put(getLogViewUrl(logViewId), {
body: JSON.stringify(putLogViewRequestPayloadRT.encode({ attributes: logViewAttributes })),
})
.catch((error) => {
throw new PutLogViewError(`Failed to write log view "${logViewId}": ${error}`);
});
const { data } = decodeOrThrow(
getLogViewResponsePayloadRT,
(message: string) =>
new PutLogViewError(`Failed to decode written log view "${logViewId}": ${message}"`)
)(response);
return data;
}
public async resolveLogView(logViewAttributes: LogViewAttributes): Promise<ResolvedLogView> {
return await resolveLogView(logViewAttributes, this.dataViews, this.config);
}
}
const decodeTotalHits = decodeOrThrow(
rt.union([
rt.number,
rt.type({
value: rt.number,
}),
])
);

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createLogViewsClientMock } from './log_views_client.mock';
import { LogViewsServiceStart } from './types';
export const createLogViewsServiceStartMock = () => ({
client: createLogViewsClientMock(),
});
export const _ensureTypeCompatibility = (): LogViewsServiceStart =>
createLogViewsServiceStartMock();

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogViewsStaticConfig } from '../../../common/log_views';
import { LogViewsClient } from './log_views_client';
import { LogViewsServiceStartDeps, LogViewsServiceSetup, LogViewsServiceStart } from './types';
export class LogViewsService {
constructor(private readonly config: LogViewsStaticConfig) {}
public setup(): LogViewsServiceSetup {}
public start({ dataViews, http, search }: LogViewsServiceStartDeps): LogViewsServiceStart {
const client = new LogViewsClient(dataViews, http, search.search, this.config);
return {
client,
};
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpStart } from 'src/core/public';
import { ISearchStart } from 'src/plugins/data/public';
import { DataViewsContract } from 'src/plugins/data_views/public';
import {
LogView,
LogViewAttributes,
LogViewStatus,
ResolvedLogView,
} from '../../../common/log_views';
export type LogViewsServiceSetup = void;
export interface LogViewsServiceStart {
client: ILogViewsClient;
}
export interface LogViewsServiceStartDeps {
dataViews: DataViewsContract;
http: HttpStart;
search: ISearchStart;
}
export interface ILogViewsClient {
getLogView(logViewId: string): Promise<LogView>;
getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise<LogViewStatus>;
getResolvedLogView(logViewId: string): Promise<ResolvedLogView>;
putLogView(logViewId: string, logViewAttributes: Partial<LogViewAttributes>): Promise<LogView>;
resolveLogView(logViewAttributes: LogViewAttributes): Promise<ResolvedLogView>;
}

View file

@ -7,7 +7,7 @@
import faker from 'faker';
import { LogEntry } from '../../common/log_entry';
import { LogSourceConfiguration } from '../containers/logs/log_source';
import { LogViewColumnConfiguration } from '../../common/log_views';
export const ENTRIES_EMPTY = {
data: {
@ -21,7 +21,7 @@ export function generateFakeEntries(
count: number,
startTimestamp: number,
endTimestamp: number,
columns: LogSourceConfiguration['configuration']['logColumns']
columns: LogViewColumnConfiguration[]
): LogEntry[] {
const entries: LogEntry[] = [];
const timestampStep = Math.floor((endTimestamp - startTimestamp) / count);

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { GetLogSourceConfigurationSuccessResponsePayload } from '../../common/http_api/log_sources';
export const DEFAULT_SOURCE_CONFIGURATION: GetLogSourceConfigurationSuccessResponsePayload = {
data: {
id: 'default',
version: 'WzQwNiwxXQ==',
updatedAt: 1608559663482,
origin: 'stored',
configuration: {
name: 'Default',
description: '',
logIndices: {
type: 'index_pattern',
indexPatternId: 'some-test-id',
},
fields: {
container: 'container.id',
host: 'host.name',
pod: 'kubernetes.pod.uid',
tiebreaker: '_doc',
timestamp: '@timestamp',
message: ['message'],
},
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

@ -8,6 +8,7 @@
import type { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public';
import { IHttpFetchError } from 'src/core/public';
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type { DataViewsPublicPluginStart } from '../../../../src/plugins/data_views/public';
import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public';
@ -27,15 +28,18 @@ import type {
} from '../../observability/public';
// import type { OsqueryPluginStart } from '../../osquery/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { UnwrapPromise } from '../common/utility_types';
import type {
SourceProviderProps,
UseNodeMetricsTableOptions,
} from './components/infrastructure_node_metrics_tables/shared';
import { LogViewsServiceStart } from './services/log_views';
// Our own setup and start contract values
export type InfraClientSetupExports = void;
export interface InfraClientStartExports {
logViews: LogViewsServiceStart;
ContainerMetricsTable: (
props: UseNodeMetricsTableOptions & Partial<SourceProviderProps>
) => JSX.Element;
@ -61,6 +65,7 @@ export interface InfraClientSetupDeps {
export interface InfraClientStartDeps {
data: DataPublicPluginStart;
dataEnhanced: DataEnhancedStart;
dataViews: DataViewsPublicPluginStart;
observability: ObservabilityPublicStart;
spaces: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
@ -79,6 +84,8 @@ export type InfraClientPluginClass = PluginClass<
InfraClientSetupDeps,
InfraClientStartDeps
>;
export type InfraClientStartServicesAccessor = InfraClientCoreSetup['getStartServices'];
export type InfraClientStartServices = UnwrapPromise<ReturnType<InfraClientStartServicesAccessor>>;
export interface InfraHttpError extends IHttpFetchError {
readonly body?: {

View file

@ -5,14 +5,11 @@
* 2.0.
*/
import { encode } from 'rison-node';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { encode } from 'rison-node';
import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public';
import { DEFAULT_SOURCE_ID, TIMESTAMP_FIELD } from '../../common/constants';
import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration';
import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status';
import { InfraClientCoreSetup, InfraClientStartDeps } from '../types';
import { resolveLogSourceConfiguration } from '../../common/log_sources';
import { InfraClientStartDeps, InfraClientStartServicesAccessor } from '../types';
interface StatsAggregation {
buckets: Array<{
@ -34,37 +31,32 @@ interface LogParams {
type StatsAndSeries = Pick<LogsFetchDataResponse, 'stats' | 'series'>;
export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) {
export function getLogsHasDataFetcher(getStartServices: InfraClientStartServicesAccessor) {
return async () => {
const [core] = await getStartServices();
const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch);
const [, , { logViews }] = await getStartServices();
const resolvedLogView = await logViews.client.getResolvedLogView(DEFAULT_SOURCE_ID);
const logViewStatus = await logViews.client.getResolvedLogViewStatus(resolvedLogView);
const hasData = logViewStatus.index === 'available';
const indices = resolvedLogView.indices;
return {
hasData: sourceStatus.data.logIndexStatus === 'available',
indices: sourceStatus.data.indices,
hasData,
indices,
};
};
}
export function getLogsOverviewDataFetcher(
getStartServices: InfraClientCoreSetup['getStartServices']
getStartServices: InfraClientStartServicesAccessor
): FetchData<LogsFetchDataResponse> {
return async (params) => {
const [core, startPlugins] = await getStartServices();
const { data } = startPlugins;
const sourceConfiguration = await callFetchLogSourceConfigurationAPI(
DEFAULT_SOURCE_ID,
core.http.fetch
);
const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration(
sourceConfiguration.data.configuration,
startPlugins.data.indexPatterns
);
const [, { data }, { logViews }] = await getStartServices();
const resolvedLogView = await logViews.client.getResolvedLogView(DEFAULT_SOURCE_ID);
const { stats, series } = await fetchLogsOverview(
{
index: resolvedLogSourceConfiguration.indices,
index: resolvedLogView.indices,
},
params,
data

View file

@ -6,26 +6,14 @@
*/
import { CoreStart } from 'kibana/public';
import { of } from 'rxjs';
import { coreMock } from 'src/core/public/mocks';
import { dataPluginMock } from 'src/plugins/data/public/mocks';
import { createIndexPatternMock } from '../../common/dependency_mocks/index_patterns';
import { GetLogSourceConfigurationSuccessResponsePayload } from '../../common/http_api/log_sources/get_log_source_configuration';
import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration';
import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status';
import { createResolvedLogViewMock } from '../../common/log_views/resolved_log_view.mock';
import { createInfraPluginStartMock } from '../mocks';
import { InfraClientStartDeps, InfraClientStartExports } from '../types';
import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './logs_overview_fetchers';
jest.mock('../containers/logs/log_source/api/fetch_log_source_status');
const mockedCallFetchLogSourceStatusAPI = callFetchLogSourceStatusAPI as jest.MockedFunction<
typeof callFetchLogSourceStatusAPI
>;
jest.mock('../containers/logs/log_source/api/fetch_log_source_configuration');
const mockedCallFetchLogSourceConfigurationAPI =
callFetchLogSourceConfigurationAPI as jest.MockedFunction<
typeof callFetchLogSourceConfigurationAPI
>;
const DEFAULT_PARAMS = {
absoluteTime: { start: 1593430680000, end: 1593430800000 },
relativeTime: { start: 'now-2m', end: 'now' }, // Doesn't matter for the test
@ -36,155 +24,110 @@ const DEFAULT_PARAMS = {
function setup() {
const core = coreMock.createStart();
const data = dataPluginMock.createStartContract();
const pluginStart = createInfraPluginStartMock();
const pluginDeps = { data } as InfraClientStartDeps;
// `dataResponder.mockReturnValue()` will be the `response` in
//
// const searcher = data.search.getSearchStrategy('sth');
// searcher.search(...).subscribe((**response**) => {});
//
const dataResponder = jest.fn();
const dataSearch = data.search.search as jest.MockedFunction<typeof data.search.search>;
(data.indexPatterns.get as jest.Mock).mockResolvedValue(
createIndexPatternMock({
id: 'test-index-pattern',
title: 'log-indices-*',
timeFieldName: '@timestamp',
type: undefined,
fields: [
{
name: 'event.dataset',
type: 'string',
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
},
{
name: 'runtime_field',
type: 'string',
runtimeField: {
type: 'keyword',
script: {
source: 'emit("runtime value")',
},
},
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
},
],
})
const mockedGetStartServices = jest.fn(() =>
Promise.resolve<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>([
core,
pluginDeps,
pluginStart,
])
);
(data.search.search as jest.Mock).mockReturnValue({
subscribe: (progress: Function, error: Function, finish: Function) => {
progress(dataResponder());
finish();
},
});
const mockedGetStartServices = jest.fn(() => {
const deps = { data };
return Promise.resolve([
core as CoreStart,
deps as InfraClientStartDeps,
{} as InfraClientStartExports,
]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>;
});
return { core, mockedGetStartServices, dataResponder };
return { core, dataSearch, mockedGetStartServices, pluginStart };
}
describe('Logs UI Observability Homepage Functions', () => {
describe('getLogsHasDataFetcher()', () => {
beforeEach(() => {
mockedCallFetchLogSourceStatusAPI.mockReset();
});
it('should return true when non-empty indices exist', async () => {
const { mockedGetStartServices } = setup();
beforeEach(() => {
jest.clearAllMocks();
});
mockedCallFetchLogSourceStatusAPI.mockResolvedValue({
data: { logIndexStatus: 'available', indices: 'test-index' },
describe('getLogsHasDataFetcher()', () => {
it('should return true when non-empty indices exist', async () => {
const { mockedGetStartServices, pluginStart } = setup();
pluginStart.logViews.client.getResolvedLogView.mockResolvedValue(
createResolvedLogViewMock({ indices: 'test-index' })
);
pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({
index: 'available',
});
const hasData = getLogsHasDataFetcher(mockedGetStartServices);
const response = await hasData();
expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1);
expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1);
expect(response).toEqual({ hasData: true, indices: 'test-index' });
});
it('should return false when only empty indices exist', async () => {
const { mockedGetStartServices } = setup();
const { mockedGetStartServices, pluginStart } = setup();
mockedCallFetchLogSourceStatusAPI.mockResolvedValue({
data: { logIndexStatus: 'empty', indices: 'test-index' },
pluginStart.logViews.client.getResolvedLogView.mockResolvedValue(
createResolvedLogViewMock({ indices: 'test-index' })
);
pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({
index: 'empty',
});
const hasData = getLogsHasDataFetcher(mockedGetStartServices);
const response = await hasData();
expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1);
expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1);
expect(response).toEqual({ hasData: false, indices: 'test-index' });
});
it('should return false when no index exists', async () => {
const { mockedGetStartServices } = setup();
const { mockedGetStartServices, pluginStart } = setup();
mockedCallFetchLogSourceStatusAPI.mockResolvedValue({
data: { logIndexStatus: 'missing', indices: 'test-index' },
pluginStart.logViews.client.getResolvedLogView.mockResolvedValue(
createResolvedLogViewMock({ indices: 'test-index' })
);
pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({
index: 'missing',
});
const hasData = getLogsHasDataFetcher(mockedGetStartServices);
const response = await hasData();
expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1);
expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1);
expect(response).toEqual({ hasData: false, indices: 'test-index' });
});
});
describe('getLogsOverviewDataFetcher()', () => {
beforeAll(() => {
mockedCallFetchLogSourceConfigurationAPI.mockResolvedValue({
data: {
configuration: {
logIndices: {
type: 'index_pattern',
indexPatternId: 'test-index-pattern',
},
},
},
} as GetLogSourceConfigurationSuccessResponsePayload);
});
afterAll(() => {
mockedCallFetchLogSourceConfigurationAPI.mockReset();
});
it('should work', async () => {
const { mockedGetStartServices, dataResponder } = setup();
const { mockedGetStartServices, dataSearch, pluginStart } = setup();
dataResponder.mockReturnValue({
rawResponse: {
aggregations: {
stats: {
buckets: [
{
key: 'nginx',
doc_count: 250, // Count is for 2 minutes
series: {
buckets: [
// Counts are per 30 seconds
{ key: 1593430680000, doc_count: 25 },
{ key: 1593430710000, doc_count: 50 },
{ key: 1593430740000, doc_count: 75 },
{ key: 1593430770000, doc_count: 100 },
],
pluginStart.logViews.client.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock());
dataSearch.mockReturnValue(
of({
rawResponse: {
aggregations: {
stats: {
buckets: [
{
key: 'nginx',
doc_count: 250, // Count is for 2 minutes
series: {
buckets: [
// Counts are per 30 seconds
{ key: 1593430680000, doc_count: 25 },
{ key: 1593430710000, doc_count: 50 },
{ key: 1593430740000, doc_count: 75 },
{ key: 1593430770000, doc_count: 100 },
],
},
},
},
],
],
},
},
},
},
});
})
);
const fetchData = getLogsOverviewDataFetcher(mockedGetStartServices);
const response = await fetchData(DEFAULT_PARAMS);

View file

@ -23,11 +23,10 @@ export const useObservable = <
createObservableOnce: (inputValues: Observable<InputValues>) => OutputObservable,
inputValues: InputValues
) => {
const [inputValues$] = useState(() => new BehaviorSubject<InputValues>(inputValues));
const [output$] = useState(() => createObservableOnce(inputValues$));
const [output$, next] = useBehaviorSubject(createObservableOnce, () => inputValues);
useEffect(() => {
inputValues$.next(inputValues);
next(inputValues);
// `inputValues` can't be statically analyzed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, inputValues);
@ -35,6 +34,19 @@ export const useObservable = <
return output$;
};
export const useBehaviorSubject = <
InputValue,
OutputValue,
OutputObservable extends Observable<OutputValue>
>(
deriveObservableOnce: (input$: Observable<InputValue>) => OutputObservable,
createInitialValue: () => InputValue
) => {
const [subject$] = useState(() => new BehaviorSubject<InputValue>(createInitialValue()));
const [output$] = useState(() => deriveObservableOnce(subject$));
return [output$, subject$.next.bind(subject$)] as const;
};
export const useObservableState = <State, InitialState>(
state$: Observable<State>,
initialState: InitialState | (() => InitialState)

Some files were not shown because too many files have changed in this diff Show more