feat(logging): add NanoShare wide event instrumentation
Build and Push Docker Container / build-and-push (push) Failing after 51s
Build and Push Docker Container / build-and-push (push) Failing after 51s
- Register quart_common wide-event logging during app setup so every HTTP request emits one canonical structured event. - Replace the inline security middleware with reusable quart_common security middleware wiring and move skip path configuration into app constants. - Add NanoShare-specific wide-event context for health checks, auth/error handlers, file list/edit/delete/serve flows and upload outcomes. - Rename runtime logging/project metadata from simple-picoshare to nanoshare where it is emitted in service context. - Update my_helpers and quart_common submodules for Convex/wide-event integration and reusable security middleware support. - Add NanoShare middleware tests covering safe user context, client IP enrichment, missing Convex handling and Convex security lookup failures.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from my_modules.decoratory.header import format_response, feature_flag_required
|
||||
from my_modules.app.setup import LIMITER
|
||||
from my_modules.app.logger import logger
|
||||
from quart_common.web.wide_event import add_wide_event_context
|
||||
|
||||
from quart import Blueprint, current_app
|
||||
|
||||
@@ -11,8 +12,10 @@ health_bp = Blueprint("health", __name__)
|
||||
@feature_flag_required("convex_health", fallback=False, status_code=404)
|
||||
@format_response
|
||||
async def get_convex_health_for_api():
|
||||
add_wide_event_context(health={"check": "convex"})
|
||||
runtime = getattr(current_app, "convex_runtime", None)
|
||||
if runtime is None:
|
||||
add_wide_event_context(health={"status": "unavailable", "reason": "runtime_missing"})
|
||||
return {
|
||||
"status": "unavailable",
|
||||
"description": "Convex runtime is not attached to app",
|
||||
@@ -20,6 +23,7 @@ async def get_convex_health_for_api():
|
||||
|
||||
is_alive = await runtime.is_alive()
|
||||
metrics = runtime.get_metrics()
|
||||
add_wide_event_context(health={"status": "ok" if is_alive else "unhealthy"})
|
||||
|
||||
return {
|
||||
"alive": is_alive,
|
||||
|
||||
@@ -12,6 +12,7 @@ from my_modules.functions import (
|
||||
get_request_context,
|
||||
)
|
||||
from quart_common.web.env import is_development_environment
|
||||
from quart_common.web.wide_event import add_wide_event_context
|
||||
|
||||
IGNORED_404_PATHS = [
|
||||
"/.well-known/",
|
||||
@@ -64,6 +65,7 @@ async def auth_error(message:str, status_code:int=400):
|
||||
'Authentication failed. Please try again or contact an administrator.'
|
||||
)
|
||||
|
||||
add_wide_event_context(auth={"operation_status": "error"}, error={"type": "AuthenticationError", "message": message})
|
||||
logger.error(f"[AUTH:{status_code}] {message}")
|
||||
|
||||
if context and context.path.startswith("/api"):
|
||||
@@ -78,6 +80,7 @@ async def auth_error(message:str, status_code:int=400):
|
||||
@app.errorhandler(401)
|
||||
async def handle_unauthorized(e):
|
||||
context = get_request_context()
|
||||
add_wide_event_context(auth={"operation_status": "unauthorized"}, error={"type": type(e).__name__, "message": str(e)})
|
||||
if context.path.startswith("/api"):
|
||||
return jsonify({"error": "Unauthorized Access", "message": "Gandalf has spoken: You shall not pass… until you log in."}), 401
|
||||
|
||||
@@ -109,6 +112,7 @@ async def not_found(e):
|
||||
path=context.path, status=404
|
||||
)
|
||||
|
||||
add_wide_event_context(error={"type": "NotFound", "message": str(e)})
|
||||
logger.error(f"[404] Page Not Found: {context.path}")
|
||||
|
||||
if context.path.startswith("/api"):
|
||||
@@ -131,6 +135,7 @@ async def maybe_a_hacker(e=None):
|
||||
method=request.method,
|
||||
path=request.path,
|
||||
)
|
||||
add_wide_event_context(security={"blocked": True, "block_reason": "honeypot_rate_limit"})
|
||||
logger.warning(f"[HONEYPOT] Blocked {client_ip} after accessing {request.path}")
|
||||
return await to_many_requests(e)
|
||||
|
||||
@@ -140,6 +145,7 @@ async def maybe_a_hacker(e=None):
|
||||
file={'name': 'hacker_crap.webp', 'alt': "Someone got Hacked and he says I hate this Hacker crap - Jurassic Park Movie"},
|
||||
)
|
||||
|
||||
add_wide_event_context(security={"blocked": True, "block_reason": "honeypot"})
|
||||
response = await make_response((rendered, 418))
|
||||
response.headers['X-Honeypot-Triggered'] = 'true'
|
||||
response.headers['X-Reason'] = 'Unauthorized access attempt'
|
||||
@@ -151,6 +157,7 @@ async def to_many_requests(e):
|
||||
message = "We love your enthusiasm, but our server thought it was being DDoSed… by you. The keyboard needs a new set of keys and we need a nap. Try again soon!"
|
||||
|
||||
context = get_request_context()
|
||||
add_wide_event_context(rate_limit={"limited": True}, error={"type": type(e).__name__, "message": str(e)})
|
||||
if context.path.startswith("/api") or context.path.endswith('/auth/userinfo') or context.path.endswith('/auth/refresh'):
|
||||
return jsonify({"error": "Too Many Requests - YOU SHALL NOT PASS (for now)", "message": message}), 429
|
||||
|
||||
@@ -168,6 +175,7 @@ async def internal_server_error(e):
|
||||
return await to_many_requests(e)
|
||||
|
||||
context = get_request_context()
|
||||
add_wide_event_context(error={"type": type(e).__name__, "message": str(e)})
|
||||
if context.path.startswith("/api"):
|
||||
return jsonify({"error": "Internal Server Error", "message": "It looks like you broke something... but don't worry, we're fixing it! In the meantime, we may or may not have logged your IP address (just kidding... or are we?). Either way, thanks for helping us find new ways to crash our system. Stay curious, hacker-friend!"}), 500
|
||||
|
||||
@@ -185,6 +193,7 @@ async def database_server_error(e):
|
||||
except LookupError as e:
|
||||
return await to_many_requests(e)
|
||||
|
||||
add_wide_event_context(error={"type": type(e).__name__, "message": str(e)})
|
||||
logger.error(e)
|
||||
return await render_template('views/basics/error.htm',
|
||||
title='Database Error',
|
||||
|
||||
@@ -3,6 +3,7 @@ from my_modules.functions import get_ip
|
||||
from my_modules.app.setup import LIMITER
|
||||
from my_modules.app.logger import logger
|
||||
from my_modules.expiry import parse_expires
|
||||
from quart_common.web.wide_event import add_wide_event_context
|
||||
|
||||
from quart import (
|
||||
Blueprint,
|
||||
@@ -40,12 +41,14 @@ async def index():
|
||||
@side_main_bp.route('/access')
|
||||
@login_required
|
||||
async def access_list(user):
|
||||
add_wide_event_context(nanoshare={"operation": "access_list"})
|
||||
access_data = await current_app.convex.get_all_access(user_id=user['sub'])
|
||||
return await render_template("views/webpage/access/list.htm", access_logs=access_data)
|
||||
|
||||
@side_main_bp.route('/files')
|
||||
@login_required
|
||||
async def files_list(user):
|
||||
add_wide_event_context(nanoshare={"operation": "files_list"})
|
||||
files_data = await current_app.convex.get_files(user_id=user['sub'])
|
||||
return await render_template("views/webpage/files/list.htm",
|
||||
files=files_data
|
||||
@@ -54,6 +57,7 @@ async def files_list(user):
|
||||
@side_main_bp.route('/files/<path:file_id>/info')
|
||||
@login_required
|
||||
async def file_info(file_id, user):
|
||||
add_wide_event_context(nanoshare={"operation": "file_info", "file_id": file_id})
|
||||
files_data = await current_app.convex.get_files(user_id=user["sub"])
|
||||
file_data = find_file(files_data, file_id)
|
||||
if not file_data:
|
||||
@@ -71,6 +75,7 @@ async def file_info(file_id, user):
|
||||
@side_main_bp.route("/files/<path:file_id>/edit")
|
||||
@login_required
|
||||
async def file_edit(file_id, user):
|
||||
add_wide_event_context(nanoshare={"operation": "file_edit", "file_id": file_id})
|
||||
file_data = await current_app.convex.get_file_informations(file_id=file_id, user_id=user["sub"])
|
||||
if not file_data:
|
||||
abort(404)
|
||||
@@ -83,6 +88,7 @@ async def file_edit(file_id, user):
|
||||
@side_main_bp.put("/api/file/<path:file_id>")
|
||||
@login_required
|
||||
async def file_edit_api(file_id, user):
|
||||
add_wide_event_context(nanoshare={"operation": "file_update", "file_id": file_id})
|
||||
files_data = await current_app.convex.get_files(user_id=user["sub"])
|
||||
if not find_file(files_data, file_id):
|
||||
return jsonify({"ok": False, "error": "File not found"}), 404
|
||||
@@ -115,6 +121,7 @@ async def file_edit_api(file_id, user):
|
||||
@side_main_bp.delete("/api/file/<path:file_id>")
|
||||
@login_required
|
||||
async def file_delete_api(file_id, user):
|
||||
add_wide_event_context(nanoshare={"operation": "file_delete", "file_id": file_id})
|
||||
files_data = await current_app.convex.get_files(user_id=user["sub"])
|
||||
if not find_file(files_data, file_id):
|
||||
return jsonify({"ok": False, "error": "File not found"}), 404
|
||||
@@ -125,10 +132,12 @@ async def file_delete_api(file_id, user):
|
||||
@side_main_bp.route("/-<file_id>")
|
||||
@LIMITER.limit("10 per minute;500 per hour;")
|
||||
async def serve_file(file_id: str):
|
||||
add_wide_event_context(nanoshare={"operation": "serve_file", "file_id": file_id})
|
||||
file_data = await current_app.convex.get_file(file_id=file_id)
|
||||
disable_logging = False
|
||||
|
||||
if not file_data:
|
||||
add_wide_event_context(nanoshare={"operation_status": "not_found"})
|
||||
abort(404)
|
||||
|
||||
user = session.get('user')
|
||||
@@ -136,6 +145,7 @@ async def serve_file(file_id: str):
|
||||
disable_logging = True
|
||||
|
||||
if file_data.get("expired", None):
|
||||
add_wide_event_context(nanoshare={"operation_status": "expired", "owner_request": disable_logging})
|
||||
if not disable_logging:
|
||||
await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="expired")
|
||||
return Response("This file has expired.", status=410, headers={
|
||||
@@ -149,10 +159,12 @@ async def serve_file(file_id: str):
|
||||
force_download = request.args.get("download") in {"1", "true", "yes"}
|
||||
|
||||
if not file_data.get('db_image_url', None):
|
||||
add_wide_event_context(nanoshare={"operation_status": "missing_storage", "owner_request": disable_logging})
|
||||
if not disable_logging:
|
||||
await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="error")
|
||||
abort(404)
|
||||
|
||||
add_wide_event_context(nanoshare={"operation_status": "served", "owner_request": disable_logging, "content_type": content_type, "download": force_download})
|
||||
if not disable_logging:
|
||||
await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="ok")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from my_modules.decoratory.header import login_required
|
||||
from my_modules.expiry import parse_expires, ensure_utc
|
||||
from my_modules.file_meta import iso_stamp_filename, format_size
|
||||
from quart_common.web.wide_event import add_wide_event_context
|
||||
|
||||
from quart import Blueprint, request, jsonify, current_app
|
||||
import asyncio, hashlib
|
||||
@@ -73,10 +74,13 @@ async def api_upload(user):
|
||||
text = form.get('text', '')
|
||||
orphan_registry = getattr(current_app, 'orphan_storage_registry', None)
|
||||
|
||||
add_wide_event_context(nanoshare={"operation": "upload", "has_file": bool(files.get('file')), "has_text": bool(text.strip())})
|
||||
|
||||
uploaded = files.get('file')
|
||||
expires_at_dt = ensure_utc(parse_expires(expires_raw))
|
||||
|
||||
if not uploaded and not text.strip():
|
||||
add_wide_event_context(nanoshare={"operation_status": "missing_content"})
|
||||
return jsonify({'ok': False, 'error': 'No content provided'}), 400
|
||||
|
||||
content_type = None
|
||||
@@ -84,6 +88,7 @@ async def api_upload(user):
|
||||
# --- binary upload path ---
|
||||
if uploaded:
|
||||
fname = uploaded.filename or ''
|
||||
add_wide_event_context(nanoshare={"upload_type": "file", "filename_present": bool(fname)})
|
||||
ctype = uploaded.mimetype or 'application/octet-stream'
|
||||
content_type = ctype
|
||||
storage_id = None
|
||||
@@ -115,6 +120,7 @@ async def api_upload(user):
|
||||
)
|
||||
if storage_id:
|
||||
reused_orphan_storage_id = True
|
||||
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
||||
else:
|
||||
storage_id, sent_size = await current_app.convex.send_stream_to_storage(stream=stream,content_type=content_type)
|
||||
size_bytes = sent_size
|
||||
@@ -128,6 +134,7 @@ async def api_upload(user):
|
||||
)
|
||||
if storage_id:
|
||||
reused_orphan_storage_id = True
|
||||
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
||||
else:
|
||||
storage_id = await current_app.convex.send_to_storage(data=data, content_type=content_type)
|
||||
size_bytes = len(data)
|
||||
@@ -151,6 +158,7 @@ async def api_upload(user):
|
||||
|
||||
# --- text upload path ---
|
||||
elif text.strip():
|
||||
add_wide_event_context(nanoshare={"upload_type": "text"})
|
||||
data = text.encode('utf-8')
|
||||
fname = iso_stamp_filename('pasted', 'txt')
|
||||
fingerprint = fingerprint_bytes(data)
|
||||
@@ -160,6 +168,8 @@ async def api_upload(user):
|
||||
else None
|
||||
)
|
||||
reused_orphan_storage_id = bool(storage_id)
|
||||
if reused_orphan_storage_id:
|
||||
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
||||
if not storage_id:
|
||||
storage_id = await current_app.convex.send_to_storage(
|
||||
data=data, content_type='text/plain'
|
||||
@@ -183,4 +193,5 @@ async def api_upload(user):
|
||||
await orphan_registry.remember(user['sub'], fingerprint, storage_id)
|
||||
raise
|
||||
|
||||
add_wide_event_context(nanoshare={"operation_status": "uploaded", "content_type": content_type})
|
||||
return jsonify({'ok': True})
|
||||
|
||||
Reference in New Issue
Block a user