Updated Files
هذا الالتزام موجود في:
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -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"]
|
128
README.md
128
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/<id>` → 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/<id>` → Update an existing fruit.
|
||||
- `DELETE /fruits/<id>` → Delete a fruit.
|
||||
|
||||
### Advanced Queries
|
||||
- `GET /fruits/category/<category>` → Get fruits by category.
|
||||
- `GET /fruits/search?name=<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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
@@ -0,0 +1,56 @@
|
||||
2025-09-27 23:51:46,899 [INFO] [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||
* 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] [33mPress CTRL+C to quit[0m
|
||||
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] "[33mGET /fruits/35520 HTTP/1.1[0m" 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] "[33mGET /fruits/88 HTTP/1.1[0m" 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 -
|
||||
|
158
fruit_api.py
158
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/<id> - Get fruit by ID",
|
||||
"POST /fruits - Add new fruit",
|
||||
"PUT /fruits/<id> - Update fruit",
|
||||
"DELETE /fruits/<id> - Delete fruit",
|
||||
"GET /fruits/category/<category> - Get fruits by category",
|
||||
"GET /fruits/search?name=<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/<id> - Get specific fruit
|
||||
@app.route('/fruits/<int:fruit_id>', 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/<id> - Update fruit
|
||||
@app.route('/fruits/<int:fruit_id>', 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/<id> - Delete fruit
|
||||
@app.route('/fruits/<int:fruit_id>', 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/<category> - Get fruits by category
|
||||
@app.route('/fruits/category/<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/<id> - Fruit by ID")
|
||||
print("4. POST /fruits - Create fruit")
|
||||
print("5. PUT /fruits/<id> - Update fruit")
|
||||
print("6. DELETE /fruits/<id> - Delete fruit")
|
||||
print("7. GET /fruits/category/<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)
|
||||
app.run(debug=False, host='0.0.0.0', port=5000)
|
||||
|
@@ -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
|
||||
|
المرجع في مشكلة جديدة
حظر مستخدم