App v1 - memory storage

This commit is contained in:
Elton Stoneman 2017-11-21 12:22:47 +00:00
parent a13e7d79a3
commit b989556613
21 changed files with 24 additions and 1009 deletions

View File

@ -1,94 +0,0 @@
# Node Bulletin Board
A Node.js sample app which shows an event bulletin board, using Vue.js for the front-end and SQL Server for storage. The app runs in containers and the only pre-requisite for building and running the whole stack is Docker.
![Bulletin Board app](img/bulletin-board.jpg)
### Credits
The original app is from [Vue Events Bulletin Board](https://github.com/chenkie/vue-events-bulletin) and the Prometheus integration comes from [Example Prometheus Monitoring](https://github.com/RisingStack/example-prometheus-nodejs).
## Tech Stack
The entrypoint to the app is an Nginx container, which acts as a reverse proxy to the Node.js application container. The application container stores data in the SQL Server container, and it also exposes instrumentation metrics which are scraped by a Prometheus container. The Prometheus metrics are exposed in a dashboard from the Grafana container.
## Usage
The [Docker Compose](docker-compose.yml) file uses an environment variable for your Docker ID, so you can build the images and push to Docker Hub. Start by capturing your Docker ID in an environment variable:
```
export dockerId='<your-docker-id>'
```
### Build Docker images
Build the application and database images using Docker Compose:
```
docker-compose build
```
### Run the app
You can run the app using the same compose file:
```
docker-compose up -d
```
Browse to [localhost](http://localhost) to use the app.
### Configure Grafana
The compose file runs Prometheus to collect metrics and Grafana to show an application dashbaord. Grfana needs some additional setup.
Browse to [localhost:3000](http://localhost:3000) and log in to Grafana with the credentials `admin` / `admin`.
Add a new data source with the following details:
- Name: **prometheus**
- Type: **Prometheus**
- URL: **http://bb-metrics:9090**
![Grafana data source](img/grafana-data-source.jpg)
From the Grafana icon, click _Dashboards... Import_ and load the JSON dashboard file from [bulletin-board-dashboard/dashboard.json](bulletin-board-dashboard/dashboard.json). Select the Prometheus data store.
You'll now see the application dashboard - send some load into the app by refreshing the browser, and the graphs will be populated:
![Grafana dashboard](img/grafana-dashboard.jpg)
> You can save the configured Grafana container as an image, which persists all the setup changes.
```
docker container commit nodebulletinboard_bb-dashboard_1 $dockerId/bulletin-board-dashboard
```
### Running in swarm mode
You can deploy the app to a Docker swarm for high availability and scale. A single-node swarm is fine for testing.
If you're running the app from compose, first remove all the containers:
```
docker-compose down
```
Switch to swarm mode:
```
docker swarm init
```
> If your Docker host has multiple IP addresses, you'll need to specify the local network IP in the `advertise-addr` option.
Then deploy the application as a stack:
```
docker stack deploy -c docker-stack.yml bb
```
You can browse to the app at the server address, and to the configured Grafana instance at port 3000.

View File

@ -7,6 +7,4 @@ RUN npm install
EXPOSE 8080
CMD [ "npm", "start" ]
COPY . .
HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1
COPY . .

View File

@ -1,43 +1,9 @@
var db = require('./db.js');
var events = require('./events.js');
exports.events = function (req, res) {
console.log('Loading DB events...');
db.Events
.findAll()
.then(events => {
console.log('Fetched events, count: ' + events.length);
res.json(events);
})
.catch(err => {
console.error('** Fetch failed: ', err);
});
res.json(events);
};
exports.event = function (req, res) {
console.log('Handling event call, method: ' + req.method + ', event ID: ' + req.params.eventId)
switch(req.method) {
case "DELETE":
db.Events
.destroy({
where: {
id: req.params.eventId
}
}).then(function() {
console.log('Deleted event with id: ' + req.params.eventId)
res.status(200).end();
});
break
case "POST":
db.Events
.create({
title: req.body.title,
detail: req.body.detail,
date: req.body.date
})
.then(function() {
res.send('{}');
res.status(201).end();
});
break
}
res.json(events[req.param.eventId]);
};

View File

@ -1,34 +0,0 @@
var Sequelize = require('sequelize');
var username = 'sa';
var password = 'DockerCon!!!';
var host = 'bb-db';
var dbName = 'BulletinBoard';
var sequelize = new Sequelize(dbName, username, password, {
dialect: 'mssql',
host: host,
port: 1433,
dialectOptions: {
requestTimeout: 30000
}
});
sequelize
.authenticate()
.then(() => {
console.log('Successful connection to SQL Server.');
})
.catch(err => {
console.error('** SQL Server connection failed: ', err);
process.exit(1);
});
var Event = sequelize.define('event', {
title: Sequelize.STRING,
detail: Sequelize.STRING,
date: Sequelize.DATE
});
Event.sync();
exports.Events = Event;

View File

@ -0,0 +1,19 @@
module.exports = [
{
id: 1,
title: 'Docker Workshop',
detail: 'Linuxing in Lonodon ',
date: '2017-11-21'
},
{
id: 2,
title: 'WinOps #17',
detail: 'WinOps London',
date: '2017-11-21'
},
{
id: 3,
title: 'Docker London',
date: '2017-11-13'
}
];

View File

@ -11,10 +11,7 @@
"express": "^4.13.3",
"morgan": "^1.6.1",
"vue": "^1.0.10",
"vue-resource": "^0.1.17",
"tedious": "^2.0.1",
"sequelize": "^4.20.1",
"prom-client": "^10.2.2"
"vue-resource": "^0.1.17"
},
"devDependencies": {
"body-parser": "^1.14.1",

View File

@ -3,7 +3,6 @@ var express = require('express'),
methodOverride = require('method-override'),
errorHandler = require('errorhandler'),
morgan = require('morgan'),
prometheus = require('prom-client'),
routes = require('./backend'),
api = require('./backend/api');
@ -30,42 +29,10 @@ if ('production' == app.get('env')) {
app.use(errorHandler());
}
// counter for Prometheus:
const httpRequestDurationMicroseconds = new prometheus.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'code'],
buckets: [0.10, 5, 15, 50, 100, 200, 300, 400, 500] // buckets for response time from 0.1ms to 500ms
})
// record timestamp before request handler:
app.use((req, res, next) => {
res.locals.startEpoch = Date.now()
next()
})
app.get('/', routes.index);
app.get('/api/events', api.events);
app.post('/api/events', api.event);
app.delete('/api/events/:eventId', api.event);
app.get('/metrics', (req, res) => {
res.set('Content-Type', prometheus.register.contentType)
res.end(prometheus.register.metrics())
})
// set response duration after handler:
app.use((req, res, next) => {
const responseTimeInMs = Date.now() - res.locals.startEpoch
httpRequestDurationMicroseconds
.labels(req.method, req.path, res.statusCode)
.observe(responseTimeInMs)
next()
})
prometheus.collectDefaultMetrics();
app.listen(8080);
console.log('Magic happens on port 8080...');

