mirror of
https://github.com/OPSnet/ApolloStatus.git
synced 2026-01-16 20:04:35 -05:00
initial commit
This commit is contained in:
205
.eslintrc
Normal file
205
.eslintrc
Normal file
@@ -0,0 +1,205 @@
|
||||
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
"import",
|
||||
"node",
|
||||
"promise",
|
||||
"standard"
|
||||
],
|
||||
|
||||
"globals": {
|
||||
"document": false,
|
||||
"navigator": false,
|
||||
"window": false
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"accessor-pairs": "error",
|
||||
"arrow-spacing": ["error", { "before": true, "after": true }],
|
||||
"block-spacing": ["error", "always"],
|
||||
"brace-style": ["error", "stroustrup", { "allowSingleLine": false }],
|
||||
"camelcase": ["error", { "properties": "never" }],
|
||||
"comma-dangle": ["error", {
|
||||
"arrays": "never",
|
||||
"objects": "never",
|
||||
"imports": "never",
|
||||
"exports": "never",
|
||||
"functions": "never"
|
||||
}],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"comma-style": ["error", "last"],
|
||||
"constructor-super": "error",
|
||||
"curly": ["error", "multi-line"],
|
||||
"dot-location": ["error", "property"],
|
||||
"eol-last": "error",
|
||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"generator-star-spacing": ["error", { "before": true, "after": true }],
|
||||
"handle-callback-err": ["error", "^(err|error)$" ],
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
"VariableDeclarator": 1,
|
||||
"outerIIFEBody": 1,
|
||||
"MemberExpression": 1,
|
||||
"FunctionDeclaration": { "parameters": 1, "body": 1 },
|
||||
"FunctionExpression": { "parameters": 1, "body": 1 },
|
||||
"CallExpression": { "arguments": 1 },
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1,
|
||||
"ImportDeclaration": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoreComments": false
|
||||
}],
|
||||
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
|
||||
"keyword-spacing": ["error", { "before": true, "after": true }],
|
||||
"new-cap": ["error", { "newIsCap": true, "capIsNew": false }],
|
||||
"new-parens": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-caller": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-eval": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-extra-parens": ["error", "functions"],
|
||||
"no-fallthrough": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-inner-declarations": ["error", "functions"],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-labels": ["error", { "allowLoop": false, "allowSwitch": false }],
|
||||
"no-lone-blocks": "error",
|
||||
"no-mixed-operators": ["error", {
|
||||
"groups": [
|
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
["&&", "||"],
|
||||
["in", "instanceof"]
|
||||
],
|
||||
"allowSamePrecedence": true
|
||||
}],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
|
||||
"no-negated-in-lhs": "error",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-object": "error",
|
||||
"no-new-require": "error",
|
||||
"no-new-symbol": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-path-concat": "error",
|
||||
"no-proto": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-return-assign": ["error", "except-parens"],
|
||||
"no-return-await": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-tabs": "error",
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
|
||||
"no-unreachable": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }],
|
||||
"no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }],
|
||||
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"no-with": "error",
|
||||
"no-var": "error",
|
||||
"object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }],
|
||||
"one-var": ["error", { "initialized": "never" }],
|
||||
"operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }],
|
||||
"padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }],
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"rest-spread-spacing": ["error", "never"],
|
||||
"semi": ["error", "always"],
|
||||
"semi-spacing": ["error", { "before": false, "after": true }],
|
||||
"space-before-blocks": ["error", "always"],
|
||||
"space-before-function-paren": ["error", "always"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
|
||||
"spaced-comment": ["error", "always", {
|
||||
"line": { "markers": ["*package", "!", "/", ",", "="] },
|
||||
"block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] }
|
||||
}],
|
||||
"symbol-description": "error",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"template-tag-spacing": ["error", "never"],
|
||||
"unicode-bom": ["error", "never"],
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": ["error", { "requireStringLiterals": true }],
|
||||
"wrap-iife": ["error", "any", { "functionPrototypeMethods": true }],
|
||||
"yield-star-spacing": ["error", "both"],
|
||||
"yoda": ["error", "never"],
|
||||
|
||||
"import/export": "error",
|
||||
"import/first": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/no-webpack-loader-syntax": "error",
|
||||
|
||||
"node/no-deprecated-api": "error",
|
||||
"node/process-exit-as-throw": "error",
|
||||
|
||||
"promise/param-names": "error",
|
||||
|
||||
"standard/array-bracket-even-spacing": ["error", "either"],
|
||||
"standard/computed-property-even-spacing": ["error", "even"],
|
||||
"standard/no-callback-literal": "error",
|
||||
"standard/object-curly-even-spacing": ["error", "either"]
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
/.idea/
|
||||
/node_modules/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
config.json
|
||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:8-alpine
|
||||
ADD . /srv
|
||||
WORKDIR /srv
|
||||
EXPOSE 3000
|
||||
RUN npm install && npm install nodemon
|
||||
CMD ["node_modules/.bin/nodemon", "apollostatus.js"]
|
||||
24
LICENSE.md
Normal file
24
LICENSE.md
Normal file
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
87
README.md
Normal file
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
ApolloStatus
|
||||
============
|
||||
Check the health of a site, tracker, and IRC.
|
||||
|
||||
ApolloStatus is a simple status page for torrent sites.
|
||||
It's powered by [Express](http://expressjs.com/) and [Redis](https://redis.io).
|
||||
|
||||
It's based on [WhatStatus](https://github.com/dewey/WhatStatus) as well as inspiration
|
||||
of certain features from [TrackerStatus](https://trackerstatus.info).
|
||||
|
||||
# Configure ApolloStatus
|
||||
|
||||
To make ApolloStatus more easily extendable, you can need to copy `config.json.template`
|
||||
to `config.json` and edit it to match the paths to your site, IRC, and tracker.
|
||||
|
||||
# Running ApolloStatus
|
||||
There are two methods for running and using ApolloStatus. The two covered methods
|
||||
are through Docker or Locally. The first step is to clone this repository:
|
||||
```
|
||||
git clone https://github.com/ApolloRIP/ApolloStatus
|
||||
```
|
||||
|
||||
When running ApolloStatus under either mode, it recognizes the following enviroment
|
||||
variables:
|
||||
|
||||
Variable | Description | Default
|
||||
-----------|-------------------------------------------------------------------|--------
|
||||
LOG_LEVEL | Level of messages to log. Can set to debug, info, warn, or error. | warn
|
||||
REDIS_HOST | Host of Redis that ApolloStatus should attempt to connect to. | 127.0.0.1
|
||||
REDIS_PORT | Port of Redis that ApolloStatus should attempt to connect to. | 6379
|
||||
PORT | Port that ApolloStatus should use. | 3000
|
||||
|
||||
## Docker
|
||||
The easiest way is to use [Docker Compose](https://docs.docker.com/compose/) and utilize
|
||||
the included `docker-compose.yml` file that will handle spinning up two containers (one
|
||||
for apollostatus and one for Redis) and setup the network bridge between the two applications.
|
||||
To use it, you'll just need to do `docker-compose up` which will handle building the local
|
||||
`Dockerfile` (which is for ApolloStatus), downloading the necessary Redis container, setting
|
||||
up a persistent volume layer for Redis, export port 3000 of the ApolloStatus container, and
|
||||
ensures that the containers restart automatically if they go stop for whatever reason
|
||||
(unless explicitly stopped via `docker-compose down`). You can do all of this yourself as
|
||||
well if desired, utilizing just the builtin `Dockerfile`, doing
|
||||
`docker build . -t apollo/apollostatus:latest` and then running it via `docker run`.
|
||||
|
||||
### Prerequisites
|
||||
The first (and easiest) is
|
||||
to use the included [Docker Compose](https://docs.docker.com/compose/) setup. This requires
|
||||
that you have Docker and Docker Compose installed. Usage is then as simple as:
|
||||
```docker-compose up```
|
||||
|
||||
This will spin up two containers, one for the redis server and one for the actual service.
|
||||
The site container exposes port 3000 for TCP by default so you can access it by going to
|
||||
`http://localhost:3000`.
|
||||
|
||||
## Locally
|
||||
- Install [Node.JS](http://nodejs.org/) and [NPM](https://www.npmjs.com/)
|
||||
- Install [Redis](http://redis.io/)
|
||||
- Navigate to the directory and run (as non-root user):
|
||||
```
|
||||
npm install
|
||||
```
|
||||
which will install all the dependencies listed in `package.json`
|
||||
|
||||
To then run the site, you just need to do `node apollostatus.js`. To setup the site to
|
||||
run permanently without you needing to start/stop it, it's recommended that you use
|
||||
[pm2](http://pm2.keymetrics.io/) (an alternative is [forever](https://www.npmjs.com/package/forever)).
|
||||
|
||||
The app is now running on port 3000. To serve it on your regular port 443 or 80 you'll have to setup nginx like this:
|
||||
|
||||
# Nginx Configuration
|
||||
To setup nginx to make your application accessible outside of localhost, you will want
|
||||
to setup your location block to be:
|
||||
|
||||
```
|
||||
{
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_redirect off;
|
||||
}
|
||||
```
|
||||
This will setup nginx that whenever a user visits the location you specify (whether it's
|
||||
`/` or `/status` or whatever) to proxy that connection to our NodeJS application which is
|
||||
listening to port 3000. This should be done when running the application locally
|
||||
or via Docker.
|
||||
285
apollostatus.js
Normal file
285
apollostatus.js
Normal file
@@ -0,0 +1,285 @@
|
||||
const express = require('express'),
|
||||
bodyParser = require('body-parser'),
|
||||
errorHandler = require('errorhandler'),
|
||||
favicon = require('serve-favicon'),
|
||||
morgan = require('morgan'),
|
||||
net = require('net'),
|
||||
path = require('path'),
|
||||
redis = require('redis'),
|
||||
CronJob = require('cron').CronJob,
|
||||
request = require('request'),
|
||||
winston = require('winston');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'warn',
|
||||
format: winston.format.json(),
|
||||
transports: [new winston.transports.Console({format: winston.format.simple()})]
|
||||
});
|
||||
|
||||
const config = require('./config.json');
|
||||
|
||||
const app = express();
|
||||
const db = redis.createClient(process.env.REDIS_PORT || 6379, process.env.REDIS_HOST || '127.0.0.1');
|
||||
const page_title = `Status :: ${config['site_name']}`;
|
||||
|
||||
// Catch connection errors if redis-server isn't running
|
||||
db.on('error', function (err) {
|
||||
logger.error(err.toString());
|
||||
logger.error(' Make sure redis-server is started and listening for connections.');
|
||||
});
|
||||
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
app.set('views', __dirname + '/views');
|
||||
app.set('view engine', 'pug');
|
||||
app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico')));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.text());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
if (app.get('env') === 'development') {
|
||||
app.use(errorHandler());
|
||||
app.locals.pretty = true;
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// HTML ENDPOINTS
|
||||
//////////////////////////////////
|
||||
|
||||
// Render the index page
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: page_title,
|
||||
logo_url: 'images/logos/logo.png'
|
||||
});
|
||||
});
|
||||
|
||||
// Render the Stats page
|
||||
app.get('/stats', (req, res) => {
|
||||
res.render('stats', {
|
||||
title: page_title
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api', (req, res) => {
|
||||
res.render('api', {
|
||||
title: page_title
|
||||
});
|
||||
});
|
||||
|
||||
// Render the About page
|
||||
app.get('/about', (req, res) => {
|
||||
res.render('about', {
|
||||
title: page_title
|
||||
});
|
||||
});
|
||||
|
||||
// Render the FAQ page
|
||||
app.get('/faq', (req, res) => {
|
||||
res.render('faq', {
|
||||
title: page_title
|
||||
});
|
||||
});
|
||||
|
||||
//////////////////////////////////
|
||||
// API ENDPOINTS
|
||||
//////////////////////////////////
|
||||
|
||||
// JSON Response for current component status
|
||||
app.get('/api/status', (req, res) => {
|
||||
db.mget(['status:site', 'status:irc', 'status:tracker', 'status:httptracker'], (err, results) => {
|
||||
res.json({
|
||||
site: parseInt(results[0]),
|
||||
irc: parseInt(results[1]),
|
||||
tracker: parseInt(results[2]),
|
||||
httptracker: parseInt(results[3])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/latency', (req, res) => {
|
||||
db.mget(['latency:site', 'latency: tracker', 'latency: httptracker'], (err, results) => {
|
||||
res.json({
|
||||
site: parseInt(results[0]),
|
||||
tracker: parseInt(results[1]),
|
||||
httptracker: parseInt(results[2])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// JSON Response for uptime values
|
||||
app.get('/api/uptime', (req, res) => {
|
||||
db.mget(['uptime:site', 'uptime:irc', 'uptime:tracker', 'uptime:httptracker'], (err, results) => {
|
||||
res.json({
|
||||
site: parseInt(results[0]),
|
||||
irc: parseInt(results[1]),
|
||||
tracker: parseInt(results[2]),
|
||||
httptracker: parseInt(results[3])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// JSON Response for uptime records
|
||||
app.get('/api/records', (req, res) => {
|
||||
db.mget(['uptimerecord:site', 'uptimerecord:irc', 'uptimerecord:tracker', 'uptimerecord:httptracker'], (err, results) => {
|
||||
res.json({
|
||||
site: parseInt(results[0]),
|
||||
irc: parseInt(results[1]),
|
||||
tracker: parseInt(results[2]),
|
||||
httptracker: parseInt(results[3])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/all', (req, res) => {
|
||||
db.mget(['status:site', 'status:irc', 'status:tracker', 'status:httptracker', 'latency:site',
|
||||
'latency: irc', 'latency: tracker', 'latency: httptracker', 'uptime:site', 'uptime:irc',
|
||||
'uptime:tracker', 'uptime:httptracker', 'uptimerecord:site', 'uptimerecord:irc',
|
||||
'uptimerecord:tracker', 'uptimerecord:httptracker'], (err, results) => {
|
||||
let resp = {};
|
||||
let keys = ['site', 'tracker', 'httptracker', 'irc'];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
resp[keys[i]] = {
|
||||
status: parseInt(results[i]),
|
||||
latency: parseInt(results[i+4]),
|
||||
uptime: parseInt(results[i+8]),
|
||||
uptimerecord: parseInt(results[i+12])
|
||||
};
|
||||
}
|
||||
res.json(resp);
|
||||
});
|
||||
});
|
||||
|
||||
//////////////////////////////////
|
||||
// CRON JOBS
|
||||
//////////////////////////////////
|
||||
|
||||
// Check all components every minute
|
||||
let counter = {
|
||||
site: [],
|
||||
tracker: [],
|
||||
httptracker: [],
|
||||
irc: []
|
||||
};
|
||||
|
||||
// Initialize Redis Keys to prevent "null" values
|
||||
function initializeRedis(component) {
|
||||
db.exists(component, (err, reply) => {
|
||||
if (reply !== 1) {
|
||||
db.set(component, '0');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let key of ['site', 'tracker', 'httptracker', 'irc']) {
|
||||
initializeRedis(`status:${key}`);
|
||||
initializeRedis(`latency:${key}`);
|
||||
initializeRedis(`uptimerecord:${key}`);
|
||||
initializeRedis(`uptime:${key}`);
|
||||
}
|
||||
|
||||
function checkStatus(key, uri) {
|
||||
request({uri: uri, method: 'GET', time: true}, (err, resp) => {
|
||||
counter[key].pop();
|
||||
if (!err && response.statusCode === 200) {
|
||||
db.set(`latency:${key}`, Math.trunc(response.timings.connect).toString());
|
||||
db.set(`status:${key}`, '1');
|
||||
counter[key].unshift(0);
|
||||
}
|
||||
else {
|
||||
counter[key].unshift(1);
|
||||
const upper_key = key.replace(/^\w/, c => c.toUpperCase());
|
||||
logger.info(`[Check-${upper_key}] ${upper_key} down`);
|
||||
let sum = counter[key].reduce((a, b) => a + b, 0);
|
||||
// if in the last three minutes, we've had at least one up,
|
||||
// then we're unstable, else we mark it as down
|
||||
if (sum < 3) {
|
||||
db.set(`status:${key}`, '2');
|
||||
logger.info(`[Check-${upper_key}] ${upper_key} unstable`);
|
||||
}
|
||||
else {
|
||||
db.set(`status:${key}`, '0');
|
||||
resetUptime(key);
|
||||
logger.info(`[Check-${upper_key}] ${upper_key} down`);
|
||||
}
|
||||
}
|
||||
updateRecord(key);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRecord(key) {
|
||||
const upper_key = key.replace(/^\w/, c => c.toUpperCase());
|
||||
db.get(`status:${key}`, (err, status) => {
|
||||
if (status !== '0') {
|
||||
db.incr(`uptime:${key}`);
|
||||
}
|
||||
|
||||
db.mget([`uptime:${key}`, `uptimerecord:${key}`], (err, results) => {
|
||||
if (parseInt(results[0]) > parseInt(results[1])) {
|
||||
logger.info(`[Stats-${upper_key}] ${upper_key} Records updated [${results[1]} to ${results[0]}]`);
|
||||
db.set(`uptimerecord:${key}`, results[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If there's an outtage reset uptime record counter.
|
||||
function resetUptime (component) {
|
||||
db.set('uptime:' + component, '0');
|
||||
}
|
||||
|
||||
const uris = {
|
||||
site: `https://${config.site}`,
|
||||
tracker: `https://${config.tracker.https}`,
|
||||
httptracker: `http://${config.tracker.http}`
|
||||
};
|
||||
|
||||
// Check Site Components (Cronjob running every minute)
|
||||
new CronJob('*/1 * * * *', () => {
|
||||
logger.info('Running minute cron job');
|
||||
for (let key in uris) {
|
||||
checkStatus(key, uris[key]);
|
||||
}
|
||||
|
||||
// Get IRC Status
|
||||
const time = process.hrtime();
|
||||
let client = net.connect(config.irc.port, config.irc.fqdn, () => {
|
||||
const diff = process.hrtime(time);
|
||||
db.set('status:irc', '1');
|
||||
db.set('latency:irc', Math.trunc(diff[0] * 1000000 + diff[1] / 1000).toString());
|
||||
counter['irc'].pop();
|
||||
counter['irc'].unshift(0);
|
||||
updateRecord('irc');
|
||||
});
|
||||
|
||||
// Socket connection closed
|
||||
client.on('end', null);
|
||||
|
||||
// Error on connecting to target host
|
||||
client.on('error', () => {
|
||||
counter['irc'].pop();
|
||||
counter['irc'].unshift(1);
|
||||
let sum = counter['irc'].reduce((a, b) => a + b, 0);
|
||||
logger.info(`[Check-IRC] Status counter: ${sum}`);
|
||||
if (sum < 3) {
|
||||
db.set(`status:irc`, '2');
|
||||
logger.info(`[Check-IRC] IRC unstable`);
|
||||
}
|
||||
else {
|
||||
db.set(`status:irc`, '0');
|
||||
resetUptime('irc');
|
||||
logger.info(`[Check-IRC] IRC down`);
|
||||
}
|
||||
updateRecord('irc');
|
||||
client.end();
|
||||
});
|
||||
|
||||
client.on('timeout', () => {
|
||||
logger.info('[Check-IRC] Timeout');
|
||||
client.end();
|
||||
});
|
||||
}, null, true);
|
||||
|
||||
app.listen(app.get('port'), () => {
|
||||
logger.info('ApolloStatus server listening on port: ' + app.get('port'));
|
||||
});
|
||||
12
config.json.template
Normal file
12
config.json.template
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"site_name": "EXAMPLE",
|
||||
"site": "example.com",
|
||||
"tracker": {
|
||||
"https": "tracker.example.com",
|
||||
"http": "example.com:2095"
|
||||
},
|
||||
"irc": {
|
||||
"fqdn": "irc.example.com",
|
||||
"port": 6667,
|
||||
}
|
||||
}
|
||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
status:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_HOST=redis
|
||||
- LOG_LEVEL=debug
|
||||
volumes:
|
||||
- .:/srv
|
||||
restart: always
|
||||
redis:
|
||||
image: redis:4.0-alpine
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
3690
package-lock.json
generated
Normal file
3690
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "apollostatus",
|
||||
"description": "Status site to monitor the up status of various components of a torrent site",
|
||||
"author": "ApolloRIP",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "node app"
|
||||
},
|
||||
"repository": "github:apollo/status",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.3",
|
||||
"cron": "^1.3",
|
||||
"errorhandler": "^1.5.0",
|
||||
"express": "^4.16",
|
||||
"morgan": "^1.9",
|
||||
"nodemon": "^1.17.5",
|
||||
"pug": "^2.0",
|
||||
"redis": "^2.8",
|
||||
"request": "^2.87",
|
||||
"serve-favicon": "^2.5",
|
||||
"winston": "^3.0.0"
|
||||
}
|
||||
}
|
||||
BIN
public/images/down.png
Normal file
BIN
public/images/down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/images/favicon.ico
Normal file
BIN
public/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/images/logos/logo.png
Normal file
BIN
public/images/logos/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/images/up.png
Normal file
BIN
public/images/up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/updating.png
Normal file
BIN
public/images/updating.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
169
public/stylesheets/style.css
Normal file
169
public/stylesheets/style.css
Normal file
@@ -0,0 +1,169 @@
|
||||
#content {
|
||||
margin: 0 auto;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.service-table {
|
||||
margin-top: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
ul span {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.up {
|
||||
color: #089a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
.down {
|
||||
font-weight: bold;
|
||||
color: #ff1600;
|
||||
}
|
||||
.maintenance {
|
||||
font-weight: bold;
|
||||
color: #ff1600;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
margin-top: 50px;
|
||||
width: 600px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
footer ul li {
|
||||
list-style:none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
footer ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer ul li:after {
|
||||
content:"·";
|
||||
color:#666;
|
||||
}
|
||||
|
||||
footer ul li:last-child:after {
|
||||
content:"";
|
||||
}
|
||||
|
||||
footer ul li a {
|
||||
margin:0 8px;
|
||||
color:blue;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #212328;
|
||||
color: #c6c9c9;
|
||||
text-shadow: #111 0px 1px 0px;
|
||||
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover, a:visited:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
.disclaimer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
.stats-table td {
|
||||
width: 500px;
|
||||
}
|
||||
h3 {
|
||||
padding-top: 0px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: 3px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.follow-us {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.twitter{
|
||||
width: 550px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.status-dashboard {
|
||||
width: 550px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 140px;
|
||||
margin: 50px auto 0 auto;
|
||||
width: 550px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div #chart-tracker-uptime {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
p.api-description {
|
||||
font-size: 12px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.about-logos {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.about-logos li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.about-logos li img {
|
||||
height: 50px;
|
||||
margin:0 8px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
:target {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
:target a:visited {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
views/about.pug
Normal file
12
views/about.pug
Normal file
@@ -0,0 +1,12 @@
|
||||
extends layout
|
||||
block body-content
|
||||
div#content
|
||||
h3 About
|
||||
p This is the official status site of apollo.rip
|
||||
p The services are checked every minute, if a service is down for more than 3 minutes it's reflected on the site.
|
||||
p
|
||||
| We make all service checks once a minute, and then average it over three minutes. If it's up for the
|
||||
| full three minutes, then the service is considered up and healthy. If it's down for all three minutes, then
|
||||
| the service is considered unhealthy and down. If it's not quite up or down, then it's considered unstable.
|
||||
p
|
||||
|
|
||||
30
views/api.pug
Normal file
30
views/api.pug
Normal file
@@ -0,0 +1,30 @@
|
||||
extends layout
|
||||
block body-content
|
||||
div#content
|
||||
h3 About the APIs
|
||||
h3 API Endpoints
|
||||
|
||||
p The following API endpoints are available:
|
||||
|
||||
p.api
|
||||
a(href="/api/status") /api/status
|
||||
p.api-description Get the status of all monitored services
|
||||
p.api-description 0 = offline, no response over last 3 minutes
|
||||
p.api-description 1 = online, service responded perfectly over last 3 minutes
|
||||
p.api-description 2 = unstable, intermittent responses over the past 3 minutes
|
||||
|
||||
p.api
|
||||
a(href="/api/latency") /api/latency
|
||||
p.api-description Get the latency of all monitored services as applicable
|
||||
|
||||
p.api
|
||||
a(href="/api/uptime") /api/uptime
|
||||
p.api-description Get the current uptime of all services, the numbers are displayed in minutes.
|
||||
|
||||
p.api
|
||||
a(href="/api/records") /api/records
|
||||
p.api-description Get the best recorded continuous uptime of all services, the numbers are displayed in minutes.
|
||||
|
||||
p.api
|
||||
a(href="/api/all") /api/all
|
||||
p.api-description This combines all other APIs into one convenient endpoint.
|
||||
53
views/faq.pug
Normal file
53
views/faq.pug
Normal file
@@ -0,0 +1,53 @@
|
||||
extends layout
|
||||
block body-content
|
||||
div#content
|
||||
h3 My account is disabled, is there anything I can do?
|
||||
p
|
||||
| If you are having any problems with your account you'll have to join #disabled on the IRC network.
|
||||
| This also includes other problems with your account, not just disabled accounts.
|
||||
|
||||
h3 How do I connect to the IRC network?
|
||||
p
|
||||
| You'll have to use an IRC client and connect to irc.apollo.rip. The port is 6667 or +7000
|
||||
| if you are using SSL.
|
||||
|
||||
h3 My registration/activation/password reset mail isn't arriving
|
||||
p
|
||||
| Currently, our email servers are down, ask your inviter for a direct link. In case of a password reset,
|
||||
| please join #disabled on our IRC network.
|
||||
|
||||
h3 How can I get invited to the site?
|
||||
p
|
||||
| Head over to #[a(href="https://interview.apollo.rip") interview.apollo.rip] and read the instructions.
|
||||
|
||||
h3 How do I join IRC? The Java Applet / Mibbit isn't working.
|
||||
p
|
||||
| Use a real IRC client:
|
||||
h4 Mac
|
||||
ul.faq-list
|
||||
li Textual
|
||||
li Colloquy
|
||||
li irssi
|
||||
li weechat
|
||||
|
||||
h4 Linux
|
||||
ul.faq-list
|
||||
li Hexchat
|
||||
li irssi
|
||||
li weechat
|
||||
|
||||
h4 Windows
|
||||
ul.faq-list
|
||||
li Hexchat
|
||||
li irssi
|
||||
li mIRC
|
||||
|
||||
h3 Credits
|
||||
p
|
||||
| This site is based on
|
||||
| #[a(href="https://github.com/dewey/WhatStatus") https://github.com/dewey/WhatStatus]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
61
views/index.pug
Normal file
61
views/index.pug
Normal file
@@ -0,0 +1,61 @@
|
||||
extends layout
|
||||
|
||||
block body-content
|
||||
script(type='text/javascript').
|
||||
ajaxGet('/api/status', function(components) {
|
||||
for (var component in components) {
|
||||
if (components.hasOwnProperty(component)) {
|
||||
var elem = document.getElementById('status-' + component);
|
||||
if (components[component] === 1) {
|
||||
elem.innerText = 'Up';
|
||||
elem.style.color = '#089a00';
|
||||
document.getElementById('status-' + component + '-icon').setAttribute('src', 'images/up.png');
|
||||
}
|
||||
else if (components[component] === 0) {
|
||||
elem.innerText = 'Down';
|
||||
elem.style.color = '#ff1600';
|
||||
document.getElementById('status-' + component + '-icon').setAttribute('src', 'images/down.png');
|
||||
}
|
||||
else {
|
||||
elem.innerText = 'Updating...';
|
||||
document.getElementById('status-' + component + '-icon').setAttribute('src', 'images/updating.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ajaxGet('/api/latency', function(components) {
|
||||
for (var component in components) {
|
||||
if (components.hasOwnProperty(component)) {
|
||||
var elem = document.getElementById('status-' + component);
|
||||
elem.innerText = components[component] + 'ms';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
div#content
|
||||
div.status-dashboard
|
||||
table.service-table
|
||||
tr
|
||||
td.icon
|
||||
img(src, id='status-site-icon')
|
||||
td.service Site - apollo.rip
|
||||
td#status-site
|
||||
tr
|
||||
td.icon
|
||||
img(src, id='status-tracker-icon')
|
||||
td.service HTTPS Tracker - mars.apollo.rip
|
||||
td#status-tracker
|
||||
tr
|
||||
td.icon
|
||||
img(src, id='status-httptracker-icon')
|
||||
td.service HTTP Tracker - apollo.rip:2095
|
||||
td#status-httptracker
|
||||
tr
|
||||
td.icon
|
||||
img(src, id='status-irc-icon')
|
||||
td.service IRC - irc.apollo.rip
|
||||
td#status-irc
|
||||
|
||||
|
||||
|
||||
71
views/layout.pug
Normal file
71
views/layout.pug
Normal file
@@ -0,0 +1,71 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel="stylesheet", href="/stylesheets/style.css")
|
||||
link(rel="shortcut icon", href="images/favicon.ico")
|
||||
script(type='text/javascript').
|
||||
function ajaxGet(url, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
callback(JSON.parse(xhr.responseText));
|
||||
}
|
||||
else {
|
||||
console.log("Error: " + xhr.status + ", Text: " + xhr.statusText);
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('GET', url, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function minutesToHumanString(minutes) {
|
||||
var minutesYear = 525600;
|
||||
var minutesMonth = 43200; // We assume 30 days in a month
|
||||
var minutesDay = 1440;
|
||||
var minutesHour = 60;
|
||||
var string = "";
|
||||
var years = Math.floor(minutes / minutesYear);
|
||||
if (years > 0) {
|
||||
string += years + "Y ";
|
||||
minutes -= years * minutesYear;
|
||||
}
|
||||
var months = Math.floor(minutes / minutesMonth);
|
||||
if (months > 0) {
|
||||
string += months + "M ";
|
||||
minutes -= months * minutesMonth;
|
||||
}
|
||||
var days = Math.floor(minutes / minutesDay);
|
||||
if (days > 0) {
|
||||
string += days + "D ";
|
||||
minutes -= days * minutesDay;
|
||||
}
|
||||
var hours = Math.floor(minutes / minutesHour);
|
||||
if (hours > 0) {
|
||||
string += hours + "h ";
|
||||
minutes -= hours * minutesHour;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
string += minutes + "m";
|
||||
}
|
||||
return string.trim();
|
||||
}
|
||||
body
|
||||
div.logo
|
||||
img(src='images/logos/logo.png')
|
||||
block body-content
|
||||
footer
|
||||
p All information is cached and updated every minute.
|
||||
ul
|
||||
li
|
||||
a(href='/') Home
|
||||
li
|
||||
a(href='stats') Stats
|
||||
li
|
||||
a(href='api') API
|
||||
li
|
||||
a(href='faq') FAQ
|
||||
li
|
||||
a(href='about') About
|
||||
42
views/stats.pug
Normal file
42
views/stats.pug
Normal file
@@ -0,0 +1,42 @@
|
||||
extends layout
|
||||
block body-content
|
||||
script(type='text/javascript').
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
ajaxGet('/api/uptime', function (components) {
|
||||
for (var component in components) {
|
||||
if (components.hasOwnProperty(component)) {
|
||||
document.getElementById('stats-uptime-' + component).innerHTML = minutesToHumanString(components[component]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ajaxGet('/api/records', function (components) {
|
||||
for (var component in components) {
|
||||
if (components.hasOwnProperty(component)) {
|
||||
document.getElementById('stats-uptimerecord-' + component).innerHTML = minutesToHumanString(components[component]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
div#content
|
||||
h3 Uptime
|
||||
ul.stats-list
|
||||
li Site - apollo.rip
|
||||
span#stats-uptime-site
|
||||
li HTTPS Tracker - mars.apollo.rip
|
||||
span#stats-uptime-tracker
|
||||
li HTTP Tracker - apollo.rip:2095
|
||||
span#stats-uptime-httptracker
|
||||
li IRC - irc.apollo.rip
|
||||
span#stats-uptime-irc
|
||||
h3 Uptime Records
|
||||
ul.stats-list
|
||||
li Site - apollo.rip
|
||||
span#stats-uptimerecord-site
|
||||
li HTTPS Tracker - mars.apollo.rip
|
||||
span#stats-uptimerecord-tracker
|
||||
li HTTP Tracker - apollo.rip:2095
|
||||
span#stats-uptimerecord-httptracker
|
||||
li IRC - irc.apollo.rip
|
||||
span#stats-uptimerecord-irc
|
||||
Reference in New Issue
Block a user