Files
teaching-feedback-assistant/asr-service/logging_config.py
shay7sev a875fe9f63 feat(observability): add structured application logging
Emit correlated JSON logs from the API and FunASR services, propagate request IDs to the client, and configure bounded opt-in Docker logging.

Document the host-wide Grafana/Loki query workflow and keep observability deployment ownership outside the application repository.
2026-07-22 13:52:40 +08:00

91 lines
2.6 KiB
Python

from __future__ import annotations
import json
import logging
import os
import sys
from datetime import datetime, timezone
from typing import Any
LOG_FIELDS = (
"event",
"request_id",
"segment_id",
"feedback_session_id",
"status",
"latency_ms",
"audio_size_bytes",
"audio_duration_ms",
"audio_format",
"error_class",
)
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload: dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
"level": record.levelname,
"service": "funasr",
"environment": os.getenv("APP_ENV", "development"),
"app_version": os.getenv("APP_VERSION", "unknown"),
"git_sha": os.getenv("GIT_SHA", "unknown"),
"message": record.getMessage(),
}
for field in LOG_FIELDS:
value = getattr(record, field, None)
if value is not None:
payload[field] = value
return json.dumps(payload, ensure_ascii=False, default=str)
class JsonLogStream:
encoding = "utf-8"
def __init__(self, level: int) -> None:
self.level = level
self.buffer = ""
def write(self, message: str) -> int:
self.buffer += message
while "\n" in self.buffer:
line, self.buffer = self.buffer.split("\n", 1)
self._emit(line)
return len(message)
def flush(self) -> None:
if self.buffer:
self._emit(self.buffer)
self.buffer = ""
def isatty(self) -> bool:
return False
def _emit(self, message: str) -> None:
message = message.strip()
if message:
logging.getLogger("funasr-service").log(self.level, message)
def configure_logging() -> None:
handler = logging.StreamHandler(sys.__stdout__)
handler.setFormatter(JsonFormatter())
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(logging.INFO)
for name, logger in logging.Logger.manager.loggerDict.items():
if isinstance(logger, logging.Logger):
logger.handlers.clear()
logger.propagate = name != "uvicorn.access"
def redirect_standard_streams() -> None:
sys.stdout = JsonLogStream(logging.INFO) # type: ignore[assignment]
sys.stderr = JsonLogStream(logging.ERROR) # type: ignore[assignment]
def log_event(level: int, event: str, message: str, **fields: Any) -> None:
logging.getLogger("funasr-service").log(level, message, extra={"event": event, **fields})