├── src
├── db
│ └── .gitkeep
├── api
│ ├── .nvmrc
│ ├── .dockerignore
│ ├── Dockerfile
│ ├── .eslintrc.json
│ ├── package.json
│ ├── index-api.js
│ ├── metricArrays.js
│ └── controller.js
├── web
│ ├── .nvmrc
│ ├── .dockerignore
│ ├── postcss.config.js
│ ├── Dockerfile
│ ├── client
│ │ ├── index.html
│ │ ├── index.jsx
│ │ ├── style.scss
│ │ ├── app.css
│ │ ├── components
│ │ │ ├── LineChart.jsx
│ │ │ └── ContainerButton.jsx
│ │ └── containers
│ │ │ ├── App.jsx
│ │ │ ├── Sidebar.jsx
│ │ │ └── GraphDisplay.jsx
│ ├── tailwind.config.js
│ ├── .eslintrc.json
│ ├── webpack.config.js
│ └── package.json
└── sensors
│ └── sensor1
│ ├── .nvmrc
│ ├── .dockerignore
│ ├── Dockerfile
│ ├── .eslintrc.json
│ ├── package.json
│ ├── dbInsert.js
│ └── index.js
├── __tests
├── .nvmrc
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── e2e
│ │ └── home_page.cy.js
│ └── support
│ │ ├── e2e.js
│ │ └── commands.js
├── cypress.config.js
└── package.json
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── .env.example
├── LICENSE
├── install.yaml
├── docker-compose.yaml
└── README.md
/src/db/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/__tests/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.12.1
--------------------------------------------------------------------------------
/src/api/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.12.1
--------------------------------------------------------------------------------
/src/web/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.12.1
--------------------------------------------------------------------------------
/src/sensors/sensor1/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.12.1
--------------------------------------------------------------------------------
/src/api/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
--------------------------------------------------------------------------------
/src/sensors/sensor1/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
--------------------------------------------------------------------------------
/src/web/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | npm-debug.log
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
3 | package-lock.json
4 | .DS_Store
5 | dist/
--------------------------------------------------------------------------------
/src/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('tailwindcss'), require('autoprefixer')],
3 | };
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "ms-azuretools.vscode-docker"
5 | ],
6 | "unwantedRecommendations": []
7 | }
8 |
--------------------------------------------------------------------------------
/src/api/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | FROM node:18
4 |
5 | WORKDIR /code
6 |
7 | COPY package*.json ./
8 | RUN npm install
9 |
10 | COPY . .
11 |
12 | EXPOSE 8854
13 |
--------------------------------------------------------------------------------
/src/web/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | FROM node:18
4 |
5 | WORKDIR /code
6 |
7 | COPY package*.json ./
8 | RUN npm install
9 |
10 | COPY . .
11 |
12 | EXPOSE 8855
13 |
--------------------------------------------------------------------------------
/__tests/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/src/sensors/sensor1/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | FROM node:18
4 |
5 | WORKDIR /code
6 |
7 | COPY package*.json ./
8 | RUN npm install
9 |
10 | COPY . .
11 |
12 | EXPOSE 8855
13 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DB_INFLUXDB_INIT_MODE=
2 | DB_INFLUXDB_INIT_USERNAME=
3 | DB_INFLUXDB_INIT_PASSWORD=
4 | DB_INFLUXDB_INIT_ORG=
5 | DB_INFLUXDB_INIT_BUCKET=
6 | DB_INFLUXDB_INIT_RETENTION=
7 | DB_INFLUXDB_INIT_ADMIN_TOKEN=
8 | SENSORS_SENSOR1_LOOKUP_FREQUENCY=
9 |
--------------------------------------------------------------------------------
/__tests/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require("cypress");
2 |
3 | module.exports = defineConfig({
4 | e2e: {
5 | setupNodeEvents(on, config) {
6 | // implement node event listeners here
7 | },
8 | baseUrl: 'http://localhost:8855',
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint",
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true
6 | },
7 | "eslint.validate": [
8 | "javascript",
9 | "javascriptreact"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/web/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DockerWatch
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true
6 | },
7 | "extends": "airbnb-base",
8 | "overrides": [
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": "latest"
12 | },
13 | "rules": {
14 | "no-console": "off"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/sensors/sensor1/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true
6 | },
7 | "extends": "airbnb-base",
8 | "overrides": [],
9 | "parserOptions": {
10 | "ecmaVersion": "latest"
11 | },
12 | "rules": {
13 | "no-console": "off"
14 | }
15 | }
--------------------------------------------------------------------------------
/src/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./client/**/*.{js,ts,jsx,tsx}'],
3 | theme: {
4 | extend: {
5 | colors: {
6 | 'l-blue': '#95B9E5',
7 | 'm-blue': '#3b76b7',
8 | 'd-blue': '#324a78',
9 | test: '#468DDA',
10 | },
11 | },
12 | },
13 | plugins: [require('daisyui')],
14 | };
15 |
--------------------------------------------------------------------------------
/__tests/cypress/e2e/home_page.cy.js:
--------------------------------------------------------------------------------
1 | describe('Web Page', () => {
2 | it('successfully loads', () => {
3 | cy.visit('/')
4 | })
5 |
6 | it('displays containers', () => {
7 | cy.visit('/')
8 | cy.get('button').should('have.length.gt', 3)
9 | })
10 |
11 | it('displays graphs', () => {
12 | cy.visit('/')
13 | cy.contains('web').click()
14 | cy.get('canvas').should('have.length', 4)
15 | })
16 | })
--------------------------------------------------------------------------------
/src/web/client/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './containers/App';
5 |
6 | const root = ReactDOM.createRoot(document.getElementById('root'));
7 | root.render(
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | // document.getElementById('root')
16 | );
17 |
--------------------------------------------------------------------------------
/src/sensors/sensor1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sensor1",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon index.js",
8 | "start": "node index.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "eslint": "^8.29.0",
16 | "eslint-config-airbnb-base": "^15.0.0",
17 | "eslint-plugin-import": "^2.26.0",
18 | "nodemon": "^2.0.20"
19 | },
20 | "dependencies": {
21 | "@influxdata/influxdb-client": "^1.33.0",
22 | "dotenv": "^16.0.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index-api.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node ./index-api.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@influxdata/influxdb-client": "^1.33.0",
15 | "@influxdata/influxdb-client-apis": "^1.33.0",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.0.3",
18 | "express": "^4.18.2"
19 | },
20 | "devDependencies": {
21 | "eslint": "^8.29.0",
22 | "eslint-config-airbnb-base": "^15.0.0",
23 | "eslint-plugin-import": "^2.26.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/__tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dockerwatch",
3 | "description": "Collect and visualize Docker container metrics over time.",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "cypress": "^12.3.0"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "cypress:open": "cypress open"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/docker-watch-app.git"
16 | },
17 | "keywords": [],
18 | "author": "",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/oslabs-beta/docker-watch-app/issues"
22 | },
23 | "homepage": "https://github.com/oslabs-beta/docker-watch-app#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/__tests/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
--------------------------------------------------------------------------------
/src/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "plugin:react/recommended",
8 | "airbnb"
9 | ],
10 | "overrides": [],
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "react"
17 | ],
18 | "rules": {
19 | "jsx-quotes": [
20 | "error",
21 | "prefer-single"
22 | ],
23 | "no-console": "off",
24 | "comma-dangle": [
25 | "error",
26 | {
27 | "arrays": "never",
28 | "objects": "always-multiline",
29 | "imports": "never",
30 | "exports": "never",
31 | "functions": "never"
32 | }
33 | ],
34 | "react/prop-types": "off",
35 | "import/no-extraneous-dependencies": "off"
36 | }
37 | }
--------------------------------------------------------------------------------
/src/web/client/style.scss:
--------------------------------------------------------------------------------
1 | .grid-container {
2 | background: rgb(233,205,220);
3 | background: radial-gradient(circle, rgba(233,205,220,1) 0%, rgba(218,229,240,1) 100%);
4 | display: grid;
5 | grid-template-columns: 0.5fr 1.5fr;
6 | grid-template-rows: 0.4fr 1.6fr;
7 | gap: 0px 0px;
8 | grid-template-areas:
9 | "header header"
10 | "sidebar main";
11 | }
12 | .header {
13 | border: 1px solid;
14 | grid-area: header;
15 | text-align: center;
16 | }
17 | .sidebar {
18 | border: 1px solid;
19 | grid-area: sidebar;
20 | display: flex;
21 | flex-direction: column;
22 | justify-content: center;
23 | }
24 | .main {
25 | grid-area: main;
26 | border: 1px solid;
27 | }
28 | .container {
29 | border: 1px solid;
30 | height: 100px;
31 | width: 100%;
32 | display: flex;
33 | align-items: center;
34 | justify-content: center;
35 | }
--------------------------------------------------------------------------------
/__tests/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/web/client/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .App {
6 | display: grid;
7 | grid-template-columns: 0.3fr 1.7fr;
8 | grid-template-rows: 0.1fr 1.9fr;
9 | gap: 0px 0px;
10 | grid-template-areas:
11 | "Header Header"
12 | "Sidebar Main";
13 | text-align: center;
14 | min-height: 100vh;
15 | }
16 |
17 | .Header {
18 | grid-area: Header;
19 | display: flex;
20 | align-items: center;
21 | /* background-color: #2d5ca7; */
22 | background-color: #2b7bd4;
23 | /* justify-content: center; */
24 | }
25 |
26 | .Main {
27 | grid-area: Main;
28 | flex: flex;
29 | align-items: center;
30 | }
31 |
32 | .Sidebar {
33 | grid-area: Sidebar;
34 | display: flex;
35 | justify-content: center;
36 | }
37 |
38 | .sideScroll {
39 | height: 89vh;
40 | }
41 |
42 | .container {
43 | width: 100%;
44 | }
45 |
46 | .chartWrapper {
47 | position: relative;
48 | }
49 |
50 | .chartWrapper > canvas {
51 | position: absolute;
52 | left: 0;
53 | top: 0;
54 | pointer-events: none;
55 | }
56 |
57 | .chartAreaWrapper {
58 | width: 600px;
59 | overflow-x: scroll;
60 | }
--------------------------------------------------------------------------------
/src/web/client/components/LineChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import { Chart } from "react-chartjs-2";
3 | // import { Chart as ChartJS } from "chart.js/auto";
4 | import { Chart } from 'chart.js/auto';
5 | import { Line } from 'react-chartjs-2';
6 | import zoomPlugin from 'chartjs-plugin-zoom';
7 |
8 | Chart.register(zoomPlugin);
9 |
10 | function LineChart({ graphData, title }) {
11 | const options = {
12 | responsive: true,
13 | maintainAspectRatio: false,
14 | plugins: {
15 | legend: {
16 | position: 'top',
17 | },
18 | title: {
19 | display: true,
20 | font: {
21 | size: 20,
22 | },
23 | text: title,
24 | },
25 | zoom: {
26 | zoom: {
27 | wheel: {
28 | enabled: true,
29 | speed: 0.02,
30 | },
31 | mode: 'x',
32 | },
33 | pan: {
34 | enabled: true,
35 | mode: 'x',
36 | },
37 | },
38 | },
39 | elements: {
40 | point: {
41 | radius: 0,
42 | },
43 | },
44 | };
45 | return (
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default LineChart;
53 |
--------------------------------------------------------------------------------
/src/web/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const Dotenv = require('dotenv-webpack');
4 |
5 | module.exports = {
6 | entry: './client/index.jsx',
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | filename: 'bundle.js',
10 | publicPath: '/',
11 | },
12 | mode: process.env.NODE_ENV,
13 | module: {
14 | rules: [
15 | {
16 | test: /\.jsx?/,
17 | exclude: /node_modules/,
18 | loader: 'babel-loader',
19 | options: {
20 | presets: ['@babel/env', '@babel/react'],
21 | },
22 | },
23 | {
24 | test: /\.css$/,
25 | exclude: /node_modules/,
26 | use: [
27 | 'style-loader',
28 | {
29 | loader: 'css-loader',
30 | options: {
31 | importLoaders: 1,
32 | },
33 | },
34 | 'postcss-loader'
35 | ],
36 | }
37 | ],
38 | },
39 | resolve: {
40 | extensions: ['.js', '.jsx'],
41 | },
42 | devServer: {
43 | static: {
44 | directory: path.join(__dirname, 'dist'),
45 | publicPath: '/dist',
46 | },
47 | },
48 | plugins: [
49 | new HtmlWebpackPlugin({
50 | template: './client/index.html',
51 | filename: 'index.html',
52 | }),
53 | new Dotenv({
54 | path: '../../.env',
55 | systemvars: true,
56 | })
57 | ],
58 | };
59 |
--------------------------------------------------------------------------------
/install.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | sensor1:
5 | image: dockerwatch/sensor1:latest
6 | volumes:
7 | - /var/run/docker.sock:/var/run/docker.sock:ro
8 | environment:
9 | - DB_INFLUXDB_INIT_ADMIN_TOKEN=dockerwatch
10 | - DB_INFLUXDB_INIT_ORG=dockerwatch
11 | - DB_INFLUXDB_INIT_BUCKET=dockerwatch
12 | - DB_URL=http://db:8086
13 | - SENSORS_SENSOR1_LOOKUP_FREQUENCY=5000
14 | depends_on:
15 | - db
16 | command: ["npm", "run", "start"]
17 | db:
18 | image: influxdb:2.4
19 | environment:
20 | - DOCKER_INFLUXDB_INIT_MODE=setup
21 | - DOCKER_INFLUXDB_INIT_USERNAME=dockerwatch
22 | - DOCKER_INFLUXDB_INIT_PASSWORD=dockerwatch
23 | - DOCKER_INFLUXDB_INIT_ORG=dockerwatch
24 | - DOCKER_INFLUXDB_INIT_BUCKET=dockerwatch
25 | - DOCKER_INFLUXDB_INIT_RETENTION=1w
26 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=dockerwatch
27 | api:
28 | image: dockerwatch/api:latest
29 | ports:
30 | - '8854:8854'
31 | environment:
32 | - DB_INFLUXDB_INIT_ORG=dockerwatch
33 | - DB_INFLUXDB_INIT_BUCKET=dockerwatch
34 | - DB_INFLUXDB_INIT_ADMIN_TOKEN=dockerwatch
35 | - DB_URL=http://db:8086
36 | depends_on:
37 | - db
38 | - sensor1
39 | command: ["npm", "run", "start"]
40 | web:
41 | image: dockerwatch/frontend-web:latest
42 | ports:
43 | - '8855:8855'
44 | environment:
45 | - API_URL=http://127.0.0.1:8854
46 | depends_on:
47 | - api
48 | command: ["npm", "run", "dev:docker"]
49 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | sensor1:
5 | build: ./src/sensors/sensor1
6 | volumes:
7 | - /var/run/docker.sock:/var/run/docker.sock:ro
8 | environment:
9 | - DB_INFLUXDB_INIT_ADMIN_TOKEN=${DB_INFLUXDB_INIT_ADMIN_TOKEN}
10 | - DB_INFLUXDB_INIT_ORG=${DB_INFLUXDB_INIT_ORG}
11 | - DB_INFLUXDB_INIT_BUCKET=${DB_INFLUXDB_INIT_BUCKET}
12 | - DB_URL=http://db:8086
13 | - SENSORS_SENSOR1_LOOKUP_FREQUENCY=${SENSORS_SENSOR1_LOOKUP_FREQUENCY}
14 | depends_on:
15 | - db
16 | command: ["npm", "run", "start"]
17 | db:
18 | image: influxdb:2.4
19 | ports:
20 | - '8086:8086' # TODO might not expose port in non-dev builds
21 | environment:
22 | - DOCKER_INFLUXDB_INIT_MODE=${DB_INFLUXDB_INIT_MODE}
23 | - DOCKER_INFLUXDB_INIT_USERNAME=${DB_INFLUXDB_INIT_USERNAME}
24 | - DOCKER_INFLUXDB_INIT_PASSWORD=${DB_INFLUXDB_INIT_PASSWORD}
25 | - DOCKER_INFLUXDB_INIT_ORG=${DB_INFLUXDB_INIT_ORG}
26 | - DOCKER_INFLUXDB_INIT_BUCKET=${DB_INFLUXDB_INIT_BUCKET}
27 | - DOCKER_INFLUXDB_INIT_RETENTION=${DB_INFLUXDB_INIT_RETENTION}
28 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${DB_INFLUXDB_INIT_ADMIN_TOKEN}
29 | api:
30 | build: ./src/api
31 | ports:
32 | - '8854:8854' # TODO might not expose port in non-dev builds
33 | environment:
34 | - DB_INFLUXDB_INIT_ORG=${DB_INFLUXDB_INIT_ORG}
35 | - DB_INFLUXDB_INIT_BUCKET=${DB_INFLUXDB_INIT_BUCKET}
36 | - DB_INFLUXDB_INIT_ADMIN_TOKEN=${DB_INFLUXDB_INIT_ADMIN_TOKEN}
37 | - DB_URL=http://db:8086
38 | depends_on:
39 | - db
40 | - sensor1
41 | command: ["npm", "run", "start"]
42 | web:
43 | build: ./src/web
44 | ports:
45 | - '8855:8855'
46 | environment:
47 | # - API_URL=http://api:8854 # TODO explore mapping through Docker network
48 | - API_URL=http://127.0.0.1:8854
49 | depends_on:
50 | - api
51 | command: ["npm", "run", "dev:docker"]
52 |
--------------------------------------------------------------------------------
/src/api/index-api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const express = require('express');
3 | const cors = require('cors');
4 |
5 | const controller = require('./controller');
6 |
7 | // console.log(controller.getStatsFromDB);
8 |
9 | const PORT = 8854;
10 | const HOST = '0.0.0.0';
11 |
12 | const app = express();
13 | app.use(cors());
14 |
15 | // send an object with all stats of a specific container id for the default time range
16 | app.get('/api/v1/containers/:id/stats/', controller.getContainerStats, (req, res) => {
17 | res.status(200).json(res.locals.stats);
18 | });
19 | // send an object with all stats of a specific container id for a specific timeframe
20 | app.get('/api/v1/containers/:id/stats/:range/', controller.getContainerStats, (req, res) => {
21 | res.status(200).json(res.locals.stats);
22 | });
23 | // send an object with specific stats of a specific container id
24 | app.get('/api/v1/containers/:id/stats/all/:metric', controller.getContainerStats, (req, res) => {
25 | res.status(200).json(res.locals.stats);
26 | });
27 | // send an object with specific stats of a specific container id for a specified timeframe
28 | app.get('/api/v1/containers/:id/stats/:range/:metric', controller.getContainerStats, (req, res) => {
29 | res.status(200).json(res.locals.stats);
30 | });
31 | // send an object with all running container ids and names
32 | app.get('/api/v1/containers', controller.getContainers, (req, res) => {
33 | res.status(200).json(res.locals.containers);
34 | });
35 |
36 | app.get('/', (req, res) => {
37 | res.sendStatus(200);
38 | });
39 |
40 | app.use('*', (req, res, next) => {
41 | const errorObj = {
42 | log: 'Page not found',
43 | status: 404,
44 | message: { err: 'Error 404: Page not Found' },
45 | };
46 | next(errorObj);
47 | });
48 |
49 | app.use((err, req, res) => {
50 | const defaultErr = {
51 | log: `Express error handler caught unknown middleware error: ${err}`,
52 | status: 500,
53 | message: { err: 'An error occurred' },
54 | };
55 | console.log(defaultErr.log);
56 | res.status(defaultErr.status).send(defaultErr.message);
57 | });
58 |
59 | app.listen(PORT, HOST, () => {
60 | console.log(`Running on http://${HOST}:${PORT}`);
61 | });
62 |
--------------------------------------------------------------------------------
/src/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dockervision",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "./client/index.js",
6 | "scripts": {
7 | "build:tailwindcss": "npx tailwindcss -i ./client/app.css -o ./dist/output.css",
8 | "dev": "concurrently \"npm run watch-css\" \"NODE_ENV=development webpack-dev-server --open\"",
9 | "dev:docker": "concurrently \"npm run build:tailwindcss\" \"NODE_ENV=development webpack-dev-server --port 8855 \"",
10 | "start": "NODE_ENV=production webpack-dev-server",
11 | "watch-css": "npx tailwindcss -i ./client/app.css -o ./dist/output.css --watch"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@headlessui/react": "^1.7.5",
18 | "@heroicons/react": "^2.0.13",
19 | "@tailwindcss/aspect-ratio": "^0.4.2",
20 | "@tailwindcss/forms": "^0.5.3",
21 | "@tailwindcss/typography": "^0.5.8",
22 | "axios": "^1.2.1",
23 | "browser-router": "^0.2.0",
24 | "chartjs-plugin-zoom": "^2.0.0",
25 | "concurrently": "^7.6.0",
26 | "cors": "^2.8.5",
27 | "daisyui": "^2.45.0",
28 | "dotenv-webpack": "^8.0.1",
29 | "express": "^4.18.2",
30 | "influxdb": "^0.0.1",
31 | "postcss": "^8.4.20",
32 | "postcss-loader": "^7.0.2",
33 | "react": "^18.2.0",
34 | "react-chartjs-2": "^5.0.1",
35 | "react-dom": "^18.2.0",
36 | "react-router-dom": "^6.4.5",
37 | "uuid": "^9.0.0",
38 | "webpack-cli": "^5.0.1",
39 | "webpack-dev-server": "^4.11.1"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.20.5",
43 | "@babel/preset-env": "^7.20.2",
44 | "@babel/preset-react": "^7.18.6",
45 | "babel-loader": "^9.1.0",
46 | "css-loader": "^6.7.3",
47 | "eslint": "^8.31.0",
48 | "eslint-config-airbnb": "^19.0.4",
49 | "eslint-plugin-import": "^2.26.0",
50 | "eslint-plugin-jsx-a11y": "^6.6.1",
51 | "eslint-plugin-react": "^7.31.11",
52 | "eslint-plugin-react-hooks": "^4.6.0",
53 | "html-webpack-plugin": "^5.5.0",
54 | "nodemon": "^2.0.20",
55 | "sass": "^1.56.2",
56 | "sass-loader": "^13.2.0",
57 | "style-loader": "^3.3.1",
58 | "tailwindcss": "^3.2.4",
59 | "webpack": "^5.75.0"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/api/metricArrays.js:
--------------------------------------------------------------------------------
1 | // takes in data and formats as object of arrays primed for chartjs
2 | const getMetricArrays = (data) => {
3 | // represent arrays as object of arrays
4 | const metricArrays = {
5 | dates: [],
6 | times: [],
7 | cpu_percentage: [],
8 | Memory_memory_usage: [],
9 | Network_rx_bytes: [],
10 | // Network_rx_dropped: [],
11 | // Network_rx_errors: [],
12 | // Network_rx_packets: [],
13 | Network_tx_bytes: [],
14 | // Network_tx_dropped: [],
15 | // Network_tx_errors: [],
16 | // Network_tx_packets: [],
17 | Disk_read_value: [],
18 | Disk_write_value: [],
19 | };
20 |
21 | // save previous timestamp
22 | let prevTime = 0;
23 | // iterate over every entry of object and store in empty arrays
24 | // eslint-disable-next-line no-restricted-syntax
25 | for (const [time, values] of Object.entries(data)) {
26 | // format time in human readable format
27 | const timestamp = new Date(time).toLocaleString('en-US', { timeZone: 'America/New_York' });
28 | if (!prevTime) prevTime = time;
29 | const diff = Date.parse(time) - Date.parse(prevTime);
30 | if (diff > 8000) {
31 | const dummyTimeStamp = new Date(time - 5000).toLocaleString('en-US', { timeZone: 'America/New_York' });
32 | metricArrays.dates.push(dummyTimeStamp.slice(0, 10));
33 | metricArrays.cpu_percentage.push(NaN);
34 | metricArrays.Memory_memory_usage.push(NaN);
35 | metricArrays.Network_rx_bytes.push(NaN);
36 | metricArrays.Network_tx_bytes.push(NaN);
37 | metricArrays.Disk_read_value.push(NaN);
38 | metricArrays.Disk_write_value.push(NaN);
39 | } else {
40 | metricArrays.dates.push(timestamp.slice(0, 10));
41 | // time only
42 | metricArrays.times.push(timestamp.slice(10));
43 | metricArrays.cpu_percentage.push(values.cpu_percentage);
44 | metricArrays.Memory_memory_usage.push(values.Memory_memory_usage);
45 | metricArrays.Network_rx_bytes.push(values.Network_rx_bytes);
46 | // metricArrays.Network_rx_dropped.push(values.Network_rx_dropped);
47 | // metricArrays.Network_rx_errors.push(values.Network_rx_errors);
48 | // metricArrays.Network_rx_packets.push(values.Network_rx_packets);
49 | metricArrays.Network_tx_bytes.push(values.Network_tx_bytes);
50 | // metricArrays.Network_tx_dropped.push(values.Network_tx_dropped);
51 | // metricArrays.Network_tx_errors.push(values.Network_tx_errors);
52 | // metricArrays.Network_tx_packets.push(values.Network_tx_packets);
53 | metricArrays.Disk_read_value.push(values.Disk_read_value);
54 | metricArrays.Disk_write_value.push(values.Disk_write_value);
55 | }
56 | prevTime = time;
57 | // day date only
58 | }
59 | return metricArrays;
60 | };
61 |
62 | module.exports = getMetricArrays;
63 |
--------------------------------------------------------------------------------
/src/web/client/components/ContainerButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 |
3 | export default function Container({
4 | id, text, setContainerData, intervalRef, idRef, containerRef, timeFrame,
5 | }) {
6 | const prevData = useRef({});
7 |
8 | // fetch full container data from server
9 | const getInitialData = (containerId) => {
10 | const apiURL = process.env.API_URL || 'http://127.0.0.1:8854';
11 |
12 | fetch(`${apiURL}/api/v1/containers/${containerId}/stats/${timeFrame}`)
13 | .then((response) => response.json())
14 | .then((data) => {
15 | prevData.current = data;
16 | setContainerData(data);
17 | })
18 | .catch((err) => console.log(err));
19 | };
20 | // fetch partial container data from server
21 | const getUpdatedData = (containerId) => {
22 | const apiURL = process.env.API_URL || 'http://127.0.0.1:8854';
23 |
24 | fetch(`${apiURL}/api/v1/containers/${containerId}/stats/8s`)
25 | .then((response) => response.json())
26 | .then((data) => {
27 | const newContainerData = {};
28 | // eslint-disable-next-line no-restricted-syntax
29 | for (const key of Object.keys(data)) {
30 | const newData = [...prevData.current[key], ...data[key]];
31 | newContainerData[key] = newData;
32 | }
33 | prevData.current = newContainerData;
34 | setContainerData(newContainerData);
35 | })
36 | .catch((err) => {
37 | console.log(err);
38 | });
39 | };
40 | /* a function that clears the current running setInterval that is stored in
41 | useRef (see app) and the runs a new set inerval for the newly clicked container */
42 | const containerOnClick = (containerId, didTimeChange = false) => {
43 | if (containerId === idRef.current && !didTimeChange) return;
44 | idRef.current = containerId;
45 | containerRef.current = containerId;
46 | // clears the value at current in useRef
47 | clearInterval(intervalRef.current);
48 | // initial call to containerOnClick to get initial metric data
49 | getInitialData(containerId);
50 | /* subsequent calls to containerOnClick every 5 seconds.
51 | This is stored inside of useRef to be cleared when the next container is clicked. */
52 | intervalRef.current = setInterval(() => getUpdatedData(containerId), 5000);
53 | };
54 |
55 | useEffect(() => {
56 | if (idRef.current !== 0) containerOnClick(idRef.current, true);
57 | }, [timeFrame]);
58 |
59 | return (
60 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/web/client/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import Sidebar from './Sidebar';
3 | import GraphContainer from './GraphDisplay';
4 | import '../app.css';
5 |
6 | function App() {
7 | // state that contains metric data on the currently clicked container
8 | const [containerData, setContainerData] = useState({});
9 |
10 | return (
11 |
12 |
13 |
14 | DockerWatch
15 | {' '}
16 |
27 |
28 |
29 |
30 |
34 |
38 |
39 | );
40 | }
41 |
42 | export default App;
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # Table of Contents
12 |
13 | - [About](#about)
14 | - [Installation](#installation)
15 | - [How to Use](#how-to-use)
16 | - [How it Works](#how-it-works)
17 | - [Authors](#authors)
18 | - [License](#license)
19 |
20 |
21 | ## About
22 |
23 | DockerWatch is a containerized application that collects and visualizes Docker container metrics over time.
24 |
25 | ## Installation
26 |
27 | ### Prerequisites
28 |
29 | - Download [Docker Desktop](https://www.docker.com/products/docker-desktop/).
30 |
31 | ### Setup
32 |
33 | Run the following code in your terminal:
34 |
35 | ```
36 | curl https://raw.githubusercontent.com/oslabs-beta/docker-watch-app/main/install.yaml | docker-compose -p dockerwatch -f - up -d
37 | ```
38 |
39 | ## How to Use
40 |
41 | 1. After installing, open your web brower and visit [http://localhost:8855](http://http://localhost:8855)
42 |
43 | 2. To view a container, click one on the left-hand side to view monitored metrics, including CPU, Memory, Network, and Disk.
44 |
45 |
46 | 3. View smaller or larger timeframes of data by clicking the Change Timeframe button and selecting a range of time to view. One hour will view all data from the last hour, one day will view all data from the last day, etc.
47 |
48 |
49 | ## How It Works
50 |
51 | The DockerWatch container holds four smaller containers: a web container, an api container, an InfluxDB database container, and a sensor container. The sensor collects data from the Docker daemon and stores it in the database. The API container queries for data from the database upon request from the web container. The CPU, Memory, Network, and Disk metrics for all containers in Docker Desktop, including DockerWatch, are monitored for as long as the DockerWatch container is running.
52 |
53 | ## Authors
54 |
55 | - Brynn Sakell [@BrynnSakell](https://github.com/BrynnSakell) | [LinkedIn](https://linkedin.com/in/brynnsakell)
56 | - Dan Pietsch [@dpietsch14](https://github.com/dpietsch14) | [LinkedIn](https://linkedin.com/in/danielpietsch14/)
57 | - Nadia Abowitz [@abowitzn](https://github.com/abowitzn) | [LinkedIn](https://linkedin.com/in/nadia-abowitz/)
58 | - Rob Mosher [@rob-mosher](https://github.com/rob-mosher) | [LinkedIn](https://linkedin.com/in/rob-mosher-it/)
59 | - Stephen Rivas [@stephenpharmd](https://github.com/stephenpharmd) | [LinkedIn](https://linkedin.com/in/stephenpharmd/)
60 |
61 | ## License
62 |
63 | This project is licensed under the [MIT License](LICENSE)
64 |
65 | ## Contributing
66 |
67 | DockerWatch launched on January 12, 2023 and is currently in active beta development through the OSlabs Beta community initiative. The application is licensed under the terms of the MIT license, making it a fully open source product. Developers are welcome to contribute to the codebase and expand on its features.
68 |
--------------------------------------------------------------------------------
/src/sensors/sensor1/dbInsert.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ path: '../../../.env' });
2 |
3 | /** @module write
4 | * Writes a data point to InfluxDB using the Javascript client library with Node.js. */
5 | const { InfluxDB } = require('@influxdata/influxdb-client');
6 | const { Point } = require('@influxdata/influxdb-client');
7 |
8 | /* Environment variables * */
9 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN;
10 | const org = process.env.DB_INFLUXDB_INIT_ORG;
11 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET;
12 |
13 | const dbFunc = (containerStats) => {
14 | const dbURL = process.env.DB_URL || 'http://127.0.0.1:8086';
15 |
16 | // Create a write client from the getWriteApi method. Provide your `org` and `bucket`
17 | const client = new InfluxDB({ url: dbURL, token });
18 |
19 | const writeApi = client.getWriteApi(org, bucket, 's');
20 |
21 | // Create a point and write it to the buffer
22 | writeApi.useDefaultTags({
23 | container_id: 'container_id',
24 | container_name: 'container_name',
25 | });
26 |
27 | /* Use the Point() constructor to create a point,
28 | In InfluxDB, a point represents a single data record */
29 |
30 | const pointCPU = new Point('CPU')
31 | .tag('container_id', containerStats.id)
32 | .tag('container_name', containerStats.name)
33 | .floatField('cpu_usage', containerStats.cpu_usage)
34 | .floatField('precpu_usage', containerStats.precpu_usage)
35 | .floatField('system_cpu_usage', containerStats.system_cpu_usage)
36 | .floatField('system_precpu_usage', containerStats.system_precpu_usage)
37 | .floatField('online_cpus', containerStats.online_cpus);
38 |
39 | const pointMemory = new Point('Memory')
40 | .tag('container_id', containerStats.id)
41 | .tag('container_name', containerStats.name)
42 | .floatField('memory_limit', containerStats.memory_limit)
43 | .floatField('memory_usage', containerStats.memory_usage);
44 |
45 | const pointDisk = new Point('Disk')
46 | .tag('container_id', containerStats.id)
47 | .tag('container_name', containerStats.name)
48 | .floatField('read_value', containerStats.disk_read)
49 | .floatField('write_value', containerStats.disk_write);
50 |
51 | const pointNetwork = new Point('Network')
52 | .tag('container_id', containerStats.id)
53 | .tag('container_name', containerStats.name)
54 | .floatField('rx_bytes', containerStats.rx_bytes)
55 | .floatField('rx_dropped', containerStats.rx_dropped)
56 | .floatField('rx_errors', containerStats.rx_errors)
57 | .floatField('rx_packets', containerStats.rx_packets)
58 | .floatField('tx_bytes', containerStats.tx_bytes)
59 | .floatField('tx_dropped', containerStats.tx_dropped)
60 | .floatField('tx_errors', containerStats.tx_errors)
61 | .floatField('tx_packets', containerStats.tx_packets);
62 |
63 | // Use the writePoint() method to write the point to your InfluxDB bucket
64 | writeApi.writePoint(pointCPU);
65 | writeApi.writePoint(pointMemory);
66 | writeApi.writePoint(pointDisk);
67 | writeApi.writePoint(pointNetwork);
68 |
69 | // Flush pending writes and close writeApi
70 | writeApi
71 | .close()
72 | // .then(() => {
73 | // console.log('FINISHED');
74 | // })
75 | .catch((e) => {
76 | console.error(e);
77 | console.log('Finished ERROR');
78 | });
79 | };
80 |
81 | module.exports = dbFunc;
82 |
--------------------------------------------------------------------------------
/src/web/client/containers/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import Container from '../components/ContainerButton';
3 |
4 | export default function Sidebar({ containerData, setContainerData }) {
5 | const [containerList, updateContainerList] = useState([]);
6 | // const timeFrame = useRef('1h');
7 | const [timeFrame, setTimeFrame] = useState('1h');
8 | const [timeDisplay, setTimeDisplay] = useState('Past 1 hour');
9 | /* contains the current running setInterval calling the
10 | function that requests the api for metric data for the last clicked container. */
11 | const intervalRef = useRef(0);
12 | const idRef = useRef(0);
13 |
14 | // a hook to highlight the selected container
15 | const containerRef = useRef('');
16 | // makes request to api for list of current running containers
17 | const getContainers = () => {
18 | const apiURL = process.env.API_URL || 'http://127.0.0.1:8081';
19 |
20 | fetch(`${apiURL}/api/v1/containers`)
21 | .then((response) => response.json())
22 | .then((data) => {
23 | updateContainerList(data);
24 | })
25 | .catch((err) => console.log(err));
26 | };
27 |
28 | // calls getContainers on mount
29 | useEffect(() => {
30 | getContainers();
31 | }, []);
32 |
33 | const timeRangeClicked = (str) => {
34 | setTimeFrame(str);
35 |
36 | const times = {
37 | '1m': 'Past 1 minute',
38 | '5m': 'Past 5 minutes',
39 | '15m': 'Past 15 minutes',
40 | '1h': 'Past 1 hour',
41 | '3h': 'Past 3 hours',
42 | '6h': 'Past 6 hours',
43 | '12h': 'Past 12 hours',
44 | '1d': 'Past 1 day',
45 | '7d': 'Past 7 days',
46 | };
47 |
48 | setTimeDisplay(times[str]);
49 | };
50 |
51 | // iterates through contaierList and returns an array of container buttons to render to sidebar
52 | const containerButtons = containerList.map((container) => (
53 |
64 | ));
65 | return (
66 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/sensors/sensor1/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ path: '../../../.env' });
2 |
3 | const http = require('node:http');
4 | const dbFunc = require('./dbInsert');
5 |
6 | const getContainerIDsAndWriteToDB = () => {
7 | const containerIds = [];
8 |
9 | const clientOptions = {
10 | socketPath: '/var/run/docker.sock',
11 | path: '/v1.41/containers/json',
12 | method: 'GET',
13 | };
14 |
15 | const client = http.request(clientOptions, (res) => {
16 | let body = [];
17 |
18 | res.on('data', (chunk) => {
19 | body.push(chunk);
20 | });
21 |
22 | res.on('end', () => {
23 | body = JSON.parse(Buffer.concat(body));
24 |
25 | // eslint-disable-next-line no-restricted-syntax
26 | for (const container of body) {
27 | containerIds.push(container.Id);
28 | // console.log(container.Id);
29 | }
30 |
31 | // iterate through container ids
32 | containerIds.forEach((id) => {
33 | // set path to access the stats at the current id, specify that stats only
34 | // need to be collected once
35 | const clientStatsOptions = {
36 | socketPath: '/var/run/docker.sock',
37 | path: `/v1.41/containers/${id}/stats?stream=false`,
38 | method: 'GET',
39 | };
40 | // create a new client to get stats
41 | const clientStats = http.request(clientStatsOptions, (resStats) => {
42 | let statsBody = [];
43 | // collect the data
44 | resStats.on('data', (chunk) => {
45 | statsBody.push(chunk);
46 | });
47 | // after collection, parse the buffer into a js object
48 | resStats.on('end', () => {
49 | statsBody = JSON.parse(Buffer.concat(statsBody));
50 | if (!statsBody) return;
51 |
52 | try {
53 | dbFunc({
54 | // CPU metrics:
55 | // total usage and previous usage (process time)
56 | cpu_usage: statsBody.cpu_stats.cpu_usage.total_usage,
57 | precpu_usage: statsBody.precpu_stats.cpu_usage.total_usage,
58 | // system total usage
59 | system_cpu_usage: statsBody.cpu_stats.system_cpu_usage,
60 | system_precpu_usage: statsBody.precpu_stats.system_cpu_usage,
61 | // Online CPUS
62 | online_cpus: statsBody.cpu_stats.online_cpus,
63 |
64 | // Disk utilization (Read and Write)
65 | disk_read: statsBody.blkio_stats.io_service_bytes_recursive[0].value,
66 | disk_write: statsBody.blkio_stats.io_service_bytes_recursive[1].value,
67 |
68 | // Container ID
69 | id: statsBody.id,
70 |
71 | // Memory stats:
72 | // memory limit
73 | memory_limit: statsBody.memory_stats.limit,
74 | // memory total usage
75 | memory_usage: statsBody.memory_stats.usage,
76 |
77 | // Container Name
78 | name: statsBody.name,
79 |
80 | // Network I/O:
81 | // displays the amount of received data
82 | rx_bytes: statsBody.networks.eth0.rx_bytes,
83 | // number of dropped packets for received data
84 | rx_dropped: statsBody.networks.eth0.rx_dropped,
85 | // displays the number of RX errors
86 | rx_errors: statsBody.networks.eth0.rx_errors,
87 | // displays the number of received packets
88 | rx_packets: statsBody.networks.eth0.rx_packets,
89 | // displays the amount of transmitted data
90 | tx_bytes: statsBody.networks.eth0.tx_bytes,
91 | // number of dropped packets for transmitted data
92 | tx_dropped: statsBody.networks.eth0.tx_dropped,
93 | // displays the number of TX errors
94 | tx_errors: statsBody.networks.eth0.tx_errors,
95 | // displays the number of transmitted packets
96 | tx_packets: statsBody.networks.eth0.tx_packets,
97 | });
98 | } catch (error) {
99 | console.log(error);
100 | }
101 | });
102 | });
103 |
104 | clientStats.on('error', (err) => {
105 | console.log(err);
106 | });
107 |
108 | clientStats.end();
109 | });
110 | });
111 | });
112 |
113 | client.on('error', (err) => {
114 | console.log(err);
115 | });
116 |
117 | client.end();
118 | };
119 |
120 | const getLookupFrequency = () => {
121 | const defaultMinimumLookupInMS = 5000;
122 | const envLookupFrequency = Number(process.env.SENSORS_SENSOR1_LOOKUP_FREQUENCY);
123 |
124 | if (Number.isNaN(envLookupFrequency) || envLookupFrequency < defaultMinimumLookupInMS) {
125 | return defaultMinimumLookupInMS;
126 | }
127 | return envLookupFrequency;
128 | };
129 |
130 | setInterval(getContainerIDsAndWriteToDB, getLookupFrequency());
131 |
--------------------------------------------------------------------------------
/src/web/client/containers/GraphDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | // import { v4 as uuidv4 } from 'uuid';
3 | import LineChart from '../components/LineChart';
4 | // format graph data for chartjs from container data
5 | const formatGraphData = (data) => {
6 | // iterates over metric arrays and returns smaller data subset based on adjustment
7 | // const adjustTimeFrame = (metrics, adjustment) => {
8 | // const metricsCopy = { ...metrics };
9 | // // eslint-disable-next-line no-restricted-syntax, guard-for-in
10 | // for (const key in metricsCopy) {
11 | // // const newArr = [];
12 | // // for (let j = metricsCopy[key].length - 1; j >= 0; j -= adjustment) {
13 | // // newArr.push(metricsCopy[key][j]);
14 | // // }
15 | // // metricsCopy[key] = newArr;
16 | // console.log(metricsCopy);
17 | // const newArr = [];
18 | // for (let j = 0; j > metricsCopy[key].length; j += adjustment) {
19 | // newArr.push(metricsCopy[key][j]);
20 | // }
21 | // metricsCopy[key] = newArr;
22 | // }
23 | // return metricsCopy;
24 | // };
25 | // let adjustmentFactor = 1
26 | // if (data.times.length / adjustmentFactor > 20) adjustmentFactor += 1
27 | // const timeFrames = {
28 | // '5 seconds': 1,
29 | // '30 seconds': 6,
30 | // '1 minute': 12,
31 | // '5 minutes': 60,
32 | // '30 minutes': 360,
33 | // '1 hour': 720,
34 | // };
35 | // console.log('timeframe inside graph display', timeFrame.current);
36 | // const adjustmentFactor = timeFrames[timeFrame.current];
37 | // const metrics = adjustTimeFrame(data, 1);
38 | // return metrics;
39 | const {
40 | dates,
41 | times,
42 | cpu_percentage,
43 | Memory_memory_usage,
44 | Network_rx_bytes,
45 | // Network_rx_dropped,
46 | // Network_rx_errors,
47 | // Network_rx_packets,
48 | Network_tx_bytes,
49 | // Network_tx_dropped,
50 | // Network_tx_errors,
51 | // Network_tx_packets,
52 | Disk_read_value,
53 | Disk_write_value,
54 | } = data;
55 |
56 | const cpuData = {
57 | labels: times,
58 | datasets: [
59 | {
60 | label: 'CPU, %',
61 | data: cpu_percentage,
62 | borderColor: '#3b76b7',
63 | }
64 | ],
65 | };
66 | // formats for each data chart with their respective metric data/
67 | const memoryData = {
68 | labels: times,
69 | datasets: [
70 | {
71 | label: 'Memory usage, bytes',
72 | data: Memory_memory_usage,
73 | borderColor: '#3b76b7',
74 | }
75 | ],
76 | };
77 |
78 | const networkData = {
79 | labels: times,
80 | datasets: [
81 | {
82 | label: 'Network input, bytes',
83 | data: Network_rx_bytes,
84 | borderColor: '#3b76b7',
85 | },
86 | // {
87 | // label: "Network_rx_dropped",
88 | // data: Network_rx_dropped,
89 | // },
90 | // {
91 | // label: "Network_rx_errors",
92 | // data: Network_rx_errors,
93 | // },
94 | // {
95 | // label: "Network_rx_packets",
96 | // data: Network_rx_packets,
97 | // },
98 | {
99 | label: 'Network output, bytes',
100 | data: Network_tx_bytes,
101 | borderColor: '#95B9E5',
102 | }
103 | // {
104 | // label: "Network_tx_dropped",
105 | // data: Network_tx_dropped,
106 | // },
107 | // {
108 | // label: "Network_tx_errors",
109 | // data: Network_tx_errors,
110 | // },
111 | // {
112 | // label: "Network_tx_packets",
113 | // data: Network_tx_packets,
114 | // },
115 | ],
116 | };
117 |
118 | const diskData = {
119 | labels: times,
120 | datasets: [
121 | {
122 | label: 'Disk read, bytes',
123 | data: Disk_read_value,
124 | borderColor: '#3b76b7',
125 | },
126 | {
127 | label: 'Disk write, bytes',
128 | data: Disk_write_value,
129 | borderColor: '#95B9E5',
130 | }
131 | ],
132 | };
133 |
134 | return [cpuData, memoryData, networkData, diskData];
135 | };
136 | // const date = new Date().toLocaleDateString();
137 | // creates a chart for every graph in graphData
138 | function GraphContainer({ containerData, timeFrame }) {
139 | // cancel render if no graph data
140 | if (!Object.keys(containerData).length) {
141 | return ;
142 | }
143 | const graphData = formatGraphData(containerData, timeFrame);
144 | const titles = ['CPU', 'Memory', 'Network', 'Disk'];
145 | // convert array of chartJS-ready graph data to BarChart elements
146 | const charts = graphData.map((graph, i) => (
147 | // TODO investigate visual bug when using uuid
148 | // eslint-disable-next-line react/no-array-index-key
149 |
150 |
151 |
152 | ));
153 | return {charts}
;
154 | }
155 |
156 | export default GraphContainer;
157 |
--------------------------------------------------------------------------------
/src/api/controller.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | require('dotenv').config({ path: '../../.env' });
3 |
4 | const { InfluxDB } = require('@influxdata/influxdb-client');
5 | const getMetricArrays = require('./metricArrays');
6 |
7 | const DB_URL = process.env.DB_URL || 'http://127.0.0.1:8086';
8 | const DB_API_TOKEN = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN;
9 | const DB_ORG = process.env.DB_INFLUXDB_INIT_ORG;
10 | const DB_BUCKET = process.env.DB_INFLUXDB_INIT_BUCKET;
11 |
12 | const cpuPerc = (cpuStats, preCpuStats, systemCpuStats, preSystemCpuStats, onlineCpus) => {
13 | const cpuDelta = cpuStats - preCpuStats;
14 | const systemDelta = systemCpuStats - preSystemCpuStats;
15 | if (cpuDelta > 0 && systemDelta > 0) {
16 | return ((cpuDelta / systemDelta) * onlineCpus) * 100;
17 | } return 0;
18 | };
19 |
20 | const controller = {};
21 |
22 | controller.getContainers = (req, res, next) => {
23 | // query the db to return an array of objects with container names and ids
24 |
25 | // connect to the influx db using url and token
26 | const influxDB = new InfluxDB({ url: DB_URL, token: DB_API_TOKEN });
27 |
28 | // specify the org to send query to
29 | const queryApi = influxDB.getQueryApi(DB_ORG);
30 |
31 | // specify range of time to query db
32 | const range = '1h';
33 |
34 | // initialize array to collect query data
35 | const containers = [];
36 |
37 | // write the query
38 | const query = `from(bucket: "${DB_BUCKET}")
39 | |> range(start: -${range})
40 | |> group(columns: ["container_id", "container_name"])
41 | |> distinct(column: "container_id")`;
42 | queryApi.queryRows(query, {
43 | next(row, tableMeta) {
44 | const o = tableMeta.toObject(row);
45 | // keep track of if the container is in the array
46 | let containerInArr = false;
47 | containers.forEach((nameObj) => {
48 | if (nameObj.id === o.container_id) containerInArr = true;
49 | });
50 | // if container not in array, add it
51 | if (!containerInArr) {
52 | containers.push({
53 | name: o.container_name,
54 | id: o.container_id,
55 | });
56 | }
57 | },
58 | error(error) {
59 | console.log('Finished ERROR');
60 | return next(error);
61 | },
62 | complete() {
63 | // console.log('Finished SUCCESS');
64 | res.locals.containers = containers;
65 | return next();
66 | },
67 | });
68 | };
69 |
70 | controller.getContainerStats = (req, res, next) => {
71 | // query db for all stats for a specific container
72 |
73 | // destructure id from request
74 | const { id, metric } = req.params;
75 |
76 | let { range } = req.params;
77 |
78 | // if not range passed in, specify default range
79 | if (!range) {
80 | range = '1h';
81 | }
82 |
83 | // connect to the influx db using url and token
84 | const influxDB = new InfluxDB({ url: DB_URL, token: DB_API_TOKEN });
85 |
86 | // specify the org to send query to
87 | const queryApi = influxDB.getQueryApi(DB_ORG);
88 |
89 | // initialize array to collect query data
90 | const dataObj = {};
91 |
92 | // write the query for the passed in metric, or all metrics if no metric passed in
93 | let query = '';
94 | if (metric === 'disk') {
95 | query = `from(bucket: "${DB_BUCKET}")
96 | |> range(start: -${range})
97 | |> filter(fn: (r) => r["container_id"] == "${id}")
98 | |> filter(fn: (r) => r["_measurement"] == "Disk")`;
99 | } else if (metric === 'memory') {
100 | query = `from(bucket: "${DB_BUCKET}")
101 | |> range(start: -${range})
102 | |> filter(fn: (r) => r["container_id"] == "${id}")
103 | |> filter(fn: (r) => r["_measurement"] == "Memory")`;
104 | } else if (metric === 'cpu') {
105 | query = `from(bucket: "${DB_BUCKET}")
106 | |> range(start: -${range})
107 | |> filter(fn: (r) => r["container_id"] == "${id}")
108 | |> filter(fn: (r) => r["_measurement"] == "CPU")`;
109 | } else if (metric === 'network') {
110 | query = `from(bucket: "${DB_BUCKET}")
111 | |> range(start: -${range})
112 | |> filter(fn: (r) => r["container_id"] == "${id}")
113 | |> filter(fn: (r) => r["_measurement"] == "Network")`;
114 | } else {
115 | query = `from(bucket: "${DB_BUCKET}")
116 | |> range(start: -${range})
117 | |> filter(fn: (r) => r["container_id"] == "${id}")`;
118 | }
119 | queryApi.queryRows(query, {
120 | next(row, tableMeta) {
121 | const o = tableMeta.toObject(row);
122 | // eslint-disable-next-line no-underscore-dangle
123 | // if current time not in obj, add it
124 | if (!dataObj[o._time]) {
125 | dataObj[o._time] = {};
126 | }
127 | // TODO: Change display to MB or kB etc
128 | // add info on current row to the object at the associated time key
129 | dataObj[o._time][`${o._measurement}_${o._field}`] = o._value;
130 | },
131 | error(error) {
132 | console.log('Finished ERROR');
133 | return next(error);
134 | },
135 | complete() {
136 | const updatedDataObj = { ...dataObj };
137 | // if this is a live update (and not a large data fetch)
138 | // use only the most recent piece of data
139 | if (range === '8s' && Object.keys(updatedDataObj).length === 2) {
140 | delete updatedDataObj[Object.keys(updatedDataObj).sort()[0]];
141 | }
142 | if (!metric || metric === 'cpu') {
143 | const times = Object.keys(updatedDataObj);
144 | times.forEach((time) => {
145 | updatedDataObj[time].cpu_percentage = cpuPerc(
146 | updatedDataObj[time].CPU_cpu_usage,
147 | updatedDataObj[time].CPU_precpu_usage,
148 | updatedDataObj[time].CPU_system_cpu_usage,
149 | updatedDataObj[time].CPU_system_precpu_usage,
150 | updatedDataObj[time].CPU_online_cpus,
151 | );
152 | });
153 | }
154 | const formattedDataObj = getMetricArrays(updatedDataObj);
155 | res.locals.stats = formattedDataObj;
156 | return next();
157 | },
158 | });
159 | };
160 |
161 | module.exports = controller;
162 |
--------------------------------------------------------------------------------