Skip to content

Commit

Permalink
Log Markers on Spans (#309)
Browse files Browse the repository at this point in the history
* Log Markers on Spans

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* Make tests pass with empty logs for now

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* Simple test that log markers are created

Signed-off-by: Staffan Friberg <sfriberg@medallia.com>

* Push trace separately all they way down to SpanBar

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* Send Span all the way to SpanBar instead of Logs

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* PR comments

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* Update changelog for PR 309

Signed-off-by: Joe Farro <joef@uber.com>

* CSS fix for logMarker hover at t0

Signed-off-by: Joe Farro <joef@uber.com>

* Disable clicking

Signed-off-by: Staffan Friberg <sfriberg@kth.se>

* Tweaks to UX for span log markers

Hover on log markers opens popover
Click on log markers toggles span details
Log marker popover is not interactive

Signed-off-by: Joe Farro <joef@uber.com>

* Fix changelog, missing "@ flow" and log-marker spacing

Signed-off-by: Joe Farro <joef@uber.com>
  • Loading branch information
sfriberg authored and tiffon committed Feb 21, 2019
1 parent 0992eb9 commit 2abe94e
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 82 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -26,21 +33,43 @@ type SpanBarProps = {
onClick: (SyntheticMouseEvent<any>) => void,
viewEnd: number,
viewStart: number,
getViewedBounds: ViewedBoundsFunctionType,
rpc: {
viewStart: number,
viewEnd: number,
color: string,
},
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 (
<div
Expand All @@ -61,6 +90,26 @@ function SpanBar(props: SpanBarProps) {
>
<div className={`SpanBar--label is-${hintSide}`}>{label}</div>
</div>
<div>
{Object.keys(logGroups).map(positionKey => (
<Popover
key={positionKey}
arrowPointAtCenter
overlayClassName="SpanBar--logHint"
placement="topLeft"
content={
<AccordianLogs
interactive={false}
isOpen
logs={logGroups[positionKey]}
timestamp={traceStartTime}
/>
}
>
<div className="SpanBar--logMarker" style={{ left: positionKey }} />
</Popover>
))}
</div>
{rpc && (
<div
className="SpanBar--rpc"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import React from 'react';
import { mount } from 'enzyme';
import { Popover } from 'antd';

import SpanBar from './SpanBar';

Expand All @@ -28,11 +29,40 @@ describe('<SpanBar>', () => {
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', () => {
Expand All @@ -46,4 +76,10 @@ describe('<SpanBar>', () => {
onMouseOut();
expect(labelElm.text()).toBe(shortLabel);
});

it('log markers count', () => {
// 3 log entries, two grouped together with the same timestamp
const wrapper = mount(<SpanBar {...props} />);
expect(wrapper.find(Popover).length).toEqual(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -46,9 +47,9 @@ type SpanBarRowProps = {
serviceName: string,
},
showErrorIcon: boolean,
getViewedBounds: ViewedBoundsFunctionType,
traceStartTime: number,
span: Span,
viewEnd: number,
viewStart: number,
};

/**
Expand Down Expand Up @@ -86,12 +87,15 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
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;
Expand All @@ -103,6 +107,7 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
longLabel = `${label} | ${labelDetail}`;
hintSide = 'right';
}

return (
<TimelineRow
className={`
Expand Down Expand Up @@ -155,10 +160,13 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
rpc={rpc}
viewStart={viewStart}
viewEnd={viewEnd}
getViewedBounds={getViewedBounds}
color={color}
shortLabel={label}
longLabel={longLabel}
hintSide={hintSide}
traceStartTime={traceStartTime}
span={span}
/>
</TimelineRow.Cell>
</TimelineRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ describe('<SpanBarRow>', () => {
serviceName: 'rpc-service-name',
},
showErrorIcon: false,
getViewedBounds: () => ({ start: 0, end: 1 }),
span: {
duration: 'test-duration',
hasChildren: true,
process: {
serviceName: 'service-name',
},
spanID,
logs: [],
},
viewEnd: 1,
viewStart: 0,
};

let wrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -61,21 +62,30 @@ 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 ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />;
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}

return (
<div className={cx(className, 'u-tx-ellipsis')}>
<div
className={cx('AccordianKeyValues--header', {
'is-empty': isEmpty,
'is-high-contrast': highContrast,
})}
aria-checked={isOpen}
onClick={isEmpty ? null : onToggle}
role="switch"
{...headerProps}
>
{isOpen ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />}
{arrow}
<strong data-test={markers.LABEL}>
{label}
{isOpen || ':'}
Expand All @@ -90,4 +100,6 @@ export default function AccordianKeyValues(props: AccordianKeyValuesProps) {
AccordianKeyValues.defaultProps = {
className: null,
highContrast: false,
interactive: true,
onToggle: null,
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

.AccordianLogs {
border: 1px solid #d8d8d8;
position: relative;
}

.AccordianLogs--header {
Expand Down
Loading

0 comments on commit 2abe94e

Please sign in to comment.