From 81bb895295b693a5a4b0ec5dd02a95b5647ed596 Mon Sep 17 00:00:00 2001 From: Harry Suryapambagya Date: Tue, 3 Sep 2024 13:38:58 +0800 Subject: [PATCH] feat: tinystatus is up --- .env | 17 ++++++ README.md | 98 +++++++++++++++++++++++++++++++ checks.yaml | 76 ++++++++++++++++++++++++ history.html.theme | 83 +++++++++++++++++++++++++++ incidents.md | 5 ++ index.html.theme | 86 ++++++++++++++++++++++++++++ tinystatus.py | 140 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 505 insertions(+) create mode 100644 .env create mode 100644 README.md create mode 100644 checks.yaml create mode 100644 history.html.theme create mode 100644 incidents.md create mode 100644 index.html.theme create mode 100644 tinystatus.py diff --git a/.env b/.env new file mode 100644 index 0000000..45d50ff --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# TinyStatus Configuration + +# Check interval in seconds +CHECK_INTERVAL=30 + +# Maximum number of history entries per service +MAX_HISTORY_ENTRIES=100 + +# Logging level (INFO, WARNING, ERROR, DEBUG) +LOG_LEVEL=INFO + +# File paths +CHECKS_FILE=checks.yaml +INCIDENTS_FILE=incidents.md +TEMPLATE_FILE=index.html.theme +HISTORY_TEMPLATE_FILE=history.html.theme +STATUS_HISTORY_FILE=history.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..187336b --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# TinyStatus + +TinyStatus is a simple, customizable status page generator that allows you to monitor the status of various services and display them on a clean, responsive web page. + +## Features + +- Monitor HTTP endpoints, ping hosts, and check open ports +- Responsive design for both status page and history page +- Customizable service checks via YAML configuration +- Incident history tracking +- Automatic status updates at configurable intervals + +## Prerequisites + +- Python 3.7 or higher +- pip (Python package manager) + +## Installation + +1. Clone the repository or download the source code: + ``` + git clone https://github.com/yourusername/tinystatus.git + cd tinystatus + ``` + +2. Install the required dependencies: + ``` + pip install pyyaml aiohttp markdown jinja2 + ``` + +## Configuration + +1. Create a `.env` file in the project root and customize the variables: + ``` + CHECK_INTERVAL=30 + MAX_HISTORY_ENTRIES=100 + LOG_LEVEL=INFO + CHECKS_FILE=checks.yaml + INCIDENTS_FILE=incidents.md + TEMPLATE_FILE=index.html.theme + HISTORY_TEMPLATE_FILE=history.html.theme + STATUS_HISTORY_FILE=history.json + ``` + +2. Edit the `checks.yaml` file to add or modify the services you want to monitor. Example: + ```yaml + - name: GitHub Home + type: http + host: https://github.com + expected_code: 200 + + - name: Google DNS + type: ping + host: 8.8.8.8 + + - name: Database + type: port + host: db.example.com + port: 5432 + ``` + +3. (Optional) Customize the `incidents.md` file to add any known incidents or maintenance schedules. + +4. (Optional) Modify the `index.html.theme` and `history.html.theme` files to customize the look and feel of your status pages. + +## Usage + +1. Run the TinyStatus script: + ``` + python tinystatus.py + ``` + +2. The script will generate two HTML files: + - `index.html`: The main status page + - `history.html`: The status history page + +3. To keep the status page continuously updated, you can run the script in the background: + - On Unix-like systems (Linux, macOS): + ``` + nohup python tinystatus.py & + ``` + - On Windows, you can use the Task Scheduler to run the script at startup. + +4. Serve the generated HTML files using your preferred web server (e.g., Apache, Nginx, or a simple Python HTTP server for testing). + +## Customization + +- Adjust the configuration variables in the `.env` file to customize the behavior of TinyStatus. +- Customize the appearance of the status page by editing the CSS in `index.html.theme` and `history.html.theme`. +- Add or remove services by modifying the `checks.yaml` file. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is open source and available under the [MIT License](LICENSE). \ No newline at end of file diff --git a/checks.yaml b/checks.yaml new file mode 100644 index 0000000..d5492c1 --- /dev/null +++ b/checks.yaml @@ -0,0 +1,76 @@ +- name: GitHub Home + type: http + host: https://github.com + expected_code: 200 + +- name: GitHub API + type: http + host: https://api.github.com + expected_code: 200 + +- name: Wikipedia Home + type: http + host: https://www.wikipedia.org + expected_code: 200 + +- name: Wikipedia API + type: http + host: https://en.wikipedia.org/w/api.php + expected_code: 200 + +- name: DigitalOcean + type: http + host: https://www.digitalocean.com + expected_code: 200 + +- name: DigitalOcean API + type: http + host: https://api.digitalocean.com + expected_code: 200 + +- name: Google Home + type: http + host: https://www.google.com + expected_code: 200 + +- name: Cloudflare DNS Checker + type: ping + host: 1.1.1.1 + +- name: Google Public DNS + type: ping + host: 8.8.8.8 + +- name: Dummy Postgres Database + type: port + host: ec2-54-173-89-248.compute-1.amazonaws.com + port: 5432 + +- name: Dummy MySQL Database + type: port + host: db.example.com + port: 3306 + +- name: Amazon Web Services + type: http + host: https://aws.amazon.com + expected_code: 200 + +- name: AWS S3 API + type: http + host: https://s3.amazonaws.com + expected_code: 200 + +- name: Twitter + type: http + host: https://twitter.com + expected_code: 200 + +- name: Facebook Home + type: http + host: https://www.facebook.com + expected_code: 200 + +- name: Localhost + type: ping + host: localhost diff --git a/history.html.theme b/history.html.theme new file mode 100644 index 0000000..650da58 --- /dev/null +++ b/history.html.theme @@ -0,0 +1,83 @@ + + + + + + TinyStatus History + + + +

