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

Injects configurable <base> tag to support reverse proxies #1946

Merged
merged 5 commits into from
Mar 14, 2018
Merged
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
6 changes: 6 additions & 0 deletions zipkin-autoconfigure/ui/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,11 @@
<groupId>${project.groupId}</groupId>
<artifactId>zipkin-ui</artifactId>
</dependency>
<dependency>
<!-- HTML library for injecting configurable <base> tag -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.2</version>
Copy link
Member

Choose a reason for hiding this comment

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

license is MIT (ok) and afaict no deps (good!)

</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2015-2017 The OpenZipkin Authors
* Copyright 2015-2018 The OpenZipkin Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
Expand All @@ -14,15 +14,19 @@
package zipkin.autoconfigure.ui;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
Expand All @@ -40,6 +44,7 @@

import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.servlet.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
import static zipkin.autoconfigure.ui.ZipkinUiProperties.DEFAULT_BASEPATH;

/**
* Zipkin-UI is a single-page application mounted at /zipkin. For simplicity, assume paths mentioned
Expand Down Expand Up @@ -75,6 +80,23 @@ public class ZipkinUiAutoConfiguration extends WebMvcConfigurerAdapter {
@Value("classpath:zipkin-ui/index.html")
Resource indexHtml;

@Bean
@Lazy
String processedIndexHtml() throws IOException {
String baseTagValue = "/".equals(ui.getBasepath()) ? "/" : ui.getBasepath() + "/";
Document soup;
try (InputStream is = indexHtml.getInputStream()) {
soup = Jsoup.parse(is, null, baseTagValue);
}
if (soup.head().getElementsByTag("base").isEmpty()) {
soup.head().appendChild(
soup.createElement("base")
);
}
soup.head().getElementsByTag("base").attr("href", baseTagValue);
return soup.html();
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/zipkin/**")
Expand Down Expand Up @@ -115,10 +137,13 @@ public ResponseEntity<ZipkinUiProperties> serveUiConfig() {
}

@RequestMapping(value = "/zipkin/index.html", method = GET)
public ResponseEntity<Resource> serveIndex() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES))
.body(indexHtml);
public ResponseEntity<?> serveIndex() throws IOException {
ResponseEntity.BodyBuilder result = ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES))
.contentType(MediaType.TEXT_HTML);
return DEFAULT_BASEPATH.equals(ui.getBasepath())
? result.body(indexHtml)
: result.body(processedIndexHtml());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@

@ConfigurationProperties("zipkin.ui")
public class ZipkinUiProperties {
static final String DEFAULT_BASEPATH = "/zipkin";

private String environment;
private int queryLimit = 10;
private int defaultLookback = (int) TimeUnit.DAYS.toMillis(7);
private String instrumented = ".*";
private String logsUrl = null;
private String basepath = DEFAULT_BASEPATH;
private boolean searchEnabled = true;
private Dependency dependency = new Dependency();

Expand Down Expand Up @@ -85,6 +88,14 @@ public void setDependency(Dependency dependency) {
this.dependency = dependency;
}

public String getBasepath() {
return basepath;
}

public void setBasepath(String basepath) {
this.basepath = basepath;
}

public static class Dependency {
private float lowErrorRate = 0.5f; // 50% of calls in error turns line yellow
private float highErrorRate = 0.75f; // 75% of calls in error turns line red
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@
*/
package zipkin.autoconfigure.ui;

import java.io.IOException;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.internal.matchers.ThrowableCauseMatcher.hasCause;
import static org.springframework.boot.test.util.EnvironmentTestUtils.addEnvironment;

public class ZipkinUiAutoConfigurationTest {
Expand All @@ -33,26 +42,52 @@ public void close() {
}
}

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void indexHtmlFromClasspath() {
context = createContext();

assertThat(context.getBean(ZipkinUiAutoConfiguration.class).indexHtml)
.isNotNull();
.isNotNull();
}

@Test
public void indexContentType() throws IOException {
context = createContext();
assertThat(
context.getBean(ZipkinUiAutoConfiguration.class).serveIndex().getHeaders().getContentType())
.isEqualTo(MediaType.TEXT_HTML);
}

@Test
public void invalidIndexHtml() throws IOException {
// I failed to make Jsoup barf, even on nonsense like: "<head wait no I changed my mind this HTML is totally invalid <<<<<<<<<<<"
// So let's just run with a case where the file doesn't exist
context = createContextWithOverridenProperty("zipkin.ui.basepath:/foo/bar");
ZipkinUiAutoConfiguration ui = context.getBean(ZipkinUiAutoConfiguration.class);
ui.indexHtml = new ClassPathResource("does-not-exist.html");

thrown.expect(BeanCreationException.class);
// There's a BeanInstantiationException nested in between BeanCreationException and IOException,
// so we go one level deeper about causes. There's no `expectRootCause`.
thrown.expectCause(hasCause(isA(IOException.class)));
ui.serveIndex();
}

@Test
public void canOverridesProperty_defaultLookback() {
context = createContextWithOverridenProperty("zipkin.ui.defaultLookback:100");

assertThat(context.getBean(ZipkinUiProperties.class).getDefaultLookback())
.isEqualTo(100);
.isEqualTo(100);
}

