From f0b5155ab84a5e46e094794847cc5faba4f76a92 Mon Sep 17 00:00:00 2001 From: Daniele Rolando Date: Mon, 9 Mar 2020 23:58:54 -0700 Subject: [PATCH] Add Archive Trace button This button lets you easily reupload the current trace to a different server. The main motivation for having this is that you can have the archival server have a very long retention period and use it as very long term storage for traces that you care about. For example when sharing a trace in a jira ticket since otherwise the link would expire after 1 week. Design doc explaining the reason behind this in more details and why we went with this implementation: https://github.com/openzipkin/openzipkin.github.io/wiki/Favorite-trace --- .../TracePage/TraceSummaryHeader.jsx | 80 +++++++++++++++++++ .../TracePage/TraceSummaryHeader.test.jsx | 40 ++++++++++ zipkin-lens/src/translations/en/messages.json | 1 + zipkin-lens/src/translations/es/messages.json | 1 + .../src/translations/zh-cn/messages.json | 1 + zipkin-lens/src/zipkin/trace.js | 9 ++- zipkin-server/README.md | 2 + .../internal/ui/ZipkinUiConfiguration.java | 2 + .../internal/ui/ZipkinUiProperties.java | 24 ++++++ .../internal/ui/ITZipkinUiConfiguration.java | 2 + .../ui/ZipkinUiConfigurationTest.java | 16 ++++ 11 files changed, 177 insertions(+), 1 deletion(-) diff --git a/zipkin-lens/src/components/TracePage/TraceSummaryHeader.jsx b/zipkin-lens/src/components/TracePage/TraceSummaryHeader.jsx index 62fb65c07c8..ee109d6ca36 100644 --- a/zipkin-lens/src/components/TracePage/TraceSummaryHeader.jsx +++ b/zipkin-lens/src/components/TracePage/TraceSummaryHeader.jsx @@ -11,6 +11,7 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ +/* eslint-disable no-alert */ import { t, Trans } from '@lingui/macro'; import { useLingui } from '@lingui/react'; import PropTypes from 'prop-types'; @@ -105,6 +106,67 @@ const TraceSummaryHeader = React.memo(({ traceSummary, rootSpanIndex }) => { ? config.logsUrl.replace('{traceId}', traceSummary.traceId) : undefined; + const archivePostUrl = + config.archivePostUrl && traceSummary ? config.archivePostUrl : undefined; + + const archiveUrl = + config.archiveUrl && traceSummary + ? config.archiveUrl.replace('{traceId}', traceSummary.traceId) + : undefined; + + const archiveClick = useCallback(() => { + // We don't store the raw json in the browser yet, so we need to make an + // HTTP call to retrieve it again. + fetch(`${api.TRACE}/${traceSummary.traceId}`) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch trace from backend'); + } + return response.json(); + }) + .then((json) => { + // Add zipkin.archived tag to root span + /* eslint-disable-next-line no-restricted-syntax */ + for (const span of json) { + if ('parentId' in span === false) { + const tags = span.tags || {}; + tags['zipkin.archived'] = 'true'; + span.tags = tags; + break; + } + } + + fetch(archivePostUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(json), + }) + .then((response) => { + if ( + !response.ok || + (response.status !== 202 && response.status === 200) + ) { + throw new Error('Failed to archive the trace'); + } + if (archiveUrl) { + alert( + `Archive successful! This trace is now accessible at ${archiveUrl}`, + ); + } else { + alert(`Archive successful!`); + } + }) + .catch(() => { + alert('Failed to archive the trace'); + }); + }) + .catch(() => { + alert('Failed to fetch trace from backend'); + }); + }, [archivePostUrl, archiveUrl, traceSummary]); + const handleSaveButtonClick = useCallback(() => { if (!traceSummary || !traceSummary.traceId) { return; @@ -207,6 +269,24 @@ const TraceSummaryHeader = React.memo(({ traceSummary, rootSpanIndex }) => { )} + {archivePostUrl && ( + + + + )} diff --git a/zipkin-lens/src/components/TracePage/TraceSummaryHeader.test.jsx b/zipkin-lens/src/components/TracePage/TraceSummaryHeader.test.jsx index 1016464eb75..60bf705e77b 100644 --- a/zipkin-lens/src/components/TracePage/TraceSummaryHeader.test.jsx +++ b/zipkin-lens/src/components/TracePage/TraceSummaryHeader.test.jsx @@ -62,4 +62,44 @@ describe('', () => { expect(logsLink.target).toEqual('_blank'); expect(logsLink.rel).toEqual('noopener'); }); + + it('does not render Archive Trace link with default config', () => { + const { queryByTestId } = render( + , + ); + expect(queryByTestId('archive-trace-link')).not.toBeInTheDocument(); + }); + + it('does render Archive Trace link when logs URL in config', () => { + const { queryByTestId } = render( + , + { uiConfig: { archivePostUrl: 'http://localhost:9411/api/v2/spans' } }, + ); + const logsLink = queryByTestId('archive-trace-link'); + expect(logsLink).toBeInTheDocument(); + }); }); diff --git a/zipkin-lens/src/translations/en/messages.json b/zipkin-lens/src/translations/en/messages.json index 2985bad8801..fd5657be4dc 100644 --- a/zipkin-lens/src/translations/en/messages.json +++ b/zipkin-lens/src/translations/en/messages.json @@ -36,6 +36,7 @@ "Trace ID": "", "Upload JSON": "", "View Logs": "", + "Archive Trace": "", "Zipkin Home": "", "hide annotations": "", "show all annotations": "", diff --git a/zipkin-lens/src/translations/es/messages.json b/zipkin-lens/src/translations/es/messages.json index d28be10a9b5..8328f16a2b8 100644 --- a/zipkin-lens/src/translations/es/messages.json +++ b/zipkin-lens/src/translations/es/messages.json @@ -36,6 +36,7 @@ "Trace ID": "ID de Traza", "Upload JSON": "Subir JSON", "View Logs": "Ver Logs", + "Archive Trace": "", "Zipkin Home": "Zipkin Inicio", "hide annotations": "ocultar anotaciones", "show all annotations": "mostrar todas las anotaciones", diff --git a/zipkin-lens/src/translations/zh-cn/messages.json b/zipkin-lens/src/translations/zh-cn/messages.json index 1700952a04b..ad39d1e9450 100644 --- a/zipkin-lens/src/translations/zh-cn/messages.json +++ b/zipkin-lens/src/translations/zh-cn/messages.json @@ -36,6 +36,7 @@ "Trace ID": "", "Upload JSON": "", "View Logs": "", + "Archive Trace": "", "Zipkin Home": "", "hide annotations": "", "show all annotations": "", diff --git a/zipkin-lens/src/zipkin/trace.js b/zipkin-lens/src/zipkin/trace.js index dec760c74dd..43237a75ee7 100644 --- a/zipkin-lens/src/zipkin/trace.js +++ b/zipkin-lens/src/zipkin/trace.js @@ -291,7 +291,12 @@ function addLayoutDetails( } } -export function detailedTraceSummary(root, logsUrl) { +export function detailedTraceSummary( + root, + logsUrl, + archivePostUrl, + archiveUrl, +) { const serviceNameToCount = {}; let queue = root.queueRootMostSpans(); const modelview = { @@ -381,6 +386,8 @@ export function detailedTraceSummary(root, logsUrl) { modelview.duration = duration; modelview.durationStr = mkDurationStr(duration); if (logsUrl) modelview.logsUrl = logsUrl; + if (archivePostUrl) modelview.archivePostUrl = archivePostUrl; + if (archiveUrl) modelview.archiveUrl = archiveUrl; return modelview; } diff --git a/zipkin-server/README.md b/zipkin-server/README.md index 8164c6fd985..ed980ab996c 100644 --- a/zipkin-server/README.md +++ b/zipkin-server/README.md @@ -177,6 +177,8 @@ queryLimit | zipkin.ui.query-limit | Default limit for Find Traces. Defaults to instrumented | zipkin.ui.instrumented | Which sites this Zipkin UI covers. Regex syntax. e.g. `http:\/\/example.com\/.*` Defaults to match all websites (`.*`). logsUrl | zipkin.ui.logs-url | Logs query service url pattern. If specified, a button will appear on the trace page and will replace {traceId} in the url by the traceId. Not required. supportUrl / zipkin.ui.support-url / A URL where a user can ask for support. If specified, a link will be placed in the side menu to this URL, for example a page to file support tickets. Not required. +archivePostUrl | zipkin.ui.archive-post-url | Url to POST the current trace in Zipkin v2 json format. e.g. 'https://longterm/api/v2/spans'. If specified, a button will appear on the trace page accordingly. Not required. +archiveUrl | zipkin.ui.archive-url | Url to a web application serving an archived trace, templated by '{traceId}'. e.g. https://longterm/zipkin/trace/{traceId}'. This is shown in a confirmation message after a trace is successfully POSTed to the `archivePostUrl`. Not required. dependency.lowErrorRate | zipkin.ui.dependency.low-error-rate | The rate of error calls on a dependency link that turns it yellow. Defaults to 0.5 (50%) set to >1 to disable. dependency.highErrorRate | zipkin.ui.dependency.high-error-rate | The rate of error calls on a dependency link that turns it red. Defaults to 0.75 (75%) set to >1 to disable. basePath | zipkin.ui.basepath | path prefix placed into the tag in the UI HTML; useful when running behind a reverse proxy. Default "/zipkin" diff --git a/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java b/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java index 16f50d4e28a..e17a1dfd164 100644 --- a/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java +++ b/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiConfiguration.java @@ -141,6 +141,8 @@ static String writeConfig(ZipkinUiProperties ui) throws IOException { generator.writeBooleanField("searchEnabled", ui.isSearchEnabled()); generator.writeStringField("logsUrl", ui.getLogsUrl()); generator.writeStringField("supportUrl", ui.getSupportUrl()); + generator.writeStringField("archivePostUrl", ui.getArchivePostUrl()); + generator.writeStringField("archiveUrl", ui.getArchiveUrl()); generator.writeObjectFieldStart("dependency"); generator.writeNumberField("lowErrorRate", ui.getDependency().getLowErrorRate()); generator.writeNumberField("highErrorRate", ui.getDependency().getHighErrorRate()); diff --git a/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiProperties.java b/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiProperties.java index 3dfbe72c0d3..0212a2d275c 100644 --- a/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiProperties.java +++ b/zipkin-server/src/main/java/zipkin2/server/internal/ui/ZipkinUiProperties.java @@ -28,6 +28,8 @@ class ZipkinUiProperties { private String instrumented = ".*"; private String logsUrl = null; private String supportUrl = null; + private String archivePostUrl = null; + private String archiveUrl = null; private String basepath = DEFAULT_BASEPATH; private boolean searchEnabled = true; private Dependency dependency = new Dependency(); @@ -68,6 +70,15 @@ public String getLogsUrl() { return logsUrl; } + public String getArchivePostUrl() { + return archivePostUrl; + } + + + public String getArchiveUrl() { + return archiveUrl; + } + public void setLogsUrl(String logsUrl) { if (!StringUtils.isEmpty(logsUrl)) { this.logsUrl = logsUrl; @@ -82,6 +93,19 @@ public void setSupportUrl(String supportUrl) { if (!StringUtils.isEmpty(supportUrl)) { this.supportUrl = supportUrl; } + + } + + public void setArchivePostUrl(String archivePostUrl) { + if (!StringUtils.isEmpty(archivePostUrl)) { + this.archivePostUrl = archivePostUrl; + } + } + + public void setArchiveUrl(String archiveUrl) { + if (!StringUtils.isEmpty(archiveUrl)) { + this.archiveUrl = archiveUrl; + } } public boolean isSearchEnabled() { diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java index 005e922613e..7a3a8865251 100644 --- a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java +++ b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java @@ -60,6 +60,8 @@ public class ITZipkinUiConfiguration { + " \"searchEnabled\" : true,\n" + " \"logsUrl\" : null,\n" + " \"supportUrl\" : null,\n" + + " \"archivePostUrl\" : null,\n" + + " \"archiveUrl\" : null,\n" + " \"dependency\" : {\n" + " \"lowErrorRate\" : 0.5,\n" + " \"highErrorRate\" : 0.75\n" diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java index f3dfdadb6cd..2071d544e40 100644 --- a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java +++ b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java @@ -82,6 +82,22 @@ public void canOverrideProperty_logsUrl() { assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isEqualTo(url); } + @Test + public void canOverrideProperty_archivePostUrl() { + final String url = "http://zipkin.archive.com/api/v2/spans"; + context = createContextWithOverridenProperty("zipkin.ui.archive-post-url:" + url); + + assertThat(context.getBean(ZipkinUiProperties.class).getArchivePostUrl()).isEqualTo(url); + } + + @Test + public void canOverrideProperty_archiveUrl() { + final String url = "http://zipkin.archive.com/zipkin/traces/{traceId}"; + context = createContextWithOverridenProperty("zipkin.ui.archive-url:" + url); + + assertThat(context.getBean(ZipkinUiProperties.class).getArchiveUrl()).isEqualTo(url); + } + @Test public void canOverrideProperty_supportUrl() { final String url = "http://mycompany.com/file-a-bug";