TinyStatus History

+
+ {% for service, entries in history.items() %} +
+

{{ service }}

+ {% for entry in entries|reverse %} +
+ {{ entry.timestamp.split('T')[0] }} {{ entry.timestamp.split('T')[1][:8] }} + + {{ 'Up' if entry.status else 'Down' }} + +
+ {% endfor %} +
+ {% endfor %} +
+ + + diff --git a/incidents.md b/incidents.md new file mode 100644 index 0000000..8770006 --- /dev/null +++ b/incidents.md @@ -0,0 +1,5 @@ +## 2024-03-15: Scheduled Maintenance +Our database will undergo scheduled maintenance from 02:00 AM to 04:00 AM UTC. + +## 2024-03-10: API Outage +We experienced an API outage from 15:30 to 16:45 UTC. The issue has been resolved. \ No newline at end of file diff --git a/index.html.theme b/index.html.theme new file mode 100644 index 0000000..091e8d4 --- /dev/null +++ b/index.html.theme @@ -0,0 +1,86 @@ + + + + + + TinyStatus + + + +

TinyStatus

+

Current Status

+
+ {% for check in checks %} +
+

{{ check.name }}

+

+ {{ 'Operational' if check.status else 'Down' }} +

+
+ {% endfor %} +
+

Incident History

+
+ {{ incidents | safe }} +
+ + + diff --git a/tinystatus.py b/tinystatus.py new file mode 100644 index 0000000..bc9a3e0 --- /dev/null +++ b/tinystatus.py @@ -0,0 +1,140 @@ +import os +from dotenv import load_dotenv +import yaml +import asyncio +import aiohttp +import subprocess +import markdown +from jinja2 import Template +from datetime import datetime +import json +import logging + +# Load environment variables +load_dotenv() + +# Configuration +CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', 30)) +MAX_HISTORY_ENTRIES = int(os.getenv('MAX_HISTORY_ENTRIES', 100)) +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') +CHECKS_FILE = os.getenv('CHECKS_FILE', 'checks.yaml') +INCIDENTS_FILE = os.getenv('INCIDENTS_FILE', 'incidents.md') +TEMPLATE_FILE = os.getenv('TEMPLATE_FILE', 'index.html.theme') +HISTORY_TEMPLATE_FILE = os.getenv('HISTORY_TEMPLATE_FILE', 'history.html.theme') +STATUS_HISTORY_FILE = os.getenv('STATUS_HISTORY_FILE', 'history.json') + +# Service check functions +async def check_http(url, expected_code): + async with aiohttp.ClientSession() as session: + try: + async with session.get(url) as response: + return response.status == expected_code + except: + return False + +async def check_ping(host): + try: + result = subprocess.run(['ping', '-c', '1', '-W', '2', host], capture_output=True, text=True) + return result.returncode == 0 + except: + return False + +async def check_port(host, port): + try: + _, writer = await asyncio.open_connection(host, port) + writer.close() + await writer.wait_closed() + return True + except: + return False + +async def run_checks(checks): + results = [] + for check in checks: + if check['type'] == 'http': + status = await check_http(check['host'], check['expected_code']) + elif check['type'] == 'ping': + status = await check_ping(check['host']) + elif check['type'] == 'port': + status = await check_port(check['host'], check['port']) + results.append({'name': check['name'], 'status': status}) + return results + +# History management +def load_history(): + if os.path.exists(STATUS_HISTORY_FILE): + with open(STATUS_HISTORY_FILE, 'r') as f: + return json.load(f) + return {} + +def save_history(history): + with open(STATUS_HISTORY_FILE, 'w') as f: + json.dump(history, f, indent=2) + +def update_history(results): + history = load_history() + current_time = datetime.now().isoformat() + for check in results: + if check['name'] not in history: + history[check['name']] = [] + history[check['name']].append({'timestamp': current_time, 'status': check['status']}) + history[check['name']] = history[check['name']][-MAX_HISTORY_ENTRIES:] + save_history(history) + +# Main monitoring loop +async def monitor_services(): + while True: + try: + with open(CHECKS_FILE, 'r') as f: + checks = yaml.safe_load(f) + + with open(INCIDENTS_FILE, 'r') as f: + incidents = markdown.markdown(f.read()) + + with open(TEMPLATE_FILE, 'r') as f: + template = Template(f.read()) + + with open(HISTORY_TEMPLATE_FILE, 'r') as f: + history_template = Template(f.read()) + + results = await run_checks(checks) + update_history(results) + + html = template.render(checks=results, incidents=incidents, last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + with open('index.html', 'w') as f: + f.write(html) + + history_html = history_template.render(history=load_history(), last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + with open('history.html', 'w') as f: + f.write(history_html) + + logging.info(f"Status page and history updated at {datetime.now()}") + down_services = [check['name'] for check in results if not check['status']] + if down_services: + logging.warning(f"Services currently down: {', '.join(down_services)}") + + except Exception as e: + logging.error(f"An error occurred: {str(e)}") + + await asyncio.sleep(CHECK_INTERVAL) + +# Main function +def main(): + with open(CHECKS_FILE, 'r') as f: + checks = yaml.safe_load(f) + + with open(INCIDENTS_FILE, 'r') as f: + incidents = markdown.markdown(f.read()) + + with open(TEMPLATE_FILE, 'r') as f: + template = Template(f.read()) + + results = asyncio.run(run_checks(checks)) + html = template.render(checks=results, incidents=incidents, last_updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + + with open('index.html', 'w') as f: + f.write(html) + +if __name__ == "__main__": + logging.basicConfig(level=getattr(logging, LOG_LEVEL), format='%(asctime)s - %(levelname)s - %(message)s') + asyncio.run(monitor_services())