@Test
public void canOverrideProperty_logsUrl() {
final String url = "http://mycompany.com/kibana";
context = createContextWithOverridenProperty("zipkin.ui.logs-url:"+ url);
context = createContextWithOverridenProperty("zipkin.ui.logs-url:" + url);

assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isEqualTo(url);
}
Expand All @@ -76,7 +111,6 @@ public void canOverridesProperty_disable() {
context = createContextWithOverridenProperty("zipkin.ui.enabled:false");

context.getBean(ZipkinUiProperties.class);

}

@Test
Expand All @@ -102,14 +136,41 @@ public void canOverrideProperty_dependencyHighErrorRate() {
.isEqualTo(0.1f);
}

@Test
public void defaultBaseUrl_doesNotChangeResource() throws IOException {
context = createContext();
Resource index =
(Resource) context.getBean(ZipkinUiAutoConfiguration.class).serveIndex().getBody();

assertThat(index.getInputStream())
.hasSameContentAs(getClass().getResourceAsStream("/zipkin-ui/index.html"));
}

@Test
public void canOverideProperty_basePath() throws IOException {
context = createContextWithOverridenProperty("zipkin.ui.basepath:/foo/bar");

assertThat(context.getBean(ZipkinUiAutoConfiguration.class).serveIndex().getBody().toString())
.contains("<base href=\"/foo/bar/\">");
}

@Test
public void canOverideProperty_specialCaseRoot() throws IOException {
context = createContextWithOverridenProperty("zipkin.ui.basepath:/");

assertThat(context.getBean(ZipkinUiAutoConfiguration.class).serveIndex().getBody().toString())
.contains("<base href=\"/\">");
}

private static AnnotationConfigApplicationContext createContext() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiAutoConfiguration.class);
context.refresh();
return context;
}

private static AnnotationConfigApplicationContext createContextWithOverridenProperty(String pair) {
private static AnnotationConfigApplicationContext createContextWithOverridenProperty(
String pair) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
addEnvironment(context, pair);
context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiAutoConfiguration.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<html>
Copy link
Member

Choose a reason for hiding this comment

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

clever

<head></head>
<body>index.html</body>
</html>
1 change: 1 addition & 0 deletions zipkin-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ instrumented | zipkin.ui.instrumented | Which sites this Zipkin UI covers. Regex
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.
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 <base> tag in the UI HTML; useful when running behind a reverse proxy. Default "/zipkin"

For example, if using docker you can set `ZIPKIN_UI_QUERY_LIMIT=100` to affect `$.queryLimit` in `/config.json`.

Expand Down
2 changes: 2 additions & 0 deletions zipkin-server/src/main/resources/zipkin-server-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ zipkin:
# - .*example2.com
# Default is "match all websites"
instrumented: .*
# URL placed into the <base> tag in the HTML
base-path: /zipkin/

server:
port: ${QUERY_PORT:9411}
Expand Down
12 changes: 6 additions & 6 deletions zipkin-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,12 @@ To disable coloring of lines, set both rates to a number higher than 1.

## Running behind a reverse proxy
Starting with Zipkin `1.31.2`, Zipkin UI supports running under an arbitrary _context root_. As a result, it can be proxied
under a different path than `/zipkin/` such as `/proxy/foo/bar/zipkin/`.
under a different path than `/zipkin/` such as `/proxy/foo/bar/zipkin/`.

> Note that Zipkin requires the last path segment to be `zipkin`.

> Also note that due to `html-webpack-plugin` limitations, Zipkin UI relies on a
[`base` tag](https://www.w3schools.com/TAgs/tag_base.asp) and its `href` attribute to be set in the `index.html` file.
> Also note that due to `html-webpack-plugin` limitations, Zipkin UI relies on a
[`base` tag](https://www.w3schools.com/TAgs/tag_base.asp) and its `href` attribute to be set in the `index.html` file.
By default its value is `/zipkin/` and as such the reverse proxy must rewrite the value to an alternate _context root_.

### Apache HTTP as a Zipkin reverse proxy
Expand All @@ -132,7 +132,7 @@ ProxyPass /proxy/foo/bar/ http://localhost:9411/
SetOutputFilter proxy-html
ProxyHTMLURLMap /zipkin/ /proxy/foo/bar/zipkin/
ProxyHTMLLinks base href
```
```

To access Zipkin UI behind the reverse proxy, execute:
```bash
Expand All @@ -143,7 +143,7 @@ $ curl http://localhost/proxy/foo/bar/zipkin/
--><base href="/proxy/foo/bar/zipkin/"><link rel="icon" type="image/x-icon" href="favicon.ico"><meta charset="UTF-8"><title>Webpack App</title><link href="app-94a6ee84dc608c5f9e66.min.css" rel="stylesheet"></head><body>
<script type="text/javascript" src="app-94a6ee84dc608c5f9e66.min.js"></script></body></html>
```
As you would see, the attribute `href` of the `base` tag is rewritten which is the way to get around the
As you would see, the attribute `href` of the `base` tag is rewritten which is the way to get around the
`html-webpack-plugin` limitations.

Uploading the span is easy as
Expand All @@ -153,5 +153,5 @@ $ curl -H "Content-Type: application/json" --data-binary "[$(cat ../benchmarks/s

And then it's observable in the UI:
```bash
$ open http://localhost/proxy/foo/bar/zipkin/?serviceName=zipkin-server&startTs=1378193040000&endTs=1505463856013
$ open http://localhost/proxy/foo/bar/zipkin/?serviceName=zipkin-server&startTs=1378193040000&endTs=1505463856013
```