├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── screenshot-circuit-breaker.png ├── screenshot-history.png ├── screenshot.png └── src ├── main ├── java │ └── com │ │ └── github │ │ └── vanroy │ │ └── cloud │ │ └── dashboard │ │ ├── config │ │ ├── CacheConfig.java │ │ ├── CloudDashboardConfig.java │ │ ├── CloudDashboardProperties.java │ │ ├── EnableCloudDashboard.java │ │ └── HttpClientProperties.java │ │ ├── controller │ │ ├── ApplicationController.java │ │ ├── DashboardController.java │ │ └── RegistryController.java │ │ ├── model │ │ ├── Application.java │ │ ├── Instance.java │ │ └── InstanceHistory.java │ │ ├── repository │ │ ├── ApplicationRepository.java │ │ ├── RegistryRepository.java │ │ ├── aws │ │ │ ├── BeanstalkProperties.java │ │ │ └── BeanstalkRepository.java │ │ └── eureka │ │ │ ├── EurekaRepository.java │ │ │ ├── LocaleEurekaRepository.java │ │ │ └── RemoteEurekaRepository.java │ │ └── stream │ │ └── CircuitBreakerStreamServlet.java └── webapp │ ├── WEB-INF │ ├── wro.properties │ └── wro.xml │ ├── public │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── montserrat-webfont.eot │ │ ├── montserrat-webfont.svg │ │ ├── montserrat-webfont.ttf │ │ ├── montserrat-webfont.woff │ │ ├── varela_round-webfont.eot │ │ ├── varela_round-webfont.svg │ │ ├── varela_round-webfont.ttf │ │ └── varela_round-webfont.woff │ ├── img │ │ ├── favicon.png │ │ ├── loading-big.gif │ │ ├── loading-medium.gif │ │ ├── loading-small.gif │ │ ├── platform-spring-boot.png │ │ ├── refresh.gif │ │ └── spring-logo.png │ ├── js │ │ ├── circuit-breaker-command.js │ │ ├── circuit-breaker-threadpool.js │ │ ├── jquery.tinysort.min.js │ │ └── tmpl.js │ ├── scripts │ │ ├── app.js │ │ ├── controllers │ │ │ ├── circuit-breaker.js │ │ │ └── controllers.js │ │ ├── directives │ │ │ ├── richMetricBar.js │ │ │ └── simpleMetricBar.js │ │ ├── filters │ │ │ └── filters.js │ │ └── services │ │ │ └── services.js │ ├── styles │ │ ├── circuit-breaker.css │ │ └── main.css │ └── views │ │ ├── about.html │ │ ├── apps.html │ │ ├── apps │ │ ├── details.html │ │ ├── details │ │ │ ├── classpath.html │ │ │ └── metrics.html │ │ ├── environment.html │ │ ├── history.html │ │ ├── jmx.html │ │ ├── logging.html │ │ ├── threads.html │ │ └── trace.html │ │ ├── circuit-breaker │ │ ├── circuit-container.html │ │ ├── circuit.html │ │ ├── index.html │ │ ├── threadpool-container.html │ │ └── threadpool.html │ │ ├── overview.html │ │ └── overview.selected.html │ └── templates │ └── dashboard.html └── test ├── java └── com │ └── github │ └── vanroy │ └── cloud │ └── dashboard │ ├── DashboardApplicationTest.java │ ├── controller │ └── ApplicationControllerTest.java │ └── turbine │ └── MockStreamServlet.java └── resources ├── application.properties ├── hystrix.stream └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.settings 3 | /.classpath 4 | /.project 5 | /.idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spring-cloud-dashboard 2 | ================================ 3 | 4 | This application provides a simple GUI to administrate Spring Cloud applications infrastructure. 5 | It's a fork of [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) to manage applications registered in service registry (Netflix Eureka and AWS Beanstalk). 6 | 7 | At the moment it provides the following features for every registered application (most of then inherited of spring-boot-admin). 8 | 9 | * Show name/id and version number 10 | * Show health status 11 | * Show details, like 12 | * Java System- / Environment- / Spring properties 13 | * JVM & memory metrics 14 | * Counter & gauge Metrics 15 | * Datasource Metrics 16 | * Easy loggerlevel management 17 | * Interact with JMX-Beans 18 | * View Threaddump 19 | 20 | Any specific spring cloud information are also available in dashboard 21 | 22 | * Application registry history ( from Eureka Server ) 23 | * Circuit Breaker dashboard ( from Hystrix or Turbine ) 24 | 25 | ## Easy Setup 26 | Add the following dependency to your pom.xml after you have build this project locally. 27 | 28 | ``` 29 | 30 | com.github.vanroy 31 | spring-cloud-dashboard 32 | 1.2.0.RELEASE 33 | 34 | ``` 35 | 36 | Create the Spring Cloud Dashboard with only one single Annotation. 37 | ``` 38 | @SpringBootApplication 39 | @EnableEurekaServer 40 | @EnableDiscoveryClient 41 | @EnableCloudDashboard 42 | public class Application { 43 | public static void main(String[] args) { 44 | SpringApplication.run(Application.class, args); 45 | } 46 | } 47 | ``` 48 | 49 | #### HTTP Client Configuration 50 | 51 | Spring Cloud Dashboard uses an Apache HTTP Client to query your instance's actuator endpoints. Sometimes it is possible that these endpoints are secured. Configurations are available to customize you http client with Basic pre authorization headers. 52 | 53 | ``` 54 | spring: 55 | cloud: 56 | dashboard: 57 | http: 58 | # Basic Credentials 59 | username: user 60 | password: password 61 | 62 | # Optional Defaults values 63 | maxConnection: 100 64 | connectTimeout: 1000 65 | socketTimeout: 2000 66 | requestTimeout: 1000 67 | ``` 68 | 69 | #### Samples: 70 | 71 | Samples are available in this repository : https://github.com/VanRoy/spring-cloud-dashboard-samples 72 | 73 | #### Screenshot: 74 | 75 | ##### Application registry: 76 | [](url "title") 77 | 78 | 79 | ##### Circuit breaker: 80 | [](url "title") 81 | 82 | 83 | ##### Registry history: 84 | [](url "title") 85 | 86 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /screenshot-circuit-breaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/screenshot-circuit-breaker.png -------------------------------------------------------------------------------- /screenshot-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/screenshot-history.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/screenshot.png -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/config/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.config; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import com.google.common.cache.CacheBuilder; 6 | import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.cache.guava.GuavaCacheManager; 9 | import org.springframework.context.annotation.AdviceMode; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | /** 14 | * Spring Cloud Dashboard Caching configuration 15 | * @author Julien Roy 16 | */ 17 | @Configuration 18 | @EnableCaching(mode = AdviceMode.ASPECTJ) 19 | public class CacheConfig { 20 | 21 | @Bean 22 | public CacheManagerCustomizer allCacheManagerCustomizer() { 23 | return cacheManager -> { 24 | cacheManager.setCacheBuilder( 25 | CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES) 26 | ); 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/config/CloudDashboardConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.config; 17 | 18 | import java.util.Optional; 19 | 20 | import com.github.vanroy.cloud.dashboard.repository.ApplicationRepository; 21 | import com.github.vanroy.cloud.dashboard.repository.aws.BeanstalkRepository; 22 | import com.github.vanroy.cloud.dashboard.repository.eureka.LocaleEurekaRepository; 23 | import com.github.vanroy.cloud.dashboard.repository.eureka.RemoteEurekaRepository; 24 | import com.github.vanroy.cloud.dashboard.stream.CircuitBreakerStreamServlet; 25 | import com.netflix.discovery.EurekaClient; 26 | import com.netflix.eureka.registry.PeerAwareInstanceRegistry; 27 | 28 | import java.util.Base64; 29 | import java.util.Collections; 30 | 31 | import org.apache.http.Header; 32 | import org.apache.http.client.HttpClient; 33 | import org.apache.http.client.config.RequestConfig; 34 | import org.apache.http.config.SocketConfig; 35 | import org.apache.http.impl.client.HttpClientBuilder; 36 | import org.apache.http.impl.client.HttpClients; 37 | import org.apache.http.message.BasicHeader; 38 | import org.springframework.beans.factory.annotation.Autowired; 39 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 40 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 41 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 42 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 43 | import org.springframework.context.annotation.Bean; 44 | import org.springframework.context.annotation.ComponentScan; 45 | import org.springframework.context.annotation.Configuration; 46 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 47 | 48 | /** 49 | * Spring Cloud Dashboard WebApp configuration 50 | * @author Julien Roy 51 | */ 52 | @Configuration 53 | @AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration") 54 | @ComponentScan("com.github.vanroy.cloud.dashboard") 55 | public class CloudDashboardConfig extends WebMvcConfigurerAdapter { 56 | 57 | @Autowired 58 | private HttpClientProperties httpClientProperties; 59 | 60 | @ConditionalOnClass(name = "com.netflix.eureka.registry.PeerAwareInstanceRegistry") 61 | @ConditionalOnMissingBean(ApplicationRepository.class) 62 | public static class Eureka { 63 | 64 | @Bean(name="applicationRepository") 65 | public ApplicationRepository eurekaRepository(Optional peerAwareInstanceRegistry, Optional eurekaClient) { 66 | if(peerAwareInstanceRegistry.isPresent()) { 67 | return new LocaleEurekaRepository(peerAwareInstanceRegistry.get()); 68 | } 69 | if(eurekaClient.isPresent()) { 70 | return new RemoteEurekaRepository(eurekaClient.get()); 71 | } 72 | return null; 73 | } 74 | } 75 | 76 | @Bean(name="applicationRepository") 77 | @ConditionalOnClass(name="com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient") 78 | @ConditionalOnMissingBean(ApplicationRepository.class) 79 | public ApplicationRepository beanstalkRepository() { 80 | return new BeanstalkRepository(); 81 | } 82 | 83 | @Bean 84 | @Autowired 85 | public ServletRegistrationBean circuitBreakerStreamServlet(ApplicationRepository repository) { 86 | return new ServletRegistrationBean(new CircuitBreakerStreamServlet(HttpClient(), repository), "/circuitBreaker.stream"); 87 | } 88 | 89 | @Bean 90 | public HttpClient HttpClient() { 91 | 92 | HttpClientBuilder builder = HttpClients.custom() 93 | .setMaxConnTotal(httpClientProperties.getMaxConnection()) 94 | .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(httpClientProperties.getSocketTimeout()).build()) 95 | .setDefaultRequestConfig(RequestConfig.custom() 96 | .setSocketTimeout(httpClientProperties.getSocketTimeout()) 97 | .setConnectTimeout(httpClientProperties.getConnectTimeout()) 98 | .setConnectionRequestTimeout(httpClientProperties.getRequestTimeout()) 99 | .build()); 100 | 101 | if (httpClientProperties.getUsername() != null && httpClientProperties.getPassword() != null) { 102 | builder.setDefaultHeaders(Collections.singletonList( 103 | createBasicAuthHeader(httpClientProperties.getUsername(),httpClientProperties.getPassword()) 104 | )); 105 | } 106 | 107 | return builder.build(); 108 | } 109 | 110 | private static Header createBasicAuthHeader(String username, String password) { 111 | String basicAuth = new String(Base64.getEncoder().encode((username + ":" + password).getBytes())); 112 | return new BasicHeader("Authorization", "Basic " + basicAuth); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/config/CloudDashboardProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Global dashboard properties 8 | * @author Julien Roy 9 | */ 10 | @Component 11 | @ConfigurationProperties("spring.cloud.dashboard") 12 | public class CloudDashboardProperties { 13 | 14 | // Global refresh timeout in milliseconds 15 | private int refreshTimeout = 30000; 16 | 17 | public int getRefreshTimeout() { 18 | return refreshTimeout; 19 | } 20 | 21 | public void setRefreshTimeout(int refreshTimeout) { 22 | this.refreshTimeout = refreshTimeout; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/config/EnableCloudDashboard.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013-2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.config; 17 | 18 | import org.springframework.context.annotation.Import; 19 | 20 | import java.lang.annotation.*; 21 | 22 | /** 23 | * Enable Spring Cloud Dashboard 24 | * @author Julien Roy 25 | */ 26 | @Target(ElementType.TYPE) 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @Documented 29 | @Import(CloudDashboardConfig.class) 30 | public @interface EnableCloudDashboard { 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/config/HttpClientProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.config; 2 | 3 | import javax.annotation.PostConstruct; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @ConfigurationProperties(prefix = "spring.cloud.dashboard.http") 10 | public class HttpClientProperties { 11 | 12 | private String username; 13 | private String password; 14 | private Integer maxConnection = 100; 15 | private Integer connectTimeout = 2000; 16 | private Integer socketTimeout = 5000; 17 | private Integer requestTimeout = 2000; 18 | 19 | public Integer getMaxConnection() { 20 | return maxConnection; 21 | } 22 | 23 | public void setMaxConnection(Integer maxConnection) { 24 | this.maxConnection = maxConnection; 25 | } 26 | 27 | public String getUsername() { 28 | return username; 29 | } 30 | 31 | public void setUsername(String username) { 32 | this.username = username; 33 | } 34 | 35 | public String getPassword() { 36 | return password; 37 | } 38 | 39 | public void setPassword(String password) { 40 | this.password = password; 41 | } 42 | 43 | public Integer getConnectTimeout() { 44 | return connectTimeout; 45 | } 46 | 47 | public void setConnectTimeout(Integer connectTimeout) { 48 | this.connectTimeout = connectTimeout; 49 | } 50 | 51 | public Integer getSocketTimeout() { 52 | return socketTimeout; 53 | } 54 | 55 | public void setSocketTimeout(Integer socketTimeout) { 56 | this.socketTimeout = socketTimeout; 57 | } 58 | 59 | public Integer getRequestTimeout() { 60 | return requestTimeout; 61 | } 62 | 63 | public void setRequestTimeout(Integer requestTimeout) { 64 | this.requestTimeout = requestTimeout; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/controller/ApplicationController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.controller; 17 | 18 | import java.util.Collection; 19 | import javax.servlet.ServletRequest; 20 | import javax.servlet.http.HttpServletRequest; 21 | import com.google.common.collect.Lists; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import com.github.vanroy.cloud.dashboard.model.Application; 26 | import com.github.vanroy.cloud.dashboard.model.Instance; 27 | import com.github.vanroy.cloud.dashboard.repository.ApplicationRepository; 28 | import org.apache.http.HttpResponse; 29 | import org.apache.http.client.HttpClient; 30 | import org.apache.http.client.methods.HttpGet; 31 | import org.apache.http.client.methods.HttpPost; 32 | import org.apache.http.client.utils.URIBuilder; 33 | import org.apache.http.entity.StringEntity; 34 | import org.apache.http.util.EntityUtils; 35 | import org.springframework.beans.factory.annotation.Autowired; 36 | import org.springframework.http.HttpStatus; 37 | import org.springframework.http.ResponseEntity; 38 | import org.springframework.web.bind.annotation.PathVariable; 39 | import org.springframework.web.bind.annotation.RequestBody; 40 | import org.springframework.web.bind.annotation.RequestMapping; 41 | import org.springframework.web.bind.annotation.RequestMethod; 42 | import org.springframework.web.bind.annotation.RequestParam; 43 | import org.springframework.web.bind.annotation.RestController; 44 | 45 | /** 46 | * REST controller for retrieve applications information. 47 | * @author Julien Roy 48 | */ 49 | @RestController 50 | public class ApplicationController { 51 | 52 | private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationController.class); 53 | 54 | @Autowired 55 | private ApplicationRepository repository; 56 | 57 | @Autowired 58 | private HttpClient httpClient; 59 | 60 | /** 61 | * List all applications with name 62 | * 63 | * @return List. 64 | */ 65 | @RequestMapping(value = "/api/applications", method = RequestMethod.GET, produces = "application/json") 66 | public Collection applications(@RequestParam(value = "name", required = false) String name) { 67 | LOGGER.debug("Deliver applications with name= {}", name); 68 | if (name == null || name.isEmpty()) { 69 | return repository.findAll(); 70 | } else { 71 | return Lists.newArrayList(repository.findByName(name)); 72 | } 73 | } 74 | 75 | /** 76 | * Get a single instance. 77 | * 78 | * @param id The instance identifier. 79 | * @return The instance. 80 | */ 81 | @RequestMapping(value = "/api/instance/{id}", method = RequestMethod.GET, produces = "application/json") 82 | public ResponseEntity instance(@PathVariable String id) { 83 | LOGGER.debug("Deliver application with ID '{}'", id); 84 | Instance instance = repository.findInstance(id); 85 | if (instance != null) { 86 | return new ResponseEntity<>(instance, HttpStatus.OK); 87 | } else { 88 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 89 | } 90 | } 91 | 92 | /** 93 | * Proxy call instance with specific management method 94 | * @param id id of instance 95 | * @param method Management method name 96 | * @return Return directly from instance 97 | */ 98 | @RequestMapping(value = {"/api/instance/{id}/{method}"}, method = RequestMethod.GET) 99 | public ResponseEntity proxy(@PathVariable String id, @PathVariable String method, ServletRequest request) { 100 | 101 | String managementUrl = repository.getInstanceManagementUrl(id); 102 | if(managementUrl == null) { 103 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 104 | } 105 | 106 | try { 107 | URIBuilder builder = new URIBuilder(managementUrl + "/" + method); 108 | request.getParameterMap().forEach((key, value) -> builder.setParameter(key, value[0])); 109 | HttpResponse response = httpClient.execute(new HttpGet(builder.build())); 110 | return ResponseEntity.status(response.getStatusLine().getStatusCode()).body(EntityUtils.toString(response.getEntity())); 111 | } catch (Exception e) { 112 | LOGGER.warn("Cannot proxy metrics to instance", e); 113 | } 114 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 115 | } 116 | 117 | /** 118 | * Proxy call instance with specific management method 119 | * @param id id of instance 120 | * @param method Management method name 121 | * @param param Management param value 122 | * @return Return directly from instance 123 | */ 124 | @RequestMapping(value = "/api/instance/{id}/{method}/{param:.*}", method = RequestMethod.GET) 125 | public ResponseEntity proxy(@PathVariable String id, @PathVariable String method, @PathVariable String param, ServletRequest request) { 126 | 127 | String managementUrl = repository.getInstanceManagementUrl(id); 128 | if(managementUrl == null) { 129 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 130 | } 131 | 132 | try { 133 | URIBuilder builder = new URIBuilder(managementUrl + "/" + method + "/" + param); 134 | request.getParameterMap().forEach((key, value) -> builder.setParameter(key, value[0])); 135 | HttpResponse response = httpClient.execute(new HttpGet(builder.build())); 136 | return ResponseEntity.status(response.getStatusLine().getStatusCode()).body(EntityUtils.toString(response.getEntity())); 137 | } catch (Exception e) { 138 | LOGGER.warn("Cannot proxy metrics to instance", e); 139 | } 140 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 141 | } 142 | 143 | /** 144 | * Proxy call instance with specific management method (with Http POST method) 145 | * @param id id of instance 146 | * @param method Management method name 147 | * @return Return directly from instance 148 | */ 149 | @RequestMapping(value = "/api/instance/{id}/{method}", method = RequestMethod.POST) 150 | public ResponseEntity proxyPost(@PathVariable String id, @PathVariable String method, @RequestBody(required = false) String body, HttpServletRequest request) { 151 | 152 | String managementUrl = repository.getInstanceManagementUrl(id); 153 | if(managementUrl == null) { 154 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 155 | } 156 | 157 | try { 158 | 159 | URIBuilder builder = new URIBuilder(managementUrl + "/" + method); 160 | request.getParameterMap().forEach((key, value) -> builder.setParameter(key, value[0])); 161 | 162 | HttpPost post = new HttpPost(builder.build()); 163 | if(body != null) { 164 | post.setEntity(new StringEntity(body)); 165 | } 166 | HttpResponse response = httpClient.execute(post); 167 | return ResponseEntity.status(response.getStatusLine().getStatusCode()).body(EntityUtils.toString(response.getEntity())); 168 | } catch (Exception e) { 169 | LOGGER.warn("Cannot proxy metrics to instance", e); 170 | } 171 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 172 | } 173 | 174 | /** 175 | * Proxy call instance with specific management method (with Http POST method) 176 | * @param id id of instance 177 | * @param method Management method name 178 | * @param param Management param value 179 | * @return Return directly from instance 180 | */ 181 | @RequestMapping(value = "/api/instance/{id}/{method}/{param:.*}", method = RequestMethod.POST) 182 | public ResponseEntity proxyPost(@PathVariable String id, @PathVariable String method, @RequestBody(required = false) String body, @PathVariable String param, ServletRequest request) { 183 | 184 | String managementUrl = repository.getInstanceManagementUrl(id); 185 | if(managementUrl == null) { 186 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 187 | } 188 | 189 | try { 190 | 191 | URIBuilder builder = new URIBuilder(managementUrl + "/" + method + "/" + param); 192 | request.getParameterMap().forEach((key, value) -> builder.setParameter(key, value[0])); 193 | 194 | HttpPost post = new HttpPost(builder.build()); 195 | if(body != null) { 196 | post.setEntity(new StringEntity(body)); 197 | } 198 | HttpResponse response = httpClient.execute(post); 199 | return ResponseEntity.status(response.getStatusLine().getStatusCode()).body(EntityUtils.toString(response.getEntity())); 200 | } catch (Exception e) { 201 | LOGGER.warn("Cannot proxy metrics to instance", e); 202 | } 203 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/controller/DashboardController.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.controller; 2 | 3 | import com.github.vanroy.cloud.dashboard.config.CloudDashboardProperties; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.ResponseBody; 9 | import org.springframework.web.servlet.ModelAndView; 10 | import org.springframework.web.servlet.view.InternalResourceView; 11 | 12 | /** 13 | * Dashboard home controller 14 | * @author Julien Roy 15 | */ 16 | @Controller 17 | public class DashboardController { 18 | 19 | @Autowired 20 | CloudDashboardProperties properties; 21 | 22 | @RequestMapping("${spring.cloud.dashboard.context:/}") 23 | public ModelAndView home() { 24 | return new ModelAndView("dashboard") 25 | .addObject("refreshTimeout", properties.getRefreshTimeout()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/controller/RegistryController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.controller; 17 | 18 | import com.google.common.collect.ImmutableMap; 19 | import com.github.vanroy.cloud.dashboard.repository.RegistryRepository; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.http.ResponseEntity; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RequestMethod; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | /** 29 | * REST controller for retrieve registry information. 30 | * @author Julien Roy 31 | */ 32 | @RestController 33 | public class RegistryController { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class); 36 | 37 | @Autowired(required = false) 38 | private RegistryRepository repository; 39 | 40 | /** 41 | * Return instance history registration/cancellation 42 | * @return List of registered/cancelled instances 43 | */ 44 | @RequestMapping(value = "/api/registry/history", method = RequestMethod.GET) 45 | public ResponseEntity instancesHistory() { 46 | 47 | if(repository == null) { 48 | return ResponseEntity.notFound().build(); 49 | } 50 | 51 | return ResponseEntity.ok(ImmutableMap.of( 52 | "lastRegistered", repository.getRegisteredInstanceHistory(), 53 | "lastCancelled", repository.getCanceledInstanceHistory() 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/model/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.model; 17 | 18 | import java.util.List; 19 | 20 | /** 21 | * The domain model for all application at the spring cloud dashboard application. 22 | * @author Julien Roy 23 | */ 24 | public class Application { 25 | 26 | private final String name; 27 | private final List instances; 28 | 29 | public Application(String name, List instances) { 30 | this.name = name; 31 | this.instances = instances; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public List getInstances() { 39 | return instances; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/model/Instance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.vanroy.cloud.dashboard.model; 17 | 18 | /** 19 | * The domain model for instance of application at the spring cloud dashboard application. 20 | * @author Julien Roy 21 | */ 22 | public class Instance { 23 | 24 | private final String id; 25 | private final String url; 26 | private final String name; 27 | private final String status; 28 | 29 | public Instance(String url, String name, String id, String status) { 30 | this.url = url.replaceFirst("/+$", ""); 31 | this.name = name; 32 | this.id = id; 33 | this.status = status; 34 | } 35 | 36 | public String getId() { 37 | return id; 38 | } 39 | 40 | public String getUrl() { 41 | return url; 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | 48 | public String getStatus() { 49 | return status; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/model/InstanceHistory.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.model; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * The domain model for an instance history 7 | * @author Julien Roy 8 | */ 9 | public class InstanceHistory { 10 | 11 | private final String id; 12 | private final Date date; 13 | 14 | public InstanceHistory(String id, Date date) { 15 | this.id = id; 16 | this.date = date; 17 | } 18 | 19 | public String getId() { 20 | return id; 21 | } 22 | 23 | public Date getDate() { 24 | return date; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/ApplicationRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository; 2 | 3 | import com.github.vanroy.cloud.dashboard.model.Application; 4 | import com.github.vanroy.cloud.dashboard.model.Instance; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * Application repository interface 10 | * @author Julien Roy 11 | */ 12 | public interface ApplicationRepository { 13 | 14 | /** 15 | * @return all Applications registered; 16 | */ 17 | Collection findAll(); 18 | 19 | /** 20 | * @param name the applications name 21 | * @return all Applications with the specified name; 22 | */ 23 | Application findByName(String name); 24 | 25 | /** 26 | * Return circuit breaker url to application 27 | * @param name Name of application 28 | * @return Circuit Breaker Stream 29 | */ 30 | String getApplicationCircuitBreakerStreamUrl(String name); 31 | 32 | /** 33 | * Return circuit breaker url to instance 34 | * @param instanceId Id of instance 35 | * @return Circuit Breaker Stream 36 | */ 37 | String getInstanceCircuitBreakerStreamUrl(String instanceId); 38 | 39 | /** 40 | * @param id the instance by id 41 | * @return the Instance with the specified id; 42 | */ 43 | Instance findInstance(String id); 44 | 45 | /** 46 | * Return management url to service 47 | * @param id Id of instance 48 | * @return Management URL 49 | */ 50 | String getInstanceManagementUrl(String id); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/RegistryRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository; 2 | 3 | import com.github.vanroy.cloud.dashboard.model.InstanceHistory; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Application repository interface 9 | * @author Julien Roy 10 | */ 11 | public interface RegistryRepository { 12 | 13 | /** 14 | * Return list of last registered instance in registry 15 | * @return List of 1000 last instance registered 16 | */ 17 | List getRegisteredInstanceHistory(); 18 | 19 | /** 20 | * Return list of last canceled instance in registry 21 | * @return List of 1000 last instance cancelled 22 | */ 23 | List getCanceledInstanceHistory(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/aws/BeanstalkProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository.aws; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Beanstalk configuration properties 10 | * @author Julien Roy 11 | */ 12 | @Component 13 | @ConfigurationProperties("spring.cloud.dashboard.beanstalk") 14 | public class BeanstalkProperties { 15 | 16 | private String endpoint = "https://elasticbeanstalk.us-east-1.amazonaws.com"; 17 | 18 | private String environment; 19 | private Map environmentTags; 20 | 21 | private CloudFormation cloudFormation = new CloudFormation(); 22 | 23 | private Instance instances = new Instance(); 24 | 25 | public String getEnvironment() { 26 | return environment; 27 | } 28 | 29 | public void setEnvironment(String environment) { 30 | this.environment = environment; 31 | } 32 | 33 | public Instance getInstances() { 34 | return instances; 35 | } 36 | 37 | public void setInstances(Instance instances) { 38 | this.instances = instances; 39 | } 40 | 41 | public String getEndpoint() { 42 | return endpoint; 43 | } 44 | 45 | public void setEndpoint(String endpoint) { 46 | this.endpoint = endpoint; 47 | } 48 | 49 | public Map getEnvironmentTags() { 50 | return environmentTags; 51 | } 52 | 53 | public void setEnvironmentTags(Map environmentTags) { 54 | this.environmentTags = environmentTags; 55 | } 56 | 57 | public CloudFormation getCloudFormation() { 58 | return cloudFormation; 59 | } 60 | 61 | public void setCloudFormation(CloudFormation cloudFormation) { 62 | this.cloudFormation = cloudFormation; 63 | } 64 | 65 | public static class Instance { 66 | 67 | private Management management = new Management(); 68 | private String endpoint = "https://ec2.us-east-1.amazonaws.com"; 69 | 70 | public Management getManagement() { 71 | return management; 72 | } 73 | 74 | public void setManagement(Management management) { 75 | this.management = management; 76 | } 77 | 78 | public String getEndpoint() { 79 | return endpoint; 80 | } 81 | 82 | public void setEndpoint(String endpoint) { 83 | this.endpoint = endpoint; 84 | } 85 | } 86 | 87 | public static class Management { 88 | 89 | private String scheme = "http"; 90 | private Integer port = 80; 91 | private String path = "/"; 92 | 93 | public String getScheme() { 94 | return scheme; 95 | } 96 | 97 | public void setScheme(String scheme) { 98 | this.scheme = scheme; 99 | } 100 | 101 | public Integer getPort() { 102 | return port; 103 | } 104 | 105 | public void setPort(Integer port) { 106 | this.port = port; 107 | } 108 | 109 | public String getPath() { 110 | return path; 111 | } 112 | 113 | public void setPath(String path) { 114 | this.path = path; 115 | } 116 | } 117 | 118 | public static class CloudFormation { 119 | 120 | private String endpoint = "https://cloudformation.us-east-1.amazonaws.com"; 121 | 122 | public String getEndpoint() { 123 | return endpoint; 124 | } 125 | 126 | public void setEndpoint(String endpoint) { 127 | this.endpoint = endpoint; 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/aws/BeanstalkRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository.aws; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.amazonaws.AmazonServiceException; 16 | import com.amazonaws.auth.AWSCredentials; 17 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; 18 | import com.amazonaws.services.cloudformation.AmazonCloudFormation; 19 | import com.amazonaws.services.cloudformation.AmazonCloudFormationClient; 20 | import com.amazonaws.services.cloudformation.model.DescribeStacksRequest; 21 | import com.amazonaws.services.cloudformation.model.DescribeStacksResult; 22 | import com.amazonaws.services.cloudformation.model.Stack; 23 | import com.amazonaws.services.ec2.AmazonEC2; 24 | import com.amazonaws.services.ec2.AmazonEC2Client; 25 | import com.amazonaws.services.ec2.model.DescribeInstanceStatusRequest; 26 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 27 | import com.amazonaws.services.ec2.model.InstanceState; 28 | import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalk; 29 | import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalkClient; 30 | import com.amazonaws.services.elasticbeanstalk.model.ApplicationDescription; 31 | import com.amazonaws.services.elasticbeanstalk.model.DescribeApplicationsRequest; 32 | import com.amazonaws.services.elasticbeanstalk.model.DescribeApplicationsResult; 33 | import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentResourcesRequest; 34 | import com.amazonaws.services.elasticbeanstalk.model.DescribeEnvironmentsRequest; 35 | import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription; 36 | import com.amazonaws.services.elasticbeanstalk.model.EnvironmentResourceDescription; 37 | import com.github.vanroy.cloud.dashboard.model.Application; 38 | import com.github.vanroy.cloud.dashboard.model.Instance; 39 | import com.github.vanroy.cloud.dashboard.repository.ApplicationRepository; 40 | import org.springframework.beans.factory.annotation.Autowired; 41 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 42 | import org.springframework.cache.annotation.Cacheable; 43 | import org.springframework.util.StringUtils; 44 | 45 | /** 46 | * Amazon Web Service Beanstalk registry implementation of application repository 47 | * @author Julien Roy 48 | */ 49 | @EnableConfigurationProperties({BeanstalkProperties.class}) 50 | public class BeanstalkRepository implements ApplicationRepository { 51 | 52 | private static final Logger LOGGER = LoggerFactory.getLogger(BeanstalkRepository.class); 53 | 54 | @Autowired 55 | private BeanstalkProperties properties; 56 | 57 | public BeanstalkRepository() { 58 | } 59 | 60 | @Override 61 | public Collection findAll() { 62 | 63 | DescribeApplicationsResult result = getBeanstalk().describeApplications(); 64 | 65 | LOGGER.info("Find applications : {}", result.getApplications().size()); 66 | 67 | return result.getApplications().parallelStream() 68 | .map(TO_APPLICATION) 69 | .sorted(Comparator.comparing(Application::getName)) 70 | .collect(Collectors.toList()); 71 | } 72 | 73 | @Override 74 | public Application findByName(String name) { 75 | return TO_APPLICATION.apply( 76 | getBeanstalk().describeApplications(new DescribeApplicationsRequest().withApplicationNames(name) 77 | ).getApplications().get(0)); 78 | } 79 | 80 | @Override 81 | public String getApplicationCircuitBreakerStreamUrl(String name) { 82 | return null; 83 | } 84 | 85 | @Override 86 | public String getInstanceCircuitBreakerStreamUrl(String instanceId) { 87 | return null; 88 | } 89 | 90 | @Override 91 | public Instance findInstance(String id) { 92 | return TO_INSTANCE.apply( 93 | getEC2().describeInstances(new DescribeInstancesRequest().withInstanceIds(id) 94 | ).getReservations().get(0).getInstances().get(0)); 95 | } 96 | 97 | @Override 98 | public String getInstanceManagementUrl(String id) { 99 | BeanstalkProperties.Management management = properties.getInstances().getManagement(); 100 | String privateDns = getInstancePrivateDns(id); 101 | return management.getScheme()+"://"+privateDns+":"+management.getPort()+management.getPath(); 102 | } 103 | 104 | @Cacheable("awsBeanstalkClient") 105 | private AWSElasticBeanstalk getBeanstalk() { 106 | AWSCredentials credentials = new DefaultAWSCredentialsProviderChain().getCredentials(); 107 | AWSElasticBeanstalk beanstalk = new AWSElasticBeanstalkClient(credentials); 108 | beanstalk.setEndpoint(properties.getEndpoint()); 109 | return beanstalk; 110 | } 111 | 112 | @Cacheable("awsEC2Client") 113 | private AmazonEC2 getEC2() { 114 | AWSCredentials credentials = new DefaultAWSCredentialsProviderChain().getCredentials(); 115 | AmazonEC2Client ec2 = new AmazonEC2Client(credentials); 116 | ec2.setEndpoint(properties.getInstances().getEndpoint()); 117 | return ec2; 118 | } 119 | 120 | @Cacheable("awsCloudformationStacks") 121 | private List getStacks() { 122 | AWSCredentials credentials = new DefaultAWSCredentialsProviderChain().getCredentials(); 123 | AmazonCloudFormation cf = new AmazonCloudFormationClient(credentials); 124 | cf.setEndpoint(properties.getCloudFormation().getEndpoint()); 125 | 126 | List stacks = new ArrayList<>(); 127 | DescribeStacksResult stacksResults = cf.describeStacks(); 128 | stacks.addAll(stacksResults.getStacks()); 129 | 130 | for (int i=0 ; i<100 ; i++) { 131 | if(stacksResults.getNextToken() == null) { 132 | return stacks; 133 | } else { 134 | stacksResults = cf.describeStacks(new DescribeStacksRequest().withNextToken(stacksResults.getNextToken())); 135 | stacks.addAll(stacksResults.getStacks()); 136 | } 137 | } 138 | 139 | return stacks; 140 | } 141 | 142 | @Cacheable("awsEC2InstancePrivateDns") 143 | private String getInstancePrivateDns(String instanceId) { 144 | return getEC2().describeInstances(new DescribeInstancesRequest().withInstanceIds(instanceId)) 145 | .getReservations().get(0).getInstances().get(0).getPrivateDnsName(); 146 | 147 | } 148 | 149 | @Cacheable("awsBeanstalkApplications") 150 | private Application getApplication(String name) { 151 | return new Application(name, getInstances(name)); 152 | } 153 | 154 | private List getInstances(String appName) { 155 | 156 | DescribeEnvironmentsRequest envRequest = new DescribeEnvironmentsRequest().withApplicationName(appName); 157 | 158 | List environments = getBeanstalk().describeEnvironments(envRequest).getEnvironments(); 159 | if (environments == null || environments.isEmpty()) { 160 | return Collections.emptyList(); 161 | } 162 | 163 | DescribeEnvironmentResourcesRequest request = new DescribeEnvironmentResourcesRequest(); 164 | 165 | if(StringUtils.hasText(properties.getEnvironment())) { 166 | Optional environment = environments.stream().filter(e -> e.getEnvironmentName().matches(properties.getEnvironment())).findFirst(); 167 | if (!environment.isPresent()) { 168 | return Collections.emptyList(); 169 | } 170 | 171 | request.withEnvironmentName(environment.get().getEnvironmentName()); 172 | } 173 | 174 | if(properties.getEnvironmentTags() != null && !properties.getEnvironmentTags().isEmpty()) { 175 | 176 | List environmentIds = environments.stream().map(EnvironmentDescription::getEnvironmentId).collect(Collectors.toList()); 177 | 178 | Optional oEnvironmentId = getStacks().stream() 179 | .map(Stack::getTags) 180 | .map(tags -> tags.stream().collect(Collectors.toMap(com.amazonaws.services.cloudformation.model.Tag::getKey, com.amazonaws.services.cloudformation.model.Tag::getValue))) 181 | .filter(tags -> { 182 | 183 | for (Map.Entry entry : properties.getEnvironmentTags().entrySet()) { 184 | //Check value with existing stack tags 185 | if (!entry.getValue().equalsIgnoreCase(tags.get(entry.getKey()))) { 186 | return false; 187 | } 188 | } 189 | 190 | return true; 191 | 192 | }) 193 | .map(tags -> tags.get("elasticbeanstalk:environment-id")) 194 | .filter(environmentIds::contains) 195 | .findFirst(); 196 | 197 | if (!oEnvironmentId.isPresent()) { 198 | return Collections.emptyList(); 199 | } 200 | 201 | request.withEnvironmentId(oEnvironmentId.get()); 202 | } 203 | 204 | EnvironmentResourceDescription resources = getBeanstalk().describeEnvironmentResources(request).getEnvironmentResources(); 205 | return resources.getInstances().stream().map(instance -> new Instance("", instance.getId(), instance.getId(), getInstanceStatus(instance.getId()))).collect(Collectors.toList()); 206 | } 207 | 208 | private String getInstanceStatus(String... id) { 209 | try { 210 | return TO_STATUS.apply(getEC2().describeInstanceStatus(new DescribeInstanceStatusRequest().withInstanceIds(id)) 211 | .getInstanceStatuses().get(0) 212 | .getInstanceState()); 213 | } catch (AmazonServiceException e) { 214 | LOGGER.warn("Instance not found", e); 215 | return "UNKNOWN"; 216 | } 217 | } 218 | 219 | private Function TO_APPLICATION = app -> { 220 | if(app == null) { return null; } 221 | return getApplication(app.getApplicationName()); 222 | }; 223 | 224 | private Function TO_INSTANCE = instance -> { 225 | if(instance == null) { return null; } 226 | return new Instance("", instance.getInstanceId(), instance.getInstanceId(), getInstanceStatus(instance.getInstanceId())); 227 | }; 228 | 229 | private Function TO_STATUS = state -> { 230 | switch (state.getName()) { 231 | case "pending": return "STARTING"; 232 | case "running": return "UP"; 233 | case "shutting-down": return "OUT_OF_SERVICE"; 234 | case "terminated": return "OUT_OF_SERVICE"; 235 | case "stopping": return "DOWN"; 236 | case "stopped": return "DOWN"; 237 | default: return "UNKNOWN"; 238 | } 239 | }; 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/eureka/EurekaRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository.eureka; 2 | 3 | import com.netflix.appinfo.InstanceInfo; 4 | import com.github.vanroy.cloud.dashboard.model.Application; 5 | import com.github.vanroy.cloud.dashboard.model.Instance; 6 | import com.github.vanroy.cloud.dashboard.repository.ApplicationRepository; 7 | import org.springframework.beans.factory.annotation.Value; 8 | 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * Eureka registry implementation of application repository 14 | * @author Julien Roy 15 | */ 16 | public abstract class EurekaRepository implements ApplicationRepository { 17 | 18 | @Value("${spring.cloud.dashboard.turbine.url:http://localhost:${server.port}/turbine.stream}") 19 | private String turbineUrl; 20 | 21 | public abstract Application findByName(String name); 22 | 23 | @Override 24 | public String getApplicationCircuitBreakerStreamUrl(String name) { 25 | if(findByName(name) == null) { 26 | return null; 27 | } 28 | return turbineUrl+"?cluster="+name; 29 | } 30 | 31 | @Override 32 | public String getInstanceCircuitBreakerStreamUrl(String instanceId) { 33 | String url = getInstanceManagementUrl(instanceId); 34 | if( url == null ){ 35 | return null; 36 | } 37 | return url+"/hystrix.stream"; 38 | } 39 | 40 | @Override 41 | public Instance findInstance(String id) { 42 | return TO_INSTANCE.apply(findInstanceInfo(id)); 43 | } 44 | 45 | @Override 46 | public String getInstanceManagementUrl(String id) { 47 | 48 | InstanceInfo info = findInstanceInfo(id); 49 | if(info == null) { 50 | return null; 51 | } 52 | 53 | String url = info.getHomePageUrl(); 54 | if(info.getMetadata().containsKey("managementPath")) { 55 | url += info.getMetadata().get("managementPath"); 56 | } 57 | 58 | return url; 59 | } 60 | 61 | protected abstract InstanceInfo findInstanceInfo(String id); 62 | 63 | protected Function TO_APPLICATION = new Function() { 64 | @Override 65 | public Application apply(com.netflix.discovery.shared.Application app) { 66 | if(app == null) { return null; } 67 | return new Application(app.getName(), app.getInstances().stream().map(TO_INSTANCE).sorted((o1, o2) -> o1.getName().compareTo(o2.getName())).collect(Collectors.toList())); 68 | } 69 | }; 70 | 71 | protected Function TO_INSTANCE = instance -> { 72 | if(instance == null) { return null; } 73 | return new Instance(instance.getHomePageUrl(), instance.getId(), instance.getAppName()+"_"+instance.getId().replaceAll("\\.","_"), instance.getStatus().toString()); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/eureka/LocaleEurekaRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository.eureka; 2 | 3 | import com.github.vanroy.cloud.dashboard.model.Application; 4 | import com.github.vanroy.cloud.dashboard.model.InstanceHistory; 5 | import com.github.vanroy.cloud.dashboard.repository.RegistryRepository; 6 | import com.netflix.appinfo.InstanceInfo; 7 | import com.netflix.discovery.shared.Pair; 8 | import com.netflix.eureka.registry.PeerAwareInstanceRegistry; 9 | 10 | import java.util.Collection; 11 | import java.util.Date; 12 | import java.util.List; 13 | import java.util.function.Function; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Eureka registry implementation of application repository 18 | * @author Julien Roy 19 | */ 20 | public class LocaleEurekaRepository extends EurekaRepository implements RegistryRepository { 21 | 22 | private final PeerAwareInstanceRegistry registry; 23 | 24 | public LocaleEurekaRepository(PeerAwareInstanceRegistry registry) { 25 | this.registry = registry; 26 | } 27 | 28 | @Override 29 | public Collection findAll() { 30 | return registry.getSortedApplications().stream() 31 | .map(TO_APPLICATION) 32 | .collect(Collectors.toList()); 33 | } 34 | 35 | @Override 36 | public Application findByName(String name) { 37 | return TO_APPLICATION.apply(registry.getApplication(name)); 38 | } 39 | 40 | @Override 41 | public List getCanceledInstanceHistory() { 42 | return registry.getLastNCanceledInstances().stream().map(TO_REGISTRY_HISTORY).collect(Collectors.toList()); 43 | } 44 | 45 | @Override 46 | public List getRegisteredInstanceHistory() { 47 | return registry.getLastNRegisteredInstances().stream().map(TO_REGISTRY_HISTORY).collect(Collectors.toList()); 48 | } 49 | protected InstanceInfo findInstanceInfo(String id) { 50 | String[] instanceIds = id.split("_", 2); 51 | return registry.getInstanceByAppAndId(instanceIds[0], instanceIds[1].replaceAll("_", ".")); 52 | } 53 | 54 | private Function, InstanceHistory> TO_REGISTRY_HISTORY = history -> new InstanceHistory(history.second(), new Date(history.first())); 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/repository/eureka/RemoteEurekaRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.repository.eureka; 2 | 3 | import com.netflix.appinfo.InstanceInfo; 4 | import com.netflix.discovery.DiscoveryManager; 5 | import com.github.vanroy.cloud.dashboard.model.Application; 6 | import com.netflix.discovery.EurekaClient; 7 | 8 | import java.util.Collection; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * Eureka registry implementation of application repository 13 | * @author Julien Roy 14 | */ 15 | public class RemoteEurekaRepository extends EurekaRepository { 16 | 17 | private final EurekaClient eurekaClient; 18 | 19 | public RemoteEurekaRepository(EurekaClient eurekaClient) { 20 | this.eurekaClient = eurekaClient; 21 | } 22 | 23 | @Override 24 | public Collection findAll() { 25 | return eurekaClient.getApplications().getRegisteredApplications().stream() 26 | .map(TO_APPLICATION) 27 | .collect(Collectors.toList()); 28 | } 29 | 30 | @Override 31 | public Application findByName(String name) { 32 | return TO_APPLICATION.apply(eurekaClient.getApplications().getRegisteredApplications(name)); 33 | } 34 | 35 | protected InstanceInfo findInstanceInfo(String id) { 36 | String[] instanceIds = id.split("_", 2); 37 | return eurekaClient.getApplication(instanceIds[0]).getByInstanceId(instanceIds[1].replaceAll("_", ".")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/vanroy/cloud/dashboard/stream/CircuitBreakerStreamServlet.java: -------------------------------------------------------------------------------- 1 | package com.github.vanroy.cloud.dashboard.stream; 2 | 3 | import com.github.vanroy.cloud.dashboard.repository.ApplicationRepository; 4 | import org.apache.http.Header; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.HttpStatus; 7 | import org.apache.http.client.HttpClient; 8 | import org.apache.http.client.methods.HttpGet; 9 | import org.apache.http.client.methods.HttpHead; 10 | import org.apache.http.client.methods.HttpUriRequest; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.ServletException; 15 | import javax.servlet.http.HttpServlet; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.OutputStream; 21 | 22 | /** 23 | * Proxy an EventStream request (data.stream via proxy.stream) since EventStream does not yet support CORS (https://bugs.webkit.org/show_bug.cgi?id=61862) 24 | * so that a UI can request a stream from a different server. 25 | * @author Julien Roy 26 | */ 27 | public class CircuitBreakerStreamServlet extends HttpServlet { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerStreamServlet.class); 30 | 31 | private final HttpClient httpClient; 32 | private final ApplicationRepository repository; 33 | 34 | public CircuitBreakerStreamServlet(HttpClient httpClient, ApplicationRepository repository) { 35 | super(); 36 | this.httpClient = httpClient; 37 | this.repository = repository; 38 | } 39 | 40 | /** 41 | * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) 42 | */ 43 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 44 | 45 | String proxyUrl; 46 | String appName = request.getParameter("appName"); 47 | String instanceId = request.getParameter("instanceId"); 48 | 49 | if(appName == null && instanceId == null) { 50 | response.setStatus(500); 51 | response.getWriter().println("Please use appName or instanceId to select data to stream"); 52 | return; 53 | } 54 | 55 | if(appName != null) { 56 | proxyUrl = repository.getApplicationCircuitBreakerStreamUrl(appName); 57 | if (proxyUrl == null) { 58 | response.setStatus(500); 59 | response.getWriter().println("Application cluster circuit breaker not found"); 60 | return; 61 | } 62 | } else { 63 | proxyUrl = repository.getInstanceCircuitBreakerStreamUrl(instanceId); 64 | if (proxyUrl == null) { 65 | response.setStatus(500); 66 | response.getWriter().println("Instance circuit breaker not found"); 67 | return; 68 | } 69 | } 70 | 71 | HttpUriRequest httpRequest = null; 72 | InputStream is = null; 73 | 74 | logger.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n"); 75 | try { 76 | if(HttpHead.METHOD_NAME.equalsIgnoreCase(request.getMethod())) { 77 | httpRequest = new HttpHead(proxyUrl); 78 | } else { 79 | httpRequest = new HttpGet(proxyUrl); 80 | } 81 | 82 | HttpResponse httpResponse = httpClient.execute(httpRequest); 83 | int statusCode = httpResponse.getStatusLine().getStatusCode(); 84 | if (statusCode == HttpStatus.SC_OK) { 85 | // writeTo swallows exceptions and never quits even if outputstream is throwing IOExceptions (such as broken pipe) ... since the inputstream is infinite 86 | // httpResponse.getEntity().writeTo(new OutputStreamWrapper(response.getOutputStream())); 87 | // so I copy it manually ... 88 | if(httpResponse.getEntity() != null) { 89 | 90 | is = httpResponse.getEntity().getContent(); 91 | 92 | // set headers 93 | for (Header header : httpResponse.getAllHeaders()) { 94 | response.addHeader(header.getName(), header.getValue()); 95 | } 96 | 97 | // copy data from source to response 98 | OutputStream os = response.getOutputStream(); 99 | int b; 100 | while ((b = is.read()) != -1) { 101 | try { 102 | os.write(b); 103 | if (b == 10 /** flush buffer on line feed */) { 104 | os.flush(); 105 | } 106 | } catch (Exception e) { 107 | if (e.getClass().getSimpleName().equalsIgnoreCase("ClientAbortException")) { 108 | // don't throw an exception as this means the user closed the connection 109 | logger.debug("Connection closed by client. Will stop proxying ..."); 110 | // break out of the while loop 111 | break; 112 | } else { 113 | // received unknown error while writing so throw an exception 114 | throw new RuntimeException(e); 115 | } 116 | } 117 | } 118 | } 119 | } else { 120 | response.setStatus(statusCode); 121 | } 122 | } catch (Exception e) { 123 | logger.error("Error proxying request: " + proxyUrl, e); 124 | response.setStatus(500); 125 | } finally { 126 | if (httpRequest != null) { 127 | try { 128 | httpRequest.abort(); 129 | } catch (Exception e) { 130 | logger.error("failed aborting proxy connection.", e); 131 | } 132 | } 133 | 134 | // httpget.abort() MUST be called first otherwise is.close() hangs (because data is still streaming?) 135 | if (is != null) { 136 | // this should already be closed by httpget.abort() above 137 | try { 138 | is.close(); 139 | } catch (Exception e) { 140 | // e.printStackTrace(); 141 | } 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/wro.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/WEB-INF/wro.properties -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/wro.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | webjar:bootstrap/@bootstrap.version@/css/bootstrap.min.css 4 | webjar:webjars/nvd3/@nvd3.version@/nv.d3.min.css 5 | /styles/main.css 6 | 7 | webjar:jquery/@jquery.version@/jquery.min.js 8 | webjar:angularjs/@angularjs.version@/angular.min.js 9 | webjar:bootstrap/@bootstrap.version@/js/bootstrap.min.js 10 | webjar:angularjs/@angularjs.version@/angular-resource.min.js 11 | webjar:angularjs/@angularjs.version@/angular-route.min.js 12 | webjar:angular-ui-router/@angular-ui-router.version@/angular-ui-router.min.js 13 | webjar:angular-ui-bootstrap/@angular-ui-bootstrap.version@/ui-bootstrap.min.js 14 | webjar:angular-ui-bootstrap/@angular-ui-bootstrap.version@/ui-bootstrap-tpls.min.js 15 | webjar:d3js/@d3js.version@/d3.min.js 16 | webjar:nvd3/@nvd3.version@/nv.d3.min.js 17 | webjar:angularjs-nvd3-directives/0.0.7/angularjs-nvd3-directives.js 18 | webjar:jolokia.js/@jolokia-js.version@/jolokia.js 19 | /scripts/app.js 20 | /scripts/filters/filters.js 21 | /scripts/controllers/controllers.js 22 | /scripts/controllers/circuit-breaker.js 23 | /scripts/services/services.js 24 | /scripts/directives/richMetricBar.js 25 | /scripts/directives/simpleMetricBar.js 26 | 27 | -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/montserrat-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/montserrat-webfont.eot -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/montserrat-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/montserrat-webfont.ttf -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/montserrat-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/montserrat-webfont.woff -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/varela_round-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/varela_round-webfont.eot -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/varela_round-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/varela_round-webfont.ttf -------------------------------------------------------------------------------- /src/main/webapp/public/fonts/varela_round-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/fonts/varela_round-webfont.woff -------------------------------------------------------------------------------- /src/main/webapp/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/favicon.png -------------------------------------------------------------------------------- /src/main/webapp/public/img/loading-big.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/loading-big.gif -------------------------------------------------------------------------------- /src/main/webapp/public/img/loading-medium.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/loading-medium.gif -------------------------------------------------------------------------------- /src/main/webapp/public/img/loading-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/loading-small.gif -------------------------------------------------------------------------------- /src/main/webapp/public/img/platform-spring-boot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/platform-spring-boot.png -------------------------------------------------------------------------------- /src/main/webapp/public/img/refresh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/refresh.gif -------------------------------------------------------------------------------- /src/main/webapp/public/img/spring-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VanRoy/spring-cloud-dashboard/0a4758b42013b3901b00c7d36fd2ad33db69b17b/src/main/webapp/public/img/spring-logo.png -------------------------------------------------------------------------------- /src/main/webapp/public/js/circuit-breaker-threadpool.js: -------------------------------------------------------------------------------- 1 | 2 | (function(window) { 3 | 4 | // cache the templates we use on this page as global variables (asynchronously) 5 | jQuery.get(getRelativePath("views/circuit-breaker/threadpool.html"), function(data) { 6 | htmlTemplate = data; 7 | }); 8 | jQuery.get(getRelativePath("views/circuit-breaker/threadpool-container.html"), function(data) { 9 | htmlTemplateContainer = data; 10 | }); 11 | 12 | function getRelativePath(path) { 13 | var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1); 14 | return p + path; 15 | } 16 | 17 | /** 18 | * Object containing functions for displaying and updating the UI with streaming data. 19 | * 20 | * Publish this externally as "HystrixThreadPoolMonitor" 21 | */ 22 | window.HystrixThreadPoolMonitor = function(containerId) { 23 | 24 | var self = this; // keep scope under control 25 | 26 | this.containerId = containerId; 27 | 28 | /** 29 | * Initialization on construction 30 | */ 31 | // intialize various variables we use for visualization 32 | var maxXaxisForCircle="40%"; 33 | var maxYaxisForCircle="40%"; 34 | var maxRadiusForCircle="125"; 35 | var maxDomain = 2000; 36 | 37 | self.circleRadius = d3.scale.pow().exponent(0.5).domain([0, maxDomain]).range(["5", maxRadiusForCircle]); // requests per second per host 38 | self.circleYaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxXaxisForCircle]); 39 | self.circleXaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxYaxisForCircle]); 40 | self.colorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]); 41 | self.errorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]); 42 | 43 | /** 44 | * We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds 45 | * to maintain whatever sort the user (or default) has chosen. 46 | * 47 | * In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing. 48 | */ 49 | setInterval(function() { 50 | // sort since we have added a new one 51 | self.sortSameAsLast(); 52 | }, 1000); 53 | 54 | /** 55 | * END of Initialization on construction 56 | */ 57 | 58 | /** 59 | * Event listener to handle new messages from EventSource as streamed from the server. 60 | */ 61 | /* public */ self.eventSourceMessageListener = function(e) { 62 | var data = JSON.parse(e.data); 63 | if(data) { 64 | // check for reportingHosts (if not there, set it to 1 for singleHost vs cluster) 65 | if(!data.reportingHosts) { 66 | data.reportingHosts = 1; 67 | } 68 | 69 | if(data && data.type == 'HystrixThreadPool') { 70 | if (data.deleteData == 'true') { 71 | deleteThreadPool(data.escapedName); 72 | } else { 73 | displayThreadPool(data); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | /** 80 | * Pre process the data before displying in the UI. 81 | * e.g Get Averages from sums, do rate calculation etc. 82 | */ 83 | function preProcessData(data) { 84 | validateData(data); 85 | // escape string used in jQuery & d3 selectors 86 | data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1'); 87 | // do math 88 | converAllAvg(data); 89 | calcRatePerSecond(data); 90 | } 91 | 92 | function converAllAvg(data) { 93 | convertAvg(data, "propertyValue_queueSizeRejectionThreshold", false); 94 | 95 | // the following will break when it becomes a compound string if the property is dynamically changed 96 | convertAvg(data, "propertyValue_metricsRollingStatisticalWindowInMilliseconds", false); 97 | } 98 | 99 | function convertAvg(data, key, decimal) { 100 | if (decimal) { 101 | data[key] = roundNumber(data[key]/data["reportingHosts"]); 102 | } else { 103 | data[key] = Math.floor(data[key]/data["reportingHosts"]); 104 | } 105 | } 106 | 107 | function calcRatePerSecond(data) { 108 | var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000; 109 | 110 | var totalThreadsExecuted = data["rollingCountThreadsExecuted"]; 111 | if (totalThreadsExecuted < 0) { 112 | totalThreadsExecuted = 0; 113 | } 114 | data["ratePerSecond"] = roundNumber(totalThreadsExecuted / numberSeconds); 115 | data["ratePerSecondPerHost"] = roundNumber(totalThreadsExecuted / numberSeconds / data["reportingHosts"]); 116 | } 117 | 118 | function validateData(data) { 119 | 120 | assertNotNull(data,"type"); 121 | assertNotNull(data,"name"); 122 | // assertNotNull(data,"currentTime"); 123 | assertNotNull(data,"currentActiveCount"); 124 | assertNotNull(data,"currentCompletedTaskCount"); 125 | assertNotNull(data,"currentCorePoolSize"); 126 | assertNotNull(data,"currentLargestPoolSize"); 127 | assertNotNull(data,"currentMaximumPoolSize"); 128 | assertNotNull(data,"currentPoolSize"); 129 | assertNotNull(data,"currentQueueSize"); 130 | assertNotNull(data,"currentTaskCount"); 131 | assertNotNull(data,"rollingCountThreadsExecuted"); 132 | assertNotNull(data,"rollingMaxActiveThreads"); 133 | assertNotNull(data,"reportingHosts"); 134 | 135 | assertNotNull(data,"propertyValue_queueSizeRejectionThreshold"); 136 | assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds"); 137 | } 138 | 139 | function assertNotNull(data, key) { 140 | if(data[key] == undefined) { 141 | if (key == "dependencyOwner") { 142 | data["dependencyOwner"] = data.name; 143 | } else { 144 | throw new Error("Key Missing: " + key + " for " + data.name) 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * Method to display the THREAD_POOL data 151 | * 152 | * @param data 153 | */ 154 | /* private */ function displayThreadPool(data) { 155 | 156 | try { 157 | preProcessData(data); 158 | } catch (err) { 159 | log("Failed preProcessData: " + err.message); 160 | return; 161 | } 162 | 163 | // add the 'addCommas' function to the 'data' object so the HTML templates can use it 164 | data.addCommas = addCommas; 165 | // add the 'roundNumber' function to the 'data' object so the HTML templates can use it 166 | data.roundNumber = roundNumber; 167 | 168 | var addNew = false; 169 | // check if we need to create the container 170 | if(!$('#THREAD_POOL_' + data.escapedName).length) { 171 | // it doesn't exist so add it 172 | var html = tmpl(htmlTemplateContainer, data); 173 | // remove the loading thing first 174 | $('#' + containerId + ' span.loading').remove(); 175 | // get the current last column and remove the 'last' class from it 176 | $('#' + containerId + ' div.last').removeClass('last'); 177 | // now create the new data and add it 178 | $('#' + containerId + '').append(html); 179 | // add the 'last' class to the column we just added 180 | $('#' + containerId + ' div.monitor').last().addClass('last'); 181 | 182 | // add the default sparkline graph 183 | d3.selectAll('#graph_THREAD_POOL_' + data.escapedName + ' svg').append("svg:path"); 184 | 185 | // remember this is new so we can trigger a sort after setting data 186 | addNew = true; 187 | } 188 | 189 | // set the rate on the div element so it's available for sorting 190 | $('#THREAD_POOL_' + data.escapedName).attr('rate_value', data.ratePerSecondPerHost); 191 | 192 | // now update/insert the data 193 | $('#THREAD_POOL_' + data.escapedName + ' div.monitor_data').html(tmpl(htmlTemplate, data)); 194 | 195 | // set variables for circle visualization 196 | var rate = data.ratePerSecondPerHost; 197 | // we will treat each item in queue as 1% of an error visualization 198 | // ie. 5 threads in queue per instance == 5% error percentage 199 | var errorPercentage = data.currentQueueSize / data.reportingHosts; 200 | 201 | updateCircle('#THREAD_POOL_' + data.escapedName + ' circle', rate, errorPercentage); 202 | 203 | if(addNew) { 204 | // sort since we added a new circuit 205 | self.sortSameAsLast(); 206 | } 207 | } 208 | 209 | /* round a number to X digits: num => the number to round, dec => the number of decimals */ 210 | /* private */ function roundNumber(num) { 211 | var dec=1; // we are hardcoding to support only 1 decimal so that our padding logic at the end is simple 212 | var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); 213 | var resultAsString = result.toString(); 214 | if(resultAsString.indexOf('.') == -1) { 215 | resultAsString = resultAsString + '.'; 216 | for(var i=0; i parseInt(maxXaxisForCircle)) { 227 | newXaxisForCircle = maxXaxisForCircle; 228 | } 229 | var newYaxisForCircle = self.circleYaxis(rate); 230 | if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) { 231 | newYaxisForCircle = maxYaxisForCircle; 232 | } 233 | var newRadiusForCircle = self.circleRadius(rate); 234 | if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) { 235 | newRadiusForCircle = maxRadiusForCircle; 236 | } 237 | 238 | d3.selectAll(cssTarget) 239 | .transition() 240 | .duration(400) 241 | .attr("cy", newYaxisForCircle) 242 | .attr("cx", newXaxisForCircle) 243 | .attr("r", newRadiusForCircle) 244 | .style("fill", self.colorRange(errorPercentage)); 245 | } 246 | 247 | /* private */ function deleteThreadPool(poolName) { 248 | $('#THREAD_POOL_' + poolName).remove(); 249 | } 250 | 251 | }; 252 | 253 | // public methods for sorting 254 | HystrixThreadPoolMonitor.prototype.sortByVolume = function() { 255 | var direction = "desc"; 256 | if(this.sortedBy == 'rate_desc') { 257 | direction = 'asc'; 258 | } 259 | this.sortByVolumeInDirection(direction); 260 | }; 261 | 262 | HystrixThreadPoolMonitor.prototype.sortByVolumeInDirection = function(direction) { 263 | this.sortedBy = 'rate_' + direction; 264 | $('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'}); 265 | }; 266 | 267 | HystrixThreadPoolMonitor.prototype.sortAlphabetically = function() { 268 | var direction = "asc"; 269 | if(this.sortedBy == 'alph_asc') { 270 | direction = 'desc'; 271 | } 272 | this.sortAlphabeticalInDirection(direction); 273 | }; 274 | 275 | HystrixThreadPoolMonitor.prototype.sortAlphabeticalInDirection = function(direction) { 276 | this.sortedBy = 'alph_' + direction; 277 | $('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction}); 278 | }; 279 | 280 | HystrixThreadPoolMonitor.prototype.sortByMetricInDirection = function(direction, metric) { 281 | $('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction}); 282 | } ; 283 | 284 | // this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose 285 | HystrixThreadPoolMonitor.prototype.sortSameAsLast = function() { 286 | if(this.sortedBy == 'alph_asc') { 287 | this.sortAlphabeticalInDirection('asc'); 288 | } else if(this.sortedBy == 'alph_desc') { 289 | this.sortAlphabeticalInDirection('desc'); 290 | } else if(this.sortedBy == 'rate_asc') { 291 | this.sortByVolumeInDirection('asc'); 292 | } else if(this.sortedBy == 'rate_desc') { 293 | this.sortByVolumeInDirection('desc'); 294 | } else if(this.sortedBy == 'error_asc') { 295 | this.sortByErrorInDirection('asc'); 296 | } else if(this.sortedBy == 'error_desc') { 297 | this.sortByErrorInDirection('desc'); 298 | } else if(this.sortedBy == 'lat90_asc') { 299 | this.sortByMetricInDirection('asc', 'p90'); 300 | } else if(this.sortedBy == 'lat90_desc') { 301 | this.sortByMetricInDirection('desc', 'p90'); 302 | } else if(this.sortedBy == 'lat99_asc') { 303 | this.sortByMetricInDirection('asc', 'p99'); 304 | } else if(this.sortedBy == 'lat99_desc') { 305 | this.sortByMetricInDirection('desc', 'p99'); 306 | } else if(this.sortedBy == 'lat995_asc') { 307 | this.sortByMetricInDirection('asc', 'p995'); 308 | } else if(this.sortedBy == 'lat995_desc') { 309 | this.sortByMetricInDirection('desc', 'p995'); 310 | } else if(this.sortedBy == 'latMean_asc') { 311 | this.sortByMetricInDirection('asc', 'pMean'); 312 | } else if(this.sortedBy == 'latMean_desc') { 313 | this.sortByMetricInDirection('desc', 'pMean'); 314 | } else if(this.sortedBy == 'latMedian_asc') { 315 | this.sortByMetricInDirection('asc', 'pMedian'); 316 | } else if(this.sortedBy == 'latMedian_desc') { 317 | this.sortByMetricInDirection('desc', 'pMedian'); 318 | } 319 | }; 320 | 321 | // default sort type and direction 322 | this.sortedBy = 'alph_asc'; 323 | 324 | 325 | // a temporary home for the logger until we become more sophisticated 326 | function log(message) { 327 | console.log(message); 328 | }; 329 | 330 | function addCommas(nStr){ 331 | nStr += ''; 332 | if(nStr.length <=3) { 333 | return nStr; //shortcut if we don't need commas 334 | } 335 | x = nStr.split('.'); 336 | x1 = x[0]; 337 | x2 = x.length > 1 ? '.' + x[1] : ''; 338 | var rgx = /(\d+)(\d{3})/; 339 | while (rgx.test(x1)) { 340 | x1 = x1.replace(rgx, '$1' + ',' + '$2'); 341 | } 342 | return x1 + x2; 343 | } 344 | })(window); 345 | 346 | 347 | -------------------------------------------------------------------------------- /src/main/webapp/public/js/jquery.tinysort.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery TinySort - A plugin to sort child nodes by (sub) contents or attributes. 3 | * 4 | * Version: 1.0.5 5 | * 6 | * Copyright (c) 2008-2011 Ron Valstar http://www.sjeiti.com/ 7 | * 8 | * Dual licensed under the MIT and GPL licenses: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * http://www.gnu.org/licenses/gpl.html 11 | */ 12 | (function(b){b.tinysort={id:"TinySort",version:"1.0.5",copyright:"Copyright (c) 2008-2011 Ron Valstar",uri:"http://tinysort.sjeiti.com/",defaults:{order:"asc",attr:"",place:"start",returns:false,useVal:false}};b.fn.extend({tinysort:function(h,j){if(h&&typeof(h)!="string"){j=h;h=null}var e=b.extend({},b.tinysort.defaults,j);var p={};this.each(function(t){var v=(!h||h=="")?b(this):b(this).find(h);var u=e.order=="rand"?""+Math.random():(e.attr==""?(e.useVal?v.val():v.text()):v.attr(e.attr));var s=b(this).parent();if(!p[s]){p[s]={s:[],n:[]}}if(v.length>0){p[s].s.push({s:u,e:b(this),n:t})}else{p[s].n.push({e:b(this),n:t})}});for(var g in p){var d=p[g];d.s.sort(function k(t,s){var i=t.s.toLowerCase?t.s.toLowerCase():t.s;var u=s.s.toLowerCase?s.s.toLowerCase():s.s;if(c(t.s)&&c(s.s)){i=parseFloat(t.s);u=parseFloat(s.s)}return(e.order=="asc"?1:-1)*(iu?1:0))})}var m=[];for(var g in p){var d=p[g];var n=[];var f=b(this).length;switch(e.place){case"first":b.each(d.s,function(s,t){f=Math.min(f,t.n)});break;case"org":b.each(d.s,function(s,t){n.push(t.n)});break;case"end":f=d.n.length;break;default:f=0}var q=[0,0];for(var l=0;l=f&&l0?d[1]:false}function a(e,f){var d=false;b.each(e,function(h,g){if(!d){d=g==f}});return d}b.fn.TinySort=b.fn.Tinysort=b.fn.tsort=b.fn.tinysort})(jQuery); -------------------------------------------------------------------------------- /src/main/webapp/public/js/tmpl.js: -------------------------------------------------------------------------------- 1 | 2 | //Simple JavaScript Templating 3 | //John Resig - http://ejohn.org/ - MIT Licensed 4 | // http://ejohn.org/blog/javascript-micro-templating/ 5 | (function(window, undefined) { 6 | var cache = {}; 7 | 8 | window.tmpl = function tmpl(str, data) { 9 | try { 10 | // Figure out if we're getting a template, or if we need to 11 | // load the template - and be sure to cache the result. 12 | var fn = !/\W/.test(str) ? 13 | cache[str] = cache[str] || 14 | tmpl(document.getElementById(str).innerHTML) : 15 | 16 | // Generate a reusable function that will serve as a template 17 | // generator (and which will be cached). 18 | new Function("obj", 19 | "var p=[],print=function(){p.push.apply(p,arguments);};" + 20 | 21 | // Introduce the data as local variables using with(){} 22 | "with(obj){p.push('" + 23 | 24 | // Convert the template into pure JavaScript 25 | str 26 | .replace(/[\r\t\n]/g, " ") 27 | .split("<%").join("\t") 28 | .replace(/((^|%>)[^\t]*)'/g, "$1\r") 29 | .replace(/\t=(.*?)%>/g, "',$1,'") 30 | .split("\t").join("');") 31 | .split("%>").join("p.push('") 32 | .split("\r").join("\\'") 33 | + "');}return p.join('');"); 34 | 35 | //console.log(fn); 36 | 37 | // Provide some basic currying to the user 38 | return data ? fn(data) : fn; 39 | }catch(e) { 40 | console.log(e); 41 | } 42 | }; 43 | })(window); 44 | -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard', [ 19 | 'ngResource', 20 | 'ngRoute', 21 | 'ui.router', 22 | 'ui.bootstrap', 23 | 'springCloudDashboard.services', 24 | 'nvd3ChartDirectives' 25 | ]) 26 | .config(function ($stateProvider, $urlRouterProvider) { 27 | $urlRouterProvider 28 | .when('/', '/app') 29 | .otherwise('/'); 30 | $stateProvider 31 | .state('overview', { 32 | url: '/app', 33 | templateUrl: 'views/overview.html', 34 | controller: 'overviewCtrl' 35 | }) 36 | .state('overview.select', { 37 | url: '/:id', 38 | templateUrl: 'views/overview.selected.html', 39 | controller: 'overviewSelectedCtrl' 40 | }) 41 | .state('about', { 42 | url: '/about', 43 | templateUrl: 'views/about.html' 44 | }) 45 | .state('apps', { 46 | abstract:true, 47 | url: '/app/:appId/instance/:instanceId', 48 | controller: 'appsCtrl', 49 | templateUrl: 'views/apps.html', 50 | resolve: { 51 | instance: ['$stateParams', 'Instance' , function($stateParams, Instance) { 52 | return Instance.query({id: $stateParams.instanceId}).$promise; 53 | }] 54 | } 55 | }) 56 | .state('history', { 57 | url: '/history', 58 | templateUrl: 'views/apps/history.html', 59 | controller: 'appsHistoryCtrl' 60 | }) 61 | .state('circuit-breaker', { 62 | url: '/circuit-breaker/:type/:id', 63 | templateUrl: 'views/circuit-breaker/index.html', 64 | controller: 'circuitBreakerCtrl' 65 | }) 66 | .state('apps.details', { 67 | url: '/details', 68 | templateUrl: 'views/apps/details.html', 69 | controller: 'detailsCtrl' 70 | }) 71 | .state('apps.details.metrics', { 72 | url: '/metrics', 73 | templateUrl: 'views/apps/details/metrics.html', 74 | controller: 'detailsMetricsCtrl' 75 | }) 76 | .state('apps.details.classpath', { 77 | url: '/classpath', 78 | templateUrl: 'views/apps/details/classpath.html', 79 | controller: 'detailsClasspathCtrl' 80 | }) 81 | .state('apps.env', { 82 | url: '/env', 83 | templateUrl: 'views/apps/environment.html', 84 | controller: 'environmentCtrl' 85 | }) 86 | .state('apps.logging', { 87 | url: '/logging', 88 | templateUrl: 'views/apps/logging.html', 89 | controller: 'loggingCtrl' 90 | }) 91 | .state('apps.jmx', { 92 | url: '/jmx', 93 | templateUrl: 'views/apps/jmx.html', 94 | controller: 'jmxCtrl' 95 | }) 96 | .state('apps.threads', { 97 | url: '/threads', 98 | templateUrl: 'views/apps/threads.html', 99 | controller: 'threadsCtrl' 100 | }) 101 | .state('apps.trace', { 102 | url: '/trace', 103 | templateUrl: 'views/apps/trace.html', 104 | controller: 'traceCtrl' 105 | }); 106 | }) 107 | .run(function ($rootScope, $state, $stateParams, $log) { 108 | $rootScope.$state = $state; 109 | $rootScope.$stateParams = $stateParams; 110 | }); 111 | -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/controllers/circuit-breaker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard') 19 | .controller('circuitBreakerCtrl', ['$scope', '$stateParams', 'Instance', 20 | function ($scope, $stateParams, Instance) { 21 | 22 | if($stateParams.type == 'app') { 23 | var stream = "/circuitBreaker.stream?appName="+$stateParams.id; 24 | $scope.subtitle = $stateParams.id; 25 | } else if($stateParams.type == 'instance') { 26 | var stream = "/circuitBreaker.stream?instanceId="+$stateParams.id; 27 | var instance = Instance.query({id: $stateParams.id}, function(instance){ 28 | $scope.subtitle = instance.name; 29 | }); 30 | } 31 | 32 | // commands 33 | $scope.hystrixMonitor = new HystrixCommandMonitor('dependencies', {includeDetailIcon:false}); 34 | 35 | // sort by error+volume by default 36 | $scope.hystrixMonitor.sortByErrorThenVolume(); 37 | 38 | // start the EventSource which will open a streaming connection to the server 39 | $scope.source = new EventSource(stream); 40 | 41 | // add the listener that will process incoming events 42 | $scope.source.addEventListener('message', $scope.hystrixMonitor.eventSourceMessageListener, false); 43 | 44 | $scope.source.addEventListener('error', function(e) { 45 | if (e.eventPhase == EventSource.CLOSED) { 46 | // Connection was closed. 47 | console.log("Connection was closed on error: " + JSON.stringify(e)); 48 | } else { 49 | console.log("Error occurred while streaming: " + JSON.stringify(e)); 50 | } 51 | }, false); 52 | 53 | // thread pool 54 | $scope.dependencyThreadPoolMonitor = new HystrixThreadPoolMonitor('dependencyThreadPools'); 55 | 56 | $scope.dependencyThreadPoolMonitor.sortByVolume(); 57 | 58 | // start the EventSource which will open a streaming connection to the server 59 | $scope.threadSource = new EventSource(stream); 60 | 61 | // add the listener that will process incoming events 62 | $scope.threadSource.addEventListener('message', $scope.dependencyThreadPoolMonitor.eventSourceMessageListener, false); 63 | 64 | $scope.threadSource.addEventListener('error', function(e) { 65 | if (e.eventPhase == EventSource.CLOSED) { 66 | // Connection was closed. 67 | console.log("Connection was closed on error: " + e); 68 | } else { 69 | console.log("Error occurred while streaming: " + e); 70 | } 71 | }, false); 72 | 73 | $scope.$on('$destroy', function clearEventSource() { 74 | if($scope.source) { $scope.source.close(); delete $scope.source; } 75 | if($scope.threadSource) { $scope.threadSource.close(); delete $scope.threadSource; } 76 | }) 77 | }]); -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/directives/richMetricBar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard') 19 | .directive('richMetricBar', function() { 20 | return { 21 | restrict: 'E', 22 | scope: { 23 | metric: '=forMetric', 24 | globalMax: '=?globalMax' 25 | }, 26 | link: function(scope) { 27 | scope.globalMax = scope.globalMax || scope.metric.max; 28 | scope.minWidth = (scope.metric.min / scope.globalMax * 100).toFixed(2); 29 | scope.avgWidth = (scope.metric.avg / scope.globalMax * 100).toFixed(2); 30 | scope.valueWidth = (scope.metric.value / scope.globalMax * 100).toFixed(2); 31 | scope.maxWidth = (scope.metric.max / scope.globalMax * 100).toFixed(2); 32 | }, 33 | template: '
\ 34 | {{metric.name}} (count: {{metric.count}}) \ 35 |
\ 36 |
\ 37 |
\ 38 |
\ 39 |
\ 40 |
{{metric.value}}
\ 41 |
\ 42 |
\ 43 |
{{metric.min}}
\ 44 |
{{metric.max}}
\ 45 |
{{metric.avg}}
\ 46 |
\ 47 |
\ 48 |
{{metric.value}}
\ 49 |
\ 50 |
' 51 | } 52 | }); -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/directives/simpleMetricBar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard') 19 | .directive('simpleMetricBar', function() { 20 | return { 21 | restrict: 'E', 22 | scope: { 23 | metric: '=forMetric', 24 | globalMax: '=globalMax' 25 | }, 26 | link: function(scope) { 27 | scope.valueWidth = (scope.metric.value / scope.globalMax * 100).toFixed(2); 28 | }, 29 | template: '
\ 30 | {{metric.name}}\ 31 |
\ 32 |
{{metric.value}}
\ 33 |
\ 34 |
' 35 | }; 36 | }); 37 | -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/filters/filters.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard') 19 | .filter('timeInterval', function() { 20 | function padZero(i,n) { 21 | var s = i + ""; 22 | while (s.length < n) s = "0" + s; 23 | return s; 24 | } 25 | 26 | return function(input) { 27 | var s = input || 0; 28 | var d = padZero(Math.floor(s / 86400000),2); 29 | var h = padZero(Math.floor(s % 86400000 / 3600000),2); 30 | var m = padZero(Math.floor(s % 3600000 / 60000),2); 31 | var s = padZero(Math.floor(s % 60000 / 1000),2); 32 | return d + ':' + h + ':' + m + ':' + s; 33 | } 34 | }) 35 | .filter('classNameLoggerOnly', function() { 36 | return function(input, active) { 37 | if (!active) { 38 | return input; 39 | } 40 | var result = []; 41 | for (var j in input) { 42 | var name = input[j].name; 43 | var i = name.lastIndexOf('.') + 1; 44 | if ( name.charAt(i) === name.charAt(i).toUpperCase() ) { 45 | result.push(input[j]); 46 | } 47 | } 48 | return result; 49 | } 50 | }) 51 | .filter('capitalize', function() { 52 | return function(input, active) { 53 | var s = input + ""; 54 | return s.charAt(0).toUpperCase() + s.slice(1); 55 | } 56 | }) 57 | .filter('flatten', function($filter) { 58 | var flatten = function (obj, prefix) { 59 | if (obj instanceof Date) { 60 | obj = $filter('date')(obj, 'dd.MM.yyyy HH:mm:ss'); 61 | } 62 | if (typeof obj === 'boolean' || typeof obj === 'string' || typeof obj === 'number') { 63 | return (prefix ? prefix + ': ' : '') + obj; 64 | } 65 | 66 | var result = ''; 67 | var first = true; 68 | angular.forEach(obj, function (value, key) { 69 | if (angular.isString(value) && (key === 'time' || key === 'timestamp')) { 70 | if (/^\d+$/.test(value)) { 71 | value = new Date(value * 1000); 72 | } else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{4}$/.test(value)) { 73 | value = new Date(value); 74 | } 75 | } 76 | 77 | if (first) { 78 | first = false; 79 | } else { 80 | result = result + '\n'; 81 | } 82 | 83 | result = result + flatten(value, prefix ? prefix + '.' + key : key); 84 | }); 85 | 86 | return result; 87 | }; 88 | return flatten; 89 | }) 90 | .filter('joinArray', function() { 91 | return function (input, separator) { 92 | if (!Array.isArray(input) ) { 93 | return input; 94 | } else { 95 | return input.join(separator); 96 | } 97 | }; 98 | }) 99 | .filter('humanBytes', function () { 100 | var units = { B: Math.pow(1024, 0) 101 | , K: Math.pow(1024, 1) 102 | , M: Math.pow(1024, 2) 103 | , G: Math.pow(1024, 3) 104 | , T: Math.pow(1024, 4) 105 | , P: Math.pow(1024, 5) 106 | }; 107 | 108 | return function (input, unit) { 109 | input = input || 0; 110 | unit = unit || 'B'; 111 | 112 | var bytes = input * (units[unit] || 1 ); 113 | 114 | var chosen = 'B'; 115 | for (var u in units) { 116 | if (units[chosen] < units[u] && bytes >= units[u]) { 117 | chosen = u; 118 | } 119 | } 120 | 121 | return (bytes / units[chosen]).toFixed(1).replace(/\.0$/, '').replace(/,/g, '') + chosen; 122 | }; 123 | }) 124 | .filter('capitalize', function () { 125 | return function (input) { 126 | var s = input + ''; 127 | return s.charAt(0).toUpperCase() + s.slice(1); 128 | }; 129 | }); -------------------------------------------------------------------------------- /src/main/webapp/public/scripts/services/services.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by instancelicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | angular.module('springCloudDashboard.services', ['ngResource']) 19 | .factory('Applications', ['$resource', function($resource) { 20 | return $resource( 21 | 'api/applications', {}, { 22 | query: { method:'GET', isArray:true } 23 | }); 24 | } 25 | ]) 26 | .service('ApplicationOverview', ['$http', function($http) { 27 | this.getCircuitBreakerInfo = function(application) { 28 | return $http.head('/circuitBreaker.stream', { 29 | params: {appName: application.name} 30 | }).success(function(response) { 31 | application.circuitBreaker = true; 32 | }).error(function() { 33 | application.circuitBreaker = false; 34 | }); 35 | }; 36 | }]) 37 | .factory('Instance', ['$resource', 'InstanceDetails', function($resource, InstanceDetails) { 38 | return $resource( 39 | 'api/instance/:id', {}, { 40 | query: { 41 | method:'GET', 42 | transformResponse: function(data) { 43 | var instance = angular.fromJson(data); 44 | InstanceDetails.getCapabilities(instance); 45 | return instance; 46 | } 47 | } 48 | }); 49 | } 50 | ]) 51 | .factory('InstancesHistory', ['$resource', function($resource) { 52 | return $resource( 53 | 'api/registry/history', {}, { 54 | query: { method:'GET'} 55 | }); 56 | } 57 | ]) 58 | .service('InstanceOverview', ['$http', function($http) { 59 | this.getInfo = function(instance) { 60 | return $http.get('api/instance/'+ instance.id + '/info/').success(function(response) { 61 | var appInfo = response.app || response; 62 | instance.version = appInfo.version; 63 | instance.info = appInfo; 64 | }).error(function() { 65 | instance.version = '---'; 66 | }); 67 | }; 68 | this.getHealth = function(instance) { 69 | return $http.get('api/instance/'+ instance.id + '/health/').success(function (response) { 70 | instance.health = response.status; 71 | }).error(function (response, httpStatus) { 72 | if (httpStatus === 503) { 73 | instance.health = response.status; 74 | } else if (httpStatus === 404) { 75 | instance.health = 'OFFLINE'; 76 | } else { 77 | instance.health = 'UNKNOWN'; 78 | } 79 | }); 80 | }; 81 | }]) 82 | .service('InstanceDetails', ['$http', function($http) { 83 | 84 | var _this = this; 85 | this.isEndpointPresent = function(endpoint, configprops) { 86 | return !!configprops[endpoint]; 87 | }; 88 | 89 | this.getCapabilities = function(instance) { 90 | instance.capabilities = {}; 91 | 92 | $http.get('api/instance/' + instance.id + '/configprops').success(function(configprops) { 93 | instance.capabilities.logfile = _this .isEndpointPresent('logfileEndpoint', configprops); 94 | instance.capabilities.restart = _this .isEndpointPresent('restartEndpoint', configprops); 95 | instance.capabilities.refresh = _this .isEndpointPresent('refreshEndpoint', configprops); 96 | instance.capabilities.pause = _this .isEndpointPresent('pauseEndpoint', configprops); 97 | instance.capabilities.resume = _this .isEndpointPresent('resumeEndpoint', configprops); 98 | }); 99 | }; 100 | 101 | this.getInfo = function(instance) { 102 | return $http.get('api/instance/'+ instance.id + '/info/'); 103 | }; 104 | this.getMetrics = function(instance) { 105 | return $http.get('api/instance/'+ instance.id + '/metrics/'); 106 | }; 107 | this.getEnv = function (instance, key) { 108 | if(key) { 109 | return $http.get('api/instance/' + instance.id + '/env' + (key ? '/' + key : '' )); 110 | } else { 111 | return $http.get('api/instance/'+ instance.id + '/env/'); 112 | } 113 | }; 114 | this.setEnv = function (instance, map) { 115 | return $http.post('api/instance/' + instance.id + '/env', '', {params: map}); 116 | }; 117 | this.resetEnv = function (instance) { 118 | return $http.post('api/instance/' + instance.id + '/env/reset'); 119 | }; 120 | this.refresh = function (instance) { 121 | return $http.post('api/instance/' + instance.id + '/refresh'); 122 | }; 123 | this.getHealth = function(instance) { 124 | return $http.get('api/instance/'+ instance.id + '/health/'); 125 | }; 126 | this.getTraces = function (instance) { 127 | return $http.get('api/instance/' + instance.id + '/trace'); 128 | }; 129 | this.getCircuitBreakerInfo = function(instance) { 130 | return $http.head('/circuitBreaker.stream', { 131 | params: {instanceId: instance.id} 132 | }); 133 | }; 134 | }]) 135 | .service('InstanceLogging', ['$http' , 'Jolokia', function($http, jolokia) { 136 | var LOGBACK_MBEAN = 'ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator'; 137 | 138 | this.getLoglevel = function(instance, loggers) { 139 | var requests = []; 140 | for (var j in loggers) { 141 | requests.push({ type: 'exec', mbean: LOGBACK_MBEAN, operation: 'getLoggerEffectiveLevel', arguments: [ loggers[j].name ] }) 142 | } 143 | return jolokia.bulkRequest('api/instance/'+ instance.id + '/jolokia/', requests); 144 | }; 145 | 146 | this.setLoglevel = function(instance, logger, level) { 147 | return jolokia.exec('api/instance/'+ instance.id + '/jolokia/', LOGBACK_MBEAN, 'setLoggerLevel' , [ logger, level] ); 148 | }; 149 | 150 | this.getAllLoggers = function(instance) { 151 | return jolokia.readAttr('api/instance/'+ instance.id + '/jolokia/', LOGBACK_MBEAN, 'LoggerList'); 152 | } 153 | }]) 154 | .service('InstanceJMX', ['$rootScope', 'Abbreviator', 'Jolokia', function($rootScope, Abbreviator, jolokia) { 155 | this.list = function(instance) { 156 | return jolokia.list('api/instance/'+ instance.id + '/jolokia/').then(function(response) { 157 | var domains = []; 158 | for (var rDomainName in response.value) { 159 | var rDomain = response.value[rDomainName]; 160 | var domain = {name : rDomainName, beans: [] }; 161 | 162 | for (var rBeanName in rDomain ) { 163 | var rBean = rDomain[rBeanName]; 164 | var bean = { id : domain.name + ':' + rBeanName, 165 | name : '', 166 | nameProps: {}, 167 | description : rBean.desc, 168 | operations : rBean.op, 169 | attributes : rBean.attr 170 | }; 171 | 172 | var name = ''; 173 | var type = ''; 174 | var parts = rBeanName.split(','); 175 | for (var i in parts ) { 176 | var tokens = parts[i].split('='); 177 | if (tokens[0].toLowerCase() === 'name') { 178 | name = tokens[1]; 179 | } else{ 180 | bean.nameProps[tokens[0]] = tokens[1]; 181 | if ((tokens[0].toLowerCase() === 'type' || tokens[0].toLowerCase() == 'j2eetype') && type.length ==0 ) { 182 | type = tokens[1]; 183 | } 184 | } 185 | } 186 | 187 | if (name.length !== 0) { 188 | bean.name = name; 189 | } 190 | if ( type.length !== 0) { 191 | if (bean.name !== 0) { 192 | bean.name += ' '; 193 | } 194 | bean.name += '[' + Abbreviator.abbreviate(type, '.', 25, 1, 1) + ']'; 195 | } 196 | 197 | if (bean.name.length === 0) { 198 | bean.name = rBeanName; 199 | } 200 | 201 | domain.beans.push(bean); 202 | } 203 | 204 | domains.push(domain); 205 | } 206 | 207 | return domains; 208 | }, function(response) { 209 | return response; 210 | }); 211 | }; 212 | 213 | this.readAllAttr = function(instance, bean) { 214 | return jolokia.read('api/instance/'+ instance.id + '/jolokia/', bean.id) 215 | }; 216 | 217 | this.writeAttr = function(instance, bean, attr, val) { 218 | return jolokia.writeAttr('api/instance/'+ instance.id + '/jolokia/', bean.id, attr, val); 219 | }; 220 | 221 | this.invoke = function(instance, bean, opname, args) { 222 | return jolokia.exec('api/instance/'+ instance.id + '/jolokia/', bean.id, opname, args); 223 | } 224 | 225 | }]) 226 | .service('Abbreviator', [function() { 227 | function _computeDotIndexes(fqName, delimiter, preserveLast) { 228 | var dotArray = []; 229 | 230 | //iterate over String and find dots 231 | var lastIndex = -1; 232 | do { 233 | lastIndex = fqName.indexOf(delimiter, lastIndex + 1); 234 | if (lastIndex !== -1) { 235 | dotArray.push(lastIndex); 236 | } 237 | } while (lastIndex !== -1) 238 | 239 | // remove dots to preserve more than the last element 240 | for (var i = 0; i < preserveLast -1; i++ ) { 241 | dotArray.pop(); 242 | } 243 | 244 | return dotArray; 245 | } 246 | 247 | function _computeLengthArray(fqName, targetLength, dotArray, shortenThreshold) { 248 | var lengthArray = []; 249 | var toTrim = fqName.length - targetLength; 250 | 251 | for (var i = 0; i < dotArray.length; i++) { 252 | var previousDotPosition = -1; 253 | if (i > 0) { 254 | previousDotPosition = dotArray[i - 1]; 255 | } 256 | 257 | var len = dotArray[i] - previousDotPosition - 1; 258 | var newLen = (toTrim > 0 && len > shortenThreshold ? 1 : len); 259 | 260 | toTrim -= (len - newLen); 261 | lengthArray[i] = newLen + 1; 262 | } 263 | 264 | var lastDotIndex = dotArray.length - 1; 265 | lengthArray[dotArray.length] = fqName.length - dotArray[lastDotIndex]; 266 | 267 | return lengthArray; 268 | } 269 | 270 | this.abbreviate = function(fqName, delimiter, targetLength, preserveLast, shortenThreshold) { 271 | if (fqName.length < targetLength) { 272 | return fqName; 273 | } 274 | 275 | var dotIndexesArray = _computeDotIndexes(fqName, delimiter, preserveLast); 276 | 277 | if (dotIndexesArray.length === 0) { 278 | return fqName; 279 | } 280 | 281 | var lengthArray = _computeLengthArray(fqName, targetLength, dotIndexesArray, shortenThreshold); 282 | 283 | var result = ""; 284 | for (var i = 0; i <= dotIndexesArray.length; i++) { 285 | if (i === 0 ) { 286 | result += fqName.substr(0, lengthArray[i] -1); 287 | } else { 288 | result += fqName.substr(dotIndexesArray[i - 1], lengthArray[i]); 289 | } 290 | } 291 | 292 | return result; 293 | } 294 | }]) 295 | .service('Jolokia', [ '$q' , '$rootScope', function($q){ 296 | var outer = this; 297 | var j4p = new Jolokia(); 298 | 299 | 300 | this.bulkRequest = function(url, requests) { 301 | var deferred = $q.defer(); 302 | deferred.notify(requests); 303 | 304 | var hasError = false; 305 | var responses = []; 306 | 307 | j4p.request( requests, 308 | { url: url, 309 | method: 'post', 310 | success: function (response) { 311 | responses.push(response); 312 | if (responses.length >= requests.length) { 313 | if (!hasError) { 314 | deferred.resolve(responses); 315 | } else { 316 | deferred.resolve(responses); 317 | } 318 | } 319 | }, 320 | error: function (response) { 321 | hasError = true; 322 | responses.push(response); 323 | if (responses.length >= requests.length) { 324 | deferred.reject(responses); 325 | } 326 | } 327 | }); 328 | 329 | return deferred.promise; 330 | }; 331 | 332 | 333 | this.request = function(url, request) { 334 | var deferred = $q.defer(); 335 | deferred.notify(request); 336 | 337 | j4p.request( request, 338 | { url: url, 339 | method: 'post', 340 | success: function (response) { 341 | deferred.resolve(response); 342 | }, 343 | error: function (response) { 344 | deferred.reject(response); 345 | } 346 | }); 347 | 348 | return deferred.promise; 349 | }; 350 | 351 | this.exec = function(url, mbean, op, args) { 352 | return outer.request(url, { type: 'exec', mbean: mbean, operation: op, arguments: args }); 353 | }; 354 | 355 | this.read = function(url, mbean) { 356 | return outer.request(url, { type: 'read', mbean: mbean }); 357 | }; 358 | 359 | this.readAttr = function(url, mbean, attr) { 360 | return outer.request(url, { type: 'read', mbean: mbean, attribute: attr }); 361 | }; 362 | 363 | this.writeAttr = function(url, mbean, attr, val) { 364 | return outer.request(url, { type: 'write', mbean: mbean, attribute: attr, value: val }); 365 | }; 366 | 367 | this.list = function(url) { 368 | return outer.request(url, { type: 'list' }); 369 | } 370 | }]) 371 | .service('MetricsHelper', [function() { 372 | this.find = function (metrics, regexes, callbacks) { 373 | for (var metric in metrics) { 374 | for (var i in regexes) { 375 | var match = regexes[i].exec(metric); 376 | if (match != null) { 377 | callbacks[i](metric, match, metrics[metric]); 378 | break; 379 | } 380 | } 381 | } 382 | } 383 | }]) 384 | .service('InstanceThreads', ['$http', function($http) { 385 | this.getDump = function(instance) { 386 | return $http.get('api/instance/'+ instance.id + '/dump/'); 387 | } 388 | } ]); 389 | -------------------------------------------------------------------------------- /src/main/webapp/public/styles/circuit-breaker.css: -------------------------------------------------------------------------------- 1 | /** Command **/ 2 | .dependencies .spacer { 3 | width: 100%; 4 | margin: 0 auto; 5 | padding-top:4px; 6 | clear:both; 7 | } 8 | 9 | 10 | .dependencies .last { 11 | margin-right: 0px; 12 | } 13 | 14 | .dependencies span.loading { 15 | display: block; 16 | padding-top: 6%; 17 | padding-bottom: 6%; 18 | color: gray; 19 | text-align: center; 20 | } 21 | 22 | .dependencies span.loading.failed { 23 | color: red; 24 | } 25 | 26 | 27 | .dependencies div.monitor { 28 | float: left; 29 | margin-right:5px; 30 | margin-top:5px; 31 | } 32 | 33 | .dependencies div.monitor p.name { 34 | font-weight:bold; 35 | font-size: 10pt; 36 | text-align: right; 37 | padding-bottom: 5px; 38 | } 39 | 40 | .dependencies div.monitor_data { 41 | margin: 0 auto; 42 | } 43 | 44 | /* override the HREF when we have specified it as a infotip to not act like a link */ 45 | .dependencies div.monitor_data a.infotip { 46 | text-decoration: none; 47 | cursor: default; 48 | } 49 | 50 | .dependencies div.monitor_data div.counters { 51 | text-align: right; 52 | padding-bottom: 10px; 53 | font-size: 10pt; 54 | clear: both; 55 | 56 | } 57 | 58 | .dependencies div.monitor_data div.counters div.cell { 59 | display: inline; 60 | float: right; 61 | } 62 | 63 | .dependencies .borderRight { 64 | border-right: 1px solid grey; 65 | padding-right: 6px; 66 | padding-left: 8px; 67 | } 68 | 69 | .dependencies div.cell .line { 70 | display: block; 71 | } 72 | 73 | .dependencies div.monitor_data a, 74 | .dependencies span.rate_value { 75 | font-weight:bold; 76 | } 77 | 78 | 79 | .dependencies span.smaller { 80 | font-size: 8pt; 81 | color: grey; 82 | } 83 | 84 | 85 | 86 | .dependencies div.tableRow { 87 | width:100%; 88 | white-space: nowrap; 89 | font-size: 8pt; 90 | margin: 0 auto; 91 | clear:both; 92 | padding-left:26%; 93 | } 94 | 95 | .dependencies div.tableRow .cell { 96 | float:left; 97 | } 98 | 99 | .dependencies div.tableRow .header { 100 | width:20%; 101 | text-align:right; 102 | } 103 | 104 | .dependencies div.tableRow .data { 105 | width:30%; 106 | font-weight: bold; 107 | text-align:right; 108 | } 109 | 110 | 111 | .dependencies div.monitor { 112 | width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ 113 | height: 150px; 114 | } 115 | 116 | .dependencies .success { 117 | color: green; 118 | } 119 | .dependencies .shortCircuited { 120 | color: blue; 121 | } 122 | .dependencies .timeout { 123 | color: #FF9900; /* shade of orange */ 124 | } 125 | .dependencies .failure { 126 | color: red; 127 | } 128 | 129 | .dependencies .rejected { 130 | color: purple; 131 | } 132 | 133 | .dependencies .exceptionsThrown { 134 | color: brown; 135 | } 136 | 137 | .dependencies div.monitor_data a.rate { 138 | color: black; 139 | font-size: 11pt; 140 | } 141 | 142 | .dependencies div.rate { 143 | padding-top: 1px; 144 | clear:both; 145 | text-align:right; 146 | } 147 | 148 | .dependencies .errorPercentage { 149 | color: grey; 150 | } 151 | 152 | .dependencies div.cell .errorPercentage { 153 | padding-left:5px; 154 | font-size: 12pt !important; 155 | } 156 | 157 | 158 | .dependencies div.monitor div.chart { 159 | } 160 | 161 | .dependencies div.monitor div.chart svg { 162 | } 163 | 164 | .dependencies div.monitor div.chart svg text { 165 | fill: white; 166 | } 167 | 168 | 169 | .dependencies div.circuitStatus { 170 | width:100%; 171 | white-space: nowrap; 172 | font-size: 9pt; 173 | margin: 0 auto; 174 | clear:both; 175 | text-align:right; 176 | padding-top: 4px; 177 | } 178 | 179 | .dependencies #hidden { 180 | width:1px; 181 | height:1px; 182 | background: lightgrey; 183 | display: none; 184 | } 185 | 186 | 187 | 188 | /* sparkline */ 189 | .dependencies path { 190 | stroke: steelblue; 191 | stroke-width: 1; 192 | fill: none; 193 | } 194 | 195 | /** Thread Pool **/ 196 | .dependencyThreadPools .spacer { 197 | width: 100%; 198 | margin: 0 auto; 199 | padding-top:4px; 200 | clear:both; 201 | } 202 | 203 | 204 | .dependencyThreadPools .last { 205 | margin-right: 0px; 206 | } 207 | 208 | .dependencyThreadPools span.loading { 209 | display: block; 210 | padding-top: 6%; 211 | padding-bottom: 6%; 212 | color: gray; 213 | text-align: center; 214 | } 215 | 216 | .dependencyThreadPools span.loading.failed { 217 | color: red; 218 | } 219 | 220 | 221 | .dependencyThreadPools div.monitor { 222 | float: left; 223 | margin-right:5px; /* these are tweaked to look good on desktop and iPad portrait, and fit things densely */ 224 | margin-top:5px; 225 | } 226 | 227 | .dependencyThreadPools div.monitor p.name { 228 | font-weight:bold; 229 | font-size: 10pt; 230 | text-align: right; 231 | padding-bottom: 5px; 232 | } 233 | 234 | .dependencyThreadPools div.monitor_data { 235 | margin: 0 auto; 236 | } 237 | 238 | .dependencyThreadPools span.smaller { 239 | font-size: 8pt; 240 | color: grey; 241 | } 242 | 243 | 244 | .dependencyThreadPools div.tableRow { 245 | width:100%; 246 | white-space: nowrap; 247 | font-size: 8pt; 248 | margin: 0 auto; 249 | clear:both; 250 | } 251 | 252 | .dependencyThreadPools div.tableRow .cell { 253 | float:left; 254 | } 255 | 256 | .dependencyThreadPools div.tableRow .header { 257 | text-align:right; 258 | padding-right:5px; 259 | } 260 | 261 | .dependencyThreadPools div.tableRow .header.left { 262 | width:85px; 263 | } 264 | 265 | .dependencyThreadPools div.tableRow .header.right { 266 | width:75px; 267 | } 268 | 269 | .dependencyThreadPools div.tableRow .data { 270 | font-weight: bold; 271 | text-align:right; 272 | } 273 | 274 | .dependencyThreadPools div.tableRow .data.left { 275 | width:30px; 276 | } 277 | 278 | .dependencyThreadPools div.tableRow .data.right { 279 | width:45px; 280 | } 281 | 282 | .dependencyThreadPools div.monitor { 283 | width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */ 284 | height: 110px; 285 | } 286 | 287 | 288 | 289 | 290 | 291 | /* override the HREF when we have specified it as a infotip to not act like a link */ 292 | .dependencyThreadPools div.monitor_data a.infotip { 293 | text-decoration: none; 294 | cursor: default; 295 | } 296 | 297 | .dependencyThreadPools div.monitor_data a.rate { 298 | font-weight:bold; 299 | color: black; 300 | font-size: 11pt; 301 | } 302 | 303 | .dependencyThreadPools div.rate { 304 | padding-top: 1px; 305 | clear:both; 306 | text-align:right; 307 | } 308 | 309 | .dependencyThreadPools span.rate_value { 310 | font-weight:bold; 311 | } 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | .dependencyThreadPools div.monitor div.chart { 320 | } 321 | 322 | .dependencyThreadPools div.monitor div.chart svg { 323 | } 324 | 325 | .dependencyThreadPools div.monitor div.chart svg text { 326 | fill: white; 327 | } 328 | 329 | .dependencyThreadPools #hidden { 330 | width:1px; 331 | height:1px; 332 | background: lightgrey; 333 | display: none; 334 | } -------------------------------------------------------------------------------- /src/main/webapp/public/styles/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Varela Round'; 3 | src: url('../fonts/varela_round-webfont.eot'); 4 | src: url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/varela_round-webfont.woff') format('woff'), 6 | url('../fonts/varela_round-webfont.ttf') format('truetype'), 7 | url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Montserrat'; 14 | src: url('../fonts/montserrat-webfont.eot'); 15 | src: url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'), 16 | url('../fonts/montserrat-webfont.woff') format('woff'), 17 | url('../fonts/montserrat-webfont.ttf') format('truetype'), 18 | url('../fonts/montserrat-webfont.svg#montserratregular') format('svg'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | /** main classes **/ 24 | html { 25 | position: relative; 26 | min-height: 100%; 27 | } 28 | 29 | body { 30 | background-color: #f1f1f1; 31 | font-family: "Varela Round",sans-serif; 32 | margin-bottom: 50px; 33 | } 34 | 35 | .content { 36 | margin-top: 20px; 37 | margin-bottom: 50px; 38 | } 39 | 40 | .center-block { 41 | display: block; 42 | margin-left: auto; 43 | margin-right: auto; 44 | } 45 | 46 | dd { 47 | margin-left: 10px; 48 | } 49 | 50 | a { 51 | color: #5fa134; 52 | -moz-user-select: none; 53 | } 54 | 55 | a:hover, 56 | a:active { 57 | color: #5fa134; 58 | } 59 | 60 | pre { 61 | white-space: pre; 62 | } 63 | 64 | .btn-inverse:hover, .btn-inverse:focus, .btn-inverse:active, .btn-inverse.active, .btn-inverse.disabled, .btn-inverse[disabled] { 65 | background-color: #222; 66 | color: #fff; 67 | } 68 | 69 | .glyphicon-white { 70 | color: #fff; 71 | } 72 | 73 | .btn-no-border-left { 74 | margin-left: -1px; 75 | border-bottom-left-radius: 0; 76 | border-top-left-radius: 0; 77 | } 78 | 79 | .btn-no-border-right { 80 | border-bottom-right-radius: 0; 81 | border-top-right-radius: 0; 82 | } 83 | 84 | .bottom-margin { 85 | margin-bottom: 20px; 86 | } 87 | 88 | .no-margin-left { 89 | margin-left: 0; 90 | } 91 | 92 | .main-loader-mask { 93 | display: block; 94 | position: absolute; 95 | height: auto; 96 | bottom: 0; 97 | top: 0; 98 | left: 0; 99 | right: 0; 100 | margin: 180px 15px 50px; 101 | background-color:#f1f1f1; 102 | z-index:9999; 103 | } 104 | .loading-big { 105 | display: block; 106 | position: absolute; 107 | bottom: 0; 108 | top: -30px; 109 | left: 0; 110 | right: 0; 111 | margin: 0; 112 | background: url("../img/loading-big.gif") no-repeat center; 113 | } 114 | 115 | /** TAB LEFT **/ 116 | .tab-content { 117 | overflow: auto; 118 | } 119 | 120 | .tabs-left > .nav { 121 | border-right: 1px solid #ddd; 122 | float: left; 123 | margin-right: 19px; 124 | } 125 | 126 | /** NVD3 **/ 127 | .nvd3 text, div.nvtooltip { 128 | font-size: 14px; 129 | } 130 | 131 | /** HEADER **/ 132 | .header--navbar { 133 | margin: 0; 134 | } 135 | 136 | .navbar { 137 | border: 0; 138 | } 139 | 140 | .navbar-nav>li>span { 141 | padding-top: 15px; 142 | padding-bottom: 15px; 143 | line-height: 20px; 144 | display: block; 145 | font-size: 20px; 146 | } 147 | 148 | .navbar-static-top { 149 | font-family: Montserrat,sans-serif; 150 | position: absolute; 151 | z-index: 999; 152 | background-image: none; 153 | filter: none; 154 | background-color: #34302d; 155 | border: none; 156 | border-top: 4px solid #6db33f; 157 | box-shadow: none; 158 | position: relative; 159 | border-radius: 0; 160 | padding: 0; 161 | margin-bottom: 0; 162 | } 163 | .navbar-static-top .spring-logo--container { 164 | display: inline-block; 165 | } 166 | .navbar-static-top .spring-logo--container .spring-logo { 167 | margin: 12px 0 6px; 168 | width: 160px; 169 | height: 46px; 170 | display: inline-block; 171 | text-decoration: none; 172 | } 173 | .navbar-static-top .navbar-link.active a { 174 | background-color: #6db33f; 175 | box-shadow: none; 176 | } 177 | .navbar-static-top .navbar-link a { 178 | color: #eeeeee; 179 | text-transform: uppercase; 180 | text-shadow: none; 181 | font-size: 14px; 182 | line-height: 14px; 183 | padding: 28px 20px; 184 | transition: all 0.15s; 185 | -webkit-transition: all 0.15s; 186 | -moz-transition: all 0.15s; 187 | -o-transition: all 0.15s; 188 | -ms-transition: all 0.15s; 189 | } 190 | .navbar-static-top .navbar-link:hover a { 191 | color: #eeeeee; 192 | background-color: #6db33f; 193 | } 194 | .navbar-static-top .navbar-link.nav-search { 195 | padding: 20px 0 23px; 196 | } 197 | .navbar-static-top .navbar-link.nav-search .navbar-search--icon { 198 | color: #eeeeee; 199 | font-size: 24px; 200 | padding: 3px 16px 3px 18px; 201 | cursor: pointer; 202 | } 203 | .nav, .pagination, .carousel, .panel-title a { 204 | cursor: pointer; 205 | } 206 | .navbar-static-top .navbar-link.nav-search:hover .navbar-search--icon { 207 | text-shadow: 0 0 10px #6db33f; 208 | } 209 | .navbar-static-top .navbar-link.nav-search .search-input-close { 210 | display: none; 211 | } 212 | .navbar-static-top .navbar-link.nav-search.js-highlight { 213 | background-color: #6db33f; 214 | } 215 | 216 | a.spring-logo { 217 | background: url("../img/spring-logo.png") -1px -1px no-repeat; 218 | } 219 | 220 | a.spring-logo span { 221 | display: block; 222 | width: 160px; 223 | height: 46px; 224 | background: url("../img/spring-logo.png") -1px -48px no-repeat; 225 | opacity: 0; 226 | -moz-transition: opacity 0.12s ease-in-out; 227 | -webkit-transition: opacity 0.12s ease-in-out; 228 | -o-transition: opacity 0.12s ease-in-out; 229 | } 230 | 231 | a:hover.spring-logo span { 232 | opacity: 1; 233 | } 234 | 235 | .icon-bar { 236 | background-color: white; 237 | } 238 | 239 | /** FOOTER **/ 240 | .footer { 241 | position: absolute; 242 | bottom: 0; 243 | width: 100%; 244 | background-color: #34302d; 245 | color: #eeeeee; 246 | height: 50px; 247 | padding: 20px 0; 248 | } 249 | .footer a { 250 | color: #6db33f; 251 | } 252 | 253 | /** LOGOS **/ 254 | a.spring-boot-logo { 255 | background: url("../img/platform-spring-boot.png") -1px -1px no-repeat; 256 | } 257 | 258 | a.spring-boot-logo span { 259 | display: block; 260 | width: 160px; 261 | height: 50px; 262 | background: url("../img/platform-spring-boot.png") 20px -6px no-repeat; 263 | } 264 | 265 | a:hover.spring-boot-logo span { 266 | opacity: 1; 267 | } 268 | 269 | /** Status in Application Overview-View **/ 270 | span.status-UP { 271 | color: #00AA00; 272 | font-weight: bold; 273 | } 274 | 275 | span.status-OFFLINE { 276 | color: #000000; 277 | font-weight: bold; 278 | } 279 | 280 | span.status-DOWN, 281 | span.status-OUT_OF_SERVICE { 282 | color: #DD0000; 283 | font-weight: bold; 284 | } 285 | 286 | span.status-UNKNOWN, 287 | span.status-STARTING { 288 | font-weight: bold; 289 | color: #FF8800; 290 | } 291 | 292 | span.refresh { 293 | display: block; 294 | width: 16px; 295 | height: 16px; 296 | background: url("../img/refresh.gif") no-repeat; 297 | } 298 | 299 | /** Headings and Tabs in Detail-View **/ 300 | .panel-heading , 301 | .panel-heading > .panel-title > a, 302 | .panel-heading > .panel-title > a:hover, 303 | .panel-heading > .panel-title > a:focus, 304 | .nav-tabs > .active > a, 305 | .nav-tabs > .active > a:hover, 306 | .nav-tabs > .active > a:focus, 307 | .nav-pills > li.active > a, 308 | .nav-pills > li.active > a:hover, 309 | .nav-pills > li.active > a:focus, 310 | .table > thead > tr > th { 311 | background-color: #34302D !important; 312 | border-color: #34302D !important; 313 | color: #f1f1f1 !important; 314 | } 315 | 316 | .nav > li > a { 317 | color: #838789; 318 | } 319 | 320 | .nav { 321 | margin-bottom: 0; 322 | } 323 | 324 | .nav-tabs > .active > a, 325 | .nav-tabs > .active > a:hover, 326 | .nav-tabs > .active > a:focus, 327 | .nav-pills > li.active > a, 328 | .nav-pills > li.active > a:hover, 329 | .nav-pills > li.active > a:focus, 330 | .table > thead > tr > th { 331 | background-color: #34302D !important; 332 | border-color: #34302D !important; 333 | color: #f1f1f1 !important; 334 | } 335 | 336 | .tabs-inverse > .nav-pills > li > a { 337 | background-color: #34302D !important; 338 | border-color: #34302D !important; 339 | color: #f1f1f1 !important; 340 | } 341 | 342 | .tabs-inverse > .nav-pills > li > a:hover, 343 | .tabs-inverse > .nav-pills > li > a:focus { 344 | color: #6db33f !important; 345 | } 346 | 347 | .tabs-inverse > .nav-pills > li.active > a { 348 | background-color: #6db33f !important; 349 | border-color: #6db33f !important; 350 | color: #f1f1f1 !important; 351 | } 352 | 353 | 354 | .tabs-inverse > .nav-pills > li.active > a > tab-heading > div > span.label-success { 355 | background-color: #34302D !important; 356 | } 357 | 358 | /** highlighted rows on Detail-Environment-View **/ 359 | .table tr.highlight > td { 360 | background-color: #999 !important; 361 | font-weight: bold; 362 | } 363 | 364 | /** Tabs-list in domain-section in JMX-View **/ 365 | .tabs-left > ul { 366 | max-width: 30%; 367 | overflow: hidden; 368 | } 369 | 370 | /** Circuit breaker **/ 371 | .row-centered { 372 | width: 100%; 373 | margin: 0 auto; 374 | overflow: hidden; 375 | } 376 | 377 | .dependencies { 378 | line-height: 1; 379 | } 380 | 381 | .last { 382 | margin-right: 0px; 383 | } 384 | 385 | .menu_legend { 386 | margin-right: 20px; 387 | } 388 | 389 | .success { 390 | color: green; 391 | } 392 | .shortCircuited { 393 | color: blue; 394 | } 395 | .timeout { 396 | color: #FF9900; /* shade of orange */ 397 | } 398 | .failure { 399 | color: red; 400 | } 401 | 402 | .rejected { 403 | color: purple; 404 | } 405 | 406 | .exceptionsThrown { 407 | color: brown; 408 | } 409 | 410 | /** Headings and Tabs in Detail-View **/ 411 | .accordion-heading , 412 | .accordion-heading > a, 413 | .accordion-heading > a:hover, 414 | .accordion-heading > a:focus, 415 | .nav-tabs > .active > a, 416 | .nav-tabs > .active > a:hover, 417 | .nav-tabs > .active > a:focus, 418 | .table > thead > tr > th { 419 | background-color: #34302D; 420 | border-color: #34302D; 421 | color: #f1f1f1; 422 | } 423 | 424 | .accordion-group, 425 | .boxed { 426 | border: 1px solid #34302D; 427 | } 428 | 429 | .sortable { 430 | cursor: pointer; 431 | } 432 | 433 | .sorted-ascending, 434 | .sorted-descending { 435 | color: #6db33f; 436 | } 437 | 438 | .sorted-ascending::after { 439 | content: " \25b2"; 440 | } 441 | .sorted-descending::after { 442 | content: " \25bc"; 443 | } 444 | 445 | .nav > li > a { 446 | color: #838789; 447 | } 448 | 449 | .nav { 450 | margin-bottom: 0; 451 | } 452 | 453 | /** Tabs-list in domain-section in JMX-View **/ 454 | .tabs-left > ul { 455 | max-width: 30%; 456 | overflow: hidden; 457 | } 458 | 459 | /** health status**/ 460 | dl.health-status > dt { 461 | border-bottom: 1px solid #ddd; 462 | background-color: #f9f9f9; 463 | line-height: 20px; 464 | padding: 8px; 465 | } 466 | 467 | dl.health-status { 468 | margin-top: 0px; 469 | } 470 | 471 | dl.health-status dl.health-status { 472 | margin-top: 20px !important; 473 | } 474 | 475 | dl.health-status td { 476 | border-top: none; 477 | border-bottom: 1px solid #ddd; 478 | } 479 | 480 | 481 | /** Metric Bars **/ 482 | .bar-offset { 483 | float: left; 484 | box-sizing: border-box; 485 | height: 100%; 486 | } 487 | 488 | .with-marks { 489 | position: relative; 490 | } 491 | 492 | .bar-scale { 493 | position: relative; 494 | margin-top: -20px; 495 | margin-bottom: 40px; 496 | } 497 | 498 | .pitch-line { 499 | position: absolute; 500 | width: 1px; 501 | background-image: linear-gradient(to bottom, #333333 5px, transparent 5px); 502 | background-repeat: repeat-x; 503 | } 504 | 505 | .value-mark { 506 | position: absolute; 507 | border-left: 1px solid #ee5f5b; 508 | font-size: 12px; 509 | color: #fff; 510 | text-align: center; 511 | text-shadow: 0 -1px 0 rgba(0,0,0,0.25); 512 | } 513 | 514 | .mark-current { 515 | width: 1px; 516 | height: 20px; 517 | position: absolute; 518 | background-color: #dd514c; 519 | background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); 520 | background-repeat: repeat-x; 521 | } 522 | 523 | /** Timeline **/ 524 | .timeline { 525 | position: relative; 526 | list-style-type: none; 527 | padding: 1em 0 1em 120px; 528 | } 529 | 530 | .timeline:before { 531 | position: absolute; 532 | top: 0; 533 | content: ' '; 534 | display: block; 535 | width: 6px; 536 | height: 100%; 537 | margin-left: 0; 538 | background: rgb(80,80,80); 539 | background: -moz-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 540 | background: -webkit-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 541 | background: -o-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 542 | background: -ms-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 543 | background: linear-gradient(to bottom, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%); 544 | z-index: 5; 545 | } 546 | 547 | .timeline li { 548 | padding: 1em 0; 549 | } 550 | 551 | .timeline li:after { 552 | content: ""; 553 | display: block; 554 | height: 0; 555 | clear: both; 556 | visibility: hidden; 557 | } 558 | 559 | .timeline .event { 560 | position: relative; 561 | width: 100%; 562 | display: inline-block; 563 | left: 15px; 564 | padding-left: 5px; 565 | cursor:pointer; 566 | } 567 | 568 | .timeline .event .time { 569 | position: absolute; 570 | left: -120px; 571 | margin-left: -25px; 572 | display: inline-block; 573 | vertical-align: middle; 574 | text-align:right; 575 | width: 120px; 576 | } 577 | 578 | .timeline .event:before { 579 | content: ' '; 580 | display: block; 581 | width: 20px; 582 | height: 20px; 583 | background: #fff; 584 | border-radius: 10px; 585 | border: 4px solid #6db33f; 586 | z-index: 10; 587 | position: absolute; 588 | left: -6px; 589 | margin-left: -15px; 590 | } 591 | 592 | .timeline .event:hover:before { 593 | background: #ccc; 594 | } 595 | 596 | /** Application-Header **/ 597 | .header--application .navbar-inner, 598 | .header--application-urls .navbar-inner { 599 | font-family: Montserrat,sans-serif; 600 | z-index: 999; 601 | filter: none; 602 | background: #666 none; 603 | border: none; 604 | border-bottom: 1px solid #34302D; 605 | box-shadow: none; 606 | position: relative; 607 | border-radius: 0; 608 | padding: 0; 609 | } 610 | 611 | .header--application .application--name { 612 | font-family: "Montserrat",sans-serif; 613 | font-size: 24px; 614 | line-height: 24px; 615 | color: #ebf1e7; 616 | display: inline-block; 617 | margin-top: 10px; 618 | margin-bottom: 10px; 619 | } 620 | 621 | .header--application .navbar-inner .navbar-link a { 622 | color: #eeeeee; 623 | text-transform: uppercase; 624 | text-shadow: none; 625 | font-size: 14px; 626 | line-height: 14px; 627 | padding: 16px 10px; 628 | transition: all 0.15s; 629 | -webkit-transition: all 0.15s; 630 | -moz-transition: all 0.15s; 631 | -o-transition: all 0.15s; 632 | -ms-transition: all 0.15s; 633 | } 634 | 635 | .header--application .navbar-inner .navbar-link:hover a , 636 | .header--application .navbar-inner .navbar-link.active a { 637 | color: #ebf1e7; 638 | background-color: #666; 639 | box-shadow: none; 640 | } 641 | 642 | .header--application .navbar-inner .navbar-link a>span { 643 | transition: color 0.15s; 644 | -webkit-transition: color 0.15s; 645 | -moz-transition: color 0.15s; 646 | -o-transition: color 0.15s; 647 | -ms-transition: color 0.15s; 648 | } 649 | 650 | .header--application .navbar-inner .navbar-link:hover a>span, 651 | .header--application .navbar-inner .navbar-link.active a>span { 652 | border-top: 2px solid #6db33f; 653 | border-bottom: 2px solid #6db33f; 654 | } 655 | 656 | .header--application-urls .navbar-inner { 657 | background-color: #dedede; 658 | } 659 | 660 | .header--application-urls .navbar-inner li>a, 661 | .header--application-urls .navbar-inner li>a:hover { 662 | text-shadow: none; 663 | color: #34302D; 664 | } 665 | 666 | .header--application-urls .navbar-inner li>a:hover { 667 | text-decoration:underline; 668 | } 669 | 670 | /** ENV **/ 671 | .refresh-env-alert { 672 | margin-top: 50px; 673 | } -------------------------------------------------------------------------------- /src/main/webapp/public/views/about.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | This is an administration GUI for Spring Cloud applications. All applications has to register to the service registry (eg: Eureka). 4 |
5 |
-------------------------------------------------------------------------------- /src/main/webapp/public/views/apps.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 |
29 | -------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/details.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | Error: {{ error }} 5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Applicationraw JSON
{{ key }}{{ value | flatten }}
16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Gitraw JSON
{{ key }}{{ value | flatten }}
27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Health Checksraw JSON
38 |
39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 55 | 56 | 57 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Memoryraw JSON
47 | Total Memory ({{ metrics['mem.used'] / 1024 | number:2 }}M / {{ metrics['mem'] / 1024 | number:2 }}M) 48 |
49 |
51 | {{memPercentage}}% 52 |
53 |
54 |
58 | Heap Memory ({{ metrics['heap.used'] / 1024 | number:2 }}M / {{ metrics['heap.committed'] / 1024 | number:2 }}M) 59 |
60 |
62 | {{heapPercentage}}% 63 |
64 |
65 |
Initial Heap (-Xms){{metrics['heap.init'] / 1024 | number:2}}M
Maximum Heap (-Xmx){{metrics['heap'] / 1024 | number:2}}M
76 |
77 | 78 |
79 | 80 | 81 | 82 | 83 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
JVMraw JSON
Systemload (last minute average) 84 |
85 |
87 | {{metrics['systemload.averagepercent'] | number:0}}% 88 |
89 |
90 |
Uptime{{ metrics['uptime'] + ticks | timeInterval }} [d:h:m:s]
Available Processors{{ metrics['processors'] }}
Current loaded Classes{{ metrics['classes']}}
Total loaded Classes{{ metrics['classes.loaded']}}
Unloaded Classes{{ metrics['classes.unloaded']}}
Threads{{ metrics['threads'] }} total / {{ metrics['threads.daemon'] }} daemon / {{ metrics['threads.peak'] }} peak
118 |
119 | 120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
Garbage Collectionraw JSON
{{name | capitalize}} GC Count{{value.count}}
{{name | capitalize}} GC Time{{value.time}} ms
133 |
134 | 135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
Servlet Container
Http sessions{{ metrics['httpsessions.active'] }} active / {{ metrics['httpsessions.max'] == -1 ? 'unlimited' : metrics['httpsessions.max'] + ' max' }}
145 |
146 | 147 |
148 | 149 | 150 | 151 | 160 | 161 | 162 |
Datasources
152 | {{name | capitalize}} Datasource Connections (active: {{ value.active }}) 153 |
154 |
156 | {{value.usage * 100 | number}}% 157 |
158 |
159 |
163 |
164 | 165 |
166 |
167 |
168 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | 180 | 181 | 200 | 201 | 216 | 217 | 247 | -------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/details/classpath.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
Classpath raw JSON
12 | {{ element }} 13 |
17 | 18 | -------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/details/metrics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Counters
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Gauges
28 | -------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/environment.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Error: {{ error }} 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
Profiles raw JSON
{{ profile }}
No profiles active
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 60 | 61 | 62 |
Overriden properties
{{ key }}{{ value }}
30 | 31 | 32 | 33 | 34 | 36 | 37 |
44 |
45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
{{ overrides.changes }}
54 |
55 |
56 | Error: {{ overrides.error }} 57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
{{item.key}}
{{ property.key }}{{ property.value }}
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/history.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Service registry history

5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
DateInstance
{{ instance.date | date : 'short' }}{{ instance.id }}
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
DateInstance
{{ instance.date | date : 'short' }}{{ instance.id }}
40 |
41 |
42 |
43 |
-------------------------------------------------------------------------------- /src/main/webapp/public/views/apps/jmx.html: -------------------------------------------------------------------------------- 1 |
2 | Error: {{ error }} 3 |
4 |
5 | 6 | 7 | 8 | Domain {{domain.name}} 9 | 10 | 11 | 12 | 13 | 14 | MBean {{bean.name }} 15 | 16 |
17 |

18 | {{bean.name}}
{{bean.description}} 19 |

20 |
21 |
Id
22 |
{{ bean.id }}
23 |
{{ name }}
24 |
{{ value }}
25 |
26 | 27 |
28 | 29 | Attributes 30 | 31 | 32 |
33 | Error: {{ bean.error }} 34 |
35 |
36 | 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |