Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,103 @@
|
||||
"""Structured JSON logging (stdlib only).
|
||||
|
||||
Logging policy: log lines carry metadata only — never
|
||||
prompts, LLM responses, user messages or document text. Exceptions are
|
||||
reduced to their type plus a sanitized message; SQLAlchemy statement/param
|
||||
dumps are stripped because parameters can contain user content.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from contextvars import ContextVar
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)
|
||||
conversation_id: ContextVar[str | None] = ContextVar("conversation_id", default=None)
|
||||
|
||||
# LogRecord attributes that are not user-supplied extras.
|
||||
_STANDARD_ATTRS = frozenset(
|
||||
{
|
||||
"args",
|
||||
"asctime",
|
||||
"created",
|
||||
"exc_info",
|
||||
"exc_text",
|
||||
"filename",
|
||||
"funcName",
|
||||
"levelname",
|
||||
"levelno",
|
||||
"lineno",
|
||||
"message",
|
||||
"module",
|
||||
"msecs",
|
||||
"msg",
|
||||
"name",
|
||||
"pathname",
|
||||
"process",
|
||||
"processName",
|
||||
"relativeCreated",
|
||||
"stack_info",
|
||||
"taskName",
|
||||
"thread",
|
||||
"threadName",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def safe_error(exc: BaseException, limit: int = 300) -> str:
|
||||
"""Exception text that is safe to log or persist (no content leaks).
|
||||
|
||||
SQLAlchemy appends "[SQL: ...] [parameters: (...)]" to its messages;
|
||||
parameters can contain user content, so everything from "[SQL" on is cut.
|
||||
"""
|
||||
text = str(exc).split("[SQL", 1)[0].strip()
|
||||
return f"{type(exc).__name__}: {text[:limit]}"
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload: dict[str, Any] = {
|
||||
"ts": datetime.fromtimestamp(record.created, tz=UTC).isoformat(
|
||||
timespec="milliseconds"
|
||||
),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
cid = correlation_id.get()
|
||||
if cid:
|
||||
payload["correlation_id"] = cid
|
||||
conv = conversation_id.get()
|
||||
if conv:
|
||||
payload["conversation_id"] = conv
|
||||
for key, value in record.__dict__.items():
|
||||
if key not in _STANDARD_ATTRS and not key.startswith("_"):
|
||||
payload[key] = value
|
||||
if record.exc_info and record.exc_info[1] is not None:
|
||||
payload["error"] = safe_error(record.exc_info[1])
|
||||
return json.dumps(payload, default=str)
|
||||
|
||||
|
||||
# These third-party loggers dump request/response bodies at DEBUG — with
|
||||
# prompts and user content in them, so they stay capped at INFO unless
|
||||
# content debug logging is explicitly enabled.
|
||||
_CONTENT_DEBUG_LOGGERS = ("openai", "httpx", "httpcore")
|
||||
|
||||
|
||||
def apply_content_log_guard() -> None:
|
||||
level = logging.DEBUG if get_settings().debug_log_prompts else logging.INFO
|
||||
for name in _CONTENT_DEBUG_LOGGERS:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(JsonFormatter())
|
||||
root = logging.getLogger()
|
||||
root.handlers = [handler]
|
||||
root.setLevel(get_settings().log_level.upper())
|
||||
apply_content_log_guard()
|
||||
Reference in New Issue
Block a user