From 53e16356190e0696eeb32019aa85f7766cf0aeae Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Thu, 29 Aug 2019 08:56:22 -0500 Subject: [PATCH] Derive DDG from search results Signed-off-by: Ruben Vargas --- .../components/DeepDependencies/traces.tsx | 154 ++++++++++++++++++ .../SearchResults/AltViewOptions.tsx | 30 ++++ .../SearchTracePage/SearchResults/index.js | 68 ++++++-- .../src/components/SearchTracePage/index.js | 9 + .../components/SearchTracePage/index.test.js | 11 +- .../src/model/ddg/transformTracesToPaths.tsx | 117 +++++++++++++ packages/jaeger-ui/src/model/ddg/types.tsx | 6 +- 7 files changed, 373 insertions(+), 22 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/traces.tsx create mode 100644 packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx create mode 100644 packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx diff --git a/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx b/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx new file mode 100644 index 0000000000..d9a01ede6e --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx @@ -0,0 +1,154 @@ +// 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 React, { Component } from 'react'; +import { History as RouterHistory, Location } from 'history'; +import _get from 'lodash/get'; +import { connect } from 'react-redux'; +import { getUrlState } from './url'; +import { getUrl } from '../SearchTracePage/url'; + +import Graph from './Graph'; +import { extractUiFindFromState, TExtractUiFindFromStateReturn } from '../common/UiFindInput'; +import { EDirection, TDdgModel, TDdgModelParams, TDdgSparseUrlState } from '../../model/ddg/types'; +import TGraph, { makeGraph } from '../../model/ddg/Graph'; +import { encodeDistance } from '../../model/ddg/visibility-codec'; +import { ReduxState } from '../../types'; + +import './index.css'; +import transformDdgData from '../../model/ddg/transformDdgData'; +import transformTracesToPaths from '../../model/ddg/transformTracesToPaths'; +import HopsSelector from './Header/HopsSelector'; + +type TDispatchProps = { + fetchDeepDependencyGraph: (query: TDdgModelParams) => void; + fetchServices: () => void; + fetchServiceOperations: (service: string) => void; + transformTracesToDDG: (trace: any) => void; +}; + +type TReduxProps = TExtractUiFindFromStateReturn & { + graph: TGraph | undefined; + services?: string[] | null; + urlState: TDdgSparseUrlState; + trace: any; + model: TDdgModel | undefined; +}; + +type TOwnProps = { + history: RouterHistory; + location: Location; +}; + +type TProps = TDispatchProps & TReduxProps & TOwnProps; + +// export for tests +export class SearchResultsDDG extends Component { + // shouldComponentUpdate is necessary as we don't want the plexus graph to re-render due to a uxStatus change + shouldComponentUpdate(nextProps: TProps) { + const updateCauses = [ + 'uiFind', + 'trace', + 'services', + 'urlState.service', + 'urlState.operation', + 'urlState.start', + 'urlState.end', + 'urlState.visEncoding', + ]; + return updateCauses.some(cause => _get(nextProps, cause) !== _get(this.props, cause)); + } + + setDistance = (distance: number, direction: EDirection) => { + const { visEncoding } = this.props.urlState; + const { model } = this.props; + if (model) { + this.updateUrlState({ + visEncoding: encodeDistance({ + ddgModel: model, + direction, + distance, + prevVisEncoding: visEncoding, + }), + }); + } + }; + + updateUrlState = (newValues: Partial) => { + const { uiFind, urlState, history } = this.props; + history.push(getUrl({ uiFind, ...urlState, ...newValues })); + }; + + render() { + const { graph, uiFind, urlState, model } = this.props; + const { visEncoding } = urlState; + const uiFindMatches = graph && graph.getVisibleUiFindMatches(uiFind, visEncoding); + + let content = ( +
+

Unknown graphState:

+

${JSON.stringify(graph)}

