better app logs

This commit is contained in:
Gamosoft 2026-06-30 16:13:01 +02:00
parent 6ddb533fe6
commit f2d565e7d9
2 changed files with 32 additions and 24 deletions

View File

@ -12,12 +12,15 @@ from starlette.middleware.sessions import SessionMiddleware
import os import os
import yaml import yaml
import json import json
import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
import aiofiles import aiofiles
from datetime import datetime from datetime import datetime
import bcrypt import bcrypt
import secrets import secrets
logger = logging.getLogger("uvicorn.error")
from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
@ -79,9 +82,9 @@ with open(version_path, 'r', encoding='utf-8') as f:
if 'AUTHENTICATION_ENABLED' in os.environ: if 'AUTHENTICATION_ENABLED' in os.environ:
auth_enabled = os.getenv('AUTHENTICATION_ENABLED', 'false').lower() in ('true', '1', 'yes') auth_enabled = os.getenv('AUTHENTICATION_ENABLED', 'false').lower() in ('true', '1', 'yes')
config['authentication']['enabled'] = auth_enabled config['authentication']['enabled'] = auth_enabled
print(f"🔐 Authentication {'ENABLED' if auth_enabled else 'DISABLED'} (from AUTHENTICATION_ENABLED env var)") logger.info("Authentication %s (from AUTHENTICATION_ENABLED env var)", 'ENABLED' if auth_enabled else 'DISABLED')
else: else:
print(f"🔐 Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)") logger.info("Authentication %s (from config.yaml)", 'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED')
# Password configuration priority: # Password configuration priority:
# 1. AUTHENTICATION_PASSWORD env var (hashed at startup) # 1. AUTHENTICATION_PASSWORD env var (hashed at startup)
@ -94,9 +97,9 @@ if 'AUTHENTICATION_PASSWORD' in os.environ:
plain_password.encode('utf-8'), plain_password.encode('utf-8'),
bcrypt.gensalt() bcrypt.gensalt()
).decode('utf-8') ).decode('utf-8')
print("🔑 Password loaded from AUTHENTICATION_PASSWORD env var") logger.info("Password loaded from AUTHENTICATION_PASSWORD env var")
else: else:
print("⚠️ WARNING: AUTHENTICATION_PASSWORD env var is empty - ignoring") logger.warning("AUTHENTICATION_PASSWORD env var is empty - ignoring")
elif config.get('authentication', {}).get('password', '').strip(): elif config.get('authentication', {}).get('password', '').strip():
plain_password = config['authentication']['password'].strip() plain_password = config['authentication']['password'].strip()
config['authentication']['password_hash'] = bcrypt.hashpw( config['authentication']['password_hash'] = bcrypt.hashpw(
@ -104,12 +107,12 @@ elif config.get('authentication', {}).get('password', '').strip():
bcrypt.gensalt() bcrypt.gensalt()
).decode('utf-8') ).decode('utf-8')
del config['authentication']['password'] del config['authentication']['password']
print("🔑 Password loaded from config.yaml") logger.info("Password loaded from config.yaml")
# Allow secret key to be set via environment variable (for session security) # Allow secret key to be set via environment variable (for session security)
if 'AUTHENTICATION_SECRET_KEY' in os.environ: if 'AUTHENTICATION_SECRET_KEY' in os.environ:
config['authentication']['secret_key'] = os.getenv('AUTHENTICATION_SECRET_KEY') config['authentication']['secret_key'] = os.getenv('AUTHENTICATION_SECRET_KEY')
print("🔐 Secret key loaded from AUTHENTICATION_SECRET_KEY env var") logger.info("Secret key loaded from AUTHENTICATION_SECRET_KEY env var")
# API key configuration for external integrations (MCP servers, scripts, etc.) # API key configuration for external integrations (MCP servers, scripts, etc.)
# Priority: AUTHENTICATION_API_KEY env var > authentication.api_key in config.yaml # Priority: AUTHENTICATION_API_KEY env var > authentication.api_key in config.yaml
@ -117,11 +120,11 @@ if 'AUTHENTICATION_API_KEY' in os.environ:
api_key_value = os.getenv('AUTHENTICATION_API_KEY', '').strip() api_key_value = os.getenv('AUTHENTICATION_API_KEY', '').strip()
if api_key_value: if api_key_value:
config['authentication']['api_key'] = api_key_value config['authentication']['api_key'] = api_key_value
print("🔑 API key loaded from AUTHENTICATION_API_KEY env var") logger.info("API key loaded from AUTHENTICATION_API_KEY env var")
else: else:
config['authentication']['api_key'] = '' config['authentication']['api_key'] = ''
elif config.get('authentication', {}).get('api_key', '').strip(): elif config.get('authentication', {}).get('api_key', '').strip():
print("🔑 API key loaded from config.yaml") logger.info("API key loaded from config.yaml")
else: else:
config['authentication']['api_key'] = '' config['authentication']['api_key'] = ''
@ -133,15 +136,15 @@ if config.get('authentication', {}).get('enabled', False):
_is_default_secret = _secret_key in ('', 'change_this_to_a_random_secret_key_in_production') _is_default_secret = _secret_key in ('', 'change_this_to_a_random_secret_key_in_production')
if not _has_password and not _has_api_key: if not _has_password and not _has_api_key:
print("🚨 CRITICAL: Authentication enabled but NO auth methods configured - ALL access will be denied!") logger.critical("Authentication enabled but NO auth methods configured - ALL access will be denied!")
else: else:
if not _has_password: if not _has_password:
print("⚠️ WARNING: No password configured - web UI login will not work") logger.warning("No password configured - web UI login will not work")
if not _has_api_key: if not _has_api_key:
print("⚠️ WARNING: No API key configured - external integrations will require session cookies") logger.warning("No API key configured - external integrations will require session cookies")
if _is_default_secret: if _is_default_secret:
print("🚨 SECURITY WARNING: Using default secret_key - sessions can be forged! Change it in config.yaml") logger.critical("Using default secret_key - sessions can be forged! Change it in config.yaml")
# OpenAPI tag metadata for grouping endpoints in Swagger UI # OpenAPI tag metadata for grouping endpoints in Swagger UI
tags_metadata = [ tags_metadata = [
@ -178,7 +181,7 @@ app.add_middleware(
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
print(f"🌐 CORS allowed origins: {allowed_origins}") logger.info("CORS allowed origins: %s", allowed_origins)
# =========================================================== # ===========================================================
# ================= # =================
@ -201,7 +204,7 @@ def safe_error_message(error: Exception, user_message: str = "An error occurred"
error_details = f"{type(error).__name__}: {str(error)}" error_details = f"{type(error).__name__}: {str(error)}"
# Always log the full error server-side # Always log the full error server-side
print(f"⚠️ [ERROR] {error_details}") logger.error(error_details)
# In debug mode, return detailed error to help with development # In debug mode, return detailed error to help with development
if config.get('server', {}).get('debug', False): if config.get('server', {}).get('debug', False):
@ -247,7 +250,7 @@ if DEMO_MODE:
limiter = Limiter(key_func=get_remote_address, default_limits=["200/hour"]) limiter = Limiter(key_func=get_remote_address, default_limits=["200/hour"])
app.state.limiter = limiter app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
print("🎭 DEMO MODE enabled - Rate limiting active") logger.info("DEMO MODE enabled - Rate limiting active")
else: else:
# Production/self-hosted mode - no restrictions # Production/self-hosted mode - no restrictions
# Create a dummy limiter that doesn't actually limit # Create a dummy limiter that doesn't actually limit
@ -416,7 +419,7 @@ def verify_password(password: str) -> bool:
try: try:
return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')) return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
except Exception as e: except Exception as e:
print(f"Password verification error: {e}") logger.error("Password verification error: %s", e)
return False return False
@ -1832,13 +1835,15 @@ app.include_router(pages_router)
@app.on_event("startup") @app.on_event("startup")
def _warmup_note_index() -> None: def _warmup_note_index() -> None:
import threading import threading
import time
def _build() -> None: def _build() -> None:
t0 = time.perf_counter()
try: try:
ensure_index_built(config['storage']['notes_dir']) ensure_index_built(config['storage']['notes_dir'])
print("✅ Note index warmed up") logger.info("Note index ready (%.2fs)", time.perf_counter() - t0)
except Exception as exc: except Exception as exc:
print(f"⚠️ Note index warmup failed (will build on first request): {exc}") logger.warning("Note index build failed (will retry on first request): %s", exc)
threading.Thread(target=_build, name="note-index-warmup", daemon=True).start() threading.Thread(target=_build, name="note-index-warmup", daemon=True).start()

View File

@ -5,10 +5,13 @@ Plugins can hook into events like note save, delete, etc.
import os import os
import json import json
import logging
import importlib.util import importlib.util
from pathlib import Path from pathlib import Path
from typing import List, Dict, Callable from typing import List, Dict, Callable
logger = logging.getLogger("uvicorn.error")
class Plugin: class Plugin:
"""Base plugin class""" """Base plugin class"""
@ -113,7 +116,7 @@ class PluginManager:
plugin = module.Plugin() plugin = module.Plugin()
self.plugins[plugin_file.stem] = plugin self.plugins[plugin_file.stem] = plugin
except Exception as e: except Exception as e:
print(f"Failed to load plugin {plugin_file.stem}: {e}") logger.error("Failed to load plugin %s: %s", plugin_file.stem, e)
def _create_example_plugin(self): def _create_example_plugin(self):
"""Create an example plugin to show developers how to build plugins""" """Create an example plugin to show developers how to build plugins"""
@ -167,7 +170,7 @@ class Plugin:
with open(self.config_file, 'r', encoding='utf-8') as f: with open(self.config_file, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
print(f"Failed to load plugin config: {e}") logger.error("Failed to load plugin config: %s", e)
return {} return {}
def _save_config(self): def _save_config(self):
@ -180,7 +183,7 @@ class Plugin:
with open(self.config_file, 'w', encoding='utf-8') as f: with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2) json.dump(config, f, indent=2)
except Exception as e: except Exception as e:
print(f"Failed to save plugin config: {e}") logger.error("Failed to save plugin config: %s", e)
def _apply_saved_state(self): def _apply_saved_state(self):
"""Apply saved plugin states after loading plugins""" """Apply saved plugin states after loading plugins"""
@ -188,7 +191,7 @@ class Plugin:
for plugin_id, enabled in saved_config.items(): for plugin_id, enabled in saved_config.items():
if plugin_id in self.plugins: if plugin_id in self.plugins:
self.plugins[plugin_id].enabled = enabled self.plugins[plugin_id].enabled = enabled
print(f"Plugin '{plugin_id}': {'enabled' if enabled else 'disabled'} (from config)") logger.info("Plugin '%s': %s (from config)", plugin_id, 'enabled' if enabled else 'disabled')
def enable_plugin(self, plugin_id: str): def enable_plugin(self, plugin_id: str):
"""Enable a plugin and persist the state""" """Enable a plugin and persist the state"""
@ -238,7 +241,7 @@ class Plugin:
method(**kwargs) method(**kwargs)
except Exception as e: except Exception as e:
print(f"Plugin {plugin.name} error in {hook_name}: {e}") logger.error("Plugin %s error in %s: %s", plugin.name, hook_name, e)
return result if 'content' in kwargs else None return result if 'content' in kwargs else None
@ -266,7 +269,7 @@ class Plugin:
if 'initial_content' in kwargs and result is not None: if 'initial_content' in kwargs and result is not None:
kwargs['initial_content'] = result kwargs['initial_content'] = result
except Exception as e: except Exception as e:
print(f"Plugin {plugin.name} error in {hook_name}: {e}") logger.error("Plugin %s error in %s: %s", plugin.name, hook_name, e)
# Return the final modified value # Return the final modified value
return kwargs.get('initial_content', '') return kwargs.get('initial_content', '')