File size: 2,124 Bytes
087643a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# Structured JSON logs, one line per request - lets me grep/jq for stuff
# later ("how many /ask requests hit the guardrail today") instead of
# re-running things to find out.
from __future__ import annotations

import json
import logging
import sys
import time
import uuid
from contextlib import contextmanager


class _JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "level": record.levelname,
            "message": record.getMessage(),
        }
        # extra={...} fields ride along here instead of needing a custom
        # formatter per call site
        for key, value in getattr(record, "extra_fields", {}).items():
            payload[key] = value
        return json.dumps(payload)


def configure_logging() -> logging.Logger:
    logger = logging.getLogger("rap_doclayout")
    if logger.handlers:
        # avoid double handlers on repeated calls (app reload, test imports)
        return logger

    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(_JsonFormatter())
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)
    return logger


@contextmanager
def log_request(logger: logging.Logger, endpoint: str, **fields):
    """Logs one line per request with latency, success or failure either
    way.

    Usage:
        with log_request(logger, "/detect") as ctx:
            ...
            ctx["detection_count"] = len(detections)
    """
    request_id = uuid.uuid4().hex[:12]
    started = time.perf_counter()
    ctx = {"request_id": request_id, "endpoint": endpoint, **fields}
    error: Exception | None = None
    try:
        yield ctx
    except Exception as exc:
        error = exc
        raise
    finally:
        ctx["latency_ms"] = round((time.perf_counter() - started) * 1000, 2)
        if error is not None:
            ctx["error"] = str(error)
            logger.error("request failed", extra={"extra_fields": ctx})
        else:
            logger.info("request completed", extra={"extra_fields": ctx})