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.
91 lines
2.6 KiB
Python
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})
|