Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding the ability to import traces from JSON into zipkin-ui #1884

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public ResponseEntity<Resource> serveIndex() {
// If the path is a a file w/an extension, treat normally.
// Otherwise instead of returning 404, forward to the index.
// See https://github.com/twitter/finatra/blob/458c6b639c3afb4e29873d123125eeeb2b02e2cd/http/src/main/scala/com/twitter/finatra/http/response/ResponseBuilder.scala#L321
@RequestMapping(value = {"/zipkin/", "/zipkin/traces/{id}", "/zipkin/dependency"}, method = GET)
@RequestMapping(value = {"/zipkin/", "/zipkin/traces/{id}", "/zipkin/dependency", "/zipkin/traceViewer"}, method = GET)
public ModelAndView forwardUiEndpoints() {
return new ModelAndView("forward:/zipkin/index.html");
}
Expand Down
73 changes: 73 additions & 0 deletions zipkin-ui/js/component_ui/uploadTrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {component} from 'flightjs';
import FullPageSpinnerUI from '../component_ui/fullPageSpinner';
import traceToMustache from '../../js/component_ui/traceToMustache';
import {SPAN_V1} from '../spanConverter';

function rootToFrontComparator(span1/* , span2*/) {
return span1.parentId === undefined ? -1 : 0;
}

function sort(trace) {
if (trace != null) {
trace.sort(rootToFrontComparator);
}
}

function ensureV1(trace) {
if (trace == null || trace.length === 0
|| (trace[0].localEndpoint === undefined && trace[0].remoteEndpoint === undefined)) {
return trace;
}

const newTrace = [];
for (let i = 0; i < trace.length; i++) {
newTrace.push(SPAN_V1.convert(trace[i]));
}

return newTrace;
}

export default component(function uploadTrace() {
this.doUpload = function() {
const files = this.node.files;
if (files.length === 0) {
return;
}

const reader = new FileReader();
reader.onload = evt => {
let model;
try {
let trace = JSON.parse(evt.target.result);
trace = ensureV1(trace);
sort(trace);

const modelview = traceToMustache(trace);
model = {modelview, trace};
} catch (e) {
this.trigger('uiServerError',
{desc: 'Cannot parse file', message: e});
throw e;
} finally {
this.trigger(document, 'uiHideFullPageSpinner');
}

this.trigger(document, 'traceViewerPageModelView', model);
};

reader.onerror = evt => {
this.trigger(document, 'uiHideFullPageSpinner');
this.trigger('uiServerError',
{desc: 'Cannot load file', message: `${evt.target.error.name}`});
};

this.trigger(document, 'uiShowFullPageSpinner');
setTimeout(() => reader.readAsText(files[0]), 0);
};

this.after('initialize', function() {
this.on('change', this.doUpload);
FullPageSpinnerUI.teardownAll();
FullPageSpinnerUI.attachTo('#fullPageSpinner');
});
});
2 changes: 2 additions & 0 deletions zipkin-ui/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {compose, registry, advice, debug} from 'flightjs';
import crossroads from 'crossroads';
import initializeDefault from './page/default';
import initializeTrace from './page/trace';
import initializeTraceViewer from './page/traceViewer';
import initializeDependency from './page/dependency';
import CommonUI from './page/common';
import loadConfig from './config';
Expand All @@ -21,6 +22,7 @@ loadConfig().then(config => {

crossroads.addRoute(contextRoot, () => initializeDefault(config));
crossroads.addRoute(`${contextRoot}traces/{id}`, traceId => initializeTrace(traceId, config));
crossroads.addRoute(`${contextRoot}traceViewer`, () => initializeTraceViewer(config));
crossroads.addRoute(`${contextRoot}dependency`, () => initializeDependency(config));
crossroads.parse(window.location.pathname);
}, e => {
Expand Down
75 changes: 75 additions & 0 deletions zipkin-ui/js/page/traceViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {component} from 'flightjs';
import $ from 'jquery';
import FilterAllServicesUI from '../component_ui/filterAllServices';
import JsonPanelUI from '../component_ui/jsonPanel';
import ServiceFilterSearchUI from '../component_ui/serviceFilterSearch';
import SpanPanelUI from '../component_ui/spanPanel';
import TraceUI from '../component_ui/trace';
import FilterLabelUI from '../component_ui/filterLabel';
import ZoomOut from '../component_ui/zoomOutSpans';
import UploadTraceUI from '../component_ui/uploadTrace';
import {traceViewerTemplate} from '../templates';
import {contextRoot} from '../publicPath';

const TraceViewerPageComponent = component(function TraceViewerPage() {
this.render = function(model) {
try {
this.$node.html(traceViewerTemplate({
contextRoot,
...model
}));
} catch (e) {
this.trigger('uiServerError',
{desc: 'Failed to render template', message: e.message});
}

UploadTraceUI.attachTo('#traceFile');
};

this.attach = function() {
FilterAllServicesUI.attachTo('#filterAllServices', {
totalServices: $('.trace-details.services span').length
});
JsonPanelUI.attachTo('#jsonPanel');
ServiceFilterSearchUI.attachTo('#serviceFilterSearch');
SpanPanelUI.attachTo('#spanPanel');
TraceUI.attachTo('#trace-container');
FilterLabelUI.attachTo('.service-filter-label');
ZoomOut.attachTo('#zoomOutSpans');
};

this.teardown = function() {
ZoomOut.teardownAll();
FilterLabelUI.teardownAll();
TraceUI.teardownAll();
SpanPanelUI.teardownAll();
ServiceFilterSearchUI.teardownAll();
JsonPanelUI.teardownAll();
FilterAllServicesUI.teardownAll();
};

this.after('initialize', function() {
window.document.title = 'Zipkin - Trace Viewer';

this.render({});

this.on(document, 'traceViewerPageModelView', function(ev, data) {
this.teardown();
this.render(data.modelview);
this.attach();

this.$node.find('#traceJsonLink').click(e => {
e.preventDefault();
this.trigger('uiRequestJsonPanel', {title: `Trace ${data.modelview.traceId}`,
obj: data.trace,
link: `${contextRoot}traceViewer`});
});

$('.annotation:not(.core)').tooltip({placement: 'left'});
});
});
});

export default function initializeTrace(config) {
TraceViewerPageComponent.attachTo('.content', {config});
}
114 changes: 114 additions & 0 deletions zipkin-ui/js/spanConverter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
function toV1Endpoint(endpoint) {
if (endpoint === undefined) {
return undefined;
}
const res = {
serviceName: endpoint.serviceName || '', // undefined is not allowed in v1
};
if (endpoint.ipv4) {
res.ipv4 = endpoint.ipv4;
}
if (endpoint.port) {
res.port = endpoint.port;
}
return res;
}

function toV1Annotation(ann, endpoint) {
return {
value: ann.value,
timestamp: ann.timestamp,
endpoint
};
}

// Copied from https://github.com/openzipkin/zipkin-js/blob/8018e441d01804b02d0d217f10cd82759e71e02a/packages/zipkin/src/jsonEncoder.js#L25
// Modified to correct assumption that 'annotations' always exist and ensure
// that 'beginAnnotation' comes first timestamp/duration should always be copied over
function convertV1(span) {
const res = {
traceId: span.traceId
};
if (span.parentId) { // instead of writing "parentId": NULL
res.parentId = span.parentId;
}
res.id = span.id;
res.name = span.name || ''; // undefined is not allowed in v1
res.timestamp = span.timestamp;
res.duration = span.duration;

const jsonEndpoint = toV1Endpoint(span.localEndpoint);

let beginAnnotation;
let endAnnotation;
let addressKey;
switch (span.kind) {
case 'CLIENT':
beginAnnotation = span.timestamp ? 'cs' : undefined;
endAnnotation = 'cr';
addressKey = 'sa';
break;
case 'SERVER':
beginAnnotation = span.timestamp ? 'sr' : undefined;
endAnnotation = 'ss';
addressKey = 'ca';
break;
default:
}

if (span.annotations !== undefined && span.annotations.length > 0
|| beginAnnotation) { // don't write empty array
res.annotations = [];
}

if (beginAnnotation) {
res.annotations.push({
value: beginAnnotation,
timestamp: span.timestamp,
endpoint: jsonEndpoint
});
}

if (span.annotations !== undefined && span.annotations.length > 0) {
span.annotations.forEach((ann) =>
res.annotations.push(toV1Annotation(ann, jsonEndpoint))
);
}

if (beginAnnotation && span.duration) {
res.annotations.push({
value: endAnnotation,
timestamp: span.timestamp + span.duration,
endpoint: jsonEndpoint
});
}

const keys = Object.keys(span.tags || {});
if (keys.length > 0 || span.remoteEndpoint) { // don't write empty array
res.binaryAnnotations = keys.map(key => ({
key,
value: span.tags[key],
endpoint: jsonEndpoint
}));
}

if (span.remoteEndpoint) {
const address = {
key: addressKey,
value: true,
endpoint: toV1Endpoint(span.remoteEndpoint)
};
res.binaryAnnotations.push(address);
}

if (span.debug) { // instead of writing "debug": false
res.debug = true;
}
return res;
}

module.exports.SPAN_V1 = {
convert(span) {
return convertV1(span);
}
};
1 change: 1 addition & 0 deletions zipkin-ui/js/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const searchDisabled = require('../templates/searchDisabled.mustache');
export const layoutTemplate = require('../templates/layout.mustache');
export const dependenciesTemplate = require('../templates/dependency.mustache');
export const traceTemplate = require('../templates/trace.mustache');
export const traceViewerTemplate = require('../templates/traceViewer.mustache');
3 changes: 2 additions & 1 deletion zipkin-ui/static/nav.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
nav.inves = Investigate system behavior
nav.find = Find a trace
nav.dep = Dependencies
nav.search = Go to trace
nav.search = Go to trace
nav.viewSaved = View Saved Trace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MrGlaucus @gianarb @drolando can you contribute chinese and italian text for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on the translation as the merge is done.

1 change: 1 addition & 0 deletions zipkin-ui/static/trace.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
trace.duration = Duration:
trace.file = Trace JSON:
trace.service = Services:
trace.depth = Depth:
trace.spans = Total Spans:
Expand Down
7 changes: 4 additions & 3 deletions zipkin-ui/templates/layout.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@
<div class="navbar-left">
<ul class="nav navbar-nav">
<li data-route="index"><a href="{{contextRoot}}" data-i18n="nav.find">Find a trace</a></li>
<li data-route="traceViewer"><a href="{{contextRoot}}traceViewer" data-i18n="nav.viewSaved">View Saved Trace</a></li>
<li data-route="dependency"><a href="{{contextRoot}}dependency/" data-i18n="nav.dep">Dependencies</a></li>
</ul>
</div>
<div class="navbar-right" style="width: 50%">
<div class="navbar-right" style="width: 30%">
<div class="navbar-right" style='padding-left: 15px'>
<p class="navbar-text muted" id="environment"></p>
</div>
<form id="traceIdQueryForm" class="form-inline" role="form">
<div class="navbar-right" style="width: 25%; float: right">
<div class="navbar-right" style="float: right">
<div class="row well-sm clearfix">
<input type="text" class="form-control input-sm" id="traceIdQuery" name="traceIdQuery" style="width: 100%" placeholder='Go to trace' data-i18n='nav.search'>
<input type="text" class="form-control input-sm" id="traceIdQuery" name="traceIdQuery" placeholder='Go to trace' data-i18n='nav.search'>
</div>
</div>
</form>
Expand Down
Loading