├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker ├── Dockerfile └── entrypoint.sh ├── example ├── java │ ├── .gitignore │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── cn │ │ └── imdada │ │ └── overwatch │ │ ├── Config.java │ │ ├── Demo.java │ │ ├── RedisDemo.java │ │ ├── ServerStats.java │ │ ├── SocketIODemo.java │ │ └── SystemFailure.java ├── rest-demo.sh └── system-stats.json ├── install.sh ├── interface ├── system-failure.dto.ts ├── system-info.dto.ts ├── system-stats.dto.ts └── user.dto.ts ├── server ├── .gitignore ├── app │ ├── app.ts │ ├── common │ │ ├── base-dao.ts │ │ ├── config.ts │ │ ├── event-source.ts │ │ ├── handlers.ts │ │ ├── log.ts │ │ ├── model-definition.ts │ │ ├── redis-util.ts │ │ ├── response-writer.ts │ │ ├── socket-util.ts │ │ └── types.ts │ ├── config.example.json │ ├── config.json │ ├── inversify.config.ts │ ├── main.ts │ ├── server.ts │ ├── service │ │ ├── system-failure │ │ │ ├── system-failure.controller.ts │ │ │ ├── system-failure.dao.ts │ │ │ ├── system-failure.dto.ts │ │ │ ├── system-failure.model.ts │ │ │ └── system-failure.service.ts │ │ ├── system-info │ │ │ ├── system-info.controller.ts │ │ │ ├── system-info.dao.ts │ │ │ ├── system-info.dto.ts │ │ │ ├── system-info.model.ts │ │ │ └── system-info.service.ts │ │ └── system-stats │ │ │ ├── system-stats.controller.ts │ │ │ ├── system-stats.dto.ts │ │ │ ├── system-stats.model.ts │ │ │ ├── system-stats.repo.ts │ │ │ └── system-stats.service.ts │ └── typings.d.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── tslint.json └── web ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routing.ts │ ├── app.style.scss │ ├── app.template.html │ ├── common │ │ ├── common.module.ts │ │ ├── connector.service.ts │ │ ├── join.pipe.ts │ │ ├── response.ts │ │ ├── socket.service.ts │ │ ├── system-failure │ │ │ ├── system-failure.dto.ts │ │ │ └── system-failure.service.ts │ │ ├── system-info │ │ │ ├── system-info.dto.ts │ │ │ ├── system-info.service.ts │ │ │ └── system-info.vo.ts │ │ ├── system-stats │ │ │ ├── system-stats.dto.ts │ │ │ ├── system-stats.service.ts │ │ │ └── system-stats.vo.ts │ │ ├── timestamp.pipe.ts │ │ └── user │ │ │ ├── layout.dto.ts │ │ │ ├── user.dto.ts │ │ │ ├── user.service.ts │ │ │ └── user.vo.ts │ ├── dashboard │ │ ├── dashboard.component.ts │ │ ├── dashboard.module.ts │ │ ├── dashboard.style.scss │ │ ├── dashboard.template.html │ │ ├── failure-roller │ │ │ ├── failure-roller.component.ts │ │ │ ├── failure-roller.style.scss │ │ │ └── failure-roller.template.html │ │ ├── system-detail │ │ │ ├── system-detail.component.ts │ │ │ ├── system-detail.style.scss │ │ │ └── system-detail.template.html │ │ └── system-summary │ │ │ ├── system-summary.component.ts │ │ │ ├── system-summary.style.scss │ │ │ └── system-summary.template.html │ ├── diagram │ │ ├── diagram.component.ts │ │ ├── diagram.module.ts │ │ ├── diagram.style.scss │ │ ├── diagram.template.html │ │ ├── layout-editor │ │ │ ├── layout-editor.component.ts │ │ │ └── layout.vo.ts │ │ ├── requests-time-chart │ │ │ ├── chart.component.ts │ │ │ └── data.vo.ts │ │ └── systems-rpc-graph │ │ │ ├── graph.component.ts │ │ │ ├── graph.style.scss │ │ │ ├── system-selected.event.ts │ │ │ └── system-stats.vo.ts │ ├── history │ │ ├── history.component.ts │ │ ├── history.module.ts │ │ ├── history.style.scss │ │ └── history.template.html │ ├── layout │ │ ├── layout-name-input-dialog.template.html │ │ ├── layout.component.ts │ │ ├── layout.module.ts │ │ ├── layout.style.scss │ │ └── layout.template.html │ └── toolbar │ │ ├── toolbar.component.ts │ │ ├── toolbar.module.ts │ │ ├── toolbar.style.scss │ │ └── toolbar.template.html ├── assets │ └── myriad-set-pro_ultralight.woff ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode 3 | 4 | # General 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | # Windows thumbnail cache files 33 | Thumbs.db 34 | ehthumbs.db 35 | ehthumbs_vista.db 36 | 37 | # Dump file 38 | *.stackdump 39 | 40 | # Folder config file 41 | Desktop.ini 42 | 43 | # Recycle Bin used on file shares 44 | $RECYCLE.BIN/ 45 | 46 | # Windows Installer files 47 | *.cab 48 | *.msi 49 | *.msm 50 | *.msp 51 | 52 | # Windows shortcuts 53 | *.lnk 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to submit pull requests. 2 | 3 | Contact Info: 4 | - thomas931225@126.com 5 | - ZstringX (wechat) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2018, 张玄 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overwatch 2 | 3 | See [demo here](http://overwatch.imdada.cn) | or [here](https://imdada.github.io/overwatch/) 4 | 5 | Overwatch is a general RPC monitoring system for distributed systems, utilizing [D3](https://github.com/d3/d3) force layout as main diagram. 6 | 7 | Overwatch provides an overview of the current state of the entire system, making it super easy for system administrators to understand the ongoing RPC events and pinpoint the source of failure in the system. 8 | 9 | Unlike common monitoring systems, a well-designed graph (with D3 force layout) is used to visualize data. 10 | 11 | ![](https://imdada.github.io/overwatch/ss-01.png) 12 | 13 | - every circle represents a system 14 | - every line represents a dependency between two systems 15 | - rpm = requests per minute, fpm = failure per minute 16 | - circle size indicates rpm of the system 17 | - line dash density indicates rpm between two systems 18 | - circle color indicates the health of the system in the last 1 minute 19 | - two additional ring outside the circle inicates the health of the system in the past 5 minutes and 15 minutes 20 | 21 | ## Installation 22 | 23 | [Download release version](#) (not available yet...) 24 | 25 | Or use install.sh to build from source 26 | 27 | ## Dependencies 28 | 29 | - NodeJS / NPM 30 | - RDBMS (MySQL is recommended) 31 | - Redis (Optional) 32 | 33 | ## Getting Started 34 | 35 | After proper installation, you have to 36 | 37 | 1. modify config files **server/app/config.json** & **web/src/environments/environment.ts** 38 | 39 | 2. start server: under **server** run 40 | 41 | ``` 42 | $ npm start 43 | ``` 44 | 45 | 3. build & serve web content: under **web** run 46 | 47 | for testing: 48 | ``` 49 | $ npm start 50 | ``` 51 | then visit localhost:4200 52 | 53 | for production: 54 | ``` 55 | $ npm run build 56 | ``` 57 | then serve static directory **web/dist** with Nginx (or whatever) 58 | 59 | --- 60 | 61 | ### Submiting Statistics 62 | 63 | #### Send individual server stats via Socket.IO client 64 | 65 | This is recommended for testing. 66 | 67 | Demo: [SocketIODemo.java](example/java/src/main/java/cn/imdada/overwatch/SocketIODemo.java) 68 | 69 | #### Send individual server stats via Redis pub/sub 70 | 71 | This is recommended for small-scale systems to publish stats. 72 | 73 | Demo: [RedisDemo.java](example/java/src/main/java/cn/imdada/overwatch/RedisDemo.java) 74 | 75 | #### Send aggregated server stats via REST 76 | 77 | This is the recommended method for large & complex systems to publish aggregated stats. 78 | 79 | Demo: [rest-demo.sh](example/rest-demo.sh) 80 | 81 | ## Licensing 82 | Please see [LICENSE](LICENSE) for more info. 83 | 84 | ## Contributing 85 | Please see [CONTRIBUTING](CONTRIBUTING.md) for more info. 86 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # NOTE: This should not be used in production! 2 | # 3 | # biuld: 4 | # docker build -t overwatch . 5 | # run: 6 | # docker run --name=overwatch-dev -p 4200:80 -p 3000:3000 -tid overwatch 7 | # attach: 8 | # docker exec -it overwatch-dev bash 9 | # 10 | # visit: http://localhost:4200 after build & run 11 | # 12 | 13 | FROM node:boron-slim 14 | MAINTAINER ZstringX 15 | WORKDIR /root 16 | 17 | # install packages 18 | RUN { \ 19 | echo debconf debconf/frontend select Noninteractive; \ 20 | echo mysql-community-server mysql-community-server/data-dir select ''; \ 21 | echo mysql-community-server mysql-community-server/root-pass password ''; \ 22 | echo mysql-community-server mysql-community-server/re-root-pass password ''; \ 23 | echo mysql-community-server mysql-community-server/remove-test-db select true; \ 24 | } | debconf-set-selections \ 25 | && apt-get update && apt-get install -y unzip nginx mysql-server 26 | 27 | # validate mysql & create database 28 | # https://serverfault.com/a/892896 29 | RUN find /var/lib/mysql -type f -exec touch {} \; && \ 30 | service mysql start && \ 31 | echo 'create database overwatch' | mysql -uroot 32 | 33 | # download source 34 | RUN wget https://github.com/imdada/overwatch/archive/master.zip && \ 35 | unzip master.zip && rm -f master.zip && mv overwatch-master overwatch 36 | 37 | # build source 38 | WORKDIR /root/overwatch 39 | RUN yes | sh install.sh 40 | WORKDIR /root/overwatch/web 41 | RUN npm run build && cp -r dist/* /var/www/html && nginx 42 | 43 | # clean 44 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 45 | 46 | WORKDIR /root/overwatch/server 47 | ADD entrypoint.sh /root/entrypoint.sh 48 | EXPOSE 80 3000 49 | ENTRYPOINT [ "/root/entrypoint.sh" ] 50 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find /var/lib/mysql -type f -exec touch {} \; && service mysql start 3 | nginx 4 | cd /root/overwatch/server && npm start 5 | -------------------------------------------------------------------------------- /example/java/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | -------------------------------------------------------------------------------- /example/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.imdada 8 | overwatch-example 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | 14 | io.socket 15 | socket.io-client 16 | 1.0.0 17 | 18 | 19 | 20 | redis.clients 21 | jedis 22 | 2.9.0 23 | 24 | 25 | 26 | com.fasterxml.jackson.core 27 | jackson-databind 28 | 2.9.1 29 | 30 | 31 | 32 | com.fasterxml.jackson.core 33 | jackson-core 34 | 2.9.1 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-compiler-plugin 44 | 3.7.0 45 | 46 | 1.8 47 | 1.8 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/Config.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | /** 4 | * Created by zhangxuan on 24/09. 5 | */ 6 | public class Config { 7 | 8 | public static final String HOST = "localhost"; 9 | public static final int PORT = 3000; 10 | public static final String REDIS_HOST = "localhost"; 11 | public static final int REDIS_PORT = 6379; 12 | public static final String IO_PATH = "/socket"; 13 | public static final String EVENT_SUBMIT_FAILURE = "submit_failure"; 14 | public static final String EVENT_SUBMIT_STATS = "submit_stats"; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/Demo.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | import java.util.Scanner; 4 | 5 | /** 6 | * Created by zhangxuan on 24/09. 7 | */ 8 | public abstract class Demo { 9 | 10 | protected void start() throws Exception { 11 | System.out.println("enter 1 to emit a system failure, 2 to emit a server stats, 0 to terminate"); 12 | System.out.println("awaiting input..."); 13 | Scanner scanner = new Scanner(System.in); 14 | while (true) { 15 | int i = scanner.nextInt(); 16 | if (i == 1) { 17 | submitSystemFailure(); 18 | System.out.println("system failure submitted"); 19 | } else if (i == 2) { 20 | submitServerStats(); 21 | System.out.println("server stats submitted"); 22 | } else if (i == 0) break; 23 | } 24 | scanner.close(); 25 | System.exit(0); 26 | } 27 | 28 | protected abstract void submitSystemFailure() throws Exception; 29 | 30 | protected abstract void submitServerStats() throws Exception; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/RedisDemo.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import redis.clients.jedis.Jedis; 5 | 6 | class RedisDemo extends Demo { 7 | 8 | private static Jedis client; 9 | private static ObjectMapper mapper; 10 | 11 | public static void main(String[] args) { 12 | try { 13 | client = new Jedis(Config.REDIS_HOST, Config.REDIS_PORT); 14 | mapper = new ObjectMapper(); 15 | 16 | RedisDemo demo = new RedisDemo(); 17 | demo.start(); 18 | } catch (Exception e) { 19 | e.printStackTrace(); 20 | } 21 | } 22 | 23 | @Override 24 | protected void submitSystemFailure() throws Exception { 25 | client.publish(Config.EVENT_SUBMIT_FAILURE, mapper.writeValueAsString(SystemFailure.generate())); 26 | } 27 | 28 | @Override 29 | protected void submitServerStats() throws Exception { 30 | client.publish(Config.EVENT_SUBMIT_STATS, mapper.writeValueAsString(ServerStats.generate())); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/ServerStats.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | class ServerStats { 8 | 9 | private Integer time; 10 | private String system; 11 | private Map> stats; 12 | 13 | public static class NodeStats { 14 | 15 | private Integer rpm; 16 | private Integer fpm; 17 | 18 | public NodeStats(Integer rpm, Integer fpm) { 19 | this.rpm = rpm; 20 | this.fpm = fpm; 21 | } 22 | 23 | public Integer getRpm() { 24 | return rpm; 25 | } 26 | 27 | public Integer getFpm() { 28 | return fpm; 29 | } 30 | 31 | } 32 | 33 | public Integer getTime() { 34 | return time; 35 | } 36 | 37 | public void setTime(Integer time) { 38 | this.time = time; 39 | } 40 | 41 | public String getSystem() { 42 | return system; 43 | } 44 | 45 | public void setSystem(String system) { 46 | this.system = system; 47 | } 48 | 49 | public Map> getStats() { 50 | return stats; 51 | } 52 | 53 | public void setStats(Map> stats) { 54 | this.stats = stats; 55 | } 56 | 57 | public static ServerStats generate() { 58 | ServerStats stats = new ServerStats(); 59 | stats.setTime((int) (System.currentTimeMillis() / 1000)); 60 | stats.setSystem("test"); 61 | Map> statsMap = new HashMap<>(); 62 | Map nodeStatsMap = new HashMap<>(); 63 | nodeStatsMap.put("10.10.1.1", new NodeStats(1000, 10)); 64 | nodeStatsMap.put("10.10.1.2", new NodeStats(2000, 20)); 65 | nodeStatsMap.put("10.10.1.3", new NodeStats(3000, 30)); 66 | statsMap.put("target_system", nodeStatsMap); 67 | stats.setStats(statsMap); 68 | return stats; 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/SocketIODemo.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.socket.client.IO; 5 | import io.socket.client.Socket; 6 | 7 | class SocketIODemo extends Demo { 8 | 9 | private static Socket socket; 10 | private static ObjectMapper mapper = new ObjectMapper(); 11 | 12 | public static void main(String[] args) { 13 | try { 14 | IO.Options options = new IO.Options(); 15 | options.path = Config.IO_PATH; 16 | socket = IO.socket(String.format("http://%s:%d", Config.HOST, Config.PORT), options); 17 | socket.connect(); 18 | 19 | SocketIODemo demo = new SocketIODemo(); 20 | demo.start(); 21 | } catch (Exception e) { 22 | e.printStackTrace(); 23 | } finally { 24 | if (socket != null && socket.connected()) socket.disconnect(); 25 | } 26 | } 27 | 28 | protected void submitSystemFailure() throws Exception { 29 | socket.emit(Config.EVENT_SUBMIT_FAILURE, mapper.writeValueAsString(SystemFailure.generate())); 30 | } 31 | 32 | protected void submitServerStats() throws Exception { 33 | socket.emit(Config.EVENT_SUBMIT_STATS, mapper.writeValueAsString(ServerStats.generate())); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /example/java/src/main/java/cn/imdada/overwatch/SystemFailure.java: -------------------------------------------------------------------------------- 1 | package cn.imdada.overwatch; 2 | 3 | class SystemFailure { 4 | 5 | private Integer time; 6 | private String system; 7 | private String host; 8 | private String url; 9 | private String status; 10 | 11 | public Integer getTime() { 12 | return time; 13 | } 14 | 15 | public void setTime(Integer time) { 16 | this.time = time; 17 | } 18 | 19 | public String getSystem() { 20 | return system; 21 | } 22 | 23 | public void setSystem(String system) { 24 | this.system = system; 25 | } 26 | 27 | public String getHost() { 28 | return host; 29 | } 30 | 31 | public void setHost(String host) { 32 | this.host = host; 33 | } 34 | 35 | public String getUrl() { 36 | return url; 37 | } 38 | 39 | public void setUrl(String url) { 40 | this.url = url; 41 | } 42 | 43 | public String getStatus() { 44 | return status; 45 | } 46 | 47 | public void setStatus(String status) { 48 | this.status = status; 49 | } 50 | 51 | public static SystemFailure generate() { 52 | SystemFailure failure = new SystemFailure(); 53 | failure.setSystem("test"); 54 | failure.setTime((int) (System.currentTimeMillis() / 1000)); 55 | failure.setHost("127.0.0.1"); 56 | failure.setUrl("/test"); 57 | failure.setStatus("ReadTimeout"); 58 | return failure; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /example/rest-demo.sh: -------------------------------------------------------------------------------- 1 | host=localhost:3000 2 | system=`uname` 3 | if [ $system = Darwin ] 4 | then 5 | sed -i "" -e "s/\"time\": .*,/\"time\": `date +%s`,/" system-stats.json 6 | else 7 | sed -i -e "s/\"time\": .*,/\"time\": `date +%s`,/" system-stats.json 8 | fi 9 | curl -XPOST "http://$host/stats/" -H "Content-Type: application/json" -d @system-stats.json 10 | -------------------------------------------------------------------------------- /example/system-stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1513047531, 3 | "stats": { 4 | "gateway-lb": { 5 | "service-A": { 6 | "10.10.1.1": { 7 | "fpm": 0, 8 | "rpm": 1000 9 | }, 10 | "10.10.1.2": { 11 | "fpm": 0, 12 | "rpm": 1000 13 | }, 14 | "10.10.1.3": { 15 | "fpm": 0, 16 | "rpm": 1000 17 | } 18 | }, 19 | "service-B": { 20 | "10.10.2.1": { 21 | "fpm": 0, 22 | "rpm": 2000 23 | }, 24 | "10.10.2.2": { 25 | "fpm": 0, 26 | "rpm": 2000 27 | }, 28 | "10.10.2.3": { 29 | "fpm": 0, 30 | "rpm": 2000 31 | } 32 | } 33 | }, 34 | "service-A": { 35 | "service-C": { 36 | "10.10.3.1": { 37 | "fpm": 20, 38 | "rpm": 1000 39 | }, 40 | "10.10.3.2": { 41 | "fpm": 25, 42 | "rpm": 1000 43 | } 44 | }, 45 | "service-E": { 46 | "10.10.5.1": { 47 | "fpm": 0, 48 | "rpm": 3000 49 | }, 50 | "10.10.5.2": { 51 | "fpm": 0, 52 | "rpm": 2000 53 | } 54 | } 55 | }, 56 | "service-B": { 57 | "service-C": { 58 | "10.10.3.1": { 59 | "fpm": 50, 60 | "rpm": 500 61 | }, 62 | "10.10.3.2": { 63 | "fpm": 45, 64 | "rpm": 450 65 | } 66 | }, 67 | "service-D": { 68 | "10.10.4.1": { 69 | "fpm": 0, 70 | "rpm": 1000 71 | } 72 | }, 73 | "service-E": { 74 | "10.10.5.1": { 75 | "fpm": 0, 76 | "rpm": 1000 77 | }, 78 | "10.10.5.2": { 79 | "fpm": 0, 80 | "rpm": 800 81 | } 82 | } 83 | }, 84 | "service-C": { 85 | "service-E": { 86 | "10.10.5.1": { 87 | "fpm": 400, 88 | "rpm": 2000 89 | }, 90 | "10.10.5.2": { 91 | "fpm": 300, 92 | "rpm": 1500 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -p "GFW? (Y/n)" yn 4 | case $yn in 5 | [Yy]* | "" ) NPM_ARGS="--registry=https://registry.npm.taobao.org";export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/dist;export SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass;; 6 | [Nn]* ) NPM_ARGS="";; 7 | esac 8 | 9 | install_node() { 10 | export NVM_DIR="$HOME/.nvm" 11 | if ! [ -s "$NVM_DIR/nvm.sh" ]; then 12 | echo "installing nvm... (https://github.com/creationix/nvm)" 13 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.4/install.sh | bash 14 | fi 15 | . "$NVM_DIR/nvm.sh" 16 | echo "installing node..." 17 | nvm install --lts 18 | } 19 | 20 | # check NodeJS 21 | if ! [ -x "$(command -v npm)" ]; then 22 | while true; do 23 | read -p "npm is not installed. install now? (Y/n)" yn 24 | case $yn in 25 | [Yy]* | "" ) install_node; break;; 26 | [Nn]* ) echo "npm is required, please make sure npm is available and try again."; exit 1;; 27 | esac 28 | done 29 | fi 30 | 31 | # npm install $NPM_ARGS -g typescript ts-node @angular/cli 32 | 33 | echo "installing server..." 34 | ( cd server && npm $NPM_ARGS install ) 35 | echo "done" 36 | 37 | echo "installing web..." 38 | ( cd web && npm $NPM_ARGS install ) 39 | echo "done" 40 | 41 | echo "all done" 42 | -------------------------------------------------------------------------------- /interface/system-failure.dto.ts: -------------------------------------------------------------------------------- 1 | export interface SystemFailureDto { 2 | id: number; 3 | time: number; 4 | system: string; 5 | host: string; 6 | url: string; 7 | status: string; 8 | } 9 | -------------------------------------------------------------------------------- /interface/system-info.dto.ts: -------------------------------------------------------------------------------- 1 | export interface SystemInfoDto { 2 | id: number; 3 | time: number; 4 | name: string; 5 | node: string; 6 | source: string; 7 | rpm: number; 8 | fpm: number; 9 | } 10 | 11 | // compact format to minimize data transfer costs 12 | export interface SystemInfoMapDto { 13 | [time: number]: [number, number]; 14 | } 15 | 16 | // compact format to minimize data transfer costs 17 | export interface SystemInfoDetailMapDto { 18 | [time: number]: Array<[string, number, number]>; 19 | } 20 | -------------------------------------------------------------------------------- /interface/system-stats.dto.ts: -------------------------------------------------------------------------------- 1 | export type SystemStatsNodeDto = [string, Array, Array]; 2 | export type SystemStatsLinkDto = [string, string, Array, Array]; 3 | 4 | export interface SystemStatsDto { 5 | time: number; 6 | nodes: Array; 7 | links: Array; 8 | } 9 | 10 | export interface SingleServerStatsInput { 11 | time: number; 12 | system: string; 13 | stats: RpcStatsInput; 14 | } 15 | 16 | export interface ServerStatsInput { 17 | [host: string]: { 18 | rpm: number; 19 | fpm: number; 20 | }; 21 | } 22 | 23 | export interface RpcStatsInput { 24 | [target: string]: ServerStatsInput; 25 | } 26 | 27 | export interface SystemStatsInput { 28 | [system: string]: RpcStatsInput; 29 | } 30 | -------------------------------------------------------------------------------- /interface/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /server/app/app.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as express from "express"; 3 | import { container } from "./inversify.config"; 4 | import { Server } from "./server"; 5 | import TYPES from "./common/types"; 6 | import { Log } from "./common/log"; 7 | import { Socket } from "./common/socket-util"; 8 | import { CommonHandlers } from "./common/handlers"; 9 | import { ResponseWriter } from "./common/response-writer"; 10 | import { SystemInfoController } from "./service/system-info/system-info.controller"; 11 | import { SystemStatsController } from "./service/system-stats/system-stats.controller"; 12 | import { SystemFailureController } from "./service/system-failure/system-failure.controller"; 13 | 14 | export class Application { 15 | 16 | private static readonly LOGGER = Log.getLogger("Application"); 17 | private _httpHandler: express.Application; 18 | 19 | get httpHandler(): (req: http.IncomingMessage, res: http.ServerResponse) => void { 20 | return this._httpHandler; 21 | } 22 | 23 | public constructor() { 24 | this._httpHandler = express(); 25 | } 26 | 27 | public start(): Promise { 28 | Application.LOGGER.info("loading app..."); 29 | return new Promise((resolve, reject) => { 30 | 31 | let commonHandlers: CommonHandlers = new CommonHandlers(); 32 | commonHandlers.handlers.forEach(handler => this._httpHandler.use(handler)); 33 | 34 | let systemInfoController: SystemInfoController = container.get(TYPES.SystemInfoController); 35 | this._httpHandler.use("/system", systemInfoController.getRouter()); 36 | 37 | let systemStatsController: SystemStatsController = container.get(TYPES.SystemStatsController); 38 | this._httpHandler.use("/stats", systemStatsController.getRouter()); 39 | 40 | let systemFailureController: SystemFailureController = container.get(TYPES.SystemFailureController); 41 | this._httpHandler.use("/failure", systemFailureController.getRouter()); 42 | 43 | this._httpHandler.use(commonHandlers.errorHandler); 44 | Application.LOGGER.info("app loaded"); 45 | return resolve(); 46 | }); 47 | } 48 | 49 | public initSocket(server: Server): void { 50 | let socket: Socket = container.get(TYPES.Socket); 51 | socket.initSocketServer(server.server); 52 | } 53 | 54 | public close(): Promise { 55 | Application.LOGGER.info("closing app..."); 56 | let socket: Socket = container.get(TYPES.Socket); 57 | return new Promise((resolve, reject) => { 58 | resolve(); 59 | }); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /server/app/common/base-dao.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | import * as Bluebird from "bluebird"; 3 | import { inject, injectable } from "inversify"; 4 | import TYPES from "./types"; 5 | import { Config } from "./config"; 6 | import { ModelDefinition } from "./model-definition"; 7 | 8 | class InitListener { 9 | resolve: Function; 10 | reject: Function; 11 | } 12 | 13 | @injectable() 14 | export abstract class BaseDao { 15 | 16 | protected static engine: Sequelize.Sequelize; 17 | protected model: Sequelize.Model; 18 | private initialized = -1; 19 | private initListeners: Array = new Array(); 20 | 21 | protected abstract getModelDefinition(): ModelDefinition; 22 | 23 | constructor() { } 24 | 25 | protected init(): Promise { 26 | if (BaseDao.engine === undefined) { 27 | const dbConfig = Config.get("db"); 28 | BaseDao.engine = new Sequelize(dbConfig.uri, dbConfig); 29 | } 30 | let def = this.getModelDefinition(); 31 | this.model = BaseDao.engine.define(def.modelName, def.columns, def.indexes); 32 | return new Promise((resolve, reject) => { 33 | if (this.initialized === 1) return resolve(); 34 | else { 35 | this.initListeners.push({ 36 | resolve: resolve, 37 | reject: reject 38 | }); 39 | if (this.initialized === 0) return; 40 | this.initialized = 0; 41 | this.model.sync({ force: false }) 42 | .then(() => { 43 | this.initialized = 1; 44 | this.initListeners.forEach((listener) => { 45 | listener.resolve(); 46 | }); 47 | }); 48 | } 49 | }); 50 | } 51 | 52 | protected toPromise(b: Bluebird): Promise> { 53 | return new Promise>((resolve, reject) => { 54 | b.then(resolve, reject); 55 | }); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /server/app/common/config.ts: -------------------------------------------------------------------------------- 1 | const activeConfig = process.env.config; 2 | let config: any; 3 | if (activeConfig === undefined) { 4 | config = require("../config.json"); 5 | console.log("using config "); 6 | } else { 7 | config = require(`../config.${ activeConfig }.json`); 8 | console.log(`using config <${ activeConfig }>`); 9 | } 10 | 11 | export class Config { 12 | 13 | public static get(key: string): any { 14 | return config[key]; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /server/app/common/event-source.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import TYPES from "./types"; 3 | import { Config } from "./config"; 4 | import { Log } from "./log"; 5 | import { Socket } from "./socket-util"; 6 | import { Redis } from "./redis-util"; 7 | 8 | @injectable() 9 | export class EventSource { 10 | 11 | private static readonly LOGGER = Log.getLogger("EventSource"); 12 | private impl: any; 13 | 14 | constructor( 15 | @inject(TYPES.Socket) socket: Socket, 16 | @inject(TYPES.Redis) redis: Redis 17 | ) { 18 | const messagingConfig = Config.get("messaging"); 19 | if (messagingConfig === "redis") { 20 | this.impl = redis; 21 | EventSource.LOGGER.info("using event source "); 22 | } else { 23 | this.impl = socket; 24 | EventSource.LOGGER.info("using event source "); 25 | } 26 | } 27 | 28 | public listen(event: string, handler: (message: T) => void): void { 29 | this.impl.listen(event, (message: string) => { 30 | let obj: T; 31 | try { 32 | obj = JSON.parse(message); 33 | } catch (e) { 34 | console.error(e); 35 | EventSource.LOGGER.error(`invalid message received for <${ event }>: ${ message }`); 36 | } 37 | if (obj !== undefined) handler(obj); 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /server/app/common/handlers.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as uuid from "uuid"; 3 | import * as cors from "cors"; 4 | import * as bodyParser from "body-parser"; 5 | import * as express from "express"; 6 | import { Config } from "./config"; 7 | import { Log } from "./log"; 8 | import { ResponseWriter } from "./response-writer"; 9 | 10 | export class CommonHandlers { 11 | 12 | private _handlers: Array<(req: express.Request, res: express.Response, next: express.NextFunction) => any>; 13 | private _errorHandler: (err: any, req: express.Request, res: express.Response, next: express.NextFunction) => any; 14 | 15 | get handlers() { 16 | return this._handlers; 17 | } 18 | 19 | get errorHandler() { 20 | return this._errorHandler; 21 | } 22 | 23 | public constructor() { 24 | 25 | let corsHandler = cors({ 26 | origin: Config.get("corsDomain"), 27 | credentials: true 28 | }); 29 | 30 | let bodyParserHandler = bodyParser.json(); 31 | 32 | let accessLogger = Log.getLogger("access"); 33 | let loggingHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => { 34 | let method: string = req.method; 35 | let url: string = req.originalUrl; 36 | let headers: string = JSON.stringify(req.headers); 37 | let body: string = JSON.stringify(req.body); 38 | req.id = uuid.v1(); 39 | res.reqId = req.id; 40 | accessLogger.info("<" + req.id + ">", method, url, headers, body); 41 | next(); 42 | }; 43 | 44 | this._handlers = [ corsHandler, bodyParserHandler, loggingHandler ]; 45 | this._errorHandler = (err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { 46 | console.error(err.stack); 47 | ResponseWriter.error(res, 500); 48 | }; 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /server/app/common/log.ts: -------------------------------------------------------------------------------- 1 | import { configure, getLogger, Logger } from "log4js"; 2 | import { Config } from "./config"; 3 | configure(Config.get("log")); 4 | 5 | export class Log { 6 | 7 | public static getLogger(name: string): Logger { 8 | return getLogger(name); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /server/app/common/model-definition.ts: -------------------------------------------------------------------------------- 1 | export interface ModelDefinition { 2 | modelName: string; 3 | columns: any; 4 | indexes: any; 5 | } 6 | -------------------------------------------------------------------------------- /server/app/common/redis-util.ts: -------------------------------------------------------------------------------- 1 | import * as redis from "redis"; 2 | import { inject, injectable } from "inversify"; 3 | import TYPES from "./types"; 4 | import { Config } from "./config"; 5 | 6 | @injectable() 7 | export class Redis { 8 | 9 | private client: redis.RedisClient; 10 | private handlers: Map void> = new Map void>(); 11 | private initialized = false; 12 | 13 | constructor() { } 14 | 15 | public init(): void { 16 | const redisConfig = Config.get("redis"); 17 | const host = redisConfig.host || "127.0.0.1"; 18 | const port = redisConfig.port || 6379; 19 | this.client = redis.createClient(port, host); 20 | this.client.on("message", (channel: string, message: string) => { 21 | if (!this.handlers.has(channel)) return; 22 | const handler = this.handlers.get(channel); 23 | handler(message); 24 | }); 25 | this.initialized = true; 26 | } 27 | 28 | public listen(event: string, handler: (message: string) => void): void { 29 | if (!this.initialized) this.init(); 30 | this.handlers.set(event, handler); 31 | this.client.subscribe(event); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /server/app/common/response-writer.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { Log } from "./log"; 3 | 4 | export class ResponseWriter { 5 | 6 | private static accessLogger = Log.getLogger("access"); 7 | 8 | public static send(res: express.Response, status: number, payload: string): void { 9 | this.accessLogger.info("<" + res.reqId + ">", status, payload); 10 | res.status(status).type("json").send(payload); 11 | } 12 | 13 | public static success(res: express.Response, data?: any, msg?: string): void { 14 | let payload: string = JSON.stringify({ 15 | status: 200, 16 | msg: msg || "success", 17 | data: data || null 18 | }); 19 | ResponseWriter.send(res, 200, payload); 20 | } 21 | 22 | public static error(res: express.Response, status: number, msg?: string): void { 23 | status = status || 500; 24 | let payload: string = JSON.stringify({ 25 | status: status, 26 | msg: msg || "error", 27 | data: null 28 | }); 29 | ResponseWriter.send(res, status, payload); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /server/app/common/socket-util.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as https from "https"; 3 | import * as socket from "socket.io"; 4 | import { inject, injectable } from "inversify"; 5 | import TYPES from "./types"; 6 | import { Config } from "./config"; 7 | import { Log } from "./log"; 8 | 9 | class Listeners { 10 | event: string; 11 | handler: (message: T) => void; 12 | } 13 | 14 | @injectable() 15 | export class Socket { 16 | 17 | public static readonly topics = { 18 | systemFailure: "data_failure", 19 | systemStats: "data_stats", 20 | }; 21 | 22 | private static readonly LOGGER = Log.getLogger("Socket"); 23 | private socketServer: SocketIO.Server; 24 | private initialized = false; 25 | private sockets: Set = new Set(); 26 | private listeners: Set> = new Set>(); 27 | 28 | constructor() { } 29 | 30 | public initSocketServer(server: http.Server | https.Server): void { 31 | Socket.LOGGER.info("creating socket server..."); 32 | this.socketServer = socket(server, Config.get("socket") || { }); 33 | this.socketServer.on("connect", (socket: SocketIO.Socket) => { 34 | let socketId = socket.id; 35 | Socket.LOGGER.info(`socket <${ socketId }> connected`); 36 | this.sockets.add(socket); 37 | socket.on("disconnect", () => { 38 | Socket.LOGGER.info(`socket <${ socketId }> disconnected`); 39 | this.sockets.delete(socket); 40 | }); 41 | socket.on("sub", (topic: string) => { 42 | socket.join(topic); 43 | Socket.LOGGER.info(`socket <${ socketId }> subscribed to <${ topic }>`); 44 | }); 45 | socket.on("unsub", (topic: string) => { 46 | socket.leave(topic); 47 | Socket.LOGGER.info(`socket <${ socketId }> unsubscribed from <${ topic }>`); 48 | }); 49 | this.listeners.forEach((listener: Listeners) => this.registerEventHandler(socket, listener.event, listener.handler)); 50 | this.initialized = true; 51 | }); 52 | Socket.LOGGER.info("socket server created"); 53 | } 54 | 55 | public broadcast(topic: string, message: any): void { 56 | try { 57 | this.socketServer.to(topic).emit(topic, message); 58 | Socket.LOGGER.info(`message sent to <${ topic }>`); 59 | } catch (ex) { 60 | Socket.LOGGER.error("failed to broadcast message", ex); 61 | } 62 | } 63 | 64 | public listen(event: string, handler: (message: string) => void): void { 65 | if (this.initialized) { 66 | this.sockets.forEach((socket: SocketIO.Socket) => { 67 | this.registerEventHandler(socket, event, handler); 68 | }); 69 | } else { 70 | this.listeners.add({ 71 | event: event, 72 | handler: handler 73 | }); 74 | } 75 | } 76 | 77 | private registerEventHandler(socket: SocketIO.Socket, event: string, handler: (message: string) => void): void { 78 | socket.on(event, handler); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /server/app/common/types.ts: -------------------------------------------------------------------------------- 1 | let TYPES = { 2 | Socket: Symbol("Socket"), 3 | Redis: Symbol("Redis"), 4 | EventSource: Symbol("EventSource"), 5 | 6 | SystemInfoController: Symbol("SystemInfoController"), 7 | SystemInfoService: Symbol("SystemInfoService"), 8 | SystemInfoDao: Symbol("SystemInfoDao"), 9 | 10 | SystemStatsController: Symbol("SystemStatsController"), 11 | SystemStatsService: Symbol("SystemStatsService"), 12 | SystemStatsRepository: Symbol("SystemStatsRepository"), 13 | 14 | SystemFailureController: Symbol("SystemFailureController"), 15 | SystemFailureService: Symbol("SystemFailureService"), 16 | SystemFailureDao: Symbol("SystemFailureDao") 17 | 18 | }; 19 | 20 | export default TYPES; 21 | -------------------------------------------------------------------------------- /server/app/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | // ** json should not contain comments ** 4 | // this file is merely a document 5 | 6 | // to use alternative configs, 7 | // when you have config.prod.json, 8 | // run: 9 | // config=prod npm start 10 | 11 | "https": { 12 | "enabled": false, 13 | "key": "", 14 | "cert": "" 15 | }, 16 | "port": 3000, 17 | 18 | "socket": { 19 | "path": "/socket" 20 | }, 21 | 22 | // domain name 23 | "corsDomain": "http://localhost:4200", 24 | 25 | // see README.md#submiting-statistics 26 | // options: [ socket | redis ] 27 | "messaging": "socket", 28 | 29 | // see http://docs.sequelizejs.com/manual/installation/getting-started.html#setting-up-a-connection 30 | "db": { 31 | "uri": "mysql://user:pass@127.0.0.1:3306/overwatch", 32 | "pool": { 33 | "max": 5, 34 | "min": 0, 35 | "idle": 10000 36 | }, 37 | // SQLite only 38 | "storage": "path/to/database.sqlite" 39 | }, 40 | 41 | // if "redis" is set for "messaging" 42 | // then you have to config this 43 | "redis": { 44 | "host": "127.0.0.1", 45 | "port": 6379 46 | }, 47 | 48 | // see https://github.com/nomiddlename/log4js-node 49 | "log": { 50 | "appenders": { 51 | "file": { 52 | "type": "dateFile", 53 | "filename": "/data/log/overwatch/overwatch.log", 54 | "pattern": "-yyyy-MM-dd", 55 | "alwaysIncludePattern": false 56 | } 57 | }, 58 | "categories": { "default": { "appenders": ["file"], "level": "info" } } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/app/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "https": { 3 | "enabled": false, 4 | "key": "", 5 | "cert": "" 6 | }, 7 | "port": 3000, 8 | "socket": { 9 | "path": "/socket" 10 | }, 11 | "corsDomain": "http://localhost:4200", 12 | "messaging": "socket", 13 | "db": { 14 | "uri": "mysql://root@127.0.0.1:3306/overwatch", 15 | "pool": { 16 | "max": 5, 17 | "min": 0, 18 | "idle": 10000 19 | } 20 | }, 21 | "redis": { 22 | "host": "127.0.0.1", 23 | "port": 6379 24 | }, 25 | "log": { 26 | "appenders": { "out": { "type": "stdout" } }, 27 | "categories": { "default": { "appenders": ["out"], "level": "debug" } } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/app/inversify.config.ts: -------------------------------------------------------------------------------- 1 | import { Container } from "inversify"; 2 | import TYPES from "./common/types"; 3 | 4 | import { Socket } from "./common/socket-util"; 5 | import { Redis } from "./common/redis-util"; 6 | import { EventSource } from "./common/event-source"; 7 | 8 | import { SystemInfoController, SystemInfoControllerImpl } from "./service/system-info/system-info.controller"; 9 | import { SystemInfoService, SystemInfoServiceImpl } from "./service/system-info/system-info.service"; 10 | import { SystemInfoDao, SystemInfoDaoImpl } from "./service/system-info/system-info.dao"; 11 | 12 | import { SystemStatsController, SystemStatsControllerImpl } from "./service/system-stats/system-stats.controller"; 13 | import { SystemStatsService, SystemStatsServiceImpl } from "./service/system-stats/system-stats.service"; 14 | import { SystemStatsRepository, SystemStatsRepositoryImpl } from "./service/system-stats/system-stats.repo"; 15 | 16 | import { SystemFailureController, SystemFailureControllerImpl } from "./service/system-failure/system-failure.controller"; 17 | import { SystemFailureService, SystemFailureServiceImpl } from "./service/system-failure/system-failure.service"; 18 | import { SystemFailureDao, SystemFailureDaoImpl } from "./service/system-failure/system-failure.dao"; 19 | 20 | let container = new Container(); 21 | 22 | container.bind(TYPES.Socket).to(Socket).inSingletonScope(); 23 | container.bind(TYPES.Redis).to(Redis).inSingletonScope(); 24 | container.bind(TYPES.EventSource).to(EventSource).inSingletonScope(); 25 | 26 | container.bind(TYPES.SystemInfoController).to(SystemInfoControllerImpl); 27 | container.bind(TYPES.SystemInfoService).to(SystemInfoServiceImpl); 28 | container.bind(TYPES.SystemInfoDao).to(SystemInfoDaoImpl).inSingletonScope(); 29 | 30 | container.bind(TYPES.SystemStatsController).to(SystemStatsControllerImpl); 31 | container.bind(TYPES.SystemStatsService).to(SystemStatsServiceImpl); 32 | container.bind(TYPES.SystemStatsRepository).to(SystemStatsRepositoryImpl).inSingletonScope(); 33 | 34 | container.bind(TYPES.SystemFailureController).to(SystemFailureControllerImpl); 35 | container.bind(TYPES.SystemFailureService).to(SystemFailureServiceImpl); 36 | container.bind(TYPES.SystemFailureDao).to(SystemFailureDaoImpl).inSingletonScope(); 37 | 38 | export { container }; 39 | -------------------------------------------------------------------------------- /server/app/main.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "reflect-metadata"; 3 | /// 4 | 5 | import { Application } from "./app"; 6 | import { Server } from "./server"; 7 | import { Socket } from "./common/socket-util"; 8 | import { Log } from "./common/log"; 9 | 10 | const LOGGER = Log.getLogger("main"); 11 | 12 | let app: Application = new Application(); 13 | let server: Server = new Server(app.httpHandler); 14 | 15 | let shuttingDown = false; 16 | process.on("SIGINT", () => { 17 | if (shuttingDown) { 18 | LOGGER.info("shutting down..."); 19 | return; 20 | } 21 | shuttingDown = true; 22 | app.close().then(() => { 23 | server.close(); 24 | LOGGER.info("shut down complete"); 25 | process.exit(); 26 | }); 27 | }); 28 | 29 | app.start() 30 | .then(() => { 31 | server.init(); 32 | app.initSocket(server); 33 | server.start(); 34 | }) 35 | .then(null, (err) => { 36 | LOGGER.error(err); 37 | }); 38 | -------------------------------------------------------------------------------- /server/app/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as https from "https"; 3 | import * as fs from "fs"; 4 | import * as express from "express"; 5 | import { Config } from "./common/config"; 6 | import { Log } from "./common/log"; 7 | 8 | export class Server { 9 | 10 | private static readonly LOGGER = Log.getLogger("Server"); 11 | private _server: http.Server | https.Server; 12 | 13 | get server(): http.Server | https.Server { 14 | return this._server; 15 | } 16 | 17 | constructor(private handler: (req: http.IncomingMessage, res: http.ServerResponse) => void) { } 18 | 19 | public init(): void { 20 | Server.LOGGER.info("starting server..."); 21 | let httpsEnabled = false; 22 | let httpsConfig: any = Config.get("https"); 23 | if (httpsConfig !== undefined && httpsConfig.enabled === true) httpsEnabled = true; 24 | if (httpsEnabled) { 25 | let options = { 26 | key: fs.readFileSync(httpsConfig.key).toString(), 27 | cert: fs.readFileSync(httpsConfig.cert).toString() 28 | }; 29 | this._server = https.createServer(options, this.handler); 30 | } else { 31 | this._server = http.createServer(this.handler); 32 | } 33 | this._server.on("listening", () => Server.LOGGER.info("server started")); 34 | } 35 | 36 | public start(): void { 37 | this._server.listen(Config.get("port")); 38 | } 39 | 40 | public close(): void { 41 | Server.LOGGER.info("closing server..."); 42 | this._server.close(); 43 | Server.LOGGER.info("server closed"); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /server/app/service/system-failure/system-failure.controller.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import * as express from "express"; 3 | import TYPES from "../../common/types"; 4 | import { Log } from "../../common/log"; 5 | import { Socket } from "../../common/socket-util"; 6 | import { EventSource } from "../../common/event-source"; 7 | 8 | import { SystemFailureService } from "./system-failure.service"; 9 | import { SystemFailureDto } from "./system-failure.dto"; 10 | import { ResponseWriter } from "../../common/response-writer"; 11 | 12 | interface SystemFailureController { 13 | getRouter(): express.Router; 14 | } 15 | 16 | @injectable() 17 | class SystemFailureControllerImpl implements SystemFailureController { 18 | 19 | private static readonly LOGGER = Log.getLogger("SystemFailureController"); 20 | 21 | private errorHandler = (res: express.Response, next: Function) => { 22 | return (err: Error) => { 23 | next(err); 24 | }; 25 | } 26 | 27 | constructor( 28 | @inject(TYPES.Socket) private socket: Socket, 29 | @inject(TYPES.EventSource) private eventSource: EventSource, 30 | @inject(TYPES.SystemFailureService) private systemFailureService: SystemFailureService 31 | ) { 32 | this.socket = socket; 33 | this.systemFailureService = systemFailureService; 34 | eventSource.listen("submit_failure", this._saveSystemFailure); 35 | } 36 | 37 | private _saveSystemFailure = (systemFailure: SystemFailureDto): void => { 38 | // TODO: batch save & rate limit 39 | systemFailure.id = undefined; 40 | this.systemFailureService.saveSystemFailure([ systemFailure ]); 41 | this.socket.broadcast(Socket.topics.systemFailure, systemFailure); 42 | } 43 | 44 | public getRouter(): express.Router { 45 | let router = express.Router(); 46 | router.get("", this.getLatestSystemFailure); 47 | router.get("/:system", this.getSystemFailure); 48 | router.post("/", this.saveSystemFailure); 49 | return router; 50 | } 51 | 52 | private saveSystemFailure = (req: express.Request, res: express.Response, next: Function) => { 53 | let systemFailure: SystemFailureDto = req.body; 54 | this._saveSystemFailure(systemFailure); 55 | ResponseWriter.success(res); 56 | } 57 | 58 | private getLatestSystemFailure = (req: express.Request, res: express.Response, next: Function) => { 59 | let count = parseInt(req.query.count) || undefined; 60 | let begin = req.query.begin || Math.round(new Date().getTime() / 1000) - 3 * 60 * 60; 61 | this.systemFailureService.getSystemFailure(undefined, begin, undefined, count) 62 | .then((systemFailures) => { 63 | ResponseWriter.success(res, systemFailures); 64 | }) 65 | .then(null, this.errorHandler(res, next)); 66 | } 67 | 68 | private getSystemFailure = (req: express.Request, res: express.Response, next: Function) => { 69 | let system = req.params.system; 70 | let count = parseInt(req.query.count) || 20; 71 | count = count < 100 ? count : 100; 72 | let begin = parseInt(req.query.begin) || undefined; 73 | let end = parseInt(req.query.end) || undefined; 74 | this.systemFailureService.getSystemFailure(system, begin, end, count) 75 | .then((systemFailures) => { 76 | ResponseWriter.success(res, systemFailures); 77 | }) 78 | .then(null, this.errorHandler(res, next)); 79 | } 80 | 81 | } 82 | 83 | export { SystemFailureController, SystemFailureControllerImpl }; 84 | -------------------------------------------------------------------------------- /server/app/service/system-failure/system-failure.dao.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { BaseDao } from "../../common/base-dao"; 4 | import { ModelDefinition } from "../../common/model-definition"; 5 | import { SystemFailureAttribute, SystemFailureInstance, SystemFailureDefinitions } from "./system-failure.model"; 6 | 7 | interface SystemFailureDao { 8 | getSystemFailure(system?: string, begin?: number, end?: number, limit?: number): Promise>; 9 | saveSystemFailure(systemFailure: Array): Promise>; 10 | } 11 | 12 | @injectable() 13 | class SystemFailureDaoImpl extends BaseDao implements SystemFailureDao { 14 | 15 | protected getModelDefinition(): ModelDefinition { 16 | return new SystemFailureDefinitions(); 17 | } 18 | 19 | public getSystemFailure(system?: string, begin?: number, end?: number, limit?: number): Promise> { 20 | limit = limit || 1000; 21 | if (limit < 0) limit = 1; 22 | let searchParam: any = { 23 | where: { }, 24 | limit: limit, 25 | order: [ [ "time", "DESC" ] ] 26 | }; 27 | if (system !== undefined) searchParam.where.system = system; 28 | if (begin !== undefined || end !== undefined) { 29 | searchParam.where.time = { }; 30 | if (begin !== undefined) searchParam.where.time.$gte = begin; 31 | if (end !== undefined) searchParam.where.time.$lte = end; 32 | } 33 | return this.init().then(() => { 34 | return this.toPromise(this.model.findAll(searchParam)); 35 | }); 36 | } 37 | 38 | public saveSystemFailure(systemFailure: Array): Promise> { 39 | return this.init().then(() => { 40 | return this.toPromise(this.model.bulkCreate(systemFailure)); 41 | }); 42 | } 43 | 44 | } 45 | 46 | export { SystemFailureDao, SystemFailureDaoImpl }; 47 | -------------------------------------------------------------------------------- /server/app/service/system-failure/system-failure.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../interface/system-failure.dto.ts -------------------------------------------------------------------------------- /server/app/service/system-failure/system-failure.model.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | import { ModelDefinition } from "../../common/model-definition"; 3 | 4 | export interface SystemFailureAttribute { 5 | id: number; 6 | time: number; 7 | system: string; 8 | host: string; 9 | url: string; 10 | status: string; 11 | } 12 | 13 | export interface SystemFailureInstance extends Sequelize.Instance, SystemFailureAttribute { 14 | } 15 | 16 | export interface SystemFailureModel extends Sequelize.Model { } 17 | 18 | export class SystemFailureDefinitions implements ModelDefinition { 19 | 20 | modelName = "SystemFailure"; 21 | 22 | columns = { 23 | id: { 24 | type: Sequelize.BIGINT, 25 | primaryKey: true, 26 | autoIncrement: true 27 | }, 28 | time: { type: Sequelize.BIGINT }, 29 | system: { type: Sequelize.STRING }, 30 | host: { type: Sequelize.STRING }, 31 | url: { type: Sequelize.STRING }, 32 | status: { type: Sequelize.STRING } 33 | }; 34 | 35 | indexes = { 36 | tableName: "overwatch_system_failure", 37 | indexes: [{ 38 | name: "idx_system_time", 39 | method: "BTREE", 40 | fields: [ "system", "time" ] 41 | }] 42 | }; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /server/app/service/system-failure/system-failure.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { SystemFailureInstance } from "./system-failure.model"; 4 | import { SystemFailureDto } from "./system-failure.dto"; 5 | import { SystemFailureDao } from "./system-failure.dao"; 6 | 7 | interface SystemFailureService { 8 | getSystemFailure(system?: string, begin?: number, end?: number, limit?: number): Promise>; 9 | saveSystemFailure(systemFailure: Array): Promise>; 10 | } 11 | 12 | @injectable() 13 | class SystemFailureServiceImpl implements SystemFailureService { 14 | 15 | constructor(@inject(TYPES.SystemFailureDao) private systemFailureDao: SystemFailureDao) { } 16 | 17 | private convertToDto(systemFailureInstance: SystemFailureInstance): SystemFailureDto { 18 | let systemFailureDto: SystemFailureDto = { 19 | id: systemFailureInstance.id, 20 | time: systemFailureInstance.time, 21 | system: systemFailureInstance.system, 22 | host: systemFailureInstance.host, 23 | url: systemFailureInstance.url, 24 | status: systemFailureInstance.status 25 | }; 26 | return systemFailureDto; 27 | } 28 | 29 | public getSystemFailure(system?: string, begin?: number, end?: number, limit?: number): Promise> { 30 | return this.systemFailureDao.getSystemFailure(system, begin, end, limit) 31 | .then((systemFailures: Array) => { 32 | return systemFailures.map(this.convertToDto); 33 | }); 34 | } 35 | 36 | public saveSystemFailure(systemFailures: Array): Promise> { 37 | return this.systemFailureDao.saveSystemFailure(systemFailures) 38 | .then((failures: Array) => { 39 | return failures.map(this.convertToDto); 40 | }); 41 | } 42 | 43 | } 44 | 45 | export { SystemFailureService, SystemFailureServiceImpl }; 46 | -------------------------------------------------------------------------------- /server/app/service/system-info/system-info.controller.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import * as express from "express"; 4 | import { SystemInfoService } from "./system-info.service"; 5 | import { SystemInfoMapDto, SystemInfoDetailMapDto } from "./system-info.dto"; 6 | import { ResponseWriter } from "../../common/response-writer"; 7 | 8 | interface SystemInfoController { 9 | getRouter(): express.Router; 10 | } 11 | 12 | @injectable() 13 | class SystemInfoControllerImpl implements SystemInfoController { 14 | 15 | private errorHandler = (res: express.Response, next: Function) => { 16 | return (err: Error) => { 17 | next(err); 18 | }; 19 | } 20 | 21 | constructor(@inject(TYPES.SystemInfoService) private systemInfoService: SystemInfoService) { } 22 | 23 | public getRouter(): express.Router { 24 | let router = express.Router(); 25 | router.get("/list", this.getSystemList); 26 | router.get("/:system", this.getSystemInfo); 27 | return router; 28 | } 29 | 30 | private getSystemList = (req: express.Request, res: express.Response, next: Function): void => { 31 | this.systemInfoService.getSystemList() 32 | .then((systemList: Array) => { 33 | ResponseWriter.success(res, systemList); 34 | }) 35 | .then(null, this.errorHandler(res, next)); 36 | } 37 | 38 | private getSystemInfo = (req: express.Request, res: express.Response, next: Function): void => { 39 | let system: string = req.params.system; 40 | let detail: boolean = req.query.detail === "true"; 41 | let begin: number = parseInt(req.query.begin); 42 | let end: number = parseInt(req.query.end); 43 | let count: number = parseInt(req.query.count); 44 | if (system === null) { 45 | ResponseWriter.success(res, [ ]); 46 | return; 47 | } 48 | if (detail) { 49 | this.systemInfoService.getSystemInfoDetail(system, begin, end, count) 50 | .then((systemInfoList: SystemInfoDetailMapDto) => { 51 | ResponseWriter.success(res, systemInfoList); 52 | }) 53 | .then(null, this.errorHandler(res, next)); 54 | } else { 55 | this.systemInfoService.getSystemInfo(system, begin, end, count) 56 | .then((systemInfoList: SystemInfoMapDto) => { 57 | ResponseWriter.success(res, systemInfoList); 58 | }) 59 | .then(null, this.errorHandler(res, next)); 60 | } 61 | } 62 | 63 | } 64 | 65 | export { SystemInfoController, SystemInfoControllerImpl }; 66 | -------------------------------------------------------------------------------- /server/app/service/system-info/system-info.dao.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { BaseDao } from "../../common/base-dao"; 4 | import { ModelDefinition } from "../../common/model-definition"; 5 | import { SystemInfoAttribute, SystemInfoInstance, SystemInfoDefinitions } from "./system-info.model"; 6 | 7 | interface SystemInfoDao { 8 | getSystemList(): Promise>; 9 | getLastestInfoTime(system: string, duration: number): Promise; 10 | getSystemInfo(system: string, begin?: number, end?: number, count?: number): Promise>; 11 | saveSystemInfo(systemInfo: Array): Promise>; 12 | getAllSystemInfo(begin: number): Promise>; 13 | } 14 | 15 | @injectable() 16 | class SystemInfoDaoImpl extends BaseDao implements SystemInfoDao { 17 | 18 | protected getModelDefinition(): ModelDefinition { 19 | return new SystemInfoDefinitions(); 20 | } 21 | 22 | public getLastestInfoTime(system: string, duration: number): Promise { 23 | return this.init().then(() => { 24 | return this.toPromise(this.model.findAll({ 25 | attributes: [ "time" ], 26 | where: { name: system }, 27 | group: [ "time" ], 28 | order: [ [ "time", "DESC" ] ], 29 | limit: duration 30 | })); 31 | }).then((result) => { 32 | const len = result.length; 33 | if (len === 0) return null; 34 | return result[len - 1].time; 35 | }); 36 | } 37 | 38 | public getSystemList(): Promise> { 39 | return this.init().then(() => { 40 | return this.toPromise(this.model.aggregate("name", "DISTINCT", { plain: false })); 41 | }).then((systemInfoList: any) => { 42 | return systemInfoList.map((r) => r.DISTINCT); 43 | }); 44 | } 45 | 46 | public getSystemInfo(system: string, begin?: number, end?: number, count?: number): Promise> { 47 | let searchParam: any = { 48 | where: { name: system }, 49 | order: [ [ "time", "DESC" ] ] 50 | }; 51 | if (count && count > 0) searchParam.limit = count; 52 | else if (count !== -1) searchParam.limit = 20; 53 | if (begin || end) { 54 | searchParam.where["time"] = {}; 55 | if (begin) searchParam.where.time["$gte"] = begin; 56 | if (end) searchParam.where.time["$lte"] = end; 57 | } 58 | return this.init().then(() => { 59 | return this.toPromise(this.model.findAll(searchParam)); 60 | }); 61 | } 62 | 63 | public saveSystemInfo(systemInfo: Array): Promise> { 64 | return this.init().then(() => { 65 | return this.toPromise(this.model.bulkCreate(systemInfo)); 66 | }); 67 | } 68 | 69 | public getAllSystemInfo(begin: number): Promise> { 70 | return this.init().then(() => { 71 | return this.toPromise(this.model.findAll({ 72 | where: { 73 | time: { 74 | $gte: begin 75 | } 76 | } 77 | })); 78 | }); 79 | } 80 | 81 | } 82 | 83 | export { SystemInfoDao, SystemInfoDaoImpl }; 84 | -------------------------------------------------------------------------------- /server/app/service/system-info/system-info.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../interface/system-info.dto.ts -------------------------------------------------------------------------------- /server/app/service/system-info/system-info.model.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | import { ModelDefinition } from "../../common/model-definition"; 3 | 4 | export interface SystemInfoAttribute { 5 | id?: number; 6 | time: number; 7 | name: string; 8 | node: string; 9 | source: string; 10 | rpm: number; 11 | fpm: number; 12 | } 13 | 14 | export interface SystemInfoInstance extends Sequelize.Instance, SystemInfoAttribute { 15 | } 16 | 17 | export interface SystemInfoModel extends Sequelize.Model { } 18 | 19 | export class SystemInfoDefinitions implements ModelDefinition { 20 | 21 | modelName = "SystemInfo"; 22 | 23 | columns = { 24 | id: { 25 | type: Sequelize.BIGINT, 26 | primaryKey: true, 27 | autoIncrement: true 28 | }, 29 | time: { type: Sequelize.BIGINT }, 30 | name: { type: Sequelize.STRING }, 31 | node: { type: Sequelize.STRING }, 32 | source: { type: Sequelize.STRING }, 33 | rpm: { type: Sequelize.INTEGER }, 34 | fpm: { type: Sequelize.INTEGER } 35 | }; 36 | 37 | indexes = { 38 | tableName: "overwatch_system_info", 39 | indexes: [{ 40 | name: "idx_system_time", 41 | method: "BTREE", 42 | fields: [ "name", "time" ] 43 | }, { 44 | name: "idx_source", 45 | method: "BTREE", 46 | fields: [ "source" ] 47 | }] 48 | }; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /server/app/service/system-info/system-info.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { SystemInfoInstance, SystemInfoAttribute } from "./system-info.model"; 4 | import { SystemInfoDto, SystemInfoMapDto, SystemInfoDetailMapDto } from "./system-info.dto"; 5 | import { SystemInfoDao } from "./system-info.dao"; 6 | 7 | interface SystemInfoService { 8 | getSystemList(): Promise>; 9 | getAllSystemInfo(begin: number): Promise>; 10 | getSystemInfo(system: string, begin: number, end: number, count: number): Promise; 11 | getSystemInfoDetail(system: string, begin: number, end: number, count: number): Promise; 12 | saveSystemInfo(systemInfos: Array): Promise; 13 | } 14 | 15 | @injectable() 16 | class SystemInfoServiceImpl implements SystemInfoService { 17 | 18 | constructor(@inject(TYPES.SystemInfoDao) private systemInfoDao: SystemInfoDao) { } 19 | 20 | public getSystemList(): Promise> { 21 | return this.systemInfoDao.getSystemList(); 22 | } 23 | 24 | public getAllSystemInfo(begin: number): Promise> { 25 | return this.systemInfoDao.getAllSystemInfo(begin) 26 | .then((systemInfos: Array) => { 27 | return systemInfos.map((systemInfo: SystemInfoInstance) => { 28 | return { 29 | id: systemInfo.id, 30 | time: systemInfo.time, 31 | name: systemInfo.name, 32 | node: systemInfo.node, 33 | source: systemInfo.source, 34 | rpm: systemInfo.rpm, 35 | fpm: systemInfo.fpm 36 | }; 37 | }); 38 | }); 39 | } 40 | 41 | public getSystemInfo(system: string, begin: number, end: number, count: number): Promise { 42 | return this.systemInfoDao.getSystemInfo(system, begin, end, count) 43 | .then((systemInfos: Array) => { 44 | let result: SystemInfoMapDto = {}; 45 | systemInfos.forEach((item: SystemInfoInstance) => { 46 | let time: number = item.time; 47 | if (result[time] === undefined) result[time] = [0, 0]; 48 | result[time][0] += item.rpm; 49 | result[time][1] += item.fpm; 50 | }); 51 | return result; 52 | }); 53 | } 54 | 55 | public getSystemInfoDetail(system: string, begin: number, end: number, count: number): Promise { 56 | return this.systemInfoDao.getSystemInfo(system, begin, end, count) 57 | .then((systemInfos: Array) => { 58 | let result: SystemInfoDetailMapDto = {}; 59 | systemInfos.forEach((item: SystemInfoInstance) => { 60 | let time: number = item.time; 61 | if (result[time] === undefined) result[time] = [ ]; 62 | result[time].push([item.node, item.rpm, item.fpm]); 63 | }); 64 | return result; 65 | }); 66 | } 67 | 68 | public saveSystemInfo(systemInfos: Array): Promise { 69 | return this.systemInfoDao.saveSystemInfo(systemInfos) 70 | .then((result: Array) => { 71 | return; 72 | }); 73 | } 74 | 75 | } 76 | 77 | export { SystemInfoService, SystemInfoServiceImpl }; 78 | -------------------------------------------------------------------------------- /server/app/service/system-stats/system-stats.controller.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import * as express from "express"; 4 | import { Log } from "../../common/log"; 5 | import { Socket } from "../../common/socket-util"; 6 | import { EventSource } from "../../common/event-source"; 7 | 8 | import { SystemStatsService } from "./system-stats.service"; 9 | import { SystemStatsDto, SystemStatsInput, SingleServerStatsInput } from "./system-stats.dto"; 10 | import { ResponseWriter } from "../../common/response-writer"; 11 | 12 | interface SystemStatsController { 13 | getRouter(): express.Router; 14 | } 15 | 16 | @injectable() 17 | class SystemStatsControllerImpl implements SystemStatsController { 18 | 19 | private static readonly LOGGER = Log.getLogger("SystemFailureController"); 20 | private serverStats: Array = new Array(); 21 | 22 | private errorHandler = (res: express.Response, next: Function) => { 23 | return (err: Error) => { 24 | next(err); 25 | }; 26 | } 27 | 28 | constructor( 29 | @inject(TYPES.Socket) private socket: Socket, 30 | @inject(TYPES.EventSource) private eventSource: EventSource, 31 | @inject(TYPES.SystemStatsService) private systemStatsService: SystemStatsService 32 | ) { 33 | eventSource.listen("submit_stats", this.saveServerStats); 34 | const now: Date = new Date(); 35 | const wait = (80 - now.getSeconds()) * 1000; 36 | setTimeout(this.startAggregation, wait); 37 | } 38 | 39 | private saveServerStats = (stats: SingleServerStatsInput): void => { 40 | this.serverStats.push(stats); 41 | } 42 | 43 | private startAggregation = () => { 44 | if (this.serverStats.length > 0) { 45 | const now: Date = new Date(); 46 | now.setSeconds(0); 47 | const begin = Math.floor((new Date(now.getTime() - 60000).getTime()) / 1000); 48 | const end = begin + 60; 49 | 50 | let minuteStats: Array = new Array(); 51 | this.serverStats = this.serverStats.filter((stats: SingleServerStatsInput) => { 52 | if (stats.time >= begin && stats.time < end) { 53 | minuteStats.push(stats); 54 | return false; 55 | } else { 56 | return true; 57 | } 58 | }); 59 | 60 | if (minuteStats.length > 0) { 61 | SystemStatsControllerImpl.LOGGER.info(`aggregating <${ minuteStats.length }> stats...`); 62 | 63 | let systemStats: SystemStatsInput = {}; 64 | minuteStats.forEach((stats: SingleServerStatsInput) => { 65 | const system: string = stats.system; 66 | if (systemStats[system] === undefined) systemStats[system] = {}; 67 | for (let target in stats.stats) { 68 | if (target === undefined) continue; 69 | if (systemStats[system][target] === undefined) systemStats[system][target] = {}; 70 | for (let host in stats.stats[target]) { 71 | if (host === undefined) continue; 72 | if (systemStats[system][target][host] === undefined) 73 | systemStats[system][target][host] = { 74 | rpm: 0, 75 | fpm: 0 76 | }; 77 | systemStats[system][target][host].rpm += stats.stats[target][host].rpm; 78 | systemStats[system][target][host].fpm += stats.stats[target][host].fpm; 79 | } 80 | } 81 | }); 82 | this.systemStatsService.saveSystemStats(begin, systemStats) 83 | .then(() => { 84 | return this.systemStatsService.getSystemStats(); 85 | }) 86 | .then((systemStatsDto: SystemStatsDto) => { 87 | this.socket.broadcast(Socket.topics.systemStats, systemStatsDto); 88 | }); 89 | } 90 | } 91 | setTimeout(this.startAggregation, 60000); 92 | } 93 | 94 | public getRouter(): express.Router { 95 | let router = express.Router(); 96 | router.get("", this.getSystemStats); 97 | router.post("/", this.saveSystemStats); 98 | return router; 99 | } 100 | 101 | private getSystemStats = (req: express.Request, res: express.Response, next: Function): void => { 102 | this.systemStatsService.getSystemStats() 103 | .then((systemStatsDto: SystemStatsDto) => { 104 | ResponseWriter.success(res, systemStatsDto); 105 | }) 106 | .then(null, this.errorHandler(res, next)); 107 | } 108 | 109 | private saveSystemStats = (req: express.Request, res: express.Response, next: Function): void => { 110 | let time: number = req.body.time; 111 | let stats: SystemStatsInput = req.body.stats; 112 | if (stats === undefined) { 113 | ResponseWriter.error(res, 400, "stats is required"); 114 | return; 115 | } 116 | this.systemStatsService.saveSystemStats(time, stats) 117 | .then(() => { 118 | ResponseWriter.success(res); 119 | }) 120 | .then(() => { 121 | return this.systemStatsService.getSystemStats(); 122 | }) 123 | .then((systemStatsDto: SystemStatsDto) => { 124 | this.socket.broadcast(Socket.topics.systemStats, systemStatsDto); 125 | }) 126 | .then(null, this.errorHandler(res, next)); 127 | } 128 | 129 | } 130 | 131 | export { SystemStatsController, SystemStatsControllerImpl }; 132 | -------------------------------------------------------------------------------- /server/app/service/system-stats/system-stats.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../interface/system-stats.dto.ts -------------------------------------------------------------------------------- /server/app/service/system-stats/system-stats.model.ts: -------------------------------------------------------------------------------- 1 | export interface SystemStatsNode { 2 | name: string; 3 | rpm: number; 4 | fpm: number; 5 | } 6 | 7 | export interface SystemStatsLink { 8 | source: string; 9 | target: string; 10 | rpm: number; 11 | fpm: number; 12 | } 13 | 14 | export interface SystemStats { 15 | time: number; 16 | nodes: Array; 17 | links: Array; 18 | } 19 | -------------------------------------------------------------------------------- /server/app/service/system-stats/system-stats.repo.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { Log } from "../../common/log"; 4 | import { SystemInfoService } from "../system-info/system-info.service"; 5 | import { SystemInfoDto } from "../system-info/system-info.dto"; 6 | import { SystemStatsNode, SystemStatsLink, SystemStats } from "./system-stats.model"; 7 | 8 | interface SystemStatsLookupMap { 9 | nodes: Map; 10 | links: Map; 11 | } 12 | 13 | interface PropertyLookupMap { 14 | [property: string]: Map; 15 | } 16 | 17 | interface InitListener { 18 | resolve: Function; 19 | reject: Function; 20 | } 21 | 22 | interface SystemStatsRepository { 23 | getSystemStats(begin: number, end: number): Promise>; 24 | getLatestStatsTime(): Promise; 25 | saveSystemStats(systemStats: SystemStats): Promise; 26 | } 27 | 28 | @injectable() 29 | class SystemStatsRepositoryImpl implements SystemStatsRepository { 30 | 31 | private static readonly LOGGER = Log.getLogger("SystemStatsDao"); 32 | private static readonly MAX_STATS: number = 15 * 60; // default 15 minutes max 33 | 34 | private statsTime: Array = new Array(); 35 | private systemStatsMap: Map = new Map(); 36 | private initialized: number = -1; 37 | private listeners: Array = new Array(); 38 | 39 | public constructor(@inject(TYPES.SystemInfoService) private systemInfoService: SystemInfoService) { } 40 | 41 | private getLinkKey(source: string, target: string): string { 42 | return `${ source }-${ target }`; 43 | } 44 | 45 | private calcStats(systemInfos: Array): void { 46 | let timeMap: Map = new Map(); 47 | systemInfos.forEach((systemInfo: SystemInfoDto) => { 48 | let time = systemInfo.time; 49 | if (!timeMap.has(time)) { 50 | timeMap.set(time, { 51 | nodes: new Map(), 52 | links: new Map() 53 | }); 54 | } 55 | let system = systemInfo.name; 56 | let source = systemInfo.source; 57 | 58 | let lookupMap = timeMap.get(time); 59 | if (!lookupMap.nodes.has(system)) { 60 | lookupMap.nodes.set(system, { 61 | name: system, 62 | rpm: 0, 63 | fpm: 0 64 | }); 65 | } 66 | if (!lookupMap.nodes.has(source)) { 67 | lookupMap.nodes.set(source, { 68 | name: source, 69 | rpm: 0, 70 | fpm: 0 71 | }); 72 | } 73 | lookupMap.nodes.get(system).rpm += systemInfo.rpm; 74 | lookupMap.nodes.get(system).fpm += systemInfo.fpm; 75 | 76 | let linkKey: string = this.getLinkKey(systemInfo.source, systemInfo.name); 77 | if (!lookupMap.links.has(linkKey)) { 78 | lookupMap.links.set(linkKey, { 79 | source: source, 80 | target: system, 81 | rpm: 0, 82 | fpm: 0 83 | }); 84 | } 85 | lookupMap.links.get(linkKey).rpm += systemInfo.rpm; 86 | lookupMap.links.get(linkKey).fpm += systemInfo.fpm; 87 | }); 88 | 89 | timeMap.forEach((lookupMap: SystemStatsLookupMap, time: number) => { 90 | let systemStats: SystemStats = { 91 | time: time, 92 | nodes: [ ], 93 | links: [ ] 94 | }; 95 | lookupMap.nodes.forEach((node: SystemStatsNode, key: string) => { 96 | systemStats.nodes.push(node); 97 | }); 98 | lookupMap.links.forEach((link: SystemStatsLink, key: string) => { 99 | systemStats.links.push(link); 100 | }); 101 | this.statsTime.push(time); 102 | this.systemStatsMap.set(time, systemStats); 103 | }); 104 | this.statsTime.sort().reverse(); 105 | } 106 | 107 | private init(): Promise { 108 | return new Promise((resolve, reject) => { 109 | if (this.initialized === 1) return resolve(); 110 | else { 111 | this.listeners.push({ 112 | resolve: resolve, 113 | reject: reject 114 | }); 115 | if (this.initialized === 0) return; 116 | this.initialized = 0; 117 | let now: number = Math.floor(new Date().getTime() / 1000); 118 | let begin: number = now - SystemStatsRepositoryImpl.MAX_STATS; 119 | this.systemInfoService.getAllSystemInfo(begin) 120 | .then((systemInfos: Array) => { 121 | SystemStatsRepositoryImpl.LOGGER.info("calculating system stats..."); 122 | this.calcStats(systemInfos); 123 | this.initialized = 1; 124 | this.listeners.forEach((listener) => { 125 | listener.resolve(); 126 | }); 127 | SystemStatsRepositoryImpl.LOGGER.info("done"); 128 | }, (err) => { 129 | this.initialized = -1; 130 | this.listeners.forEach((listener) => { 131 | listener.reject(err); 132 | }); 133 | }); 134 | } 135 | }); 136 | } 137 | 138 | public getSystemStats(begin: number, end: number): Promise> { 139 | return this.init().then(() => { 140 | let result = this.statsTime.filter((item) => { 141 | return item >= begin && item <= end; 142 | }).map((time) => { 143 | return this.systemStatsMap.get(time); 144 | }); 145 | return result; 146 | }); 147 | } 148 | 149 | public getLatestStatsTime(): Promise { 150 | return this.init().then(() => { 151 | if (this.statsTime.length === 0) return null; 152 | else return this.statsTime[0]; 153 | }); 154 | } 155 | 156 | public saveSystemStats(systemStats: SystemStats): Promise { 157 | return this.init().then(() => { 158 | let time = systemStats.time; 159 | if (this.statsTime.findIndex((value, index, obj) => value === time) >= 0) { 160 | let currentStats = this.systemStatsMap.get(time); 161 | let mergedStats = this.mergeSystemStats(time, currentStats, systemStats); 162 | this.systemStatsMap.set(time, mergedStats); 163 | } else { 164 | this.statsTime.unshift(time); 165 | this.statsTime.sort().reverse(); 166 | while (this.statsTime.length > SystemStatsRepositoryImpl.MAX_STATS) { 167 | let deletedTime = this.statsTime.pop(); 168 | this.systemStatsMap.delete(deletedTime); 169 | } 170 | this.systemStatsMap.set(time, systemStats); 171 | } 172 | }); 173 | } 174 | 175 | private mergeSystemStats(time: number, currentStats: SystemStats, newStats: SystemStats): SystemStats { 176 | 177 | let nodeMap: Map = new Map(); 178 | currentStats.nodes.forEach((node) => nodeMap.set(node.name, { 179 | name: node.name, 180 | rpm: node.rpm, 181 | fpm: node.fpm 182 | })); 183 | newStats.nodes.forEach((node) => { 184 | if (nodeMap.has(node.name)) { 185 | let nodeStats: SystemStatsNode = nodeMap.get(node.name); 186 | nodeStats.rpm += node.rpm; 187 | nodeStats.fpm += node.fpm; 188 | } else { 189 | nodeMap.set(node.name, { 190 | name: node.name, 191 | rpm: node.rpm, 192 | fpm: node.fpm 193 | }); 194 | } 195 | }); 196 | 197 | let linkMap: Map = new Map(); 198 | currentStats.links.forEach((link) => linkMap.set(this.getLinkKey(link.source, link.target), { 199 | source: link.source, 200 | target: link.target, 201 | rpm: link.rpm, 202 | fpm: link.fpm 203 | })); 204 | newStats.links.forEach((link) => { 205 | let key = this.getLinkKey(link.source, link.target); 206 | if (linkMap.has(key)) { 207 | let linkStats: SystemStatsLink = linkMap.get(key); 208 | linkStats.rpm += link.rpm; 209 | linkStats.fpm += link.fpm; 210 | } else { 211 | linkMap.set(key, { 212 | source: link.source, 213 | target: link.target, 214 | rpm: link.rpm, 215 | fpm: link.fpm 216 | }); 217 | } 218 | }); 219 | 220 | let result: SystemStats = { 221 | time: time, 222 | nodes: new Array(), 223 | links: new Array() 224 | }; 225 | 226 | nodeMap.forEach((stats, node) => result.nodes.push(stats)); 227 | linkMap.forEach((stats, link) => result.links.push(stats)); 228 | 229 | return result; 230 | } 231 | 232 | } 233 | 234 | export { SystemStatsRepository, SystemStatsRepositoryImpl }; 235 | -------------------------------------------------------------------------------- /server/app/service/system-stats/system-stats.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "inversify"; 2 | import TYPES from "../../common/types"; 3 | import { Log } from "../../common/log"; 4 | import { SystemInfoAttribute } from "../system-info/system-info.model"; 5 | import { SystemInfoService } from "../system-info/system-info.service"; 6 | import { SystemStats } from "./system-stats.model"; 7 | import { SystemStatsRepository } from "./system-stats.repo"; 8 | import { SystemStatsDto, SystemStatsInput, RpcStatsInput, ServerStatsInput, SingleServerStatsInput } from "./system-stats.dto"; 9 | 10 | interface PropertyLookupMap { 11 | [property: string]: Map; 12 | } 13 | 14 | interface SystemInfo { 15 | rpm: number; 16 | fpm: number; 17 | hosts: Map; 18 | } 19 | 20 | interface ServerInfo { 21 | rpm: number; 22 | fpm: number; 23 | } 24 | 25 | interface SystemStatsService { 26 | getSystemStats(): Promise; 27 | saveSystemStats(time: number, stats: SystemStatsInput): Promise; 28 | } 29 | 30 | @injectable() 31 | class SystemStatsServiceImpl implements SystemStatsService { 32 | 33 | private static readonly LOGGER = Log.getLogger("SystemStatsService"); 34 | private static readonly MAX_STATS: number = 20; 35 | 36 | public constructor( 37 | @inject(TYPES.SystemStatsRepository) private systemStatsRepository: SystemStatsRepository, 38 | @inject(TYPES.SystemInfoService) private systemInfoService: SystemInfoService 39 | ) { } 40 | 41 | private getLinkKey(source: string, target: string): string { 42 | return `${ Buffer.from(source).toString("base64") }-${ Buffer.from(target).toString("base64") }`; 43 | } 44 | 45 | private fromLinkKey(key: string): Array { 46 | const parts = key.split("-"); 47 | return [ Buffer.from(parts[0], "base64").toString("utf8"), Buffer.from(parts[1], "base64").toString("utf8") ]; 48 | } 49 | 50 | private calcDataSum(data: Map, begin: number, end: number): number { 51 | let sum = 0; 52 | for (let time = begin; time <= end; time ++) { 53 | if (data.has(time)) { 54 | sum += data.get(time); 55 | } 56 | } 57 | return sum; 58 | } 59 | 60 | public getSystemStats(): Promise { 61 | let holder: Map = new Map(); 62 | let result: SystemStatsDto = { 63 | time: 0, 64 | nodes: [ ], 65 | links: [ ] 66 | }; 67 | return this.systemStatsRepository.getLatestStatsTime() 68 | .then((time: number) => { 69 | if (time === null) return [ ]; 70 | holder.set("time", time); 71 | return this.systemStatsRepository.getSystemStats(time - 14 * 60, time); 72 | }) 73 | .then((stats: Array) => { 74 | if (stats.length < 1) return result; 75 | result.time = holder.get("time"); 76 | let nodeMap: Map = new Map(); 77 | let linkMap: Map = new Map(); 78 | stats.forEach((stat: SystemStats) => { 79 | let time = stat.time; 80 | for (let node of stat.nodes) { 81 | let key = node.name; 82 | if (!nodeMap.has(key)) { 83 | nodeMap.set(key, { 84 | "rpm": new Map(), 85 | "fpm": new Map() 86 | }); 87 | } 88 | nodeMap.get(key)["rpm"].set(time, node.rpm); 89 | nodeMap.get(key)["fpm"].set(time, node.fpm); 90 | } 91 | for (let link of stat.links) { 92 | let key = this.getLinkKey(link.source, link.target); 93 | if (!linkMap.has(key)) { 94 | linkMap.set(key, { 95 | "rpm": new Map(), 96 | "fpm": new Map() 97 | }); 98 | } 99 | linkMap.get(key)["rpm"].set(time, link.rpm); 100 | linkMap.get(key)["fpm"].set(time, link.fpm); 101 | } 102 | }); 103 | 104 | let calcNodeAvg = (node: string, property: string, minutes: number): number => { 105 | let end: number = holder.get("time"); 106 | let begin: number = end - minutes * 60; 107 | let data: Map = nodeMap.get(node)[property]; 108 | let sum = this.calcDataSum(data, begin, end); 109 | return parseFloat((sum / minutes).toFixed(3)); 110 | }; 111 | 112 | let calcLinkAvg = (link: string, property: string, minutes: number): number => { 113 | let end: number = holder.get("time"); 114 | let begin: number = end - minutes * 60; 115 | let data: Map = linkMap.get(link)[property]; 116 | let sum = this.calcDataSum(data, begin, end); 117 | return parseFloat((sum / minutes).toFixed(3)); 118 | }; 119 | 120 | for (let node of nodeMap.keys()) { 121 | let rpm: Array = [ 122 | calcNodeAvg(node, "rpm", 1), 123 | calcNodeAvg(node, "rpm", 5), 124 | calcNodeAvg(node, "rpm", 15) 125 | ]; 126 | let fpm: Array = [ 127 | calcNodeAvg(node, "fpm", 1), 128 | calcNodeAvg(node, "fpm", 5), 129 | calcNodeAvg(node, "fpm", 15) 130 | ]; 131 | result.nodes.push([ node, rpm, fpm ]); 132 | } 133 | 134 | for (let link of linkMap.keys()) { 135 | let rpm: Array = [ 136 | calcLinkAvg(link, "rpm", 60), 137 | calcLinkAvg(link, "rpm", 5 * 60), 138 | calcLinkAvg(link, "rpm", 15 * 60) 139 | ]; 140 | let fpm: Array = [ 141 | calcLinkAvg(link, "fpm", 60), 142 | calcLinkAvg(link, "fpm", 5 * 60), 143 | calcLinkAvg(link, "fpm", 15 * 60) 144 | ]; 145 | const parts = this.fromLinkKey(link); 146 | let source: string = parts[0]; 147 | let target: string = parts[1]; 148 | result.links.push([ source, target, rpm, fpm ]); 149 | } 150 | 151 | return result; 152 | }); 153 | } 154 | 155 | public saveSystemStats(time: number, stats: SystemStatsInput): Promise { 156 | let systemStats: SystemStats = { 157 | time: time, 158 | nodes: [ ], 159 | links: [ ] 160 | }; 161 | let systems: Map = new Map(); 162 | let systemInfos: Array = new Array(); 163 | for (let system in stats) { 164 | if (system === undefined) continue; 165 | if (!systems.has(system)) { 166 | systems.set(system, { 167 | rpm: 0, 168 | fpm: 0, 169 | hosts: new Map() 170 | }); 171 | } 172 | for (let target in stats[system]) { 173 | if (target === undefined) continue; 174 | if (!systems.has(target)) { 175 | systems.set(target, { 176 | rpm: 0, 177 | fpm: 0, 178 | hosts: new Map() 179 | }); 180 | } 181 | let targetSystem: SystemInfo = systems.get(target); 182 | 183 | let rpm = 0; 184 | let fpm = 0; 185 | for (let host in stats[system][target]) { 186 | if (host === undefined) continue; 187 | if (!targetSystem.hosts.has(host)) { 188 | targetSystem.hosts.set(host, { 189 | rpm: 0, 190 | fpm: 0 191 | }); 192 | } 193 | let info: any = stats[system][target][host]; 194 | let targetServer: ServerInfo = targetSystem.hosts.get(host); 195 | targetServer.rpm += info.rpm || 0; 196 | targetServer.fpm += info.fpm || 0; 197 | rpm += info.rpm || 0; 198 | fpm += info.fpm || 0; 199 | systemInfos.push({ 200 | time: time, 201 | name: target, 202 | node: host, 203 | source: system, 204 | rpm: info.rpm, 205 | fpm: info.fpm 206 | }); 207 | } 208 | 209 | targetSystem.rpm += rpm; 210 | targetSystem.fpm += fpm; 211 | systemStats.links.push({ 212 | source: system, 213 | target: target, 214 | fpm: fpm, 215 | rpm: rpm 216 | }); 217 | } 218 | } 219 | 220 | systems.forEach((info, system) => { 221 | let fpm = info.fpm; 222 | let rpm = info.rpm; 223 | systemStats.nodes.push({ 224 | name: system, 225 | fpm: fpm, 226 | rpm: rpm 227 | }); 228 | }); 229 | 230 | return this.systemStatsRepository.saveSystemStats(systemStats) 231 | .then(() => { 232 | return this.systemInfoService.saveSystemInfo(systemInfos); 233 | }); 234 | 235 | } 236 | 237 | } 238 | 239 | export { SystemStats, SystemStatsService, SystemStatsServiceImpl }; 240 | -------------------------------------------------------------------------------- /server/app/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare namespace Express { 7 | 8 | export interface Request { 9 | id: string; 10 | } 11 | 12 | export interface Response { 13 | reqId: string; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overwatch-server", 3 | "version": "0.0.1", 4 | "description": "overwatch server", 5 | "main": "server.js", 6 | "author": "zstringx", 7 | "license": "BSD-3-Clause", 8 | "scripts": { 9 | "start": "./node_modules/.bin/ts-node app/main.ts" 10 | }, 11 | "dependencies": { 12 | "@types/body-parser": "^1.16.5", 13 | "@types/cors": "^2.8.1", 14 | "@types/express": "^4.0.37", 15 | "@types/node": "^6.0.88", 16 | "@types/redis": "^2.6.0", 17 | "@types/sequelize": "^4.0.73", 18 | "@types/socket.io": "^1.4.30", 19 | "@types/uuid": "^3.4.2", 20 | "body-parser": "^1.18.0", 21 | "cors": "^2.8.4", 22 | "express": "^4.15.4", 23 | "express-validator": "^4.1.1", 24 | "inversify": "^4.3.0", 25 | "lodash": "^4.17.4", 26 | "log4js": "^2.3.3", 27 | "mysql2": "^1.4.2", 28 | "redis": "^2.8.0", 29 | "reflect-metadata": "^0.1.10", 30 | "sequelize": "^4.8.2", 31 | "socket.io": "^2.0.3", 32 | "ts-node": "~3.2.0", 33 | "typescript": "~2.3.3", 34 | "uuid": "^3.1.0", 35 | "yargs": "^8.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "types": ["reflect-metadata", "node"], 6 | "lib": ["es6"], 7 | "noImplicitAny": false, 8 | "sourceMap": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "../web/node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": false, 14 | "eofline": true, 15 | "forin": true, 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | { 31 | "order": [ 32 | "static-field", 33 | "instance-field", 34 | "static-method", 35 | "instance-method" 36 | ] 37 | } 38 | ], 39 | "no-arg": true, 40 | "no-bitwise": true, 41 | "no-construct": true, 42 | "no-debugger": true, 43 | "no-duplicate-super": true, 44 | "no-empty": false, 45 | "no-empty-interface": true, 46 | "no-eval": true, 47 | "no-inferrable-types": [ 48 | true, 49 | "ignore-params" 50 | ], 51 | "no-misused-new": true, 52 | "no-non-null-assertion": true, 53 | "no-shadowed-variable": true, 54 | "no-string-literal": false, 55 | "no-string-throw": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unnecessary-initializer": true, 59 | "no-unused-expression": true, 60 | "no-use-before-declare": true, 61 | "no-var-keyword": true, 62 | "object-literal-sort-keys": false, 63 | "one-line": [ 64 | true, 65 | "check-open-brace", 66 | "check-catch", 67 | "check-else", 68 | "check-whitespace" 69 | ], 70 | "prefer-const": false, 71 | "quotemark": [ 72 | true, 73 | "double" 74 | ], 75 | "semicolon": [ 76 | true, 77 | "always" 78 | ], 79 | "triple-equals": [ 80 | true, 81 | "allow-null-check" 82 | ], 83 | "typedef-whitespace": [ 84 | true, 85 | { 86 | "call-signature": "nospace", 87 | "index-signature": "nospace", 88 | "parameter": "nospace", 89 | "property-declaration": "nospace", 90 | "variable-declaration": "nospace" 91 | } 92 | ], 93 | "typeof-compare": true, 94 | "unified-signatures": true, 95 | "variable-name": false, 96 | "whitespace": [ 97 | true, 98 | "check-branch", 99 | "check-decl", 100 | "check-operator", 101 | "check-separator", 102 | "check-type" 103 | ], 104 | "use-input-property-decorator": true, 105 | "use-output-property-decorator": true, 106 | "use-host-property-decorator": true, 107 | "no-input-rename": true, 108 | "no-output-rename": true, 109 | "use-life-cycle-interface": true, 110 | "use-pipe-transform-interface": true, 111 | "component-class-suffix": true, 112 | "directive-class-suffix": true, 113 | "no-access-missing-member": true, 114 | "templates-use-public": true, 115 | "invoke-injectable": true 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /web/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "overwatch" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "ow", 21 | "styles": [ 22 | "styles.scss" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json", 40 | "exclude": "**/node_modules/**" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json", 44 | "exclude": "**/node_modules/**" 45 | }, 46 | { 47 | "project": "e2e/tsconfig.e2e.json", 48 | "exclude": "**/node_modules/**" 49 | } 50 | ], 51 | "test": { 52 | "karma": { 53 | "config": "./karma.conf.js" 54 | } 55 | }, 56 | "defaults": { 57 | "styleExt": "scss", 58 | "component": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | indent_size = 4 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | yarn-error.log 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Overwatch 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.3.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 29 | -------------------------------------------------------------------------------- /web/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('overwatch App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to ow!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /web/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('ow-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overwatch", 3 | "version": "0.0.1", 4 | "license": "BSD-3-Clause", 5 | "scripts": { 6 | "ng": "./node_modules/.bin/ng", 7 | "start": "./node_modules/.bin/ng serve", 8 | "build": "./node_modules/.bin/ng build -e prod", 9 | "test": "./node_modules/.bin/ng test", 10 | "lint": "./node_modules/.bin/ng lint", 11 | "e2e": "./node_modules/.bin/ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^4.3.6", 16 | "@angular/cdk": "^2.0.0-beta.12", 17 | "@angular/common": "^4.3.6", 18 | "@angular/compiler": "^4.3.6", 19 | "@angular/core": "^4.3.6", 20 | "@angular/forms": "^4.3.6", 21 | "@angular/http": "^4.3.6", 22 | "@angular/material": "^2.0.0-beta.12", 23 | "@angular/platform-browser": "^4.3.6", 24 | "@angular/platform-browser-dynamic": "^4.3.6", 25 | "@angular/router": "^4.3.6", 26 | "core-js": "^2.4.1", 27 | "d3": "^4.10.0", 28 | "font-awesome": "^4.7.0", 29 | "moment": "^2.18.1", 30 | "rxjs": "^5.4.2", 31 | "socket.io-client": "^2.0.3", 32 | "web-animations-js": "^2.3.1", 33 | "zone.js": "^0.8.14" 34 | }, 35 | "devDependencies": { 36 | "@angular/cli": "1.3.2", 37 | "@angular/compiler-cli": "^4.3.6", 38 | "@angular/language-service": "^4.3.6", 39 | "@types/jasmine": "~2.5.53", 40 | "@types/jasminewd2": "~2.0.2", 41 | "@types/node": "~6.0.60", 42 | "@types/d3": "^4.10.0", 43 | "@types/socket.io-client": "^1.4.30", 44 | "codelyzer": "~3.1.1", 45 | "jasmine-core": "~2.6.2", 46 | "jasmine-spec-reporter": "~4.1.0", 47 | "karma": "~1.7.0", 48 | "karma-chrome-launcher": "~2.1.1", 49 | "karma-cli": "~1.0.1", 50 | "karma-coverage-istanbul-reporter": "^1.2.1", 51 | "karma-jasmine": "~1.1.0", 52 | "karma-jasmine-html-reporter": "^0.2.2", 53 | "protractor": "~5.1.2", 54 | "ts-node": "~3.2.0", 55 | "tslint": "~5.3.2", 56 | "typescript": "~2.3.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /web/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "ow-root", 5 | templateUrl: "app.template.html", 6 | styleUrls: [ "app.style.scss" ] 7 | }) 8 | export class AppComponent { } 9 | -------------------------------------------------------------------------------- /web/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { BrowserModule } from "@angular/platform-browser"; 3 | import { APP_BASE_HREF } from "@angular/common"; 4 | 5 | import { routing } from "./app.routing"; 6 | import { OverwatchCommonModule } from "./common/common.module"; 7 | import { ToolbarModule } from "./toolbar/toolbar.module"; 8 | import { HistoryModule } from "./history/history.module"; 9 | import { DashboardModule } from "./dashboard/dashboard.module"; 10 | import { LayoutModule } from "./layout/layout.module"; 11 | 12 | import { AppComponent } from "./app.component"; 13 | @NgModule({ 14 | imports: [ 15 | BrowserModule, 16 | ToolbarModule, 17 | HistoryModule, 18 | DashboardModule, 19 | LayoutModule, 20 | OverwatchCommonModule, 21 | routing 22 | ], 23 | declarations: [ AppComponent ], 24 | providers: [ { provide: APP_BASE_HREF, useValue: "/" } ], 25 | bootstrap: [ AppComponent ] 26 | }) 27 | export class AppModule { } 28 | -------------------------------------------------------------------------------- /web/src/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { Dashboard } from "./dashboard/dashboard.component"; 5 | import { History } from "./history/history.component"; 6 | import { Layout } from "./layout/layout.component"; 7 | 8 | const appRoutes: Routes = [{ 9 | path: "", 10 | redirectTo: "/dashboard", 11 | pathMatch: "full" 12 | }, { 13 | path: "dashboard", 14 | component: Dashboard 15 | }, { 16 | path: "history", 17 | component: History 18 | }, { 19 | path: "layout", 20 | component: Layout 21 | }, { 22 | path: "**", 23 | redirectTo: "dashboard" 24 | }]; 25 | 26 | export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); 27 | -------------------------------------------------------------------------------- /web/src/app/app.style.scss: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | position: fixed; 3 | height: 3em; 4 | width: 100%; 5 | z-index: 100; 6 | } 7 | 8 | .content { 9 | 10 | flex: auto; 11 | display: flex; 12 | flex-direction: column; 13 | 14 | router-outlet { 15 | display: none; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/app.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /web/src/app/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { HttpModule } from "@angular/http"; 3 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 4 | 5 | import { JoinPipe } from "./join.pipe"; 6 | import { TimestampPipe } from "./timestamp.pipe"; 7 | import { Connector } from "./connector.service"; 8 | import { SocketService } from "./socket.service"; 9 | 10 | @NgModule({ 11 | imports: [ 12 | HttpModule, BrowserAnimationsModule 13 | ], 14 | declarations: [ JoinPipe, TimestampPipe ], 15 | exports: [ JoinPipe, TimestampPipe ], 16 | providers: [ Connector, SocketService ] 17 | }) 18 | export class OverwatchCommonModule { } 19 | -------------------------------------------------------------------------------- /web/src/app/common/connector.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { RequestOptions, Request, RequestMethod, Headers, Http, URLSearchParams, ResponseContentType } from "@angular/http"; 3 | import "rxjs/add/operator/toPromise"; 4 | 5 | import { environment } from "../../environments/environment"; 6 | import { Response } from "./response"; 7 | 8 | @Injectable() 9 | export class Connector { 10 | 11 | private jsonHeader = new Headers({"Content-Type": "application/json"}); 12 | private baseUrl: string = environment.apiUrl; 13 | 14 | constructor(private http: Http) {} 15 | 16 | private errorHandler(error: any): Promise { 17 | console.error("An error occurred", error); 18 | return Promise.reject(error.message || error); 19 | } 20 | 21 | private responseHandler(response: Response): ResponseDataType { 22 | if (response.status === 200) { 23 | return response.data; 24 | } else { 25 | // TODO exception 26 | } 27 | } 28 | 29 | get(url: string, params?: any): Promise { 30 | let urlParams: URLSearchParams; 31 | if (params) { 32 | urlParams = new URLSearchParams(); 33 | for (let key in params) { 34 | if (key === undefined) continue; 35 | urlParams.set(key, params[key]); 36 | } 37 | } 38 | let opt = new RequestOptions({ 39 | method: RequestMethod.Get, 40 | url: this.baseUrl + url, 41 | search: urlParams, 42 | responseType: ResponseContentType.Json 43 | }); 44 | return this.http.request(new Request(opt)) 45 | .toPromise() 46 | .then(response => this.responseHandler(response.json() as Response)) 47 | .catch(this.errorHandler); 48 | } 49 | 50 | post(url: string, params?: any): Promise { 51 | let opt = new RequestOptions({ 52 | method: RequestMethod.Post, 53 | url: this.baseUrl + url, 54 | headers: this.jsonHeader, 55 | body: JSON.stringify(params), 56 | responseType: ResponseContentType.Json 57 | }); 58 | return this.http.request(new Request(opt)) 59 | .toPromise() 60 | .then(response => this.responseHandler(response.json() as Response)) 61 | .catch(this.errorHandler); 62 | } 63 | 64 | put(url: string, params?: any): Promise { 65 | let opt = new RequestOptions({ 66 | method: RequestMethod.Put, 67 | url: this.baseUrl + url, 68 | headers: this.jsonHeader, 69 | body: JSON.stringify(params), 70 | responseType: ResponseContentType.Json 71 | }); 72 | return this.http.request(new Request(opt)) 73 | .toPromise() 74 | .then(response => this.responseHandler(response.json() as Response)) 75 | .catch(this.errorHandler); 76 | } 77 | 78 | delete(url: string, params?: any): Promise { 79 | let urlParams: URLSearchParams; 80 | if (params) { 81 | urlParams = new URLSearchParams(); 82 | for (let key in params) { 83 | if (key === undefined) continue; 84 | urlParams.set(key, params[key]); 85 | } 86 | } 87 | let opt = new RequestOptions({ 88 | method: RequestMethod.Delete, 89 | url: this.baseUrl + url, 90 | search: urlParams, 91 | responseType: ResponseContentType.Json 92 | }); 93 | return this.http.request(new Request(opt)) 94 | .toPromise() 95 | .then(response => this.responseHandler(response.json() as Response)) 96 | .catch(this.errorHandler); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /web/src/app/common/join.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({name: "join"}) 4 | export class JoinPipe implements PipeTransform { 5 | transform(value: any[], delimiter: string): string { 6 | let result = ""; 7 | let idx = 0; 8 | for (const item of value) { 9 | result += item; 10 | if (idx < value.length - 1) result += delimiter; 11 | idx++; 12 | } 13 | return result; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/src/app/common/response.ts: -------------------------------------------------------------------------------- 1 | export class Response { 2 | status: number; 3 | msg: string; 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /web/src/app/common/socket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import * as scoket from "socket.io-client"; 3 | 4 | import { environment } from "../../environments/environment"; 5 | 6 | @Injectable() 7 | export class SocketService { 8 | 9 | private url: string = environment.apiUrl; 10 | private socket: SocketIOClient.Socket; 11 | 12 | constructor() { 13 | this.init(); 14 | } 15 | 16 | init(): void { 17 | let socketUrl: URL = new URL(this.url); 18 | let socketPath = "socket"; 19 | let path: string; 20 | if (socketUrl.pathname.endsWith("/")) 21 | path = `${ socketUrl.pathname }${ socketPath }`; 22 | else 23 | path = `${ socketUrl.pathname }/${ socketPath }`; 24 | this.socket = scoket.connect(socketUrl.origin, { path: path }); 25 | this.socket.on("connect", () => console.log("socket connected")); 26 | this.socket.on("disconnect", () => console.log("socket disconnected")); 27 | this.socket.on("error", (error: string) => console.error(`scoket error: "${ error }"`)); 28 | } 29 | 30 | getSystemFailureTopic(): string { 31 | return "data_failure"; 32 | } 33 | 34 | getSystemStatsTopic(): string { 35 | return "data_stats"; 36 | } 37 | 38 | subscribe(topic: string, handler): void { 39 | if (topic === null || topic === undefined) return; 40 | this.socket.emit("sub", topic); 41 | this.socket.on(topic, handler); 42 | console.log(`subscribed to ${ topic }`); 43 | } 44 | 45 | unsubscribe(topic: string): void { 46 | if (topic === null || topic === undefined) return; 47 | this.socket.emit("unsub", topic); 48 | console.log(`unsubscribed from ${ topic }`); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /web/src/app/common/system-failure/system-failure.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../../interface/system-failure.dto.ts -------------------------------------------------------------------------------- /web/src/app/common/system-failure/system-failure.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { Connector } from "../connector.service"; 4 | import { SystemFailureDto } from "./system-failure.dto"; 5 | 6 | export interface SystemFailureOptions { 7 | system?: string; 8 | count?: number; 9 | begin?: number; 10 | end?: number; 11 | } 12 | 13 | @Injectable() 14 | export class SystemFailureService { 15 | 16 | constructor(private connector: Connector) { } 17 | 18 | getSystemFailure(opt: SystemFailureOptions): Promise> { 19 | let url = "/failure"; 20 | if (opt.system) url = `${ url }/${ opt.system }`; 21 | opt.system = undefined; 22 | return this.connector.get>(url, opt); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /web/src/app/common/system-info/system-info.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../../interface/system-info.dto.ts -------------------------------------------------------------------------------- /web/src/app/common/system-info/system-info.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { Connector } from "../connector.service"; 4 | import { SystemInfoMapDto, SystemInfoDetailMapDto } from "./system-info.dto"; 5 | import { SystemInfoVo, SystemInfoDetailVo } from "./system-info.vo"; 6 | 7 | @Injectable() 8 | export class SystemInfoService { 9 | 10 | constructor(private connector: Connector) { } 11 | 12 | getSystemList(): Promise> { 13 | return this.connector.get>(`/system/list`); 14 | } 15 | 16 | getSystemInfoDetail(system: string, begin: number, end: number): Promise> { 17 | if (begin >= end) throw new Error("begin >= end"); 18 | if (end - begin > 2 * 60 * 60) throw new Error("too large"); 19 | return this.connector.get(`/system/${ system }`, { begin: begin, end: end, detail: true, count: -1 }) 20 | .then((data: any) => { 21 | return SystemInfoDetailVo.parse(data); 22 | }); 23 | } 24 | 25 | getSystemInfo(system: string, begin: number, end: number): Promise> { 26 | return this.connector.get(`/system/${ system }`, { begin: begin, end: end, count: -1 }) 27 | .then((data: SystemInfoMapDto) => SystemInfoVo.parse(data)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /web/src/app/common/system-info/system-info.vo.ts: -------------------------------------------------------------------------------- 1 | import { SystemInfoMapDto, SystemInfoDetailMapDto } from "./system-info.dto"; 2 | 3 | export interface SystemNodeInfo { 4 | name: string; 5 | rpm: number; 6 | fpm: number; 7 | } 8 | 9 | export class SystemInfoDetailVo { 10 | 11 | time: number; 12 | nodes: Array; 13 | 14 | public static parse(data: SystemInfoDetailMapDto): Array { 15 | let result: Array = [ ]; 16 | for (let time in data) { 17 | if (time === undefined) continue; 18 | let info: SystemInfoDetailVo = { 19 | time: parseInt(time, 10), 20 | nodes: [ ] 21 | }; 22 | for (let host in data[time]) { 23 | if (host === undefined) continue; 24 | info.nodes.push({ 25 | name: data[time][host][0], 26 | rpm: data[time][host][1], 27 | fpm: data[time][host][2] 28 | }); 29 | } 30 | result.push(info); 31 | } 32 | return result; 33 | } 34 | 35 | } 36 | 37 | export class SystemInfoVo { 38 | 39 | time: number; 40 | rpm: number; 41 | fpm: number; 42 | 43 | public static parse(data: SystemInfoMapDto): Array { 44 | let result: Array = []; 45 | for (let time in data) { 46 | if (time === undefined) continue; 47 | let vo: SystemInfoVo = new SystemInfoVo(); 48 | vo.time = parseInt(time, 10); 49 | vo.rpm = data[time][0]; 50 | vo.fpm = data[time][1]; 51 | result.push(vo); 52 | } 53 | return result; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /web/src/app/common/system-stats/system-stats.dto.ts: -------------------------------------------------------------------------------- 1 | ../../../../../interface/system-stats.dto.ts -------------------------------------------------------------------------------- /web/src/app/common/system-stats/system-stats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { Connector } from "../connector.service"; 4 | import { SystemStats } from "./system-stats.vo"; 5 | import { SystemStatsDto } from "./system-stats.dto"; 6 | 7 | @Injectable() 8 | export class SystemStatsService { 9 | 10 | constructor(private connector: Connector) { } 11 | 12 | getSystemStats(): Promise { 13 | return this.connector.get("/stats") 14 | .then((data: SystemStatsDto) => new SystemStats(data)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /web/src/app/common/system-stats/system-stats.vo.ts: -------------------------------------------------------------------------------- 1 | import { SystemStatsDto, SystemStatsNodeDto, SystemStatsLinkDto } from "./system-stats.dto"; 2 | 3 | export class NodeInfo { 4 | 5 | name: string; 6 | rpm: Array; 7 | fpm: Array; 8 | 9 | constructor(data: SystemStatsNodeDto) { 10 | this.name = data[0]; 11 | this.rpm = data[1]; 12 | this.fpm = data[2]; 13 | } 14 | 15 | } 16 | 17 | export class LinkInfo { 18 | 19 | source: string; 20 | target: string; 21 | rpm: Array; 22 | fpm: Array; 23 | 24 | constructor(data: SystemStatsLinkDto) { 25 | this.source = data[0]; 26 | this.target = data[1]; 27 | this.rpm = data[2]; 28 | this.fpm = data[3]; 29 | } 30 | 31 | } 32 | 33 | export class SystemStats { 34 | 35 | time: number; 36 | nodes: Array; 37 | links: Array; 38 | 39 | constructor(data: SystemStatsDto) { 40 | this.time = data.time; 41 | this.nodes = []; 42 | this.links = []; 43 | for (let node of data.nodes) this.nodes.push(new NodeInfo(node)); 44 | for (let link of data.links) this.links.push(new LinkInfo(link)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /web/src/app/common/timestamp.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({name: "timestamp"}) 4 | export class TimestampPipe implements PipeTransform { 5 | 6 | private normalize(num: number): string { 7 | return `${ num > 9 ? num : "0" + num }`; 8 | } 9 | 10 | transform(value: number, format: string): string { 11 | let date: Date = new Date(value * 1000); 12 | if (format === "time") { 13 | let hours: number = date.getHours(); 14 | let minutes: number = date.getMinutes(); 15 | return `${ this.normalize(hours) }:${ this.normalize(minutes) }`; 16 | } else if (format === "date") { 17 | let month: number = date.getMonth() + 1; 18 | let day: number = date.getDate(); 19 | return `${ this.normalize(month) }-${ this.normalize(day) }`; 20 | } else { 21 | let month: number = date.getMonth() + 1; 22 | let day: number = date.getDate(); 23 | let hours: number = date.getHours(); 24 | let minutes: number = date.getMinutes(); 25 | return `${ this.normalize(month) }-${ this.normalize(day) } ${ this.normalize(hours) }:${ this.normalize(minutes) }`; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/src/app/common/user/layout.dto.ts: -------------------------------------------------------------------------------- 1 | export interface LayoutBoxDto { 2 | x: number; 3 | y: number; 4 | width: number; 5 | height: number; 6 | } 7 | 8 | export class LayoutDto { 9 | 10 | name: string; 11 | boxes: Map; 12 | nodes: Map; 13 | 14 | constructor() { 15 | this.boxes = new Map(); 16 | this.nodes = new Map(); 17 | } 18 | 19 | public static parse(data: string): LayoutDto { 20 | let layout = new LayoutDto(); 21 | let parsed: any = JSON.parse(data); 22 | layout.name = parsed.name; 23 | for (let boxId in parsed.boxes) { 24 | if (boxId === undefined) continue; 25 | let boxData = parsed.boxes[boxId]; 26 | layout.boxes.set(boxId, { 27 | x: boxData.x, y: boxData.y, width: boxData.width, height: boxData.height 28 | }); 29 | } 30 | for (let nodeName in parsed.nodes) { 31 | if (nodeName === undefined) continue; 32 | layout.nodes.set(nodeName, parsed.nodes[nodeName]); 33 | } 34 | return layout; 35 | } 36 | 37 | public stringify(): string { 38 | let obj = { 39 | name: this.name, 40 | boxes: { }, 41 | nodes: { } 42 | }; 43 | this.boxes.forEach((boxVo: LayoutBoxDto, boxId: string) => { 44 | obj.boxes[boxId] = { 45 | x: boxVo.x, y: boxVo.y, width: boxVo.width, height: boxVo.height 46 | }; 47 | }); 48 | this.nodes.forEach((boxId: string, nodeName: string) => { 49 | obj.nodes[nodeName] = boxId; 50 | }); 51 | return JSON.stringify(obj); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /web/src/app/common/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDto { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /web/src/app/common/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { Connector } from "../connector.service"; 4 | import { UserDto } from "./user.dto"; 5 | import { UserVo } from "./user.vo"; 6 | import { LayoutBoxDto, LayoutDto } from "./layout.dto"; 7 | 8 | // TODO: 9 | // user account management 10 | @Injectable() 11 | export class UserService { 12 | 13 | private userPreferences: Map; 14 | 15 | constructor( 16 | private connector: Connector 17 | ) { } 18 | 19 | private mapToString(map: Map): string { 20 | let obj: any = { }; 21 | map.forEach((value, key) => obj[key] = value); 22 | return JSON.stringify(obj); 23 | } 24 | 25 | private strToMap(value: string): Map { 26 | let map = new Map(); 27 | let obj; 28 | try { obj = JSON.parse(value); } catch (e) { obj = { }; } 29 | for (let k in obj) { 30 | if (k === undefined) continue; 31 | map.set(k, obj[k]); 32 | } 33 | return map; 34 | } 35 | 36 | getUserPreferences(): Promise> { 37 | return new Promise>((resolve, reject) => { 38 | if (this.userPreferences) return resolve(this.userPreferences); 39 | this.userPreferences = this.strToMap(window.localStorage.getItem("user_preferences")); 40 | if (!this.userPreferences) this.userPreferences = new Map(); 41 | resolve(this.userPreferences); 42 | }); 43 | } 44 | 45 | updateUserPreferences(key: string, value: string): Promise { 46 | return new Promise((resolve, reject) => { 47 | this.userPreferences.set(key, value); 48 | window.localStorage.setItem("user_preferences", this.mapToString(this.userPreferences)); 49 | resolve(); 50 | }); 51 | } 52 | 53 | getUserLayouts(): Promise> { 54 | return this.getUserPreferences() 55 | .then((preferences: Map) => { 56 | let rawLayoutsData = preferences.get("layouts"); 57 | let rawLayouts: Array = rawLayoutsData !== undefined ? JSON.parse(rawLayoutsData) : [ ]; 58 | return rawLayouts.map(LayoutDto.parse); 59 | }); 60 | } 61 | 62 | saveLayout(layout: LayoutDto): Promise { 63 | return this.getUserLayouts() 64 | .then((layouts: Array) => { 65 | let found = layouts.find((i) => i.name === layout.name); 66 | if (found) { 67 | found.boxes = layout.boxes; 68 | found.nodes = layout.nodes; 69 | } else { 70 | layouts.push(layout); 71 | } 72 | return this.updateUserPreferences("layouts", JSON.stringify(layouts.map((i) => i.stringify()))); 73 | }); 74 | } 75 | 76 | deleteLayout(layoutName: string): Promise { 77 | return this.getUserLayouts() 78 | .then((layouts: Array) => { 79 | let found = layouts.findIndex((i) => i.name === layoutName); 80 | layouts.splice(found, 1); 81 | return this.updateUserPreferences("layouts", JSON.stringify(layouts.map((i) => i.stringify()))); 82 | }); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /web/src/app/common/user/user.vo.ts: -------------------------------------------------------------------------------- 1 | export interface UserVo { 2 | name: string; 3 | preferences: Map; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, OnDestroy, ViewChild } from "@angular/core"; 2 | import { ActivatedRoute, Router } from "@angular/router"; 3 | import { FormControl } from "@angular/forms"; 4 | import * as moment from "moment"; 5 | import { trigger, state, style, transition, animate } from "@angular/animations"; 6 | import { FailureRoller } from "./failure-roller/failure-roller.component"; 7 | import { SystemSummary } from "./system-summary/system-summary.component"; 8 | import { SystemDetail } from "./system-detail/system-detail.component"; 9 | import { SystemsRpcGraph, GraphLayout, BoundingBox } from "../diagram/systems-rpc-graph/graph.component"; 10 | import { UserService } from "../common/user/user.service"; 11 | import { SystemStatsService } from "../common/system-stats/system-stats.service"; 12 | import { SystemFailureService } from "../common/system-failure/system-failure.service"; 13 | import { SocketService } from "../common/socket.service"; 14 | import { environment } from "../../environments/environment"; 15 | 16 | import { SystemStats, NodeInfo, LinkInfo } from "../common/system-stats/system-stats.vo"; 17 | import { LayoutDto, LayoutBoxDto } from "../common/user/layout.dto"; 18 | import { SystemSelectedEvent } from "../diagram/systems-rpc-graph/system-selected.event"; 19 | 20 | @Component({ 21 | selector: "ow-dashboard", 22 | templateUrl: "dashboard.template.html", 23 | styleUrls: [ "dashboard.style.scss" ], 24 | animations: [ 25 | trigger("detailState", [ 26 | state("hidden", style({ 27 | display: "none", 28 | opacity: 0, 29 | transform: "scale(0.6)" 30 | })), 31 | state("shown", style({ 32 | display: "block", 33 | opacity: 1, 34 | transform: "scale(1)" 35 | })), 36 | transition("hidden => shown", [ 37 | style({ display: "block" }), 38 | animate("200ms ease-in") 39 | ]), 40 | transition("shown => hidden", animate("200ms ease-out")) 41 | ]) 42 | ] 43 | }) 44 | export class Dashboard implements OnInit, OnDestroy { 45 | 46 | private systemStats: SystemStats = null; 47 | private detailState = "hidden"; 48 | @ViewChild("failureRoller") private failureRoller: FailureRoller; 49 | @ViewChild("systemSummary") private systemSummary: SystemSummary; 50 | @ViewChild("systemDetail") private systemDetail: SystemDetail; 51 | @ViewChild("diagram") private diagram: SystemsRpcGraph; 52 | private layoutInputCtrl: FormControl = new FormControl(); 53 | private layouts: Array = [ ]; 54 | private selectedLayout: LayoutDto | "all" = "all"; 55 | private currentLayout: GraphLayout = null; 56 | private updateStatsTimeoutId: number; 57 | 58 | constructor( 59 | private route: ActivatedRoute, 60 | private router: Router, 61 | private userService: UserService, 62 | private systemStatsService: SystemStatsService, 63 | private systemFailureService: SystemFailureService, 64 | private socketService: SocketService 65 | ) { } 66 | 67 | ngOnInit() { 68 | const loadStats = () => { 69 | console.log("updating stats..."); 70 | this.systemStatsService.getSystemStats() 71 | .then((systemStats: SystemStats) => { 72 | this.updateStats(systemStats); 73 | }) 74 | .then(null, (err) => { 75 | console.log(err); 76 | console.log("failed to load system stats"); 77 | }); 78 | // this.updateStatsTimeoutId = window.setTimeout(loadStats, 120000); 79 | }; 80 | loadStats(); 81 | 82 | // load layouts 83 | this.userService.getUserLayouts() 84 | .then((layouts: Array) => { 85 | this.layouts = layouts; 86 | }); 87 | this.layoutInputCtrl.valueChanges.subscribe((layout: LayoutDto | "all") => { 88 | if (layout === undefined) return; 89 | if (layout === "all") this.currentLayout = null; 90 | else this.currentLayout = this.fromVo(layout); 91 | }); 92 | 93 | // load system failures 94 | this.systemFailureService.getSystemFailure({ }) 95 | .then((systemFailures) => systemFailures.map(this.failureRoller.addLogItem)); 96 | 97 | this.socketService.subscribe(this.socketService.getSystemFailureTopic(), this.failureRoller.addLogItem); 98 | this.socketService.subscribe(this.socketService.getSystemStatsTopic(), (data) => { 99 | const stats = new SystemStats(data); 100 | this.updateStats(stats); 101 | }); 102 | 103 | } 104 | 105 | ngOnDestroy() { 106 | this.socketService.unsubscribe(this.socketService.getSystemFailureTopic()); 107 | this.socketService.unsubscribe(this.socketService.getSystemStatsTopic()); 108 | window.clearTimeout(this.updateStatsTimeoutId); 109 | } 110 | 111 | testDiagram() { 112 | 113 | let removedNodes: Set = new Set(); 114 | 115 | // randomly change & remove nodes & links 116 | this.systemStats.nodes = this.systemStats.nodes.filter((node: NodeInfo) => { 117 | if (Math.random() > 0.2) { 118 | node.rpm = [ Math.round(Math.random() * 100000), Math.round(Math.random() * 100000), Math.round(Math.random() * 100000)]; 119 | node.fpm = [ Math.round(Math.random() * 1000), Math.round(Math.random() * 1000), Math.round(Math.random() * 1000)]; 120 | return true; 121 | } else { 122 | removedNodes.add(node.name); 123 | return false; 124 | } 125 | }); 126 | this.systemStats.links = this.systemStats.links.filter((link: LinkInfo) => { 127 | if (!removedNodes.has(link.source) && !removedNodes.has(link.target)) { 128 | link.rpm = [ Math.round(Math.random() * 100000), Math.round(Math.random() * 100000), Math.round(Math.random() * 100000)]; 129 | link.fpm = [ Math.round(Math.random() * 1000), Math.round(Math.random() * 1000), Math.round(Math.random() * 1000)]; 130 | return true; 131 | } else { 132 | return false; 133 | } 134 | }); 135 | 136 | // randomly add nodes & links 137 | for (let i = 0; i < 5; i++) { 138 | if (Math.random() < 0.8) continue; 139 | let nodeName = `test-${ Math.round(Math.random() * 1000) }`; 140 | this.systemStats.nodes.forEach((node: NodeInfo) => { 141 | if (Math.random() < 0.5) { 142 | this.systemStats.links.push({ 143 | source: nodeName, 144 | target: node.name, 145 | rpm: [ Math.round(Math.random() * 100000), Math.round(Math.random() * 100000), Math.round(Math.random() * 100000)], 146 | fpm: [ Math.round(Math.random() * 1000), Math.round(Math.random() * 1000), Math.round(Math.random() * 1000)] 147 | }); 148 | } 149 | }); 150 | this.systemStats.nodes.push({ 151 | name: nodeName, 152 | rpm: [ Math.round(Math.random() * 100000), Math.round(Math.random() * 100000), Math.round(Math.random() * 100000)], 153 | fpm: [ Math.round(Math.random() * 1000), Math.round(Math.random() * 1000), Math.round(Math.random() * 1000)] 154 | }); 155 | } 156 | 157 | this.diagram.update(); 158 | } 159 | 160 | private fromVo(vo: LayoutDto): GraphLayout { 161 | let result: GraphLayout = { 162 | boundingBoxes: new Map(), 163 | nodeBoxMap: new Map() 164 | }; 165 | vo.boxes.forEach((box: LayoutBoxDto, id: string) => { 166 | result.boundingBoxes.set(id, new BoundingBox(box.x, box.y, box.width, box.height)); 167 | }); 168 | vo.nodes.forEach((boxId: string, name: string) => { 169 | result.nodeBoxMap.set(name, boxId); 170 | }); 171 | return result; 172 | } 173 | 174 | updateStats = (systemStats: SystemStats) => { 175 | this.systemStats = systemStats; 176 | this.systemSummary.summarize(systemStats); 177 | } 178 | 179 | showDetail(event: SystemSelectedEvent) { 180 | if (event.eventType === "stats") { 181 | this.detailState = "shown"; 182 | this.systemDetail.showDetail(event.system); 183 | } else { 184 | let begin: number; 185 | let end: number; 186 | let now = moment(); 187 | if (event.eventType === "stats_1h") { 188 | end = now.unix(); 189 | begin = end - 3600; 190 | } else if (event.eventType === "stats_1d") { 191 | now.hours(0); 192 | now.minutes(0); 193 | now.seconds(0); 194 | now.milliseconds(0); 195 | begin = now.unix(); 196 | now.days(now.days() + 1); 197 | end = now.unix(); 198 | } else { 199 | return; 200 | } 201 | this.router.navigate([ "history"], { 202 | queryParams: { 203 | system: event.system, 204 | begin: begin, 205 | end: end 206 | } 207 | }); 208 | } 209 | 210 | } 211 | 212 | hideDetail() { 213 | this.detailState = "hidden"; 214 | } 215 | 216 | animationDone(event) { 217 | if (event.toState === "hidden") 218 | this.systemDetail.clearDetail(); 219 | else 220 | this.systemDetail.drawDiagram(); 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /web/src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 4 | import { MatSelectModule, MatProgressSpinnerModule } from "@angular/material"; 5 | 6 | import { OverwatchCommonModule } from "../common/common.module"; 7 | import { DiagramModule } from "../diagram/diagram.module"; 8 | 9 | import { SystemInfoService } from "../common/system-info/system-info.service"; 10 | import { SystemStatsService } from "../common/system-stats/system-stats.service"; 11 | import { SystemFailureService } from "../common/system-failure/system-failure.service"; 12 | import { Dashboard } from "./dashboard.component"; 13 | import { SystemDetail } from "./system-detail/system-detail.component"; 14 | import { SystemSummary } from "./system-summary/system-summary.component"; 15 | import { FailureRoller } from "./failure-roller/failure-roller.component"; 16 | 17 | @NgModule({ 18 | imports: [ CommonModule, FormsModule, ReactiveFormsModule, MatSelectModule, MatProgressSpinnerModule, 19 | OverwatchCommonModule, DiagramModule ], 20 | declarations: [ Dashboard, SystemDetail, SystemSummary, FailureRoller ], 21 | entryComponents: [ SystemDetail ], 22 | exports: [ Dashboard ], 23 | providers: [ SystemInfoService, SystemStatsService, SystemFailureService ] 24 | }) 25 | export class DashboardModule { } 26 | -------------------------------------------------------------------------------- /web/src/app/dashboard/dashboard.style.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | flex: auto; 4 | } 5 | 6 | @keyframes flash { 7 | 0% { opacity: 1; } 8 | 50% { opacity: 0; } 9 | 100% { opacity: 1; } 10 | } 11 | 12 | .loading { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | width: 100%; 17 | height: 100%; 18 | display: flex; 19 | flex: auto; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | 25 | .diagram-wrapper { 26 | position: absolute; 27 | left: 0; 28 | top: 0; 29 | width: 100%; 30 | height: 100%; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .overlay { 36 | position: absolute; 37 | left: 0; 38 | top: 0; 39 | width: 100%; 40 | height: 100%; 41 | } 42 | 43 | .layout-select { 44 | 45 | margin-top: 5em; 46 | margin-left: 5em; 47 | 48 | .padding { 49 | display: inline-block; 50 | width: 15em; 51 | } 52 | 53 | } 54 | 55 | .system-summary, .log-roller { 56 | position: absolute; 57 | width: 20em; 58 | height: 100%; 59 | top: 0; 60 | padding-top: 10em; 61 | } 62 | 63 | .system-summary { 64 | left: 0; 65 | } 66 | 67 | .log-roller { 68 | right: 0; 69 | } 70 | 71 | @media screen and (max-width: 1000px) { 72 | .hidden-md { 73 | display: none!important; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /web/src/app/dashboard/dashboard.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | All 20 | 21 | {{ layout.name }} 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /web/src/app/dashboard/failure-roller/failure-roller.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, OnInit } from "@angular/core"; 2 | import { trigger, state, style, transition, animate, keyframes } from "@angular/animations"; 3 | import { SystemFailureDto } from "../../common/system-failure/system-failure.dto"; 4 | 5 | interface FailureRowVo { 6 | id: string; 7 | state: "idle" | "changed"; 8 | failure: SystemFailureVo; 9 | } 10 | 11 | interface SystemFailureVo { 12 | time: number; 13 | system: string; 14 | status: string; 15 | count: number; 16 | } 17 | 18 | @Component({ 19 | selector: "ow-failure-roller", 20 | templateUrl: "failure-roller.template.html", 21 | styleUrls: [ "failure-roller.style.scss" ], 22 | animations: [ 23 | trigger("rowState", [ 24 | state("idle", style({ opacity: 1, transform: "translateX(0)" })), 25 | state("changed", style({ opacity: 1, transform: "translateX(0)" })), 26 | transition("void => *", [ 27 | style({ opacity: 0, transform: "translateX(-100%)" }), 28 | animate("0.2s ease-out") 29 | ]), 30 | transition("idle <=> changed", [ 31 | animate(1000, keyframes([ 32 | style({ opacity: 1 }), 33 | style({ opacity: 0.3 }), 34 | style({ opacity: 1 }), 35 | style({ opacity: 0.3 }), 36 | style({ opacity: 1 }) 37 | ])) 38 | ]) 39 | ]) 40 | ] 41 | }) 42 | export class FailureRoller implements OnInit { 43 | 44 | @Output() systemSelected = new EventEmitter(); 45 | private failureRows: Array = []; 46 | private failureLookup: Map = new Map(); 47 | private maxLogRows = 20; 48 | 49 | ngOnInit() { 50 | } 51 | 52 | onSystemSelected(system: string) { 53 | this.systemSelected.emit(system); 54 | } 55 | 56 | private failureHash(failure: SystemFailureDto): string { 57 | let time = failure.time; 58 | time = time - time % 60; 59 | return `${ time }_${ failure.system }_${ failure.status }`; 60 | } 61 | 62 | addLogItem = (failure: SystemFailureDto) => { 63 | let id = this.failureHash(failure); 64 | if (this.failureLookup.has(id)) { 65 | let rowVo: FailureRowVo = this.failureLookup.get(id); 66 | rowVo.failure.count++; 67 | rowVo.state = rowVo.state === "idle" ? "changed" : "idle"; 68 | } else { 69 | let vo: SystemFailureVo = { 70 | time: failure.time, 71 | system: failure.system, 72 | status: failure.status, 73 | count: 1 74 | }; 75 | let newRow: FailureRowVo = { 76 | id: id, 77 | failure: vo, 78 | state: "idle" 79 | }; 80 | this.failureLookup.set(id, newRow); 81 | this.failureRows.unshift(newRow); 82 | this.failureRows.sort((a, b) => b.failure.time - a.failure.time); 83 | if (this.failureRows.length > this.maxLogRows) { 84 | let removed = this.failureRows.pop(); 85 | this.failureLookup.delete(removed.id); 86 | } 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /web/src/app/dashboard/failure-roller/failure-roller.style.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-bottom: 0; 3 | font-weight: 100; 4 | font-size: 1.3em; 5 | text-align: center; 6 | } 7 | 8 | hr { 9 | border: none; 10 | border-top: 1px solid rgba(255, 255, 255, 0.1); 11 | } 12 | 13 | .log-row { 14 | 15 | overflow: hidden; 16 | line-height: 1.5em; 17 | cursor: pointer; 18 | 19 | &:hover { background-color: rgba(255, 255, 255, 0.3); } 20 | &> div { 21 | overflow: hidden; 22 | float: left; 23 | white-space: nowrap; 24 | text-overflow: ellipsis; 25 | } 26 | 27 | .date { width: 2.5em; } 28 | .system { width: 5em; } 29 | .status { 30 | width: 10em; 31 | padding: 0 .5em; 32 | color: #f44; 33 | font-weight: bold; 34 | } 35 | .count { 36 | width: 2.5em; 37 | text-align: right; 38 | padding-right: .5em; 39 | color: #f70; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /web/src/app/dashboard/failure-roller/failure-roller.template.html: -------------------------------------------------------------------------------- 1 |
2 |

