[Discover] Add logs source and document contexts (#184601)

## 📝 Summary

This PR adds basic implementations for resolving "logs" data source and
document contexts to their respective profiles. Due to the limited set
of profile customization points the new profiles are empty.

- closes #184079 
- closes #184080 

## 🔍 Implementation details

- In order to organize these and future profiles this PR introduces the
`profile_providers` folder in `context_awareness`.
- For a more structured organization, utilities for resolving logs
sources have been moved/implemented in the `@kbn/discover-utils` and
`@kbn/data-view-utils` packages.
- The code ownership for the two logs profiles is shared between the
data discovery team and the obs ux logs team.

### Document Level Logs Resolution
The document logs context resolution is performed with the following
criteria, as far as one complies, the context will be evaluated as a
match:
- The `data_stream.type` field exists on the document and it's equal to
`logs`
- The document contains any field from the [ECS Log field
set](https://www.elastic.co/guide/en/ecs/current/ecs-log.html) (fields
staring with `log.`)
- The `_index` field exists and tests positive against the allowed
indices from the [built-in definition/
settings](https://github.com/elastic/kibana/pull/184601/files#diff-5e1646fa4ec758a92aa38910dc047b18cb826e287a36b43e811eb5fc7a3b0fe9R28).

### Data Source Logs Resolution
The data source logs context resolution is performed with the following
criteria, as far as one complies, the context will be evaluated as a
match:
- Being the source of a data view type, the related index tests positive
against the allowed indices from the [built-in definition/
settings](https://github.com/elastic/kibana/pull/184601/files#diff-5e1646fa4ec758a92aa38910dc047b18cb826e287a36b43e811eb5fc7a3b0fe9R28).
- Being the source of a ES|QL query type, the related index extracted
from the query tests positive against the allowed indices from the
[built-in definition/
settings](https://github.com/elastic/kibana/pull/184601/files#diff-5e1646fa4ec758a92aa38910dc047b18cb826e287a36b43e811eb5fc7a3b0fe9R28).


## 🕵️‍♀️ Review notes 

> [!NOTE]
> Notes in this format have been left through the PR to give additional
context about some choices, any further feedback is welcome

---------

Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani01@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Kerry Gallagher <k.gallagher.05@gmail.com>
This commit is contained in:
Felix Stürmer 2024-06-18 12:18:07 +02:00 committed by GitHub
parent 4a95ffbd93
commit 051b91a47f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 696 additions and 201 deletions

View file

@ -7,3 +7,6 @@
*/
export * from './src/constants';
export { createRegExpPatternFrom } from './src/utils/create_regexp_pattern_from';
export { testPatternAgainstAllowedList } from './src/utils/test_pattern_against_allowed_list';

View file

@ -0,0 +1,68 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createRegExpPatternFrom } from './create_regexp_pattern_from';
describe('createRegExpPatternFrom should create a regular expression starting from a string that', () => {
const regExpPattern = createRegExpPatternFrom('logs');
it('tests positive for single index patterns starting with the passed base pattern', () => {
expect('logs*').toMatch(regExpPattern);
expect('logs-*').toMatch(regExpPattern);
expect('logs-*-*').toMatch(regExpPattern);
expect('logs-system.syslog-*').toMatch(regExpPattern);
expect('logss*').not.toMatch(regExpPattern);
expect('logss-*').not.toMatch(regExpPattern);
expect('metrics*').not.toMatch(regExpPattern);
expect('metrics-*').not.toMatch(regExpPattern);
});
it('tests positive for single index patterns containing the passed base pattern', () => {
expect('foo-logs*').toMatch(regExpPattern);
expect('foo-logs-*').toMatch(regExpPattern);
expect('foo-logs-*-*').toMatch(regExpPattern);
expect('foo-logs-system.syslog-*').toMatch(regExpPattern);
expect('.ds-kibana_sample_data_logs-2024.06.13-000001').toMatch(regExpPattern);
expect('foo-logss*').not.toMatch(regExpPattern);
expect('foo-logss-*').not.toMatch(regExpPattern);
expect('foo-metrics*').not.toMatch(regExpPattern);
expect('foo-metrics-*').not.toMatch(regExpPattern);
});
it('tests positive for single index patterns with CCS prefixes', () => {
expect('cluster1:logs-*').toMatch(regExpPattern);
expect('cluster1:logs-*-*').toMatch(regExpPattern);
expect('cluster1:logs-system.syslog-*').toMatch(regExpPattern);
expect('cluster1:logs-system.syslog-default').toMatch(regExpPattern);
expect('cluster1:logss*').not.toMatch(regExpPattern);
expect('cluster1:logss-*').not.toMatch(regExpPattern);
expect('cluster1:metrics*').not.toMatch(regExpPattern);
expect('cluster1:metrics-*').not.toMatch(regExpPattern);
});
it('tests positive for multiple index patterns comma-separated if all of them individually match the criteria', () => {
expect('logs-*,cluster1:logs-*').toMatch(regExpPattern);
expect('cluster1:logs-*,cluster2:logs-*').toMatch(regExpPattern);
expect('*:logs-system.syslog-*,*:logs-system.errors-*').toMatch(regExpPattern);
expect('*:metrics-system.syslog-*,logs-system.errors-*').not.toMatch(regExpPattern);
});
it('tests positive for patterns with trailing commas', () => {
expect('logs-*,').toMatch(regExpPattern);
expect('cluster1:logs-*,logs-*,').toMatch(regExpPattern);
});
it('tests negative for patterns with spaces and unexpected commas', () => {
expect('cluster1:logs-*,clust,er2:logs-*').not.toMatch(regExpPattern);
expect('cluster1:logs-*, cluster2:logs-*').not.toMatch(regExpPattern);
});
});

View file

@ -1,15 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const buildIndexPatternRegExp = (basePatterns: string[]) => {
export const createRegExpPatternFrom = (basePatterns: string | string[]) => {
const patterns = Array.isArray(basePatterns) ? basePatterns : [basePatterns];
// Create the base patterns union with strict boundaries
const basePatternGroup = `\\b(${basePatterns.join('|')})\\b([^,\\s]+)?`;
const basePatternGroup = `[^,\\s]*(\\b|_)(${patterns.join('|')})(\\b|_)([^,\\s]*)?`;
// Apply base patterns union for local and remote clusters
const localAndRemotePatternGroup = `((${basePatternGroup})|([^:,\\s]+:${basePatternGroup}))`;
const localAndRemotePatternGroup = `((${basePatternGroup})|([^:,\\s]*:${basePatternGroup}))`;
// Handle trailing comma and multiple pattern concatenation
return new RegExp(`^${localAndRemotePatternGroup}(,${localAndRemotePatternGroup})*(,$|$)`, 'i');
};

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { testPatternAgainstAllowedList } from './test_pattern_against_allowed_list';
describe('testPatternAgainstAllowedList', () => {
const allowedList = ['foo-logs-bar', /^\b(logs)\b([^,\s]*)/i];
it('should return true if the passed input matches any string or regexp of the passed list', () => {
expect(testPatternAgainstAllowedList(allowedList)('logs-*')).toBeTruthy();
expect(testPatternAgainstAllowedList(allowedList)('logs-*-*')).toBeTruthy();
expect(testPatternAgainstAllowedList(allowedList)('logs-system.syslog-*')).toBeTruthy();
expect(testPatternAgainstAllowedList(allowedList)('foo-logs-bar')).toBeTruthy();
expect(testPatternAgainstAllowedList(allowedList)('logss-*')).toBeFalsy();
expect(testPatternAgainstAllowedList(allowedList)('metrics*')).toBeFalsy();
expect(testPatternAgainstAllowedList(allowedList)('metrics-*')).toBeFalsy();
});
});

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const testPatternAgainstAllowedList =
(allowedList: Array<string | RegExp>) => (value: string) => {
for (const allowedItem of allowedList) {
const isMatchingString = typeof allowedItem === 'string' && value === allowedItem;
const isMatchingRegExp = allowedItem instanceof RegExp && allowedItem.test(value);
if (isMatchingString || isMatchingRegExp) {
return true;
}
}
// If no match is found in the allowedList, return false
return false;
};

View file

@ -10,6 +10,7 @@ export {
CONTEXT_DEFAULT_SIZE_SETTING,
CONTEXT_STEP_SETTING,
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
DEFAULT_ALLOWED_LOGS_BASE_PATTERNS,
DEFAULT_COLUMNS_SETTING,
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
@ -30,6 +31,7 @@ export {
IgnoredReason,
buildDataTableRecord,
buildDataTableRecordList,
createLogsContextService,
fieldConstants,
formatFieldValue,
formatHit,
@ -43,4 +45,6 @@ export {
usePager,
} from './src';
export type { LogsContextService } from './src';
export * from './src/types';

View file

@ -8,3 +8,5 @@
export * from './types';
export * from './utils';
export * from './logs_context_service';

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createRegExpPatternFrom, testPatternAgainstAllowedList } from '@kbn/data-view-utils';
export interface LogsContextService {
isLogsIndexPattern(indexPattern: unknown): boolean;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface LogsContextServiceDeps {
// We will probably soon add uiSettings as a dependency
// to consume user configured indices
}
export const DEFAULT_ALLOWED_LOGS_BASE_PATTERNS = [
'log',
'logs',
'logstash',
'auditbeat',
'filebeat',
'winlogbeat',
];
export const createLogsContextService = (_deps: LogsContextServiceDeps = {}) => {
// This is initially an hard-coded set of well-known base patterns,
// we can extend this allowed list with any setting coming from uiSettings
const ALLOWED_LOGS_DATA_SOURCES = [createRegExpPatternFrom(DEFAULT_ALLOWED_LOGS_BASE_PATTERNS)];
const isLogsIndexPattern = (indexPattern: unknown) => {
return (
typeof indexPattern === 'string' &&
testPatternAgainstAllowedList(ALLOWED_LOGS_DATA_SOURCES)(indexPattern)
);
};
return {
isLogsIndexPattern,
};
};

View file

@ -18,6 +18,7 @@
"kbn_references": [
"@kbn/data-service",
"@kbn/data-views-plugin",
"@kbn/data-view-utils",
"@kbn/es-query",
"@kbn/field-formats-plugin",
"@kbn/field-types",

View file

@ -25,3 +25,11 @@ export const isDataSourceType = <T extends DataSourceType>(
dataSource: DiscoverDataSource | undefined,
type: T
): dataSource is Extract<DiscoverDataSource, { type: T }> => dataSource?.type === type;
export const isDataViewSource = (
dataSource: DiscoverDataSource | undefined
): dataSource is DataViewDataSource => isDataSourceType(dataSource, DataSourceType.DataView);
export const isEsqlSource = (
dataSource: DiscoverDataSource | undefined
): dataSource is EsqlDataSource => isDataSourceType(dataSource, DataSourceType.Esql);

View file

@ -56,7 +56,7 @@ import { memoize, noop } from 'lodash';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
import type { DiscoverStartPlugins } from './plugin';
import type { DiscoverStartPlugins } from './types';
import type { DiscoverContextAppLocator } from './application/context/services/locator';
import type { DiscoverSingleDocLocator } from './application/doc/locator';
import type { DiscoverAppLocator } from '../common';

View file

@ -19,6 +19,7 @@ import {
RootProfileService,
SolutionType,
} from '../profiles';
import { createProfileProviderServices } from '../profiles/profile_provider_services';
import { ProfilesManager } from '../profiles_manager';
export const createContextAwarenessMocks = () => {
@ -107,6 +108,8 @@ export const createContextAwarenessMocks = () => {
documentProfileServiceMock
);
const profileProviderServices = createProfileProviderServices();
return {
rootProfileProviderMock,
dataSourceProfileProviderMock,
@ -114,5 +117,6 @@ export const createContextAwarenessMocks = () => {
contextRecordMock,
contextRecordMock2,
profilesManagerMock,
profileProviderServices,
};
};

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { createLogDocumentProfileProvider } from './profile';

View file

@ -0,0 +1,78 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { buildDataTableRecord } from '@kbn/discover-utils';
import { DocumentType } from '../../profiles';
import { createContextAwarenessMocks } from '../../__mocks__';
import { createLogDocumentProfileProvider } from './profile';
const mockServices = createContextAwarenessMocks().profileProviderServices;
describe('logDocumentProfileProvider', () => {
const logDocumentProfileProvider = createLogDocumentProfileProvider(mockServices);
const RESOLUTION_MATCH = {
isMatch: true,
context: {
type: DocumentType.Log,
},
};
const RESOLUTION_MISMATCH = {
isMatch: false,
};
it('matches records with the correct data stream type', () => {
expect(
logDocumentProfileProvider.resolve({
record: buildMockRecord('logs-2000-01-01', {
'data_stream.type': ['logs'],
}),
})
).toEqual(RESOLUTION_MATCH);
});
it('matches records with fields prefixed with "log."', () => {
expect(
logDocumentProfileProvider.resolve({
record: buildMockRecord('logs-2000-01-01', {
'log.level': ['INFO'],
}),
})
).toEqual(RESOLUTION_MATCH);
});
it('matches records with indices matching the allowed pattern', () => {
expect(
logDocumentProfileProvider.resolve({
record: buildMockRecord('logs-2000-01-01'),
})
).toEqual(RESOLUTION_MATCH);
expect(
logDocumentProfileProvider.resolve({
record: buildMockRecord('remote_cluster:filebeat'),
})
).toEqual(RESOLUTION_MATCH);
});
it('does not match records with neither characteristic', () => {
expect(
logDocumentProfileProvider.resolve({
record: buildMockRecord('another-index'),
})
).toEqual(RESOLUTION_MISMATCH);
});
});
const buildMockRecord = (index: string, fields: Record<string, unknown[]> = {}) =>
buildDataTableRecord({
_id: '',
_index: index,
fields: {
_index: index,
...fields,
},
});

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DataTableRecord } from '@kbn/discover-utils';
import { DocumentProfileProvider, DocumentType } from '../../profiles';
import { ProfileProviderServices } from '../../profiles/profile_provider_services';
export const createLogDocumentProfileProvider = (
services: ProfileProviderServices
): DocumentProfileProvider => ({
profileId: 'log-document-profile',
profile: {},
resolve: ({ record }) => {
const isLogRecord = getIsLogRecord(record, services.logsContextService.isLogsIndexPattern);
if (!isLogRecord) {
return { isMatch: false };
}
return {
isMatch: true,
context: {
type: DocumentType.Log,
},
};
},
});
const getIsLogRecord = (
record: DataTableRecord,
isLogsIndexPattern: ProfileProviderServices['logsContextService']['isLogsIndexPattern']
) => {
return (
getDataStreamType(record).includes('logs') ||
hasFieldsWithPrefix('log.')(record) ||
getIndices(record).some(isLogsIndexPattern)
);
};
const getFieldValues =
(field: string) =>
(record: DataTableRecord): unknown[] => {
const value = record.flattened[field];
return Array.isArray(value) ? value : [value];
};
const getDataStreamType = getFieldValues('data_stream.type');
const getIndices = getFieldValues('_index');
const hasFieldsWithPrefix = (prefix: string) => (record: DataTableRecord) => {
return Object.keys(record.flattened).some((field) => field.startsWith(prefix));
};

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { createLogsDataSourceProfileProvider } from './profile';

View file

@ -0,0 +1,78 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createStubIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub';
import { createDataViewDataSource, createEsqlDataSource } from '../../../../common/data_sources';
import { DataSourceCategory } from '../../profiles';
import { createContextAwarenessMocks } from '../../__mocks__';
import { createLogsDataSourceProfileProvider } from './profile';
const mockServices = createContextAwarenessMocks().profileProviderServices;
describe('logsDataSourceProfileProvider', () => {
const logsDataSourceProfileProvider = createLogsDataSourceProfileProvider(mockServices);
const VALID_INDEX_PATTERN = 'logs-nginx.access-*';
const MIXED_INDEX_PATTERN = 'logs-nginx.access-*,metrics-*';
const INVALID_INDEX_PATTERN = 'my_source-access-*';
const RESOLUTION_MATCH = {
isMatch: true,
context: { category: DataSourceCategory.Logs },
};
const RESOLUTION_MISMATCH = {
isMatch: false,
};
it('should match ES|QL sources with an allowed index pattern in its query', () => {
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createEsqlDataSource(),
query: { esql: `from ${VALID_INDEX_PATTERN}` },
})
).toEqual(RESOLUTION_MATCH);
});
it('should NOT match ES|QL sources with a mixed or not allowed index pattern in its query', () => {
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createEsqlDataSource(),
query: { esql: `from ${INVALID_INDEX_PATTERN}` },
})
).toEqual(RESOLUTION_MISMATCH);
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createEsqlDataSource(),
query: { esql: `from ${MIXED_INDEX_PATTERN}` },
})
).toEqual(RESOLUTION_MISMATCH);
});
it('should match data view sources with an allowed index pattern', () => {
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createDataViewDataSource({ dataViewId: VALID_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: VALID_INDEX_PATTERN } }),
})
).toEqual(RESOLUTION_MATCH);
});
it('should NOT match data view sources with a mixed or not allowed index pattern', () => {
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createDataViewDataSource({ dataViewId: INVALID_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: INVALID_INDEX_PATTERN } }),
})
).toEqual(RESOLUTION_MISMATCH);
expect(
logsDataSourceProfileProvider.resolve({
dataSource: createDataViewDataSource({ dataViewId: MIXED_INDEX_PATTERN }),
dataView: createStubIndexPattern({ spec: { title: MIXED_INDEX_PATTERN } }),
})
).toEqual(RESOLUTION_MISMATCH);
});
});

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import { isDataViewSource, isEsqlSource } from '../../../../common/data_sources';
import {
DataSourceCategory,
DataSourceProfileProvider,
DataSourceProfileProviderParams,
} from '../../profiles';
import { ProfileProviderServices } from '../../profiles/profile_provider_services';
export const createLogsDataSourceProfileProvider = (
services: ProfileProviderServices
): DataSourceProfileProvider => ({
profileId: 'logs-data-source-profile',
profile: {},
resolve: (params) => {
const indexPattern = extractIndexPatternFrom(params);
if (!services.logsContextService.isLogsIndexPattern(indexPattern)) {
return { isMatch: false };
}
return {
isMatch: true,
context: { category: DataSourceCategory.Logs },
};
},
});
const extractIndexPatternFrom = ({
dataSource,
dataView,
query,
}: DataSourceProfileProviderParams) => {
if (isEsqlSource(dataSource) && isOfAggregateQueryType(query)) {
return getIndexPatternFromESQLQuery(query.esql);
} else if (isDataViewSource(dataSource) && dataView) {
return dataView.getIndexPattern();
}
return null;
};

