From 19b644cbd629cbaa84e53ae69631d113a7a600c7 Mon Sep 17 00:00:00 2001 From: Logic-32 <25107222+Logic-32@users.noreply.github.com> Date: Wed, 17 Jan 2018 10:57:16 -0700 Subject: [PATCH 1/2] Adding the ability to import traces from JSON into zipkin-ui. Resized navbar to accomodate new link. Adding configuration to forward requests to traceViewer Updated traceViewer to support v2 traces by recoding in v1 format. Add error handling in case JSON parsing fails. Renamed feature to 'View Saved Trace'. Added il8n properties. Extracted span conversion into it's own file so it could be tested more easily. Fixed missing 'contextPath' issue. --- .../ui/ZipkinUiAutoConfiguration.java | 2 +- zipkin-ui/js/component_ui/uploadTrace.js | 62 ++++ zipkin-ui/js/main.js | 2 + zipkin-ui/js/page/traceViewer.js | 75 +++++ zipkin-ui/js/spanConverter.js | 114 ++++++++ zipkin-ui/js/templates.js | 1 + zipkin-ui/static/nav.properties | 3 +- zipkin-ui/static/trace.properties | 1 + zipkin-ui/templates/layout.mustache | 7 +- zipkin-ui/templates/traceViewer.mustache | 250 ++++++++++++++++ zipkin-ui/test/spanConverter.test.js | 269 ++++++++++++++++++ 11 files changed, 781 insertions(+), 5 deletions(-) create mode 100644 zipkin-ui/js/component_ui/uploadTrace.js create mode 100644 zipkin-ui/js/page/traceViewer.js create mode 100644 zipkin-ui/js/spanConverter.js create mode 100644 zipkin-ui/templates/traceViewer.mustache create mode 100644 zipkin-ui/test/spanConverter.test.js diff --git a/zipkin-autoconfigure/ui/src/main/java/zipkin/autoconfigure/ui/ZipkinUiAutoConfiguration.java b/zipkin-autoconfigure/ui/src/main/java/zipkin/autoconfigure/ui/ZipkinUiAutoConfiguration.java index a8e109431b4..a1c85bea44f 100644 --- a/zipkin-autoconfigure/ui/src/main/java/zipkin/autoconfigure/ui/ZipkinUiAutoConfiguration.java +++ b/zipkin-autoconfigure/ui/src/main/java/zipkin/autoconfigure/ui/ZipkinUiAutoConfiguration.java @@ -129,7 +129,7 @@ public ResponseEntity 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"); } diff --git a/zipkin-ui/js/component_ui/uploadTrace.js b/zipkin-ui/js/component_ui/uploadTrace.js new file mode 100644 index 00000000000..f75dbb7fba8 --- /dev/null +++ b/zipkin-ui/js/component_ui/uploadTrace.js @@ -0,0 +1,62 @@ +import {component} from 'flightjs'; +import FullPageSpinnerUI from '../component_ui/fullPageSpinner'; +import traceToMustache from '../../js/component_ui/traceToMustache'; +import {SPAN_V1} from '../spanConverter'; + +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); + + 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'); + }); +}); diff --git a/zipkin-ui/js/main.js b/zipkin-ui/js/main.js index 9141dd27128..c9593803f4b 100644 --- a/zipkin-ui/js/main.js +++ b/zipkin-ui/js/main.js @@ -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'; @@ -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 => { diff --git a/zipkin-ui/js/page/traceViewer.js b/zipkin-ui/js/page/traceViewer.js new file mode 100644 index 00000000000..31a17da1c00 --- /dev/null +++ b/zipkin-ui/js/page/traceViewer.js @@ -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}); +} diff --git a/zipkin-ui/js/spanConverter.js b/zipkin-ui/js/spanConverter.js new file mode 100644 index 00000000000..0ee52ef9e12 --- /dev/null +++ b/zipkin-ui/js/spanConverter.js @@ -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); + } +}; diff --git a/zipkin-ui/js/templates.js b/zipkin-ui/js/templates.js index 22c77a4d6b0..d789e0c3f08 100644 --- a/zipkin-ui/js/templates.js +++ b/zipkin-ui/js/templates.js @@ -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'); diff --git a/zipkin-ui/static/nav.properties b/zipkin-ui/static/nav.properties index 19d540656a1..dbb83c333e5 100644 --- a/zipkin-ui/static/nav.properties +++ b/zipkin-ui/static/nav.properties @@ -1,4 +1,5 @@ nav.inves = Investigate system behavior nav.find = Find a trace nav.dep = Dependencies -nav.search = Go to trace \ No newline at end of file +nav.search = Go to trace +nav.viewSaved = View Saved Trace \ No newline at end of file diff --git a/zipkin-ui/static/trace.properties b/zipkin-ui/static/trace.properties index 92c07868a51..8d7abbc3df2 100644 --- a/zipkin-ui/static/trace.properties +++ b/zipkin-ui/static/trace.properties @@ -1,4 +1,5 @@ trace.duration = Duration: +trace.file = Trace JSON: trace.service = Services: trace.depth = Depth: trace.spans = Total Spans: diff --git a/zipkin-ui/templates/layout.mustache b/zipkin-ui/templates/layout.mustache index 446a2215d67..ae5752f0a31 100644 --- a/zipkin-ui/templates/layout.mustache +++ b/zipkin-ui/templates/layout.mustache @@ -8,17 +8,18 @@ -