17_log/server.py
"""Server that handles file uploads with HTMX, enhanced with structured logging."""
from flask import Flask, render_template, request
from flask_cors import CORS
import os
from pathlib import Path
from werkzeug.utils import secure_filename
import structlog
# Simple in-memory log storage (no thread safety needed for single-threaded mode)
LOGS = []
# Custom processor to store logs in memory
def store_log_processor(logger, method_name, event_dict):
"""Store log entries in the LOGS list."""
log_entry = event_dict.copy()
log_entry['level'] = method_name
LOGS.append(log_entry)
return event_dict
# Configure structlog with our custom processor
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
store_log_processor,
structlog.processors.JSONRenderer()
],
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
# Create logger
logger = structlog.get_logger()
# Configure upload folder
UPLOAD_FOLDER = 'uploads'
def get_request_logger():
"""Create a logger with request context."""
return logger.bind(
user_agent=request.headers.get('User-Agent'),
ip_address=request.remote_addr
)
def get_uploaded_files():
"""Get list of uploaded files."""
return os.listdir(UPLOAD_FOLDER) if os.path.exists(UPLOAD_FOLDER) else []
def create_app():
"""Build application and configure routes."""
app = Flask(
"upload_server",
static_folder=Path("../static").absolute(),
static_url_path="/static"
)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
CORS(app)
# Create upload folder if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@app.before_request
def log_request():
"""Log all incoming requests."""
if not request.path.startswith('/static'):
logger.info("request",
method=request.method,
path=request.path,
ip=request.remote_addr,
user_agent=request.headers.get('User-Agent'))
@app.route('/')
def index():
"""Render the upload form."""
# Log page visit with context
get_request_logger().info("page_view", page="index")
files = get_uploaded_files()
return render_template('index.html', files=files)
@app.route('/upload', methods=['POST'])
def upload_file():
"""Handle file upload."""
req_logger = get_request_logger()
if 'file' not in request.files:
req_logger.warning("upload_failed", reason="no_file_in_request")
return "No file selected", 400
file = request.files['file']
if file.filename == '':
req_logger.warning("upload_failed", reason="empty_filename")
return "No file selected", 400
filename = secure_filename(file.filename)
try:
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
file_size = os.path.getsize(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# Log successful upload with metadata
req_logger.info("file_uploaded",
filename=filename,
file_size_bytes=file_size,
content_type=file.content_type)
# Return a success message that HTMX will use to update the UI
return render_template('file_item.html', filename=filename)
except Exception as e:
# Log error with exception details
req_logger.exception("upload_error",
filename=filename,
error_type=type(e).__name__,
error_msg=str(e))
return "Upload failed", 500
@app.route('/files')
def list_files():
"""List uploaded files."""
# Log files listing request
get_request_logger().info("files_listed")
files = get_uploaded_files()
return render_template('file_list.html', files=files)
@app.route('/delete/<filename>', methods=['DELETE'])
def delete_file(filename):
"""Delete an uploaded file."""
secure_name = secure_filename(filename)
filepath = os.path.join(UPLOAD_FOLDER, secure_name)
log_ctx = {'filename': secure_name}
req_logger = get_request_logger()
if not os.path.exists(filepath):
req_logger.warning("delete_failed", reason="file_not_found", **log_ctx)
return "File not found", 404
try:
file_size = os.path.getsize(filepath)
os.remove(filepath)
req_logger.info("file_deleted", file_size_bytes=file_size, **log_ctx)
return "", 200
except Exception as e:
req_logger.exception("delete_error",
error_type=type(e).__name__,
error_msg=str(e),
**log_ctx)
return "Delete failed", 500
@app.route('/logs')
def view_logs():
"""View recent logs."""
try:
# Log the view logs request
get_request_logger().info("logs_viewed")
# Get logs from our in-memory storage and sort by timestamp (newest first)
recent_logs = sorted(LOGS.copy(), key=lambda x: x.get('timestamp', ''), reverse=True)
return render_template('logs.html', logs=recent_logs)
except Exception as e:
get_request_logger().exception("logs_view_error",
error_type=type(e).__name__,
error_msg=str(e))
return "Error fetching logs", 500
return app
if __name__ == "__main__":
# Log application startup
logger.info("application_startup",
upload_folder=UPLOAD_FOLDER,
max_content_length=16*1024*1024)
app = create_app()
# Run in single-threaded mode
app.run(debug=True, threaded=False)