Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
.git

static/status.json
.idea
#config.js
docker-compose.yml
apulse_data

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
static/status.json
.idea
config.js
docker-compose.yml
apulse_data

# Logs
logs
*.log
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:20-slim
WORKDIR /app
ADD . .
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ Visit https://apulse.ybouane.com for a demo!
[<img src="screenshot.png" alt="aPulse — Server Status Open Source NodeJS Tool" />](https://apulse.ybouane.com)

# Features
- Highly and easily configurable, edit the config.js file to add test endpoints and configure the watcher
- Highly and easily configurable, copy config.js from config.dist.js, then edit file to add test endpoints and configure the watcher
- Supports sending outage notifications by: Telegram, Discord, Slack, SMS (Twilio API), Email (SendGrid API)
- Uses the Fetch API to test server-responses, you can configure GET, POST, PUT... requests and have full control over the fetch options.
- Check content for validity, HTTP status...
- Measures latency
- Minimal and easy to use dashboard
- Easy to setup. Run the watcher.js script and open the static/index.html page to view the dashboard.
- Easy to setup. Run the `entrypoint.sh` script and open the static/index.html page to view the dashboard.
- Auto-reload of the config file (no need to restart the watcher)
- No dependencies

Expand All @@ -30,6 +30,7 @@ export default {
verbose : true, // Whether or not to output pulse messages in the console
readableStatusJson : true, // Format status.json to be human readable
logsMaxDatapoints : 200, // Maximum datapoints history to keep (per endpoint)
statusFile : './static/status.json', // Path to status.json file
telegram : {}, // optional, tokens to send notifications through telegram
slack : {}, // optional, tokens to send notifications through slack
discord : {}, // optional, tokens to send notifications through discord
Expand Down Expand Up @@ -67,12 +68,12 @@ Clone the repo:
git clone https://github.com/ybouane/aPulse.git
```

Either run the watcher.js script directly (you need to keep it running in the background)
Either run the entrypoint.sh script directly (you need to keep it running in the background)
```shell
cd aPulse
```
```shell
node watcher.js
./entrypoint.sh
```

Or use a tool like PM2 (prefered method):
Expand All @@ -93,7 +94,7 @@ pm2 save
```

### Serving the status page
The `watcher.js` script only takes care of running the status checks and updates the `status.json` file in the `static/` folder. If you want to view the final result, you simply need to serve the files in the `static/` folder. You can use Nginx with a config like:
The `watcher.js` script only takes care of running the status checks and updates the file defined by `statusFile` (by default `static/status.json`). The `entrypoint.sh` script patches `static/client.js` with this path before starting `watcher.js`. If you want to view the final result, you simply need to serve the files in the `static/` folder (or wherever your dashboard points to). You can use Nginx with a config like:
```nginx
# Pulse
server {
Expand Down
2 changes: 2 additions & 0 deletions config.js → config.dist.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export default {
verbose : true, // Whether or not to output pulse messages in the console
readableStatusJson : true, // Format status.json to be human readable
logsMaxDatapoints : 200, // Maximum datapoints history to keep (per endpoint)
statusFile : './static/status.json', // Path to status.json file
showLinks : true, // Display links on UI
telegram : { // optional, tokens to send notifications through telegram
botToken : '', // Contact @BotFather on telegram to create a bot
chatId : '',// Send a message to the bot, then visit https://api.telegram.org/bot<token>/getUpdates to get the chatId
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.dist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:

watcher:
build:
context: .
volumes:
- ./apulse_data:/app/data
- ./config.js:/app/config.js
restart: unless-stopped

nginx:
image: nginx:alpine
volumes:
- ./apulse_data:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- "8080:80"
restart: unless-stopped
24 changes: 24 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
set -e

STATUS_FILE=$(node -e "
const fs = require('fs');
const path = require('path');

const configRaw = fs.readFileSync('./config.js', 'utf8');
const match = configRaw.match(/statusFile\s*:\s*['\"](.*?)['\"]/);
const result = match ? match[1] : './static/status.json';
console.log(result);
")

STATUS_DIR=$(dirname "$STATUS_FILE")

if [ "$STATUS_DIR" != "./static" ]; then
echo "Copying static files to: $STATUS_DIR"
mkdir -p "$STATUS_DIR"
cp -r static/* "$STATUS_DIR"
else
echo "statusFile is in ./static; skipping copy."
fi

exec node watcher.js
15 changes: 15 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

location /favicon.ico {
return 301 /favicon.png;
}

location / {
try_files $uri $uri/ =404;
}
}
2 changes: 1 addition & 1 deletion pm2.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"apps": [{
"name" : "aPulse",
"script" : "./watcher.js"
"script" : "./entrypoint.sh"
}]
}
2 changes: 1 addition & 1 deletion static/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ document.addEventListener("DOMContentLoaded", async () => {

$endpointName = document.createElement('h3');
$endpointName.innerText = endpoint.name;
if(endpoint.link) {
if((config.showLinks ?? true) && endpoint.link) {
let $link = document.createElement('a');
$link.href = endpoint.link;
$link.target = '_blank';
Expand Down
1 change: 0 additions & 1 deletion static/status.json

This file was deleted.

20 changes: 10 additions & 10 deletions watcher.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import {promises as fs, watchFile} from 'fs';
let config = (await import('./config.js')).default;
let statusFile = config.statusFile || './static/status.json';

watchFile('./config.js', async ()=>{ // Dynamically reload config and watch it for changes.
try {
config = (await import('./config.js?refresh='+Date.now())).default;
console.log('Reloaded config file.')
} catch(e) {
console.error(e);
}
watchFile('./config.js', async () => { // Dynamically reload config and watch it for changes.
try {
config = (await import('./config.js?refresh=' + Date.now())).default;
console.log('Reloaded config file.')
} catch (e) {
console.error(e);
}
});

const statusFile = './static/status.json';

const delay = async t=>new Promise(r=>setTimeout(r, t));
const handlize = s=>s.toLowerCase().replace(/[^a-z0-9]/g, ' ').trim().replace(/\s+/g, '-');
const checkContent = async (content, criterion, negate=false) => {
Expand Down Expand Up @@ -139,6 +138,7 @@ while(true) {
nDataPoints : config.nDataPoints,
responseTimeGood : config.responseTimeGood,
responseTimeWarning : config.responseTimeWarning,
showLinks : config.showLinks,
};

status.ui = [];
Expand Down Expand Up @@ -176,7 +176,7 @@ while(true) {
endpoint_.link = endpoint.link || endpoint.url;
endpoint_.logs = endpoint_.logs || [];
let start;

try {
performance.clearResourceTimings();
start = performance.now();
Expand Down