From c67107905a23feed8e3e512697506d569abfbe90 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Mon, 7 Jan 2019 16:46:58 -0500 Subject: [PATCH] Add ability to search for nodes in TraceDiffGraph and TraceGraph (#307) Signed-off-by: Everett Ross Signed-off-by: vvvprabhakar --- .../TraceDiff/TraceDiffGraph/GraphSearch.css | 21 ++++++ .../TraceDiff/TraceDiffGraph/GraphSearch.js | 63 ++++++++++++++++++ .../TraceDiffGraph/TraceDiffGraph.js | 2 + .../TraceDiff/TraceDiffGraph/drawNode.css | 6 ++ .../TraceDiff/TraceDiffGraph/drawNode.js | 32 ++++++++- .../TracePage/TraceGraph/OpNode.css | 5 ++ .../components/TracePage/TraceGraph/OpNode.js | 51 ++++++++++++-- .../TracePage/TraceGraph/TraceGraph.js | 3 + .../src/components/TracePage/index.js | 56 ++-------------- .../jaeger-ui/src/model/trace-dag/TraceDag.js | 1 + packages/jaeger-ui/src/utils/filter-spans.js | 66 +++++++++++++++++++ 11 files changed, 247 insertions(+), 59 deletions(-) create mode 100644 packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.css create mode 100644 packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.js create mode 100644 packages/jaeger-ui/src/utils/filter-spans.js diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.css new file mode 100644 index 0000000000..374854bd5c --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.css @@ -0,0 +1,21 @@ +/* +Copyright (c) 2019 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.GraphSearch { + position: absolute; + right: 20px; + bottom: 20px; +} diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.js new file mode 100644 index 0000000000..fa858eeb01 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/GraphSearch.js @@ -0,0 +1,63 @@ +// @flow + +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { Icon, Input } from 'antd'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import queryString from 'query-string'; + +import type { Location, /* Match, */ RouterHistory } from 'react-router-dom'; + +import prefixUrl from '../../../utils/prefix-url'; + +import type { ReduxState } from '../../../types/index'; + +import './GraphSearch.css'; + +type propsType = { + graphSearch?: string, + history: RouterHistory, + location: Location, +}; + +export function UnconnectedGraphSearch(props: propsType) { + function inputOnChange(evt) { + const { graphSearch, ...queryParams } = queryString.parse(props.location.search); + const { value } = evt.target; + if (value) { + queryParams.graphSearch = value; + } + props.history.replace(prefixUrl(`?${queryString.stringify(queryParams)}`)); + } + return ( +
+ + +
+ ); +} + +UnconnectedGraphSearch.defaultProps = { + graphSearch: null, +}; + +export function mapStateToProps(state: ReduxState): { graphSearch?: string } { + const { graphSearch } = queryString.parse(state.router.location.search); + return { graphSearch }; +} + +export default withRouter(connect(mapStateToProps)(UnconnectedGraphSearch)); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js index 2f182644f1..35f70a5e5e 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js @@ -18,6 +18,7 @@ import * as React from 'react'; import { DirectedGraph, LayoutManager } from '@jaegertracing/plexus'; import drawNode from './drawNode'; +import GraphSearch from './GraphSearch'; import ErrorMessage from '../../common/ErrorMessage'; import LoadingIndicator from '../../common/LoadingIndicator'; import { fetchedState } from '../../../constants'; @@ -112,6 +113,7 @@ export default class TraceDiffGraph extends React.PureComponent { edges={edges} vertices={vertices} /> + ); } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css index 13c1c79fd9..cdd8f5ea91 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css @@ -52,6 +52,12 @@ limitations under the License. color: #fff; } +.DiffNode.is-graph-search-match { + outline-style: solid; + outline-color: green; + outline-width: 1px; +} + .DiffNode--metricCell { padding: 0.3rem 0.5rem; background: rgba(255, 255, 255, 0.3); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js index cbe2d3b9f6..3ba5bfd041 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js @@ -17,14 +17,22 @@ import * as React from 'react'; import { Popover } from 'antd'; import cx from 'classnames'; +import _map from 'lodash/map'; +import _memoize from 'lodash/memoize'; +import queryString from 'query-string'; +import { connect } from 'react-redux'; +import filterSpans from '../../../utils/filter-spans'; import type { PVertex } from '../../../model/trace-dag/types'; +import type { ReduxState } from '../../../types/index'; import './drawNode.css'; type Props = { a: number, b: number, + graphSearch?: string, + members: any[], operation: string, service: string, }; @@ -34,9 +42,18 @@ const max = Math.max; class DiffNode extends React.PureComponent { props: Props; + filterSpans: typeof filterSpans; + static defaultProps = { + graphSearch: '', + }; + + constructor(props: Props) { + super(props); + this.filterSpans = _memoize(filterSpans); + } render() { - const { a, b, operation, service } = this.props; + const { a, b, graphSearch, operation, service } = this.props; const isSame = a === b; const className = cx({ 'is-same': isSame, @@ -45,6 +62,7 @@ class DiffNode extends React.PureComponent { 'is-added': a === 0, 'is-less': a > b && b > 0, 'is-removed': b === 0, + 'is-graph-search-match': this.filterSpans(graphSearch, _map(this.props.members, 'span')).size, }); const chgSign = a < b ? '+' : '-'; const table = ( @@ -81,7 +99,15 @@ class DiffNode extends React.PureComponent { } } +// TODO: This mapStateToProps is duplicative in three components +export function mapStateToProps(state: ReduxState): { graphSearch?: string } { + const { graphSearch } = queryString.parse(state.router.location.search); + return { graphSearch }; +} + +const ConnectedDiffNode = connect(mapStateToProps)(DiffNode); + export default function drawNode(vertex: PVertex) { - const { data, operation, service } = vertex.data; - return ; + const { data, members, operation, service } = vertex.data; + return ; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css index 75e2a545cf..d66d8bf09a 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css @@ -27,6 +27,11 @@ limitations under the License. .OpNode th { border: none; } +.OpNode.is-graph-search-match { + outline-style: solid; + outline-color: green; + outline-width: 1px; +} .OpMode--mode-service { background: #bbb; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js index 43d7df6bf4..99aae12b0f 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js @@ -16,9 +16,17 @@ import * as React from 'react'; import { Popover } from 'antd'; +import cx from 'classnames'; +import _map from 'lodash/map'; +import _memoize from 'lodash/memoize'; +import queryString from 'query-string'; +import { connect } from 'react-redux'; + +import filterSpans from '../../../utils/filter-spans'; import colorGenerator from '../../../utils/color-generator'; import type { PVertex } from '../../../model/trace-dag/types'; +import type { ReduxState } from '../../../types/index'; import './OpNode.css'; @@ -32,6 +40,8 @@ type Props = { operation: string, service: string, mode: string, + graphSearch?: string, + members: any[], }; export const MODE_SERVICE = 'service'; @@ -63,9 +73,29 @@ export function round2(percent: number) { export default class OpNode extends React.PureComponent { props: Props; + filterSpans: typeof filterSpans; + static defaultProps = { + graphSearch: '', + }; + + constructor(props: Props) { + super(props); + this.filterSpans = _memoize(filterSpans); + } render() { - const { count, errors, time, percent, selfTime, percentSelfTime, operation, service, mode } = this.props; + const { + count, + errors, + time, + percent, + selfTime, + percentSelfTime, + operation, + service, + mode, + graphSearch, + } = this.props; // Spans over 20 % time are full red - we have probably to reconsider better approach let backgroundColor; @@ -81,8 +111,12 @@ export default class OpNode extends React.PureComponent { .join(); } + const className = cx('OpNode', `OpNode--mode-${mode}`, { + 'is-graph-search-match': this.filterSpans(graphSearch, _map(this.props.members, 'span')).size, + }); + const table = ( - +
{ } } +export function mapStateToProps(state: ReduxState): { graphSearch?: string } { + const { graphSearch } = queryString.parse(state.router.location.search); + return { graphSearch }; +} + +const ConnectedOpNode = connect(mapStateToProps)(OpNode); + export function getNodeDrawer(mode: string) { return function drawNode(vertex: PVertex) { - const { data, operation, service } = vertex.data; - return ; + const { data, members, operation, service } = vertex.data; + return ( + + ); }; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js index f6ee4a5fa6..374394857e 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js @@ -20,6 +20,8 @@ import { DirectedGraph, LayoutManager } from '@jaegertracing/plexus'; import DRange from 'drange'; import { getNodeDrawer, MODE_SERVICE, MODE_TIME, MODE_SELFTIME, HELP_TABLE } from './OpNode'; +// TODO: Location implies only used by diff, need to move +import GraphSearch from '../../TraceDiff/TraceDiffGraph/GraphSearch'; import convPlexus from '../../../model/trace-dag/convPlexus'; import TraceDag from '../../../model/trace-dag/TraceDag'; @@ -336,6 +338,7 @@ export default class TraceGraph extends React.PureComponent { )} + ); } diff --git a/packages/jaeger-ui/src/components/TracePage/index.js b/packages/jaeger-ui/src/components/TracePage/index.js index 948f9ffe8e..e8163a506d 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.js +++ b/packages/jaeger-ui/src/components/TracePage/index.js @@ -17,6 +17,7 @@ import * as React from 'react'; import { Input } from 'antd'; import _clamp from 'lodash/clamp'; +import _get from 'lodash/get'; import _mapValues from 'lodash/mapValues'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -38,13 +39,14 @@ import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { fetchedState } from '../../constants'; +import filterSpans from '../../utils/filter-spans'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; import type { ViewRange, ViewRangeTimeUpdate } from './types'; import type { FetchedTrace, ReduxState } from '../../types'; import type { TraceArchive } from '../../types/archive'; import type { EmbeddedState } from '../../types/embedded'; -import type { KeyValuePair, Span } from '../../types/trace'; +// import type { KeyValuePair, Span } from '../../types/trace'; import './index.css'; @@ -217,60 +219,10 @@ export class TracePageImpl extends React.PureComponent { - const spans = this.props.trace && this.props.trace.data && this.props.trace.data.spans; - if (!spans) return null; - - // if a span field includes at least one filter in includeFilters, the span is a match - const includeFilters = []; - - // values with keys that include text in any one of the excludeKeys will be ignored - const excludeKeys = []; - - // split textFilter by whitespace, remove empty strings, and extract includeFilters and excludeKeys - textFilter - .split(' ') - .map(s => s.trim()) - .filter(s => s) - .forEach(w => { - if (w[0] === '-') { - excludeKeys.push(w.substr(1).toLowerCase()); - } else { - includeFilters.push(w.toLowerCase()); - } - }); - - const isTextInFilters = (filters: Array, text: string) => - filters.some(filter => text.toLowerCase().includes(filter)); - - const isTextInKeyValues = (kvs: Array) => - kvs - ? kvs.some(kv => { - // ignore checking key and value for a match if key is in excludeKeys - if (isTextInFilters(excludeKeys, kv.key)) return false; - // match if key or value matches an item in includeFilters - return ( - isTextInFilters(includeFilters, kv.key) || isTextInFilters(includeFilters, kv.value.toString()) - ); - }) - : false; - - const isSpanAMatch = (span: Span) => - isTextInFilters(includeFilters, span.operationName) || - isTextInFilters(includeFilters, span.process.serviceName) || - isTextInKeyValues(span.tags) || - span.logs.some(log => isTextInKeyValues(log.fields)) || - isTextInKeyValues(span.process.tags); - - // declare as const because need to disambiguate the type - const rv: Set = new Set(spans.filter(isSpanAMatch).map((span: Span) => span.spanID)); - return rv; - }; - updateTextFilter = (textFilter: string) => { let findMatchesIDs; if (textFilter.trim()) { - findMatchesIDs = this.filterSpans(textFilter); + findMatchesIDs = filterSpans(textFilter, _get(this.props, 'trace.data.spans')); } else { findMatchesIDs = null; } diff --git a/packages/jaeger-ui/src/model/trace-dag/TraceDag.js b/packages/jaeger-ui/src/model/trace-dag/TraceDag.js index ae8701b7c3..48e2044067 100644 --- a/packages/jaeger-ui/src/model/trace-dag/TraceDag.js +++ b/packages/jaeger-ui/src/model/trace-dag/TraceDag.js @@ -43,6 +43,7 @@ export default class TraceDag { }); const { data } = node; data[key] = src.count; + node.members.push(...src.members); node.count = data.b - data.a; if (!node.parentID) { dt.rootIDs.add(node.id); diff --git a/packages/jaeger-ui/src/utils/filter-spans.js b/packages/jaeger-ui/src/utils/filter-spans.js new file mode 100644 index 0000000000..47b8ed4c3c --- /dev/null +++ b/packages/jaeger-ui/src/utils/filter-spans.js @@ -0,0 +1,66 @@ +/* flow */ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { /* KeyValuePair, */ Span } from '../types/trace'; + +export default function filterSpans(textFilter: string, spans: Span) { + // const spans = this.props.trace && this.props.trace.data && this.props.trace.data.spans; + if (!spans) return null; + + // if a span field includes at least one filter in includeFilters, the span is a match + const includeFilters = []; + + // values with keys that include text in any one of the excludeKeys will be ignored + const excludeKeys = []; + + // split textFilter by whitespace, remove empty strings, and extract includeFilters and excludeKeys + textFilter + .split(' ') + .map(s => s.trim()) + .filter(s => s) + .forEach(w => { + if (w[0] === '-') { + excludeKeys.push(w.substr(1).toLowerCase()); + } else { + includeFilters.push(w.toLowerCase()); + } + }); + + const isTextInFilters = (filters: Array, text: string) => + filters.some(filter => text.toLowerCase().includes(filter)); + + const isTextInKeyValues = (kvs: Array) => + kvs + ? kvs.some(kv => { + // ignore checking key and value for a match if key is in excludeKeys + if (isTextInFilters(excludeKeys, kv.key)) return false; + // match if key or value matches an item in includeFilters + return ( + isTextInFilters(includeFilters, kv.key) || isTextInFilters(includeFilters, kv.value.toString()) + ); + }) + : false; + + const isSpanAMatch = (span: Span) => + isTextInFilters(includeFilters, span.operationName) || + isTextInFilters(includeFilters, span.process.serviceName) || + isTextInKeyValues(span.tags) || + span.logs.some(log => isTextInKeyValues(log.fields)) || + isTextInKeyValues(span.process.tags); + + // declare as const because need to disambiguate the type + const rv: Set = new Set(spans.filter(isSpanAMatch).map((span: Span) => span.spanID)); + return rv; +}