/** * Copyright 2015-2017 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 * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package zipkin.autoconfigure.ui; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; 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.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.http.ResponseEntity; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.GET; /** * Zipkin-UI is a single-page application that reads configuration from /config.json. * * <p>When looking at a trace, the browser is sent to the path "/traces/{id}". For the single-page * app to serve that route, the server needs to forward the request to "/index.html". The same * forwarding applies to "/dependencies" and any other routes the UI controls. * * <p>Under the scenes the JavaScript code looks at {@code window.location} to figure out what the * UI should do. This is handled by a route api defined in the crossroads library. * * <h3>Caching</h3> * <p>This includes a hard-coded cache policy, consistent with zipkin-scala. * <ul> * <li>1 minute for index.html</li> * <li>10 minute for /config.json</li> * <li>365 days for hashed resources (ex /app-e12b3bbb7e5a572f270d.min.js)</li> * </ul> * Since index.html links to hashed resource names, any change to it will orphan old resources. * That's why hashed resource age can be 365 days. */ @Configuration @EnableConfigurationProperties(ZipkinUiProperties.class) @ConditionalOnProperty(name = "zipkin.ui.enabled", matchIfMissing = true) @RestController public class ZipkinUiAutoConfiguration extends WebMvcConfigurerAdapter { @Autowired ZipkinUiProperties ui; @Value("classpath:zipkin-ui/index.html") Resource indexHtml; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/zipkin-ui/") .setCachePeriod((int) TimeUnit.DAYS.toSeconds(365)); } /** * This opts out of adding charset to png resources. * * <p>By default, {@linkplain CharacterEncodingFilter} adds a charset qualifier to all resources, * which helps, as javascript assets include extended character sets. However, the filter also * adds charset to well-known binary ones like png. This creates confusing content types, such as * "image/png;charset=UTF-8". * * See https://github.com/spring-projects/spring-boot/issues/5459 */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new CharacterEncodingFilter() { @Override protected boolean shouldNotFilter(HttpServletRequest request) { return request.getServletPath().endsWith(".png"); } }; filter.setEncoding("UTF-8"); filter.setForceEncoding(true); return filter; } @RequestMapping(value = "/config.json", method = GET, produces = APPLICATION_JSON_VALUE) public ResponseEntity<ZipkinUiProperties> serveUiConfig() { return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES)) .body(ui); } @RequestMapping(value = "/index.html", method = GET) public ResponseEntity<Resource> serveIndex() { return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES)) .body(indexHtml); } /** * This cherry-picks well-known routes the single-page app serves, and forwards to that as opposed * to returning a 404. */ // TODO This approach requires maintenance when new UI routes are added. Change to the following: // 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 = {"/", "/traces/{id}", "/dependency"}, method = GET) public ModelAndView forwardUiEndpoints(ModelMap model) { // Note: RequestMapping "/" requires us to use ModelAndView result vs just a string. // When "/" is mapped, the server literally returns "forward:/index.html" vs forwarding. return new ModelAndView("forward:/index.html", model); } }