├── .babelrc ├── .eslintrc.yml ├── .gitignore ├── README.md ├── client ├── components │ ├── App.css │ ├── App.js │ ├── Topology.css │ └── Topology.js ├── index.html └── index.js ├── img ├── labels.png ├── response_time_95th.png ├── throuhput.png └── topology.png ├── package-lock.json ├── package.json ├── prometheus-data └── prometheus.yml ├── services ├── server1.js ├── server2.js └── server3.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", "es2015" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: airbnb 3 | env: 4 | node: true 5 | mocha: true 6 | es6: true 7 | parserOptions: 8 | sourceType: module 9 | rules: 10 | generator-star-spacing: 11 | - 2 12 | - before: true 13 | after: true 14 | no-shadow: 0 15 | require-yield: 0 16 | no-param-reassign: 0 17 | comma-dangle: 18 | - error 19 | - never 20 | no-underscore-dangle: 0 21 | import/no-extraneous-dependencies: 22 | - 2 23 | - devDependencies: true 24 | import/order: 0 25 | no-new: 0 26 | no-console: 0 27 | func-names: 0 28 | no-unused-expressions: 0 29 | prefer-arrow-callback: 1 30 | no-use-before-define: 31 | - 2 32 | - functions: false 33 | space-before-function-paren: 34 | - 2 35 | - always 36 | max-len: 37 | - 2 38 | - 120 39 | - 2 40 | semi: 41 | - 2 42 | - never 43 | strict: 0 44 | arrow-parens: 45 | - 2 46 | - always 47 | jsx-a11y/href-no-hash: 0 48 | react/jsx-filename-extension: 0 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opentracing-infrastructure-graph 2 | 3 | Visualizing infrastructure topology via OpenTracing instrumentation. 4 | 5 | This application uses the following libraries to extract the topology metrics to [Prometheus](https://prometheus.io/) via [OpenTracing](http://opentracing.io/): 6 | 7 | - [opentracing-metrics-tracer](https://github.com/RisingStack/opentracing-metrics-tracer) 8 | - [opentracing-auto](https://github.com/RisingStack/opentracing-auto) 9 | 10 | ## Requirements 11 | 12 | - Docker 13 | 14 | ### Run Prometheus 15 | 16 | Modify: `/prometheus-data/prometheus.yml`, replace `192.168.0.10` with your own host machine's IP. 17 | Host machine IP address: `ifconfig | grep 'inet 192'| awk '{ print $2}'` 18 | 19 | ```sh 20 | docker run -p 9090:9090 -v "$(pwd)/prometheus-data":/prometheus-data prom/prometheus -config.file=/prometheus-data/prometheus.yml 21 | ``` 22 | 23 | Open Prometheus: [http://http://localhost:9090](http://http://localhost:9090/graph) 24 | 25 | ## Getting started 26 | 27 | It will start three web servers and simulate a service call chain: 28 | `server1` calls `server2` and `server3` parallel. 29 | 30 | ``` 31 | npm start 32 | curl http://localhost:3001 33 | ``` 34 | 35 | ## Metrics between services 36 | 37 | `parent_service="unknown"` label means that the request initiator is not instrumented *(Prometheus scraper, curl, etc)*. 38 | 39 | ![parent_service labels](img/labels.png) 40 | 41 | ### Throughput 42 | 43 | Prometheus query: 44 | 45 | ``` 46 | sum(rate(operation_duration_seconds_count{name="http_server"}[1m])) by (service, parent_service) * 60 47 | ``` 48 | 49 | ![Throughput between services](img/throuhput.png) 50 | 51 | ### 95th response time 52 | 53 | Prometheus query: 54 | 55 | ``` 56 | histogram_quantile(0.95, sum(rate(operation_duration_seconds_bucket{name="http_server"}[1m])) by (le, service, parent_service)) * 1000 57 | ``` 58 | 59 | ![95th response time between services](img/response_time_95th.png) 60 | 61 | ## Infrastructure topology 62 | 63 | Data comes from Prometheus. 64 | Uses [vizceral](https://github.com/Netflix/vizceral). 65 | 66 | ``` 67 | npm run start-client 68 | open http://localhost:8080 69 | ``` 70 | 71 | ![Infrastructure topology](img/topology.png) 72 | 73 | ## Future 74 | 75 | - add databases 76 | - show latency 77 | -------------------------------------------------------------------------------- /client/components/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | min-height: 100%; 3 | height:100%; 4 | } 5 | body { 6 | position: relative; 7 | font-family: 'Source Sans Pro', sans-serif; 8 | font-weight: 400; 9 | font-style: normal; 10 | height: 100%; 11 | } 12 | 13 | body > .container { 14 | position: absolute; 15 | top: 51px; 16 | padding-left: 0; 17 | padding-right: 0; 18 | margin-left: 0; 19 | margin-right: 0; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | width: 100%; 24 | } 25 | -------------------------------------------------------------------------------- /client/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Topology from './Topology' 3 | 4 | import './App.css' 5 | 6 | function App () { 7 | return ( 8 | 9 | ) 10 | } 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /client/components/Topology.css: -------------------------------------------------------------------------------- 1 | .vizceral-container { 2 | height: 100%; 3 | position: relative; 4 | } 5 | -------------------------------------------------------------------------------- /client/components/Topology.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Vizceral from 'vizceral-react' 3 | import superagent from 'superagent' 4 | 5 | import 'vizceral-react/dist/vizceral.css' 6 | import './Topology.css' 7 | 8 | const PROMETHEUS_QUERY = 'sum(rate(operation_duration_seconds_count{name="http_server"}[1m]))' 9 | + ' by (service, parent_service) * 60' 10 | 11 | const ROOT_NODE = { 12 | name: 'INTERNET' 13 | } 14 | 15 | const UPDATE_INTERVAL = 10000 16 | 17 | class Topology extends Component { 18 | constructor () { 19 | super() 20 | 21 | this.updateInterval = setInterval(() => this.update(), UPDATE_INTERVAL) 22 | 23 | this.state = { 24 | traffic: { 25 | layout: 'ltrTree', 26 | maxVolume: 10000, 27 | updated: Date.now(), 28 | name: 'Infrastructure', 29 | renderer: 'region', 30 | nodes: [ 31 | ROOT_NODE 32 | ], 33 | connections: [] 34 | } 35 | } 36 | 37 | this.update() 38 | } 39 | 40 | componentWillUnmount () { 41 | clearInterval(this.updateInterval) 42 | } 43 | 44 | update () { 45 | const epoch = Math.round(Date.now() / 1000) 46 | const uri = 'http://localhost:9090/api/v1/query' 47 | + `?query=${PROMETHEUS_QUERY}&start=${epoch - 60}&end=${epoch}` 48 | 49 | superagent.get(uri) 50 | .then(({ body }) => { 51 | const { traffic } = this.state 52 | const nodes = new Set() 53 | 54 | traffic.updated = Date.now() 55 | 56 | traffic.nodes = [ROOT_NODE] 57 | traffic.connections = [] 58 | 59 | body.data.result.forEach((result) => { 60 | // Add node 61 | if (!nodes.has(result.metric.service)) { 62 | traffic.nodes.push({ 63 | name: result.metric.service 64 | }) 65 | 66 | nodes.add(result.metric.service) 67 | } 68 | 69 | // Add edge 70 | traffic.connections.push({ 71 | source: result.metric.parent_service === 'unknown' ? ROOT_NODE.name : result.metric.parent_service, 72 | target: result.metric.service, 73 | metrics: { 74 | normal: Math.round(Number(result.value[1]) || 0), 75 | danger: 0, 76 | warning: 0 77 | } 78 | }) 79 | }) 80 | 81 | this.setState({ traffic }) 82 | }) 83 | .catch((err) => { 84 | console.error(err) 85 | }) 86 | } 87 | 88 | render () { 89 | const { traffic } = this.state 90 | 91 | return ( 92 |
93 | 98 |
99 | ) 100 | } 101 | } 102 | 103 | export default Topology 104 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Topology 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | 5 | ReactDOM.render(, global.document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /img/labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RisingStack/opentracing-infrastructure-graph/99c2379b1da5f526c36a4a0034a4fa3b07fa9fa4/img/labels.png -------------------------------------------------------------------------------- /img/response_time_95th.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RisingStack/opentracing-infrastructure-graph/99c2379b1da5f526c36a4a0034a4fa3b07fa9fa4/img/response_time_95th.png -------------------------------------------------------------------------------- /img/throuhput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RisingStack/opentracing-infrastructure-graph/99c2379b1da5f526c36a4a0034a4fa3b07fa9fa4/img/throuhput.png -------------------------------------------------------------------------------- /img/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RisingStack/opentracing-infrastructure-graph/99c2379b1da5f526c36a4a0034a4fa3b07fa9fa4/img/topology.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@risingstack/opentracing-infrastructure-graph", 3 | "version": "0.0.0", 4 | "description": "Visualizing infrastructure topology via OpenTracing instrumentation", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "eslint test services", 8 | "start-client": "webpack-dev-server", 9 | "start": "npm-run-all --parallel start-server1 start-server2 start-server3", 10 | "start-server1": "node services/server1", 11 | "start-server2": "node services/server2", 12 | "start-server3": "node services/server3" 13 | }, 14 | "author": "RisingStack, Inc.", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/RisingStack/opentracing-infrastructure-graph.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/RisingStack/opentracing-infrastructure-graph/issues" 22 | }, 23 | "homepage": "https://github.com/RisingStack/opentracing-infrastructure-graph#readme", 24 | "keywords": [ 25 | "infrastructure", 26 | "visualization", 27 | "opentracing" 28 | ], 29 | "dependencies": { 30 | "@risingstack/opentracing-auto": "1.2.1", 31 | "@risingstack/opentracing-metrics-tracer": "2.0.1", 32 | "express": "4.15.4", 33 | "request": "2.81.0", 34 | "request-promise-native": "1.0.4" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.21.0", 38 | "babel-loader": "7.1.2", 39 | "babel-preset-es2015": "^6.18.0", 40 | "babel-preset-react": "^6.16.0", 41 | "css-loader": "0.28.7", 42 | "eslint": "4.4.1", 43 | "eslint-config-airbnb": "15.1.0", 44 | "eslint-config-airbnb-base": "11.3.1", 45 | "eslint-plugin-import": "2.7.0", 46 | "eslint-plugin-jsx-a11y": "6.0.2", 47 | "eslint-plugin-promise": "3.5.0", 48 | "eslint-plugin-react": "7.2.0", 49 | "html-webpack-plugin": "^2.26.0", 50 | "npm-run-all": "4.1.1", 51 | "react": "15.6.1", 52 | "react-dom": "15.6.1", 53 | "style-loader": "0.18.2", 54 | "superagent": "3.6.0", 55 | "vizceral-react": "4.5.3", 56 | "webpack": "3.5.5", 57 | "webpack-dev-server": "2.7.1" 58 | }, 59 | "engines": { 60 | "node": ">=8.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /prometheus-data/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: 'services' 3 | scrape_interval: 5s 4 | 5 | static_configs: 6 | - targets: ['192.168.0.10:3001'] 7 | labels: 8 | service: 'my-server-1' 9 | - targets: ['192.168.0.10:3002'] 10 | labels: 11 | service: 'my-server-2' 12 | - targets: ['192.168.0.10:3003'] 13 | labels: 14 | service: 'my-server-3' 15 | -------------------------------------------------------------------------------- /services/server1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MetricsTracer = require('@risingstack/opentracing-metrics-tracer') 4 | 5 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 6 | const metricsTracer = new MetricsTracer('my-server-1', [prometheusReporter]) 7 | 8 | // Auto instrumentation 9 | const Instrument = require('@risingstack/opentracing-auto') 10 | 11 | new Instrument({ 12 | tracers: [metricsTracer] 13 | }) 14 | 15 | // Web server 16 | const request = require('request-promise-native') 17 | const express = require('express') 18 | 19 | const app = express() 20 | const port = process.env.PORT || 3001 21 | 22 | app.get('/', async (req, res) => { 23 | const [server2Resp, server3Resp] = await Promise.all([ 24 | request({ 25 | uri: 'http://localhost:3002', 26 | json: true 27 | }), 28 | request({ 29 | uri: 'http://localhost:3003', 30 | json: true 31 | }) 32 | ]) 33 | 34 | res.json({ 35 | server2: server2Resp, 36 | server3: server3Resp, 37 | status: 'ok' 38 | }) 39 | }) 40 | 41 | app.get('/metrics', (req, res) => { 42 | res.set('Content-Type', MetricsTracer.PrometheusReporter.Prometheus.register.contentType) 43 | res.end(prometheusReporter.metrics()) 44 | }) 45 | 46 | app.listen(port, (err) => { 47 | console.log(err || `Server 1 is listening on ${port}`) 48 | }) 49 | -------------------------------------------------------------------------------- /services/server2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MetricsTracer = require('@risingstack/opentracing-metrics-tracer') 4 | 5 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 6 | const metricsTracer = new MetricsTracer('my-server-2', [prometheusReporter]) 7 | 8 | // Auto instrumentation 9 | const Instrument = require('@risingstack/opentracing-auto') 10 | 11 | new Instrument({ 12 | tracers: [metricsTracer] 13 | }) 14 | 15 | // Web server 16 | const express = require('express') 17 | 18 | const app = express() 19 | const port = process.env.PORT || 3002 20 | 21 | app.get('/', async (req, res) => { 22 | res.json({ 23 | status: 'ok' 24 | }) 25 | }) 26 | 27 | app.get('/metrics', (req, res) => { 28 | res.set('Content-Type', MetricsTracer.PrometheusReporter.Prometheus.register.contentType) 29 | res.end(prometheusReporter.metrics()) 30 | }) 31 | 32 | app.listen(port, (err) => { 33 | console.log(err || `Server 1 is listening on ${port}`) 34 | }) 35 | -------------------------------------------------------------------------------- /services/server3.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MetricsTracer = require('@risingstack/opentracing-metrics-tracer') 4 | 5 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 6 | const metricsTracer = new MetricsTracer('my-server-2', [prometheusReporter]) 7 | 8 | // Auto instrumentation 9 | const Instrument = require('@risingstack/opentracing-auto') 10 | 11 | new Instrument({ 12 | tracers: [metricsTracer] 13 | }) 14 | 15 | // Web server 16 | const express = require('express') 17 | 18 | const app = express() 19 | const port = process.env.PORT || 3003 20 | 21 | app.get('/', async (req, res) => { 22 | res.json({ 23 | status: 'ok' 24 | }) 25 | }) 26 | 27 | app.get('/metrics', (req, res) => { 28 | res.set('Content-Type', MetricsTracer.PrometheusReporter.Prometheus.register.contentType) 29 | res.end(prometheusReporter.metrics()) 30 | }) 31 | 32 | app.listen(port, (err) => { 33 | console.log(err || `Server 1 is listening on ${port}`) 34 | }) 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | 6 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 7 | template: './client/index.html', 8 | filename: 'index.html', 9 | inject: 'body' 10 | }) 11 | 12 | module.exports = { 13 | entry: './client/index.js', 14 | output: { 15 | path: path.join(__dirname, './dist'), 16 | filename: 'index_bundle.js' 17 | }, 18 | module: { 19 | loaders: [ 20 | { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ }, 21 | { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }, 22 | { test: /\.css?$/, use: ['style-loader', 'css-loader'] } 23 | ] 24 | }, 25 | plugins: [HtmlWebpackPluginConfig] 26 | } 27 | --------------------------------------------------------------------------------