+
+ ); + if (graph && model) { + const { edges, vertices } = graph.getVisible(visEncoding); + const distanceToPathElems = model.distanceToPathElems; + content = ( +
+ +
+ graph.getVisiblePathElems(key, visEncoding)} + uiFindMatches={uiFindMatches} + vertices={vertices} + /> +
+
+ ); + } + return content; + } +} + +// export for tests +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const { trace } = state; + const urlState = getUrlState(ownProps.location.search); + const { density, operation, service, showOp } = urlState; + let graph: TGraph | undefined; + let model: TDdgModel | undefined; + + if (service) { + const paths = transformTracesToPaths(trace.traces, service, operation); + model = transformDdgData(paths, { service, operation }); + graph = makeGraph(model, showOp, density); + } else { + graph = undefined; + } + + return { + model, + trace, + graph, + urlState, + ...extractUiFindFromState(state), + }; +} + +export default connect(mapStateToProps)(SearchResultsDDG); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx new file mode 100644 index 0000000000..db74d0b722 --- /dev/null +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx @@ -0,0 +1,30 @@ +// Copyright (c) 2018 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 { Button } from 'antd'; + +type Props = { + traceResultsView: boolean; + onTraceGraphViewClicked: () => void; +}; + +export default function AltViewOptions(props: Props) { + const { onTraceGraphViewClicked, traceResultsView } = props; + return ( + + ); +} diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js index 82c6d343cd..aa00bff697 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js @@ -17,13 +17,15 @@ import * as React from 'react'; import { Select } from 'antd'; import { Link } from 'react-router-dom'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; +import { Field, formValueSelector, reduxForm } from 'redux-form'; +import { History as RouterHistory, Location } from 'history'; import DiffSelection from './DiffSelection'; import * as markers from './index.markers'; import ResultItem from './ResultItem'; import ScatterPlot from './ScatterPlot'; import { getUrl } from '../url'; + import LoadingIndicator from '../../common/LoadingIndicator'; import NewWindowIcon from '../../common/NewWindowIcon'; import { getLocation } from '../../TracePage/url'; @@ -36,6 +38,8 @@ import type { FetchedTrace } from '../../../types'; import type { SearchQuery } from '../../../types/search'; import './index.css'; +import AltViewOptions from './AltViewOptions'; +import SearchResultsDGG from '../../DeepDependencies/traces'; type SearchResultsProps = { cohortAddTrace: string => void, @@ -50,6 +54,12 @@ type SearchResultsProps = { showStandaloneLink: boolean, skipMessage?: boolean, traces: TraceSummary[], + history: RouterHistory, + location: Location, +}; + +type StateProps = { + traceResultsView: boolean, }; const Option = Select.Option; @@ -81,11 +91,16 @@ const SelectSort = reduxForm({ export const sortFormSelector = formValueSelector('traceResultsSort'); -export default class SearchResults extends React.PureComponent { +export default class SearchResults extends React.Component { props: SearchResultsProps; static defaultProps = { skipMessage: false, queryOfResults: undefined }; + constructor(props) { + super(props); + this.state = { traceResultsView: true }; + } + toggleComparison = (traceID: string, remove: boolean) => { const { cohortAddTrace, cohortRemoveTrace } = this.props; if (remove) { @@ -95,6 +110,13 @@ export default class SearchResults extends React.PureComponent { + const { traceResultsView } = this.state; + this.setState({ + traceResultsView: !traceResultsView, + }); + }; + render() { const { diffCohort, @@ -107,7 +129,12 @@ export default class SearchResults extends React.PureComponent ); @@ -158,6 +185,10 @@ export default class SearchResults extends React.PureComponent 1 && 's'} + {showStandaloneLink && (
- {diffSelection} -
    - {traces.map(trace => ( -
  • - -
  • - ))} -