View File

@ -1,5 +0,0 @@
FROM grafana/grafana:4.6.2
ENV GF_PATHS_DATA='/data'
WORKDIR /data

View File

@ -1,613 +0,0 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "4.6.2"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
},
{
"datasource": "${DS_PROMETHEUS}",
"enable": false,
"expr": "ALERTS",
"hide": false,
"iconColor": "rgba(255, 96, 96, 1)",
"limit": 100,
"name": "Alerts",
"showIn": 0,
"step": "10s",
"type": "alert"
}
]
},
"description": "Bulletin Board",
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"hideControls": false,
"id": null,
"links": [],
"refresh": "5s",
"rows": [
{
"collapse": false,
"height": 250,
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"rgba(50, 172, 45, 0.97)",
"rgba(237, 129, 40, 0.89)",
"rgba(245, 54, 54, 0.9)"
],
"datasource": "${DS_PROMETHEUS}",
"decimals": null,
"format": "none",
"gauge": {
"maxValue": 1,
"minValue": 0,
"show": true,
"thresholdLabels": true,
"thresholdMarkers": true
},
"hideTimeOverride": false,
"id": 6,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"span": 3,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"tableColumn": "Value",
"targets": [
{
"expr": "sum(increase(http_request_duration_ms_count{code=~\"^5..$\"}[1m])) / sum(increase(http_request_duration_ms_count[1m]))",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "",
"refId": "A",
"step": 20
}
],
"thresholds": "0.1",
"title": "Error rate",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"id": 1,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [
{
"type": "dashboard"
}
],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 9,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(http_request_duration_ms_count[1m])) by (service, route, method, code) * 60",
"format": "time_series",
"hide": false,
"intervalFactor": 2,
"legendFormat": "{{service}} - {{method}} {{route}} {{code}}",
"metric": "",
"refId": "A",
"step": 2
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Throughput",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "rpm",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": true,
"title": "Throughput",
"titleSize": "h6"
},
{
"collapse": false,
"height": 250,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "histogram_quantile(0.5, sum(rate(http_request_duration_ms_bucket[1m])) by (le, service, route, method))",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{service}} - {{method}} {{route}}",
"refId": "A",
"step": 2
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Median Response Time",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ms",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(http_request_duration_ms_bucket[1m])) by (le, service, route, method))",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{service}} - {{method}} {{route}}",
"refId": "A",
"step": 2
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "95th Response Time",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"transparent": false,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "ms",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": true,
"title": "Response time",
"titleSize": "h6"
},
{
"collapse": false,
"height": 305,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"id": 3,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(nodejs_external_memory_bytes / 1024) by (service)",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{service}}",
"refId": "A",
"step": 2
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Memory usage",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "decmbytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"id": 7,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"span": 6,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "avg(process_cpu_seconds_total) by (service)",
"format": "time_series",
"intervalFactor": 2,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "CPU usage",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"repeat": null,
"repeatIteration": null,
"repeatRowId": null,
"showTitle": true,
"title": "Compute stats",
"titleSize": "h6"
}
],
"schemaVersion": 14,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "browser",
"title": "Bulletin Board",
"version": 3
}