View file

@ -0,0 +1,27 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createLogsContextService, LogsContextService } from '@kbn/discover-utils';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ProfileProviderDeps {
// We will probably soon add uiSettings as a dependency
// to consume user configured indices
}
export interface ProfileProviderServices {
logsContextService: LogsContextService;
}
export const createProfileProviderServices = (
_deps: ProfileProviderDeps = {}
): ProfileProviderServices => {
return {
logsContextService: createLogsContextService(),
};
};

View file

@ -9,7 +9,7 @@
import type { PluginInitializerContext } from '@kbn/core/public';
import { DiscoverPlugin } from './plugin';
export type { DiscoverSetup, DiscoverStart } from './plugin';
export type { DiscoverSetup, DiscoverStart } from './types';
export function plugin(initializerContext: PluginInitializerContext) {
return new DiscoverPlugin(initializerContext);
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { ComponentType } from 'react';
import React from 'react';
import { BehaviorSubject, map, Observable } from 'rxjs';
import {
AppMountParameters,
@ -17,42 +17,10 @@ import {
PluginInitializerContext,
ScopedHistory,
} from '@kbn/core/public';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public';
import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public';
import { SharePluginStart, SharePluginSetup } from '@kbn/share-plugin/public';
import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { UnifiedDocViewerStart } from '@kbn/unified-doc-viewer-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import { TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
import type {
ObservabilityAIAssistantPublicSetup,
ObservabilityAIAssistantPublicStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
import { PLUGIN_ID } from '../common';
import { registerFeature } from './register_feature';
import { buildServices, UrlTracker } from './build_services';
@ -88,132 +56,10 @@ import {
ProfilesManager,
RootProfileService,
} from './context_awareness';
/**
* @public
*/
export interface DiscoverSetup {
/**
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
readonly showInlineTopNav: () => void;
readonly configureInlineTopNav: (
projectNavId: string,
options: DiscoverCustomizationContext['inlineTopNav']
) => void;
}
export interface DiscoverStart {
/**
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
readonly DiscoverContainer: ComponentType<DiscoverContainerProps>;
}
/**
* @internal
*/
export interface DiscoverSetupPlugins {
dataViews: DataViewsServicePublic;
share?: SharePluginSetup;
uiActions: UiActionsSetup;
embeddable: EmbeddableSetup;
urlForwarding: UrlForwardingSetup;
home?: HomePublicPluginSetup;
data: DataPublicPluginSetup;
expressions: ExpressionsSetup;
globalSearch?: GlobalSearchPluginSetup;
observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup;
}
/**
* @internal
*/
export interface DiscoverStartPlugins {
aiops?: AiopsPluginStart;
dataViews: DataViewsServicePublic;
dataViewEditor: DataViewEditorStart;
dataVisualizer?: DataVisualizerPluginStart;
uiActions: UiActionsStart;
embeddable: EmbeddableStart;
navigation: NavigationStart;
charts: ChartsPluginStart;
data: DataPublicPluginStart;
fieldFormats: FieldFormatsStart;
share?: SharePluginStart;
urlForwarding: UrlForwardingStart;
inspector: InspectorPublicPluginStart;
usageCollection?: UsageCollectionSetup;
dataViewFieldEditor: IndexPatternFieldEditorStart;
spaces?: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
expressions: ExpressionsStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedSearch: SavedSearchPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
unifiedDocViewer: UnifiedDocViewerStart;
lens: LensPublicStart;
contentManagement: ContentManagementPublicStart;
noDataPage?: NoDataPagePluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
}
import { createProfileProviderServices } from './context_awareness/profiles/profile_provider_services';
import { DiscoverSetup, DiscoverSetupPlugins, DiscoverStart, DiscoverStartPlugins } from './types';
import { createLogsDataSourceProfileProvider } from './context_awareness/profile_providers/logs_data_source_profile';
import { createLogDocumentProfileProvider } from './context_awareness/profile_providers/log_document_profile';
/**
* Contains Discover, one of the oldest parts of Kibana
@ -459,10 +305,14 @@ export class DiscoverPlugin
}
private registerProfiles() {
// TODO: Conditionally register example profiles for functional testing in a follow up PR
// this.rootProfileService.registerProvider(o11yRootProfileProvider);
// this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider);
// this.documentProfileService.registerProvider(logDocumentProfileProvider);
const providerServices = createProfileProviderServices();
this.dataSourceProfileService.registerProvider(
createLogsDataSourceProfileProvider(providerServices)
);
this.documentProfileService.registerProvider(
createLogDocumentProfileProvider(providerServices)
);
}
private createProfilesManager() {

View file

@ -0,0 +1,170 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ComponentType } from 'react';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public';
import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public';
import { SharePluginStart, SharePluginSetup } from '@kbn/share-plugin/public';
import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { UnifiedDocViewerStart } from '@kbn/unified-doc-viewer-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
import type {
ObservabilityAIAssistantPublicSetup,
ObservabilityAIAssistantPublicStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
import { DiscoverAppLocator } from '../common';
import { DiscoverCustomizationContext } from './customizations';
import { type DiscoverContainerProps } from './components/discover_container';
/**
* @public
*/
export interface DiscoverSetup {
/**
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
readonly showInlineTopNav: () => void;
readonly configureInlineTopNav: (
projectNavId: string,
options: DiscoverCustomizationContext['inlineTopNav']
) => void;
}
export interface DiscoverStart {
/**
* `share` plugin URL locator for Discover app. Use it to generate links into
* Discover application, for example, navigate:
*
* ```ts
* await plugins.discover.locator.navigate({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*
* Generate a location:
*
* ```ts
* const location = await plugins.discover.locator.getLocation({
* savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
* indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
* timeRange: {
* to: 'now',
* from: 'now-15m',
* mode: 'relative',
* },
* });
* ```
*/
readonly locator: undefined | DiscoverAppLocator;
readonly DiscoverContainer: ComponentType<DiscoverContainerProps>;
}
/**
* @internal
*/
export interface DiscoverSetupPlugins {
data: DataPublicPluginSetup;
dataViews: DataViewsServicePublic;
embeddable: EmbeddableSetup;
expressions: ExpressionsSetup;
globalSearch?: GlobalSearchPluginSetup;
home?: HomePublicPluginSetup;
observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup;
share?: SharePluginSetup;
uiActions: UiActionsSetup;
urlForwarding: UrlForwardingSetup;
}
/**
* @internal
*/
export interface DiscoverStartPlugins {
aiops?: AiopsPluginStart;
charts: ChartsPluginStart;
contentManagement: ContentManagementPublicStart;
data: DataPublicPluginStart;
dataViewEditor: DataViewEditorStart;
dataViewFieldEditor: IndexPatternFieldEditorStart;
dataViews: DataViewsServicePublic;
dataVisualizer?: DataVisualizerPluginStart;
embeddable: EmbeddableStart;
expressions: ExpressionsStart;
fieldFormats: FieldFormatsStart;
inspector: InspectorPublicPluginStart;
lens: LensPublicStart;
navigation: NavigationStart;
noDataPage?: NoDataPagePluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
savedSearch: SavedSearchPublicPluginStart;
share?: SharePluginStart;
spaces?: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
uiActions: UiActionsStart;
unifiedDocViewer: UnifiedDocViewerStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
urlForwarding: UrlForwardingStart;
usageCollection?: UsageCollectionSetup;
}

View file

@ -8,7 +8,7 @@
import { AppUpdater } from '@kbn/core/public';
import { BehaviorSubject, Observable } from 'rxjs';
import { coreMock, scopedHistoryMock } from '@kbn/core/public/mocks';
import { DiscoverSetupPlugins } from '../plugin';
import { DiscoverSetupPlugins } from '../types';
import { initializeKbnUrlTracking } from './initialize_kbn_url_tracking';
describe('initializeKbnUrlTracking', () => {

View file

@ -12,7 +12,7 @@ import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { isFilterPinned } from '@kbn/es-query';
import { SEARCH_SESSION_ID_QUERY_PARAM } from '../constants';
import type { DiscoverSetupPlugins } from '../plugin';
import type { DiscoverSetupPlugins } from '../types';
/**
* It creates the kbn url tracker for Discover to listens to history changes and optionally to global state

View file

@ -20,7 +20,7 @@ import {
} from '@kbn/core/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public/plugin';
import { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public';
import type { FeaturesPluginSetup } from '@kbn/features-plugin/public';

View file

@ -59,6 +59,3 @@ export const FILTER_OUT_FIELDS_PREFIXES_FOR_CONTENT = [
'log.',
'service.',
];
export const DEFAULT_ALLOWED_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];
export const DEFAULT_ALLOWED_LOGS_DATA_VIEWS = ['logs', 'auditbeat', 'filebeat', 'winlogbeat'];

View file

@ -5,15 +5,13 @@
* 2.0.
*/
import { DEFAULT_ALLOWED_LOGS_DATA_VIEWS } from '../../constants';
import { createRegExpPatternFrom, testPatternAgainstAllowedList } from '@kbn/data-view-utils';
import { DEFAULT_ALLOWED_LOGS_BASE_PATTERNS } from '@kbn/discover-utils';
import { DataViewSpecWithId } from '../../data_source_selection';
import { DataViewDescriptorType } from '../types';
import { buildIndexPatternRegExp } from '../utils';
type AllowedList = Array<string | RegExp>;
const LOGS_ALLOWED_LIST: AllowedList = [
buildIndexPatternRegExp(DEFAULT_ALLOWED_LOGS_DATA_VIEWS),
const LOGS_ALLOWED_LIST = [
createRegExpPatternFrom(DEFAULT_ALLOWED_LOGS_BASE_PATTERNS),
// Add more strings or regex patterns as needed
];
@ -60,7 +58,9 @@ export class DataViewDescriptor {
}
testAgainstAllowedList(allowedList: string[]) {
return this.title ? isAllowed(this.title, [buildIndexPatternRegExp(allowedList)]) : false;
return this.title
? testPatternAgainstAllowedList([createRegExpPatternFrom(allowedList)])(this.title)
: false;
}
public static create({ id, namespaces, title, type, name }: DataViewDescriptorFactoryParams) {
@ -79,7 +79,7 @@ export class DataViewDescriptor {
}
static #extractDataType(title: string): DataViewDescriptorType['dataType'] {
if (isAllowed(title, LOGS_ALLOWED_LIST)) {
if (testPatternAgainstAllowedList(LOGS_ALLOWED_LIST)(title)) {
return 'logs';
}
@ -98,17 +98,3 @@ export class DataViewDescriptor {
return this.dataType === 'unresolved';
}
}
function isAllowed(value: string, allowList: AllowedList) {
for (const allowedItem of allowList) {
if (typeof allowedItem === 'string') {
return value === allowedItem;
}
if (allowedItem instanceof RegExp) {
return allowedItem.test(value);
}
}
// If no match is found in the allowList, return false
return false;
}

View file

@ -7,9 +7,9 @@
import { schema } from '@kbn/config-schema';
import { UiSettingsParams } from '@kbn/core-ui-settings-common';
import { DEFAULT_ALLOWED_LOGS_BASE_PATTERNS } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import { OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID } from '@kbn/management-settings-ids';
import { DEFAULT_ALLOWED_DATA_VIEWS } from './constants';
/**
* uiSettings definitions for Logs Explorer.
@ -20,7 +20,7 @@ export const uiSettings: Record<string, UiSettingsParams> = {
name: i18n.translate('xpack.logsExplorer.allowedDataViews', {
defaultMessage: 'Logs Explorer allowed data views',
}),
value: DEFAULT_ALLOWED_DATA_VIEWS,
value: DEFAULT_ALLOWED_LOGS_BASE_PATTERNS,
description: i18n.translate('xpack.logsExplorer.allowedDataViewsDescription', {
defaultMessage:
'A list of base patterns to match and explore data views in Logs Explorer. Remote clusters will be automatically matched for the provided base patterns.',

View file

@ -44,6 +44,7 @@
"@kbn/unified-search-plugin",
"@kbn/xstate-utils",
"@kbn/esql-utils",
"@kbn/data-view-utils",
],
"exclude": [
"target/**/*"

View file

@ -654,7 +654,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
PageObjects.observabilityLogsExplorer.getPanelEntries(menu)
);
expect(menuEntries.length).to.be(1);
expect(menuEntries.length).to.be(2);
expect(await menuEntries[0].getVisibleText()).to.be(sortedExpectedDataViews[0]);
});