+ {!traceResultsView && } + {traceResultsView && diffSelection} + {traceResultsView && ( +
    + {traces.map(trace => ( +
  • + +
  • + ))} +
+ )}
); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js index cd4426736a..79a976c81a 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.js @@ -90,10 +90,13 @@ export class SearchTracePageImpl extends Component { traceResults, queryOfResults, loadJsonTraces, + location, + history, } = this.props; const hasTraceResults = traceResults && traceResults.length > 0; const showErrors = errors && !loadingTraces; const showLogo = isHomepage && !hasTraceResults && !loadingTraces && !errors; + return (
@@ -134,6 +137,8 @@ export class SearchTracePageImpl extends Component { showStandaloneLink={Boolean(embedded)} skipMessage={isHomepage} traces={traceResults} + location={location} + history={history} /> )} {showLogo && ( @@ -191,6 +196,8 @@ SearchTracePageImpl.propTypes = { }) ), loadJsonTraces: PropTypes.func, + // eslint-disable-next-line react/forbid-prop-types + location: PropTypes.any, }; const stateTraceXformer = memoizeOne(stateTrace => { @@ -248,9 +255,11 @@ export function mapStateToProps(state) { if (serviceError) { errors.push(serviceError); } + const location = router.location; const sortBy = sortFormSelector(state, 'sortBy'); const traceResults = sortedTracesXformer(traces, sortBy); return { + location, queryOfResults, diffCohort, embedded, diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js index 65ca7277bd..c1e89d1588 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js @@ -155,9 +155,14 @@ describe('mapStateToProps()', () => { services: stateServices, }; - const { maxTraceDuration, traceResults, diffCohort, numberOfTraceResults, ...rest } = mapStateToProps( - state - ); + const { + maxTraceDuration, + traceResults, + diffCohort, + numberOfTraceResults, + location, + ...rest + } = mapStateToProps(state); expect(traceResults).toHaveLength(stateTrace.search.results.length); expect(traceResults[0].traceID).toBe(trace.traceID); expect(maxTraceDuration).toBe(trace.duration); diff --git a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx new file mode 100644 index 0000000000..29dbe3fbb7 --- /dev/null +++ b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx @@ -0,0 +1,117 @@ +// 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 { TDdgPayloadEntry, TDdgPayloadPath, TDdgPayload } from './types'; +import { Span, Trace } from '../../types/trace'; +import { FetchedTrace } from '../../types'; + +type Node = { + value?: Span; + children: Node[]; +}; + +const hasFocal = (path: TDdgPayloadEntry[], focalService: string, focalOperation: string | undefined) => { + for (let i = 0; i < path.length; ++i) { + if ( + focalService === path[i].service && + (focalOperation === undefined || focalOperation === path[i].operation) + ) { + return true; + } + } + return false; +}; + +function convertSpan(span: Span, trace: Trace) { + const serviceName = trace.processes[span.processID].serviceName; + const operationName = span.operationName; + return { service: serviceName, operation: operationName }; +} + +function findPathToRoot( + node: Node, + root: Node, + nodes: Map, + trace: Trace, + focalService: string, + focalOperation: string | undefined +): TDdgPayloadPath | null { + const path: TDdgPayloadEntry[] = []; + let actual: Node | undefined = node; + while ( + actual && + actual.value && + actual.value.references !== undefined && + Array.isArray(actual.value.references) && + actual.value.references.length + ) { + path.push(convertSpan(actual.value, trace)); + actual = nodes.get(actual.value.references[0].spanID); + } + if (actual && actual.value) { + path.push(convertSpan(actual.value, trace)); + } + if (hasFocal(path, focalService, focalOperation) === true) { + return { path: path.reverse(), trace_id: trace.traceID }; + } + return null; +} + +const processTrace = ( + trace: Trace, + focalService: string, + focalOperation: string | undefined +): TDdgPayload => { + const root: Node = { children: [] }; + const nodes: Map = new Map( + trace.spans.map(span => [span.spanID, { children: [], value: span }]) + ); + nodes.forEach(node => { + const span = node.value; + if (span && Array.isArray(span.references) && span.references.length) { + const reference = span.references[0]; + const parent = nodes.get(reference.spanID); + if (parent) { + parent.children.push(node); + } + } else { + root.children.push(node); + } + }); + // Process leaves + const tracePaths: TDdgPayload = []; + nodes.forEach(node => { + if (node.children.length === 0) { + const path = findPathToRoot(node, root, nodes, trace, focalService, focalOperation); + if (path) { + tracePaths.push(path); + } + } + }); + return tracePaths; +}; + +export default function( + traces: Record, + focalService: string, + focalOperation: string | undefined +) { + let paths: TDdgPayload = []; + Object.values(traces).forEach(trace => { + if (trace && trace.data) { + paths = paths.concat(processTrace(trace.data, focalService, focalOperation)); + } + }); + return paths; +} diff --git a/packages/jaeger-ui/src/model/ddg/types.tsx b/packages/jaeger-ui/src/model/ddg/types.tsx index e5f504145a..b228657650 100644 --- a/packages/jaeger-ui/src/model/ddg/types.tsx +++ b/packages/jaeger-ui/src/model/ddg/types.tsx @@ -25,10 +25,12 @@ export type TDdgPayloadEntry = { service: string; }; -export type TDdgPayload = { +export type TDdgPayloadPath = { path: TDdgPayloadEntry[]; trace_id: string; // eslint-disable-line camelcase -}[]; +}; + +export type TDdgPayload = TDdgPayloadPath[]; export type TDdgService = { name: string;