View File

@ -1,10 +0,0 @@
FROM microsoft/mssql-server-linux:2017-CU1
ENV ACCEPT_EULA=Y \
MSSQL_SA_PASSWORD=DockerCon!!!
WORKDIR /init
COPY init-db.* ./
RUN chmod +x ./init-db.sh
RUN /opt/mssql/bin/sqlservr & ./init-db.sh

View File

@ -1,3 +0,0 @@
sleep 30s
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P DockerCon!!! -i init-db.sql

View File

@ -1,20 +0,0 @@
CREATE DATABASE BulletinBoard;
GO
USE BulletinBoard;
CREATE TABLE Events (
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
Title NVARCHAR(50),
Detail NVARCHAR(200),
[Date] DATETIMEOFFSET,
CreatedAt DATETIMEOFFSET NOT NULL,
UpdatedAt DATETIMEOFFSET NOT NULL
);
INSERT INTO Events (Title, Detail, [Date], CreatedAt, UpdatedAt) VALUES
(N'Docker for Beginners', N'Introduction to Docker using Node.js', '2017-11-21', GETDATE(), GETDATE()),
(N'Advanced Orchestration', N'Deep dive into Docker Swarm', '2017-12-25', GETDATE(), GETDATE()),
(N'Docker on Windows', N'From 101 to production', '2018-01-01', GETDATE(), GETDATE());
SELECT * FROM BulletinBoard.dbo.Events;

View File

@ -1,3 +0,0 @@
FROM prom/prometheus:v2.0.0
COPY prometheus.yml /etc/prometheus/prometheus.yml

View File

@ -1,7 +0,0 @@
global:
scrape_interval: 5s
scrape_configs:
- job_name: 'bb-app'
static_configs:
- targets: ['bb-app:8080']

View File

@ -1,4 +0,0 @@
FROM nginx:1.13.6
RUN mkdir -p /data/nginx/cache
COPY nginx.conf /etc/nginx/nginx.conf

View File

@ -1,47 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:30m inactive=24h max_size=200m use_temp_path=off;
proxy_pass_request_headers on;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header Connection keep-alive;
gzip on;
gzip_proxied any;
add_header X-Host $hostname;
add_header X-Cache-Status $upstream_cache_status;
map $sent_http_content_type $expires {
default off;
text/css 1M;
text/javascript 1M;
application/javascript 1M;
application/x-font-woff 3M;
~image/ 6M;
}
server {
listen 80 default_server;
server_name _;
expires $expires;
location / {
proxy_pass http://bb-app:8080/;
proxy_cache STATIC;
proxy_cache_valid 200 5s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
}
}
}

View File

@ -1,54 +0,0 @@
version: '3.3'
services:
bb-db:
image: ${dockerId}/bulletin-board-db
build:
context: ./bulletin-board-db
networks:
- bb-net
bb-app:
image: ${dockerId}/bulletin-board-app
build:
context: ./bulletin-board-app
depends_on:
- bb-db
restart: on-failure
networks:
- bb-net
bb-proxy:
image: ${dockerId}/bulletin-board-proxy
build:
context: ./bulletin-board-proxy
ports:
- "80:80"
depends_on:
- bb-app
networks:
- bb-net
bb-metrics:
image: ${dockerId}/bulletin-board-metrics
build:
context: ./bulletin-board-metrics
depends_on:
- bb-app
networks:
- bb-net
bb-dashboard:
image: ${dockerId}/bulletin-board-dashboard
build:
context: ./bulletin-board-dashboard
ports:
- "3000:3000"
depends_on:
- bb-metrics
networks:
- bb-net
networks:
bb-net:

View File

@ -1,38 +0,0 @@
version: '3.3'
services:
bb-db:
image: ${dockerId}/bulletin-board-db
networks:
- bb-net
bb-app:
image: ${dockerId}/bulletin-board-app
networks:
- bb-net
bb-proxy:
image: ${dockerId}/bulletin-board-proxy
ports:
- "80:80"
networks:
- bb-net
deploy:
mode: replicated
replicas: 3
bb-metrics:
image: ${dockerId}/bulletin-board-metrics
networks:
- bb-net
bb-dashboard:
image: ${dockerId}/bulletin-board-dashboard
ports:
- "3000:3000"
networks:
- bb-net
networks:
bb-net:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB