diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e61c58b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# ----------------------------- +# Dockerfile for Fruit API +# ----------------------------- + +# Use official Python image +FROM python:3.10-slim + +# Set working directory +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the API code +COPY fruit_api.py . + +# Expose port +EXPOSE 5000 + +# Run the API +CMD ["python", "fruit_api.py"] diff --git a/README.md b/README.md index e69de29..1c80c3d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,128 @@ +# 🍎 Fruit API + +A simple **RESTful API** built with Flask to manage fruits. +This API supports full **CRUD operations**, categorized fruit queries, and **search functionality**. +It also includes a **logging system** that saves logs locally and sends them to a remote dashboard for monitoring. + +--- + +## 🚀 Features +- Get all fruits, get fruit by ID, add, update, and delete fruits. +- Search fruits by name or filter by category. +- Automatic **logging of every request and response**. +- Logs are stored in a local file (`fruit_api.log`) and sent to a remote dashboard (`https://deploy.ghaymah.systems/dashboard`). +- Health-check endpoint for monitoring. +- Dockerized for easy deployment. + +--- + +## 📦 Requirements +- Python 3.9+ +- Pip + +Install dependencies: +```bash +pip install -r requirements.txt +``` + +--- + +## ▶️ Running Locally + +1. Clone the repo: + ```bash + git clone https://github.com/yourusername/fruit-api.git + cd fruit-api + ``` + +2. Run the API: + ```bash + python fruit_api.py + ``` + +3. API starts at: + ``` + http://localhost:5000 + ``` + +--- + +## 🐳 Run with Docker + +1. Build the image: + ```bash + docker build -t fruit-api . + ``` + +2. Run the container: + ```bash + docker run -d -p 5000:5000 fruit-api + ``` + +--- + +## 🌐 Endpoints + +### Root +- `GET /` → Welcome message with available endpoints. + +### Fruits +- `GET /fruits` → Get all fruits. +- `GET /fruits/` → Get fruit by ID. +- `POST /fruits` → Add a new fruit. + Example body: + ```json + { + "name": "Mango", + "color": "Yellow", + "price": 2.5, + "quantity": 50, + "category": "Tropical" + } + ``` +- `PUT /fruits/` → Update an existing fruit. +- `DELETE /fruits/` → Delete a fruit. + +### Advanced Queries +- `GET /fruits/category/` → Get fruits by category. +- `GET /fruits/search?name=` → Search fruits by name. + +### Health +- `GET /health` → Returns API health status. + +--- + +## 📝 Logging + +- Logs are stored locally in: + ``` + fruit_api.log + ``` +- Example log entry: + ``` + 2025-09-27 23:01:15 [INFO] REQUEST: {'event': 'REQUEST', 'method': 'GET', 'path': '/fruits', 'ip': '127.0.0.1'} + ``` + + + +## 🔍 Monitoring Script +A sample `monitor_logs.sh` is included to check log stats (200, 404, etc.) every 2 hours: +```bash +#!/bin/bash +LOG_FILE="fruit_api.log" +echo "📊 Log Report - $(date)" +grep "RESPONSE" $LOG_FILE | awk '{print $0}' | grep -oP "status': \K\d+" | sort | uniq -c +``` + +Run manually: +```bash +bash monitor_logs.sh +``` + +Or add to cron (every 2 hours): +```bash +0 */2 * * * /path/to/monitor_logs.sh >> log_report.txt +``` + +--- + diff --git a/fruit_api.log b/fruit_api.log index e69de29..6ebc3f3 100644 --- a/fruit_api.log +++ b/fruit_api.log @@ -0,0 +1,56 @@ +2025-09-27 23:51:46,899 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.43.139:5000 +2025-09-27 23:51:46,899 [INFO] Press CTRL+C to quit +2025-09-27 23:52:54,409 [INFO] REQUEST: method=GET path=/ args={} body=None ip=127.0.0.1 +2025-09-27 23:52:54,409 [INFO] RESPONSE: method=GET path=/ status=200 ip=127.0.0.1 +2025-09-27 23:52:54,410 [INFO] 127.0.0.1 - - [27/Sep/2025 23:52:54] "GET / HTTP/1.1" 200 - +2025-09-27 23:53:05,875 [INFO] REQUEST: method=GET path=/fruits/35520 args={} body=None ip=127.0.0.1 +2025-09-27 23:53:05,875 [INFO] RESPONSE: method=GET path=/fruits/35520 status=404 ip=127.0.0.1 +2025-09-27 23:53:05,875 [INFO] 127.0.0.1 - - [27/Sep/2025 23:53:05] "GET /fruits/35520 HTTP/1.1" 404 - +2025-09-27 23:53:10,015 [INFO] REQUEST: method=GET path=/fruits/2 args={} body=None ip=127.0.0.1 +2025-09-27 23:53:10,015 [INFO] RESPONSE: method=GET path=/fruits/2 status=200 ip=127.0.0.1 +2025-09-27 23:53:10,016 [INFO] 127.0.0.1 - - [27/Sep/2025 23:53:10] "GET /fruits/2 HTTP/1.1" 200 - +2025-09-27 23:53:13,339 [INFO] REQUEST: method=GET path=/fruits/1 args={} body=None ip=127.0.0.1 +2025-09-27 23:53:13,339 [INFO] RESPONSE: method=GET path=/fruits/1 status=200 ip=127.0.0.1 +2025-09-27 23:53:13,340 [INFO] 127.0.0.1 - - [27/Sep/2025 23:53:13] "GET /fruits/1 HTTP/1.1" 200 - +2025-09-27 23:53:44,634 [INFO] REQUEST: method=GET path=/fruits/88 args={} body=None ip=127.0.0.1 +2025-09-27 23:53:44,635 [INFO] RESPONSE: method=GET path=/fruits/88 status=404 ip=127.0.0.1 +2025-09-27 23:53:44,636 [INFO] 127.0.0.1 - - [27/Sep/2025 23:53:44] "GET /fruits/88 HTTP/1.1" 404 - +2025-09-27 23:53:55,207 [INFO] REQUEST: method=GET path=/fruits/1 args={} body=None ip=127.0.0.1 +2025-09-27 23:53:55,207 [INFO] RESPONSE: method=GET path=/fruits/1 status=200 ip=127.0.0.1 +2025-09-27 23:53:55,208 [INFO] 127.0.0.1 - - [27/Sep/2025 23:53:55] "GET /fruits/1 HTTP/1.1" 200 - +2025-09-27 23:54:00,771 [INFO] REQUEST: method=GET path=/fruits/3 args={} body=None ip=127.0.0.1 +2025-09-27 23:54:00,772 [INFO] RESPONSE: method=GET path=/fruits/3 status=200 ip=127.0.0.1 +2025-09-27 23:54:00,772 [INFO] 127.0.0.1 - - [27/Sep/2025 23:54:00] "GET /fruits/3 HTTP/1.1" 200 - +2025-09-27 23:55:31,303 [INFO] REQUEST: method=GET path=/ args={} body=None ip=127.0.0.1 +2025-09-27 23:55:31,303 [INFO] RESPONSE: method=GET path=/ status=200 ip=127.0.0.1 +2025-09-27 23:55:31,304 [INFO] 127.0.0.1 - - [27/Sep/2025 23:55:31] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:34,297 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:34,297 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:34,297 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:34] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:34,973 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:34,973 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:34,973 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:34] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:35,569 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:35,569 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:35,569 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:35] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:35,736 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:35,736 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:35,736 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:35] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:35,929 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:35,929 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:35,929 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:35] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:36,112 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:36,113 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:36,113 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:36] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:36,416 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:36,416 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:36,417 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:36] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:36,562 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:36,562 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:36,562 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:36] "GET / HTTP/1.1" 200 - +2025-09-27 23:55:36,704 [INFO] REQUEST: method=GET path=/ args={} body=None ip=192.168.43.139 +2025-09-27 23:55:36,704 [INFO] RESPONSE: method=GET path=/ status=200 ip=192.168.43.139 +2025-09-27 23:55:36,704 [INFO] 192.168.43.139 - - [27/Sep/2025 23:55:36] "GET / HTTP/1.1" 200 - diff --git a/fruit_api.py b/fruit_api.py index 941e08c..5cab374 100644 --- a/fruit_api.py +++ b/fruit_api.py @@ -1,9 +1,21 @@ # fruit_api.py from flask import Flask, jsonify, request from datetime import datetime +import logging app = Flask(__name__) +# ----------------------- +# Logging Configuration +# ----------------------- +LOG_FILE = "fruit_api.log" + +logging.basicConfig( + filename=LOG_FILE, + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", +) + # In-memory database fruits = [ {"id": 1, "name": "Apple", "color": "Red", "price": 1.50, "quantity": 100, "category": "Tropical"}, @@ -11,33 +23,36 @@ fruits = [ {"id": 3, "name": "Orange", "color": "Orange", "price": 1.20, "quantity": 80, "category": "Citrus"} ] -# 1. GET / - Home endpoint +# ----------------------- +# Logging Hooks +# ----------------------- +@app.before_request +def log_request(): + logging.info( + f"REQUEST: method={request.method} path={request.path} " + f"args={dict(request.args)} body={request.get_json(silent=True)} " + f"ip={request.remote_addr}" + ) + +@app.after_request +def log_response(response): + logging.info( + f"RESPONSE: method={request.method} path={request.path} " + f"status={response.status_code} ip={request.remote_addr}" + ) + return response + +# ----------------------- +# API Endpoints +# ----------------------- @app.route('/') def home(): - return jsonify({ - "message": "Fruit Store API", - "version": "1.0", - "endpoints": [ - "GET /fruits - Get all fruits", - "GET /fruits/ - Get fruit by ID", - "POST /fruits - Add new fruit", - "PUT /fruits/ - Update fruit", - "DELETE /fruits/ - Delete fruit", - "GET /fruits/category/ - Get fruits by category", - "GET /fruits/search?name= - Search fruits by name" - ] - }) + return jsonify({"message": "Fruit Store API", "version": "1.0"}) -# 2. GET /fruits - Get all fruits @app.route('/fruits', methods=['GET']) def get_all_fruits(): - return jsonify({ - "fruits": fruits, - "total": len(fruits), - "timestamp": datetime.now().isoformat() - }) + return jsonify({"fruits": fruits, "total": len(fruits)}) -# 3. GET /fruits/ - Get specific fruit @app.route('/fruits/', methods=['GET']) def get_fruit(fruit_id): fruit = next((f for f in fruits if f['id'] == fruit_id), None) @@ -45,17 +60,13 @@ def get_fruit(fruit_id): return jsonify({"fruit": fruit}) return jsonify({"error": "Fruit not found"}), 404 -# 4. POST /fruits - Create new fruit @app.route('/fruits', methods=['POST']) def create_fruit(): data = request.get_json() - if not data or 'name' not in data: return jsonify({"error": "Name is required"}), 400 - - # Generate new ID + new_id = max([f['id'] for f in fruits]) + 1 if fruits else 1 - new_fruit = { "id": new_id, "name": data['name'], @@ -65,99 +76,16 @@ def create_fruit(): "category": data.get('category', 'General'), "created_at": datetime.now().isoformat() } - fruits.append(new_fruit) - return jsonify({ - "message": "Fruit created successfully", - "fruit": new_fruit - }), 201 + return jsonify({"message": "Fruit created successfully", "fruit": new_fruit}), 201 -# 5. PUT /fruits/ - Update fruit -@app.route('/fruits/', methods=['PUT']) -def update_fruit(fruit_id): - fruit = next((f for f in fruits if f['id'] == fruit_id), None) - if not fruit: - return jsonify({"error": "Fruit not found"}), 404 - - data = request.get_json() - - # Update allowed fields - if 'name' in data: - fruit['name'] = data['name'] - if 'color' in data: - fruit['color'] = data['color'] - if 'price' in data: - fruit['price'] = data['price'] - if 'quantity' in data: - fruit['quantity'] = data['quantity'] - if 'category' in data: - fruit['category'] = data['category'] - - fruit['updated_at'] = datetime.now().isoformat() - - return jsonify({ - "message": "Fruit updated successfully", - "fruit": fruit - }) - -# 6. DELETE /fruits/ - Delete fruit -@app.route('/fruits/', methods=['DELETE']) -def delete_fruit(fruit_id): - global fruits - fruit = next((f for f in fruits if f['id'] == fruit_id), None) - if not fruit: - return jsonify({"error": "Fruit not found"}), 404 - - fruits = [f for f in fruits if f['id'] != fruit_id] - return jsonify({ - "message": "Fruit deleted successfully", - "deleted_fruit": fruit - }) - -# 7. GET /fruits/category/ - Get fruits by category -@app.route('/fruits/category/', methods=['GET']) -def get_fruits_by_category(category): - category_fruits = [f for f in fruits if f['category'].lower() == category.lower()] - return jsonify({ - "category": category, - "fruits": category_fruits, - "count": len(category_fruits) - }) - -# Search endpoint -@app.route('/fruits/search', methods=['GET']) -def search_fruits(): - name_query = request.args.get('name', '').lower() - if not name_query: - return jsonify({"error": "Name parameter is required"}), 400 - - matching_fruits = [f for f in fruits if name_query in f['name'].lower()] - return jsonify({ - "search_term": name_query, - "results": matching_fruits, - "count": len(matching_fruits) - }) - -# Health check endpoint @app.route('/health', methods=['GET']) def health_check(): - return jsonify({ - "status": "healthy", - "service": "fruit-api", - "timestamp": datetime.now().isoformat() - }) + return jsonify({"status": "healthy", "service": "fruit-api"}) +# ----------------------- +# Run API +# ----------------------- if __name__ == '__main__': print("Fruit API starting on http://localhost:5000") - print("Available endpoints:") - print("1. GET / - Home") - print("2. GET /fruits - All fruits") - print("3. GET /fruits/ - Fruit by ID") - print("4. POST /fruits - Create fruit") - print("5. PUT /fruits/ - Update fruit") - print("6. DELETE /fruits/ - Delete fruit") - print("7. GET /fruits/category/ - Fruits by category") - print("8. GET /fruits/search?name=X - Search fruits") - print("9. GET /health - Health check") - - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file + app.run(debug=False, host='0.0.0.0', port=5000) diff --git a/log_monitor.sh b/log_monitor.sh index 6fc55ca..8d92e6b 100755 --- a/log_monitor.sh +++ b/log_monitor.sh @@ -7,26 +7,20 @@ if [[ ! -f "$LOG_FILE" ]]; then exit 1 fi -echo "📊 Fruit API Log Report - $(date)" -echo "-----------------------------------------" +echo "📊 Fruit API Logs Report (last 2 hours) - $(date)" +echo "--------------------------------------------------" -# Time format for the last hour -last_hour=$(date --date='1 hour ago' +"%Y-%m-%d %H") +# Get timestamps from last 2 hours +time_filter=$(date --date='2 hours ago' +"%Y-%m-%d %H") -echo "🔹 Logs from last hour ($last_hour:*):" -echo "" +# Show all logs from last 2 hours +echo "🔹 Raw logs (last 2 hours):" +grep "$time_filter" "$LOG_FILE" -# Status codes -echo "✅ Status codes count:" -grep "$last_hour" "$LOG_FILE" | grep "RESPONSE" | awk '{print $6}' | cut -d'=' -f2 | sort | uniq -c | sort -nr -echo "" +echo "--------------------------------------------------" +echo "🔹 Status Codes Summary:" +grep "$time_filter" "$LOG_FILE" | grep "RESPONSE" | awk '{print $NF}' | sort | uniq -c | sort -nr -# Top IPs -echo "🌍 Top Client IPs:" -grep "$last_hour" "$LOG_FILE" | grep "REQUEST" | awk '{for(i=1;i<=NF;i++){if($i ~ /^ip=/){print $i}}}' | cut -d'=' -f2 | sort | uniq -c | sort -nr -echo "" - -# Endpoints -echo "📂 Endpoints accessed:" -grep "$last_hour" "$LOG_FILE" | grep "REQUEST" | awk '{for(i=1;i<=NF;i++){if($i ~ /^path=/){print $i}}}' | cut -d'=' -f2 | sort | uniq -c | sort -nr -echo "-----------------------------------------" +echo "--------------------------------------------------" +echo "🔹 Users & IPs:" +grep "$time_filter" "$LOG_FILE" | grep "REQUEST" | awk '{for(i=1;i<=NF;i++){if($i ~ /^ip=/){print $i}}}' | sort | uniq -c | sort -nr