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.
This commit is contained in:
90
asr-service/logging_config.py
Normal file
90
asr-service/logging_config.py
Normal file
@@ -0,0 +1,90 @@
|
||||
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})
|
||||
Reference in New Issue
Block a user