diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0169800 --- /dev/null +++ b/.dockerignore @@ -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.* diff --git a/.gitignore b/.gitignore index 9bf1fb8..3b9d516 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ static/status.json +.idea +config.js +docker-compose.yml +apulse_data + # Logs logs *.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8213b4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20-slim +WORKDIR /app +ADD . . +RUN chmod +x /app/entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/README.md b/README.md index 79bb693..a67f7d9 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Visit https://apulse.ybouane.com for a demo! [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 @@ -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 @@ -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): @@ -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 { diff --git a/config.js b/config.dist.js similarity index 95% rename from config.js rename to config.dist.js index 5b013d6..4dfc044 100644 --- a/config.js +++ b/config.dist.js @@ -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/getUpdates to get the chatId diff --git a/docker-compose.dist.yml b/docker-compose.dist.yml new file mode 100644 index 0000000..858f99c --- /dev/null +++ b/docker-compose.dist.yml @@ -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 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..a2a1d4c --- /dev/null +++ b/entrypoint.sh @@ -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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d471e9a --- /dev/null +++ b/nginx.conf @@ -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; + } +} \ No newline at end of file diff --git a/pm2.json b/pm2.json index ab53aa5..9d9274f 100644 --- a/pm2.json +++ b/pm2.json @@ -1,6 +1,6 @@ { "apps": [{ "name" : "aPulse", - "script" : "./watcher.js" + "script" : "./entrypoint.sh" }] } \ No newline at end of file diff --git a/static/client.js b/static/client.js index 7e88c1a..cc23e92 100644 --- a/static/client.js +++ b/static/client.js @@ -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'; diff --git a/static/status.json b/static/status.json deleted file mode 100644 index 9e26dfe..0000000 --- a/static/status.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/watcher.js b/watcher.js index 658393f..50de430 100644 --- a/watcher.js +++ b/watcher.js @@ -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) => { @@ -139,6 +138,7 @@ while(true) { nDataPoints : config.nDataPoints, responseTimeGood : config.responseTimeGood, responseTimeWarning : config.responseTimeWarning, + showLinks : config.showLinks, }; status.ui = []; @@ -176,7 +176,7 @@ while(true) { endpoint_.link = endpoint.link || endpoint.url; endpoint_.logs = endpoint_.logs || []; let start; - + try { performance.clearResourceTimings(); start = performance.now();