MemPrepMate / src /app.py
Christian Kniep
'add more loggging v7'
e01ff01
"""
Flask application entry point.
Feature: 012-profile-contact-ui
"""
import json
import logging
import os
import time
from datetime import timedelta
from flask import Flask, jsonify, request, g
from flask_session import Session
from dotenv import load_dotenv
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from .services.auth_service import auth_service
from .services.storage_service import init_db
from .utils.tracing import init_tracer
# Load environment variables
load_dotenv()
class JSONFormatter(logging.Formatter):
"""Custom JSON formatter for structured logging."""
def format(self, record):
"""Format log record as JSON."""
log_obj = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"module": record.module,
"message": record.getMessage(),
}
# Add exception info if present
if record.exc_info:
log_obj["exception"] = self.formatException(record.exc_info)
# Add extra fields if present
if hasattr(record, "request_id"):
log_obj["request_id"] = record.request_id
if hasattr(record, "user_id"):
log_obj["user_id"] = record.user_id
if hasattr(record, "duration_ms"):
log_obj["duration_ms"] = record.duration_ms
if hasattr(record, "backend_latency_ms"):
log_obj["backend_latency_ms"] = record.backend_latency_ms
if hasattr(record, "status_code"):
log_obj["status_code"] = record.status_code
if hasattr(record, "method"):
log_obj["method"] = record.method
if hasattr(record, "path"):
log_obj["path"] = record.path
return json.dumps(log_obj)
def create_app():
"""Create and configure Flask application."""
app = Flask(__name__)
# Configuration
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-key-change-me")
# CRITICAL: Use server-side sessions to avoid cookie size limits (4KB)
# OAuth state tokens make cookies exceed size limit, causing state to be lost
app.config["SESSION_TYPE"] = "filesystem" # Store sessions server-side
app.config["SESSION_FILE_DIR"] = "/app/data/flask_sessions" # Session storage directory
app.config["SESSION_PERMANENT"] = False # Default to non-permanent (override per-session)
app.config["SESSION_USE_SIGNER"] = True # Sign session cookie for security
# Session cookie configuration
# For HTTPS (HF Spaces): Set SESSION_COOKIE_SECURE=true
secure_cookie = os.getenv("SESSION_COOKIE_SECURE", "False").lower() == "true"
app.config["SESSION_COOKIE_SECURE"] = secure_cookie
app.config["SESSION_COOKIE_HTTPONLY"] = (
os.getenv("SESSION_COOKIE_HTTPONLY", "True").lower() == "true"
)
# Use "None" for cross-site OAuth (HF Spaces), "Lax" for same-site (local dev)
samesite_value = os.getenv("SESSION_COOKIE_SAMESITE", "Lax")
app.config["SESSION_COOKIE_SAMESITE"] = None if samesite_value == "None" else samesite_value
app.config["SESSION_COOKIE_NAME"] = "prepmate_session" # Explicit session cookie name
# Don't set SESSION_COOKIE_DOMAIN - let Flask handle it automatically
app.config["SESSION_COOKIE_PATH"] = "/" # Ensure cookie is valid for all paths
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(
seconds=int(os.getenv("PERMANENT_SESSION_LIFETIME", "2592000"))
)
# CRITICAL: Log ALL environment variables for OAuth debugging
print("="*80)
print("[CONFIG] ENVIRONMENT VARIABLES CHECK")
print(f"[CONFIG] SESSION_COOKIE_SECURE env var: {os.getenv('SESSION_COOKIE_SECURE', 'NOT SET')}")
print(f"[CONFIG] SESSION_COOKIE_SAMESITE env var: {os.getenv('SESSION_COOKIE_SAMESITE', 'NOT SET')}")
print(f"[CONFIG] PREFERRED_URL_SCHEME env var: {os.getenv('PREFERRED_URL_SCHEME', 'NOT SET')}")
print(f"[CONFIG] SECRET_KEY env var: {'SET' if os.getenv('SECRET_KEY') else 'NOT SET'}")
print("-"*80)
print("[CONFIG] FLASK CONFIG VALUES:")
print(f"[CONFIG] SESSION_COOKIE_SECURE={secure_cookie}")
print(f"[CONFIG] SESSION_COOKIE_SAMESITE={app.config['SESSION_COOKIE_SAMESITE']}")
print(f"[CONFIG] SESSION_COOKIE_NAME={app.config['SESSION_COOKIE_NAME']}")
print(f"[CONFIG] PERMANENT_SESSION_LIFETIME={app.config['PERMANENT_SESSION_LIFETIME']}")
print("="*80)
# FAIL LOUDLY if OAuth won't work
if not secure_cookie or app.config['SESSION_COOKIE_SAMESITE'] != None:
print("⚠️ WARNING: OAuth will FAIL - cookie won't persist!")
print("⚠️ Set these in HuggingFace Space Settings → Variables:")
print(" SESSION_COOKIE_SECURE=true")
print(" SESSION_COOKIE_SAMESITE=None")
# Authlib configuration for OAuth state management
app.config["AUTHLIB_INSECURE_TRANSPORT"] = os.getenv("FLASK_ENV") == "development"
# Trust proxy headers for HTTPS detection (needed for HuggingFace Spaces)
app.config["PREFERRED_URL_SCHEME"] = os.getenv("PREFERRED_URL_SCHEME", "http")
# Configure ProxyFix middleware to trust X-Forwarded-* headers
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
# Initialize database
init_db()
# Initialize Flask-Session (must be BEFORE OAuth init)
# This stores sessions server-side to avoid 4KB cookie size limit
print("="*80)
print("[FLASK-SESSION] INITIALIZING SERVER-SIDE SESSIONS")
print(f"[FLASK-SESSION] SESSION_TYPE: {app.config.get('SESSION_TYPE')}")
print(f"[FLASK-SESSION] SESSION_FILE_DIR: {app.config.get('SESSION_FILE_DIR')}")
print(f"[FLASK-SESSION] SESSION_PERMANENT: {app.config.get('SESSION_PERMANENT')}")
print(f"[FLASK-SESSION] SESSION_USE_SIGNER: {app.config.get('SESSION_USE_SIGNER')}")
# Check if session directory exists
session_dir = app.config.get('SESSION_FILE_DIR')
if session_dir and os.path.exists(session_dir):
print(f"[FLASK-SESSION] Session directory exists: {session_dir}")
print(f"[FLASK-SESSION] Directory writable: {os.access(session_dir, os.W_OK)}")
else:
print(f"[FLASK-SESSION] ⚠️ WARNING: Session directory does NOT exist: {session_dir}")
Session(app)
# Verify Flask-Session is active
print(f"[FLASK-SESSION] Session interface type: {type(app.session_interface).__name__}")
print(f"[FLASK-SESSION] Session interface module: {type(app.session_interface).__module__}")
if 'flask_session' in type(app.session_interface).__module__:
print("[FLASK-SESSION] ✅ Server-side sessions ACTIVE")
else:
print("[FLASK-SESSION] ❌ ERROR: Still using client-side sessions!")
print("="*80)
# Initialize OAuth
auth_service.init_app(app)
# Initialize Jaeger tracing
init_tracer("prepmate-webapp")
# Configure logging
setup_logging(app)
# Register middleware
register_middleware(app)
# Register blueprints
from .routes import auth, profile, contacts, settings
app.register_blueprint(auth.bp)
app.register_blueprint(profile.bp)
app.register_blueprint(contacts.contacts_bp)
app.register_blueprint(settings.bp)
# Root route - redirect to login
@app.route("/")
def index():
"""Root route - redirect to login page."""
from flask import session, redirect, url_for, render_template
if "user_id" in session:
return redirect(url_for("profile.view_profile"))
return render_template("login.html")
# Health check endpoint
@app.route("/health")
def health():
"""Health check endpoint for monitoring."""
return jsonify({"status": "ok"}), 200
# Diagnostic endpoint to check environment variables
@app.route("/debug/config")
def debug_config():
"""Show current configuration for debugging."""
return jsonify({
"environment_variables": {
"SESSION_COOKIE_SECURE": os.getenv("SESSION_COOKIE_SECURE", "NOT SET"),
"SESSION_COOKIE_SAMESITE": os.getenv("SESSION_COOKIE_SAMESITE", "NOT SET"),
"PREFERRED_URL_SCHEME": os.getenv("PREFERRED_URL_SCHEME", "NOT SET"),
"SECRET_KEY": "SET" if os.getenv("SECRET_KEY") else "NOT SET",
},
"flask_config": {
"SESSION_COOKIE_SECURE": app.config.get("SESSION_COOKIE_SECURE"),
"SESSION_COOKIE_SAMESITE": str(app.config.get("SESSION_COOKIE_SAMESITE")),
"SESSION_COOKIE_NAME": app.config.get("SESSION_COOKIE_NAME"),
"SESSION_COOKIE_PATH": app.config.get("SESSION_COOKIE_PATH"),
"PREFERRED_URL_SCHEME": app.config.get("PREFERRED_URL_SCHEME"),
}
}), 200
return app
def setup_logging(app):
"""Configure structured JSON logging."""
log_level = os.getenv("LOG_LEVEL", "INFO")
# Create handler with JSON formatter
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
# Configure root logger
logging.basicConfig(
level=getattr(logging, log_level),
handlers=[handler],
)
# Configure app logger
app.logger.handlers = [handler]
app.logger.setLevel(getattr(logging, log_level))
def register_middleware(app):
"""Register Flask middleware for request logging and timing."""
@app.before_request
def before_request():
"""Start request timer and generate request ID, create tracing span."""
from .utils.tracing import is_tracing_enabled
g.start_time = time.time()
g.request_id = request.headers.get("X-Request-ID", os.urandom(8).hex())
# Skip tracing if disabled or for health endpoint
if not is_tracing_enabled() or request.path == "/health":
return
# Extract parent span context from headers if present
try:
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
except Exception:
ctx = None
# Start a new span for this request
tracer = trace.get_tracer(__name__)
span = tracer.start_span(
name=f"{request.method} {request.path}",
context=ctx,
)
span.set_attribute("http.method", request.method)
span.set_attribute("http.url", request.url)
span.set_attribute("request_id", g.request_id)
g.span = span
@app.after_request
def after_request(response):
"""Log request completion with duration and finish span."""
if hasattr(g, "start_time"):
duration_ms = (time.time() - g.start_time) * 1000
# Skip logging for Streamlit internal requests unless in debug mode
is_streamlit_internal = request.path.startswith("/_stcore")
is_debug_mode = app.logger.level == logging.DEBUG
if not is_streamlit_internal or is_debug_mode:
# Get user_id from session if available
user_id = None
try:
from flask import session
user_id = session.get("user_id")
except Exception:
pass
# Log request with structured data
extra = {
"request_id": g.request_id,
"duration_ms": round(duration_ms, 2),
"status_code": response.status_code,
"method": request.method,
"path": request.path,
}
if user_id:
extra["user_id"] = user_id
if hasattr(g, "backend_latency_ms"):
extra["backend_latency_ms"] = round(g.backend_latency_ms, 2)
app.logger.info(
f"{request.method} {request.path} {response.status_code}",
extra=extra,
)
# Finish tracing span
if hasattr(g, "span"):
span = g.span
span.set_attribute("http.status_code", response.status_code)
if response.status_code >= 400:
span.set_status(Status(StatusCode.ERROR))
if hasattr(g, "backend_latency_ms"):
span.set_attribute("backend_latency_ms", round(g.backend_latency_ms, 2))
span.end()
return response
# Session debugging and cookie SameSite=None fix
@app.after_request
def debug_and_fix_session_cookie(response):
"""Debug session cookie setting and force SameSite=None if needed."""
from flask import session
# CRITICAL FIX: Force Flask-Session to set cookie when session is modified
# Flask-Session only sets cookies for new sessions or when session.permanent changes
# But we need it to ALWAYS set a cookie when session is modified to replace old client-side cookies
if session.modified and not response.headers.get('Set-Cookie'):
# Manually call save_session to force cookie setting
app.session_interface.save_session(app, session, response)
# ALWAYS log, even if session is empty
print(f"[AFTER-REQUEST] Path: {request.path}, Status: {response.status_code}")
print(f"[AFTER-REQUEST] Session modified: {session.modified}, Has data: {bool(session)}, Keys: {list(session.keys())}")
print(f"[AFTER-REQUEST] Session interface: {type(app.session_interface).__name__}")
# Log session state and response cookies
set_cookie_headers = response.headers.getlist('Set-Cookie')
has_session_cookie = any('prepmate_session' in cookie for cookie in set_cookie_headers)
print(f"[AFTER-REQUEST] Set-Cookie count: {len(set_cookie_headers)}")
print(f"[AFTER-REQUEST] Has prepmate_session cookie: {has_session_cookie}")
print(f"[AFTER-REQUEST] All Set-Cookie headers: {set_cookie_headers}")
if has_session_cookie:
# Check if cookie looks like session ID (server-side) or full session (client-side)
for cookie in set_cookie_headers:
if 'prepmate_session' in cookie:
# Extract cookie value (between = and ;)
cookie_value = cookie.split('=')[1].split(';')[0] if '=' in cookie else ''
print(f"[AFTER-REQUEST] Cookie value length: {len(cookie_value)} chars")
print(f"[AFTER-REQUEST] Cookie value preview: {cookie_value[:50]}...")
if cookie_value.startswith('.'):
print("[AFTER-REQUEST] ⚠️ Cookie looks like CLIENT-SIDE session (starts with '.')")
elif len(cookie_value) < 100:
print("[AFTER-REQUEST] ✅ Cookie looks like SERVER-SIDE session ID (short)")
else:
print("[AFTER-REQUEST] ⚠️ Cookie looks like CLIENT-SIDE session (long)")
# CRITICAL FIX: Flask's session interface doesn't properly set SameSite=None and Partitioned
# Manually fix any prepmate_session cookies that are missing these attributes
print("[AFTER-REQUEST] Fixing SameSite=None and Partitioned for session cookie")
new_cookies = []
for cookie in set_cookie_headers:
if 'prepmate_session' in cookie:
# Check if SameSite=None is missing
if 'SameSite=None' not in cookie:
cookie = cookie.rstrip(';') + '; SameSite=None'
print(f"[AFTER-REQUEST] Added SameSite=None to session cookie")
# Check if Partitioned is missing (required for CHIPS - Cookies Having Independent Partitioned State)
if 'Partitioned' not in cookie:
cookie = cookie.rstrip(';') + '; Partitioned'
print(f"[AFTER-REQUEST] Added Partitioned to session cookie")
new_cookies.append(cookie)
else:
# Keep non-session cookies unchanged
new_cookies.append(cookie)
# Replace all Set-Cookie headers
response.headers.remove('Set-Cookie')
for cookie in new_cookies:
response.headers.add('Set-Cookie', cookie)
print(f"[AFTER-REQUEST] Final Set-Cookie headers: {response.headers.getlist('Set-Cookie')}")
else:
print("[AFTER-REQUEST] ⚠️ WARNING: No session cookie found in response!")
print(f"[AFTER-REQUEST] This means Flask-Session did not set a cookie")
print(f"[AFTER-REQUEST] Session was modified: {session.modified}")
print(f"[AFTER-REQUEST] Session is new: {getattr(session, 'new', 'N/A')}")
return response
# Create app instance
app = create_app()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "5000")), debug=True)