mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.14`: - [[Logs UI] Fix log entry fly-out when response is slow (#187303)](https://github.com/elastic/kibana/pull/187303) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Felix Stürmer","email":"weltenwort@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-07-02T22:37:49Z","message":"[Logs UI] Fix log entry fly-out when response is slow (#187303)\n\nThis disables a change in polling behavior of the ESE search strategy,\r\nwhich was introduced with https://github.com/elastic/kibana/pull/178921.\r\nThe response processing and progress reporting depends on it.","sha":"08017ae2dce04e08ec5c1d42896fafef224bf429","branchLabelMapping":{"^v8.15.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Logs UI","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-logs","v8.15.0"],"number":187303,"url":"https://github.com/elastic/kibana/pull/187303","mergeCommit":{"message":"[Logs UI] Fix log entry fly-out when response is slow (#187303)\n\nThis disables a change in polling behavior of the ESE search strategy,\r\nwhich was introduced with https://github.com/elastic/kibana/pull/178921.\r\nThe response processing and progress reporting depends on it.","sha":"08017ae2dce04e08ec5c1d42896fafef224bf429"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.15.0","labelRegex":"^v8.15.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187303","number":187303,"mergeCommit":{"message":"[Logs UI] Fix log entry fly-out when response is slow (#187303)\n\nThis disables a change in polling behavior of the ESE search strategy,\r\nwhich was introduced with https://github.com/elastic/kibana/pull/178921.\r\nThe response processing and progress reporting depends on it.","sha":"08017ae2dce04e08ec5c1d42896fafef224bf429"}}]}] BACKPORT--> --------- Co-authored-by: Lukas Olson <olson.lukas@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
d3a4d221ef
commit
2e36156580
7 changed files with 184 additions and 120 deletions
|
@ -153,6 +153,13 @@ export interface ISearchOptions {
|
|||
*/
|
||||
isRestore?: boolean;
|
||||
|
||||
/**
|
||||
* By default, when polling, we don't retrieve the results of the search request (until it is complete). (For async
|
||||
* search, this is the difference between calling _async_search/{id} and _async_search/status/{id}.) setting this to
|
||||
* `true` will request the search results, regardless of whether or not the search is complete.
|
||||
*/
|
||||
retrieveResults?: boolean;
|
||||
|
||||
/**
|
||||
* Represents a meta-information about a Kibana entity intitating a saerch request.
|
||||
*/
|
||||
|
@ -182,5 +189,6 @@ export type ISearchOptionsSerializable = Pick<
|
|||
| 'isStored'
|
||||
| 'isSearchStored'
|
||||
| 'isRestore'
|
||||
| 'retrieveResults'
|
||||
| 'executionContext'
|
||||
>;
|
||||
|
|
|
@ -257,6 +257,8 @@ export class SearchInterceptor {
|
|||
|
||||
if (combined.sessionId !== undefined) serializableOptions.sessionId = combined.sessionId;
|
||||
if (combined.isRestore !== undefined) serializableOptions.isRestore = combined.isRestore;
|
||||
if (combined.retrieveResults !== undefined)
|
||||
serializableOptions.retrieveResults = combined.retrieveResults;
|
||||
if (combined.legacyHitsTotal !== undefined)
|
||||
serializableOptions.legacyHitsTotal = combined.legacyHitsTotal;
|
||||
if (combined.strategy !== undefined) serializableOptions.strategy = combined.strategy;
|
||||
|
|
|
@ -36,6 +36,7 @@ export function registerSearchRoute(router: DataPluginRouter): void {
|
|||
sessionId: schema.maybe(schema.string()),
|
||||
isStored: schema.maybe(schema.boolean()),
|
||||
isRestore: schema.maybe(schema.boolean()),
|
||||
retrieveResults: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
|
@ -48,6 +49,7 @@ export function registerSearchRoute(router: DataPluginRouter): void {
|
|||
sessionId,
|
||||
isStored,
|
||||
isRestore,
|
||||
retrieveResults,
|
||||
...searchRequest
|
||||
} = request.body;
|
||||
const { strategy, id } = request.params;
|
||||
|
@ -65,6 +67,7 @@ export function registerSearchRoute(router: DataPluginRouter): void {
|
|||
sessionId,
|
||||
isStored,
|
||||
isRestore,
|
||||
retrieveResults,
|
||||
}
|
||||
)
|
||||
.pipe(first())
|
||||
|
|
|
@ -73,9 +73,11 @@ export const enhancedEsSearchStrategyProvider = (
|
|||
options: IAsyncSearchOptions,
|
||||
{ esClient }: SearchStrategyDependencies
|
||||
) {
|
||||
// First, request the status of the async search, and return the status if incomplete
|
||||
const status = await asyncSearchStatus({ id, ...request }, options, { esClient });
|
||||
if (isRunningResponse(status)) return status;
|
||||
if (!options.retrieveResults) {
|
||||
// First, request the status of the async search, and return the status if incomplete
|
||||
const status = await asyncSearchStatus({ id, ...request }, options, { esClient });
|
||||
if (isRunningResponse(status)) return status;
|
||||
}
|
||||
|
||||
// Then, if the search is complete, request & return the final results
|
||||
const client = useInternalUser ? esClient.asInternalUser : esClient.asCurrentUser;
|
||||
|
|
|
@ -118,7 +118,16 @@ export const logEntriesSearchStrategyProvider = ({
|
|||
|
||||
const searchResponse$ = concat(recoveredRequest$, initialRequest$).pipe(
|
||||
take(1),
|
||||
concatMap((esRequest) => esSearchStrategy.search(esRequest, options, dependencies))
|
||||
concatMap((esRequest) =>
|
||||
esSearchStrategy.search(
|
||||
esRequest,
|
||||
{
|
||||
...options,
|
||||
retrieveResults: true, // the subsequent processing requires the actual search results
|
||||
},
|
||||
dependencies
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return combineLatest([searchResponse$, resolvedLogView$, messageFormattingRules$]).pipe(
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import { lastValueFrom, of, throwError } from 'rxjs';
|
||||
import { errors, TransportResult } from '@elastic/elasticsearch';
|
||||
import { AsyncSearchSubmitResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
savedObjectsClientMock,
|
||||
uiSettingsServiceMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import {
|
||||
IEsSearchRequest,
|
||||
IEsSearchResponse,
|
||||
ISearchStrategy,
|
||||
SearchStrategyDependencies,
|
||||
} from '@kbn/data-plugin/server';
|
||||
import { getMockSearchConfig } from '@kbn/data-plugin/config.mock';
|
||||
import { ISearchStrategy } from '@kbn/data-plugin/server';
|
||||
import { enhancedEsSearchStrategyProvider } from '@kbn/data-plugin/server/search';
|
||||
import { createSearchSessionsClientMock } from '@kbn/data-plugin/server/search/mocks';
|
||||
import { KbnSearchError } from '@kbn/data-plugin/server/search/report_search_error';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { EMPTY, lastValueFrom } from 'rxjs';
|
||||
import { createResolvedLogViewMock } from '../../../common/log_views/resolved_log_view.mock';
|
||||
import { createLogViewsClientMock } from '../log_views/log_views_client.mock';
|
||||
import { createLogViewsServiceStartMock } from '../log_views/log_views_service.mock';
|
||||
|
@ -30,23 +30,34 @@ import {
|
|||
|
||||
describe('LogEntry search strategy', () => {
|
||||
it('handles initial search requests', async () => {
|
||||
const esSearchStrategyMock = createEsSearchStrategyMock({
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
isRunning: true,
|
||||
rawResponse: {
|
||||
took: 0,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 0 },
|
||||
timed_out: false,
|
||||
hits: { total: 0, max_score: 0, hits: [] },
|
||||
const esSearchStrategy = createEsSearchStrategy();
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
const esClient = mockDependencies.esClient.asCurrentUser;
|
||||
esClient.asyncSearch.submit.mockResolvedValueOnce({
|
||||
body: {
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
response: {
|
||||
took: 0,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 0 },
|
||||
timed_out: false,
|
||||
hits: { total: 0, max_score: 0, hits: [] },
|
||||
},
|
||||
is_partial: false,
|
||||
is_running: false,
|
||||
expiration_time_in_millis: 0,
|
||||
start_time_in_millis: 0,
|
||||
},
|
||||
});
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {} as any,
|
||||
} as TransportResult<AsyncSearchSubmitResponse> as any); // type inference for the mock fails
|
||||
|
||||
const dataMock = createDataPluginMock(esSearchStrategyMock);
|
||||
const dataMock = createDataPluginMock(esSearchStrategy);
|
||||
const logViewsClientMock = createLogViewsClientMock();
|
||||
logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock());
|
||||
const logViewsMock = createLogViewsServiceStartMock();
|
||||
logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock);
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
|
||||
const logEntrySearchStrategy = logEntrySearchStrategyProvider({
|
||||
data: dataMock,
|
||||
|
@ -66,72 +77,88 @@ describe('LogEntry search strategy', () => {
|
|||
)
|
||||
);
|
||||
|
||||
// ensure log view was resolved
|
||||
expect(logViewsMock.getScopedClient).toHaveBeenCalled();
|
||||
expect(logViewsClientMock.getResolvedLogView).toHaveBeenCalled();
|
||||
expect(esSearchStrategyMock.search).toHaveBeenCalledWith(
|
||||
{
|
||||
params: expect.objectContaining({
|
||||
index: 'log-indices-*',
|
||||
body: expect.objectContaining({
|
||||
track_total_hits: false,
|
||||
terminate_after: 1,
|
||||
query: {
|
||||
ids: {
|
||||
values: ['LOG_ENTRY_ID'],
|
||||
|
||||
// ensure search request was made
|
||||
expect(esClient.asyncSearch.submit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
index: 'log-indices-*',
|
||||
body: expect.objectContaining({
|
||||
track_total_hits: false,
|
||||
terminate_after: 1,
|
||||
query: {
|
||||
ids: {
|
||||
values: ['LOG_ENTRY_ID'],
|
||||
},
|
||||
},
|
||||
runtime_mappings: {
|
||||
runtime_field: {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: 'emit("runtime value")',
|
||||
},
|
||||
},
|
||||
runtime_mappings: {
|
||||
runtime_field: {
|
||||
type: 'keyword',
|
||||
script: {
|
||||
source: 'emit("runtime value")',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
expect.anything(),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
// ensure response content is as expected
|
||||
expect(response.id).toEqual(expect.any(String));
|
||||
expect(response.isRunning).toBe(true);
|
||||
expect(response.isRunning).toBe(false);
|
||||
});
|
||||
|
||||
it('handles subsequent polling requests', async () => {
|
||||
const date = new Date(1605116827143).toISOString();
|
||||
const esSearchStrategyMock = createEsSearchStrategyMock({
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
isRunning: false,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 1 },
|
||||
timed_out: false,
|
||||
hits: {
|
||||
total: 0,
|
||||
max_score: 0,
|
||||
hits: [
|
||||
{
|
||||
_id: 'HIT_ID',
|
||||
_index: 'HIT_INDEX',
|
||||
_score: 0,
|
||||
_source: null,
|
||||
fields: {
|
||||
'@timestamp': [date],
|
||||
message: ['HIT_MESSAGE'],
|
||||
const esSearchStrategy = createEsSearchStrategy();
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
const esClient = mockDependencies.esClient.asCurrentUser;
|
||||
|
||||
// set up response to polling request
|
||||
esClient.asyncSearch.get.mockResolvedValueOnce({
|
||||
body: {
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
response: {
|
||||
took: 0,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 0 },
|
||||
timed_out: false,
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 0,
|
||||
hits: [
|
||||
{
|
||||
_id: 'HIT_ID',
|
||||
_index: 'HIT_INDEX',
|
||||
_score: 0,
|
||||
_source: null,
|
||||
fields: {
|
||||
'@timestamp': [date],
|
||||
message: ['HIT_MESSAGE'],
|
||||
},
|
||||
sort: [date as any, 1 as any], // incorrectly typed as string upstream
|
||||
},
|
||||
sort: [date as any, 1 as any], // incorrectly typed as string upstream
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
is_partial: false,
|
||||
is_running: false,
|
||||
expiration_time_in_millis: 0,
|
||||
start_time_in_millis: 0,
|
||||
},
|
||||
});
|
||||
const dataMock = createDataPluginMock(esSearchStrategyMock);
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
warnings: [],
|
||||
meta: {} as any,
|
||||
} as TransportResult<AsyncSearchSubmitResponse> as any);
|
||||
|
||||
const dataMock = createDataPluginMock(esSearchStrategy);
|
||||
const logViewsClientMock = createLogViewsClientMock();
|
||||
logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock());
|
||||
const logViewsMock = createLogViewsServiceStartMock();
|
||||
logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock);
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
|
||||
const logEntrySearchStrategy = logEntrySearchStrategyProvider({
|
||||
data: dataMock,
|
||||
|
@ -155,9 +182,18 @@ describe('LogEntry search strategy', () => {
|
|||
)
|
||||
);
|
||||
|
||||
// ensure search was polled using the get API
|
||||
expect(esClient.asyncSearch.get).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'ASYNC_REQUEST_ID' }),
|
||||
expect.anything()
|
||||
);
|
||||
expect(esClient.asyncSearch.status).not.toHaveBeenCalled();
|
||||
|
||||
// ensure log view was not resolved again
|
||||
expect(logViewsMock.getScopedClient).not.toHaveBeenCalled();
|
||||
expect(logViewsClientMock.getResolvedLogView).not.toHaveBeenCalled();
|
||||
expect(esSearchStrategyMock.search).toHaveBeenCalled();
|
||||
|
||||
// ensure response content is as expected
|
||||
expect(response.id).toEqual(requestId);
|
||||
expect(response.isRunning).toBe(false);
|
||||
expect(response.rawResponse.data).toEqual({
|
||||
|
@ -175,22 +211,30 @@ describe('LogEntry search strategy', () => {
|
|||
});
|
||||
|
||||
it('forwards errors from the underlying search strategy', async () => {
|
||||
const esSearchStrategyMock = createEsSearchStrategyMock({
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
isRunning: false,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 1 },
|
||||
timed_out: false,
|
||||
hits: { total: 0, max_score: 0, hits: [] },
|
||||
},
|
||||
});
|
||||
const dataMock = createDataPluginMock(esSearchStrategyMock);
|
||||
const esSearchStrategy = createEsSearchStrategy();
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
const esClient = mockDependencies.esClient.asCurrentUser;
|
||||
|
||||
// set up failing response
|
||||
esClient.asyncSearch.get.mockRejectedValueOnce(
|
||||
new errors.ResponseError({
|
||||
body: {
|
||||
error: {
|
||||
type: 'mock_error',
|
||||
},
|
||||
},
|
||||
headers: {},
|
||||
meta: {} as any,
|
||||
statusCode: 404,
|
||||
warnings: [],
|
||||
})
|
||||
);
|
||||
|
||||
const dataMock = createDataPluginMock(esSearchStrategy);
|
||||
const logViewsClientMock = createLogViewsClientMock();
|
||||
logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock());
|
||||
const logViewsMock = createLogViewsServiceStartMock();
|
||||
logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock);
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
|
||||
const logEntrySearchStrategy = logEntrySearchStrategyProvider({
|
||||
data: dataMock,
|
||||
|
@ -209,26 +253,24 @@ describe('LogEntry search strategy', () => {
|
|||
mockDependencies
|
||||
);
|
||||
|
||||
await expect(response.toPromise()).rejects.toThrowError(errors.ResponseError);
|
||||
await expect(lastValueFrom(response)).rejects.toThrowError(KbnSearchError);
|
||||
});
|
||||
|
||||
it('forwards cancellation to the underlying search strategy', async () => {
|
||||
const esSearchStrategyMock = createEsSearchStrategyMock({
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
isRunning: false,
|
||||
rawResponse: {
|
||||
took: 1,
|
||||
_shards: { total: 1, failed: 0, skipped: 0, successful: 1 },
|
||||
timed_out: false,
|
||||
hits: { total: 0, max_score: 0, hits: [] },
|
||||
},
|
||||
const esSearchStrategy = createEsSearchStrategy();
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
const esClient = mockDependencies.esClient.asCurrentUser;
|
||||
|
||||
// set up response to cancellation request
|
||||
esClient.asyncSearch.delete.mockResolvedValueOnce({
|
||||
acknowledged: true,
|
||||
});
|
||||
const dataMock = createDataPluginMock(esSearchStrategyMock);
|
||||
|
||||
const dataMock = createDataPluginMock(esSearchStrategy);
|
||||
const logViewsClientMock = createLogViewsClientMock();
|
||||
logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock());
|
||||
const logViewsMock = createLogViewsServiceStartMock();
|
||||
logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock);
|
||||
const mockDependencies = createSearchStrategyDependenciesMock();
|
||||
|
||||
const logEntrySearchStrategy = logEntrySearchStrategyProvider({
|
||||
data: dataMock,
|
||||
|
@ -240,34 +282,23 @@ describe('LogEntry search strategy', () => {
|
|||
|
||||
await logEntrySearchStrategy.cancel?.(requestId, {}, mockDependencies);
|
||||
|
||||
expect(esSearchStrategyMock.cancel).toHaveBeenCalled();
|
||||
// ensure cancellation request is forwarded
|
||||
expect(esClient.asyncSearch.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'ASYNC_REQUEST_ID',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({
|
||||
search: jest.fn((esSearchRequest: IEsSearchRequest) => {
|
||||
if (typeof esSearchRequest.id === 'string') {
|
||||
if (esSearchRequest.id === esSearchResponse.id) {
|
||||
return of(esSearchResponse);
|
||||
} else {
|
||||
return throwError(
|
||||
new errors.ResponseError({
|
||||
body: {},
|
||||
headers: {},
|
||||
meta: {} as any,
|
||||
statusCode: 404,
|
||||
warnings: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return of(esSearchResponse);
|
||||
}
|
||||
}),
|
||||
cancel: jest.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
const createEsSearchStrategy = () => {
|
||||
const legacyConfig$ = EMPTY;
|
||||
const searchConfig = getMockSearchConfig({});
|
||||
const logger = loggerMock.create();
|
||||
return enhancedEsSearchStrategyProvider(legacyConfig$, searchConfig, logger);
|
||||
};
|
||||
|
||||
const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => ({
|
||||
const createSearchStrategyDependenciesMock = () => ({
|
||||
uiSettingsClient: uiSettingsServiceMock.createClient(),
|
||||
esClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
|
|
|
@ -82,7 +82,16 @@ export const logEntrySearchStrategyProvider = ({
|
|||
|
||||
return concat(recoveredRequest$, initialRequest$).pipe(
|
||||
take(1),
|
||||
concatMap((esRequest) => esSearchStrategy.search(esRequest, options, dependencies)),
|
||||
concatMap((esRequest) =>
|
||||
esSearchStrategy.search(
|
||||
esRequest,
|
||||
{
|
||||
...options,
|
||||
retrieveResults: true, // without it response will not contain progress information
|
||||
},
|
||||
dependencies
|
||||
)
|
||||
),
|
||||
map((esResponse) => ({
|
||||
...esResponse,
|
||||
rawResponse: decodeOrThrow(getLogEntryResponseRT)(esResponse.rawResponse),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue