diff --git a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.test.ts b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.test.ts index 4e85431448e4..04f9db6e72b7 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.test.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.test.ts @@ -32,11 +32,8 @@ import { } from '../constants'; import { CsvESQLGenerator, JobParamsCsvESQL } from './generate_csv_esql'; -const createMockJob = ( - params: Partial = { query: { esql: '' } } -): JobParamsCsvESQL => ({ +const createMockJob = (params: JobParamsCsvESQL): JobParamsCsvESQL => ({ ...params, - query: { esql: '' }, }); const mockTaskInstanceFields = { startedAt: null, retryAt: null }; @@ -107,7 +104,7 @@ describe('CsvESQLGenerator', () => { it('formats an empty search result to CSV content', async () => { const generateCsv = new CsvESQLGenerator( - createMockJob({ columns: ['date', 'ip', 'message'] }), + createMockJob({ query: { esql: '' }, columns: ['date', 'ip', 'message'] }), mockConfig, mockTaskInstanceFields, { @@ -139,7 +136,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -167,7 +164,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -197,7 +194,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -287,7 +284,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsvPromise = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfigWithAutoScrollDuration, taskInstanceFields, { @@ -363,7 +360,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsvPromise = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfigWithAutoScrollDuration, taskInstanceFields, { @@ -414,7 +411,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob({ columns: ['message', 'date', 'something else'] }), + createMockJob({ query: { esql: '' }, columns: ['message', 'date', 'something else'] }), mockConfig, mockTaskInstanceFields, { @@ -485,7 +482,80 @@ describe('CsvESQLGenerator', () => { }, }, locale: 'en', - query: '', + query: query.esql, + }, + }, + { + strategy: 'esql', + transport: { + requestTimeout: '30s', + }, + abortSignal: expect.any(AbortSignal), + } + ); + }); + + it('passes params to the query', async () => { + const query = { + esql: 'FROM custom-metrics-without-timestamp | WHERE event.ingested >= ?_tstart AND event.ingested <= ?_tend', + }; + const filters = [ + { + meta: {}, + query: { + range: { + 'event.ingested': { format: 'strict_date_optional_time', gte: 'now-15m', lte: 'now' }, + }, + }, + }, + ]; + + const generateCsv = new CsvESQLGenerator( + createMockJob({ query, filters }), + mockConfig, + mockTaskInstanceFields, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + new CancellationToken(), + mockLogger, + stream + ); + await generateCsv.generateData(); + + expect(mockDataClient.search).toHaveBeenCalledWith( + { + params: { + filter: { + bool: { + filter: [ + { + range: { + 'event.ingested': { + format: 'strict_date_optional_time', + gte: 'now-15m', + lte: 'now', + }, + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }, + params: expect.arrayContaining([ + expect.objectContaining({ + _tstart: expect.any(String), + }), + expect.objectContaining({ + _tend: expect.any(String), + }), + ]), + locale: 'en', + query: query.esql, }, }, { @@ -509,7 +579,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -539,7 +609,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -578,7 +648,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -607,7 +677,7 @@ describe('CsvESQLGenerator', () => { throw new Error('An unknown error'); }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { @@ -644,7 +714,7 @@ describe('CsvESQLGenerator', () => { }); const generateCsv = new CsvESQLGenerator( - createMockJob(), + createMockJob({ query: { esql: '' } }), mockConfig, mockTaskInstanceFields, { diff --git a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts index 23dc3f8d4fda..e47770374a99 100644 --- a/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts +++ b/src/platform/packages/private/kbn-generate-csv/src/generate_csv_esql.ts @@ -14,7 +14,8 @@ import type { IScopedClusterClient, IUiSettingsClient, Logger } from '@kbn/core/ import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; import { ESQL_SEARCH_STRATEGY, cellHasFormulas, getEsQueryConfig } from '@kbn/data-plugin/common'; import type { IScopedSearchClient } from '@kbn/data-plugin/server'; -import { type Filter, buildEsQuery } from '@kbn/es-query'; +import { type Filter, buildEsQuery, extractTimeRange } from '@kbn/es-query'; +import { getTimeFieldFromESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import type { ESQLSearchParams, ESQLSearchResponse } from '@kbn/es-types'; import { i18n } from '@kbn/i18n'; import { @@ -76,6 +77,17 @@ export class CsvESQLGenerator { const { maxSizeBytes, bom, escapeFormulaValues } = settings; const builder = new MaxSizeStringBuilder(this.stream, byteSizeValueToNumber(maxSizeBytes), bom); + // it will return undefined if there are no _tstart, _tend named params in the query + const timeFieldName = getTimeFieldFromESQLQuery(this.job.query.esql); + const params = []; + if (timeFieldName && this.job.filters) { + const { timeRange } = extractTimeRange(this.job.filters, timeFieldName); + if (timeRange) { + const namedParams = getStartEndParams(this.job.query.esql, timeRange); + params.push(...namedParams); + } + } + const filter = this.job.filters && buildEsQuery( @@ -91,6 +103,7 @@ export class CsvESQLGenerator { filter, // locale can be used for number/date formatting locale: i18n.getLocale(), + ...(params.length ? { params } : {}), // TODO: time_zone support was temporarily removed from ES|QL, // we will need to add it back in once it is supported again. // https://github.com/elastic/elasticsearch/pull/102767 diff --git a/src/platform/packages/private/kbn-generate-csv/tsconfig.json b/src/platform/packages/private/kbn-generate-csv/tsconfig.json index 1153488ab657..3c92da539cd8 100644 --- a/src/platform/packages/private/kbn-generate-csv/tsconfig.json +++ b/src/platform/packages/private/kbn-generate-csv/tsconfig.json @@ -30,5 +30,6 @@ "@kbn/es-types", "@kbn/data-views-plugin", "@kbn/search-types", + "@kbn/esql-utils", ] } diff --git a/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.test.ts b/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.test.ts index e1989518582c..03a54e628ef8 100644 --- a/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.test.ts +++ b/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.test.ts @@ -24,4 +24,15 @@ describe('convertRangeFilterToTimeRange', () => { expect(convertedRangeFilter).toEqual(filterAfterConvertedRangeFilter); }); + + it('should return converted range for relative dates', () => { + const filter: any = { query: { range: { '@timestamp': { gte: 'now-1d', lte: 'now' } } } }; + const filterAfterConvertedRangeFilter = { + from: 'now-1d', + to: 'now', + }; + const convertedRangeFilter = convertRangeFilterToTimeRange(filter); + + expect(convertedRangeFilter).toEqual(filterAfterConvertedRangeFilter); + }); }); diff --git a/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.ts b/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.ts index b5a58ba52885..345bb22f1717 100644 --- a/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.ts +++ b/src/platform/packages/shared/kbn-es-query/src/filters/helpers/convert_range_filter.ts @@ -12,20 +12,26 @@ import { keys } from 'lodash'; import type { RangeFilter } from '../build_filters'; import type { TimeRange } from './types'; +const isRelativeTime = (value: string | number | undefined): boolean => { + return typeof value === 'string' && value.includes('now'); +}; + export function convertRangeFilterToTimeRange(filter: RangeFilter) { const key = keys(filter.query.range)[0]; const values = filter.query.range[key]; + const from = values.gt || values.gte; + const to = values.lt || values.lte; return { - from: moment(values.gt || values.gte), - to: moment(values.lt || values.lte), + from: from && isRelativeTime(from) ? String(from) : moment(from), + to: to && isRelativeTime(to) ? String(to) : moment(to), }; } export function convertRangeFilterToTimeRangeString(filter: RangeFilter): TimeRange { const { from, to } = convertRangeFilterToTimeRange(filter); return { - from: from?.toISOString(), - to: to?.toISOString(), + from: moment.isMoment(from) ? from?.toISOString() : from, + to: moment.isMoment(to) ? to?.toISOString() : to, }; } diff --git a/src/platform/plugins/shared/data/public/query/timefilter/types.ts b/src/platform/plugins/shared/data/public/query/timefilter/types.ts index f07b01abfe2a..a28609cfc589 100644 --- a/src/platform/plugins/shared/data/public/query/timefilter/types.ts +++ b/src/platform/plugins/shared/data/public/query/timefilter/types.ts @@ -22,8 +22,8 @@ export interface TimefilterConfig { export type InputTimeRange = | TimeRange | { - from: Moment; - to: Moment; + from: Moment | string; + to: Moment | string; }; export type { TimeRangeBounds } from '../../../common';