From 4f24b7ea0218f6459f55fdc6138cad522bfbfb91 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Fri, 9 Aug 2019 19:39:08 +0100 Subject: [PATCH] [Logs UI] Show highlighted log entries in the minimap (#40745) (#43031) * Commit to allow opening the PR * Add start of the highlights menu * Add terms input * Add a container for highlights state * Start to hook up highlights state effects, and Bridge with Redux * Move highlighted logs to a separate graphql field * Add highlights argument to unit tests * Add initial draft of a log highlights hook * Finish hooking up hook state ready for GraphQL query * Take startKey and endKey from overall collection of log entries * Add sourceVersion * Fetch the data * Use columns ids instead of column indices * merge highlights into stream WIP * Add highlighting for the message column type * Use the serialized filter query * UX / Performance improvements * Show a highlight is active * Focus input when menu is opened * Icon for highlights menu * Debounce onChange handler * Loading state for highlights menu * Neaten up Provider * Tweaks * Breathing room for icon * Better debounce time for typing * Menu changes * Ensure width of popover stays consistent * Add a button to clear highlights * Add highlighting for field columns * Use phrase query for highlighting While a `phrase_prefix` query feels more natural, it doesn't work on `keyword` type fields. Using just a `phrase` query keeps the highlighting consistent between text and keyword type fields. * Factor out commonly used visibility state * Fix debounce to support changing onChange prop * Move clear highlight button inline * Translate labels * Rely on popover ownfocus prop to focus the input * Rename function to avoid i18n checker false positive * Rough and ready WIP next / previous * Add `countBefore` and `countAfter` highlight args * Fix value used * Add a check for entry * Rearrange icons * 0 falsey blurgh * Organise into separate sub-hooks for clarity * Add comment * Use filter and highlight queries also beyond interval * Refine behaviour of next / previous * Stops wrapping * Ensures that when scrolling next / previous takes off where it was left * Ensures the first highlight is scrolled to when changing highlight term * Add log highlights api tests * Fix a few runtime and compiler errors and linter warnings * Refactor prev/next jumping to avoid index errors * Use nearest index for initial jump * Include element type in translation id * Disable clear button when nothing to clear * Initial summary highlights state * Amend component usage * Comment out load call until query exists * Add graphql api for summary highlights * Add the client-side log summary highlight query * Directly use log view configuration without a bridge * Rename log entry highlight hook to maintain symmetry * Re-enable highlight rendering in the minimap * Keep the time cursor from intercepting clicks * tweak minimap highlighting colors and interaction * Clean up after merge * Remove commented-out code * Change cursor to hint at minimap highlighting interaction * Restrict highlight query filter to relevant fields * Add explanatory error message --- .../plugins/infra/common/graphql/types.ts | 87 ++++++++ .../logging/log_minimap/log_minimap.tsx | 20 +- .../logging/log_minimap/search_marker.tsx | 19 +- .../logging/log_minimap/search_markers.tsx | 9 +- .../logging/log_minimap/time_ruler.tsx | 1 + .../components/logging/log_minimap/types.ts | 6 + ...y.ts => log_entry_highlights.gql_query.ts} | 0 ..._fetching.tsx => log_entry_highlights.tsx} | 47 ++-- .../logs/log_highlights/log_highlights.tsx | 31 ++- .../log_summary_highlights.gql_query.ts | 44 ++++ .../log_highlights/log_summary_highlights.ts | 83 +++++++ .../logs/log_highlights/next_and_previous.tsx | 2 +- .../containers/logs/log_summary/index.ts | 3 +- .../logs/log_summary/log_summary.tsx | 25 +-- .../use_log_summary_buffer_interval.ts | 30 +++ .../logs/log_summary/with_summary.ts | 15 +- .../containers/logs/with_stream_items.ts | 22 -- .../infra/public/graphql/introspection.json | 205 ++++++++++++++++++ .../plugins/infra/public/graphql/types.ts | 87 ++++++++ .../pages/logs/stream/page_logs_content.tsx | 7 +- .../server/graphql/log_entries/resolvers.ts | 48 ++-- .../server/graphql/log_entries/schema.gql.ts | 39 ++++ .../plugins/infra/server/graphql/types.ts | 140 ++++++++++++ .../lib/adapters/framework/adapter_types.ts | 12 +- .../log_entries/kibana_log_entries_adapter.ts | 87 ++++++-- .../log_entries_domain/log_entries_domain.ts | 96 ++++++-- .../apis/infra/log_entry_highlights.ts | 20 +- 27 files changed, 1015 insertions(+), 170 deletions(-) rename x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/{log_highlights.gql_query.ts => log_entry_highlights.gql_query.ts} (100%) rename x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/{data_fetching.tsx => log_entry_highlights.tsx} (71%) create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.gql_query.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_summary/use_log_summary_buffer_interval.ts diff --git a/x-pack/legacy/plugins/infra/common/graphql/types.ts b/x-pack/legacy/plugins/infra/common/graphql/types.ts index 7843a54b93bed..a9c50e97f4348 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/types.ts @@ -36,6 +36,8 @@ export interface InfraSource { logEntryHighlights: InfraLogEntryInterval[]; /** A consecutive span of summary buckets within an interval */ logSummaryBetween: InfraLogSummaryInterval; + /** Spans of summary highlight buckets within an interval */ + logSummaryHighlightsBetween: InfraLogSummaryHighlightInterval[]; logItem: InfraLogItem; /** A snapshot of nodes */ @@ -224,6 +226,30 @@ export interface InfraLogSummaryBucket { /** The number of entries inside the bucket */ entriesCount: number; } +/** A consecutive sequence of log summary highlight buckets */ +export interface InfraLogSummaryHighlightInterval { + /** The millisecond timestamp corresponding to the start of the interval covered by the summary */ + start?: number | null; + /** The millisecond timestamp corresponding to the end of the interval covered by the summary */ + end?: number | null; + /** The query the log entries were filtered by */ + filterQuery?: string | null; + /** The query the log entries were highlighted with */ + highlightQuery?: string | null; + /** A list of the log entries */ + buckets: InfraLogSummaryHighlightBucket[]; +} +/** A log summary highlight bucket */ +export interface InfraLogSummaryHighlightBucket { + /** The start timestamp of the bucket */ + start: number; + /** The end timestamp of the bucket */ + end: number; + /** The number of highlighted entries inside the bucket */ + entriesCount: number; + /** The time key of a representative of the highlighted log entries in this bucket */ + representativeKey: InfraTimeKey; +} export interface InfraLogItem { /** The ID of the document */ @@ -446,6 +472,18 @@ export interface LogSummaryBetweenInfraSourceArgs { /** The query to filter the log entries by */ filterQuery?: string | null; } +export interface LogSummaryHighlightsBetweenInfraSourceArgs { + /** The millisecond timestamp that corresponds to the start of the interval */ + start: number; + /** The millisecond timestamp that corresponds to the end of the interval */ + end: number; + /** The size of each bucket in milliseconds */ + bucketSize: number; + /** The query to filter the log entries by */ + filterQuery?: string | null; + /** The highlighting to apply to the log entries */ + highlightQueries: string[]; +} export interface LogItemInfraSourceArgs { id: string; } @@ -657,6 +695,55 @@ export namespace LogEntryHighlightsQuery { export type Entries = InfraLogEntryHighlightFields.Fragment; } +export namespace LogSummaryHighlightsQuery { + export type Variables = { + sourceId?: string | null; + start: number; + end: number; + bucketSize: number; + highlightQueries: string[]; + filterQuery?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + + id: string; + + logSummaryHighlightsBetween: LogSummaryHighlightsBetween[]; + }; + + export type LogSummaryHighlightsBetween = { + __typename?: 'InfraLogSummaryHighlightInterval'; + + start?: number | null; + + end?: number | null; + + buckets: Buckets[]; + }; + + export type Buckets = { + __typename?: 'InfraLogSummaryHighlightBucket'; + + start: number; + + end: number; + + entriesCount: number; + + representativeKey: RepresentativeKey; + }; + + export type RepresentativeKey = InfraTimeKeyFields.Fragment; +} + export namespace LogSummary { export type Variables = { sourceId?: string | null; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx index 988458f30b3d1..f84fe6b713b89 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx @@ -9,12 +9,11 @@ import * as React from 'react'; import euiStyled from '../../../../../../common/eui_styled_components'; import { LogEntryTime } from '../../../../common/log_entry'; -// import { SearchSummaryBucket } from '../../../../common/log_search_summary'; import { DensityChart } from './density_chart'; import { HighlightedInterval } from './highlighted_interval'; -// import { SearchMarkers } from './search_markers'; +import { SearchMarkers } from './search_markers'; import { TimeRuler } from './time_ruler'; -import { SummaryBucket } from './types'; +import { SummaryBucket, SummaryHighlightBucket } from './types'; interface LogMinimapProps { className?: string; @@ -26,7 +25,7 @@ interface LogMinimapProps { jumpToTarget: (params: LogEntryTime) => any; intervalSize: number; summaryBuckets: SummaryBucket[]; - // searchSummaryBuckets?: SearchSummaryBucket[]; + summaryHighlightBuckets?: SummaryHighlightBucket[]; target: number | null; width: number; } @@ -81,9 +80,9 @@ export class LogMinimap extends React.Component ) : null} - - {/* + - */} + + ); } @@ -145,6 +144,7 @@ const MinimapBorder = euiStyled.line` `; const TimeCursor = euiStyled.line` + pointer-events: none; stroke-width: 1px; stroke: ${props => props.theme.darkMode diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_marker.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_marker.tsx index 8a3292312de18..5b661562a451e 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_marker.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_marker.tsx @@ -9,11 +9,11 @@ import * as React from 'react'; import euiStyled, { keyframes } from '../../../../../../common/eui_styled_components'; import { LogEntryTime } from '../../../../common/log_entry'; -import { SearchSummaryBucket } from '../../../../common/log_search_summary'; import { SearchMarkerTooltip } from './search_marker_tooltip'; +import { SummaryHighlightBucket } from './types'; interface SearchMarkerProps { - bucket: SearchSummaryBucket; + bucket: SummaryHighlightBucket; height: number; width: number; jumpToTarget: (target: LogEntryTime) => void; @@ -31,7 +31,7 @@ export class SearchMarker extends React.PureComponent = evt => { evt.stopPropagation(); - this.props.jumpToTarget(this.props.bucket.representative.fields); + this.props.jumpToTarget(this.props.bucket.representativeKey); }; public handleMouseEnter: React.MouseEventHandler = evt => { @@ -51,7 +51,7 @@ export class SearchMarker extends React.PureComponent 1 ? ( + bucket.entriesCount > 1 ? ( ) : ( <> @@ -73,9 +73,9 @@ export class SearchMarker extends React.PureComponent @@ -107,15 +107,16 @@ const SearchMarkerGroup = euiStyled.g` `; const SearchMarkerBackgroundRect = euiStyled.rect` - fill: ${props => props.theme.eui.euiColorSecondary}; + fill: ${props => props.theme.eui.euiColorAccent}; opacity: 0; transition: opacity ${props => props.theme.eui.euiAnimSpeedNormal} ease-in; + cursor: pointer; ${SearchMarkerGroup}:hover & { - opacity: 0.2; + opacity: 0.3; } `; const SearchMarkerForegroundRect = euiStyled.rect` - fill: ${props => props.theme.eui.euiColorSecondary}; + fill: ${props => props.theme.eui.euiColorAccent}; `; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_markers.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_markers.tsx index 8ad3947cc5a23..5007ce8863e70 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_markers.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/search_markers.tsx @@ -9,11 +9,11 @@ import { scaleTime } from 'd3-scale'; import * as React from 'react'; import { LogEntryTime } from '../../../../common/log_entry'; -import { SearchSummaryBucket } from '../../../../common/log_search_summary'; import { SearchMarker } from './search_marker'; +import { SummaryHighlightBucket } from './types'; interface SearchMarkersProps { - buckets: SearchSummaryBucket[]; + buckets: SummaryHighlightBucket[]; className?: string; end: number; start: number; @@ -38,7 +38,10 @@ export class SearchMarkers extends React.PureComponent { return ( {buckets.map(bucket => ( - + props.theme.eui.euiFontSizeXS}; line-height: ${props => props.theme.eui.euiLineHeight}; fill: ${props => props.theme.eui.textColors.subdued}; + pointer-events: none; `; const TimeRulerGridLine = euiStyled.line` diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/types.ts b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/types.ts index ac3ea48bc4b16..d8197935dafa7 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/types.ts +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_minimap/types.ts @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimeKey } from '../../../../common/time'; + export interface SummaryBucket { start: number; end: number; entriesCount: number; } + +export interface SummaryHighlightBucket extends SummaryBucket { + representativeKey: TimeKey; +} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.gql_query.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.gql_query.ts rename to x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.gql_query.ts diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/data_fetching.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx similarity index 71% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/data_fetching.tsx rename to x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx index 2efe6bbb5bd6a..6ead866fb960a 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/data_fetching.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx @@ -11,11 +11,11 @@ import { LogEntryHighlightsQuery } from '../../../graphql/types'; import { DependencyError, useApolloClient } from '../../../utils/apollo_context'; import { LogEntryHighlightsMap } from '../../../utils/log_entry'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { logEntryHighlightsQuery } from './log_highlights.gql_query'; +import { logEntryHighlightsQuery } from './log_entry_highlights.gql_query'; export type LogEntryHighlights = LogEntryHighlightsQuery.Query['source']['logEntryHighlights']; -export const useHighlightsFetcher = ( +export const useLogEntryHighlights = ( sourceId: string, sourceVersion: string | undefined, startKey: TimeKey | null, @@ -24,9 +24,7 @@ export const useHighlightsFetcher = ( highlightTerms: string[] ) => { const apolloClient = useApolloClient(); - const [logEntryHighlights, setLogEntryHighlights] = useState( - undefined - ); + const [logEntryHighlights, setLogEntryHighlights] = useState([]); const [loadLogEntryHighlightsRequest, loadLogEntryHighlights] = useTrackedPromise( { cancelPreviousOn: 'resolution', @@ -35,7 +33,7 @@ export const useHighlightsFetcher = ( throw new DependencyError('Failed to load source: No apollo client available.'); } if (!startKey || !endKey || !highlightTerms.length) { - throw new Error(); + throw new Error('Skipping request: Insufficient parameters'); } return await apolloClient.query< @@ -51,9 +49,7 @@ export const useHighlightsFetcher = ( filterQuery, highlights: [ { - query: JSON.stringify({ - multi_match: { query: highlightTerms[0], type: 'phrase', lenient: true }, - }), + query: highlightTerms[0], countBefore: 1, countAfter: 1, }, @@ -69,7 +65,7 @@ export const useHighlightsFetcher = ( ); useEffect(() => { - setLogEntryHighlights(undefined); + setLogEntryHighlights([]); }, [highlightTerms]); useEffect(() => { @@ -80,29 +76,24 @@ export const useHighlightsFetcher = ( ) { loadLogEntryHighlights(); } else { - setLogEntryHighlights(undefined); + setLogEntryHighlights([]); } }, [highlightTerms, startKey, endKey, filterQuery, sourceVersion]); const logEntryHighlightsById = useMemo( () => - logEntryHighlights - ? logEntryHighlights.reduce( - (accumulatedLogEntryHighlightsById, { entries }) => { - return entries.reduce( - (singleHighlightLogEntriesById, entry) => { - const highlightsForId = singleHighlightLogEntriesById[entry.gid] || []; - return { - ...singleHighlightLogEntriesById, - [entry.gid]: [...highlightsForId, entry], - }; - }, - accumulatedLogEntryHighlightsById - ); - }, - {} - ) - : {}, + logEntryHighlights.reduce( + (accumulatedLogEntryHighlightsById, { entries }) => { + return entries.reduce((singleHighlightLogEntriesById, entry) => { + const highlightsForId = singleHighlightLogEntriesById[entry.gid] || []; + return { + ...singleHighlightLogEntriesById, + [entry.gid]: [...highlightsForId, entry], + }; + }, accumulatedLogEntryHighlightsById); + }, + {} + ), [logEntryHighlights] ); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx index 5c7bdad139b04..537dd5c0070f5 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx @@ -5,11 +5,14 @@ */ import createContainer from 'constate-latest'; -import { useState } from 'react'; +import { useState, useContext } from 'react'; -import { useHighlightsFetcher } from './data_fetching'; +import { useLogEntryHighlights } from './log_entry_highlights'; +import { useLogSummaryHighlights } from './log_summary_highlights'; import { useNextAndPrevious } from './next_and_previous'; import { useReduxBridgeSetters } from './redux_bridge_setters'; +import { useLogSummaryBufferInterval } from '../log_summary'; +import { LogViewConfiguration } from '../log_view_configuration'; export const useLogHighlightsState = ({ sourceId, @@ -33,11 +36,31 @@ export const useLogHighlightsState = ({ setJumpToTarget, } = useReduxBridgeSetters(); + const { intervalSize: summaryIntervalSize } = useContext(LogViewConfiguration.Context); + const { + start: summaryStart, + end: summaryEnd, + bucketSize: summaryBucketSize, + } = useLogSummaryBufferInterval( + visibleMidpoint ? visibleMidpoint.time : null, + summaryIntervalSize + ); + const { logEntryHighlights, logEntryHighlightsById, loadLogEntryHighlightsRequest, - } = useHighlightsFetcher(sourceId, sourceVersion, startKey, endKey, filterQuery, highlightTerms); + } = useLogEntryHighlights(sourceId, sourceVersion, startKey, endKey, filterQuery, highlightTerms); + + const { logSummaryHighlights, loadLogSummaryHighlightsRequest } = useLogSummaryHighlights( + sourceId, + sourceVersion, + summaryStart, + summaryEnd, + summaryBucketSize, + filterQuery, + highlightTerms + ); const { currentHighlightKey, @@ -60,7 +83,9 @@ export const useLogHighlightsState = ({ setFilterQuery, logEntryHighlights, logEntryHighlightsById, + logSummaryHighlights, loadLogEntryHighlightsRequest, + loadLogSummaryHighlightsRequest, setVisibleMidpoint, currentHighlightKey, hasPreviousHighlight, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.gql_query.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.gql_query.ts new file mode 100644 index 0000000000000..4d2c4075b50e7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.gql_query.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +import { sharedFragments } from '../../../../common/graphql/shared'; + +export const logSummaryHighlightsQuery = gql` + query LogSummaryHighlightsQuery( + $sourceId: ID = "default" + $start: Float! + $end: Float! + $bucketSize: Float! + $highlightQueries: [String!]! + $filterQuery: String + ) { + source(id: $sourceId) { + id + logSummaryHighlightsBetween( + start: $start + end: $end + bucketSize: $bucketSize + highlightQueries: $highlightQueries + filterQuery: $filterQuery + ) { + start + end + buckets { + start + end + entriesCount + representativeKey { + ...InfraTimeKeyFields + } + } + } + } + } + + ${sharedFragments.InfraTimeKey} +`; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts new file mode 100644 index 0000000000000..34c66afda010e --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; + +import { LogSummaryHighlightsQuery } from '../../../graphql/types'; +import { DependencyError, useApolloClient } from '../../../utils/apollo_context'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { logSummaryHighlightsQuery } from './log_summary_highlights.gql_query'; + +export type LogSummaryHighlights = LogSummaryHighlightsQuery.Query['source']['logSummaryHighlightsBetween']; + +export const useLogSummaryHighlights = ( + sourceId: string, + sourceVersion: string | undefined, + start: number | null, + end: number | null, + bucketSize: number, + filterQuery: string | null, + highlightTerms: string[] +) => { + const apolloClient = useApolloClient(); + const [logSummaryHighlights, setLogSummaryHighlights] = useState([]); + + const [loadLogSummaryHighlightsRequest, loadLogSummaryHighlights] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + if (!apolloClient) { + throw new DependencyError('Failed to load source: No apollo client available.'); + } + if (!start || !end || !highlightTerms.length) { + throw new Error('Skipping request: Insufficient parameters'); + } + + return await apolloClient.query< + LogSummaryHighlightsQuery.Query, + LogSummaryHighlightsQuery.Variables + >({ + fetchPolicy: 'no-cache', + query: logSummaryHighlightsQuery, + variables: { + sourceId, + start, + end, + bucketSize, + highlightQueries: [highlightTerms[0]], + filterQuery, + }, + }); + }, + onResolve: response => { + setLogSummaryHighlights(response.data.source.logSummaryHighlightsBetween); + }, + }, + [apolloClient, sourceId, start, end, bucketSize, filterQuery, highlightTerms] + ); + + const debouncedLoadSummaryHighlights = useMemo(() => debounce(loadLogSummaryHighlights, 275), [ + loadLogSummaryHighlights, + ]); + + useEffect(() => { + setLogSummaryHighlights([]); + }, [highlightTerms]); + + useEffect(() => { + if (highlightTerms.filter(highlightTerm => highlightTerm.length > 0).length && start && end) { + debouncedLoadSummaryHighlights(); + } else { + setLogSummaryHighlights([]); + } + }, [highlightTerms, start, end, bucketSize, filterQuery, sourceVersion]); + + return { + logSummaryHighlights, + loadLogSummaryHighlightsRequest, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx index 6cf602a4a701e..95ead50119eb4 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx @@ -13,7 +13,7 @@ import { getLogEntryIndexBeforeTime, getUniqueLogEntryKey, } from '../../../utils/log_entry'; -import { LogEntryHighlights } from './data_fetching'; +import { LogEntryHighlights } from './log_entry_highlights'; export const useNextAndPrevious = ({ highlightTerms, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/index.ts index c62fd0379de58..20c4267000a25 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './with_summary'; export * from './log_summary'; +export * from './use_log_summary_buffer_interval'; +export * from './with_summary'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/log_summary.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/log_summary.tsx index b5c765c2c2a27..a188e698991a0 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/log_summary.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/log_summary.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { LogSummary as LogSummaryQuery } from '../../../graphql/types'; import { useApolloClient } from '../../../utils/apollo_context'; import { useCancellableEffect } from '../../../utils/cancellable_effect'; import { logSummaryQuery } from './log_summary.gql_query'; +import { useLogSummaryBufferInterval } from './use_log_summary_buffer_interval'; -const LOAD_BUCKETS_PER_PAGE = 100; export type LogSummaryBetween = LogSummaryQuery.Query['source']['logSummaryBetween']; export type LogSummaryBuckets = LogSummaryBetween['buckets']; @@ -24,17 +24,10 @@ export const useLogSummary = ( const [logSummaryBetween, setLogSummaryBetween] = useState({ buckets: [] }); const apolloClient = useApolloClient(); - const [bufferStart, bufferEnd] = useMemo(() => { - if (midpointTime === null || intervalSize <= 0) { - return [null, null]; - } - - const halfIntervalSize = intervalSize / 2; - return [ - (Math.floor((midpointTime - halfIntervalSize) / intervalSize) - 0.5) * intervalSize, - (Math.ceil((midpointTime + halfIntervalSize) / intervalSize) + 0.5) * intervalSize, - ]; - }, [midpointTime, intervalSize]); + const { start: bufferStart, end: bufferEnd, bucketSize } = useLogSummaryBufferInterval( + midpointTime, + intervalSize + ); useCancellableEffect( getIsCancelled => { @@ -51,7 +44,7 @@ export const useLogSummary = ( sourceId, start: bufferStart, end: bufferEnd, - bucketSize: intervalSize / LOAD_BUCKETS_PER_PAGE, + bucketSize, }, }) .then(response => { @@ -60,10 +53,12 @@ export const useLogSummary = ( } }); }, - [apolloClient, sourceId, filterQuery, bufferStart, bufferEnd, intervalSize] + [apolloClient, sourceId, filterQuery, bufferStart, bufferEnd, bucketSize] ); return { buckets: logSummaryBetween.buckets, + start: bufferStart, + end: bufferEnd, }; }; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/use_log_summary_buffer_interval.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/use_log_summary_buffer_interval.ts new file mode 100644 index 0000000000000..27af76b70f47a --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/use_log_summary_buffer_interval.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +const LOAD_BUCKETS_PER_PAGE = 100; +const UNKNOWN_BUFFER_INTERVAL = { + start: null, + end: null, + bucketSize: 0, +}; + +export const useLogSummaryBufferInterval = (midpointTime: number | null, intervalSize: number) => { + return useMemo(() => { + if (midpointTime === null || intervalSize <= 0) { + return UNKNOWN_BUFFER_INTERVAL; + } + + const halfIntervalSize = intervalSize / 2; + + return { + start: (Math.floor((midpointTime - halfIntervalSize) / intervalSize) - 0.5) * intervalSize, + end: (Math.ceil((midpointTime + halfIntervalSize) / intervalSize) + 0.5) * intervalSize, + bucketSize: intervalSize / LOAD_BUCKETS_PER_PAGE, + }; + }, [midpointTime, intervalSize]); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 5df603f0d92b6..61c603130df52 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -22,15 +22,24 @@ export const WithSummary = connect((state: State) => ({ filterQuery, visibleMidpointTime, }: { - children: RendererFunction<{ buckets: LogSummaryBuckets }>; + children: RendererFunction<{ + buckets: LogSummaryBuckets; + start: number | null; + end: number | null; + }>; filterQuery: string | null; visibleMidpointTime: number | null; }) => { const { intervalSize } = useContext(LogViewConfiguration.Context); const { sourceId } = useContext(Source.Context); - const { buckets } = useLogSummary(sourceId, visibleMidpointTime, intervalSize, filterQuery); + const { buckets, start, end } = useLogSummary( + sourceId, + visibleMidpointTime, + intervalSize, + filterQuery + ); - return children({ buckets }); + return children({ buckets, start, end }); } ); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts index 51fee075b6443..6a79d7b8e4ac9 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts @@ -27,7 +27,6 @@ export const withStreamItems = connect( entries: logEntriesSelectors.selectEntries(state), entriesStart: logEntriesSelectors.selectEntriesStart(state), entriesEnd: logEntriesSelectors.selectEntriesEnd(state), - // items: selectItems(state), }), bindPlainActionCreators({ loadNewerEntries: logEntriesActions.loadNewerEntries, @@ -77,27 +76,6 @@ export const WithStreamItems = withStreamItems( } ); -// export const WithStreamItemsOld = asChildFunctionRenderer(withStreamItems, { -// onInitialize: props => { -// if (!props.isReloading && !props.isLoadingMore) { -// props.reloadEntries(); -// } -// }, -// }); - -// const selectItems = createSelector( -// logEntriesSelectors.selectEntries, -// logEntriesSelectors.selectIsReloadingEntries, -// logPositionSelectors.selectIsAutoReloading, -// // searchResultsSelectors.selectSearchResultsById, -// (logEntries, isReloading, isAutoReloading /* , searchResults */) => -// isReloading && !isAutoReloading -// ? [] -// : logEntries.map(logEntry => -// createLogEntryStreamItem(logEntry /* , searchResults[logEntry.gid] || null */) -// ) -// ); - const createLogEntryStreamItem = ( logEntry: LogEntry, highlights: LogEntryHighlight[] diff --git a/x-pack/legacy/plugins/infra/public/graphql/introspection.json b/x-pack/legacy/plugins/infra/public/graphql/introspection.json index 055fac61cb93d..22e8ca519e513 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/infra/public/graphql/introspection.json @@ -335,6 +335,85 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "logSummaryHighlightsBetween", + "description": "Spans of summary highlight buckets within an interval", + "args": [ + { + "name": "start", + "description": "The millisecond timestamp that corresponds to the start of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "end", + "description": "The millisecond timestamp that corresponds to the end of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "bucketSize", + "description": "The size of each bucket in milliseconds", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "The query to filter the log entries by", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "highlightQueries", + "description": "The highlighting to apply to the log entries", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InfraLogSummaryHighlightInterval", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "logItem", "description": "", @@ -1676,6 +1755,132 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "InfraLogSummaryHighlightInterval", + "description": "A consecutive sequence of log summary highlight buckets", + "fields": [ + { + "name": "start", + "description": "The millisecond timestamp corresponding to the start of the interval covered by the summary", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "end", + "description": "The millisecond timestamp corresponding to the end of the interval covered by the summary", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filterQuery", + "description": "The query the log entries were filtered by", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "highlightQuery", + "description": "The query the log entries were highlighted with", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "buckets", + "description": "A list of the log entries", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InfraLogSummaryHighlightBucket", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogSummaryHighlightBucket", + "description": "A log summary highlight bucket", + "fields": [ + { + "name": "start", + "description": "The start timestamp of the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "end", + "description": "The end timestamp of the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entriesCount", + "description": "The number of highlighted entries inside the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "representativeKey", + "description": "The time key of a representative of the highlighted log entries in this bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraTimeKey", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "InfraLogItem", diff --git a/x-pack/legacy/plugins/infra/public/graphql/types.ts b/x-pack/legacy/plugins/infra/public/graphql/types.ts index 7843a54b93bed..a9c50e97f4348 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -36,6 +36,8 @@ export interface InfraSource { logEntryHighlights: InfraLogEntryInterval[]; /** A consecutive span of summary buckets within an interval */ logSummaryBetween: InfraLogSummaryInterval; + /** Spans of summary highlight buckets within an interval */ + logSummaryHighlightsBetween: InfraLogSummaryHighlightInterval[]; logItem: InfraLogItem; /** A snapshot of nodes */ @@ -224,6 +226,30 @@ export interface InfraLogSummaryBucket { /** The number of entries inside the bucket */ entriesCount: number; } +/** A consecutive sequence of log summary highlight buckets */ +export interface InfraLogSummaryHighlightInterval { + /** The millisecond timestamp corresponding to the start of the interval covered by the summary */ + start?: number | null; + /** The millisecond timestamp corresponding to the end of the interval covered by the summary */ + end?: number | null; + /** The query the log entries were filtered by */ + filterQuery?: string | null; + /** The query the log entries were highlighted with */ + highlightQuery?: string | null; + /** A list of the log entries */ + buckets: InfraLogSummaryHighlightBucket[]; +} +/** A log summary highlight bucket */ +export interface InfraLogSummaryHighlightBucket { + /** The start timestamp of the bucket */ + start: number; + /** The end timestamp of the bucket */ + end: number; + /** The number of highlighted entries inside the bucket */ + entriesCount: number; + /** The time key of a representative of the highlighted log entries in this bucket */ + representativeKey: InfraTimeKey; +} export interface InfraLogItem { /** The ID of the document */ @@ -446,6 +472,18 @@ export interface LogSummaryBetweenInfraSourceArgs { /** The query to filter the log entries by */ filterQuery?: string | null; } +export interface LogSummaryHighlightsBetweenInfraSourceArgs { + /** The millisecond timestamp that corresponds to the start of the interval */ + start: number; + /** The millisecond timestamp that corresponds to the end of the interval */ + end: number; + /** The size of each bucket in milliseconds */ + bucketSize: number; + /** The query to filter the log entries by */ + filterQuery?: string | null; + /** The highlighting to apply to the log entries */ + highlightQueries: string[]; +} export interface LogItemInfraSourceArgs { id: string; } @@ -657,6 +695,55 @@ export namespace LogEntryHighlightsQuery { export type Entries = InfraLogEntryHighlightFields.Fragment; } +export namespace LogSummaryHighlightsQuery { + export type Variables = { + sourceId?: string | null; + start: number; + end: number; + bucketSize: number; + highlightQueries: string[]; + filterQuery?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + + id: string; + + logSummaryHighlightsBetween: LogSummaryHighlightsBetween[]; + }; + + export type LogSummaryHighlightsBetween = { + __typename?: 'InfraLogSummaryHighlightInterval'; + + start?: number | null; + + end?: number | null; + + buckets: Buckets[]; + }; + + export type Buckets = { + __typename?: 'InfraLogSummaryHighlightBucket'; + + start: number; + + end: number; + + entriesCount: number; + + representativeKey: RepresentativeKey; + }; + + export type RepresentativeKey = InfraTimeKeyFields.Fragment; +} + export namespace LogSummary { export type Variables = { sourceId?: string | null; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 2ead89f94f812..00c52eecdbe34 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -28,7 +28,7 @@ import { ReduxSourceIdBridge, WithStreamItems } from '../../../containers/logs/w import { Source } from '../../../containers/source'; import { LogsToolbar } from './page_toolbar'; -import { LogHighlightsBridge } from '../../../containers/logs/log_highlights'; +import { LogHighlightsBridge, LogHighlightsState } from '../../../containers/logs/log_highlights'; export const LogsPageLogsContent: React.FunctionComponent = () => { const { createDerivedIndexPattern, source, sourceId, version } = useContext(Source.Context); @@ -42,7 +42,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { flyoutItem, isLoading, } = useContext(LogFlyoutState.Context); - + const { logSummaryHighlights } = useContext(LogHighlightsState.Context); const derivedIndexPattern = createDerivedIndexPattern('logs'); return ( @@ -129,6 +129,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { intervalSize={intervalSize} jumpToTarget={jumpToTargetPosition} summaryBuckets={buckets} + summaryHighlightBuckets={ + logSummaryHighlights.length > 0 ? logSummaryHighlights[0].buckets : [] + } target={visibleMidpointTime} /> )} diff --git a/x-pack/legacy/plugins/infra/server/graphql/log_entries/resolvers.ts b/x-pack/legacy/plugins/infra/server/graphql/log_entries/resolvers.ts index 31fd19a6e125d..f63d09807336a 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/log_entries/resolvers.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/log_entries/resolvers.ts @@ -6,11 +6,9 @@ import { failure } from 'io-ts/lib/PathReporter'; -import { JsonObject } from '../../../common/typed_json'; import { InfraLogEntryColumn, InfraLogEntryFieldColumn, - InfraLogEntryHighlightInput, InfraLogEntryMessageColumn, InfraLogEntryTimestampColumn, InfraLogMessageConstantSegment, @@ -45,6 +43,11 @@ export type InfraSourceLogSummaryBetweenResolver = ChildResolverOf< QuerySourceResolver >; +export type InfraSourceLogSummaryHighlightsBetweenResolver = ChildResolverOf< + InfraResolverOf, + QuerySourceResolver +>; + export type InfraSourceLogItem = ChildResolverOf< InfraResolverOf, QuerySourceResolver @@ -58,6 +61,7 @@ export const createLogEntriesResolvers = (libs: { logEntriesBetween: InfraSourceLogEntriesBetweenResolver; logEntryHighlights: InfraSourceLogEntryHighlightsResolver; logSummaryBetween: InfraSourceLogSummaryBetweenResolver; + logSummaryHighlightsBetween: InfraSourceLogSummaryHighlightsBetweenResolver; logItem: InfraSourceLogItem; }; InfraLogEntryColumn: { @@ -130,7 +134,7 @@ export const createLogEntriesResolvers = (libs: { source.id, args.startKey, args.endKey, - parseHighlightInputs(args.highlights), + args.highlights.filter(highlightInput => !!highlightInput.query), parseFilterQuery(args.filterQuery) ); @@ -160,6 +164,23 @@ export const createLogEntriesResolvers = (libs: { buckets, }; }, + async logSummaryHighlightsBetween(source, args, { req }) { + const summaryHighlightSets = await libs.logEntries.getLogSummaryHighlightBucketsBetween( + req, + source.id, + args.start, + args.end, + args.bucketSize, + args.highlightQueries.filter(highlightQuery => !!highlightQuery), + parseFilterQuery(args.filterQuery) + ); + + return summaryHighlightSets.map(buckets => ({ + start: buckets.length > 0 ? buckets[0].start : null, + end: buckets.length > 0 ? buckets[buckets.length - 1].end : null, + buckets, + })); + }, async logItem(source, args, { req }) { const sourceConfiguration = SourceConfigurationRuntimeType.decode( source.configuration @@ -217,24 +238,3 @@ const isConstantSegment = ( const isFieldSegment = (segment: InfraLogMessageSegment): segment is InfraLogMessageFieldSegment => 'field' in segment && 'value' in segment && 'highlights' in segment; - -const parseHighlightInputs = (highlightInputs: InfraLogEntryHighlightInput[]) => - highlightInputs - ? highlightInputs.reduce>( - (parsedHighlightInputs, highlightInput) => { - const parsedQuery = parseFilterQuery(highlightInput.query); - if (parsedQuery) { - return [ - ...parsedHighlightInputs, - { - ...highlightInput, - query: parsedQuery, - }, - ]; - } else { - return parsedHighlightInputs; - } - }, - [] - ) - : []; diff --git a/x-pack/legacy/plugins/infra/server/graphql/log_entries/schema.gql.ts b/x-pack/legacy/plugins/infra/server/graphql/log_entries/schema.gql.ts index 0e5a6203519b3..0ef896e8edfa4 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/log_entries/schema.gql.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/log_entries/schema.gql.ts @@ -92,6 +92,18 @@ export const logEntriesSchema = gql` entriesCount: Int! } + "A log summary highlight bucket" + type InfraLogSummaryHighlightBucket { + "The start timestamp of the bucket" + start: Float! + "The end timestamp of the bucket" + end: Float! + "The number of highlighted entries inside the bucket" + entriesCount: Int! + "The time key of a representative of the highlighted log entries in this bucket" + representativeKey: InfraTimeKey! + } + "A consecutive sequence of log entries" type InfraLogEntryInterval { "The key corresponding to the start of the interval covered by the entries" @@ -122,6 +134,20 @@ export const logEntriesSchema = gql` buckets: [InfraLogSummaryBucket!]! } + "A consecutive sequence of log summary highlight buckets" + type InfraLogSummaryHighlightInterval { + "The millisecond timestamp corresponding to the start of the interval covered by the summary" + start: Float + "The millisecond timestamp corresponding to the end of the interval covered by the summary" + end: Float + "The query the log entries were filtered by" + filterQuery: String + "The query the log entries were highlighted with" + highlightQuery: String + "A list of the log entries" + buckets: [InfraLogSummaryHighlightBucket!]! + } + type InfraLogItemField { "The flattened field name" field: String! @@ -183,6 +209,19 @@ export const logEntriesSchema = gql` "The query to filter the log entries by" filterQuery: String ): InfraLogSummaryInterval! + "Spans of summary highlight buckets within an interval" + logSummaryHighlightsBetween( + "The millisecond timestamp that corresponds to the start of the interval" + start: Float! + "The millisecond timestamp that corresponds to the end of the interval" + end: Float! + "The size of each bucket in milliseconds" + bucketSize: Float! + "The query to filter the log entries by" + filterQuery: String + "The highlighting to apply to the log entries" + highlightQueries: [String!]! + ): [InfraLogSummaryHighlightInterval!]! logItem(id: ID!): InfraLogItem! } `; diff --git a/x-pack/legacy/plugins/infra/server/graphql/types.ts b/x-pack/legacy/plugins/infra/server/graphql/types.ts index e223ac7b334a2..0853d1f64472f 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/types.ts @@ -64,6 +64,8 @@ export interface InfraSource { logEntryHighlights: InfraLogEntryInterval[]; /** A consecutive span of summary buckets within an interval */ logSummaryBetween: InfraLogSummaryInterval; + /** Spans of summary highlight buckets within an interval */ + logSummaryHighlightsBetween: InfraLogSummaryHighlightInterval[]; logItem: InfraLogItem; /** A snapshot of nodes */ @@ -252,6 +254,30 @@ export interface InfraLogSummaryBucket { /** The number of entries inside the bucket */ entriesCount: number; } +/** A consecutive sequence of log summary highlight buckets */ +export interface InfraLogSummaryHighlightInterval { + /** The millisecond timestamp corresponding to the start of the interval covered by the summary */ + start?: number | null; + /** The millisecond timestamp corresponding to the end of the interval covered by the summary */ + end?: number | null; + /** The query the log entries were filtered by */ + filterQuery?: string | null; + /** The query the log entries were highlighted with */ + highlightQuery?: string | null; + /** A list of the log entries */ + buckets: InfraLogSummaryHighlightBucket[]; +} +/** A log summary highlight bucket */ +export interface InfraLogSummaryHighlightBucket { + /** The start timestamp of the bucket */ + start: number; + /** The end timestamp of the bucket */ + end: number; + /** The number of highlighted entries inside the bucket */ + entriesCount: number; + /** The time key of a representative of the highlighted log entries in this bucket */ + representativeKey: InfraTimeKey; +} export interface InfraLogItem { /** The ID of the document */ @@ -474,6 +500,18 @@ export interface LogSummaryBetweenInfraSourceArgs { /** The query to filter the log entries by */ filterQuery?: string | null; } +export interface LogSummaryHighlightsBetweenInfraSourceArgs { + /** The millisecond timestamp that corresponds to the start of the interval */ + start: number; + /** The millisecond timestamp that corresponds to the end of the interval */ + end: number; + /** The size of each bucket in milliseconds */ + bucketSize: number; + /** The query to filter the log entries by */ + filterQuery?: string | null; + /** The highlighting to apply to the log entries */ + highlightQueries: string[]; +} export interface LogItemInfraSourceArgs { id: string; } @@ -650,6 +688,12 @@ export namespace InfraSourceResolvers { logEntryHighlights?: LogEntryHighlightsResolver; /** A consecutive span of summary buckets within an interval */ logSummaryBetween?: LogSummaryBetweenResolver; + /** Spans of summary highlight buckets within an interval */ + logSummaryHighlightsBetween?: LogSummaryHighlightsBetweenResolver< + InfraLogSummaryHighlightInterval[], + TypeParent, + Context + >; logItem?: LogItemResolver; /** A snapshot of nodes */ @@ -750,6 +794,24 @@ export namespace InfraSourceResolvers { filterQuery?: string | null; } + export type LogSummaryHighlightsBetweenResolver< + R = InfraLogSummaryHighlightInterval[], + Parent = InfraSource, + Context = InfraContext + > = Resolver; + export interface LogSummaryHighlightsBetweenArgs { + /** The millisecond timestamp that corresponds to the start of the interval */ + start: number; + /** The millisecond timestamp that corresponds to the end of the interval */ + end: number; + /** The size of each bucket in milliseconds */ + bucketSize: number; + /** The query to filter the log entries by */ + filterQuery?: string | null; + /** The highlighting to apply to the log entries */ + highlightQueries: string[]; + } + export type LogItemResolver< R = InfraLogItem, Parent = InfraSource, @@ -1356,6 +1418,84 @@ export namespace InfraLogSummaryBucketResolvers { Context = InfraContext > = Resolver; } +/** A consecutive sequence of log summary highlight buckets */ +export namespace InfraLogSummaryHighlightIntervalResolvers { + export interface Resolvers< + Context = InfraContext, + TypeParent = InfraLogSummaryHighlightInterval + > { + /** The millisecond timestamp corresponding to the start of the interval covered by the summary */ + start?: StartResolver; + /** The millisecond timestamp corresponding to the end of the interval covered by the summary */ + end?: EndResolver; + /** The query the log entries were filtered by */ + filterQuery?: FilterQueryResolver; + /** The query the log entries were highlighted with */ + highlightQuery?: HighlightQueryResolver; + /** A list of the log entries */ + buckets?: BucketsResolver; + } + + export type StartResolver< + R = number | null, + Parent = InfraLogSummaryHighlightInterval, + Context = InfraContext + > = Resolver; + export type EndResolver< + R = number | null, + Parent = InfraLogSummaryHighlightInterval, + Context = InfraContext + > = Resolver; + export type FilterQueryResolver< + R = string | null, + Parent = InfraLogSummaryHighlightInterval, + Context = InfraContext + > = Resolver; + export type HighlightQueryResolver< + R = string | null, + Parent = InfraLogSummaryHighlightInterval, + Context = InfraContext + > = Resolver; + export type BucketsResolver< + R = InfraLogSummaryHighlightBucket[], + Parent = InfraLogSummaryHighlightInterval, + Context = InfraContext + > = Resolver; +} +/** A log summary highlight bucket */ +export namespace InfraLogSummaryHighlightBucketResolvers { + export interface Resolvers { + /** The start timestamp of the bucket */ + start?: StartResolver; + /** The end timestamp of the bucket */ + end?: EndResolver; + /** The number of highlighted entries inside the bucket */ + entriesCount?: EntriesCountResolver; + /** The time key of a representative of the highlighted log entries in this bucket */ + representativeKey?: RepresentativeKeyResolver; + } + + export type StartResolver< + R = number, + Parent = InfraLogSummaryHighlightBucket, + Context = InfraContext + > = Resolver; + export type EndResolver< + R = number, + Parent = InfraLogSummaryHighlightBucket, + Context = InfraContext + > = Resolver; + export type EntriesCountResolver< + R = number, + Parent = InfraLogSummaryHighlightBucket, + Context = InfraContext + > = Resolver; + export type RepresentativeKeyResolver< + R = InfraTimeKey, + Parent = InfraLogSummaryHighlightBucket, + Context = InfraContext + > = Resolver; +} export namespace InfraLogItemResolvers { export interface Resolvers { diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 999d95c1b52c8..9e60abf58f4a1 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -152,15 +152,21 @@ export interface SortedSearchHit extends SearchHit { }; } -export interface InfraDateRangeAggregationBucket { +export type InfraDateRangeAggregationBucket = { from?: number; to?: number; doc_count: number; key: string; +} & NestedAggregation; + +export interface InfraDateRangeAggregationResponse { + buckets: Array>; } -export interface InfraDateRangeAggregationResponse { - buckets: InfraDateRangeAggregationBucket[]; +export interface InfraTopHitsAggregationResponse { + hits: { + hits: []; + }; } export interface InfraMetadataAggregationBucket { diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 17f2d850f1217..7f3108836aad6 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + import { timeMilliseconds } from 'd3-time'; +import * as runtimeTypes from 'io-ts'; import first from 'lodash/fp/first'; import get from 'lodash/fp/get'; import has from 'lodash/fp/has'; @@ -16,14 +19,10 @@ import { LogEntriesAdapter, LogEntryDocument, LogEntryQuery, + LogSummaryBucket, } from '../../domains/log_entries_domain'; import { InfraSourceConfiguration } from '../../sources'; -import { - InfraDateRangeAggregationBucket, - InfraDateRangeAggregationResponse, - InfraFrameworkRequest, - SortedSearchHit, -} from '../framework'; +import { InfraFrameworkRequest, SortedSearchHit } from '../framework'; import { InfraBackendFrameworkAdapter } from '../framework'; const DAY_MILLIS = 24 * 60 * 60 * 1000; @@ -111,7 +110,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { end: number, bucketSize: number, filterQuery?: LogEntryQuery - ): Promise { + ): Promise { const bucketIntervalStarts = timeMilliseconds(new Date(start), new Date(end), bucketSize); const query = { @@ -129,6 +128,18 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { to: bucketIntervalStart.getTime() + bucketSize, })), }, + aggregations: { + top_hits_by_key: { + top_hits: { + size: 1, + sort: [ + { [sourceConfiguration.fields.timestamp]: 'asc' }, + { [sourceConfiguration.fields.tiebreaker]: 'asc' }, + ], + _source: false, + }, + }, + }, }, }, query: { @@ -148,17 +159,19 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }, }, size: 0, + track_total_hits: false, }, }; - const response = await this.framework.callWithRequest< - any, - { count_by_date?: InfraDateRangeAggregationResponse } - >(request, 'search', query); + const response = await this.framework.callWithRequest(request, 'search', query); - return response.aggregations && response.aggregations.count_by_date - ? response.aggregations.count_by_date.buckets - : []; + return LogSummaryResponseRuntimeType.decode(response) + .map(logSummaryResponse => + logSummaryResponse.aggregations.count_by_date.buckets.map( + convertDateRangeBucketToSummaryBucket + ) + ) + .getOrElse([]); } public async getLogItem( @@ -322,5 +335,51 @@ const convertHitToLogEntryDocument = (fields: string[]) => ( }, }); +const convertDateRangeBucketToSummaryBucket = ( + bucket: LogSummaryDateRangeBucket +): LogSummaryBucket => ({ + entriesCount: bucket.doc_count, + start: bucket.from || 0, + end: bucket.to || 0, + topEntryKeys: bucket.top_hits_by_key.hits.hits.map(hit => ({ + tiebreaker: hit.sort[1], + time: hit.sort[0], + })), +}); + const createQueryFilterClauses = (filterQuery: LogEntryQuery | undefined) => filterQuery ? [filterQuery] : []; + +const LogSummaryDateRangeBucketRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + doc_count: runtimeTypes.number, + key: runtimeTypes.string, + top_hits_by_key: runtimeTypes.type({ + hits: runtimeTypes.type({ + hits: runtimeTypes.array( + runtimeTypes.type({ + sort: runtimeTypes.tuple([runtimeTypes.number, runtimeTypes.number]), + }) + ), + }), + }), + }), + runtimeTypes.partial({ + from: runtimeTypes.number, + to: runtimeTypes.number, + }), +]); + +export interface LogSummaryDateRangeBucket + extends runtimeTypes.TypeOf {} + +const LogSummaryResponseRuntimeType = runtimeTypes.type({ + aggregations: runtimeTypes.type({ + count_by_date: runtimeTypes.type({ + buckets: runtimeTypes.array(LogSummaryDateRangeBucketRuntimeType), + }), + }), +}); + +export interface LogSummaryResponse + extends runtimeTypes.TypeOf {} diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index d4e7c51d59669..0127f80b31357 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -14,8 +14,9 @@ import { InfraLogItem, InfraLogMessageSegment, InfraLogSummaryBucket, + InfraLogSummaryHighlightBucket, } from '../../../graphql/types'; -import { InfraDateRangeAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework'; +import { InfraFrameworkRequest } from '../../adapters/framework'; import { InfraSourceConfiguration, InfraSources, @@ -133,7 +134,7 @@ export class InfraLogEntriesDomain { startKey: TimeKey, endKey: TimeKey, highlights: Array<{ - query: JsonObject; + query: string; countBefore: number; countAfter: number; }>, @@ -147,13 +148,14 @@ export class InfraLogEntriesDomain { const documentSets = await Promise.all( highlights.map(async highlight => { + const highlightQuery = createHighlightQueryDsl(highlight.query, requiredFields); const query = filterQuery ? { bool: { - filter: [filterQuery, highlight.query], + filter: [filterQuery, highlightQuery], }, } - : highlight.query; + : highlightQuery; const [documentsBefore, documents, documentsAfter] = await Promise.all([ this.adapter.getAdjacentLogEntryDocuments( request, @@ -163,7 +165,7 @@ export class InfraLogEntriesDomain { 'desc', highlight.countBefore, query, - highlight.query + highlightQuery ), this.adapter.getContainedLogEntryDocuments( request, @@ -172,7 +174,7 @@ export class InfraLogEntriesDomain { startKey, endKey, query, - highlight.query + highlightQuery ), this.adapter.getAdjacentLogEntryDocuments( request, @@ -182,7 +184,7 @@ export class InfraLogEntriesDomain { 'asc', highlight.countAfter, query, - highlight.query + highlightQuery ), ]); const entries = [...documentsBefore, ...documents, ...documentsAfter].map( @@ -217,8 +219,50 @@ export class InfraLogEntriesDomain { bucketSize, filterQuery ); - const buckets = dateRangeBuckets.map(convertDateRangeBucketToSummaryBucket); - return buckets; + return dateRangeBuckets; + } + + public async getLogSummaryHighlightBucketsBetween( + request: InfraFrameworkRequest, + sourceId: string, + start: number, + end: number, + bucketSize: number, + highlightQueries: string[], + filterQuery?: LogEntryQuery + ): Promise { + const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const messageFormattingRules = compileFormattingRules( + getBuiltinRules(configuration.fields.message) + ); + const requiredFields = getRequiredFields(configuration, messageFormattingRules); + + const summaries = await Promise.all( + highlightQueries.map(async highlightQueryPhrase => { + const highlightQuery = createHighlightQueryDsl(highlightQueryPhrase, requiredFields); + const query = filterQuery + ? { + bool: { + must: [filterQuery, highlightQuery], + }, + } + : highlightQuery; + const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( + request, + configuration, + start, + end, + bucketSize, + query + ); + const summaryHighlightBuckets = summaryBuckets + .filter(logSummaryBucketHasEntries) + .map(convertLogSummaryBucketToSummaryHighlightBucket); + return summaryHighlightBuckets; + }) + ); + + return summaries; } public async getLogItem( @@ -283,7 +327,7 @@ export interface LogEntriesAdapter { end: number, bucketSize: number, filterQuery?: LogEntryQuery - ): Promise; + ): Promise; getLogItem( request: InfraFrameworkRequest, @@ -301,6 +345,13 @@ export interface LogEntryDocument { key: TimeKey; } +export interface LogSummaryBucket { + entriesCount: number; + start: number; + end: number; + topEntryKeys: TimeKey[]; +} + const convertLogDocumentToEntry = ( sourceId: string, logColumns: InfraSourceConfiguration['logColumns'], @@ -331,12 +382,16 @@ const convertLogDocumentToEntry = ( }), }); -const convertDateRangeBucketToSummaryBucket = ( - bucket: InfraDateRangeAggregationBucket -): InfraLogSummaryBucket => ({ - entriesCount: bucket.doc_count, - start: bucket.from || 0, - end: bucket.to || 0, +const logSummaryBucketHasEntries = (bucket: LogSummaryBucket) => + bucket.entriesCount > 0 && bucket.topEntryKeys.length > 0; + +const convertLogSummaryBucketToSummaryHighlightBucket = ( + bucket: LogSummaryBucket +): InfraLogSummaryHighlightBucket => ({ + entriesCount: bucket.entriesCount, + start: bucket.start, + end: bucket.end, + representativeKey: bucket.topEntryKeys[0], }); const getRequiredFields = ( @@ -356,3 +411,12 @@ const getRequiredFields = ( return Array.from(new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules])); }; + +const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ + multi_match: { + fields, + lenient: true, + query: phrase, + type: 'phrase', + }, +}); diff --git a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts index efc1173f9fb90..53ece86b92483 100644 --- a/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts +++ b/x-pack/test/api_integration/apis/infra/log_entry_highlights.ts @@ -55,9 +55,7 @@ export default function({ getService }: FtrProviderContext) { endKey: KEY_AFTER_END, highlights: [ { - query: JSON.stringify({ - multi_match: { query: 'message of document 0', type: 'phrase', lenient: true }, - }), + query: 'message of document 0', countBefore: 0, countAfter: 0, }, @@ -103,13 +101,7 @@ export default function({ getService }: FtrProviderContext) { endKey: KEY_AFTER_END, highlights: [ { - query: JSON.stringify({ - multi_match: { - query: 'generate_test_data/simple_logs', - type: 'phrase', - lenient: true, - }, - }), + query: 'generate_test_data/simple_logs', countBefore: 0, countAfter: 0, }, @@ -155,9 +147,7 @@ export default function({ getService }: FtrProviderContext) { }), highlights: [ { - query: JSON.stringify({ - multi_match: { query: 'message', type: 'phrase', lenient: true }, - }), + query: 'message', countBefore: 0, countAfter: 0, }, @@ -201,9 +191,7 @@ export default function({ getService }: FtrProviderContext) { endKey: KEY_BEFORE_END, highlights: [ { - query: JSON.stringify({ - multi_match: { query: 'message of document 0', type: 'phrase', lenient: true }, - }), + query: 'message of document 0', countBefore: 2, countAfter: 2, },