Failure Log

3 |
4 |
9 |
{{ row.failure.time | timestamp: 'time' }}
10 |
{{ row.failure.system }}
11 |
{{ row.failure.status }}
12 |
{{ row.failure.count >= 1000 ? "999+" : row.failure.count }}
13 |
14 |
15 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-detail/system-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, ViewChild, EventEmitter, OnInit } from "@angular/core"; 2 | import { RequestsTimeChart } from "../../diagram/requests-time-chart/chart.component"; 3 | 4 | import { SystemInfoService } from "../../common/system-info/system-info.service"; 5 | import { SystemFailureService } from "../../common/system-failure/system-failure.service"; 6 | 7 | import { SystemFailureDto } from "../../common/system-failure/system-failure.dto"; 8 | import { RequestsDataItem } from "../../diagram/requests-time-chart/data.vo"; 9 | import { SystemInfoDetailVo, SystemNodeInfo } from "../../common/system-info/system-info.vo"; 10 | 11 | interface SystemDetailVo { 12 | name: string; 13 | rpm: number; 14 | fpm: number; 15 | hosts: Array; 16 | } 17 | 18 | @Component({ 19 | selector: "ow-system-detail", 20 | templateUrl: "system-detail.template.html", 21 | styleUrls: [ "system-detail.style.scss" ] 22 | }) 23 | export class SystemDetail implements OnInit { 24 | 25 | @Output() private exit = new EventEmitter(); 26 | private systemName: string; 27 | private systemInfo: SystemDetailVo; 28 | private logItems: Array; 29 | private requestsData: Array = null; 30 | private requestsDataDelayedHolder: Array = null; 31 | private delayNeeded = false; 32 | 33 | constructor( 34 | private systemInfoService: SystemInfoService, 35 | private systemFailureService: SystemFailureService 36 | ) { } 37 | 38 | ngOnInit() { 39 | this.clearData(); 40 | } 41 | 42 | private clearData() { 43 | this.systemInfo = { 44 | name: "N/A", 45 | rpm: 0, 46 | fpm: 0, 47 | hosts: [ "N/A" ] 48 | }; 49 | this.requestsData = null; 50 | this.logItems = null; 51 | } 52 | 53 | doExit(exit) { 54 | this.exit.emit(); 55 | } 56 | 57 | private nodeSum(nodes: Array) { 58 | let result = { 59 | rpm: 0, 60 | fpm: 0, 61 | hosts: [ ] 62 | }; 63 | for (let node of nodes) { 64 | result.rpm += node.rpm; 65 | result.fpm += node.fpm; 66 | if (node.name !== "" && result.hosts.indexOf(node.name) < 0) result.hosts.push(node.name); 67 | } 68 | if (result.hosts.length === 0) result.hosts.push("N/A"); 69 | return result; 70 | } 71 | 72 | showDetail(systemName: string) { 73 | this.delayNeeded = true; 74 | this.systemName = systemName; 75 | if (systemName === null) return; 76 | let systemInfo: SystemDetailVo = { 77 | name: systemName, 78 | rpm: 0, 79 | fpm: 0, 80 | hosts: [ ] 81 | }; 82 | let now: number = Math.round(new Date().getTime() / 1000); 83 | this.systemInfoService.getSystemInfoDetail(systemName, now - 3600, now) 84 | .then((data) => { 85 | let requestsData = [ ]; 86 | if (data.length >= 2) { 87 | let latest = 0; 88 | let latestSum; 89 | 90 | for (let item of data) { 91 | let sum = this.nodeSum(item.nodes); 92 | requestsData.push({ 93 | time: item.time, 94 | rpm: sum.rpm, 95 | fpm: sum.fpm 96 | }); 97 | if (item.time > latest) { 98 | latest = item.time; 99 | latestSum = sum; 100 | } 101 | } 102 | systemInfo.rpm = latestSum.rpm; 103 | systemInfo.fpm = latestSum.fpm; 104 | systemInfo.hosts = latestSum.hosts; 105 | this.systemInfo = systemInfo; 106 | } 107 | if (this.delayNeeded) this.requestsDataDelayedHolder = requestsData; 108 | else this.requestsData = requestsData; 109 | }); 110 | this.systemFailureService.getSystemFailure({ 111 | system: systemName, 112 | begin: now - 3600, 113 | end: now 114 | }) 115 | .then((data) => { 116 | this.logItems = data; 117 | }); 118 | } 119 | 120 | clearDetail() { 121 | this.clearData(); 122 | this.requestsData = null; 123 | } 124 | 125 | drawDiagram() { 126 | this.requestsData = this.requestsDataDelayedHolder; 127 | this.requestsDataDelayedHolder = null; 128 | this.delayNeeded = false; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-detail/system-detail.style.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | .card { 10 | flex: none; 11 | overflow-x: hidden; 12 | overflow-y: auto; 13 | width: 800px; 14 | height: 80%; 15 | padding: 3em; 16 | background-color: rgba(40, 44, 52, 0.8); 17 | border: 1px solid #aaa; 18 | border-radius: 20px; 19 | box-shadow: 0 0 15px #aaa; 20 | font-family: Monaco, Consolas, "Courier new", monospace; 21 | 22 | h1 { 23 | font-family: 'Myriad Set Pro'; 24 | font-size: 2.5em; 25 | } 26 | 27 | hr { 28 | clear: both; 29 | border: none; 30 | border-top: 1px solid rgba(255, 255, 255, 0.5); 31 | } 32 | 33 | .system-info { 34 | font-size: .8em; 35 | 36 | dt { 37 | display: inline-block; 38 | float: left; 39 | clear: left; 40 | width: 10em; 41 | line-height: 1.5em; 42 | color: #aaa; 43 | } 44 | 45 | dd { 46 | margin-left: 10em; 47 | line-height: 1.5em; 48 | } 49 | 50 | } 51 | 52 | .failure-log { 53 | text-align: center; 54 | 55 | h1 { 56 | color: #aaa; 57 | font-size: 2em; 58 | } 59 | 60 | .table-wrapper { 61 | 62 | overflow-x: auto; 63 | 64 | table { 65 | width: 100%; 66 | text-align: left; 67 | font-size: .8em; 68 | 69 | thead { 70 | color: #aaa; 71 | line-height: 1.5em; 72 | } 73 | 74 | td, th { 75 | padding: .2em .5em; 76 | white-space: nowrap; 77 | } 78 | } 79 | 80 | } 81 | 82 | } 83 | 84 | } 85 | 86 | .chart-wrapper { 87 | $chart-height: 300px; 88 | height: $chart-height; 89 | 90 | .loading { text-align: center; } 91 | 92 | &> div { height: $chart-height; } 93 | 94 | ow-chart-rq-t { height: $chart-height; } 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-detail/system-detail.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ systemName }}

4 |
5 |
Requests
6 |
{{ systemInfo.rpm | number:'1.0' }} rpm
7 |
Failures
8 |
{{ systemInfo.fpm | number:'1.0' }} rpm
9 |
Success Rate
10 |
{{ (systemInfo.rpm - systemInfo.fpm) / systemInfo.rpm | percent:'1.2-2' }}
11 |
Hosts
12 |
{{ systemInfo.hosts | join: ' ' }}
13 |
14 |
15 |
16 |
Loading...
17 |
No Data
18 |
19 | 20 |
21 |
22 |
23 |
24 |

Failure Log

25 |
Loading...
26 |
No Data
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
TimeStatusHostURL
{{ logItem.time | timestamp: 'time' }}{{ logItem.status }}{{ logItem.host }}{{ logItem.url }}
46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-summary/system-summary.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, OnInit } from "@angular/core"; 2 | import { SystemStats } from "../../common/system-stats/system-stats.vo"; 3 | 4 | interface SystemSummaryItem { 5 | system: string; 6 | summary: string; 7 | priority?: [ number, number ]; 8 | } 9 | 10 | @Component({ 11 | selector: "ow-system-summary", 12 | templateUrl: "system-summary.template.html", 13 | styleUrls: [ "system-summary.style.scss" ] 14 | }) 15 | export class SystemSummary implements OnInit { 16 | 17 | private systemSummaryItems: Array = []; 18 | @Output() systemSelected = new EventEmitter(); 19 | 20 | ngOnInit() { } 21 | 22 | onSystemSelected(system: string): void { 23 | this.systemSelected.emit(system); 24 | } 25 | 26 | private sortItems(systemSummaryItems: Array) { 27 | systemSummaryItems.sort((a, b) => { 28 | if (a.priority[0] !== b.priority[0]) return a.priority[0] - b.priority[0]; 29 | else return b.priority[1] - a.priority[1]; 30 | }); 31 | } 32 | 33 | summarize(systemInfo: SystemStats) { 34 | let systemSummaryItems: Array = []; 35 | for (let node of systemInfo.nodes) { 36 | if (node.fpm[0] + node.fpm[1] + node.fpm[2] === 0) 37 | continue; 38 | let system: string = node.name; 39 | let summary: string; 40 | let priority: [ number, number ]; 41 | if (node.fpm[0] > 0) { 42 | summary = "now"; 43 | priority = [ 0, node.fpm[0] ]; 44 | } else if (node.fpm[1] > 0) { 45 | summary = "in 5min"; 46 | priority = [ 1, node.fpm[1] ]; 47 | } else if (node.fpm[2] > 0) { 48 | summary = "in 15min"; 49 | priority = [ 2, node.fpm[2] ]; 50 | } 51 | systemSummaryItems.push({ 52 | system: system, 53 | summary: `failures ${ summary }`, 54 | priority: priority 55 | }); 56 | } 57 | this.sortItems(systemSummaryItems); 58 | this.systemSummaryItems = systemSummaryItems; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-summary/system-summary.style.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-bottom: 0; 3 | font-weight: 100; 4 | font-size: 1.3em; 5 | text-align: center; 6 | } 7 | 8 | hr { 9 | border: none; 10 | border-top: 1px solid rgba(255, 255, 255, 0.1); 11 | } 12 | 13 | .summary-wrapper { 14 | 15 | text-align: center; 16 | 17 | .system-go { 18 | color: #7f0; 19 | font-size: 2em; 20 | font-weight: bold; 21 | line-height: 2em; 22 | } 23 | 24 | .summary-item { 25 | 26 | overflow: hidden; 27 | cursor: pointer; 28 | line-height: 1.5em; 29 | padding-left: .5em; 30 | padding-right: .5em; 31 | &:hover { background-color: rgba(255, 255, 255, 0.3); } 32 | 33 | .system { 34 | float: left; 35 | overflow: hidden; 36 | width: 10em; 37 | word-wrap: break-word; 38 | word-break: break-all; 39 | white-space: nowrap; 40 | text-align: right; 41 | text-overflow: ellipsis; 42 | } 43 | 44 | .summary { 45 | float: left; 46 | width: 9em; 47 | padding-left: .5em; 48 | color: #f70; 49 | font-weight: bold; 50 | white-space: nowrap; 51 | text-align: left; 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /web/src/app/dashboard/system-summary/system-summary.template.html: -------------------------------------------------------------------------------- 1 |

System Summary

2 |
3 |
4 |
5 | All Systems GO 6 |
7 |
8 |
{{ summary.system }}
9 |
{{ summary.summary }}
10 |
11 |
12 | -------------------------------------------------------------------------------- /web/src/app/diagram/diagram.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { Input, ViewChild, ElementRef } from "@angular/core"; 3 | import * as d3 from "d3-selection"; 4 | 5 | interface LineData { 6 | x1: number | string; 7 | x2: number | string; 8 | y1: number | string; 9 | y2: number | string; 10 | } 11 | 12 | type Selection = d3.Selection; 13 | 14 | @Component({}) 15 | export abstract class Diagram implements OnInit { 16 | 17 | @Input() protected data: DataType = null; 18 | @Input() protected drawOnInit: "true" | "false" = "true"; 19 | @Input("width") protected widthInput: number = null; 20 | @Input("height") protected heightInput: number = null; 21 | protected width: number; 22 | protected height: number; 23 | private updateCancelled = false; 24 | @ViewChild("canvas") protected canvasElement: ElementRef; 25 | protected canvas: d3.Selection; 26 | 27 | ngOnInit() { 28 | let el: SVGSVGElement = this.canvasElement.nativeElement; 29 | this.canvas = d3.select(el); 30 | window.onresize = () => { 31 | this.calcCanvasSize(); 32 | this.onResizeDiagram(); 33 | }; 34 | if (this.drawOnInit !== "false") { 35 | this.onDrawDiagram(); 36 | this.updateDiagram(); 37 | } 38 | } 39 | 40 | protected calcCanvasSize(): void { 41 | let el: SVGElement = this.canvasElement.nativeElement; 42 | let boundingBox: ClientRect = el.getBoundingClientRect(); 43 | this.width = this.widthInput || boundingBox.width; 44 | this.height = this.heightInput || boundingBox.height; 45 | } 46 | 47 | protected drawLine(target: Selection, x1: number, y1: number, x2: number, y2: number, 48 | attr?: Map, percentage?: boolean): Selection { 49 | let line = target.append("line") 50 | .attr("x1", percentage === true ? `${ x1 }%` : x1) 51 | .attr("x2", percentage === true ? `${ x2 }%` : x2) 52 | .attr("y1", percentage === true ? `${ y1 }%` : y1) 53 | .attr("y2", percentage === true ? `${ y2 }%` : y2) 54 | .attr("stroke-width", 1).attr("stroke", "red"); 55 | if (attr) attr.forEach((value, key) => line.attr(key, value)); 56 | return line; 57 | } 58 | 59 | protected drawBox(target: Selection, x1: number, y1: number, x2: number, y2: number, 60 | attr?: Map, percentage?: boolean): Selection { 61 | let group = target.append("g"); 62 | this.drawLine(group, x1, y1, x2, y1, attr, percentage); 63 | this.drawLine(group, x2, y1, x2, y2, attr, percentage); 64 | this.drawLine(group, x2, y2, x1, y2, attr, percentage); 65 | this.drawLine(group, x1, y2, x1, y1, attr, percentage); 66 | return group; 67 | } 68 | 69 | public clearDiagram(): void { 70 | this.canvas.selectAll("*").remove(); 71 | } 72 | 73 | protected onDrawDiagram(cross?: boolean): void { 74 | // draw a placeholder by default 75 | this.calcCanvasSize(); 76 | let attr = new Map(); 77 | attr.set("stroke", "red"); 78 | let border = this.drawBox(this.canvas, 0, 0, 100, 100, attr, true); 79 | if (cross !== false) { 80 | this.drawLine(border, 0, 0, 100, 100, attr, true); 81 | this.drawLine(border, 0, 100, 100, 0, attr, true); 82 | } 83 | } 84 | 85 | private updateDiagram(): void { 86 | if (this.data) this.onUpdateDiagram(); 87 | } 88 | 89 | protected abstract onUpdateDiagram(): void; 90 | protected abstract onResizeDiagram(): void; 91 | 92 | public update(): void { 93 | this.onUpdateDiagram(); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /web/src/app/diagram/diagram.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { OverwatchCommonModule } from "../common/common.module"; 4 | import { RequestsTimeChart } from "./requests-time-chart/chart.component"; 5 | import { SystemsRpcGraph } from "./systems-rpc-graph/graph.component"; 6 | import { LayoutEditor } from "./layout-editor/layout-editor.component"; 7 | 8 | @NgModule({ 9 | imports: [ CommonModule, OverwatchCommonModule ], 10 | declarations: [ RequestsTimeChart, SystemsRpcGraph, LayoutEditor ], 11 | exports: [ RequestsTimeChart, SystemsRpcGraph, LayoutEditor ] 12 | }) 13 | export class DiagramModule { } 14 | -------------------------------------------------------------------------------- /web/src/app/diagram/diagram.style.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | flex: auto; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | svg { 10 | width: 100%; 11 | flex: auto; 12 | } 13 | -------------------------------------------------------------------------------- /web/src/app/diagram/diagram.template.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/app/diagram/layout-editor/layout-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from "@angular/core"; 2 | import { Diagram } from "../diagram.component"; 3 | import * as d3Selection from "d3-selection"; 4 | import * as d3Scale from "d3-scale"; 5 | import { Pos, BoxVo, NodeVo, LayoutVo } from "./layout.vo"; 6 | 7 | @Component({ 8 | selector: "ow-layout-editor", 9 | templateUrl: "../diagram.template.html", 10 | styleUrls: [ "../diagram.style.scss" ] 11 | }) 12 | export class LayoutEditor extends Diagram { 13 | 14 | @Output() private nodeRemoved: EventEmitter = new EventEmitter(); 15 | @Output() private boxRemoved: EventEmitter = new EventEmitter(); 16 | 17 | private boxes: d3Selection.Selection; 18 | private nodes: d3Selection.Selection; 19 | private boxVo: Array = new Array(); 20 | private nodeVo: Array = new Array(); 21 | private startPos: Pos = null; 22 | private redrawNode: d3Selection.Selection = null; 23 | private action: "drag" | "scale" = null; 24 | private color: d3Scale.ScaleOrdinal = d3Scale.scaleOrdinal().range(d3Scale.schemeCategory20); 25 | 26 | public addNode(name: string, boxId: string): void { 27 | let x = 100; 28 | let y = 100; 29 | 30 | const box: BoxVo = this.boxVo.find((b: BoxVo) => boxId === b.id); 31 | if (box !== undefined) { 32 | x = box.x + box.width / 3 + box.width / 3 * Math.random(); 33 | y = box.y + box.height / 3 + box.height / 3 * Math.random(); 34 | } 35 | 36 | this.nodeVo.push({ 37 | name: name, 38 | boxId: boxId, 39 | x: x, 40 | y: y 41 | }); 42 | this.onUpdateDiagram(); 43 | } 44 | 45 | public removeNode(name: string): void { 46 | this.nodeVo = this.nodeVo.filter((node: NodeVo) => node.name !== name); 47 | this.onUpdateDiagram(); 48 | } 49 | 50 | public addBox(id: string, x: number, y: number, width: number, height: number): void { 51 | this.boxVo.push({ 52 | id: id, 53 | width: width, 54 | height: height, 55 | x: x, 56 | y: y 57 | }); 58 | this.onUpdateDiagram(); 59 | } 60 | 61 | public removeBox(boxId: string): void { 62 | this.boxVo = this.boxVo.filter((box: BoxVo) => box.id !== boxId); 63 | this.onUpdateDiagram(); 64 | } 65 | 66 | public save(): LayoutVo { 67 | let layoutVo: LayoutVo = { 68 | boxes: new Map(), 69 | nodes: new Map() 70 | }; 71 | this.boxes.selectAll(".box") 72 | .each((d: BoxVo) => layoutVo.boxes.set(d.id, d)); 73 | this.nodes.selectAll(".node") 74 | .each((d: NodeVo) => { 75 | let boxId = ""; 76 | layoutVo.boxes.forEach((box: BoxVo, id: string) => { 77 | const isInside = d.x > box.x 78 | && d.x < box.x + box.width 79 | && d.y > box.y 80 | && d.y < box.y + box.height; 81 | if ( 82 | d.x > box.x 83 | && d.x < box.x + box.width 84 | && d.y > box.y 85 | && d.y < box.y + box.height 86 | ) boxId = id; 87 | }); 88 | layoutVo.nodes.set(d.name, boxId); 89 | }); 90 | return layoutVo; 91 | } 92 | 93 | onDrawDiagram() { 94 | this.boxes = this.canvas.append("g") 95 | .attr("class", "boxes"); 96 | this.nodes = this.canvas.append("g") 97 | .attr("class", "nodes"); 98 | this.canvas 99 | .on("mousemove", () => { 100 | if (this.action === null) return; 101 | let vo: BoxVo = this.redrawNode.data()[0]; 102 | if (this.action === "drag") { 103 | let pos = d3Selection.mouse(this.canvas.node()); 104 | pos[0] -= this.startPos.x; 105 | pos[1] -= this.startPos.y; 106 | vo.x = pos[0] > 0 ? pos[0] : 0; 107 | vo.y = pos[1] > 0 ? pos[1] : 0; 108 | this.redrawNode 109 | .attr("transform", `translate(${ vo.x }, ${ vo.y })`); 110 | } else if (this.action === "scale") { 111 | let pos = d3Selection.mouse(this.redrawNode.node()); 112 | vo.width = pos[0] > 30 ? pos[0] : 30; 113 | vo.height = pos[1] > 30 ? pos[1] : 30; 114 | this.redrawNode.select("rect") 115 | .attr("width", vo.width) 116 | .attr("height", vo.height); 117 | this.redrawNode.select("circle") 118 | .attr("cx", vo.width) 119 | .attr("cy", vo.height); 120 | this.redrawNode.select(".btn-del") 121 | .attr("transform", `translate(${ vo.width }, 0)`); 122 | } 123 | }) 124 | .on("mouseup", () => { 125 | this.action = null; 126 | this.redrawNode = null; 127 | }); 128 | } 129 | 130 | private initBtnDel = (d: d3Selection.Selection) => { 131 | d.append("circle") 132 | .attr("cx", 0).attr("cy", 0) 133 | .attr("r", 5) 134 | .attr("fill", "red") 135 | .attr("stroke", "none"); 136 | d.append("text") 137 | .attr("text-anchor", "middle") 138 | .attr("alignment-baseline", "central") 139 | .attr("fill", "white") 140 | .attr("font-size", 16) 141 | .text("-"); 142 | d.style("cursor", "pointer"); 143 | } 144 | 145 | private initBox = (root: d3Selection.Selection) => { 146 | root.append("rect") 147 | .attr("x", (d) => 0) 148 | .attr("y", (d) => 0) 149 | .attr("width", (d) => d.width) 150 | .attr("height", (d) => d.height) 151 | .attr("stroke", (d, i) => this.color(i.toString())) 152 | .attr("stroke-width", 2) 153 | .attr("fill", "transparent") 154 | .style("cursor", "move"); 155 | root.on("mousedown", (d, i , g) => { 156 | this.action = "drag"; 157 | this.redrawNode = d3Selection.select(g[i]); 158 | let pos = d3Selection.mouse(this.redrawNode.node()); 159 | this.startPos = { x: pos[0], y: pos[1] }; 160 | }); 161 | let btnScale = root.append("circle") 162 | .attr("cx", (d) => d.width) 163 | .attr("cy", (d) => d.height) 164 | .attr("r", 5) 165 | .attr("stroke", "none") 166 | .attr("fill", (d, i) => this.color(i.toString())) 167 | .style("cursor", "nwse-resize"); 168 | btnScale.on("mousedown", (d, i, g) => { 169 | let event: MouseEvent = d3Selection.event; 170 | event.stopPropagation(); 171 | this.action = "scale"; 172 | let node: SVGGElement = (g[i]).parentElement; 173 | this.redrawNode = d3Selection.select(node); 174 | }); 175 | let btnDel = root.append("g") 176 | .attr("class", "btn-del") 177 | .attr("transform", (d) => `translate(${ d.width }, 0)`) 178 | .call(this.initBtnDel); 179 | btnDel.on("click", (d) => this.boxRemoved.emit(d.id)); 180 | } 181 | 182 | private initNode = (root: d3Selection.Selection) => { 183 | root.append("circle") 184 | .attr("cx", 0) 185 | .attr("cy", 0) 186 | .attr("r", 20) 187 | .attr("fill", "#0af") 188 | .attr("stroke", "none"); 189 | root.append("text") 190 | .attr("text-anchor", "middle") 191 | .attr("alignment-baseline", "central") 192 | .attr("fill", "white") 193 | .attr("font-size", 16) 194 | .text((d) => d.name); 195 | let btnDel = root.append("g") 196 | .attr("class", "btn-del") 197 | .attr("transform", `translate(${ 20 / Math.sqrt(2) }, ${ -20 / Math.sqrt(2) })`) 198 | .call(this.initBtnDel); 199 | btnDel.on("click", (d) => this.nodeRemoved.emit(d.name)); 200 | root.style("cursor", "move"); 201 | root.on("mousedown", (d, i, g) => { 202 | this.action = "drag"; 203 | this.redrawNode = d3Selection.select(g[i]); 204 | let pos = d3Selection.mouse(this.redrawNode.node()); 205 | this.startPos = { x: pos[0], y: pos[1] }; 206 | }); 207 | } 208 | 209 | onUpdateDiagram() { 210 | let boxes = this.boxes.selectAll(".box") 211 | .data(this.boxVo, (d: BoxVo) => d.id); 212 | boxes.enter().append("g") 213 | .attr("class", "box") 214 | .call(this.initBox) 215 | .merge(boxes) 216 | .attr("transform", (d) => `translate(${ d.x }, ${ d.y })`); 217 | boxes.exit().remove(); 218 | let nodes = this.nodes.selectAll(".node") 219 | .data(this.nodeVo, (d: NodeVo) => d.name); 220 | nodes.enter().append("g") 221 | .attr("class", "node") 222 | .call(this.initNode) 223 | .merge(nodes) 224 | .attr("transform", (d: NodeVo) => `translate(${ d.x }, ${ d.y })`); 225 | nodes.exit().remove(); 226 | } 227 | 228 | onResizeDiagram() { 229 | // TODO 230 | } 231 | 232 | clear() { 233 | if (this.boxes) this.boxes.selectAll(".box").remove(); 234 | if (this.nodes) this.nodes.selectAll(".node").remove(); 235 | this.boxVo = new Array(); 236 | this.nodeVo = new Array(); 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /web/src/app/diagram/layout-editor/layout.vo.ts: -------------------------------------------------------------------------------- 1 | export interface LayoutVo { 2 | boxes: Map; 3 | nodes: Map; 4 | } 5 | 6 | export interface Pos { 7 | x: number; 8 | y: number; 9 | } 10 | 11 | export interface BoxVo extends Pos { 12 | id: string; 13 | width: number; 14 | height: number; 15 | } 16 | 17 | export interface NodeVo extends Pos { 18 | name: string; 19 | boxId: string; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/app/diagram/requests-time-chart/data.vo.ts: -------------------------------------------------------------------------------- 1 | export interface RequestsDataItem { 2 | time: number; 3 | rpm: number; 4 | fpm: number; 5 | } 6 | -------------------------------------------------------------------------------- /web/src/app/diagram/systems-rpc-graph/graph.style.scss: -------------------------------------------------------------------------------- 1 | @keyframes lineFlow { 2 | 0% { stroke-dashoffset: 0; } 3 | 100% { stroke-dashoffset: -100%; } 4 | } 5 | -------------------------------------------------------------------------------- /web/src/app/diagram/systems-rpc-graph/system-selected.event.ts: -------------------------------------------------------------------------------- 1 | export interface SystemSelectedEvent { 2 | system: string; 3 | eventType: "stats" | "stats_1h" | "stats_1d"; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/app/diagram/systems-rpc-graph/system-stats.vo.ts: -------------------------------------------------------------------------------- 1 | export interface NodeInfo { 2 | name: string; 3 | rpm: [ number, number, number ]; 4 | fpm: [ number, number, number ]; 5 | } 6 | 7 | export interface LinkInfo { 8 | source: string; 9 | target: string; 10 | rpm: [ number, number, number ]; 11 | fpm: [ number, number, number ]; 12 | } 13 | 14 | export interface SystemStats { 15 | time: number; 16 | nodes: Array; 17 | links: Array; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/history/history.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from "@angular/core"; 2 | import { ActivatedRoute, Router } from "@angular/router"; 3 | import { FormControl } from "@angular/forms"; 4 | import { MatDatepickerInputEvent } from "@angular/material"; 5 | import { RequestsDataItem } from "../diagram/requests-time-chart/data.vo"; 6 | import { RequestsTimeChart } from "../diagram/requests-time-chart/chart.component"; 7 | import { SystemInfoService } from "../common/system-info/system-info.service"; 8 | import { SystemFailureService } from "../common/system-failure/system-failure.service"; 9 | import { SystemInfoVo } from "../common/system-info/system-info.vo"; 10 | import { SystemFailureDto } from "../common/system-failure/system-failure.dto"; 11 | 12 | interface ChartOptions { 13 | system: string; 14 | begin: Date; 15 | end: Date; 16 | } 17 | 18 | @Component({ 19 | selector: "ow-history", 20 | templateUrl: "history.template.html", 21 | styleUrls: [ "history.style.scss" ] 22 | }) 23 | export class History implements OnInit { 24 | 25 | private chartData: Array = [ ]; 26 | private chartOpt: ChartOptions; 27 | private systems: Array = [ ]; 28 | private filteredSystems: Array = [ ]; 29 | private systemInputCtrl: FormControl = new FormControl(); 30 | @ViewChild("chart") private chart: RequestsTimeChart; 31 | private chartState: "noData" | "loading" | "shown" = null; 32 | private failureLogs: Array = [ ]; 33 | 34 | constructor( 35 | private route: ActivatedRoute, 36 | private router: Router, 37 | private systemInfoService: SystemInfoService, 38 | private systemFailureService: SystemFailureService 39 | ) { 40 | let begin = new Date(); 41 | begin.setHours(0); 42 | begin.setMinutes(0); 43 | begin.setSeconds(0); 44 | begin.setMilliseconds(0); 45 | let end = new Date(); 46 | end.setHours(23); 47 | end.setMinutes(59); 48 | end.setSeconds(59); 49 | end.setMilliseconds(999); 50 | this.chartOpt = { 51 | system: "", 52 | begin: begin, 53 | end: end 54 | }; 55 | } 56 | 57 | private loadSystems() { 58 | this.systemInfoService.getSystemList() 59 | .then((systems: Array) => { 60 | this.systems = systems; 61 | this.filterSystems(this.systemInputCtrl.value); 62 | }); 63 | } 64 | 65 | private filterSystems(val: string): void { 66 | if (val) this.filteredSystems = this.systems.filter(s => new RegExp(`^${val}`, "gi").test(s)); 67 | else this.filteredSystems = this.systems; 68 | } 69 | 70 | ngOnInit(): void { 71 | this.loadSystems(); 72 | this.systemInputCtrl.valueChanges.subscribe(name => this.filterSystems(name)); 73 | this.route.queryParams.subscribe(params => { 74 | if (params["system"]) this.chartOpt.system = params["system"]; 75 | if (params["begin"]) this.chartOpt.begin = new Date(params["begin"] * 1000); 76 | if (params["end"]) this.chartOpt.end = new Date(params["end"] * 1000); 77 | if (this.chartOpt.system.trim().length > 0) this.go(); 78 | }); 79 | } 80 | 81 | testDiagram() { 82 | let data: Array = new Array(); 83 | let now: number = Math.round(new Date().getTime() / 1000); 84 | for (let i = 0; i < 100; i++) { 85 | data.push({ 86 | time: now - i * 60, 87 | rpm: Math.round(Math.random() * 300) + 500, 88 | fpm: Math.round(Math.random() * 10) 89 | }); 90 | } 91 | this.chartData = data; 92 | this.chartState = "shown"; 93 | } 94 | 95 | beginDateChanged(date: MatDatepickerInputEvent): void { 96 | let begin = date.value; 97 | begin.setHours(0); 98 | begin.setMinutes(0); 99 | begin.setSeconds(0); 100 | begin.setMilliseconds(0); 101 | if (begin > this.chartOpt.end) { 102 | let end = new Date(begin); 103 | end.setHours(23); 104 | end.setMinutes(59); 105 | end.setSeconds(59); 106 | end.setMilliseconds(999); 107 | this.chartOpt.end = end; 108 | } 109 | this.chartOpt.begin = begin; 110 | } 111 | 112 | endDateChanged(date: MatDatepickerInputEvent): void { 113 | let end = date.value; 114 | end.setHours(23); 115 | end.setMinutes(59); 116 | end.setSeconds(59); 117 | end.setMilliseconds(999); 118 | if (end < this.chartOpt.begin) { 119 | let begin = new Date(end); 120 | begin.setHours(0); 121 | begin.setMinutes(0); 122 | begin.setSeconds(0); 123 | begin.setMilliseconds(0); 124 | this.chartOpt.begin = begin; 125 | } 126 | this.chartOpt.end = end; 127 | } 128 | 129 | go(): void { 130 | let system: string = this.chartOpt.system; 131 | if (system === "") { 132 | window.alert("please select a system"); 133 | return; 134 | } 135 | let begin: number = Math.round(this.chartOpt.begin.getTime() / 1000); 136 | let end: number = Math.round(this.chartOpt.end.getTime() / 1000); 137 | this.chartState = "loading"; 138 | this.systemInfoService.getSystemInfo(system, begin, end) 139 | .then((systemInfo: Array) => { 140 | if (systemInfo.length === 0) { 141 | this.chartState = "noData"; 142 | return; 143 | } 144 | let data: Array = systemInfo.map((info) => { 145 | return { 146 | time: info.time, 147 | rpm: info.rpm, 148 | fpm: info.fpm 149 | }; 150 | }); 151 | this.chartData = data; 152 | this.chartState = "shown"; 153 | }); 154 | this.systemFailureService.getSystemFailure({ 155 | system: system, 156 | begin: begin, 157 | end: end, 158 | count: 100 159 | }) 160 | .then((systemFailures: Array) => { 161 | this.failureLogs = systemFailures; 162 | }); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /web/src/app/history/history.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 4 | import { MatInputModule, MatAutocompleteModule, MatButtonModule, 5 | MatNativeDateModule, MatDatepickerModule, MatProgressSpinnerModule } from "@angular/material"; 6 | 7 | import { OverwatchCommonModule } from "../common/common.module"; 8 | import { DiagramModule } from "../diagram/diagram.module"; 9 | 10 | import { History } from "./history.component"; 11 | import { SystemInfoService } from "../common/system-info/system-info.service"; 12 | import { SystemFailureService } from "../common/system-failure/system-failure.service"; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, FormsModule, ReactiveFormsModule, 17 | MatInputModule, MatAutocompleteModule, MatButtonModule, MatNativeDateModule, MatDatepickerModule, MatProgressSpinnerModule, 18 | OverwatchCommonModule, DiagramModule 19 | ], 20 | declarations: [ History ], 21 | exports: [ History ], 22 | providers: [ SystemInfoService, SystemFailureService ] 23 | }) 24 | export class HistoryModule { } 25 | -------------------------------------------------------------------------------- /web/src/app/history/history.style.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | overflow-y: auto; 3 | } 4 | 5 | .main { 6 | 7 | margin: auto; 8 | width: 100%; 9 | max-width: 1000px; 10 | 11 | form { 12 | margin-top: 5em; 13 | text-align: center; 14 | font-size: 1em; 15 | } 16 | 17 | .chart { 18 | 19 | $chart-height: 400px; 20 | 21 | height: $chart-height; 22 | box-shadow: 0 0 10px #666; 23 | 24 | ow-chart-rq-t { 25 | height: $chart-height; 26 | } 27 | 28 | &>div { 29 | height: $chart-height; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | } 36 | 37 | .failure-log { 38 | 39 | margin-top: 5em; 40 | 41 | h1 { 42 | text-align: center; 43 | font-size: 1.5em; 44 | } 45 | 46 | .table-wrapper { 47 | 48 | overflow-x: auto; 49 | text-align: center; 50 | margin-bottom: 5em; 51 | 52 | table { 53 | width: 100%; 54 | text-align: left; 55 | font-size: 1em; 56 | font-family: monospace; 57 | 58 | thead { 59 | color: #888; 60 | line-height: 1.5em; 61 | } 62 | 63 | td, th { 64 | padding: .2em .5em; 65 | white-space: nowrap; 66 | } 67 | 68 | td { color: #ddd; } 69 | td.status { color: #d44; } 70 | td.time { color: #888; } 71 | 72 | } 73 | 74 | } 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /web/src/app/history/history.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 9 | 10 | 11 | 12 | {{ system }} 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 | Select system & time 42 |
43 |
44 | 45 |
46 |
47 | No Data 48 |
49 |
50 | 51 |
52 |

Failure Log

53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
TimeStatusHostURL
{{ logItem.time | timestamp: 'time' }}{{ logItem.status }}{{ logItem.host }}{{ logItem.url }}
72 | No Data 73 |
74 |
75 |
76 | -------------------------------------------------------------------------------- /web/src/app/layout/layout-name-input-dialog.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /web/src/app/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from "@angular/core"; 2 | import { FormControl } from "@angular/forms"; 3 | import { MatSnackBar, MatDialog, MatDialogRef } from "@angular/material"; 4 | import { LayoutEditor } from "../diagram/layout-editor/layout-editor.component"; 5 | import { UserService } from "../common/user/user.service"; 6 | import { SystemInfoService } from "../common/system-info/system-info.service"; 7 | 8 | import { LayoutVo, BoxVo } from "../diagram/layout-editor/layout.vo"; 9 | import { LayoutDto, LayoutBoxDto } from "../common/user/layout.dto"; 10 | 11 | @Component({ 12 | selector: "ow-layout", 13 | templateUrl: "layout.template.html", 14 | styleUrls: [ "layout.style.scss" ] 15 | }) 16 | export class Layout implements OnInit { 17 | 18 | private newSystem = ""; 19 | private boxes: Set = new Set(); 20 | private selectedLayout: LayoutDto | "null" = "null"; 21 | private layouts: Array = [ ]; 22 | private layoutInputCtrl: FormControl = new FormControl(); 23 | private layoutName: string; 24 | private isNew = true; 25 | @ViewChild("editor") private layoutEditor: LayoutEditor; 26 | 27 | constructor( 28 | private snackBar: MatSnackBar, 29 | private dialog: MatDialog, 30 | private userService: UserService, 31 | private systemInfoService: SystemInfoService 32 | ) { } 33 | 34 | ngOnInit() { 35 | this.layoutInputCtrl.valueChanges.subscribe((input: LayoutDto | "null") => { 36 | if (input === undefined) return; 37 | let layout: LayoutDto; 38 | layout = input === "null" ? null : input; 39 | this.isNew = layout === null; 40 | this.layoutName = layout === null ? null : layout.name; 41 | if (layout !== null) this.showLayout(layout); 42 | else this.layoutEditor.clear(); 43 | }); 44 | this.userService.getUserLayouts() 45 | .then((layouts: Array) => { 46 | this.layouts = layouts; 47 | this.selectedLayout = "null"; 48 | }); 49 | } 50 | 51 | private showLayout(layout: LayoutDto): void { 52 | this.layoutEditor.clear(); 53 | this.boxes.clear(); 54 | layout.boxes.forEach((box: LayoutBoxDto, id: string) => this.layoutEditor.addBox(id, box.x, box.y, box.width, box.height)); 55 | layout.nodes.forEach((boxId: string, name: string) => this.layoutEditor.addNode(name, boxId)); 56 | } 57 | 58 | addNode() { 59 | if (this.newSystem.trim().length === 0) return; 60 | this.layoutEditor.addNode(this.newSystem, ""); 61 | this.newSystem = ""; 62 | } 63 | 64 | removeNode(node: string) { 65 | this.layoutEditor.removeNode(node); 66 | } 67 | 68 | addBox() { 69 | if (this.boxes.size >= 10) return; 70 | let id: string; 71 | do { id = Math.round(Math.random() * 100).toString(); } 72 | while (this.boxes.has(id)); 73 | this.layoutEditor.addBox(id, 100, 100, 200, 100); 74 | } 75 | 76 | removeBox(boxId: string) { 77 | this.layoutEditor.removeBox(boxId); 78 | this.boxes.delete(boxId); 79 | } 80 | 81 | save() { 82 | if (this.isNew) { 83 | let dialogRef = this.dialog.open(LayoutNameInputDialog); 84 | dialogRef.afterClosed().subscribe(result => { 85 | if (result && result.trim().length > 0) { 86 | this.layoutName = result; 87 | this.doSave(result, true); 88 | } 89 | }); 90 | } else { 91 | this.doSave(this.layoutName, false); 92 | } 93 | } 94 | 95 | private doSave(layoutName: string, isNew: boolean) { 96 | let layoutVo: LayoutVo = this.layoutEditor.save(); 97 | 98 | let layoutDto: LayoutDto = new LayoutDto(); 99 | layoutDto.name = layoutName; 100 | layoutVo.boxes.forEach((box: BoxVo, boxId: string) => layoutDto.boxes.set(boxId, { 101 | x: box.x, y: box.y, width: box.width, height: box.height 102 | })); 103 | layoutVo.nodes.forEach((boxId: string, name: string) => layoutDto.nodes.set(name, boxId)); 104 | 105 | this.userService.saveLayout(layoutDto) 106 | .then(() => { 107 | if (isNew) { 108 | this.layouts.push(layoutDto); 109 | this.selectedLayout = layoutDto; 110 | } else { 111 | let layout = this.layouts.find((l) => layoutDto.name === l.name); 112 | layout.nodes = layoutDto.nodes; 113 | layout.boxes = layoutDto.boxes; 114 | } 115 | this.snackBar.open("Layout Saved!", "Done", { 116 | duration: 2000 117 | }); 118 | }); 119 | } 120 | 121 | delete() { 122 | let found = this.layouts.findIndex((layout) => layout.name === this.layoutName); 123 | if (found < 0) return; 124 | this.userService.deleteLayout(this.layoutName) 125 | .then(() => { 126 | this.layouts.splice(found, 1); 127 | this.selectedLayout = "null"; 128 | this.layoutName = undefined; 129 | this.snackBar.open("Layout Deleted!", "Done", { 130 | duration: 2000 131 | }); 132 | }); 133 | } 134 | 135 | } 136 | 137 | @Component({ 138 | selector: "layout-name-input-dialog", 139 | templateUrl: "./layout-name-input-dialog.template.html", 140 | }) 141 | export class LayoutNameInputDialog { 142 | 143 | private layoutName: string; 144 | 145 | constructor(public dialogRef: MatDialogRef) { } 146 | 147 | cancel() { 148 | this.dialogRef.close(); 149 | } 150 | 151 | confirm() { 152 | this.dialogRef.close(this.layoutName); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /web/src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 4 | import { MatInputModule, MatButtonModule, MatSelectModule, MatSnackBarModule, MatDialogModule } from "@angular/material"; 5 | 6 | import { OverwatchCommonModule } from "../common/common.module"; 7 | import { DiagramModule } from "../diagram/diagram.module"; 8 | 9 | import { Layout, LayoutNameInputDialog } from "./layout.component"; 10 | import { UserService } from "../common/user/user.service"; 11 | import { SystemInfoService } from "../common/system-info/system-info.service"; 12 | import { SystemFailureService } from "../common/system-failure/system-failure.service"; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, FormsModule, ReactiveFormsModule, 17 | MatInputModule, MatButtonModule, MatSelectModule, MatSnackBarModule, MatDialogModule, 18 | OverwatchCommonModule, DiagramModule 19 | ], 20 | declarations: [ Layout, LayoutNameInputDialog ], 21 | entryComponents: [ LayoutNameInputDialog ], 22 | exports: [ Layout ], 23 | providers: [ UserService, SystemInfoService, SystemFailureService ] 24 | }) 25 | export class LayoutModule { } 26 | -------------------------------------------------------------------------------- /web/src/app/layout/layout.style.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | flex: auto; 5 | } 6 | 7 | .ops-wrapper { 8 | padding: 1em; 9 | margin-top: 5em; 10 | overflow: hidden; 11 | 12 | &> div { 13 | display: inline-block; 14 | padding-left: 5em; 15 | } 16 | 17 | } 18 | 19 | .full-width { 20 | width: 100%; 21 | } 22 | -------------------------------------------------------------------------------- /web/src/app/layout/layout.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | {{ layout.name }} 8 | 9 | <New> 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 33 | -------------------------------------------------------------------------------- /web/src/app/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from "@angular/core"; 2 | import { Router, ActivatedRoute, Params } from "@angular/router"; 3 | 4 | @Component({ 5 | selector: "ow-toolbar", 6 | templateUrl: "toolbar.template.html", 7 | styleUrls: [ "toolbar.style.scss" ] 8 | }) 9 | export class Toolbar { 10 | 11 | constructor( 12 | private route: ActivatedRoute, 13 | private router: Router 14 | ) { } 15 | 16 | goto(page: string): void { 17 | this.router.navigate([`/${ page }`]); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /web/src/app/toolbar/toolbar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { OverwatchCommonModule } from "../common/common.module"; 4 | 5 | import { Toolbar } from "./toolbar.component"; 6 | 7 | @NgModule({ 8 | imports: [ CommonModule, OverwatchCommonModule ], 9 | declarations: [ Toolbar ], 10 | exports: [ Toolbar ], 11 | providers: [ ] 12 | }) 13 | export class ToolbarModule { } 14 | -------------------------------------------------------------------------------- /web/src/app/toolbar/toolbar.style.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .menu-items { 6 | 7 | overflow: hidden; 8 | padding-left: 2em; 9 | padding-right: 2em; 10 | margin: 0; 11 | 12 | .menu-item { 13 | float: left; 14 | padding: 1em; 15 | 16 | &.right { float: right; } 17 | &:hover { 18 | background-color: rgba(255, 255, 255, 0.1); 19 | cursor: pointer; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /web/src/app/toolbar/toolbar.template.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /web/src/assets/myriad-set-pro_ultralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imdada/overwatch/d17e5aaece48a6fb5df20a0241a233fb2e710352/web/src/assets/myriad-set-pro_ultralight.woff -------------------------------------------------------------------------------- /web/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: "http://localhost:3000" 4 | }; 5 | -------------------------------------------------------------------------------- /web/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | apiUrl: "http://localhost:3000" 9 | }; 10 | -------------------------------------------------------------------------------- /web/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imdada/overwatch/d17e5aaece48a6fb5df20a0241a233fb2e710352/web/src/favicon.ico -------------------------------------------------------------------------------- /web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Overwatch 6 | 7 | 8 | 9 | 10 | 11 | Loading... 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core"; 2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; 3 | 4 | import { AppModule } from "./app/app.module"; 5 | import { environment } from "./environments/environment"; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /web/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** Evergreen browsers require these. **/ 41 | import 'core-js/es6/reflect'; 42 | import 'core-js/es7/reflect'; 43 | 44 | 45 | /** 46 | * Required to support Web Animations `@angular/animation`. 47 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 48 | **/ 49 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | /** 70 | * Need to import at least one locale-data with intl. 71 | */ 72 | // import 'intl/locale-data/jsonp/en'; 73 | -------------------------------------------------------------------------------- /web/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/prebuilt-themes/pink-bluegrey.css'; 2 | @import '~font-awesome/css/font-awesome.css'; 3 | 4 | @font-face { 5 | font-family: 'Myriad Set Pro'; 6 | font-style: normal; 7 | font-weight: 100; 8 | src: url('assets/myriad-set-pro_ultralight.woff') format('woff'); 9 | /* Copyright (c) 1992 Adobe Systems Incorporated. All Rights Reserved. Myriad is a trademark of Adobe Systems Incorporated. */ 10 | } 11 | 12 | html, body { 13 | height: 100%; 14 | } 15 | 16 | body { 17 | overflow: hidden; 18 | margin: 0; 19 | padding: 0; 20 | color: white; 21 | background-color: #282c34; 22 | font-family: 'Myriad Set Pro'; 23 | font-style: normal; 24 | font-weight: 100; 25 | font-size: 16px; 26 | } 27 | 28 | ow-root { 29 | display: flex; 30 | flex-direction: column; 31 | height: 100%; 32 | } 33 | 34 | ::-webkit-scrollbar { 35 | width: 0px; 36 | background: transparent; 37 | } 38 | 39 | * { 40 | box-sizing: border-box; 41 | } 42 | 43 | a { 44 | color: white; 45 | text-decoration: none; 46 | } 47 | 48 | none-selectable { 49 | user-select: none; 50 | -webkit-user-select: none; 51 | -moz-user-select: none; 52 | -ms-user-select: none; 53 | } 54 | -------------------------------------------------------------------------------- /web/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /web/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /web/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /web/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": false, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": false, 83 | "quotemark": [ 84 | true, 85 | "double" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "ow", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "ow", 127 | "kebab-case" 128 | ], 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "no-access-missing-member": true, 139 | "templates-use-public": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------