├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------