diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7ee96c39..043235a2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Releases +## Next (Unreleased) + +### Enhancements + +* **Trace detail:** Log Markers on Spans ([Fix #119](https://github.com/jaegertracing/jaeger-ui/issues/119)) ([@sfriberg](https://github.com/sfriberg) in [#309](https://github.com/jaegertracing/jaeger-ui/pull/309)) + ## v1.0.1 (February 15, 2019) ### Fixes diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.css index f52b8fcd73..36da603c1c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.css @@ -66,3 +66,39 @@ limitations under the License. .span-row:hover .SpanBar--label { color: #000; } + +.SpanBar--logMarker { + background-color: rgba(0, 0, 0, 0.5); + cursor: pointer; + height: 60%; + min-width: 1px; + position: absolute; + top: 20%; +} + +.SpanBar--logMarker:hover { + background-color: #000; +} + +.SpanBar--logMarker::before, +.SpanBar--logMarker::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: 0; + border: 1px solid transparent; +} + +.SpanBar--logMarker::after { + left: 0; +} + +.SpanBar--logHint { + pointer-events: none; +} + +/* Tweak the popover aesthetics - unfortunate but necessary */ +.SpanBar--logHint .ant-popover-inner-content { + padding: 0.25rem; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.js index efeb7ebd3a..64d3fbe77e 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBar.js @@ -15,8 +15,15 @@ // limitations under the License. import React from 'react'; +import { Popover } from 'antd'; +import _groupBy from 'lodash/groupBy'; import { onlyUpdateForKeys, compose, withState, withProps } from 'recompose'; +import AccordianLogs from './SpanDetail/AccordianLogs'; + +import type { ViewedBoundsFunctionType } from './utils'; +import type { Span } from '../../../types/trace'; + import './SpanBar.css'; type SpanBarProps = { @@ -26,6 +33,7 @@ type SpanBarProps = { onClick: (SyntheticMouseEvent) => void, viewEnd: number, viewStart: number, + getViewedBounds: ViewedBoundsFunctionType, rpc: { viewStart: number, viewEnd: number, @@ -33,14 +41,35 @@ type SpanBarProps = { }, setLongLabel: () => void, setShortLabel: () => void, + traceStartTime: number, + span: Span, }; function toPercent(value: number) { - return `${value * 100}%`; + return `${(value * 100).toFixed(1)}%`; } function SpanBar(props: SpanBarProps) { - const { viewEnd, viewStart, color, label, hintSide, onClick, setLongLabel, setShortLabel, rpc } = props; + const { + viewEnd, + viewStart, + getViewedBounds, + color, + label, + hintSide, + onClick, + setLongLabel, + setShortLabel, + rpc, + traceStartTime, + span, + } = props; + // group logs based on timestamps + const logGroups = _groupBy(span.logs, log => { + const posPercent = getViewedBounds(log.timestamp, log.timestamp).start; + // round to the nearest 0.2% + return toPercent(Math.round(posPercent * 500) / 500); + }); return (
{label}
+
+ {Object.keys(logGroups).map(positionKey => ( + + } + > +
+ + ))} +
{rpc && (
', () => { hintSide: 'right', viewEnd: 1, viewStart: 0, + getViewedBounds: s => { + // Log entries + if (s === 10) { + return { start: 0.1, end: 0.1 }; + } else if (s === 20) { + return { start: 0.2, end: 0.2 }; + } + return { error: 'error' }; + }, rpc: { viewStart: 0.25, viewEnd: 0.75, color: '#000', }, + tracestartTime: 0, + span: { + logs: [ + { + timestamp: 10, + fields: [{ key: 'message', value: 'oh the log message' }, { key: 'something', value: 'else' }], + }, + { + timestamp: 10, + fields: [ + { key: 'message', value: 'oh the second log message' }, + { key: 'something', value: 'different' }, + ], + }, + { + timestamp: 20, + fields: [{ key: 'message', value: 'oh the next log message' }, { key: 'more', value: 'stuff' }], + }, + ], + }, }; it('renders without exploding', () => { @@ -46,4 +76,10 @@ describe('', () => { onMouseOut(); expect(labelElm.text()).toBe(shortLabel); }); + + it('log markers count', () => { + // 3 log entries, two grouped together with the same timestamp + const wrapper = mount(); + expect(wrapper.find(Popover).length).toEqual(2); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css index e67045c64e..0fef447ef0 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css @@ -67,7 +67,7 @@ limitations under the License. .span-row.is-expanded .span-name-wrapper { background: #f0f0f0; - outline: 1px solid #ddd; + box-shadow: 0 1px 0 #ddd; } .span-row.is-expanded .span-name-wrapper.is-matching-filter { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js index 475c977e13..cf86ee85fd 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js @@ -24,6 +24,7 @@ import SpanTreeOffset from './SpanTreeOffset'; import SpanBar from './SpanBar'; import Ticks from './Ticks'; +import type { ViewedBoundsFunctionType } from './utils'; import type { Span } from '../../../types/trace'; import './SpanBarRow.css'; @@ -46,9 +47,9 @@ type SpanBarRowProps = { serviceName: string, }, showErrorIcon: boolean, + getViewedBounds: ViewedBoundsFunctionType, + traceStartTime: number, span: Span, - viewEnd: number, - viewStart: number, }; /** @@ -86,12 +87,15 @@ export default class SpanBarRow extends React.PureComponent { numTicks, rpc, showErrorIcon, + getViewedBounds, + traceStartTime, span, - viewEnd, - viewStart, } = this.props; const { duration, hasChildren: isParent, operationName, process: { serviceName } } = span; const label = formatDuration(duration); + const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration); + const viewStart = viewBounds.start; + const viewEnd = viewBounds.end; const labelDetail = `${serviceName}::${operationName}`; let longLabel; @@ -103,6 +107,7 @@ export default class SpanBarRow extends React.PureComponent { longLabel = `${label} | ${labelDetail}`; hintSide = 'right'; } + return ( { rpc={rpc} viewStart={viewStart} viewEnd={viewEnd} + getViewedBounds={getViewedBounds} color={color} shortLabel={label} longLabel={longLabel} hintSide={hintSide} + traceStartTime={traceStartTime} + span={span} /> diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js index 936a249fb2..39cd5af596 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js @@ -41,6 +41,7 @@ describe('', () => { serviceName: 'rpc-service-name', }, showErrorIcon: false, + getViewedBounds: () => ({ start: 0, end: 1 }), span: { duration: 'test-duration', hasChildren: true, @@ -48,9 +49,8 @@ describe('', () => { serviceName: 'service-name', }, spanID, + logs: [], }, - viewEnd: 1, - viewStart: 0, }; let wrapper; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js index 02bdc6ccfd..8bbea5dd3a 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react'; +import * as React from 'react'; import cx from 'classnames'; import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; @@ -29,10 +29,11 @@ type AccordianKeyValuesProps = { className?: ?string, data: KeyValuePair[], highContrast?: boolean, + interactive?: boolean, isOpen: boolean, label: string, linksGetter: ?(KeyValuePair[], number) => Link[], - onToggle: () => void, + onToggle: null | (() => void), }; // export for tests @@ -61,9 +62,20 @@ KeyValuesSummary.defaultProps = { }; export default function AccordianKeyValues(props: AccordianKeyValuesProps) { - const { className, data, highContrast, isOpen, label, linksGetter, onToggle } = props; + const { className, data, highContrast, interactive, isOpen, label, linksGetter, onToggle } = props; const isEmpty = !Array.isArray(data) || !data.length; const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty }); + let arrow: React.Node | null = null; + let headerProps: Object | null = null; + if (interactive) { + arrow = isOpen ? : ; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; + } + return (
- {isOpen ? : } + {arrow} {label} {isOpen || ':'} @@ -90,4 +100,6 @@ export default function AccordianKeyValues(props: AccordianKeyValuesProps) { AccordianKeyValues.defaultProps = { className: null, highContrast: false, + interactive: true, + onToggle: null, }; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css index fe63cea6d6..4af98f1b45 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css @@ -16,6 +16,7 @@ limitations under the License. .AccordianLogs { border: 1px solid #d8d8d8; + position: relative; } .AccordianLogs--header { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js index 5a1837f482..83d113ac84 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js @@ -14,7 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react'; +import * as React from 'react'; +import cx from 'classnames'; import _sortBy from 'lodash/sortBy'; import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; @@ -26,29 +27,40 @@ import type { Log, KeyValuePair, Link } from '../../../../types/trace'; import './AccordianLogs.css'; type AccordianLogsProps = { + interactive?: boolean, isOpen: boolean, - linksGetter: ?(KeyValuePair[], number) => Link[], + linksGetter?: ?(KeyValuePair[], number) => Link[], logs: Log[], - onItemToggle: Log => void, - onToggle: () => void, - openedItems: Set, + onItemToggle?: Log => void, + onToggle?: () => void, + openedItems?: Set, timestamp: number, }; export default function AccordianLogs(props: AccordianLogsProps) { - const { isOpen, linksGetter, logs, openedItems, onItemToggle, onToggle, timestamp } = props; + const { interactive, isOpen, linksGetter, logs, openedItems, onItemToggle, onToggle, timestamp } = props; + let arrow: React.Node | null = null; + let HeaderComponent = 'span'; + let headerProps: Object | null = null; + if (interactive) { + arrow = isOpen ? ( + + ) : ( + + ); + HeaderComponent = 'a'; + headerProps = { + 'aria-checked': isOpen, + onClick: onToggle, + role: 'switch', + }; + } return (
- - {isOpen ? : } - Logs ({logs.length}) - + + {arrow} Logs ({logs.length}) + {isOpen && (
{_sortBy(logs, 'timestamp').map((log, i) => ( @@ -57,13 +69,13 @@ export default function AccordianLogs(props: AccordianLogsProps) { // eslint-disable-next-line react/no-array-index-key key={`${log.timestamp}-${i}`} className={i < logs.length - 1 ? 'ub-mb1' : null} - // compact - highContrast - isOpen={openedItems.has(log)} - linksGetter={linksGetter} data={log.fields || []} + highContrast + interactive={interactive} + isOpen={openedItems ? openedItems.has(log) : false} label={`${formatDuration(log.timestamp - timestamp)}`} - onToggle={() => onItemToggle(log)} + linksGetter={linksGetter} + onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null} /> ))} @@ -74,3 +86,11 @@ export default function AccordianLogs(props: AccordianLogsProps) {
); } + +AccordianLogs.defaultProps = { + interactive: true, + linksGetter: undefined, + onItemToggle: undefined, + onToggle: undefined, + openedItems: undefined, +}; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css index e3ee42d720..8af3140f11 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css @@ -31,9 +31,10 @@ limitations under the License. border-left: 2px solid transparent; cursor: col-resize; height: 5000px; + margin-left: -1px; position: absolute; top: 0; - width: 5px; + width: 1px; } .TimelineColumnResizer--dragger:hover { @@ -61,7 +62,7 @@ limitations under the License. top: 0; bottom: 0; left: -8px; - right: -5px; + right: 0; content: ' '; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js index bd840a8f3c..b7b2bd74bf 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js @@ -24,11 +24,13 @@ import ListView from './ListView'; import SpanBarRow from './SpanBarRow'; import DetailState from './SpanDetail/DetailState'; import SpanDetailRow from './SpanDetailRow'; -import { findServerChildSpan, getViewedBounds, isErrorSpan, spanContainsErredSpan } from './utils'; +import { createViewedBoundsFunc, findServerChildSpan, isErrorSpan, spanContainsErredSpan } from './utils'; import getLinks from '../../../model/link-patterns'; +import colorGenerator from '../../../utils/color-generator'; + +import type { ViewedBoundsFunctionType } from './utils'; import type { Accessors } from '../ScrollManager'; import type { Log, Span, Trace, KeyValuePair } from '../../../types/trace'; -import colorGenerator from '../../../utils/color-generator'; import './VirtualizedTraceView.css'; @@ -125,6 +127,7 @@ export class VirtualizedTraceViewImpl extends React.PureComponent
); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.js index 3674db6176..e70a86f0cf 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,30 +14,45 @@ // See the License for the specific language governing permissions and // limitations under the License. +import type { Span } from '../../../types/trace'; + +export type ViewedBoundsFunctionType = (number, number) => { start: number, end: number }; /** - * Given a range (`min`, `max`), finds the position of a sub-range (`start`, - * `end`) factoring in a zoom (`viewStart`, `viewEnd`). The result is returned - * as a `{ start, end }` object with values ranging in [0, 1]. + * Given a range (`min`, `max`) and factoring in a zoom (`viewStart`, `viewEnd`) + * a function is created that will find the position of a sub-range (`start`, `end`). + * The calling the generated method will return the result as a `{ start, end }` + * object with values ranging in [0, 1]. * * @param {number} min The start of the outer range. * @param {number} max The end of the outer range. - * @param {number} start The start of the sub-range. - * @param {number} end The end of the sub-range. * @param {number} viewStart The start of the zoom, on a range of [0, 1], * relative to the `min`, `max`. * @param {number} viewEnd The end of the zoom, on a range of [0, 1], * relative to the `min`, `max`. - * @return {Object} The resultant range. + * @returns {(number, number) => Object} Created view bounds function */ -export function getViewedBounds({ min, max, start, end, viewStart, viewEnd }) { +export function createViewedBoundsFunc(viewRange: { + min: number, + max: number, + viewStart: number, + viewEnd: number, +}) { + const { min, max, viewStart, viewEnd } = viewRange; const duration = max - min; const viewMin = min + viewStart * duration; const viewMax = max - (1 - viewEnd) * duration; const viewWindow = viewMax - viewMin; - return { + + /** + * View bounds function + * @param {number} start The start of the sub-range. + * @param {number} end The end of the sub-range. + * @return {Object} The resultant range. + */ + return (start: number, end: number) => ({ start: (start - viewMin) / viewWindow, end: (end - viewMin) / viewWindow, - }; + }); } /** @@ -47,7 +64,7 @@ export function getViewedBounds({ min, max, start, end, viewStart, viewEnd }) { * items. * @return {boolean} True if a match was found. */ -export function spanHasTag(key, value, span) { +export function spanHasTag(key: string, value: any, span: Span) { if (!Array.isArray(span.tags) || !span.tags.length) { return false; } @@ -59,7 +76,7 @@ export const isServerSpan = spanHasTag.bind(null, 'span.kind', 'server'); const isErrorBool = spanHasTag.bind(null, 'error', true); const isErrorStr = spanHasTag.bind(null, 'error', 'true'); -export const isErrorSpan = span => isErrorBool(span) || isErrorStr(span); +export const isErrorSpan = (span: Span) => isErrorBool(span) || isErrorStr(span); /** * Returns `true` if at least one of the descendants of the `parentSpanIndex` @@ -72,7 +89,7 @@ export const isErrorSpan = span => isErrorBool(span) || isErrorStr(span); * the parent span will be checked. * @return {boolean} Returns `true` if a descendant contains an error tag. */ -export function spanContainsErredSpan(spans, parentSpanIndex) { +export function spanContainsErredSpan(spans: Span[], parentSpanIndex: number) { const { depth } = spans[parentSpanIndex]; let i = parentSpanIndex + 1; for (; i < spans.length && spans[i].depth > depth; i++) { @@ -86,7 +103,7 @@ export function spanContainsErredSpan(spans, parentSpanIndex) { /** * Expects the first span to be the parent span. */ -export function findServerChildSpan(spans) { +export function findServerChildSpan(spans: Span[]) { if (spans.length <= 1 || !isClientSpan(spans[0])) { return false; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.test.js index c8961f5e2b..c68d845dc0 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/utils.test.js @@ -14,7 +14,7 @@ import { findServerChildSpan, - getViewedBounds, + createViewedBoundsFunc, isClientSpan, isErrorSpan, isServerSpan, @@ -27,29 +27,29 @@ import traceGenerator from '../../../demo/trace-generators'; describe('TraceTimelineViewer/utils', () => { describe('getViewedBounds()', () => { it('works for the full range', () => { - const args = { min: 1, max: 2, start: 1, end: 2, viewStart: 0, viewEnd: 1 }; - const { start, end } = getViewedBounds(args); + const args = { min: 1, max: 2, viewStart: 0, viewEnd: 1 }; + const { start, end } = createViewedBoundsFunc(args)(1, 2); expect(start).toBe(0); expect(end).toBe(1); }); it('works for a sub-range with a full view', () => { - const args = { min: 1, max: 2, start: 1.25, end: 1.75, viewStart: 0, viewEnd: 1 }; - const { start, end } = getViewedBounds(args); + const args = { min: 1, max: 2, viewStart: 0, viewEnd: 1 }; + const { start, end } = createViewedBoundsFunc(args)(1.25, 1.75); expect(start).toBe(0.25); expect(end).toBe(0.75); }); it('works for a sub-range that fills the view', () => { - const args = { min: 1, max: 2, start: 1.25, end: 1.75, viewStart: 0.25, viewEnd: 0.75 }; - const { start, end } = getViewedBounds(args); + const args = { min: 1, max: 2, viewStart: 0.25, viewEnd: 0.75 }; + const { start, end } = createViewedBoundsFunc(args)(1.25, 1.75); expect(start).toBe(0); expect(end).toBe(1); }); it('works for a sub-range that within a sub-view', () => { - const args = { min: 100, max: 200, start: 130, end: 170, viewStart: 0.1, viewEnd: 0.9 }; - const { start, end } = getViewedBounds(args); + const args = { min: 100, max: 200, viewStart: 0.1, viewEnd: 0.9 }; + const { start, end } = createViewedBoundsFunc(args)(130, 170); expect(start).toBe(0.25); expect(end).toBe(0.75); });